/** @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, utcParse as d3UTCParse } 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, getXTicks, 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 ), type: props.type, width: props.width, }; this.tooltipRef = createRef(); } componentDidUpdate( prevProps, prevState ) { const { type, 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 } ); } if ( type !== prevProps.type ) { this.setState( { type } ); } /* 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, dateParser, height, layout, margin, orderedKeys, tooltipFormat, type, xFormat, x2Format, 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 parseDate = d3UTCParse( dateParser ); const uniqueDates = getUniqueDates( lineData, parseDate ); const xLineScale = getXLineScale( uniqueDates, adjWidth ); const xScale = getXScale( uniqueDates, adjWidth ); const xTicks = getXTicks( uniqueDates, adjWidth, layout ); return { colorScheme, dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ), height: calculatedHeight, line: getLine( xLineScale, yScale ), lineData, margin, orderedKeys: newOrderedKeys, parseDate, scale, tooltipFormat: d3TimeFormat( tooltipFormat ), type, uniqueDates, uniqueKeys, width: calculatedWidth, xFormat: d3TimeFormat( xFormat ), x2Format: d3TimeFormat( x2Format ), xGroupScale: getXGroupScale( orderedKeys, xScale ), xLineScale, xTicks, xScale, yMax, yScale, yTickOffset: getYTickOffset( adjHeight, scale, yMax ), yFormat, }; } render() { if ( ! this.props.data ) { return null; // TODO: improve messaging } return (