recharts
Version:
React charts
1,020 lines (886 loc) • 31.6 kB
JavaScript
/**
* @fileOverview Cartesian Chart
*/
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { getNiceTickValues } from 'recharts-scale';
import D3Scale from 'd3-scale';
import D3Shape from 'd3-shape';
import invariant from 'invariant';
import OuiDomUtils from 'oui-dom-utils';
import Layer from '../container/Layer';
import Tooltip from '../component/Tooltip';
import Legend from '../component/Legend';
import ReactUtils from '../util/ReactUtils';
import DOMUtils from '../util/DOMUtils';
import LodashUtils from '../util/LodashUtils';
import CartesianAxis from '../cartesian/CartesianAxis';
import CartesianGrid from '../cartesian/CartesianGrid';
import ReferenceLine from '../cartesian/ReferenceLine';
import XAxis from '../cartesian/XAxis';
import YAxis from '../cartesian/YAxis';
import Brush from '../cartesian/Brush';
const ORIENT_MAP = {
xAxis: ['bottom', 'top'],
yAxis: ['left', 'right'],
};
/**
* The base class of chart in cartesian coordinate system
*/
class CartesianChart extends Component {
static propTypes = {
width: PropTypes.number,
height: PropTypes.number,
data: PropTypes.arrayOf(PropTypes.object),
layout: PropTypes.oneOf(['horizontal', 'vertical']),
margin: PropTypes.shape({
top: PropTypes.number,
right: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
}),
className: PropTypes.string,
stackType: PropTypes.oneOf(['value', 'percent']),
title: PropTypes.string,
style: PropTypes.object,
barCategoryGap: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
barGap: PropTypes.number,
barSize: PropTypes.number,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
static defaultProps = {
style: {},
barCategoryGap: '10%',
barGap: 4,
layout: 'horizontal',
margin: { top: 5, right: 5, bottom: 5, left: 5 },
};
constructor(props) {
super(props);
this.validateAxes();
}
state = {
dataStartIndex: 0,
dataEndIndex: this.props.data.length - 1,
activeTooltipIndex: -1,
activeTooltipLabel: '',
activeTooltipCoord: { x: 0, y: 0 },
isTooltipActive: false,
activeLineKey: null,
activeBarKey: null,
};
getStackGroupsByAxisId(items, axisIdKey) {
const stackGroups = items.reduce((result, item) => {
const { stackId, dataKey } = item.props;
const axisId = item.props[axisIdKey];
if (!result[axisId]) {
result[axisId] = { hasStack: false, stackGroups: {} };
}
if (LodashUtils.isNumber(stackId) || LodashUtils.isString(stackId)) {
if (!result[axisId].stackGroups[stackId]) {
result[axisId].stackGroups[stackId] = {
items: [],
};
}
result[axisId].stackGroups[stackId].items.push(item);
if (result[axisId].stackGroups[stackId].items.length >= 2) {
result[axisId].hasStack = true;
}
} else {
result[axisId].stackGroups[LodashUtils.getUniqueId('_stackId_')] = {
items: [item],
};
}
return result;
}, {});
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];
res[stackId] = {
items: g.items,
stackedData: this.getStackedData(g.items),
};
return res;
}, {});
}
result[axisId] = group;
return result;
}, {});
}
getStackedData(stackItems) {
const { data } = this.props;
const dataKeys = stackItems.map(item => item.props.dataKey);
const stack = D3Shape.stack()
.keys(dataKeys)
.value((d, key) => (+d[key] || 0))
.order(D3Shape.stackOrderNone)
.offset(D3Shape.stackOffsetNone);
return stack(data);
}
getStackedDataOfItem(item, stackGroups) {
const { stackId } = item.props;
if (LodashUtils.isNumber(stackId) || LodashUtils.isString(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;
}
/**
* Calculate coordinate of cursor in chart
* @param {Object} e Event object
* @param {Object} containerOffset The offset of main part in the svg element
* @return {Object} {chartX, chartY}
*/
getChartPosition(e, containerOffset) {
return {
chartX: Math.round(e.pageX - containerOffset.left),
chartY: Math.round(e.pageY - containerOffset.top),
};
}
/**
* get domain of ticks
* @param {Array} ticks Ticks of axis
* @param {String} type The type of axis
* @return {Array} domain
*/
getDomainOfTicks(ticks, type) {
if (type === 'number') {
return [Math.min.apply(null, ticks), Math.max.apply(null, ticks)];
}
return ticks;
}
/**
* Get domain of data by key
* @param {String} key The unique key of a group of data
* @param {String} type The type of axis
* @return {Array} Domain of data
*/
getDomainByKey(key, type) {
const { data } = this.props;
const { dataStartIndex, dataEndIndex } = this.state;
const domain = data.slice(dataStartIndex, dataEndIndex + 1)
.map(entry => entry[key] || 0);
return type === 'number' ? [
Math.min.apply(null, domain),
Math.max.apply(null, domain),
] : domain;
}
getDomainOfStackGroups(stackGroups) {
const { dataStartIndex, dataEndIndex } = this.state;
return Object.keys(stackGroups).reduce((result, stackId) => {
const group = stackGroups[stackId];
const { stackedData } = group;
const minList = stackedData[0].slice(dataStartIndex, dataEndIndex + 1);
const maxList = stackedData[stackedData.length - 1].slice(dataStartIndex, dataEndIndex + 1);
const min = minList.reduce((res, entry) => {
return Math.min(res, entry[0]);
}, Infinity);
const max = maxList.reduce((res, entry) => {
return Math.max(res, entry[1]);
}, -Infinity);
return [Math.min(min, result[0]), Math.max(max, result[1])];
}, [Infinity, -Infinity]);
}
/**
* Get domain of data by the configuration of item element
* @param {Array} items The instances of item
* @param {String} type The type of axis, number - Number Axis, category - Category Axis
* @return {Array} Domain
*/
getDomainOfItemsWithSameAxis(items, type) {
const domains = items.map(item => {
return this.getDomainByKey(item.props.dataKey, type);
});
if (type === 'number') {
// Calculate the domain of number axis
return domains.reduce((result, entry) => {
return [
Math.min(result[0], entry[0]),
Math.max(result[1], entry[1]),
];
}, [Infinity, -Infinity]);
}
const tag = {};
// 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;
}, []);
}
/**
* Get the configuration of all x-axis or y-axis
* @param {String} axisType The type of axis
* @param {Array} items The instances of item
* @param {Object} stackGroups The items grouped by axisId and stackId
* @return {Object} Configuration
*/
getAxisMap(axisType = 'xAxis', items, stackGroups) {
const { children } = this.props;
const Axis = axisType === 'xAxis' ? XAxis : YAxis;
const axisIdKey = axisType === 'xAxis' ? 'xAxisId' : 'yAxisId';
// Get all the instance of Axis
const axes = ReactUtils.findAllByType(children, Axis);
let axisMap = {};
if (axes && axes.length) {
axisMap = this.getAxisMapByAxes(axes, items, axisType, axisIdKey, stackGroups);
} else if (items && items.length) {
axisMap = this.getAxisMapByItems(items, Axis, axisType, axisIdKey, stackGroups);
}
return axisMap;
}
/**
* Get the configuration of axis by the options of axis instance
* @param {Array} axes The instance of axes
* @param {Array} items The instances of item
* @param {String} axisType The type of axis, xAxis - x-axis, yAxis - y-axis
* @param {String} axisIdKey The unique id of an axis
* @param {Object} stackGroups The items grouped by axisId and stackId
* @return {Object} Configuration
*/
getAxisMapByAxes(axes, items, axisType, axisIdKey, stackGroups) {
const { layout } = this.props;
const { dataEndIndex, dataStartIndex } = this.state;
const len = dataEndIndex - dataStartIndex + 1;
const isCategoryAxis = (layout === 'horizontal' && axisType === 'xAxis') ||
(layout === 'vertical' && axisType === 'yAxis');
// Eliminate duplicated axes
const axisMap = axes.reduce((result, child) => {
const { type, dataKey } = child.props;
const axisId = child.props[axisIdKey];
if (!result[axisId]) {
let domain;
let isDomainFixed = false;
if (dataKey) {
domain = this.getDomainByKey(dataKey, type);
} else if (stackGroups && stackGroups[axisId] && stackGroups[axisId].hasStack
&& type === 'number') {
domain = this.getDomainOfStackGroups(stackGroups[axisId].stackGroups);
} else if (isCategoryAxis) {
domain = LodashUtils.range(0, len);
} else {
domain = this.getDomainOfItemsWithSameAxis(items.filter(entry => {
return entry.props[axisIdKey] === axisId;
}), type);
}
if (type === 'number' && child.props.domain) {
isDomainFixed = true;
domain = this.parseSpecifiedDomain(child.props.domain, domain);
}
return { ...result, [axisId]: {
...child.props,
axisType,
domain,
isDomainFixed,
} };
}
return result;
}, {});
return axisMap;
}
/**
* Get the configuration of axis by the options of item, this kind of axis does not display in chart
* @param {Array} items The instances of item
* @param {ReactElement} Axis Axis Component
* @param {String} axisType The type of axis, xAxis - x-axis, yAxis - y-axis
* @param {String} axisIdKey The unique id of an axis
* @param {Object} stackGroups The items grouped by axisId and stackId
* @return {Object} Configuration
*/
getAxisMapByItems(items, Axis, axisType, axisIdKey, stackGroups) {
const { layout } = this.props;
const { dataEndIndex, dataStartIndex } = this.state;
const len = dataEndIndex - dataStartIndex + 1;
let index = -1;
const isCategoryAxis = (layout === 'horizontal' && axisType === 'xAxis') ||
(layout === 'vertical' && axisType === 'yAxis');
// The default type of x-axis is category axis,
// The default contents of x-axis is the serial numbers of data
// The default type of y-axis is number axis
// The default contents of y-axis is the domain of data
const axisMap = items.reduce((result, child) => {
const axisId = child.props[axisIdKey];
if (!result[axisId]) {
index++;
let domain;
if (isCategoryAxis) {
domain = LodashUtils.range(0, len);
} else if (stackGroups && stackGroups[axisId] && stackGroups[axisId].hasStack) {
domain = this.getDomainOfStackGroups(stackGroups[axisId].stackGroups);
} else {
domain = this.getDomainOfItemsWithSameAxis(
items.filter(entry => entry.props[axisIdKey] === axisId), 'number'
);
}
return {
...result,
[axisId]: {
axisType,
type: Axis.defaultProps.type,
hide: true,
isAutoGenerated: true,
width: Axis.defaultProps.width,
height: Axis.defaultProps.height,
tickCount: Axis.defaultProps.tickCount,
orientation: ORIENT_MAP[axisType][index % 2],
domain,
},
};
}
return result;
}, {});
return axisMap;
}
/**
* Calculate the offset of main part in the svg element
* @param {Object} xAxisMap The configuration of x-axis
* @param {Object} yAxisMap The configuration of y-axis
* @return {Object} The offset of main part in the svg element
*/
getOffset(xAxisMap, yAxisMap) {
const { width, height, margin, children } = this.props;
const brushItem = ReactUtils.findChildByType(children, Brush);
const offsetH = Object.keys(yAxisMap).reduce((result, id) => {
const entry = yAxisMap[id];
const orientation = entry.orientation;
result[orientation] += entry.hide ? 0 : entry.width;
return result;
}, { left: margin.left || 0, right: margin.right || 0 });
const offsetV = Object.keys(xAxisMap).reduce((result, id) => {
const entry = xAxisMap[id];
const orientation = entry.orientation;
result[orientation] += entry.hide ? 0 : entry.height;
return result;
}, { top: margin.top || 0, bottom: margin.bottom || 0 });
if (brushItem) {
offsetV.bottom += brushItem.props.height || Brush.defaultProps.height;
}
return {
...offsetH,
...offsetV,
width: width - offsetH.left - offsetH.right,
height: height - offsetV.top - offsetV.bottom,
};
}
/**
* Get the main color of each graphic item
* @param {ReactElement} item A graphic item
* @return {String} Color
*/
getMainColorOfItem(item) {
const displayName = item.type.displayName;
let result;
switch (displayName) {
case 'Line':
result = item.props.stroke;
break;
default:
result = item.props.fill;
break;
}
return result;
}
/**
* Configure the scale function of axis
* @param {Object} scale The scale function
* @param {Object} opts The configuration of axis
* @return {Object} null
*/
setTicksOfScale(scale, opts) {
// Give priority to use the options of ticks
if (opts.ticks && opts.ticks) {
scale.domain(this.getDomainOfTicks(opts.ticks, opts.type));
return;
}
if (opts.type === 'number' && opts.isAutoGenerated) {
scale.domain([0, scale.domain()[1]]);
} else if (opts.tickCount && opts.type === 'number' && !opts.isDomainFixed) {
// Calculate the ticks by the number of grid when the axis is a number axis
const domain = scale.domain();
const tickValues = getNiceTickValues(domain, opts.tickCount);
opts.ticks = tickValues;
scale.domain(this.getDomainOfTicks(tickValues, opts.type));
}
}
/**
* Calculate the scale function, position, width, height of axes
* @param {Object} axisMap The configuration of axes
* @param {Object} offset The offset of main part in the svg element
* @param {Object} axisType The type of axes, x-axis or y-axis
* @return {Object} Configuration
*/
getFormatAxisMap(axisMap, offset, axisType) {
const { width, height, layout } = this.props;
const displayName = this.constructor.displayName;
const ids = Object.keys(axisMap);
const steps = {
left: offset.left,
right: width - offset.right,
top: offset.top,
bottom: height - offset.bottom,
};
return ids.reduce((result, id) => {
const axis = axisMap[id];
const { orientation, type, domain } = axis;
let range;
if (axisType === 'xAxis') {
range = [offset.left, offset.left + offset.width];
} else {
range = layout === 'horizontal' ?
[offset.top + offset.height, offset.top] :
[offset.top, offset.top + offset.height];
}
let scale;
if (type === 'number') {
scale = D3Scale.linear().domain(domain).range(range);
} else if (displayName === 'LineChart' || displayName === 'AreaChart') {
scale = D3Scale.point().domain(domain).range(range);
} else {
scale = D3Scale.band().domain(domain).range(range);
}
this.setTicksOfScale(scale, axis);
let x;
let y;
if (axisType === 'xAxis') {
x = offset.left;
y = orientation === 'top' ? steps[orientation] - axis.height : steps[orientation];
} else {
x = orientation === 'left' ? steps[orientation] - axis.width : steps[orientation];
y = offset.top;
}
result[id] = { ...axis,
x, y, scale,
width: axisType === 'xAxis' ? offset.width : axis.width,
height: axisType === 'yAxis' ? offset.height : axis.height,
};
if (!axis.hide && axisType === 'xAxis') {
steps[orientation] += (orientation === 'top' ? -1 : 1) * result[id].height;
} else if (!axis.hide) {
steps[orientation] += (orientation === 'left' ? -1 : 1) * result[id].width;
}
return result;
}, {});
}
/**
* 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
* @return {Array} Ticks
*/
getAxisTicks(axis, isGrid = false) {
const scale = axis.scale;
const offset = isGrid && axis.type === 'category' ? scale.bandwidth() / 2 : 0;
if (axis.ticks) {
return axis.ticks.map(entry => {
return { coord: scale(entry) + offset, value: entry };
});
}
if (scale.ticks) {
return scale.ticks(axis.tickCount).map(entry => {
return { coord: scale(entry) + offset, value: entry };
});
}
return scale.domain().map((entry) => {
return { coord: scale(entry) + offset, value: entry };
});
}
/**
* Calculate the ticks 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} Ticks
*/
getGridTicks(ticks, min, max) {
let hasMin;
let hasMax;
let values;
values = ticks.map(entry => {
if (entry.coord === min) { hasMin = true;}
if (entry.coord === max) { hasMax = true;}
return entry.coord;
});
if (!hasMin) { values.push(min);}
if (!hasMax) { values.push(max);}
return values;
}
/**
* Get the information of mouse in chart, return null when the mouse is not in the chart
* @param {Object} xAxisMap The configuration of all x-axes
* @param {Object} yAxisMap The configuration of all y-axes
* @param {Object} offset The offset of main part in the svg element
* @param {Object} e The event object
* @return {Object} Mouse data
*/
getMouseInfo(xAxisMap, yAxisMap, offset, e) {
const isIn = e.chartX >= offset.left && e.chartX <= offset.left + offset.width
&& e.chartY >= offset.top && e.chartY <= offset.top + offset.height;
if (!isIn) {return null;}
const { layout } = this.props;
const axisMap = layout === 'horizontal' ? xAxisMap : yAxisMap;
const pos = layout === 'horizontal' ? e.chartX : e.chartY;
const ids = Object.keys(axisMap);
const axis = axisMap[ids[0]];
const ticks = this.getAxisTicks(axis, true);
let index = 0;
const len = ticks.length;
if (len > 1) {
for (let i = 0; i < len; i++) {
if ((i === 0 && pos <= (ticks[i].coord + ticks[i + 1].coord) / 2)
|| (i > 0 && i < len - 1 && pos > (ticks[i].coord + ticks[i - 1].coord) / 2
&& pos <= (ticks[i].coord + ticks[i + 1].coord) / 2)
|| (i === len - 1 && pos > (ticks[i].coord + ticks[i - 1].coord) / 2)) {
index = i;
break;
}
}
}
return {
activeTooltipIndex: index,
activeTooltipLabel: ticks[index].value,
activeTooltipCoord: {
x: layout === 'horizontal' ? ticks[index].coord : e.chartX,
y: layout === 'horizontal' ? e.chartY : ticks[index].coord,
},
};
}
/**
* Get the content to be displayed in the tooltip
* @param {Array} items The instances of item
* @return {Array} The content of tooltip
*/
getTooltipContent(items) {
const { activeLineKey, activeBarKey, activeAreaKey, activeTooltipIndex, dataStartIndex, dataEndIndex } = this.state;
const data = this.props.data.slice(dataStartIndex, dataEndIndex + 1);
if (activeTooltipIndex < 0 || !items || !items.length) {
return null;
}
let activeItems = items;
if (activeLineKey) {
activeItems = items.filter(item => item.props.dataKey === activeLineKey && item.type.displayName === 'Line');
} else if (activeBarKey) {
activeItems = items.filter(item => item.props.dataKey === activeBarKey && item.type.displayName === 'Bar');
} else if (activeAreaKey) {
activeItems = items.filter(item => item.props.dataKey === activeAreaKey && item.type.displayName === 'Area');
}
return activeItems.map((child) => {
const { dataKey, name, unit, formatter } = child.props;
return {
key: name || dataKey,
unit: unit || '',
color: this.getMainColorOfItem(child),
value: data[activeTooltipIndex][dataKey],
formatter,
};
});
}
validateAxes() {
const { layout, children } = this.props;
const xAxes = ReactUtils.findAllByType(children, XAxis);
const yAxes = ReactUtils.findAllByType(children, YAxis);
if (layout === 'horizontal' && xAxes && xAxes.length) {
xAxes.forEach(axis => {
invariant(axis.props.type === 'category',
'x-axis should be category axis when the layout is horizontal'
);
});
} else if (layout === 'vertical') {
const displayName = this.constructor.displayName;
invariant(yAxes && yAxes.length,
`You should add <YAxis type="number"/> in ${displayName}.` +
`The layout is vertical now, y-axis should be category axis,` +
`but y-axis is number axis when no YAxis is added.`
);
invariant(xAxes && xAxes.length,
`You should add <XAxis /> in ${displayName}.` +
`The layout is vertical now, x-axis is category when no XAxis is added.`
);
if (yAxes && yAxes.length) {
yAxes.forEach(axis => {
invariant(axis.props.type === 'category',
'y-axis should be category axis when the layout is vertical'
);
});
}
}
return null;
}
parseSpecifiedDomain(specifiedDomain, autoDomain) {
if (!LodashUtils.isArray(specifiedDomain)) {
return autoDomain;
}
const domain = [];
if (!LodashUtils.isNumber(specifiedDomain[0]) || specifiedDomain[0] > autoDomain[0]) {
domain[0] = autoDomain[0];
} else {
domain[0] = specifiedDomain[0];
}
if (!LodashUtils.isNumber(specifiedDomain[1]) || specifiedDomain[1] < autoDomain[1]) {
domain[1] = autoDomain[1];
} else {
domain[1] = specifiedDomain[1];
}
return domain;
}
handleBrushChange({ startIndex, endIndex }) {
this.setState({
dataStartIndex: startIndex,
dataEndIndex: endIndex,
});
}
/**
* The handler of mouse entering chart
* @param {Object} offset The offset of main part in the svg element
* @param {Object} xAxisMap The configuration of all x-axes
* @param {Object} yAxisMap The configuration of all y-axes
* @param {Object} e Event object
* @return {Null} null
*/
handleMouseEnter(offset, xAxisMap, yAxisMap, e) {
const container = ReactDOM.findDOMNode(this);
const containerOffset = OuiDomUtils.getOffset(container);
const ne = this.getChartPosition(e, containerOffset);
const mouse = this.getMouseInfo(xAxisMap, yAxisMap, offset, ne);
if (mouse) {
this.setState({
...mouse,
isTooltipActive: true,
chartX: ne.chartX,
chartY: ne.chartY,
});
}
}
/**
* The handler of mouse moving in chart
* @param {Object} offset The offset of main part in the svg element
* @param {Object} xAxisMap The configuration of all x-axes
* @param {Object} yAxisMap The configuration of all y-axes
* @param {Object} e Event object
* @return {Null} no return
*/
handleMouseMove(offset, xAxisMap, yAxisMap, e) {
const container = ReactDOM.findDOMNode(this);
const containerOffset = OuiDomUtils.getOffset(container);
const ne = this.getChartPosition(e, containerOffset);
const mouse = this.getMouseInfo(xAxisMap, yAxisMap, offset, ne);
if (mouse) {
this.setState({
...mouse,
isTooltipActive: true,
chartX: ne.chartX,
chartY: ne.chartY,
});
} else {
this.setState({
isTooltipActive: false,
});
}
}
/**
* The handler if mouse leaving chart
* @return {Null} no return
*/
handleMouseLeave() {
this.setState({
isTooltipActive: false,
});
}
/**
* Draw x-axes
* @param {Object} xAxisMap The configuration of all x-axes
* @return {ReactElement} The instance of x-axes
*/
renderXAxis(xAxisMap) {
const { width, height } = this.props;
const ids = xAxisMap && Object.keys(xAxisMap);
if (ids && ids.length) {
const xAxes = [];
for (let i = 0, len = ids.length; i < len; i++) {
const axis = xAxisMap[ids[i]];
if (!axis.hide) {
xAxes.push((
<CartesianAxis
{...axis}
x={axis.x}
y={axis.y}
width={axis.width}
height={axis.height}
key={'x-axis-' + ids[i]}
orientation={axis.orientation}
viewBox={{ x: 0, y: 0, width, height }}
ticks={this.getAxisTicks(axis, true)}
/>
));
}
}
return xAxes.length ? <Layer key="x-axis-layer" className="recharts-x-axis">{xAxes}</Layer> : null;
}
}
/**
* Draw y-axes
* @param {Object} yAxisMap The configuration of all y-axes
* @return {ReactElement} The instance of y-axes
*/
renderYAxis(yAxisMap) {
const { width, height } = this.props;
const ids = yAxisMap && Object.keys(yAxisMap);
if (ids && ids.length) {
const yAxes = [];
for (let i = 0, len = ids.length; i < len; i++) {
const axis = yAxisMap[ids[i]];
if (!axis.hide) {
yAxes.push((
<CartesianAxis
{...axis}
key={'y-axis-' + ids[i]}
x={axis.x}
y={axis.y}
width={axis.width}
height={axis.height}
orientation={axis.orientation}
viewBox={{ x: 0, y: 0, width, height }}
ticks={this.getAxisTicks(axis, true)}
/>
));
}
}
return yAxes.length ? <Layer key="y-axis-layer" className="recharts-y-axis">{yAxes}</Layer> : null;
}
}
/**
* Draw grid
* @param {Object} xAxisMap The configuration of all x-axes
* @param {Object} yAxisMap The configuration of all y-axes
* @param {Object} offset The offset of main part in the svg element
* @return {ReactElement} The instance of grid
*/
renderGrid(xAxisMap, yAxisMap, offset) {
const { children, width, height } = this.props;
const gridItem = ReactUtils.findChildByType(children, CartesianGrid);
if (!gridItem) {return null;}
const xIds = Object.keys(xAxisMap);
const yIds = Object.keys(yAxisMap);
const xAxis = xAxisMap[xIds[0]];
const yAxis = yAxisMap[yIds[0]];
const verticalPoints = this.getGridTicks(CartesianAxis.getTicks({
...CartesianAxis.defaultProps, ...xAxis,
ticks: this.getAxisTicks(xAxis, true),
viewBox: { x: 0, y: 0, width, height },
}), offset.left, offset.left + offset.width);
const horizontalPoints = this.getGridTicks(CartesianAxis.getTicks({
...CartesianAxis.defaultProps, ...yAxis,
ticks: this.getAxisTicks(yAxis, true),
viewBox: { x: 0, y: 0, width, height },
}), offset.top, offset.top + offset.height);
return React.cloneElement(gridItem, {
key: 'grid',
x: offset.left,
y: offset.top,
width: offset.width,
height: offset.height,
verticalPoints, horizontalPoints,
});
}
/**
* Draw legend
* @param {Array} items The instances of item
* @return {ReactElement} The instance of Legend
*/
renderLegend(items) {
const { children, width, height } = this.props;
const legendItem = ReactUtils.findChildByType(children, Legend);
if (!legendItem) {return null;}
const legendData = items.map((child) => {
const { dataKey, name, legendType } = child.props;
return {
type: legendType || 'square',
color: this.getMainColorOfItem(child),
value: name || dataKey,
};
}, this);
return React.cloneElement(legendItem, {
...Legend.getWithHeight(legendItem, width, height),
payload: legendData,
});
}
/**
* Draw Tooltip
* @param {Array} items The instances of item
* @param {Object} offset The offset of main part in the svg element
* @return {ReactElement} The instance of Tooltip
*/
renderTooltip(items, offset) {
const { children } = this.props;
const tooltipItem = ReactUtils.findChildByType(children, Tooltip);
if (!tooltipItem) {
return null;
}
const { chartX, chartY, isTooltipActive,
activeTooltipLabel, activeTooltipCoord } = this.state;
const viewBox = {
x: offset.left,
y: offset.top,
width: offset.width,
height: offset.height,
};
return React.cloneElement(tooltipItem, {
viewBox,
active: isTooltipActive,
label: activeTooltipLabel,
payload: isTooltipActive ? this.getTooltipContent(items) : [],
coordinate: activeTooltipCoord,
mouseX: chartX,
mouseY: chartY,
});
}
renderBrush(xAxisMap, yAxisMap, offset) {
const { children, data } = this.props;
const brushItem = ReactUtils.findChildByType(children, Brush);
if (!brushItem) {return null;}
const dataKey = brushItem.props.dataKey;
const height = (brushItem.props.height || Brush.defaultProps.height) + 1;
return React.cloneElement(brushItem, {
onChange: ::this.handleBrushChange,
data: data.map(entry => entry[dataKey]),
x: offset.left,
y: offset.top + offset.height + offset.bottom - height,
width: offset.width,
});
}
renderReferenceLines(xAxisMap, yAxisMap, offset) {
const { children } = this.props;
const lines = ReactUtils.findAllByType(children, ReferenceLine);
if (!lines || !lines.length) {return null;}
return lines.map((entry, i) => {
return React.cloneElement(entry, {
key: 'reference-line-' + i,
xAxisMap, yAxisMap,
viewBox: {
x: offset.left,
y: offset.top,
width: offset.width,
height: offset.height,
},
});
});
}
}
export default CartesianChart;