Merge pull request woocommerce/woocommerce-admin#120 from woocommerce/add/d3-bar-component
D3 Chart Utilities: bar, line, axis and tooltip, and tests I'll be addressing the `tooltipDiv` prop, JSDocs, README in future (and smaller) PRs.
This commit is contained in:
commit
1b40a90077
|
@ -9,6 +9,11 @@ import PropTypes from 'prop-types';
|
|||
import classNames from 'classnames';
|
||||
import { select as d3Select } from 'd3-selection';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
|
||||
class D3Base extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
@ -22,11 +27,8 @@ class D3Base extends Component {
|
|||
this.updateParams();
|
||||
}
|
||||
|
||||
componentWillReceiveProps( nextProps ) {
|
||||
this.updateParams( nextProps );
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateParams( this.props );
|
||||
this.draw();
|
||||
}
|
||||
|
||||
|
@ -51,12 +53,23 @@ class D3Base extends Component {
|
|||
d3Select( this.node )
|
||||
.selectAll( 'svg' )
|
||||
.remove();
|
||||
const newNode = d3Select( this.node )
|
||||
d3Select( this.node )
|
||||
.selectAll( `.${ className }__tooltip` )
|
||||
.remove();
|
||||
|
||||
const newNode = d3Select( this.node );
|
||||
|
||||
newNode
|
||||
.append( 'svg' )
|
||||
.attr( 'class', `${ className }__viewbox` )
|
||||
.attr( 'viewBox', `0 0 ${ width } ${ height }` )
|
||||
.attr( 'preserveAspectRatio', 'xMidYMid meet' )
|
||||
.append( 'g' );
|
||||
newNode
|
||||
.append( 'div' )
|
||||
.attr( 'class', `${ className }__tooltip tooltip` )
|
||||
.style( 'display', 'none' );
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/** @format */
|
||||
|
||||
.d3-base {
|
||||
width:100%;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ describe( 'D3base', () => {
|
|||
} );
|
||||
|
||||
test( 'should render a result of the drawChart prop', () => {
|
||||
const drawChart = svg => {
|
||||
const drawChart = node => {
|
||||
const svg = node.select( 'svg' );
|
||||
return svg.append( 'circle' );
|
||||
};
|
||||
const base = mount( <D3Base drawChart={ drawChart } getParams={ noop } /> );
|
||||
|
@ -36,7 +37,8 @@ describe( 'D3base', () => {
|
|||
const getParams = () => ( {
|
||||
tagName: 'circle',
|
||||
} );
|
||||
const drawChart = ( svg, params ) => {
|
||||
const drawChart = ( node, params ) => {
|
||||
const svg = node.select( 'svg' );
|
||||
return svg.append( params.tagName );
|
||||
};
|
||||
const base = mount( <D3Base drawChart={ drawChart } getParams={ getParams } /> );
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* /* eslint-disable quote-props
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export const dummyOrders = [
|
||||
{
|
||||
date: '2018-05-30',
|
||||
Polo: 2704659,
|
||||
'T-Shirt': 4499890,
|
||||
Hoodie: 2159981,
|
||||
Sunglasses: 3853788,
|
||||
Cap: 10604510,
|
||||
},
|
||||
{
|
||||
date: '2018-05-31',
|
||||
Polo: 2027307,
|
||||
'T-Shirt': 3277946,
|
||||
Hoodie: 1420518,
|
||||
Sunglasses: 2454721,
|
||||
Cap: 7017731,
|
||||
},
|
||||
{
|
||||
date: '2018-06-01',
|
||||
Polo: 1208495,
|
||||
'T-Shirt': 2141490,
|
||||
Hoodie: 1058031,
|
||||
Sunglasses: 1999120,
|
||||
Cap: 5355235,
|
||||
},
|
||||
{
|
||||
date: '2018-06-02',
|
||||
Polo: 1140516,
|
||||
'T-Shirt': 1938695,
|
||||
Hoodie: 925060,
|
||||
Sunglasses: 1607297,
|
||||
Cap: 4782119,
|
||||
},
|
||||
{
|
||||
date: '2018-06-03',
|
||||
Polo: 894368,
|
||||
'T-Shirt': 1558919,
|
||||
Hoodie: 725973,
|
||||
Sunglasses: 1311479,
|
||||
Cap: 3596343,
|
||||
},
|
||||
{
|
||||
date: '2018-06-04',
|
||||
Polo: 737462,
|
||||
'T-Shirt': 1345341,
|
||||
Hoodie: 679201,
|
||||
Sunglasses: 1203944,
|
||||
Cap: 3157759,
|
||||
},
|
||||
];
|
|
@ -0,0 +1,141 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { format as d3Format } from 'd3-format';
|
||||
import { timeFormat as d3TimeFormat } from 'd3-time-format';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import D3Base from '../base';
|
||||
import {
|
||||
drawAxis,
|
||||
drawBars,
|
||||
drawLines,
|
||||
getColorScale,
|
||||
getDateSpaces,
|
||||
getOrderedKeys,
|
||||
getLine,
|
||||
getLineData,
|
||||
getUniqueKeys,
|
||||
getUniqueDates,
|
||||
getXScale,
|
||||
getXGroupScale,
|
||||
getXLineScale,
|
||||
getYMax,
|
||||
getYScale,
|
||||
getYTickOffset,
|
||||
} from './utils';
|
||||
|
||||
const D3Chart = ( {
|
||||
className,
|
||||
data,
|
||||
height,
|
||||
margin,
|
||||
timeseries,
|
||||
type,
|
||||
xFormat,
|
||||
yFormat,
|
||||
width,
|
||||
} ) => {
|
||||
const drawChart = ( node, params ) => {
|
||||
const g = node
|
||||
.select( 'svg' )
|
||||
.select( 'g' )
|
||||
.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,
|
||||
} );
|
||||
drawAxis( g, data, adjParams );
|
||||
type === 'line' && drawLines( node, data, adjParams );
|
||||
type === 'bar' && drawBars( node, data, adjParams );
|
||||
return node;
|
||||
};
|
||||
|
||||
const getParams = node => {
|
||||
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 orderedKeys = getOrderedKeys( data, uniqueKeys );
|
||||
const lineData = getLineData( data, orderedKeys );
|
||||
const yMax = getYMax( lineData );
|
||||
const yScale = getYScale( adjHeight, yMax );
|
||||
const uniqueDates = getUniqueDates( lineData );
|
||||
const xLineScale = getXLineScale( uniqueDates, adjWidth );
|
||||
const xScale = getXScale( uniqueDates, adjWidth );
|
||||
return {
|
||||
colorScale: getColorScale( orderedKeys ),
|
||||
dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ),
|
||||
height: calculatedHeight,
|
||||
line: getLine( data, xLineScale, yScale ),
|
||||
lineData,
|
||||
margin,
|
||||
orderedKeys,
|
||||
scale,
|
||||
type,
|
||||
uniqueDates,
|
||||
uniqueKeys,
|
||||
width: calculatedWidth,
|
||||
xFormat: timeseries ? d3TimeFormat( xFormat ) : d3Format( xFormat ),
|
||||
xGroupScale: getXGroupScale( orderedKeys, xScale ),
|
||||
xLineScale,
|
||||
xScale,
|
||||
yMax,
|
||||
yScale,
|
||||
yTickOffset: getYTickOffset( adjHeight, scale, yMax ),
|
||||
yFormat: d3Format( yFormat ),
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<D3Base className={ classNames( className ) } drawChart={ drawChart } getParams={ getParams } />
|
||||
);
|
||||
};
|
||||
|
||||
D3Chart.propTypes = {
|
||||
className: PropTypes.string,
|
||||
data: PropTypes.array,
|
||||
height: PropTypes.number,
|
||||
margin: PropTypes.shape( {
|
||||
bottom: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
top: PropTypes.number,
|
||||
} ),
|
||||
timeseries: PropTypes.bool,
|
||||
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
|
||||
width: PropTypes.number,
|
||||
xFormat: PropTypes.string,
|
||||
yFormat: PropTypes.string,
|
||||
};
|
||||
|
||||
D3Chart.defaultProps = {
|
||||
height: 200,
|
||||
margin: {
|
||||
bottom: 30,
|
||||
left: 40,
|
||||
right: 0,
|
||||
top: 20,
|
||||
},
|
||||
timeseries: true,
|
||||
type: 'line',
|
||||
width: 600,
|
||||
xFormat: '%Y-%m-%d',
|
||||
yFormat: ',.0f',
|
||||
};
|
||||
|
||||
export default D3Chart;
|
|
@ -0,0 +1,70 @@
|
|||
/** @format */
|
||||
|
||||
.key-colour {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.key-key {
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
min-width: 80px;
|
||||
height: auto;
|
||||
background-color: #fff;
|
||||
text-align: left;
|
||||
padding: 6px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
|
||||
h4 {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 7px;
|
||||
list-style: none;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
font-size: 14px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bargroup {
|
||||
&rect {
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
}
|
||||
.grid {
|
||||
line {
|
||||
stroke: #e2e4e7;
|
||||
stroke-opacity: 0.7;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
}
|
||||
.tick {
|
||||
padding-top: 10px;
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
.y-axis {
|
||||
&.tick {
|
||||
&text {
|
||||
fill: #555d66;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
// import { noop } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { dummyOrders } from '../dummy';
|
||||
import {
|
||||
getColorScale,
|
||||
getDateSpaces,
|
||||
getOrderedKeys,
|
||||
getLineData,
|
||||
getUniqueKeys,
|
||||
getUniqueDates,
|
||||
getXScale,
|
||||
getXGroupScale,
|
||||
getXLineScale,
|
||||
getYMax,
|
||||
getYScale,
|
||||
getYTickOffset,
|
||||
parseDate,
|
||||
} from '../utils';
|
||||
|
||||
const orderedKeys = [ 'Cap', 'T-Shirt', 'Sunglasses', 'Polo', 'Hoodie' ];
|
||||
const orderedDates = [
|
||||
'2018-05-30',
|
||||
'2018-05-31',
|
||||
'2018-06-01',
|
||||
'2018-06-02',
|
||||
'2018-06-03',
|
||||
'2018-06-04',
|
||||
];
|
||||
const testUniqueKeys = getUniqueKeys( dummyOrders );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
|
||||
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
||||
const testUniqueDates = getUniqueDates( testLineData );
|
||||
const testXScale = getXScale( testUniqueDates, 100 );
|
||||
const testXLineScale = getXLineScale( testUniqueDates, 100 );
|
||||
const testYMax = getYMax( testLineData );
|
||||
const testYScale = getYScale( 100, testYMax );
|
||||
|
||||
describe( 'parseDate', () => {
|
||||
it( 'correctly parse date in the expected format', () => {
|
||||
const testDate = parseDate( '2018-06-30' );
|
||||
const expectedDate = new Date( Date.UTC( 2018, 5, 30 ) );
|
||||
expect( testDate.getTime() ).toEqual( expectedDate.getTime() );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getUniqueKeys', () => {
|
||||
it( 'returns an array of keys excluding date', () => {
|
||||
// sort is a mutating action so we need a copy
|
||||
const testUniqueKeysClone = testUniqueKeys.slice();
|
||||
const sortedAZKeys = orderedKeys.slice();
|
||||
expect( testUniqueKeysClone.sort() ).toEqual( sortedAZKeys.sort() );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getOrderedKeys', () => {
|
||||
it( 'returns an array of keys order by value from largest to smallest', () => {
|
||||
expect( testOrderedKeys ).toEqual( orderedKeys );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getLineData', () => {
|
||||
it( 'returns a sorted array of objects with category key', () => {
|
||||
expect( testLineData ).toBeInstanceOf( Array );
|
||||
expect( testLineData ).toHaveLength( 5 );
|
||||
expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys );
|
||||
} );
|
||||
|
||||
testLineData.forEach( d => {
|
||||
it( 'ensure a key and that the values property is an array', () => {
|
||||
expect( d ).toHaveProperty( 'key' );
|
||||
expect( d ).toHaveProperty( 'values' );
|
||||
expect( d.values ).toBeInstanceOf( Array );
|
||||
} );
|
||||
|
||||
it( 'ensure all unique dates exist in values array', () => {
|
||||
const rowDates = d.values.map( row => row.date );
|
||||
expect( rowDates ).toEqual( orderedDates );
|
||||
} );
|
||||
|
||||
d.values.forEach( row => {
|
||||
it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
|
||||
expect( row ).toHaveProperty( 'date' );
|
||||
expect( row ).toHaveProperty( 'value' );
|
||||
expect( parseDate( row.date ) ).not.toBeNull();
|
||||
expect( typeof row.date ).toBe( 'string' );
|
||||
expect( typeof row.value ).toBe( 'number' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getXScale', () => {
|
||||
it( 'properly scale inputs to the provided domain and range', () => {
|
||||
expect( testXScale( orderedDates[ 0 ] ) ).toEqual( 3 );
|
||||
expect( testXScale( orderedDates[ 2 ] ) ).toEqual( 35 );
|
||||
expect( testXScale( orderedDates[ orderedDates.length - 1 ] ) ).toEqual( 83 );
|
||||
} );
|
||||
it( 'properly scale inputs and test the bandwidth', () => {
|
||||
expect( testXScale.bandwidth() ).toEqual( 14 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getXGroupScale', () => {
|
||||
it( 'properly scale inputs based on the getXScale', () => {
|
||||
const testXGroupScale = getXGroupScale( testOrderedKeys, testXScale );
|
||||
expect( testXGroupScale( orderedKeys[ 0 ] ) ).toEqual( 2 );
|
||||
expect( testXGroupScale( orderedKeys[ 2 ] ) ).toEqual( 6 );
|
||||
expect( testXGroupScale( orderedKeys[ orderedKeys.length - 1 ] ) ).toEqual( 10 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getXLineScale', () => {
|
||||
it( 'properly scale inputs for the line', () => {
|
||||
expect( testXLineScale( new Date( orderedDates[ 0 ] ) ) ).toEqual( 0 );
|
||||
expect( testXLineScale( new Date( orderedDates[ 2 ] ) ) ).toEqual( 40 );
|
||||
expect( testXLineScale( new Date( orderedDates[ orderedDates.length - 1 ] ) ) ).toEqual( 100 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getColorScale', () => {
|
||||
it( 'properly scale product keys into a range of colors', () => {
|
||||
const testColorScale = getColorScale( testOrderedKeys );
|
||||
testOrderedKeys.map( d => testColorScale( d ) ); // without this it fails! why? how?
|
||||
expect( testColorScale( orderedKeys[ 0 ] ) ).toEqual( 0 );
|
||||
expect( testColorScale( orderedKeys[ 2 ] ) ).toEqual( 0.5 );
|
||||
expect( testColorScale( orderedKeys[ orderedKeys.length - 1 ] ) ).toEqual( 1 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getYMax', () => {
|
||||
it( 'calculate the correct maximum y value', () => {
|
||||
expect( testYMax ).toEqual( 14139347 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getYScale', () => {
|
||||
it( 'properly scale the y values given the height and maximum y value', () => {
|
||||
expect( testYScale( 0 ) ).toEqual( 100 );
|
||||
expect( testYScale( testYMax ) ).toEqual( 0 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getYTickOffset', () => {
|
||||
it( 'properly scale the y values for the y-axis ticks given the height and maximum y value', () => {
|
||||
const testYTickOffset1 = getYTickOffset( 100, 1, testYMax );
|
||||
expect( testYTickOffset1( 0 ) ).toEqual( 112 );
|
||||
expect( testYTickOffset1( testYMax ) ).toEqual( 12 );
|
||||
const testYTickOffset2 = getYTickOffset( 100, 2, testYMax );
|
||||
expect( testYTickOffset2( 0 ) ).toEqual( 124 );
|
||||
expect( testYTickOffset2( testYMax ) ).toEqual( 24 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getdateSpaces', () => {
|
||||
it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
|
||||
const testDateSpaces = getDateSpaces( testUniqueDates, 100, testXLineScale );
|
||||
expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30' );
|
||||
expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
|
||||
expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
|
||||
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02' );
|
||||
expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
|
||||
expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04' );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,343 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
||||
import { max as d3Max, range as d3Range } from 'd3-array';
|
||||
import { axisBottom as d3AxisBottom, axisLeft as d3AxisLeft } from 'd3-axis';
|
||||
import { format as d3Format } from 'd3-format';
|
||||
import {
|
||||
scaleBand as d3ScaleBand,
|
||||
scaleLinear as d3ScaleLinear,
|
||||
scaleOrdinal as d3ScaleOrdinal,
|
||||
scaleTime as d3ScaleTime,
|
||||
} from 'd3-scale';
|
||||
import { mouse as d3Mouse, select as d3Select } from 'd3-selection';
|
||||
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';
|
||||
|
||||
export const parseDate = d3UTCParse( '%Y-%m-%d' );
|
||||
|
||||
export const getUniqueKeys = data => {
|
||||
return [
|
||||
...new Set(
|
||||
data.reduce( ( accum, curr ) => {
|
||||
Object.keys( curr ).forEach( key => key !== 'date' && accum.push( key ) );
|
||||
return accum;
|
||||
}, [] )
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
export const getOrderedKeys = ( data, uniqueKeys ) =>
|
||||
uniqueKeys
|
||||
.map( key => ( {
|
||||
key,
|
||||
total: data.reduce( ( a, c ) => a + c[ key ], 0 ),
|
||||
} ) )
|
||||
.sort( ( a, b ) => b.total - a.total )
|
||||
.map( d => d.key );
|
||||
|
||||
export const getLineData = ( data, orderedKeys ) =>
|
||||
orderedKeys.map( key => ( {
|
||||
key,
|
||||
values: data.map( d => ( {
|
||||
date: d.date,
|
||||
value: d[ key ],
|
||||
} ) ),
|
||||
} ) );
|
||||
|
||||
export const getUniqueDates = lineData => {
|
||||
return [
|
||||
...new Set(
|
||||
lineData.reduce( ( accum, { values } ) => {
|
||||
values.forEach( ( { date } ) => accum.push( date ) );
|
||||
return accum;
|
||||
}, [] )
|
||||
),
|
||||
].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
|
||||
};
|
||||
|
||||
export const getXScale = ( uniqueDates, width ) =>
|
||||
d3ScaleBand()
|
||||
.domain( uniqueDates )
|
||||
.rangeRound( [ 0, width ] )
|
||||
.paddingInner( 0.1 );
|
||||
|
||||
export const getXGroupScale = ( orderedKeys, xScale ) =>
|
||||
d3ScaleBand()
|
||||
.domain( orderedKeys )
|
||||
.rangeRound( [ 0, xScale.bandwidth() ] )
|
||||
.padding( 0.07 );
|
||||
|
||||
export const getXLineScale = ( uniqueDates, width ) =>
|
||||
d3ScaleTime()
|
||||
.domain( [ new Date( uniqueDates[ 0 ] ), new Date( uniqueDates[ uniqueDates.length - 1 ] ) ] )
|
||||
.rangeRound( [ 0, width ] );
|
||||
|
||||
export const getYMax = lineData =>
|
||||
Math.round( 4 / 3 * d3Max( lineData, d => d3Max( d.values.map( date => date.value ) ) ) );
|
||||
|
||||
export const getYScale = ( height, yMax ) =>
|
||||
d3ScaleLinear()
|
||||
.domain( [ 0, yMax ] )
|
||||
.rangeRound( [ height, 0 ] );
|
||||
|
||||
export const getYTickOffset = ( height, scale, yMax ) =>
|
||||
d3ScaleLinear()
|
||||
.domain( [ 0, yMax ] )
|
||||
.rangeRound( [ height + scale * 12, scale * 12 ] );
|
||||
|
||||
export const getColorScale = orderedKeys =>
|
||||
d3ScaleOrdinal().range( d3Range( 0, 1.1, 100 / ( orderedKeys.length - 1 ) / 100 ) );
|
||||
|
||||
export const getLine = ( data, xLineScale, yScale ) =>
|
||||
d3Line()
|
||||
.x( d => xLineScale( new Date( d.date ) ) )
|
||||
.y( d => yScale( d.value ) );
|
||||
|
||||
export const getDateSpaces = ( uniqueDates, width, xLineScale ) =>
|
||||
uniqueDates.map( ( d, i ) => {
|
||||
const xNow = xLineScale( new Date( d ) );
|
||||
const xPrev =
|
||||
i >= 1
|
||||
? xLineScale( new Date( uniqueDates[ i - 1 ] ) )
|
||||
: xLineScale( new Date( uniqueDates[ 0 ] ) );
|
||||
const xNext =
|
||||
i < uniqueDates.length - 1
|
||||
? xLineScale( new Date( uniqueDates[ i + 1 ] ) )
|
||||
: xLineScale( new Date( uniqueDates[ uniqueDates.length - 1 ] ) );
|
||||
let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
|
||||
const xStart = i === 0 ? 0 : xNow - xWidth / 2;
|
||||
xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
|
||||
return {
|
||||
date: d,
|
||||
start: uniqueDates.length > 1 ? xStart : 0,
|
||||
width: uniqueDates.length > 1 ? xWidth : width,
|
||||
};
|
||||
} );
|
||||
|
||||
export const drawAxis = ( node, data, params ) => {
|
||||
const xScale = params.type === 'line' ? params.xLineScale : params.xScale;
|
||||
|
||||
const yGrids = [];
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
yGrids.push( i / 3 * params.yMax );
|
||||
}
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis' )
|
||||
.attr( 'transform', `translate(0,${ params.height })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
.tickValues( params.uniqueDates.map( d => ( params.type === 'line' ? new Date( d ) : d ) ) )
|
||||
.tickFormat( d => d3TimeFormat( '%d' )( d instanceof Date ? d : new Date( d ) ) )
|
||||
);
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'grid' )
|
||||
.attr( 'transform', `translate(-${ params.margin.left },0)` )
|
||||
.call(
|
||||
d3AxisLeft( params.yScale )
|
||||
.tickValues( yGrids )
|
||||
.tickSize( -params.width - params.margin.left )
|
||||
.tickFormat( '' )
|
||||
)
|
||||
.call( g => g.select( '.domain' ).remove() );
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis y-axis' )
|
||||
.call(
|
||||
d3AxisLeft( params.yTickOffset )
|
||||
.tickValues( yGrids )
|
||||
.tickFormat( d => ( d !== 0 ? d3Format( '.3s' )( d ) : 0 ) )
|
||||
);
|
||||
|
||||
node
|
||||
.selectAll( '.y-axis .tick text' )
|
||||
.style( 'font-size', `${ Math.round( params.scale * 10 ) }px` );
|
||||
|
||||
node.selectAll( '.domain' ).remove();
|
||||
node
|
||||
.selectAll( '.axis' )
|
||||
.selectAll( '.tick' )
|
||||
.select( 'line' )
|
||||
.remove();
|
||||
};
|
||||
|
||||
const showTooltip = ( node, params, d ) => {
|
||||
const chartCoords = node.node().getBoundingClientRect();
|
||||
let [ xPosition, yPosition ] = d3Mouse( node.node() );
|
||||
xPosition = xPosition > chartCoords.width - 200 ? xPosition - 200 : xPosition + 20;
|
||||
yPosition = yPosition > chartCoords.height - 150 ? yPosition - 200 : yPosition + 20;
|
||||
const keys = params.orderedKeys.map(
|
||||
( key, i ) => `
|
||||
<li>
|
||||
<span class="key-colour" style="background-color:${ d3InterpolateViridis(
|
||||
params.colorScale( i )
|
||||
) }"></span>
|
||||
<span class="key-key">${ key }:</span>
|
||||
<span class="key-value">${ d3Format( ',.0f' )( d[ key ] ) }</span>
|
||||
</li>`
|
||||
);
|
||||
node
|
||||
.select( '.tooltip' )
|
||||
.style( 'left', xPosition + 'px' )
|
||||
.style( 'top', yPosition + 'px' )
|
||||
.style( 'display', 'inline-block' ).html( `
|
||||
<div>
|
||||
<h4>${ d.date }</h4>
|
||||
<ul>
|
||||
${ keys.join( '' ) }
|
||||
</ul>
|
||||
</div>
|
||||
` );
|
||||
};
|
||||
|
||||
const handleMouseOverBarChart = ( d, i, nodes, node, data, params ) => {
|
||||
d3Select( nodes[ i ].parentNode )
|
||||
.select( '.barfocus' )
|
||||
.attr( 'opacity', '0.1' );
|
||||
showTooltip( node, params, d );
|
||||
};
|
||||
|
||||
const handleMouseOutBarChart = ( d, i, nodes, node ) => {
|
||||
d3Select( nodes[ i ].parentNode )
|
||||
.select( '.barfocus' )
|
||||
.attr( 'opacity', '0' );
|
||||
node.select( '.tooltip' ).style( 'display', 'none' );
|
||||
};
|
||||
|
||||
const handleMouseOverLineChart = ( d, i, nodes, node, data, params ) => {
|
||||
d3Select( nodes[ i ].parentNode )
|
||||
.select( '.focus-grid' )
|
||||
.attr( 'opacity', '1' );
|
||||
showTooltip( node, params, data.find( e => e.date === d.date ) );
|
||||
};
|
||||
|
||||
const handleMouseOutLineChart = ( d, i, nodes, node ) => {
|
||||
d3Select( nodes[ i ].parentNode )
|
||||
.select( '.focus-grid' )
|
||||
.attr( 'opacity', '0' );
|
||||
node.select( '.tooltip' ).style( 'display', 'none' );
|
||||
};
|
||||
|
||||
export const drawLines = ( node, data, params ) => {
|
||||
const g = node
|
||||
.select( 'svg' )
|
||||
.select( 'g' )
|
||||
.select( 'g' )
|
||||
.append( 'g' );
|
||||
|
||||
const focus = g
|
||||
.selectAll( '.focus-space' )
|
||||
.data( params.dateSpaces )
|
||||
.enter()
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'focus-space' );
|
||||
|
||||
focus
|
||||
.append( 'line' )
|
||||
.attr( 'class', 'focus-grid' )
|
||||
.style( 'stroke', 'lightgray' )
|
||||
.style( 'stroke-width', 1 )
|
||||
.attr( 'x1', d => params.xLineScale( new Date( d.date ) ) )
|
||||
.attr( 'y1', 0 )
|
||||
.attr( 'x2', d => params.xLineScale( new Date( d.date ) ) )
|
||||
.attr( 'y2', params.height )
|
||||
.attr( 'opacity', '0' );
|
||||
|
||||
focus
|
||||
.append( 'rect' )
|
||||
.attr( 'class', 'focus-g' )
|
||||
.attr( 'x', d => d.start )
|
||||
.attr( 'y', 0 )
|
||||
.attr( 'width', d => d.width )
|
||||
.attr( 'height', params.height )
|
||||
.attr( 'opacity', 0 )
|
||||
.on( 'mouseover', ( d, i, nodes ) =>
|
||||
handleMouseOverLineChart( d, i, nodes, node, data, params )
|
||||
)
|
||||
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, node ) );
|
||||
|
||||
const series = g
|
||||
.selectAll( '.line-g' )
|
||||
.data( params.lineData )
|
||||
.enter()
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'line-g' );
|
||||
|
||||
series
|
||||
.append( 'path' )
|
||||
.attr( 'fill', 'none' )
|
||||
.attr( 'stroke-width', 3 )
|
||||
.attr( 'stroke-linejoin', 'round' )
|
||||
.attr( 'stroke-linecap', 'round' )
|
||||
.attr( 'stroke', ( d, i ) => d3InterpolateViridis( params.colorScale( i ) ) )
|
||||
.attr( 'd', d => params.line( d.values ) );
|
||||
|
||||
series
|
||||
.selectAll( 'circle' )
|
||||
.data( ( d, i ) => d.values.map( row => ( { ...row, i } ) ) )
|
||||
.enter()
|
||||
.append( 'circle' )
|
||||
.attr( 'r', 3.5 )
|
||||
.attr( 'fill', '#fff' )
|
||||
.attr( 'stroke', d => d3InterpolateViridis( params.colorScale( d.i ) ) )
|
||||
.attr( 'stroke-width', 3 )
|
||||
.attr( 'cx', d => params.xLineScale( new Date( d.date ) ) )
|
||||
.attr( 'cy', d => params.yScale( d.value ) );
|
||||
};
|
||||
|
||||
export const drawBars = ( node, data, params ) => {
|
||||
const barGroup = node
|
||||
.select( 'svg' )
|
||||
.select( 'g' )
|
||||
.select( 'g' )
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'bars' )
|
||||
.selectAll( 'g' )
|
||||
.data( data )
|
||||
.enter()
|
||||
.append( 'g' )
|
||||
.attr( 'transform', d => `translate(${ params.xScale( d.date ) },0)` )
|
||||
.attr( 'class', 'bargroup' );
|
||||
|
||||
barGroup
|
||||
.append( 'rect' )
|
||||
.attr( 'class', 'barfocus' )
|
||||
.attr( 'x', 0 )
|
||||
.attr( 'y', 0 )
|
||||
.attr( 'width', params.xGroupScale.range()[ 1 ] )
|
||||
.attr( 'height', params.height )
|
||||
.attr( 'opacity', '0' );
|
||||
|
||||
barGroup
|
||||
.selectAll( '.bar' )
|
||||
.data( d => params.orderedKeys.map( key => ( { key: key, value: d[ key ] } ) ) )
|
||||
.enter()
|
||||
.append( 'rect' )
|
||||
.attr( 'class', 'bar' )
|
||||
.attr( 'x', d => params.xGroupScale( d.key ) )
|
||||
.attr( 'y', d => params.yScale( d.value ) )
|
||||
.attr( 'width', params.xGroupScale.bandwidth() )
|
||||
.attr( 'height', d => params.height - params.yScale( d.value ) )
|
||||
.attr( 'fill', ( d, i ) => d3InterpolateViridis( params.colorScale( i ) ) );
|
||||
|
||||
barGroup
|
||||
.append( 'rect' )
|
||||
.attr( 'class', 'barmouse' )
|
||||
.attr( 'x', 0 )
|
||||
.attr( 'y', 0 )
|
||||
.attr( 'width', params.xGroupScale.range()[ 1 ] )
|
||||
.attr( 'height', params.height )
|
||||
.attr( 'opacity', '0' )
|
||||
.on( 'mouseover', ( d, i, nodes ) =>
|
||||
handleMouseOverBarChart( d, i, nodes, node, data, params )
|
||||
)
|
||||
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, node ) );
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Card from 'components/card';
|
||||
import D3Chart from 'components/d3/charts';
|
||||
import { dummyOrders } from 'components/d3/charts/dummy';
|
||||
|
||||
class WidgetCharts extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Card title={ __( 'Store Charts', 'woo-dash' ) }>
|
||||
<div className="woo-dash__widget">
|
||||
<D3Chart
|
||||
className="woo-dash__widget-bar-chart"
|
||||
data={ dummyOrders }
|
||||
height={ 300 }
|
||||
type={ 'line' }
|
||||
width={ 1042 }
|
||||
/>
|
||||
</div>
|
||||
<div className="woo-dash__widget">
|
||||
<D3Chart
|
||||
className="woo-dash__widget-bar-chart"
|
||||
data={ dummyOrders }
|
||||
height={ 300 }
|
||||
type={ 'bar' }
|
||||
width={ 1042 }
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WidgetCharts;
|
|
@ -11,6 +11,7 @@ import { Component, Fragment } from '@wordpress/element';
|
|||
import Agenda from './widgets/agenda';
|
||||
import Header from 'layout/header';
|
||||
import StorePerformance from './store-performance';
|
||||
import Charts from './charts';
|
||||
|
||||
export default class Dashboard extends Component {
|
||||
render() {
|
||||
|
@ -19,6 +20,7 @@ export default class Dashboard extends Component {
|
|||
<Header sections={ [ __( 'Dashboard', 'woo-dash' ) ] } />
|
||||
<StorePerformance />
|
||||
<Agenda />
|
||||
<Charts />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -66,7 +66,14 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"d3-array": "^1.2.1",
|
||||
"d3-axis": "^1.0.8",
|
||||
"d3-format": "^1.3.0",
|
||||
"d3-scale": "^2.0.0",
|
||||
"d3-scale-chromatic": "^1.3.0",
|
||||
"d3-selection": "^1.3.0",
|
||||
"d3-shape": "^1.2.0",
|
||||
"d3-time-format": "^2.1.1",
|
||||
"lodash": "^4.17.10",
|
||||
"prop-types": "^15.6.1",
|
||||
"react-dates": "^16.7.0",
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"client/**/*.js",
|
||||
"!**/node_modules/**",
|
||||
"!**/vendor/**",
|
||||
"!**/test/**"
|
||||
"!**/test/**",
|
||||
"!client/**/dummy.js"
|
||||
],
|
||||
"moduleDirectories": ["node_modules", "<rootDir>/client"],
|
||||
"moduleNameMapper": {
|
||||
|
@ -23,5 +24,6 @@
|
|||
],
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(gutenberg)/)"
|
||||
]
|
||||
],
|
||||
"verbose": true
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue