UNPKG

recharts

Version:
525 lines (471 loc) 15.8 kB
/** * @fileOverview Scatter Chart */ import React, { Component, PropTypes } from 'react'; import invariant from 'invariant'; import classNames from 'classnames'; import { linear } from 'd3-scale'; import { getNiceTickValues } from 'recharts-scale'; import Surface from '../container/Surface'; import Layer from '../container/Layer'; import Legend from '../component/Legend'; import Tooltip from '../component/Tooltip'; import Cross from '../shape/Cross'; import CartesianAxis from '../cartesian/CartesianAxis'; import CartesianGrid from '../cartesian/CartesianGrid'; import Scatter from '../cartesian/Scatter'; import XAxis from '../cartesian/XAxis'; import YAxis from '../cartesian/YAxis'; import ZAxis from '../cartesian/ZAxis'; import ReactUtils from '../util/ReactUtils'; class ScatterChart extends Component { static displayName = 'ScatterChart'; static propTypes = { width: PropTypes.number, height: PropTypes.number, margin: PropTypes.shape({ top: PropTypes.number, right: PropTypes.number, bottom: PropTypes.number, left: PropTypes.number, }), title: PropTypes.string, style: PropTypes.object, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node, ]), className: PropTypes.string, }; static defaultProps = { style: {}, margin: { top: 5, right: 5, bottom: 5, left: 5 }, }; state = { activeTooltipCoord: { x: 0, y: 0 }, isTooltipActive: false, activeItem: null, }; /** * Compose the data of each group * @param {Array} data The original data * @param {Object} xAxis The configuration of x-axis * @param {Object} yAxis The configuration of y-axis * @param {Object} zAxis The configuration of z-axis * @return {Array} Composed data */ getComposedData(data, xAxis, yAxis, zAxis) { const xAxisDataKey = xAxis.dataKey; const yAxisDataKey = yAxis.dataKey; const zAxisDataKey = zAxis.dataKey; return data.map(entry => { return { cx: xAxis.scale(entry[xAxisDataKey]), cy: yAxis.scale(entry[yAxisDataKey]), r: zAxisDataKey !== undefined ? zAxis.scale(entry[zAxisDataKey]) : zAxis.range[0], payload: { x: entry[xAxisDataKey], y: entry[yAxisDataKey], z: (zAxisDataKey !== undefined && entry[zAxisDataKey]) || '-', }, }; }); } /** * 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; if (axis.ticks) { return axis.ticks.map(entry => { return { coord: scale(entry), value: entry, }; }); } if (scale.ticks) { return scale.ticks(axis.tickCount).map(entry => { return { coord: scale(entry), value: entry, }; }); } return scale.domain().map((entry) => { return { coord: scale(entry), 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 domain of ticks * @param {Array} ticks Ticks of axis * @return {Array} domain */ getDomainOfTicks(ticks) { return [Math.min.apply(null, ticks), Math.max.apply(null, ticks)]; } getDomain(items, dataKey) { const domain = items.reduce((result, item) => { return result.concat(item.props.data.map(entry => entry[dataKey])); }, []); return [Math.min.apply(null, domain), Math.max.apply(null, domain)]; } /** * Get the configuration of x-axis or y-axis * @param {String} axisType The type of axis * @param {Array} items The instances of item * @return {Object} Configuration */ getAxis(axisType = 'xAxis', items) { const { children } = this.props; const Axis = axisType === 'xAxis' ? XAxis : YAxis; const axis = ReactUtils.findChildByType(children, Axis); invariant(axis, 'recharts: ScatterChart must has %s', Axis.displayName); if (axis) { const domain = this.getDomain(items, axis.props.dataKey); return { ...axis.props, axisType, domain, }; } return null; } /** * Get the configuration of z-axis * @param {Array} items The instances of item * @return {Object} Configuration */ getZAxis(items) { const { children } = this.props; const axisItem = ReactUtils.findChildByType(children, ZAxis); const axisProps = (axisItem && axisItem.props) || ZAxis.defaultProps; const domain = axisProps.dataKey ? this.getDomain(items, axisProps.dataKey) : [-1, 1]; return { ...axisProps, domain, scale: linear().domain(domain).range(axisProps.range), }; } getOffset(xAxis, yAxis) { const { width, height, margin } = this.props; const offset = { ...margin }; offset[xAxis.orientation] += xAxis.height; offset[yAxis.orientation] += yAxis.width; return { ...offset, width: width - offset.left - offset.right, height: height - offset.top - offset.bottom, }; } /** * 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) { opts.domain = this.getDomainOfTicks(opts.ticks, opts.type); scale.domain(opts.domain) .ticks(opts.ticks.length); } else { // Calculate the ticks by the number of grid const domain = scale.domain(); const tickValues = getNiceTickValues(domain, opts.tickCount); opts.ticks = tickValues; opts.domain = this.getDomainOfTicks(tickValues, opts.type); scale.domain(opts.domain) .ticks(opts.tickCount); } } /** * Calculate the scale function, position, width, height of axes * @param {Object} axis The configuration of axis * @param {Object} offset The offset of main part in the svg element * @param {Object} axisType The type of axis, x-axis or y-axis * @return {Object} Configuration */ getFormatAxis(axis, offset, axisType) { const { orientation, domain, tickFormat } = axis; const range = axisType === 'xAxis' ? [offset.left, offset.left + offset.width] : [offset.top + offset.height, offset.top]; const scale = linear().domain(domain).range(range); this.setTicksOfScale(scale, axis); if (tickFormat) { scale.tickFormat(tickFormat); } let x; let y; if (axisType === 'xAxis') { x = offset.left; y = orientation === 'top' ? offset.top - axis.height : offset.top + offset.height; } else { x = orientation === 'left' ? offset.left - axis.width : offset.right; y = offset.top; } return { ...axis, scale, width: axisType === 'xAxis' ? offset.width : axis.width, height: axisType === 'yAxis' ? offset.height : axis.height, x, y, }; } /** * Get the content to be displayed in the tooltip * @param {Object} data The data of active item * @param {Object} xAxis The configuration of x-axis * @param {Object} yAxis The configuration of y-axis * @param {Object} zAxis The configuration of z-axis * @return {Array} The content of tooltip */ getTooltipContent(data, xAxis, yAxis, zAxis) { if (!data) {return null;} const content = [{ key: xAxis.name || xAxis.dataKey, unit: xAxis.unit || '', value: data.x, }, { key: yAxis.name || yAxis.dataKey, unit: yAxis.unit || '', value: data.y, }]; if (data.z && data.z !== '-') { content.push({ key: zAxis.name || zAxis.dataKey, unit: zAxis.unit || '', value: data.z, }); } return content; } /** * The handler of mouse entering a scatter * @param {Object} el The active scatter * @param {Object} e Event object * @return {Object} no return */ handleScatterMouseEnter(el, e) { this.setState({ isTooltipActive: true, activeItem: el, activeTooltipCoord: { x: el.cx, y: el.cy }, }); } /** * The handler of mouse leaving a scatter * @return {Object} no return */ handleScatterMouseLeave() { this.setState({ isTooltipActive: false, }); } /** * Draw Tooltip * @param {Array} items The instances of Scatter * @param {Object} xAxis The configuration of x-axis * @param {Object} yAxis The configuration of y-axis * @param {Object} zAxis The configuration of z-axis * @param {Object} offset The offset of main part in the svg element * @return {ReactElement} The instance of Tooltip */ renderTooltip(items, xAxis, yAxis, zAxis, offset) { const { children } = this.props; const tooltipItem = ReactUtils.findChildByType(children, Tooltip); if (!tooltipItem || !tooltipItem.props.cursor || !this.state.isTooltipActive) { return null; } const { chartX, chartY, isTooltipActive, activeItem, 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: '', payload: this.getTooltipContent(activeItem && activeItem.payload, xAxis, yAxis, zAxis), coordinate: activeTooltipCoord, mouseX: chartX, mouseY: chartY, }); } /** * Draw grid * @param {Object} xAxis The configuration of x-axis * @param {Object} yAxis The configuration of y-axis * @param {Object} offset The offset of main part in the svg element * @return {ReactElement} The instance of grid */ renderGrid(xAxis, yAxis, offset) { const { children, width, height } = this.props; const gridItem = ReactUtils.findChildByType(children, CartesianGrid); if (!gridItem) {return null;} const verticalPoints = this.getGridTicks(CartesianAxis.getTicks({ ...CartesianAxis.defaultProps, ...xAxis, ticks: this.getAxisTicks(xAxis), 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), 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 Scatters * @param {Object} offset The offset of main part in the svg element * @return {ReactElement} The instance of Legend */ renderLegend(items, offset) { const { children, width, height } = this.props; const legendItem = ReactUtils.findChildByType(children, Legend); if (!legendItem) {return null;} const legendData = items.map((child) => { const { name, fill, legendType } = child.props; return { type: legendType || 'square', color: fill, value: name || '', }; }, this); return React.cloneElement(legendItem, { ...Legend.getWithHeight(legendItem, width, height), payload: legendData, }); } /** * Draw axis * @param {Object} axis The configuration of axis * @param {String} layerKey The key of layer * @return {ReactElement} The instance of axis */ renderAxis(axis, layerKey) { const { width, height } = this.props; if (axis && !axis.hide) { return ( <Layer key={layerKey} className={layerKey}> <CartesianAxis 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)} /> </Layer> ); } } renderCursor(xAxis, yAxis, offset) { const { children } = this.props; const tooltipItem = ReactUtils.findChildByType(children, Tooltip); if (!tooltipItem || !this.state.isTooltipActive) {return null;} const { activeItem } = this.state; const cursorProps = { fill: '#f1f1f1', ...ReactUtils.getPresentationAttributes(tooltipItem.props.cursor), ...offset, x: activeItem.cx, y: activeItem.cy, payload: activeItem, }; return React.isValidElement(tooltipItem.props.cursor) ? React.cloneElement(tooltipItem.props.cursor, cursorProps) : React.createElement(Cross, cursorProps); } /** * Draw the main part of scatter chart * @param {Array} items All the instance of Scatter * @param {Object} xAxis The configuration of all x-axis * @param {Object} yAxis The configuration of all y-axis * @param {Object} zAxis The configuration of all z-axis * @return {ReactComponent} All the instances of Scatter */ renderItems(items, xAxis, yAxis, zAxis) { const { activeGroupId } = this.state; return items.map((child, i) => { const { strokeWidth, data } = child.props; let finalStrokeWidth = strokeWidth === +strokeWidth ? strokeWidth : 1; finalStrokeWidth = activeGroupId === 'scatter-' + i ? finalStrokeWidth + 2 : finalStrokeWidth; return React.cloneElement(child, { key: 'scatter-' + i, groupId: 'scatter-' + i, strokeWidth: finalStrokeWidth, onMouseLeave: ::this.handleScatterMouseLeave, onMouseEnter: ::this.handleScatterMouseEnter, points: this.getComposedData(data, xAxis, yAxis, zAxis), }); }, this); } render() { if (!ReactUtils.validateWidthHeight(this)) {return null;} const { style, children, className, width, height } = this.props; const items = ReactUtils.findAllByType(children, Scatter); const zAxis = this.getZAxis(items); let xAxis = this.getAxis('xAxis', items); let yAxis = this.getAxis('yAxis', items); const offset = this.getOffset(xAxis, yAxis); xAxis = this.getFormatAxis(xAxis, offset, 'xAxis'); yAxis = this.getFormatAxis(yAxis, offset, 'yAxis'); return ( <div className={classNames('recharts-wrapper', className)} style={{ position: 'relative', cursor: 'default', ...style }} > <Surface width={width} height={height}> {this.renderGrid(xAxis, yAxis, offset)} {this.renderAxis(xAxis, 'recharts-x-axis')} {this.renderAxis(yAxis, 'recharts-y-axis')} {this.renderCursor(xAxis, yAxis, offset)} {this.renderItems(items, xAxis, yAxis, zAxis, offset)} </Surface> {this.renderLegend(items)} {this.renderTooltip(items, xAxis, yAxis, zAxis, offset)} </div> ); } } export default ScatterChart;