240 lines
5.7 KiB
JavaScript
240 lines
5.7 KiB
JavaScript
/** @format */
|
||
|
||
/**
|
||
* External dependencies
|
||
*/
|
||
import { isEqual } from 'lodash';
|
||
import { Component, createRef } from '@wordpress/element';
|
||
import PropTypes from 'prop-types';
|
||
import classNames from 'classnames';
|
||
import { timeFormat as d3TimeFormat } from 'd3-time-format';
|
||
import { select as d3Select } from 'd3-selection';
|
||
|
||
/**
|
||
* Internal dependencies
|
||
*/
|
||
import './style.scss';
|
||
import D3Base from './d3-base';
|
||
import {
|
||
drawAxis,
|
||
drawBars,
|
||
drawLines,
|
||
getDateSpaces,
|
||
getOrderedKeys,
|
||
getLine,
|
||
getLineData,
|
||
getUniqueKeys,
|
||
getUniqueDates,
|
||
getXScale,
|
||
getXGroupScale,
|
||
getXLineScale,
|
||
getYMax,
|
||
getYScale,
|
||
getYTickOffset,
|
||
} from './utils';
|
||
|
||
/**
|
||
* A simple D3 line and bar chart component for timeseries data in React.
|
||
*/
|
||
class D3Chart extends Component {
|
||
constructor( props ) {
|
||
super( props );
|
||
this.drawChart = this.drawChart.bind( this );
|
||
this.getAllData = this.getAllData.bind( this );
|
||
this.getParams = this.getParams.bind( this );
|
||
this.state = {
|
||
allData: this.getAllData( props ),
|
||
width: props.width,
|
||
};
|
||
this.tooltipRef = createRef();
|
||
}
|
||
|
||
componentDidUpdate( prevProps, prevState ) {
|
||
const { width } = this.props;
|
||
/* eslint-disable react/no-did-update-set-state */
|
||
if ( width !== prevProps.width ) {
|
||
this.setState( { width } );
|
||
}
|
||
const nextAllData = this.getAllData( this.props );
|
||
if ( ! isEqual( [ ...nextAllData ].sort(), [ ...prevState.allData ].sort() ) ) {
|
||
this.setState( { allData: nextAllData } );
|
||
}
|
||
/* eslint-enable react/no-did-update-set-state */
|
||
}
|
||
|
||
getAllData( props ) {
|
||
const orderedKeys =
|
||
props.orderedKeys || getOrderedKeys( props.data, getUniqueKeys( props.data ) );
|
||
return [ ...props.data, ...orderedKeys ];
|
||
}
|
||
|
||
drawChart( node, params ) {
|
||
const { data, margin, type } = this.props;
|
||
const g = node
|
||
.attr( 'id', 'chart' )
|
||
.append( 'g' )
|
||
.attr( 'transform', `translate(${ margin.left },${ margin.top })` );
|
||
|
||
const adjParams = Object.assign( {}, params, {
|
||
height: params.height - margin.top - margin.bottom,
|
||
width: params.width - margin.left - margin.right,
|
||
tooltip: d3Select( this.tooltipRef.current ),
|
||
} );
|
||
drawAxis( g, adjParams );
|
||
type === 'line' && drawLines( g, data, adjParams );
|
||
type === 'bar' && drawBars( g, data, adjParams );
|
||
|
||
return node;
|
||
}
|
||
|
||
getParams( node ) {
|
||
const {
|
||
colorScheme,
|
||
data,
|
||
height,
|
||
margin,
|
||
orderedKeys,
|
||
tooltipFormat,
|
||
type,
|
||
xFormat,
|
||
yFormat,
|
||
} = this.props;
|
||
const { width } = this.state;
|
||
const calculatedWidth = width || node.offsetWidth;
|
||
const calculatedHeight = height || node.offsetHeight;
|
||
const scale = width / node.offsetWidth;
|
||
const adjHeight = calculatedHeight - margin.top - margin.bottom;
|
||
const adjWidth = calculatedWidth - margin.left - margin.right;
|
||
const uniqueKeys = getUniqueKeys( data );
|
||
const newOrderedKeys = orderedKeys || getOrderedKeys( data, uniqueKeys );
|
||
const lineData = getLineData( data, newOrderedKeys );
|
||
const yMax = getYMax( lineData );
|
||
const yScale = getYScale( adjHeight, yMax );
|
||
const uniqueDates = getUniqueDates( lineData );
|
||
const xLineScale = getXLineScale( uniqueDates, adjWidth );
|
||
const xScale = getXScale( uniqueDates, adjWidth );
|
||
return {
|
||
colorScheme,
|
||
dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ),
|
||
height: calculatedHeight,
|
||
line: getLine( xLineScale, yScale ),
|
||
lineData,
|
||
margin,
|
||
orderedKeys: newOrderedKeys,
|
||
scale,
|
||
tooltipFormat: d3TimeFormat( tooltipFormat ),
|
||
type,
|
||
uniqueDates,
|
||
uniqueKeys,
|
||
width: calculatedWidth,
|
||
xFormat: d3TimeFormat( xFormat ),
|
||
xGroupScale: getXGroupScale( orderedKeys, xScale ),
|
||
xLineScale,
|
||
xScale,
|
||
yMax,
|
||
yScale,
|
||
yTickOffset: getYTickOffset( adjHeight, scale, yMax ),
|
||
yFormat,
|
||
};
|
||
}
|
||
|
||
render() {
|
||
if ( ! this.props.data ) {
|
||
return null; // TODO: improve messaging
|
||
}
|
||
return (
|
||
<div
|
||
className={ classNames( 'woocommerce-chart__container', this.props.className ) }
|
||
style={ { height: this.props.height } }
|
||
>
|
||
<D3Base
|
||
className={ classNames( this.props.className ) }
|
||
data={ this.state.allData }
|
||
drawChart={ this.drawChart }
|
||
getParams={ this.getParams }
|
||
width={ this.state.width }
|
||
/>
|
||
<div className="tooltip" ref={ this.tooltipRef } />
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
|
||
D3Chart.propTypes = {
|
||
/**
|
||
* Additional CSS classes.
|
||
*/
|
||
className: PropTypes.string,
|
||
/**
|
||
* A chromatic color function to be passed down to d3.
|
||
*/
|
||
colorScheme: PropTypes.func,
|
||
/**
|
||
* An array of data.
|
||
*/
|
||
data: PropTypes.array.isRequired,
|
||
/**
|
||
* Relative viewpoirt height of the `svg`.
|
||
*/
|
||
height: PropTypes.number,
|
||
/**
|
||
* Interval specification (hourly, daily, weekly etc.)
|
||
*/
|
||
interval: PropTypes.oneOf( [ 'hourly', 'daily', 'weekly', 'monthly', 'quaterly', 'yearly' ] ),
|
||
/**
|
||
* @todo Remove – not used?
|
||
*/
|
||
legend: PropTypes.array,
|
||
/**
|
||
* Margins for axis and chart padding.
|
||
*/
|
||
margin: PropTypes.shape( {
|
||
bottom: PropTypes.number,
|
||
left: PropTypes.number,
|
||
right: PropTypes.number,
|
||
top: PropTypes.number,
|
||
} ),
|
||
/**
|
||
* The list of labels for this chart.
|
||
*/
|
||
orderedKeys: PropTypes.array,
|
||
/**
|
||
* A datetime formatting string to format the title of the toolip, passed to d3TimeFormat.
|
||
*/
|
||
tooltipFormat: PropTypes.string,
|
||
/**
|
||
* Chart type of either `line` or `bar`.
|
||
*/
|
||
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
|
||
/**
|
||
* Relative viewport width of the `svg`.
|
||
*/
|
||
width: PropTypes.number,
|
||
/**
|
||
* A datetime formatting string, passed to d3TimeFormat.
|
||
*/
|
||
xFormat: PropTypes.string,
|
||
/**
|
||
* A number formatting string, passed to d3Format.
|
||
*/
|
||
yFormat: PropTypes.string,
|
||
};
|
||
|
||
D3Chart.defaultProps = {
|
||
data: [],
|
||
height: 200,
|
||
margin: {
|
||
bottom: 30,
|
||
left: 40,
|
||
right: 0,
|
||
top: 20,
|
||
},
|
||
tooltipFormat: '%Y-%m-%d',
|
||
type: 'line',
|
||
width: 600,
|
||
xFormat: '%Y-%m-%d',
|
||
yFormat: '.3s',
|
||
};
|
||
|
||
export default D3Chart;
|