recharts
Version:
React charts
313 lines (262 loc) • 10.1 kB
JavaScript
/**
* @fileOverview Radar Chart
*/
import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import invariant from 'invariant';
import D3Scale from 'd3-scale';
import { getNiceTickValues } from 'recharts-scale';
import Surface from '../container/Surface';
import Layer from '../container/Layer';
import Legend from '../component/Legend';
import Radar from '../polar/Radar';
import PolarGrid from '../polar/PolarGrid';
import PolarAngleAxis from '../polar/PolarAngleAxis';
import PolarRadiusAxis from '../polar/PolarRadiusAxis';
import ReactUtils from '../util/ReactUtils';
import LodashUtils from '../util/LodashUtils';
import { polarToCartesian, getMaxRadius } from '../util/PolarUtils';
class RadarChart extends Component {
static displayName = 'RadarChart';
static propTypes = {
width: PropTypes.number,
height: PropTypes.number,
margin: PropTypes.shape({
top: PropTypes.number,
right: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
}),
cx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
cy: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
startAngle: PropTypes.number,
innerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
outerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
clockWise: PropTypes.bool,
data: PropTypes.array,
style: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
className: PropTypes.string,
};
static defaultProps = {
width: 0,
height: 0,
startAngle: 90,
clockWise: true,
data: [],
margin: { top: 0, right: 0, bottom: 0, left: 0 },
};
getRadiusAxisCfg(radiusAxis, innerRadius, outerRadius) {
const { children } = this.props;
let domain;
let tickCount;
let ticks;
if (radiusAxis && radiusAxis.props.domain) {
tickCount = Math.max(radiusAxis.props.tickCount || PolarRadiusAxis.defaultProps.tickCount, 2);
domain = radiusAxis.props.domain;
invariant(domain.length === 2 && domain[0] === +domain[0] && domain[1] === +domain[1],
`domain in PolarRadiusAxis should be an array which has two numbers`
);
} else if (radiusAxis && radiusAxis.props.ticks) {
ticks = radiusAxis.props.ticks;
tickCount = ticks.length;
domain = [Math.min.apply(null, ticks), Math.max.apply(null, ticks)];
} else {
tickCount = Math.max(radiusAxis && radiusAxis.props.tickCount || PolarRadiusAxis.defaultProps.tickCount, 2);
ticks = this.getTicksByItems(radiusAxis, tickCount);
domain = [Math.min.apply(null, ticks), Math.max.apply(null, ticks)];
}
return {
tickCount,
ticks,
scale: D3Scale.linear().domain(domain).range([innerRadius, outerRadius]),
};
}
getTicksByItems(axisItem, tickCount) {
const { data, children } = this.props;
const radarItems = ReactUtils.findAllByType(children, Radar);
const dataKeys = radarItems.map(item => item.props.dataKey);
const max = data.reduce((prev, current) => {
const currentMax = Math.max.apply(null, dataKeys.map(v => current[v] || 0));
return Math.max(prev, currentMax);
}, 0);
const tickValues = getNiceTickValues([0, max], tickCount);
return tickValues;
}
getGridRadius(gridCount, innerRadius, outerRadius) {
const domain = LodashUtils.range(0, gridCount);
const scale = D3Scale.point().domain(domain)
.range([innerRadius, outerRadius]);
return domain.map(v => scale(v));
}
getAngle(index, dataLength, startAngle, clockWise) {
const sign = clockWise ? -1 : 1;
const angleInterval = 360 / dataLength;
return startAngle + index * sign * angleInterval;
}
getAngleTicks(dataLength, startAngle, clockWise) {
const angles = [];
for (let i = 0; i < dataLength; i++) {
angles.push(this.getAngle(i, dataLength, startAngle, clockWise));
}
return angles;
}
getRadiusTicks(axisCfg) {
const { ticks, scale } = axisCfg;
if (ticks && ticks.length) {
return ticks.map(entry => {
return {
radius: scale(entry),
value: entry,
};
});
}
const { tickCount } = axisCfg;
const domain = scale.domain();
return LodashUtils.range(0, tickCount).map((v, i) => {
const value = domain[0] + i * (domain[1] - domain[0]) / (tickCount - 1);
return {
value,
radius: scale(value),
};
});
}
getComposedData(item, scale, cx, cy, innerRadius, outerRadius) {
const { dataKey } = item.props;
const { data, startAngle, clockWise } = this.props;
const len = data.length;
return data.map((entry, i) => {
const value = entry[dataKey] || 0;
const angle = this.getAngle(i, len, startAngle, clockWise);
const radius = scale(value);
return {
...polarToCartesian(cx, cy, radius, angle),
value,
cx, cy, radius, angle,
payload: entry,
};
});
}
renderRadars(items, scale, cx, cy, innerRadius, outerRadius) {
if (!items || !items.length) {return null;}
const baseProps = ReactUtils.getPresentationAttributes(this.props);
return items.map((el, index) => {
return React.cloneElement(el, {
...baseProps,
...ReactUtils.getPresentationAttributes(el),
points: this.getComposedData(el, scale, cx, cy, innerRadius, outerRadius),
key: 'radar-' + index,
});
});
}
renderGrid(radiusAxisCfg, cx, cy, innerRadius, outerRadius) {
const { children } = this.props;
const grid = ReactUtils.findChildByType(children, PolarGrid);
if (!grid) {return null;}
const { startAngle, clockWise, data } = this.props;
const len = data.length;
const gridCount = radiusAxisCfg.tickCount;
return React.cloneElement(grid, {
polarAngles: this.getAngleTicks(len, startAngle, clockWise),
polarRadius: this.getGridRadius(gridCount, innerRadius, outerRadius),
cx, cy, innerRadius, outerRadius,
key: 'layer-grid',
});
}
renderAngleAxis(cx, cy, outerRadius, maxRadius) {
const { children } = this.props;
const angleAxis = ReactUtils.findChildByType(children, PolarAngleAxis);
if (!angleAxis || angleAxis.props.hide) {return null;}
const { data, width, height, startAngle, clockWise } = this.props;
const len = data.length;
const grid = ReactUtils.findChildByType(children, PolarGrid);
const radius = LodashUtils.getPercentValue(angleAxis.props.radius, maxRadius, outerRadius);
const { dataKey } = angleAxis.props;
return React.cloneElement(angleAxis, {
ticks: data.map((v, i) => {
return {
value: dataKey ? v[dataKey] : i,
angle: this.getAngle(i, len, startAngle, clockWise),
};
}),
cx, cy, radius,
axisLineType: (grid && grid.props && grid.props.gridType)
|| PolarGrid.defaultProps.gridType,
key: 'layer-angle-axis',
});
}
renderRadiusAxis(radiusAxis, radiusAxisCfg, cx, cy) {
if (!radiusAxis || radiusAxis.props.hide) {return null;}
const { startAngle } = this.props;
return React.cloneElement(radiusAxis, {
angle: radiusAxis.props.angle || startAngle,
ticks: this.getRadiusTicks(radiusAxisCfg),
cx, cy,
});
}
/**
* Draw legend
* @param {Array} items The instances of item
* @return {ReactElement} The instance of Legend
*/
renderLegend(items) {
const { children } = this.props;
const legendItem = ReactUtils.findChildByType(children, Legend);
if (!legendItem) {return null;}
const { width, height } = this.props;
const legendData = items.map((child) => {
const { dataKey, name, legendType } = child.props;
return {
type: legendType || 'square',
color: child.props.stroke || child.props.fill,
value: name || dataKey,
};
}, this);
return React.cloneElement(legendItem, {
...Legend.getWithHeight(legendItem, width, height),
payload: legendData,
});
}
render() {
if (!ReactUtils.validateWidthHeight(this)) {return null;}
const { className, data, width, height, margin, children, style } = this.props;
const cx = LodashUtils.getPercentValue(this.props.cx, width, width / 2);
const cy = LodashUtils.getPercentValue(this.props.cy, height, height / 2);
const maxRadius = getMaxRadius(width, height, cx, cy, margin);
const innerRadius = LodashUtils.getPercentValue(this.props.innerRadius, maxRadius, 0);
const outerRadius = LodashUtils.getPercentValue(this.props.outerRadius, maxRadius, maxRadius * 0.8);
if (outerRadius <= 0 || !data || !data.length) {
invariant(outerRadius > 0,
`outerRadius should be greater than 0.`
);
invariant(data && data.length,
`data(${data}) should not be null, undefined, or an empty array.`
);
return null;
}
invariant(outerRadius > innerRadius,
`outerRadius(${this.props.outerRadius}) should be ` +
`greater than innerRadius(${this.props.innerRadius}). `
);
const items = ReactUtils.findAllByType(children, Radar);
const radiusAxis = ReactUtils.findChildByType(children, PolarRadiusAxis);
const radiusAxisCfg = this.getRadiusAxisCfg(radiusAxis, innerRadius, outerRadius);
return (
<div className={classNames('recharts-wrapper', className)}
style={{ position: 'relative', cursor: 'default', ...style }}
>
<Surface width={width} height={height}>
{this.renderGrid(radiusAxisCfg, cx, cy, innerRadius, outerRadius)}
{this.renderRadiusAxis(radiusAxis, radiusAxisCfg, cx, cy)}
{this.renderAngleAxis(cx, cy, outerRadius, maxRadius)}
{this.renderRadars(items, radiusAxisCfg.scale, cx, cy, innerRadius, outerRadius)}
</Surface>
{this.renderLegend(items)}
</div>
);
}
}
export default RadarChart;