src/AreaChart.js (135 lines of code) (raw):
import { area } from 'd3-shape';
import isUndefined from 'lodash/isUndefined';
import uniqueId from 'lodash/uniqueId';
import PropTypes from 'prop-types';
import React from 'react';
import * as CustomPropTypes from './utils/CustomPropTypes';
import { combineDomains, domainFromData, makeAccessor2 } from './utils/Data';
import xyPropsEqual from './utils/xyPropsEqual';
/**
* `AreaChart` represents a simple bivariate area chart,
* a filled path drawn between two lines (datasets).
*/
// todo horizontal prop, for filling area horizontally?
// todo support ordinal (like days of the week) data?
// todo build StackedAreaChart that composes multiple AreaCharts
export default class AreaChart extends React.Component {
static propTypes = {
/**
* The array of data objects
*/
data: PropTypes.array.isRequired,
/**
* Accessor function for area X values, called once per datum,
* or a single X value to be used for the entire line.
*/
x: CustomPropTypes.valueOrAccessor,
/**
* Accessor function for area's starting (minimum) Y values, called once per datum,
* or a single Y value to be used for the entire line.
* Should return the minimum of the Y range spanned by the area at this point.
*/
y: CustomPropTypes.valueOrAccessor,
/**
* Accessor function for area's ending (maximum) Y values, called once per datum,
* or a single Y value to be used for the entire line.
* Should return the maximum of the Y range spanned by the area at this point.
*/
yEnd: CustomPropTypes.valueOrAccessor,
/**
* Class attribute to be applied to area path element.
*/
// TODO: update to allow function to be passed
pathClassName: PropTypes.string,
/**
* Inline style object to be applied to area path element.
*/
// TODO: update to allow function to be passed
pathStyle: PropTypes.object,
/**
* If isDifference is true, AreaChart generates a "difference chart" with two area paths instead of one:
* one path which shows when YEnd > Y, and one vice versa, allowing them to be styled differently (eg red/green).
*/
isDifference: PropTypes.bool,
/**
* When isDifference is true, pathStylePositive can be passed to style the
* positive area difference.
* Ignored if isDifference is false.
*/
pathStylePositive: PropTypes.object,
/**
* When isDifference is true, pathStyleNegative can be passed to style the
* negative area difference.
* Ignored if isDifference is false.
*/
pathStyleNegative: PropTypes.object,
/**
* If true, will show gaps in the shaded area for data where props.isDefined(datum) returns false.
*/
shouldShowGaps: PropTypes.bool,
/**
* If shouldShowGaps is true, isDefined function describes when a datum
* should be considered "defined" vs. when to show gap by default.
* Shows gap if either y or yEnd are undefined.
*/
isDefined: PropTypes.func,
/**
* D3 scale for X axis - provided by XYPlot.
*/
xScale: PropTypes.func,
/**
* D3 scale for Y axis - provided by XYPlot.
*/
yScale: PropTypes.func,
/**
* Type of X scale - provided by XYPlot.
*/
xScaleType: PropTypes.string,
/**
* Type of Y scale - provided by XYPlot.
*/
yScaleType: PropTypes.string,
/**
* Height of chart - provided by XYPlot.
*/
height: PropTypes.number,
/**
* D3 curve for path generation.
*/
curve: PropTypes.func,
};
static defaultProps = {
shouldShowGaps: true,
isDefined: (d, i, accessors) => {
return (
!isUndefined(accessors.y(d, i)) && !isUndefined(accessors.yEnd(d, i))
);
},
pathClassName: '',
pathStyle: {},
};
static getDomain(props) {
// custom Y domain - the total (union) extent of getY and getYEnd combined
const { data, x, y, yEnd } = props;
const accessors = {
x: makeAccessor2(x),
y: makeAccessor2(y),
yEnd: makeAccessor2(yEnd),
};
return {
yDomain: combineDomains([
domainFromData(data, accessors.y),
domainFromData(data, accessors.yEnd),
]),
};
}
shouldComponentUpdate(nextProps) {
const shouldUpdate = !xyPropsEqual(this.props, nextProps, [
'pathStyle',
'pathStylePositive',
'pathStyleNegative',
]);
return shouldUpdate;
}
render() {
const {
data,
x,
y,
yEnd,
xScale,
yScale,
isDifference,
pathStyle,
pathStylePositive,
pathStyleNegative,
shouldShowGaps,
pathClassName,
isDefined,
curve,
} = this.props;
const accessors = {
x: makeAccessor2(x),
y: makeAccessor2(y),
yEnd: makeAccessor2(yEnd),
};
// create d3 area path generator
const areaGenerator = area();
// if gaps in data should be shown, use `props.isDefined` function as the `defined` param for d3's area generator;
// but wrap it & pass in accessors as well, so that the function can easily access the relevant data values
if (shouldShowGaps) {
areaGenerator.defined((d, i) => isDefined(d, i, accessors));
}
areaGenerator
.x((d, i) => xScale(accessors.x(d, i)))
.y0((d, i) => yScale(accessors.y(d, i)))
.y1((d, i) => yScale(accessors.yEnd(d, i)));
if (curve) {
areaGenerator.curve(curve);
}
const areaPathStr = areaGenerator(data);
if (isDifference) {
// difference chart - create 2 clip paths, one which clips to only show path where YEnd > Y, and other vice versa
// don't document height prop from XYPlot
/* eslint-disable react/prop-types */
areaGenerator.y0(this.props.height);
/* eslint-enable react/prop-types */
const clipBelowPathStr = areaGenerator(data);
areaGenerator.y0(0);
const clipAbovePathStr = areaGenerator(data);
// make sure we have a unique ID for this chart, so clip path IDs don't affect other charts
const chartId = uniqueId();
const clipAboveId = `clip-above-area-${chartId}`;
const clipBelowId = `clip-below-area-${chartId}`;
const pathStyleAbove = pathStylePositive || pathStyle || {};
const pathStyleBelow = pathStyleNegative || pathStyle || {};
return (
<g className="rct-area-chart--difference">
<clipPath id={clipAboveId}>
<path className="rct-area-chart-path" d={clipAbovePathStr} />
</clipPath>
<clipPath id={clipBelowId}>
<path className="rct-area-chart-path" d={clipBelowPathStr} />
</clipPath>
<path
className={`rct-area-chart-path ${pathClassName}`}
d={areaPathStr}
clipPath={`url(#${clipAboveId})`}
style={pathStyleAbove}
/>
<path
className={`rct-area-chart-path ${pathClassName}`}
d={areaPathStr}
clipPath={`url(#${clipBelowId})`}
style={pathStyleBelow}
/>
</g>
);
}
return (
<g className="rct-area-chart" aria-hidden="true">
<path
className={`rct-area-chart-path ${pathClassName}`}
d={areaPathStr}
style={pathStyle || {}}
/>
</g>
);
}
}