woocommerce/plugins/woocommerce-admin/client/components/chart/charts.js

266 lines
6.5 KiB
JavaScript
Raw Normal View History

2018-06-15 18:11:25 +00:00
/** @format */
/**
* External dependencies
*/
import { isEqual } from 'lodash';
2018-08-08 11:00:45 +00:00
import { Component, createRef } from '@wordpress/element';
2018-06-15 18:11:25 +00:00
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format';
2018-07-25 14:58:32 +00:00
import { select as d3Select } from 'd3-selection';
2018-06-15 18:11:25 +00:00
/**
* Internal dependencies
*/
import './style.scss';
import D3Base from './d3-base';
import {
drawAxis,
drawBars,
drawLines,
getDateSpaces,
getOrderedKeys,
getLine,
getLineData,
getXTicks,
getUniqueKeys,
getUniqueDates,
getXScale,
getXGroupScale,
getXLineScale,
getYMax,
getYScale,
getYTickOffset,
} from './utils';
2018-06-15 18:11:25 +00:00
/**
* A simple D3 line and bar chart component for timeseries data in React.
*/
class D3Chart extends Component {
2018-08-08 11:00:45 +00:00
constructor( props ) {
super( props );
this.drawChart = this.drawChart.bind( this );
2018-08-08 11:00:45 +00:00
this.getAllData = this.getAllData.bind( this );
this.getParams = this.getParams.bind( this );
2018-08-08 11:00:45 +00:00
this.state = {
allData: this.getAllData( props ),
2018-09-11 11:04:26 +00:00
type: props.type,
2018-08-08 11:00:45 +00:00
width: props.width,
};
this.tooltipRef = createRef();
}
2018-08-08 11:00:45 +00:00
componentDidUpdate( prevProps, prevState ) {
2018-09-11 11:04:26 +00:00
const { type, width } = this.props;
2018-08-08 11:00:45 +00:00
/* eslint-disable react/no-did-update-set-state */
if ( width !== prevProps.width ) {
this.setState( { width } );
}
2018-08-08 11:00:45 +00:00
const nextAllData = this.getAllData( this.props );
if ( ! isEqual( [ ...nextAllData ].sort(), [ ...prevState.allData ].sort() ) ) {
this.setState( { allData: nextAllData } );
}
2018-09-11 11:04:26 +00:00
if ( type !== prevProps.type ) {
this.setState( { type } );
}
2018-08-08 11:00:45 +00:00
/* eslint-enable react/no-did-update-set-state */
}
2018-08-08 11:00:45 +00:00
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' )
2018-06-15 18:11:25 +00:00
.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,
2018-07-25 14:58:32 +00:00
tooltip: d3Select( this.tooltipRef.current ),
2018-06-15 18:11:25 +00:00
} );
2018-08-12 17:01:10 +00:00
drawAxis( g, adjParams );
type === 'line' && drawLines( g, data, adjParams );
type === 'bar' && drawBars( g, data, adjParams );
return node;
}
2018-06-15 18:11:25 +00:00
getParams( node ) {
2018-09-05 20:52:35 +00:00
const {
colorScheme,
data,
dateParser,
2018-09-05 20:52:35 +00:00
height,
layout,
2018-09-05 20:52:35 +00:00
margin,
orderedKeys,
tooltipFormat,
type,
xFormat,
2018-09-07 10:28:02 +00:00
x2Format,
2018-09-05 20:52:35 +00:00
yFormat,
} = this.props;
2018-08-08 11:00:45 +00:00
const { width } = this.state;
2018-06-15 18:11:25 +00:00
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 );
2018-08-12 17:01:10 +00:00
const newOrderedKeys = orderedKeys || getOrderedKeys( data, uniqueKeys );
const lineData = getLineData( data, newOrderedKeys );
const yMax = getYMax( lineData );
const yScale = getYScale( adjHeight, yMax );
const parseDate = d3UTCParse( dateParser );
const uniqueDates = getUniqueDates( lineData, parseDate );
const xLineScale = getXLineScale( uniqueDates, adjWidth );
const xScale = getXScale( uniqueDates, adjWidth );
const xTicks = getXTicks( uniqueDates, adjWidth, layout );
2018-06-15 18:11:25 +00:00
return {
colorScheme,
dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ),
2018-06-15 18:11:25 +00:00
height: calculatedHeight,
line: getLine( xLineScale, yScale ),
lineData,
2018-06-15 18:11:25 +00:00
margin,
orderedKeys: newOrderedKeys,
parseDate,
scale,
2018-09-05 20:52:35 +00:00
tooltipFormat: d3TimeFormat( tooltipFormat ),
type,
uniqueDates,
uniqueKeys,
width: calculatedWidth,
2018-07-10 12:46:57 +00:00
xFormat: d3TimeFormat( xFormat ),
2018-09-07 10:28:02 +00:00
x2Format: d3TimeFormat( x2Format ),
xGroupScale: getXGroupScale( orderedKeys, xScale ),
xLineScale,
xTicks,
xScale,
yMax,
yScale,
yTickOffset: getYTickOffset( adjHeight, scale, yMax ),
2018-09-05 20:52:35 +00:00
yFormat,
2018-06-15 18:11:25 +00:00
};
}
2018-06-15 18:11:25 +00:00
render() {
if ( ! this.props.data ) {
return null; // TODO: improve messaging
}
return (
<div
className={ classNames( 'woocommerce-chart__container', this.props.className ) }
2018-08-08 11:00:45 +00:00
style={ { height: this.props.height } }
>
2018-07-25 14:58:32 +00:00
<D3Base
className={ classNames( this.props.className ) }
data={ this.state.allData }
drawChart={ this.drawChart }
getParams={ this.getParams }
2018-09-11 11:04:26 +00:00
type={ this.state.type }
2018-08-08 11:00:45 +00:00
width={ this.state.width }
2018-07-25 14:58:32 +00:00
/>
<div className="tooltip" ref={ this.tooltipRef } />
</div>
);
}
}
2018-06-15 18:11:25 +00:00
2018-08-08 11:00:45 +00:00
D3Chart.propTypes = {
/**
* Additional CSS classes.
*/
2018-08-08 11:00:45 +00:00
className: PropTypes.string,
/**
* A chromatic color function to be passed down to d3.
*/
colorScheme: PropTypes.func,
/**
* An array of data.
*/
2018-08-08 11:00:45 +00:00
data: PropTypes.array.isRequired,
/**
* Format to parse dates into d3 time format
*/
dateParser: PropTypes.string.isRequired,
/**
* Relative viewpoirt height of the `svg`.
*/
2018-08-08 11:00:45 +00:00
height: PropTypes.number,
2018-09-05 20:52:35 +00:00
/**
* Interval specification (hourly, daily, weekly etc.)
*/
interval: PropTypes.oneOf( [ 'hour', 'day', 'week', 'month', 'quarter', 'year' ] ),
/**
* `standard` (default) legend layout in the header or `comparison` moves legend layout
* to the left or 'compact' has the legend below
*/
layout: PropTypes.oneOf( [ 'standard', 'comparison', 'compact' ] ),
/**
* Margins for axis and chart padding.
*/
2018-08-08 11:00:45 +00:00
margin: PropTypes.shape( {
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
} ),
/**
* The list of labels for this chart.
*/
2018-08-08 11:00:45 +00:00
orderedKeys: PropTypes.array,
2018-09-05 20:52:35 +00:00
/**
* A datetime formatting string to format the title of the toolip, passed to d3TimeFormat.
*/
tooltipFormat: PropTypes.string,
/**
* Chart type of either `line` or `bar`.
*/
2018-08-08 11:00:45 +00:00
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
/**
* Relative viewport width of the `svg`.
*/
2018-08-08 11:00:45 +00:00
width: PropTypes.number,
/**
* A datetime formatting string, passed to d3TimeFormat.
*/
2018-08-08 11:00:45 +00:00
xFormat: PropTypes.string,
2018-09-07 10:28:02 +00:00
/**
* A datetime formatting string, passed to d3TimeFormat.
*/
x2Format: PropTypes.string,
/**
* A number formatting string, passed to d3Format.
*/
2018-08-08 11:00:45 +00:00
yFormat: PropTypes.string,
};
D3Chart.defaultProps = {
2018-08-12 17:01:10 +00:00
data: [],
dateParser: '%Y-%m-%dT%H:%M:%S',
2018-08-08 11:00:45 +00:00
height: 200,
margin: {
bottom: 30,
left: 40,
right: 0,
top: 20,
},
layout: 'standard',
2018-09-05 20:52:35 +00:00
tooltipFormat: '%Y-%m-%d',
2018-08-08 11:00:45 +00:00
type: 'line',
width: 600,
xFormat: '%Y-%m-%d',
2018-09-07 10:28:02 +00:00
x2Format: '',
2018-09-05 20:52:35 +00:00
yFormat: '.3s',
2018-08-08 11:00:45 +00:00
};
2018-06-15 18:11:25 +00:00
export default D3Chart;