recharts
Version:
React charts
1,156 lines (984 loc) • 34 kB
text/typescript
import _ from 'lodash';
import { getNiceTickValues, getTickValuesFixedDomain } from 'recharts-scale';
import * as d3Scales from 'd3-scale';
import {
stack as shapeStack,
stackOrderNone,
stackOffsetExpand,
stackOffsetNone,
stackOffsetSilhouette,
stackOffsetWiggle,
} from 'd3-shape';
import { ReactElement, ReactNode } from 'react';
import { isNumOrStr, uniqueId, isNumber, getPercentValue, mathSign, findEntryInArray } from './DataUtils';
import { Legend } from '../component/Legend';
import { findAllByType, findChildByType, getDisplayName } from './ReactUtils';
// TODO: Cause of circular dependency. Needs refactor.
// import { RadiusAxisProps, AngleAxisProps } from '../polar/types';
import { LayoutType, PolarLayoutType, AxisType, TickItem, BaseAxisProps, DataKey, filterProps } from './types';
export function getValueByDataKey<T>(obj: T, dataKey: DataKey<any>, defaultValue?: any) {
if (_.isNil(obj) || _.isNil(dataKey)) {
return defaultValue;
}
if (isNumOrStr(dataKey as string)) {
return _.get(obj, dataKey as string, defaultValue);
}
if (_.isFunction(dataKey)) {
return dataKey(obj);
}
return defaultValue;
}
/**
* Get domain of data by key
* @param {Array} data The data displayed in the chart
* @param {String} key The unique key of a group of data
* @param {String} type The type of axis
* @param {Boolean} filterNil Whether or not filter nil values
* @return {Array} Domain of data
*/
export function getDomainOfDataByKey<T>(data: Array<T>, key: string, type: string, filterNil?: boolean) {
const flattenData = _.flatMap(data, entry => getValueByDataKey(entry, key));
if (type === 'number') {
const domain = flattenData.filter(entry => isNumber(entry) || parseFloat(entry));
return domain.length ? [_.min(domain), _.max(domain)] : [Infinity, -Infinity];
}
const validateData = filterNil ? flattenData.filter(entry => !_.isNil(entry)) : flattenData;
// 支持Date类型的x轴
return validateData.map(entry => (isNumOrStr(entry) || entry instanceof Date ? entry : ''));
}
export const calculateActiveTickIndex = (
coordinate: number,
ticks: Array<TickItem> = [],
unsortedTicks: Array<TickItem>,
axis: BaseAxisProps,
) => {
let index = -1;
const len = ticks?.length ?? 0;
if (len > 1) {
if (axis && axis.axisType === 'angleAxis' && Math.abs(Math.abs(axis.range[1] - axis.range[0]) - 360) <= 1e-6) {
const { range } = axis;
// ticks are distributed in a circle
for (let i = 0; i < len; i++) {
const before = i > 0 ? unsortedTicks[i - 1].coordinate : unsortedTicks[len - 1].coordinate;
const cur = unsortedTicks[i].coordinate;
const after = i >= len - 1 ? unsortedTicks[0].coordinate : unsortedTicks[i + 1].coordinate;
let sameDirectionCoord;
if (mathSign(cur - before) !== mathSign(after - cur)) {
const diffInterval = [];
if (mathSign(after - cur) === mathSign(range[1] - range[0])) {
sameDirectionCoord = after;
const curInRange = cur + range[1] - range[0];
diffInterval[0] = Math.min(curInRange, (curInRange + before) / 2);
diffInterval[1] = Math.max(curInRange, (curInRange + before) / 2);
} else {
sameDirectionCoord = before;
const afterInRange = after + range[1] - range[0];
diffInterval[0] = Math.min(cur, (afterInRange + cur) / 2);
diffInterval[1] = Math.max(cur, (afterInRange + cur) / 2);
}
const sameInterval = [
Math.min(cur, (sameDirectionCoord + cur) / 2),
Math.max(cur, (sameDirectionCoord + cur) / 2),
];
if (
(coordinate > sameInterval[0] && coordinate <= sameInterval[1]) ||
(coordinate >= diffInterval[0] && coordinate <= diffInterval[1])
) {
({ index } = unsortedTicks[i]);
break;
}
} else {
const min = Math.min(before, after);
const max = Math.max(before, after);
if (coordinate > (min + cur) / 2 && coordinate <= (max + cur) / 2) {
({ index } = unsortedTicks[i]);
break;
}
}
}
} else {
// ticks are distributed in a single direction
for (let i = 0; i < len; i++) {
if (
(i === 0 && coordinate <= (ticks[i].coordinate + ticks[i + 1].coordinate) / 2) ||
(i > 0 &&
i < len - 1 &&
coordinate > (ticks[i].coordinate + ticks[i - 1].coordinate) / 2 &&
coordinate <= (ticks[i].coordinate + ticks[i + 1].coordinate) / 2) ||
(i === len - 1 && coordinate > (ticks[i].coordinate + ticks[i - 1].coordinate) / 2)
) {
({ index } = ticks[i]);
break;
}
}
}
} else {
index = 0;
}
return index;
};
/**
* Get the main color of each graphic item
* @param {ReactElement} item A graphic item
* @return {String} Color
*/
export const getMainColorOfGraphicItem = (item: ReactElement) => {
const {
type: { displayName },
} = item as any; // TODO: check if displayName is valid.
const { stroke, fill } = item.props;
let result;
switch (displayName) {
case 'Line':
result = stroke;
break;
case 'Area':
case 'Radar':
result = stroke && stroke !== 'none' ? stroke : fill;
break;
default:
result = fill;
break;
}
return result;
};
// TODO: Formated -> Formatted.
interface FormatedGraphicalItem {
props: any;
childIndex: number;
item: any;
}
export const getLegendProps = ({
children,
formatedGraphicalItems,
legendWidth,
legendContent,
}: {
children: any;
formatedGraphicalItems?: Array<FormatedGraphicalItem>;
legendWidth: number;
legendContent?: any;
}) => {
const legendItem = findChildByType(children, Legend.displayName);
if (!legendItem) {
return null;
}
let legendData;
if (legendItem.props && legendItem.props.payload) {
legendData = legendItem.props && legendItem.props.payload;
} else if (legendContent === 'children') {
legendData = (formatedGraphicalItems || []).reduce((result, { item, props }) => {
const data = props.sectors || props.data || [];
return result.concat(
data.map((entry: any) => ({
type: legendItem.props.iconType || item.props.legendType,
value: entry.name,
color: entry.fill,
payload: entry,
})),
);
}, []);
} else {
legendData = (formatedGraphicalItems || []).map(({ item }) => {
const { dataKey, name, legendType, hide } = item.props;
return {
inactive: hide,
dataKey,
type: legendItem.props.iconType || legendType || 'square',
color: getMainColorOfGraphicItem(item),
value: name || dataKey,
payload: item.props,
};
});
}
return {
...legendItem.props,
...Legend.getWithHeight(legendItem, legendWidth),
payload: legendData,
item: legendItem,
};
};
/**
* Calculate the size of all groups for stacked bar graph
* @param {Object} stackGroups The items grouped by axisId and stackId
* @return {Object} The size of all groups
*/
export const getBarSizeList = ({
barSize: globalSize,
stackGroups = {},
}: {
barSize: number | string;
stackGroups: any;
}) => {
if (!stackGroups) {
return {};
}
const result: Record<string, any> = {};
const numericAxisIds = Object.keys(stackGroups);
for (let i = 0, len = numericAxisIds.length; i < len; i++) {
const sgs = stackGroups[numericAxisIds[i]].stackGroups;
const stackIds = Object.keys(sgs);
for (let j = 0, sLen = stackIds.length; j < sLen; j++) {
const { items, cateAxisId } = sgs[stackIds[j]];
const barItems = items.filter((item: any) => getDisplayName(item.type).indexOf('Bar') >= 0);
if (barItems && barItems.length) {
const { barSize: selfSize } = barItems[0].props;
const cateId = barItems[0].props[cateAxisId];
if (!result[cateId]) {
result[cateId] = [];
}
result[cateId].push({
item: barItems[0],
stackList: barItems.slice(1),
barSize: _.isNil(selfSize) ? globalSize : selfSize,
});
}
}
}
return result;
};
/**
* Calculate the size of each bar and the gap between two bars
* @param {Number} bandSize The size of each category
* @param {sizeList} sizeList The size of all groups
* @param {maxBarSize} maxBarSize The maximum size of bar
* @return {Number} The size of each bar and the gap between two bars
*/
export const getBarPosition = ({
barGap,
barCategoryGap,
bandSize,
sizeList = [],
maxBarSize,
}: {
barGap: string | number;
barCategoryGap: string | number;
bandSize: number;
sizeList: Array<any>;
maxBarSize: number;
}) => {
const len = sizeList.length;
if (len < 1) return null;
let realBarGap = getPercentValue(barGap, bandSize, 0, true);
let result;
// whether or not is barSize setted by user
if (sizeList[0].barSize === +sizeList[0].barSize) {
let useFull = false;
let fullBarSize = bandSize / len;
let sum = sizeList.reduce((res, entry) => res + entry.barSize || 0, 0);
sum += (len - 1) * realBarGap;
if (sum >= bandSize) {
sum -= (len - 1) * realBarGap;
realBarGap = 0;
}
if (sum >= bandSize && fullBarSize > 0) {
useFull = true;
fullBarSize *= 0.9;
sum = len * fullBarSize;
}
const offset = ((bandSize - sum) / 2) >> 0;
let prev = { offset: offset - realBarGap, size: 0 };
result = sizeList.reduce((res, entry) => {
const newRes = [
...res,
{
item: entry.item,
position: {
offset: prev.offset + prev.size + realBarGap,
size: useFull ? fullBarSize : entry.barSize,
},
},
];
prev = newRes[newRes.length - 1].position;
if (entry.stackList && entry.stackList.length) {
entry.stackList.forEach((item: any) => {
newRes.push({ item, position: prev });
});
}
return newRes;
}, []);
} else {
const offset = getPercentValue(barCategoryGap, bandSize, 0, true);
if (bandSize - 2 * offset - (len - 1) * realBarGap <= 0) {
realBarGap = 0;
}
let originalSize = (bandSize - 2 * offset - (len - 1) * realBarGap) / len;
if (originalSize > 1) {
originalSize >>= 0;
}
const size = maxBarSize === +maxBarSize ? Math.min(originalSize, maxBarSize) : originalSize;
result = sizeList.reduce((res, entry, i) => {
const newRes = [
...res,
{
item: entry.item,
position: {
offset: offset + (originalSize + realBarGap) * i + (originalSize - size) / 2,
size,
},
},
];
if (entry.stackList && entry.stackList.length) {
entry.stackList.forEach((item: any) => {
newRes.push({ item, position: newRes[newRes.length - 1].position });
});
}
return newRes;
}, []);
}
return result;
};
export const appendOffsetOfLegend = (offset: any, items: Array<FormatedGraphicalItem>, props: any, legendBox: any) => {
const { children, width, margin } = props;
const legendWidth = width - (margin.left || 0) - (margin.right || 0);
// const legendHeight = height - (margin.top || 0) - (margin.bottom || 0);
const legendProps = getLegendProps({ children, legendWidth });
let newOffset = offset;
if (legendProps) {
const box = legendBox || {};
const { align, verticalAlign, layout } = legendProps;
if ((layout === 'vertical' || (layout === 'horizontal' && verticalAlign === 'center')) && isNumber(offset[align])) {
newOffset = { ...offset, [align]: newOffset[align] + (box.width || 0) };
}
if ((layout === 'horizontal' || (layout === 'vertical' && align === 'center')) && isNumber(offset[verticalAlign])) {
newOffset = { ...offset, [verticalAlign]: newOffset[verticalAlign] + (box.height || 0) };
}
}
return newOffset;
};
export const getDomainOfErrorBars = (data: any[], item: any, dataKey: any, axisType?: AxisType) => {
const { children } = item.props;
const errorBars = findAllByType(children, 'ErrorBar').filter((errorBarChild: any) => {
const { direction } = errorBarChild.props;
return _.isNil(direction) || _.isNil(axisType) ? true : axisType.indexOf(direction) >= 0;
});
if (errorBars && errorBars.length) {
const keys = errorBars.map((errorBarChild: any) => errorBarChild.props.dataKey);
return data.reduce(
(result, entry) => {
const entryValue = getValueByDataKey(entry, dataKey, 0);
const mainValue = _.isArray(entryValue) ? [_.min(entryValue), _.max(entryValue)] : [entryValue, entryValue];
const errorDomain = keys.reduce(
(prevErrorArr: [number, number], k: any) => {
const errorValue = getValueByDataKey(entry, k, 0);
const lowerValue = mainValue[0] - Math.abs(_.isArray(errorValue) ? errorValue[0] : errorValue);
const upperValue = mainValue[1] + Math.abs(_.isArray(errorValue) ? errorValue[1] : errorValue);
return [Math.min(lowerValue, prevErrorArr[0]), Math.max(upperValue, prevErrorArr[1])];
},
[Infinity, -Infinity],
);
return [Math.min(errorDomain[0], result[0]), Math.max(errorDomain[1], result[1])];
},
[Infinity, -Infinity],
);
}
return null;
};
export const parseErrorBarsOfAxis = (data: any[], items: any[], dataKey: any, axisType: AxisType) => {
const domains = items
.map(item => getDomainOfErrorBars(data, item, dataKey, axisType))
.filter(entry => !_.isNil(entry));
if (domains && domains.length) {
return domains.reduce((result, entry) => [Math.min(result[0], entry[0]), Math.max(result[1], entry[1])], [
Infinity,
-Infinity,
]);
}
return null;
};
/**
* Get domain of data by the configuration of item element
* @param {Array} data The data displayed in the chart
* @param {Array} items The instances of item
* @param {String} type The type of axis, number - Number Axis, category - Category Axis
* @param {Boolean} filterNil Whether or not filter nil values
* @return {Array} Domain
*/
export const getDomainOfItemsWithSameAxis = (data: any[], items: any[], type: string, filterNil?: boolean) => {
const domains = items.map(item => {
const { dataKey } = item.props;
if (type === 'number' && dataKey) {
return getDomainOfErrorBars(data, item, dataKey) || getDomainOfDataByKey(data, dataKey, type, filterNil);
}
return getDomainOfDataByKey(data, dataKey, type, filterNil);
});
if (type === 'number') {
// Calculate the domain of number axis
return domains.reduce((result, entry) => [Math.min(result[0], entry[0]), Math.max(result[1], entry[1])], [
Infinity,
-Infinity,
]);
}
const tag: Record<string, any> = {};
// Get the union set of category axis
return domains.reduce((result, entry) => {
for (let i = 0, len = entry.length; i < len; i++) {
if (!tag[entry[i]]) {
tag[entry[i]] = true;
result.push(entry[i]);
}
}
return result;
}, []);
};
export const isCategoricalAxis = (layout: LayoutType | PolarLayoutType, axisType: AxisType) =>
(layout === 'horizontal' && axisType === 'xAxis') ||
(layout === 'vertical' && axisType === 'yAxis') ||
(layout === 'centric' && axisType === 'angleAxis') ||
(layout === 'radial' && axisType === 'radiusAxis');
/**
* Calculate the Coordinates of grid
* @param {Array} ticks The ticks in axis
* @param {Number} min The minimun value of axis
* @param {Number} max The maximun value of axis
* @return {Array} Coordinates
*/
export const getCoordinatesOfGrid = (ticks: Array<TickItem>, min: number, max: number) => {
let hasMin, hasMax;
const values = ticks.map(entry => {
if (entry.coordinate === min) {
hasMin = true;
}
if (entry.coordinate === max) {
hasMax = true;
}
return entry.coordinate;
});
if (!hasMin) {
values.push(min);
}
if (!hasMax) {
values.push(max);
}
return values;
};
/**
* Get the ticks of an axis
* @param {Object} axis The configuration of an axis
* @param {Boolean} isGrid Whether or not are the ticks in grid
* @param {Boolean} isAll Return the ticks of all the points or not
* @return {Array} Ticks
*/
export const getTicksOfAxis = (axis: any, isGrid?: boolean, isAll?: boolean): TickItem[] => {
if (!axis) return null;
const { scale } = axis;
const { duplicateDomain, type, range } = axis;
let offset = (isGrid || isAll) && type === 'category' && scale.bandwidth ? scale.bandwidth() / 2 : 0;
offset = axis.axisType === 'angleAxis' ? mathSign(range[0] - range[1]) * 2 * offset : offset;
// The ticks setted by user should only affect the ticks adjacent to axis line
if (isGrid && (axis.ticks || axis.niceTicks)) {
return (axis.ticks || axis.niceTicks).map((entry: TickItem) => {
const scaleContent = duplicateDomain ? duplicateDomain.indexOf(entry) : entry;
return {
coordinate: scale(scaleContent) + offset,
value: entry,
offset,
};
});
}
// When axis is a categorial axis, but the type of axis is number or the scale of axis is not "auto"
if (axis.isCategorical && axis.categoricalDomain) {
return axis.categoricalDomain.map((entry: any, index: number) => ({
coordinate: scale(entry) + offset,
value: entry,
index,
offset,
}));
}
if (scale.ticks && !isAll) {
return scale
.ticks(axis.tickCount)
.map((entry: any) => ({ coordinate: scale(entry) + offset, value: entry, offset }));
}
// When axis has duplicated text, serial numbers are used to generate scale
return scale.domain().map((entry: any, index: number) => ({
coordinate: scale(entry) + offset,
value: duplicateDomain ? duplicateDomain[entry] : entry,
index,
offset,
}));
};
/**
* combine the handlers
* @param {Function} defaultHandler Internal private handler
* @param {Function} parentHandler Handler function specified in parent component
* @param {Function} childHandler Handler function specified in child component
* @return {Function} The combined handler
*/
export const combineEventHandlers = (defaultHandler: Function, parentHandler: Function, childHandler: Function) => {
let customizedHandler: Function;
if (_.isFunction(childHandler)) {
customizedHandler = childHandler;
} else if (_.isFunction(parentHandler)) {
customizedHandler = parentHandler;
}
if (_.isFunction(defaultHandler) || customizedHandler) {
return (arg1: any, arg2: any, arg3: any, arg4: any) => {
if (_.isFunction(defaultHandler)) {
defaultHandler(arg1, arg2, arg3, arg4);
}
if (_.isFunction(customizedHandler)) {
customizedHandler(arg1, arg2, arg3, arg4);
}
};
}
return null;
};
/**
* Parse the scale function of axis
* @param {Object} axis The option of axis
* @param {String} chartType The displayName of chart
* @param {Boolean} hasBar if it has a bar
* @return {Function} The scale function
*/
export const parseScale = (axis: any, chartType: string, hasBar?: boolean) => {
const { scale, type, layout, axisType } = axis;
if (scale === 'auto') {
if (layout === 'radial' && axisType === 'radiusAxis') {
return { scale: d3Scales.scaleBand(), realScaleType: 'band' };
}
if (layout === 'radial' && axisType === 'angleAxis') {
return { scale: d3Scales.scaleLinear(), realScaleType: 'linear' };
}
if (
type === 'category' &&
chartType &&
(chartType.indexOf('LineChart') >= 0 ||
chartType.indexOf('AreaChart') >= 0 ||
(chartType.indexOf('ComposedChart') >= 0 && !hasBar))
) {
return { scale: d3Scales.scalePoint(), realScaleType: 'point' };
}
if (type === 'category') {
return { scale: d3Scales.scaleBand(), realScaleType: 'band' };
}
return { scale: d3Scales.scaleLinear(), realScaleType: 'linear' };
}
if (_.isString(scale)) {
const name = `scale${_.upperFirst(scale)}`;
return {
scale: ((d3Scales as Record<string, any>)[name] || d3Scales.scalePoint)(),
realScaleType: (d3Scales as Record<string, any>)[name] ? name : 'point',
};
}
return _.isFunction(scale) ? { scale } : { scale: d3Scales.scalePoint(), realScaleType: 'point' };
};
const EPS = 1e-4;
export const checkDomainOfScale = (scale: any) => {
const domain = scale.domain();
if (!domain || domain.length <= 2) {
return;
}
const len = domain.length;
const range = scale.range();
const min = Math.min(range[0], range[1]) - EPS;
const max = Math.max(range[0], range[1]) + EPS;
const first = scale(domain[0]);
const last = scale(domain[len - 1]);
if (first < min || first > max || last < min || last > max) {
scale.domain([domain[0], domain[len - 1]]);
}
};
export const findPositionOfBar = (barPosition: any[], child: ReactNode) => {
if (!barPosition) {
return null;
}
for (let i = 0, len = barPosition.length; i < len; i++) {
if (barPosition[i].item === child) {
return barPosition[i].position;
}
}
return null;
};
export const truncateByDomain = (value: any[], domain: any[]) => {
if (!domain || domain.length !== 2 || !isNumber(domain[0]) || !isNumber(domain[1])) {
return value;
}
const min = Math.min(domain[0], domain[1]);
const max = Math.max(domain[0], domain[1]);
const result = [value[0], value[1]];
if (!isNumber(value[0]) || value[0] < min) {
result[0] = min;
}
if (!isNumber(value[1]) || value[1] > max) {
result[1] = max;
}
if (result[0] > max) {
result[0] = max;
}
if (result[1] < min) {
result[1] = min;
}
return result;
};
/* eslint no-param-reassign: 0 */
export const offsetSign = (series: any) => {
const n = series.length;
if (n <= 0) {
return;
}
for (let j = 0, m = series[0].length; j < m; ++j) {
let positive = 0;
let negative = 0;
for (let i = 0; i < n; ++i) {
const value = _.isNaN(series[i][j][1]) ? series[i][j][0] : series[i][j][1];
/* eslint-disable prefer-destructuring */
if (value >= 0) {
series[i][j][0] = positive;
series[i][j][1] = positive + value;
positive = series[i][j][1];
} else {
series[i][j][0] = negative;
series[i][j][1] = negative + value;
negative = series[i][j][1];
}
/* eslint-enable prefer-destructuring */
}
}
};
/* eslint no-param-reassign: 0 */
export const offsetPositive = (series: any) => {
const n = series.length;
if (n <= 0) {
return;
}
for (let j = 0, m = series[0].length; j < m; ++j) {
let positive = 0;
for (let i = 0; i < n; ++i) {
const value = _.isNaN(series[i][j][1]) ? series[i][j][0] : series[i][j][1];
/* eslint-disable prefer-destructuring */
if (value >= 0) {
series[i][j][0] = positive;
series[i][j][1] = positive + value;
positive = series[i][j][1];
} else {
series[i][j][0] = 0;
series[i][j][1] = 0;
}
/* eslint-enable prefer-destructuring */
}
}
};
const STACK_OFFSET_MAP: Record<string, any> = {
sign: offsetSign,
expand: stackOffsetExpand,
none: stackOffsetNone,
silhouette: stackOffsetSilhouette,
wiggle: stackOffsetWiggle,
positive: offsetPositive,
};
export const getStackedData = (data: any, stackItems: any, offsetType: string) => {
const dataKeys = stackItems.map((item: any) => item.props.dataKey);
const stack = shapeStack()
.keys(dataKeys)
.value((d, key) => +getValueByDataKey(d, key, 0))
.order(stackOrderNone)
.offset(STACK_OFFSET_MAP[offsetType]);
return stack(data);
};
export const getStackGroupsByAxisId = (
data: any,
_items: Array<any>,
numericAxisId: string,
cateAxisId: string,
offsetType: any,
reverseStackOrder: boolean,
) => {
if (!data) {
return null;
}
// reversing items to affect render order (for layering)
const items = reverseStackOrder ? _items.reverse() : _items;
const stackGroups = items.reduce((result, item) => {
const { stackId, hide } = item.props;
if (hide) {
return result;
}
const axisId = item.props[numericAxisId];
const parentGroup = result[axisId] || { hasStack: false, stackGroups: {} };
if (isNumOrStr(stackId)) {
const childGroup = parentGroup.stackGroups[stackId] || {
numericAxisId,
cateAxisId,
items: [],
};
childGroup.items.push(item);
parentGroup.hasStack = true;
parentGroup.stackGroups[stackId] = childGroup;
} else {
parentGroup.stackGroups[uniqueId('_stackId_')] = {
numericAxisId,
cateAxisId,
items: [item],
};
}
return { ...result, [axisId]: parentGroup };
}, {});
return Object.keys(stackGroups).reduce((result, axisId) => {
const group = stackGroups[axisId];
if (group.hasStack) {
group.stackGroups = Object.keys(group.stackGroups).reduce((res, stackId) => {
const g = group.stackGroups[stackId];
return {
...res,
[stackId]: {
numericAxisId,
cateAxisId,
items: g.items,
stackedData: getStackedData(data, g.items, offsetType),
},
};
}, {});
}
return { ...result, [axisId]: group };
}, {});
};
/**
* get domain of ticks
* @param {Array} ticks Ticks of axis
* @param {String} type The type of axis
* @return {Array} domain
*/
export const calculateDomainOfTicks = (ticks: Array<TickItem>, type: string) => {
if (type === 'number') {
return [_.min(ticks), _.max(ticks)];
}
return ticks;
};
/**
* Configure the scale function of axis
* @param {Object} scale The scale function
* @param {Object} opts The configuration of axis
* @return {Object} null
*/
export const getTicksOfScale = (scale: any, opts: any) => {
const { realScaleType, type, tickCount, originalDomain, allowDecimals } = opts;
const scaleType = realScaleType || opts.scale;
if (scaleType !== 'auto' && scaleType !== 'linear') {
return null;
}
if (
tickCount &&
type === 'number' &&
originalDomain &&
(originalDomain[0] === 'auto' || originalDomain[1] === 'auto')
) {
// Calculate the ticks by the number of grid when the axis is a number axis
const domain = scale.domain();
if (!domain.length) {
return null;
}
const tickValues = getNiceTickValues(domain, tickCount, allowDecimals);
scale.domain(calculateDomainOfTicks(tickValues, type));
return { niceTicks: tickValues };
}
if (tickCount && type === 'number') {
const domain = scale.domain();
const tickValues = getTickValuesFixedDomain(domain, tickCount, allowDecimals);
return { niceTicks: tickValues };
}
return null;
};
export const getCateCoordinateOfLine = ({
axis,
ticks,
bandSize,
entry,
index,
dataKey,
}: {
axis: any;
ticks: Array<TickItem>;
bandSize: number;
entry: any;
index: number;
dataKey?: string | number | ((obj: any) => any);
}) => {
if (axis.type === 'category') {
// find coordinate of category axis by the value of category
if (!axis.allowDuplicatedCategory && axis.dataKey && !_.isNil(entry[axis.dataKey])) {
const matchedTick = findEntryInArray(ticks, 'value', entry[axis.dataKey]);
if (matchedTick) {
return matchedTick.coordinate + bandSize / 2;
}
}
return ticks[index] ? ticks[index].coordinate + bandSize / 2 : null;
}
const value = getValueByDataKey(entry, !_.isNil(dataKey) ? dataKey : axis.dataKey);
return !_.isNil(value) ? axis.scale(value) : null;
};
export const getCateCoordinateOfBar = ({
axis,
ticks,
offset,
bandSize,
entry,
index,
}: {
axis: any; // RadiusAxisProps & { dataKey?: any }; // TODO: should dataKey be included in RadiusAxisProps?
ticks: Array<TickItem>;
offset: any;
bandSize: number;
entry: any;
index: number;
}) => {
if (axis.type === 'category') {
return ticks[index] ? ticks[index].coordinate + offset : null;
}
const value = getValueByDataKey(entry, axis.dataKey, axis.domain[index]);
return !_.isNil(value) ? axis.scale(value) - bandSize / 2 + offset : null;
};
export const getBaseValueOfBar = ({
numericAxis,
}: {
numericAxis: any; // AngleAxisProps | RadiusAxisProps
}) => {
const domain = numericAxis.scale.domain();
if (numericAxis.type === 'number') {
const min = Math.min(domain[0], domain[1]);
const max = Math.max(domain[0], domain[1]);
if (min <= 0 && max >= 0) {
return 0;
}
if (max < 0) {
return max;
}
return min;
}
return domain[0];
};
export const getStackedDataOfItem = (item: any, stackGroups: any) => {
const { stackId } = item.props;
if (isNumOrStr(stackId)) {
const group = stackGroups[stackId];
if (group && group.items.length) {
let itemIndex = -1;
for (let i = 0, len = group.items.length; i < len; i++) {
if (group.items[i] === item) {
itemIndex = i;
break;
}
}
return itemIndex >= 0 ? group.stackedData[itemIndex] : null;
}
}
return null;
};
const getDomainOfSingle = (data: Array<any>) =>
data.reduce(
(result, entry) => [
_.min(entry.concat([result[0]]).filter(isNumber)),
_.max(entry.concat([result[1]]).filter(isNumber)),
],
[Infinity, -Infinity],
);
export const getDomainOfStackGroups = (stackGroups: any, startIndex: number, endIndex: number) =>
Object.keys(stackGroups)
.reduce(
(result, stackId) => {
const group = stackGroups[stackId];
const { stackedData } = group;
const domain = stackedData.reduce(
(res: [number, number], entry: any) => {
const s = getDomainOfSingle(entry.slice(startIndex, endIndex + 1));
return [Math.min(res[0], s[0]), Math.max(res[1], s[1])];
},
[Infinity, -Infinity],
);
return [Math.min(domain[0], result[0]), Math.max(domain[1], result[1])];
},
[Infinity, -Infinity],
)
.map(result => (result === Infinity || result === -Infinity ? 0 : result));
export const MIN_VALUE_REG = /^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/;
export const MAX_VALUE_REG = /^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/;
export const parseSpecifiedDomain = (specifiedDomain: any, dataDomain: any, allowDataOverflow: boolean) => {
if (!_.isArray(specifiedDomain)) {
return dataDomain;
}
const domain = [];
/* eslint-disable prefer-destructuring */
if (isNumber(specifiedDomain[0])) {
domain[0] = allowDataOverflow ? specifiedDomain[0] : Math.min(specifiedDomain[0], dataDomain[0]);
} else if (MIN_VALUE_REG.test(specifiedDomain[0])) {
const value = +MIN_VALUE_REG.exec(specifiedDomain[0])[1];
domain[0] = dataDomain[0] - value;
} else if (_.isFunction(specifiedDomain[0])) {
domain[0] = specifiedDomain[0](dataDomain[0]);
} else {
domain[0] = dataDomain[0];
}
if (isNumber(specifiedDomain[1])) {
domain[1] = allowDataOverflow ? specifiedDomain[1] : Math.max(specifiedDomain[1], dataDomain[1]);
} else if (MAX_VALUE_REG.test(specifiedDomain[1])) {
const value = +MAX_VALUE_REG.exec(specifiedDomain[1])[1];
domain[1] = dataDomain[1] + value;
} else if (_.isFunction(specifiedDomain[1])) {
domain[1] = specifiedDomain[1](dataDomain[1]);
} else {
domain[1] = dataDomain[1];
}
/* eslint-enable prefer-destructuring */
return domain;
};
/**
* Calculate the size between two category
* @param {Object} axis The options of axis
* @param {Array} ticks The ticks of axis
* @param {Boolean} isBar if items in axis are bars
* @return {Number} Size
*/
export const getBandSizeOfAxis = (axis: any, ticks?: Array<TickItem>, isBar?: boolean) => {
if (axis && axis.scale && axis.scale.bandwidth) {
const bandWidth = axis.scale.bandwidth();
if (!isBar || bandWidth > 0) {
return bandWidth;
}
}
if (axis && ticks && ticks.length >= 2) {
const orderedTicks = _.sortBy(ticks, o => o.coordinate);
let bandSize = Infinity;
for (let i = 1, len = orderedTicks.length; i < len; i++) {
const cur = orderedTicks[i];
const prev = orderedTicks[i - 1];
bandSize = Math.min((cur.coordinate || 0) - (prev.coordinate || 0), bandSize);
}
return bandSize === Infinity ? 0 : bandSize;
}
return isBar ? undefined : 0;
};
/**
* parse the domain of a category axis when a domain is specified
* @param {Array} specifiedDomain The domain specified by users
* @param {Array} calculatedDomain The domain calculated by dateKey
* @param {ReactElement} axisChild The axis element
* @returns {Array} domains
*/
export const parseDomainOfCategoryAxis = (
specifiedDomain: Array<any>,
calculatedDomain: Array<any>,
axisChild: ReactElement,
) => {
if (!specifiedDomain || !specifiedDomain.length) {
return calculatedDomain;
}
if (_.isEqual(specifiedDomain, _.get(axisChild, 'type.defaultProps.domain'))) {
return calculatedDomain;
}
return specifiedDomain;
};
export const getTooltipItem = (graphicalItem: any, payload: any) => {
const { dataKey, name, unit, formatter, tooltipType, chartType } = graphicalItem.props;
return {
...filterProps(graphicalItem),
dataKey,
unit,
formatter,
name: name || dataKey,
color: getMainColorOfGraphicItem(graphicalItem),
value: getValueByDataKey(payload, dataKey),
type: tooltipType,
payload,
chartType,
};
};