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

240 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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;