Merge pull request woocommerce/woocommerce-admin#270 from woocommerce/add/chart-card-components

D3 Chart Component: split example and index

Managed to address https://github.com/woocommerce/wc-admin/pull/270#issuecomment-412011428 in this PR and did a bit of additional refactoring.
This commit is contained in:
Robert Elliott 2018-08-13 12:42:47 +02:00 committed by GitHub
commit 09f33dd1fa
11 changed files with 3967 additions and 3887 deletions

View File

@ -3,9 +3,8 @@
/** /**
* External dependencies * External dependencies
*/ */
import { isEqual } from 'lodash';
import React from 'react'; import { Component, createRef } from '@wordpress/element';
import { Component } from '@wordpress/element';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { format as d3Format } from 'd3-format'; import { format as d3Format } from 'd3-format';
@ -21,7 +20,6 @@ import {
drawAxis, drawAxis,
drawBars, drawBars,
drawLines, drawLines,
getColorScale,
getDateSpaces, getDateSpaces,
getOrderedKeys, getOrderedKeys,
getLine, getLine,
@ -37,52 +35,33 @@ import {
} from './utils'; } from './utils';
class D3Chart extends Component { class D3Chart extends Component {
static propTypes = { constructor( props ) {
className: PropTypes.string, super( props );
data: PropTypes.array.isRequired, this.getAllData = this.getAllData.bind( this );
height: PropTypes.number, this.state = {
legend: PropTypes.array, allData: this.getAllData( props ),
margin: PropTypes.shape( { width: props.width,
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
} ),
orderedKeys: PropTypes.array,
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
width: PropTypes.number,
xFormat: PropTypes.string,
yFormat: PropTypes.string,
}; };
this.tooltipRef = createRef();
static defaultProps = {
height: 200,
margin: {
bottom: 30,
left: 40,
right: 0,
top: 20,
},
type: 'line',
width: 600,
xFormat: '%Y-%m-%d',
yFormat: ',.0f',
};
state = {
allData: null,
};
tooltipRef = React.createRef();
static getDerivedStateFromProps( nextProps, prevState ) {
const nextAllData = [ ...nextProps.data, ...nextProps.orderedKeys ];
if ( prevState.allData !== nextAllData ) {
return { allData: nextAllData };
} }
return null; 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 ) => { drawChart = ( node, params ) => {
@ -97,8 +76,7 @@ class D3Chart extends Component {
width: params.width - margin.left - margin.right, width: params.width - margin.left - margin.right,
tooltip: d3Select( this.tooltipRef.current ), tooltip: d3Select( this.tooltipRef.current ),
} ); } );
drawAxis( g, adjParams );
drawAxis( g, data, adjParams );
type === 'line' && drawLines( g, data, adjParams ); type === 'line' && drawLines( g, data, adjParams );
type === 'bar' && drawBars( g, data, adjParams ); type === 'bar' && drawBars( g, data, adjParams );
@ -106,25 +84,26 @@ class D3Chart extends Component {
}; };
getParams = node => { getParams = node => {
const { data, height, margin, orderedKeys, type, width, xFormat, yFormat } = this.props; const { colorScheme, data, height, margin, orderedKeys, type, xFormat, yFormat } = this.props;
const { width } = this.state;
const calculatedWidth = width || node.offsetWidth; const calculatedWidth = width || node.offsetWidth;
const calculatedHeight = height || node.offsetHeight; const calculatedHeight = height || node.offsetHeight;
const scale = width / node.offsetWidth; const scale = width / node.offsetWidth;
const adjHeight = calculatedHeight - margin.top - margin.bottom; const adjHeight = calculatedHeight - margin.top - margin.bottom;
const adjWidth = calculatedWidth - margin.left - margin.right; const adjWidth = calculatedWidth - margin.left - margin.right;
const uniqueKeys = getUniqueKeys( data ); const uniqueKeys = getUniqueKeys( data );
const newOrderedKeys = orderedKeys ? orderedKeys : getOrderedKeys( data, uniqueKeys ); const newOrderedKeys = orderedKeys || getOrderedKeys( data, uniqueKeys );
const lineData = getLineData( data, orderedKeys ); const lineData = getLineData( data, newOrderedKeys );
const yMax = getYMax( lineData ); const yMax = getYMax( lineData );
const yScale = getYScale( adjHeight, yMax ); const yScale = getYScale( adjHeight, yMax );
const uniqueDates = getUniqueDates( lineData ); const uniqueDates = getUniqueDates( lineData );
const xLineScale = getXLineScale( uniqueDates, adjWidth ); const xLineScale = getXLineScale( uniqueDates, adjWidth );
const xScale = getXScale( uniqueDates, adjWidth ); const xScale = getXScale( uniqueDates, adjWidth );
return { return {
colorScale: getColorScale( orderedKeys ), colorScheme,
dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ), dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ),
height: calculatedHeight, height: calculatedHeight,
line: getLine( data, xLineScale, yScale ), line: getLine( xLineScale, yScale ),
lineData, lineData,
margin, margin,
orderedKeys: newOrderedKeys, orderedKeys: newOrderedKeys,
@ -148,14 +127,17 @@ class D3Chart extends Component {
if ( ! this.props.data ) { if ( ! this.props.data ) {
return null; // TODO: improve messaging return null; // TODO: improve messaging
} }
return ( return (
<div className={ classNames( 'woocommerce-chart__container', this.props.className ) }> <div
className={ classNames( 'woocommerce-chart__container', this.props.className ) }
style={ { height: this.props.height } }
>
<D3Base <D3Base
className={ classNames( this.props.className ) } className={ classNames( this.props.className ) }
data={ this.state.allData } data={ this.state.allData }
drawChart={ this.drawChart } drawChart={ this.drawChart }
getParams={ this.getParams } getParams={ this.getParams }
width={ this.state.width }
/> />
<div className="tooltip" ref={ this.tooltipRef } /> <div className="tooltip" ref={ this.tooltipRef } />
</div> </div>
@ -163,4 +145,38 @@ class D3Chart extends Component {
} }
} }
D3Chart.propTypes = {
colorScheme: PropTypes.func,
className: PropTypes.string,
data: PropTypes.array.isRequired,
height: PropTypes.number,
legend: PropTypes.array,
margin: PropTypes.shape( {
bottom: PropTypes.number,
left: PropTypes.number,
right: PropTypes.number,
top: PropTypes.number,
} ),
orderedKeys: PropTypes.array,
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
width: PropTypes.number,
xFormat: PropTypes.string,
yFormat: PropTypes.string,
};
D3Chart.defaultProps = {
data: [],
height: 200,
margin: {
bottom: 30,
left: 40,
right: 0,
top: 20,
},
type: 'line',
width: 600,
xFormat: '%Y-%m-%d',
yFormat: ',.0f',
};
export default D3Chart; export default D3Chart;

View File

@ -5,8 +5,8 @@
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import { Component, createRef } from '@wordpress/element';
import { isEmpty } from 'lodash'; import { isEmpty, isEqual } from 'lodash';
import { select as d3Select } from 'd3-selection'; import { select as d3Select } from 'd3-selection';
/** /**
@ -38,25 +38,30 @@ export default class D3Base extends Component {
params: null, params: null,
drawChart: null, drawChart: null,
getParams: null, getParams: null,
width: null,
}; };
chartRef = React.createRef(); chartRef = createRef();
static getDerivedStateFromProps( nextProps, prevState ) { static getDerivedStateFromProps( nextProps, prevState ) {
let state = {}; let state = {};
if ( nextProps.data !== prevState.data ) { if ( ! isEqual( nextProps.data, prevState.data ) ) {
state = { ...state, data: nextProps.data }; state = { ...state, data: nextProps.data };
} }
if ( nextProps.drawChart !== prevState.drawChart ) { if ( ! isEqual( nextProps.drawChart, prevState.drawChart ) ) {
state = { ...state, drawChart: nextProps.drawChart }; state = { ...state, drawChart: nextProps.drawChart };
} }
if ( nextProps.getParams !== prevState.getParams ) { if ( ! isEqual( nextProps.getParams, prevState.getParams ) ) {
state = { ...state, getParams: nextProps.getParams }; state = { ...state, getParams: nextProps.getParams };
} }
if ( nextProps.width !== prevState.width ) {
state = { ...state, width: nextProps.width };
}
if ( ! isEmpty( state ) ) { if ( ! isEmpty( state ) ) {
return { ...state, params: null }; return { ...state, params: null };
} }
@ -72,8 +77,9 @@ export default class D3Base extends Component {
shouldComponentUpdate( nextProps, nextState ) { shouldComponentUpdate( nextProps, nextState ) {
return ( return (
( nextState.params !== null && this.state.params !== nextState.params ) || ( nextState.params !== null && ! isEqual( this.state.params, nextState.params ) ) ||
this.state.data !== nextState.data ! isEqual( this.state.data, nextState.data ) ||
this.state.width !== nextState.width
); );
} }

View File

@ -0,0 +1,103 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
/**
* Internal dependencies
*/
import Card from 'components/card';
import Chart from 'components/chart';
import dummyOrders from './test/fixtures/dummy';
class WidgetCharts extends Component {
constructor() {
super( ...arguments );
this.handleChange = this.handleChange.bind( this );
this.getSomeOrders = this.getSomeOrders.bind( this );
const products = [
{
key: 'date',
selected: true,
},
{
key: 'Cap',
selected: true,
},
{
key: 'T-Shirt',
selected: true,
},
{
key: 'Sunglasses',
selected: true,
},
{
key: 'Polo',
selected: true,
},
{
key: 'Hoodie',
selected: true,
},
];
const someOrders = this.getSomeOrders( products );
this.state = {
products,
someOrders,
};
}
getSomeOrders( products ) {
return dummyOrders.map( d => {
return Object.keys( d )
.filter( key =>
products
.filter( k => k.selected )
.map( k => k.key )
.includes( key )
)
.reduce( ( accum, current ) => ( { ...accum, [ current ]: d[ current ] } ), {} );
} );
}
handleChange( event ) {
const products = this.state.products.map( d => ( {
...d,
selected: d.key === event.target.id ? ! d.selected : d.selected,
} ) );
const someOrders = this.getSomeOrders( products );
this.setState( { products, someOrders } );
}
render() {
return (
<Fragment>
<Card title={ __( 'Test Categories', 'wc-admin' ) }>
<ul>
{ this.state.products.map( d => (
<li key={ d.key } style={ { display: 'inline', marginRight: '12px' } }>
<label htmlFor={ d.key }>
<input
id={ d.key }
type="checkbox"
onChange={ this.handleChange }
checked={ d.selected }
/>{' '}
{ d.key }
</label>
</li>
) ) }
</ul>
</Card>
<Card title={ __( 'Store Charts', 'wc-admin' ) }>
<Chart data={ this.state.someOrders } title="Example Chart" />
</Card>
</Fragment>
);
}
}
export default WidgetCharts;

View File

@ -2,90 +2,22 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n';
import classNames from 'classnames'; import classNames from 'classnames';
import { Component, Fragment } from '@wordpress/element'; 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 * Internal dependencies
*/ */
import Card from 'components/card';
import D3Chart from './charts'; import D3Chart from './charts';
import dummyOrders from './test/fixtures/dummy';
import Legend from './legend'; import Legend from './legend';
import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss';
const WIDE_BREAKPOINT = 1130; const WIDE_BREAKPOINT = 1100;
class WidgetCharts extends Component { function getOrderedKeys( data ) {
constructor() {
super( ...arguments );
this.getOrderedKeys = this.getOrderedKeys.bind( this );
this.handleLegendHover = this.handleLegendHover.bind( this );
this.handleLegendToggle = this.handleLegendToggle.bind( this );
this.updateDimensions = this.updateDimensions.bind( this );
this.handleChange = this.handleChange.bind( this );
this.getSomeOrders = this.getSomeOrders.bind( this );
const products = [
{
key: 'date',
selected: true,
},
{
key: 'Cap',
selected: true,
},
{
key: 'T-Shirt',
selected: true,
},
{
key: 'Sunglasses',
selected: true,
},
{
key: 'Polo',
selected: true,
},
{
key: 'Hoodie',
selected: true,
},
];
const someOrders = this.getSomeOrders( products );
this.state = {
products,
orderedKeys: this.getOrderedKeys( someOrders ).map( d => ( {
...d,
visible: true,
focus: true,
} ) ),
someOrders,
bodyWidth: document.getElementById( 'wpbody' ).getBoundingClientRect().width,
};
}
getSomeOrders( products ) {
return dummyOrders.map( d => {
return Object.keys( d )
.filter( key =>
products
.filter( k => k.selected )
.map( k => k.key )
.includes( key )
)
.reduce( ( accum, current ) => ( { ...accum, [ current ]: d[ current ] } ), {} );
} );
}
componentDidMount() {
window.addEventListener( 'resize', this.updateDimensions );
}
componentWillUnmount() {
window.removeEventListener( 'resize', this.updateDimensions );
}
getOrderedKeys( data ) {
return [ return [
...new Set( ...new Set(
data.reduce( ( accum, curr ) => { data.reduce( ( accum, curr ) => {
@ -97,30 +29,61 @@ class WidgetCharts extends Component {
.map( key => ( { .map( key => ( {
key, key,
total: data.reduce( ( a, c ) => a + c[ key ], 0 ), total: data.reduce( ( a, c ) => a + c[ key ], 0 ),
visible: true,
focus: true,
} ) ) } ) )
.sort( ( a, b ) => b.total - a.total ); .sort( ( a, b ) => b.total - a.total );
} }
handleChange( event ) { class Chart extends Component {
const products = this.state.products.map( d => ( { constructor( props ) {
...d, super( props );
selected: d.key === event.target.id ? ! d.selected : d.selected, this.chartRef = createRef();
} ) ); const wpBody = document.getElementById( 'wpbody' ).getBoundingClientRect().width;
const someOrders = this.getSomeOrders( products ); const wpWrap = document.getElementById( 'wpwrap' ).getBoundingClientRect().width;
const orderedKeys = this.getOrderedKeys( someOrders ).map( d => ( { const calcGap = wpWrap > 782 ? gaplarge.match( /\d+/ )[ 0 ] : gap.match( /\d+/ )[ 0 ];
...d, this.state = {
visible: true, data: props.data,
focus: true, orderedKeys: getOrderedKeys( props.data ),
} ) ); visibleData: [ ...props.data ],
this.setState( { products, orderedKeys, someOrders } ); 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 ) { handleLegendToggle( event ) {
this.setState( { const { data } = this.props;
orderedKeys: this.state.orderedKeys.map( d => ( { const orderedKeys = this.state.orderedKeys.map( d => ( {
...d, ...d,
visible: d.key === event.target.id ? ! d.visible : d.visible, visible: d.key === event.target.id ? ! d.visible : d.visible,
} ) ), } ) );
this.setState( {
orderedKeys,
visibleData: this.getVisibleData( data, orderedKeys ),
} ); } );
} }
@ -138,23 +101,30 @@ class WidgetCharts extends Component {
updateDimensions() { updateDimensions() {
this.setState( { this.setState( {
bodyWidth: document.getElementById( 'wpbody' ).getBoundingClientRect().width, 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() { render() {
const legendDirection = const { orderedKeys, visibleData, width } = this.state;
this.state.orderedKeys.length <= 2 && this.state.bodyWidth > WIDE_BREAKPOINT const legendDirection = orderedKeys.length <= 2 && width > WIDE_BREAKPOINT ? 'row' : 'column';
? 'row' const chartDirection = orderedKeys.length > 2 && width > WIDE_BREAKPOINT ? 'row' : 'column';
: 'column';
const chartDirection =
this.state.orderedKeys.length > 2 && this.state.bodyWidth > WIDE_BREAKPOINT
? 'row'
: 'column';
const legend = ( const legend = (
<Legend <Legend
className={ 'woocommerce_dashboard__widget-legend' } className={ 'woocommerce-chart__legend' }
data={ this.state.orderedKeys } colorScheme={ d3InterpolateViridis }
data={ orderedKeys }
handleLegendHover={ this.handleLegendHover } handleLegendHover={ this.handleLegendHover }
handleLegendToggle={ this.handleLegendToggle } handleLegendToggle={ this.handleLegendToggle }
legendDirection={ legendDirection } legendDirection={ legendDirection }
@ -163,57 +133,44 @@ class WidgetCharts extends Component {
const margin = { const margin = {
bottom: 50, bottom: 50,
left: 50, left: 50,
right: 20, right: 10,
top: 0, top: 0,
}; };
return ( return (
<Fragment> <div className="woocommerce-chart" ref={ this.chartRef }>
<Card title={ __( 'Test Categories', 'wc-admin' ) }> <div className="woocommerce-chart__header">
<ul> { width > WIDE_BREAKPOINT && legendDirection === 'row' && legend }
{ this.state.products.map( d => (
<li key={ d.key } style={ { display: 'inline', marginRight: '12px' } }>
<label htmlFor={ d.key }>
<input
id={ d.key }
type="checkbox"
onChange={ this.handleChange }
checked={ d.selected }
/>{' '}
{ d.key }
</label>
</li>
) ) }
</ul>
</Card>
<Card title={ __( 'Store Charts', 'wc-admin' ) }>
<div className="woocommerce-dashboard__widget woocommerce-dashboard__widget-chart">
<div className="woocommerce-dashboard__widget-chart-header">
{ this.state.bodyWidth > WIDE_BREAKPOINT && legendDirection === 'row' && legend }
</div> </div>
<div <div
className={ classNames( className={ classNames(
'woocommerce-dashboard__widget-chart-body', 'woocommerce-chart__body',
`woocommerce-dashboard__widget-chart-body-${ chartDirection }` `woocommerce-chart__body-${ chartDirection }`
) } ) }
> >
{ this.state.bodyWidth > WIDE_BREAKPOINT && legendDirection === 'column' && legend } { width > WIDE_BREAKPOINT && legendDirection === 'column' && legend }
<D3Chart <D3Chart
data={ this.state.someOrders } colorScheme={ d3InterpolateViridis }
data={ visibleData }
height={ 300 } height={ 300 }
margin={ margin } margin={ margin }
orderedKeys={ this.state.orderedKeys } orderedKeys={ orderedKeys }
type={ 'line' } type={ 'line' }
width={ chartDirection === 'row' ? 722 : 1042 } width={ chartDirection === 'row' ? width - 320 : width }
/> />
</div> </div>
{ this.state.bodyWidth < WIDE_BREAKPOINT && ( { width < WIDE_BREAKPOINT && <div className="woocommerce-chart__footer">{ legend }</div> }
<div className="woocommerce-dashboard__widget-chart-footer">{ legend }</div>
) }
</div> </div>
</Card>
</Fragment>
); );
} }
} }
export default WidgetCharts; Chart.propTypes = {
data: PropTypes.array.isRequired,
title: PropTypes.string,
};
Chart.defaultProps = {
data: [],
};
export default Chart;

View File

@ -5,20 +5,27 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { Component } from '@wordpress/element'; import { Component } from '@wordpress/element';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { scaleOrdinal as d3ScaleOrdinal } from 'd3-scale';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
import { range as d3Range } from 'd3-array';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './style.scss'; import './style.scss';
import { formatCurrency } from 'lib/currency'; import { formatCurrency } from 'lib/currency';
import { getColor } from './utils';
class Legend extends Component { class Legend extends Component {
render() { render() {
const { data, handleLegendHover, handleLegendToggle, legendDirection } = this.props; const {
const d3Color = d3ScaleOrdinal().range( d3Range( 0, 1.1, 100 / ( data.length - 1 ) / 100 ) ); colorScheme,
data,
handleLegendHover,
handleLegendToggle,
legendDirection,
} = this.props;
const colorParams = {
orderedKeys: data,
colorScheme,
};
return ( return (
<ul <ul
className={ classNames( className={ classNames(
@ -27,7 +34,7 @@ class Legend extends Component {
this.props.className this.props.className
) } ) }
> >
{ data.map( ( row, i ) => ( { data.map( row => (
<li <li
className="woocommerce-legend__item" className="woocommerce-legend__item"
key={ row.key } key={ row.key }
@ -44,7 +51,7 @@ class Legend extends Component {
'woocommerce-legend__item-checkmark-checked': row.visible, 'woocommerce-legend__item-checkmark-checked': row.visible,
} ) } } ) }
id={ row.key } id={ row.key }
style={ { backgroundColor: d3InterpolateViridis( d3Color( i ) ) } } style={ { backgroundColor: getColor( row.key, colorParams ) } }
/> />
<span className="woocommerce-legend__item-title" id={ row.key }> <span className="woocommerce-legend__item-title" id={ row.key }>
{ row.key } { row.key }
@ -63,6 +70,7 @@ class Legend extends Component {
Legend.propTypes = { Legend.propTypes = {
className: PropTypes.string, className: PropTypes.string,
colorScheme: PropTypes.func,
data: PropTypes.array.isRequired, data: PropTypes.array.isRequired,
handleLegendToggle: PropTypes.func, handleLegendToggle: PropTypes.func,
handleLegendHover: PropTypes.func, handleLegendHover: PropTypes.func,

View File

@ -1,5 +1,5 @@
/** @format */ /** @format */
.woocommerce-dashboard__widget-chart { .woocommerce-chart {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
@ -7,24 +7,24 @@
margin: -20px; margin: -20px;
border-top: 1px solid $core-grey-light-200; border-top: 1px solid $core-grey-light-200;
.woocommerce-dashboard__widget-chart-header { .woocommerce-chart__header {
min-height: 50px; min-height: 50px;
border-top: 1px solid $core-grey-light-200; border-top: 1px solid $core-grey-light-200;
} }
.woocommerce-dashboard__widget-chart-body { .woocommerce-chart__body {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
width: 100%; width: 100%;
&.woocommerce-dashboard__widget-chart-body-column { &.woocommerce-chart__body-column {
flex-direction: column; flex-direction: column;
} }
} }
.woocommerce-dashboard__widget-chart-footer { .woocommerce-chart__footer {
width: 100%; width: 100%;
.woocommerce-legend { .woocommerce-legend {
@ -167,6 +167,7 @@
li { li {
button { button {
background-color: $white;
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -187,7 +188,6 @@
position: relative; position: relative;
padding: 3px 0 3px 24px; padding: 3px 0 3px 24px;
cursor: pointer; cursor: pointer;
font-family: 'SF UI Text';
font-size: 13px; font-size: 13px;
user-select: none; user-select: none;
width: 100%; width: 100%;

View File

@ -10,7 +10,6 @@
*/ */
import dummyOrders from './fixtures/dummy'; import dummyOrders from './fixtures/dummy';
import { import {
getColorScale,
getDateSpaces, getDateSpaces,
getOrderedKeys, getOrderedKeys,
getLineData, getLineData,
@ -156,16 +155,6 @@ describe( 'getXLineScale', () => {
} ); } );
} ); } );
describe( 'getColorScale', () => {
it( 'properly scale product keys into a range of colors', () => {
const testColorScale = getColorScale( testOrderedKeys );
testOrderedKeys.map( d => testColorScale( d.key ) ); // without this it fails! why? how?
expect( testColorScale( orderedKeys[ 0 ].key ) ).toEqual( 0 );
expect( testColorScale( orderedKeys[ 2 ].key ) ).toEqual( 0.5 );
expect( testColorScale( orderedKeys[ orderedKeys.length - 1 ].key ) ).toEqual( 1 );
} );
} );
describe( 'getYMax', () => { describe( 'getYMax', () => {
it( 'calculate the correct maximum y value', () => { it( 'calculate the correct maximum y value', () => {
expect( testYMax ).toEqual( 14139347 ); expect( testYMax ).toEqual( 14139347 );

View File

@ -4,18 +4,17 @@
* External dependencies * External dependencies
*/ */
import { max as d3Max, range as d3Range } from 'd3-array'; import { findIndex } from 'lodash';
import { max as d3Max } from 'd3-array';
import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis'; import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis';
import { format as d3Format } from 'd3-format'; import { format as d3Format } from 'd3-format';
import { import {
scaleBand as d3ScaleBand, scaleBand as d3ScaleBand,
scaleLinear as d3ScaleLinear, scaleLinear as d3ScaleLinear,
scaleOrdinal as d3ScaleOrdinal,
scaleTime as d3ScaleTime, scaleTime as d3ScaleTime,
} from 'd3-scale'; } from 'd3-scale';
import { mouse as d3Mouse, select as d3Select } from 'd3-selection'; import { mouse as d3Mouse, select as d3Select } from 'd3-selection';
import { line as d3Line } from 'd3-shape'; import { line as d3Line } from 'd3-shape';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format'; import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format';
export const parseDate = d3UTCParse( '%Y-%m-%d' ); export const parseDate = d3UTCParse( '%Y-%m-%d' );
@ -87,6 +86,14 @@ export const getUniqueDates = lineData => {
].sort( ( a, b ) => parseDate( a ) - parseDate( b ) ); ].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
}; };
export const getColor = ( key, params ) => {
const keyValue =
params.orderedKeys.length > 1
? findIndex( params.orderedKeys, d => d.key === key ) / ( params.orderedKeys.length - 1 )
: 0;
return params.colorScheme( keyValue );
};
/** /**
* Describes getXScale * Describes getXScale
* @param {array} uniqueDates - from `getUniqueDates` * @param {array} uniqueDates - from `getUniqueDates`
@ -107,7 +114,7 @@ export const getXScale = ( uniqueDates, width ) =>
*/ */
export const getXGroupScale = ( orderedKeys, xScale ) => export const getXGroupScale = ( orderedKeys, xScale ) =>
d3ScaleBand() d3ScaleBand()
.domain( orderedKeys.map( d => d.key ) ) .domain( orderedKeys.filter( d => d.visible ).map( d => d.key ) )
.rangeRound( [ 0, xScale.bandwidth() ] ) .rangeRound( [ 0, xScale.bandwidth() ] )
.padding( 0.07 ); .padding( 0.07 );
@ -155,20 +162,11 @@ export const getYTickOffset = ( height, scale, yMax ) =>
/** /**
* Describes getyTickOffset * Describes getyTickOffset
* @param {array} orderedKeys - from `getOrderedKeys`
* @returns {function} the D3 ordinal scale of the categories
*/
export const getColorScale = orderedKeys =>
d3ScaleOrdinal().range( d3Range( 0, 1.1, 100 / ( orderedKeys.length - 1 ) / 100 ) );
/**
* Describes getyTickOffset
* @param {array} data - The chart component's `data` prop.
* @param {function} xLineScale - from `getXLineScale`. * @param {function} xLineScale - from `getXLineScale`.
* @param {function} yScale - from `getYScale`. * @param {function} yScale - from `getYScale`.
* @returns {function} the D3 line function for plotting all category values * @returns {function} the D3 line function for plotting all category values
*/ */
export const getLine = ( data, xLineScale, yScale ) => export const getLine = ( xLineScale, yScale ) =>
d3Line() d3Line()
.x( d => xLineScale( new Date( d.date ) ) ) .x( d => xLineScale( new Date( d.date ) ) )
.y( d => yScale( d.value ) ); .y( d => yScale( d.value ) );
@ -201,7 +199,7 @@ export const getDateSpaces = ( uniqueDates, width, xLineScale ) =>
}; };
} ); } );
export const drawAxis = ( node, data, params ) => { export const drawAxis = ( node, params ) => {
const xScale = params.type === 'line' ? params.xLineScale : params.xScale; const xScale = params.type === 'line' ? params.xLineScale : params.xScale;
const yGrids = []; const yGrids = [];
@ -257,17 +255,16 @@ const showTooltip = ( node, params, d ) => {
let [ xPosition, yPosition ] = d3Mouse( node.node() ); let [ xPosition, yPosition ] = d3Mouse( node.node() );
xPosition = xPosition > chartCoords.width - 200 ? xPosition - 200 : xPosition + 20; xPosition = xPosition > chartCoords.width - 200 ? xPosition - 200 : xPosition + 20;
yPosition = yPosition > chartCoords.height - 150 ? yPosition - 200 : yPosition + 20; yPosition = yPosition > chartCoords.height - 150 ? yPosition - 200 : yPosition + 20;
const keys = params.orderedKeys.map( const keys = params.orderedKeys.filter( row => row.visible ).map(
( row, i ) => ` row => `
<li> <li>
<span class="key-colour" style="background-color:${ d3InterpolateViridis( <span class="key-colour" style="background-color:${ getColor( row.key, params ) }"></span>
params.colorScale( i )
) }"></span>
<span class="key-key">${ row.key }:</span> <span class="key-key">${ row.key }:</span>
<span class="key-value">${ d3Format( ',.0f' )( d[ row.key ] ) }</span> <span class="key-value">${ d3Format( ',.0f' )( d[ row.key ] ) }</span>
</li> </li>
` `
); );
params.tooltip params.tooltip
.style( 'left', xPosition + 'px' ) .style( 'left', xPosition + 'px' )
.style( 'top', yPosition + 'px' ) .style( 'top', yPosition + 'px' )
@ -345,7 +342,7 @@ export const drawLines = ( node, data, params ) => {
.append( 'g' ) .append( 'g' )
.attr( 'class', 'lines' ) .attr( 'class', 'lines' )
.selectAll( '.line-g' ) .selectAll( '.line-g' )
.data( params.lineData ) .data( params.lineData.filter( d => d.visible ) )
.enter() .enter()
.append( 'g' ) .append( 'g' )
.attr( 'class', 'line-g' ); .attr( 'class', 'line-g' );
@ -356,7 +353,7 @@ export const drawLines = ( node, data, params ) => {
.attr( 'stroke-width', 3 ) .attr( 'stroke-width', 3 )
.attr( 'stroke-linejoin', 'round' ) .attr( 'stroke-linejoin', 'round' )
.attr( 'stroke-linecap', 'round' ) .attr( 'stroke-linecap', 'round' )
.attr( 'stroke', ( d, i ) => d3InterpolateViridis( params.colorScale( i ) ) ) .attr( 'stroke', d => getColor( d.key, params ) )
.style( 'opacity', d => { .style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1; const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0; return d.visible ? opacity : 0;
@ -365,12 +362,12 @@ export const drawLines = ( node, data, params ) => {
series series
.selectAll( 'circle' ) .selectAll( 'circle' )
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible } ) ) ) .data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
.enter() .enter()
.append( 'circle' ) .append( 'circle' )
.attr( 'r', 3.5 ) .attr( 'r', 3.5 )
.attr( 'fill', '#fff' ) .attr( 'fill', '#fff' )
.attr( 'stroke', d => d3InterpolateViridis( params.colorScale( d.i ) ) ) .attr( 'stroke', d => getColor( d.key, params ) )
.attr( 'stroke-width', 3 ) .attr( 'stroke-width', 3 )
.style( 'opacity', d => { .style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1; const opacity = d.focus ? 1 : 0.1;
@ -403,7 +400,7 @@ export const drawBars = ( node, data, params ) => {
barGroup barGroup
.selectAll( '.bar' ) .selectAll( '.bar' )
.data( d => .data( d =>
params.orderedKeys.map( row => ( { params.orderedKeys.filter( row => row.visible ).map( row => ( {
key: row.key, key: row.key,
focus: row.focus, focus: row.focus,
value: d[ row.key ], value: d[ row.key ],
@ -417,7 +414,7 @@ export const drawBars = ( node, data, params ) => {
.attr( 'y', d => params.yScale( d.value ) ) .attr( 'y', d => params.yScale( d.value ) )
.attr( 'width', params.xGroupScale.bandwidth() ) .attr( 'width', params.xGroupScale.bandwidth() )
.attr( 'height', d => params.height - params.yScale( d.value ) ) .attr( 'height', d => params.height - params.yScale( d.value ) )
.attr( 'fill', ( d, i ) => d3InterpolateViridis( params.colorScale( i ) ) ) .attr( 'fill', d => getColor( d.key, params ) )
.style( 'opacity', d => { .style( 'opacity', d => {
const opacity = d.focus ? 1 : 0.1; const opacity = d.focus ? 1 : 0.1;
return d.visible ? opacity : 0; return d.visible ? opacity : 0;

View File

@ -12,7 +12,7 @@ import './style.scss';
import Header from 'layout/header'; import Header from 'layout/header';
import StorePerformance from './store-performance'; import StorePerformance from './store-performance';
import TopSellingProducts from './top-selling-products'; import TopSellingProducts from './top-selling-products';
import Chart from 'components/chart'; import Chart from 'components/chart/example';
import Card from 'components/card'; import Card from 'components/card';
export default class Dashboard extends Component { export default class Dashboard extends Component {

View File

@ -20,3 +20,8 @@ $light-gray-500: $core-grey-light-500;
$dark-gray-300: $core-grey-dark-300; $dark-gray-300: $core-grey-dark-300;
$dark-gray-900: $core-grey-dark-900; $dark-gray-900: $core-grey-dark-900;
$alert-red: $error-red; $alert-red: $error-red;
:export {
gaplarge: $gap-large;
gap: $gap;
}

File diff suppressed because it is too large Load Diff