2018-07-11 15:23:16 +00:00
|
|
|
/** @format */
|
|
|
|
/**
|
|
|
|
* External dependencies
|
|
|
|
*/
|
|
|
|
import classNames from 'classnames';
|
2018-08-08 11:00:45 +00:00
|
|
|
import { isEqual } from 'lodash';
|
|
|
|
import { Component, createRef } from '@wordpress/element';
|
2018-08-01 09:10:36 +00:00
|
|
|
import PropTypes from 'prop-types';
|
2018-08-13 10:19:32 +00:00
|
|
|
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
|
2018-07-11 15:23:16 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal dependencies
|
|
|
|
*/
|
|
|
|
import D3Chart from './charts';
|
|
|
|
import Legend from './legend';
|
2018-08-08 11:00:45 +00:00
|
|
|
import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss';
|
2018-07-11 15:23:16 +00:00
|
|
|
|
2018-08-08 11:00:45 +00:00
|
|
|
const WIDE_BREAKPOINT = 1100;
|
2018-07-11 15:23:16 +00:00
|
|
|
|
2018-08-07 11:57:45 +00:00
|
|
|
function getOrderedKeys( data ) {
|
|
|
|
return [
|
|
|
|
...new Set(
|
|
|
|
data.reduce( ( accum, curr ) => {
|
|
|
|
Object.keys( curr ).forEach( key => key !== 'date' && accum.push( key ) );
|
|
|
|
return accum;
|
|
|
|
}, [] )
|
|
|
|
),
|
|
|
|
]
|
|
|
|
.map( key => ( {
|
|
|
|
key,
|
|
|
|
total: data.reduce( ( a, c ) => a + c[ key ], 0 ),
|
2018-08-08 11:00:45 +00:00
|
|
|
visible: true,
|
|
|
|
focus: true,
|
2018-08-07 11:57:45 +00:00
|
|
|
} ) )
|
|
|
|
.sort( ( a, b ) => b.total - a.total );
|
|
|
|
}
|
|
|
|
|
2018-08-31 17:27:21 +00:00
|
|
|
/**
|
|
|
|
* A chart container using d3, to display timeseries data with an interactive legend.
|
|
|
|
*/
|
2018-08-01 09:10:36 +00:00
|
|
|
class Chart extends Component {
|
|
|
|
constructor( props ) {
|
|
|
|
super( props );
|
2018-08-08 11:00:45 +00:00
|
|
|
this.chartRef = createRef();
|
|
|
|
const wpBody = document.getElementById( 'wpbody' ).getBoundingClientRect().width;
|
|
|
|
const wpWrap = document.getElementById( 'wpwrap' ).getBoundingClientRect().width;
|
|
|
|
const calcGap = wpWrap > 782 ? gaplarge.match( /\d+/ )[ 0 ] : gap.match( /\d+/ )[ 0 ];
|
2018-07-11 15:23:16 +00:00
|
|
|
this.state = {
|
2018-08-08 11:00:45 +00:00
|
|
|
data: props.data,
|
|
|
|
orderedKeys: getOrderedKeys( props.data ),
|
2018-08-12 17:01:10 +00:00
|
|
|
visibleData: [ ...props.data ],
|
2018-08-08 11:00:45 +00:00
|
|
|
width: wpBody - 2 * calcGap,
|
2018-07-11 15:23:16 +00:00
|
|
|
};
|
2018-08-01 09:10:36 +00:00
|
|
|
this.handleLegendToggle = this.handleLegendToggle.bind( this );
|
|
|
|
this.handleLegendHover = this.handleLegendHover.bind( this );
|
|
|
|
this.updateDimensions = this.updateDimensions.bind( this );
|
2018-08-12 17:01:10 +00:00
|
|
|
this.getVisibleData = this.getVisibleData.bind( this );
|
2018-07-11 15:23:16 +00:00
|
|
|
}
|
|
|
|
|
2018-08-08 11:00:45 +00:00
|
|
|
componentDidUpdate( prevProps ) {
|
|
|
|
const { data } = this.props;
|
2018-08-12 17:01:10 +00:00
|
|
|
const orderedKeys = getOrderedKeys( data );
|
2018-08-08 11:00:45 +00:00
|
|
|
if ( ! isEqual( [ ...data ].sort(), [ ...prevProps.data ].sort() ) ) {
|
|
|
|
/* eslint-disable react/no-did-update-set-state */
|
|
|
|
this.setState( {
|
2018-08-12 17:01:10 +00:00
|
|
|
orderedKeys: orderedKeys,
|
|
|
|
visibleData: this.getVisibleData( data, orderedKeys ),
|
2018-08-08 11:00:45 +00:00
|
|
|
} );
|
|
|
|
/* eslint-enable react/no-did-update-set-state */
|
2018-08-07 11:57:45 +00:00
|
|
|
}
|
2018-07-11 15:23:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
window.addEventListener( 'resize', this.updateDimensions );
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
window.removeEventListener( 'resize', this.updateDimensions );
|
|
|
|
}
|
|
|
|
|
|
|
|
handleLegendToggle( event ) {
|
2018-08-12 17:01:10 +00:00
|
|
|
const { data } = this.props;
|
|
|
|
const orderedKeys = this.state.orderedKeys.map( d => ( {
|
|
|
|
...d,
|
|
|
|
visible: d.key === event.target.id ? ! d.visible : d.visible,
|
|
|
|
} ) );
|
2018-09-07 09:21:52 +00:00
|
|
|
const copyEvent = { ...event }; // can't pass a synthetic event into the hover handler
|
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
orderedKeys,
|
|
|
|
visibleData: this.getVisibleData( data, orderedKeys ),
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
this.handleLegendHover( copyEvent );
|
|
|
|
}
|
|
|
|
);
|
2018-07-11 15:23:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
handleLegendHover( event ) {
|
2018-09-07 09:21:52 +00:00
|
|
|
const hoverTarget = this.state.orderedKeys.filter( d => d.key === event.target.id )[ 0 ];
|
2018-07-11 15:23:16 +00:00
|
|
|
this.setState( {
|
|
|
|
orderedKeys: this.state.orderedKeys.map( d => {
|
2018-09-07 09:21:52 +00:00
|
|
|
let enterFocus = d.key === event.target.id ? true : false;
|
|
|
|
enterFocus = ! hoverTarget.visible ? true : enterFocus;
|
2018-07-11 15:23:16 +00:00
|
|
|
return {
|
|
|
|
...d,
|
|
|
|
focus: event.type === 'mouseleave' || event.type === 'blur' ? true : enterFocus,
|
|
|
|
};
|
|
|
|
} ),
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
updateDimensions() {
|
|
|
|
this.setState( {
|
2018-08-08 11:00:45 +00:00
|
|
|
width: this.chartRef.current.offsetWidth,
|
2018-07-11 15:23:16 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2018-08-12 17:01:10 +00:00
|
|
|
getVisibleData( data, orderedKeys ) {
|
|
|
|
const visibleKeys = orderedKeys.filter( d => d.visible );
|
|
|
|
return data.map( d => {
|
|
|
|
const newRow = { date: d.date };
|
|
|
|
visibleKeys.forEach( row => {
|
|
|
|
newRow[ row.key ] = d[ row.key ];
|
|
|
|
} );
|
|
|
|
return newRow;
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2018-07-11 15:23:16 +00:00
|
|
|
render() {
|
2018-08-12 17:01:10 +00:00
|
|
|
const { orderedKeys, visibleData, width } = this.state;
|
2018-08-08 11:00:45 +00:00
|
|
|
const legendDirection = orderedKeys.length <= 2 && width > WIDE_BREAKPOINT ? 'row' : 'column';
|
|
|
|
const chartDirection = orderedKeys.length > 2 && width > WIDE_BREAKPOINT ? 'row' : 'column';
|
2018-07-11 15:23:16 +00:00
|
|
|
const legend = (
|
|
|
|
<Legend
|
2018-08-08 11:00:45 +00:00
|
|
|
className={ 'woocommerce-chart__legend' }
|
2018-08-13 10:19:32 +00:00
|
|
|
colorScheme={ d3InterpolateViridis }
|
2018-08-01 09:10:36 +00:00
|
|
|
data={ orderedKeys }
|
2018-07-11 15:23:16 +00:00
|
|
|
handleLegendHover={ this.handleLegendHover }
|
|
|
|
handleLegendToggle={ this.handleLegendToggle }
|
|
|
|
legendDirection={ legendDirection }
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
const margin = {
|
|
|
|
bottom: 50,
|
2018-09-04 11:31:18 +00:00
|
|
|
left: 80,
|
2018-09-04 13:03:46 +00:00
|
|
|
right: 30,
|
2018-07-11 15:23:16 +00:00
|
|
|
top: 0,
|
|
|
|
};
|
|
|
|
return (
|
2018-08-08 11:00:45 +00:00
|
|
|
<div className="woocommerce-chart" ref={ this.chartRef }>
|
|
|
|
<div className="woocommerce-chart__header">
|
2018-08-13 11:09:44 +00:00
|
|
|
<span className="woocommerce-chart__title">{ this.props.title }</span>
|
2018-08-08 11:00:45 +00:00
|
|
|
{ width > WIDE_BREAKPOINT && legendDirection === 'row' && legend }
|
2018-08-01 09:10:36 +00:00
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
className={ classNames(
|
2018-08-08 11:00:45 +00:00
|
|
|
'woocommerce-chart__body',
|
|
|
|
`woocommerce-chart__body-${ chartDirection }`
|
2018-08-01 09:10:36 +00:00
|
|
|
) }
|
|
|
|
>
|
2018-08-08 11:00:45 +00:00
|
|
|
{ width > WIDE_BREAKPOINT && legendDirection === 'column' && legend }
|
2018-08-01 09:10:36 +00:00
|
|
|
<D3Chart
|
2018-08-13 10:19:32 +00:00
|
|
|
colorScheme={ d3InterpolateViridis }
|
2018-08-12 17:01:10 +00:00
|
|
|
data={ visibleData }
|
2018-08-01 09:10:36 +00:00
|
|
|
height={ 300 }
|
|
|
|
margin={ margin }
|
|
|
|
orderedKeys={ orderedKeys }
|
2018-09-05 20:52:35 +00:00
|
|
|
tooltipFormat={ this.props.tooltipFormat }
|
|
|
|
type={ this.props.type }
|
2018-08-08 11:00:45 +00:00
|
|
|
width={ chartDirection === 'row' ? width - 320 : width }
|
2018-09-05 20:52:35 +00:00
|
|
|
xFormat={ this.props.xFormat }
|
2018-09-07 10:28:02 +00:00
|
|
|
x2Format={ this.props.x2Format }
|
2018-09-05 20:52:35 +00:00
|
|
|
yFormat={ this.props.yFormat }
|
2018-08-01 09:10:36 +00:00
|
|
|
/>
|
|
|
|
</div>
|
2018-08-08 11:00:45 +00:00
|
|
|
{ width < WIDE_BREAKPOINT && <div className="woocommerce-chart__footer">{ legend }</div> }
|
2018-08-01 09:10:36 +00:00
|
|
|
</div>
|
2018-07-11 15:23:16 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-01 09:10:36 +00:00
|
|
|
Chart.propTypes = {
|
2018-08-31 17:27:21 +00:00
|
|
|
/**
|
|
|
|
* An array of data.
|
|
|
|
*/
|
2018-08-01 09:10:36 +00:00
|
|
|
data: PropTypes.array.isRequired,
|
2018-08-31 17:27:21 +00:00
|
|
|
/**
|
|
|
|
* A title describing this chart.
|
|
|
|
*/
|
2018-08-01 09:10:36 +00:00
|
|
|
title: PropTypes.string,
|
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`.
|
|
|
|
*/
|
|
|
|
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
|
|
|
|
/**
|
|
|
|
* A datetime formatting string, passed to d3TimeFormat.
|
|
|
|
*/
|
|
|
|
xFormat: PropTypes.string,
|
2018-09-07 10:28:02 +00:00
|
|
|
/**
|
|
|
|
* A datetime formatting string, passed to d3TimeFormat.
|
|
|
|
*/
|
|
|
|
x2Format: PropTypes.string,
|
2018-09-05 20:52:35 +00:00
|
|
|
/**
|
|
|
|
* A number formatting string, passed to d3Format.
|
|
|
|
*/
|
|
|
|
yFormat: PropTypes.string,
|
2018-08-01 09:10:36 +00:00
|
|
|
};
|
|
|
|
|
2018-08-08 11:00:45 +00:00
|
|
|
Chart.defaultProps = {
|
|
|
|
data: [],
|
2018-09-05 20:52:35 +00:00
|
|
|
tooltipFormat: '%Y-%m-%d',
|
2018-09-07 10:28:02 +00:00
|
|
|
xFormat: '%d',
|
|
|
|
x2Format: '%b %Y',
|
2018-09-07 12:39:31 +00:00
|
|
|
yFormat: '$.3s',
|
2018-08-08 11:00:45 +00:00
|
|
|
};
|
|
|
|
|
2018-08-01 09:10:36 +00:00
|
|
|
export default Chart;
|