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

217 lines
5.8 KiB
JavaScript
Raw Normal View History

/** @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';
import PropTypes from 'prop-types';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
/**
* 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-08-08 11:00:45 +00:00
const WIDE_BREAKPOINT = 1100;
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 );
}
/**
* A chart container using d3, to display timeseries data with an interactive legend.
*/
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 ];
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,
};
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-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
}
}
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 );
}
);
}
handleLegendHover( event ) {
2018-09-07 09:21:52 +00:00
const hoverTarget = this.state.orderedKeys.filter( d => d.key === event.target.id )[ 0 ];
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;
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-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;
} );
}
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';
const legend = (
<Legend
2018-08-08 11:00:45 +00:00
className={ 'woocommerce-chart__legend' }
colorScheme={ d3InterpolateViridis }
data={ orderedKeys }
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,
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 }
</div>
<div
className={ classNames(
2018-08-08 11:00:45 +00:00
'woocommerce-chart__body',
`woocommerce-chart__body-${ chartDirection }`
) }
>
2018-08-08 11:00:45 +00:00
{ width > WIDE_BREAKPOINT && legendDirection === 'column' && legend }
<D3Chart
colorScheme={ d3InterpolateViridis }
2018-08-12 17:01:10 +00:00
data={ visibleData }
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 }
yFormat={ this.props.yFormat }
/>
</div>
2018-08-08 11:00:45 +00:00
{ width < WIDE_BREAKPOINT && <div className="woocommerce-chart__footer">{ legend }</div> }
</div>
);
}
}
Chart.propTypes = {
/**
* An array of data.
*/
data: PropTypes.array.isRequired,
/**
* A title describing this chart.
*/
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,
/**
* A number formatting string, passed to d3Format.
*/
yFormat: PropTypes.string,
};
2018-08-08 11:00:45 +00:00
Chart.defaultProps = {
data: [],
2018-09-05 20:52:35 +00:00
tooltipFormat: '%Y-%m-%d',
xFormat: '%Y-%m-%d',
yFormat: '.3s',
2018-08-08 11:00:45 +00:00
};
export default Chart;