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:
Robert Elliott 2018-07-09 15:40:49 +02:00 committed by GitHub
commit 1b40a90077
13 changed files with 6504 additions and 5529 deletions

View File

@ -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;
}

View File

@ -1,3 +1,8 @@
/** @format */
.d3-base {
width:100%;
background: transparent;
position: relative;
width: 100%;
height: 100%;
}

View File

@ -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 } /> );

View File

@ -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,
},
];

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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 );
} );
} );

View File

@ -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 ) );
};

View File

@ -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;

View File

@ -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

View File

@ -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",

View File

@ -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
}