/** @format */ /** * External dependencies */ import classNames from 'classnames'; 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'; import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss'; const WIDE_BREAKPOINT = 1100; 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 ), visible: true, focus: true, } ) ) .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 ); 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 = { data: props.data, orderedKeys: getOrderedKeys( props.data ), visibleData: [ ...props.data ], width: wpBody - 2 * calcGap, }; this.handleLegendToggle = this.handleLegendToggle.bind( this ); this.handleLegendHover = this.handleLegendHover.bind( this ); this.updateDimensions = this.updateDimensions.bind( this ); this.getVisibleData = this.getVisibleData.bind( this ); } componentDidUpdate( prevProps ) { const { data } = this.props; const orderedKeys = getOrderedKeys( data ); if ( ! isEqual( [ ...data ].sort(), [ ...prevProps.data ].sort() ) ) { /* eslint-disable react/no-did-update-set-state */ this.setState( { orderedKeys: orderedKeys, visibleData: this.getVisibleData( data, orderedKeys ), } ); /* eslint-enable react/no-did-update-set-state */ } } componentDidMount() { window.addEventListener( 'resize', this.updateDimensions ); } componentWillUnmount() { window.removeEventListener( 'resize', this.updateDimensions ); } handleLegendToggle( event ) { const { data } = this.props; const orderedKeys = this.state.orderedKeys.map( d => ( { ...d, visible: d.key === event.target.id ? ! d.visible : d.visible, } ) ); 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 ) { const hoverTarget = this.state.orderedKeys.filter( d => d.key === event.target.id )[ 0 ]; this.setState( { orderedKeys: this.state.orderedKeys.map( d => { 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( { width: this.chartRef.current.offsetWidth, } ); } 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() { const { orderedKeys, visibleData, width } = this.state; const legendDirection = orderedKeys.length <= 2 && width > WIDE_BREAKPOINT ? 'row' : 'column'; const chartDirection = orderedKeys.length > 2 && width > WIDE_BREAKPOINT ? 'row' : 'column'; const legend = ( ); const margin = { bottom: 50, left: 80, right: 30, top: 0, }; return (
{ this.props.title } { width > WIDE_BREAKPOINT && legendDirection === 'row' && legend }
{ width > WIDE_BREAKPOINT && legendDirection === 'column' && legend }
{ width < WIDE_BREAKPOINT &&
{ legend }
}
); } } Chart.propTypes = { /** * An array of data. */ data: PropTypes.array.isRequired, /** * A title describing this chart. */ title: PropTypes.string, /** * 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 datetime formatting string, passed to d3TimeFormat. */ x2Format: PropTypes.string, /** * A number formatting string, passed to d3Format. */ yFormat: PropTypes.string, }; Chart.defaultProps = { data: [], tooltipFormat: '%Y-%m-%d', xFormat: '%d', x2Format: '%b %Y', yFormat: '.3s', }; export default Chart;