Merge pull request woocommerce/woocommerce-admin#360 from woocommerce/add/chart-intervals
Chart Component: Add/chart intervals
This commit is contained in:
commit
2df9b05b5f
|
@ -6,13 +6,12 @@ A simple D3 line and bar chart component for timeseries data in React.
|
|||
## Usage
|
||||
|
||||
```jsx
|
||||
<D3Chart
|
||||
className="woocommerce-dashboard__*"
|
||||
<Chart
|
||||
data={ timeseries }
|
||||
height={ 200 }
|
||||
margin={ { bottom: 30, left: 40, right: 0, top: 20 } }
|
||||
tooltipFormat={ 'Date is %Y-%m-%d' }
|
||||
type={ 'bar' }
|
||||
width={ 600 }
|
||||
xFormat={ '%d' }
|
||||
yFormat={ '.3s' }
|
||||
/>
|
||||
```
|
||||
|
||||
|
@ -21,7 +20,7 @@ This component accepts timeseries `data` prop in the following format (with date
|
|||
```
|
||||
[
|
||||
{
|
||||
date: 'YYYY-mm-dd', // string
|
||||
date: '%Y-%m-%dT%H:%M:%S', // string
|
||||
category1: value, // number
|
||||
category2: value, // number
|
||||
...
|
||||
|
@ -33,7 +32,7 @@ For example:
|
|||
```
|
||||
[
|
||||
{
|
||||
date: '2018-06-25',
|
||||
date: '2018-06-25T00:00:00',
|
||||
category1: 1234.56,
|
||||
category2: 9876,
|
||||
...
|
||||
|
@ -47,68 +46,9 @@ Required props are marked with `*`.
|
|||
|
||||
Name | Type | Default | Description
|
||||
--- | --- | --- | ---
|
||||
`data`* | `array` | none | An array of data as specified above
|
||||
`height` | `number` | `200` | Relative viewpoirt height of the `svg`
|
||||
`margin` | `object` | `{ bottom: 30, left: 40, right: 0, top: 20 }` | Margins for axis and chart padding
|
||||
`orderedKeys` | `array` | `getOrderedKeys` | Override for `getOrderedKeys` and used by the `<Legend />` (below)
|
||||
`data`* | `array` | none | An array of data as specified above(below)
|
||||
`type`* | `string` | `line` | Chart type of either `line` or `bar`
|
||||
`width` | `number` | `600` | Relative viewport width of the `svg`
|
||||
|
||||
D3 Chart Legend
|
||||
===
|
||||
|
||||
A legend specifically designed for the WooCommerce admin charts.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
<Legend
|
||||
className={ 'woocommerce-legend' }
|
||||
data={ data }
|
||||
handleLegendHover={ this.handleLegendHover }
|
||||
handleLegendToggle={ this.handleLegendToggle }
|
||||
legendDirection={ legendDirection }
|
||||
/>
|
||||
```
|
||||
|
||||
### Expected Data Format
|
||||
This component needs to include the category keys present in the D3 Chart Component's `orderedKeys`, in fact, you should use the same `orderedKeys` for both the legend `data` and the chart.
|
||||
|
||||
The `handleLegendHover` could toggle the `focus` parameter to highlight the category that has the mouse over it.
|
||||
|
||||
The `handleLegendToggle` could toggle the `visible` parameter to hide/show the category that has been selected.
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
key: "Product", // string
|
||||
total: number, // number
|
||||
visible: true, // boolean
|
||||
focus: true, // boolean
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
For example:
|
||||
```
|
||||
[
|
||||
{
|
||||
date: 'Hoodie',
|
||||
total: 1234.56,
|
||||
visible: true,
|
||||
focus: true,
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### Props
|
||||
Required props are marked with `*`.
|
||||
|
||||
Name | Type | Default | Description
|
||||
--- | --- | --- | ---
|
||||
`data`* | `array` | none | An array of `orderedKeys` as specified above
|
||||
`handleLegendHover` | `function` | none | Handles `onMouseEnter`/`onMouseLeave` events
|
||||
`handleLegendToggle` | `function` | none | Handles `onClick` event
|
||||
`legendDirection` | `string` | none | Display legend items as a `row` or `column` inside a flex-box
|
||||
`title` | `string` | none | Chart title
|
||||
`tooltipFormat` | `string` | `%Y-%m-%d` | Title and format of the tooltip title
|
||||
`xFormat` | `string` | `%Y-%m-%d` | d3TimeFormat of the x-axis values
|
||||
`yFormat` | `string` | `.3s` | d3Format of the y-axis values
|
||||
|
|
|
@ -7,7 +7,6 @@ import { isEqual } from 'lodash';
|
|||
import { Component, createRef } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { format as d3Format } from 'd3-format';
|
||||
import { timeFormat as d3TimeFormat } from 'd3-time-format';
|
||||
import { select as d3Select } from 'd3-selection';
|
||||
|
||||
|
@ -89,7 +88,17 @@ class D3Chart extends Component {
|
|||
}
|
||||
|
||||
getParams( node ) {
|
||||
const { colorScheme, data, height, margin, orderedKeys, type, xFormat, yFormat } = this.props;
|
||||
const {
|
||||
colorScheme,
|
||||
data,
|
||||
height,
|
||||
margin,
|
||||
orderedKeys,
|
||||
tooltipFormat,
|
||||
type,
|
||||
xFormat,
|
||||
yFormat,
|
||||
} = this.props;
|
||||
const { width } = this.state;
|
||||
const calculatedWidth = width || node.offsetWidth;
|
||||
const calculatedHeight = height || node.offsetHeight;
|
||||
|
@ -113,6 +122,7 @@ class D3Chart extends Component {
|
|||
margin,
|
||||
orderedKeys: newOrderedKeys,
|
||||
scale,
|
||||
tooltipFormat: d3TimeFormat( tooltipFormat ),
|
||||
type,
|
||||
uniqueDates,
|
||||
uniqueKeys,
|
||||
|
@ -124,7 +134,7 @@ class D3Chart extends Component {
|
|||
yMax,
|
||||
yScale,
|
||||
yTickOffset: getYTickOffset( adjHeight, scale, yMax ),
|
||||
yFormat: d3Format( yFormat ),
|
||||
yFormat,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -168,9 +178,9 @@ D3Chart.propTypes = {
|
|||
*/
|
||||
height: PropTypes.number,
|
||||
/**
|
||||
* @todo Remove – not used?
|
||||
* Interval specification (hourly, daily, weekly etc.)
|
||||
*/
|
||||
legend: PropTypes.array,
|
||||
interval: PropTypes.oneOf( [ 'hour', 'day', 'week', 'month', 'quater', 'year' ] ),
|
||||
/**
|
||||
* Margins for axis and chart padding.
|
||||
*/
|
||||
|
@ -184,6 +194,10 @@ D3Chart.propTypes = {
|
|||
* The list of labels for this chart.
|
||||
*/
|
||||
orderedKeys: PropTypes.array,
|
||||
/**
|
||||
* A datetime formatting string to format the title of the toolip, passed to d3TimeFormat.
|
||||
*/
|
||||
tooltipFormat: PropTypes.string,
|
||||
/**
|
||||
* Chart type of either `line` or `bar`.
|
||||
*/
|
||||
|
@ -211,10 +225,11 @@ D3Chart.defaultProps = {
|
|||
right: 0,
|
||||
top: 20,
|
||||
},
|
||||
tooltipFormat: '%Y-%m-%d',
|
||||
type: 'line',
|
||||
width: 600,
|
||||
xFormat: '%Y-%m-%d',
|
||||
yFormat: ',.0f',
|
||||
yFormat: '.3s',
|
||||
};
|
||||
|
||||
export default D3Chart;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Component } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import Card from 'components/card';
|
||||
import Chart from './index';
|
||||
import dummyOrders from './test/fixtures/dummy-hour';
|
||||
|
||||
class WidgetCharts extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Card title={ __( 'Test Categories', 'wc-admin' ) }>
|
||||
<Chart
|
||||
data={ dummyOrders }
|
||||
tooltipFormat={ 'Hour of %H' }
|
||||
type={ 'bar' }
|
||||
xFormat={ '%H' }
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WidgetCharts;
|
|
@ -158,8 +158,11 @@ class Chart extends Component {
|
|||
height={ 300 }
|
||||
margin={ margin }
|
||||
orderedKeys={ orderedKeys }
|
||||
type={ 'line' }
|
||||
tooltipFormat={ this.props.tooltipFormat }
|
||||
type={ this.props.type }
|
||||
width={ chartDirection === 'row' ? width - 320 : width }
|
||||
xFormat={ this.props.xFormat }
|
||||
yFormat={ this.props.yFormat }
|
||||
/>
|
||||
</div>
|
||||
{ width < WIDE_BREAKPOINT && <div className="woocommerce-chart__footer">{ legend }</div> }
|
||||
|
@ -177,10 +180,29 @@ Chart.propTypes = {
|
|||
* A title describing this chart.
|
||||
*/
|
||||
title: PropTypes.string,
|
||||
/**
|
||||
* A datetime formatting string to format the title of the toolip, passed to d3TimeFormat.
|
||||
*/
|
||||
tooltipFormat: PropTypes.string,
|
||||
/**
|
||||
* Chart type of either `line` or `bar`.
|
||||
*/
|
||||
type: PropTypes.oneOf( [ 'bar', 'line' ] ),
|
||||
/**
|
||||
* A datetime formatting string, passed to d3TimeFormat.
|
||||
*/
|
||||
xFormat: PropTypes.string,
|
||||
/**
|
||||
* A number formatting string, passed to d3Format.
|
||||
*/
|
||||
yFormat: PropTypes.string,
|
||||
};
|
||||
|
||||
Chart.defaultProps = {
|
||||
data: [],
|
||||
tooltipFormat: '%Y-%m-%d',
|
||||
xFormat: '%Y-%m-%d',
|
||||
yFormat: '.3s',
|
||||
};
|
||||
|
||||
export default Chart;
|
||||
|
|
25
plugins/woocommerce-admin/client/components/chart/test/fixtures/dummy-hour.js
vendored
Normal file
25
plugins/woocommerce-admin/client/components/chart/test/fixtures/dummy-hour.js
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
/** @format */
|
||||
|
||||
// /**
|
||||
// * /* eslint-disable quote-props
|
||||
// *
|
||||
// * @format
|
||||
// */
|
||||
|
||||
export default [
|
||||
{
|
||||
date: '2018-08-01T00:00:00',
|
||||
'Custom (Aug 1, 2018)': 58929.99,
|
||||
'Previous Period (Jul 31, 2018)': 160130.74000000002,
|
||||
},
|
||||
{
|
||||
date: '2018-08-01T01:00:00',
|
||||
'Custom (Aug 1, 2018)': 3805.56,
|
||||
'Previous Period (Jul 31, 2018)': 0,
|
||||
},
|
||||
{
|
||||
date: '2018-08-01T02:00:00',
|
||||
'Custom (Aug 1, 2018)': 3805.56,
|
||||
'Previous Period (Jul 31, 2018)': 0,
|
||||
},
|
||||
];
|
|
@ -1,3 +1,5 @@
|
|||
/** @format */
|
||||
|
||||
/**
|
||||
* /* eslint-disable quote-props
|
||||
*
|
||||
|
@ -6,7 +8,7 @@
|
|||
|
||||
export default [
|
||||
{
|
||||
date: '2018-05-30',
|
||||
date: '2018-05-30T00:00:00',
|
||||
Polo: 2704659,
|
||||
'T-Shirt': 4499890,
|
||||
Hoodie: 2159981,
|
||||
|
@ -14,7 +16,7 @@ export default [
|
|||
Cap: 10604510,
|
||||
},
|
||||
{
|
||||
date: '2018-05-31',
|
||||
date: '2018-05-31T00:00:00',
|
||||
Polo: 2027307,
|
||||
'T-Shirt': 3277946,
|
||||
Hoodie: 1420518,
|
||||
|
@ -22,7 +24,7 @@ export default [
|
|||
Cap: 7017731,
|
||||
},
|
||||
{
|
||||
date: '2018-06-01',
|
||||
date: '2018-06-01T00:00:00',
|
||||
Polo: 1208495,
|
||||
'T-Shirt': 2141490,
|
||||
Hoodie: 1058031,
|
||||
|
@ -30,7 +32,7 @@ export default [
|
|||
Cap: 5355235,
|
||||
},
|
||||
{
|
||||
date: '2018-06-02',
|
||||
date: '2018-06-02T00:00:00',
|
||||
Polo: 1140516,
|
||||
'T-Shirt': 1938695,
|
||||
Hoodie: 925060,
|
||||
|
@ -38,7 +40,7 @@ export default [
|
|||
Cap: 4782119,
|
||||
},
|
||||
{
|
||||
date: '2018-06-03',
|
||||
date: '2018-06-03T00:00:00',
|
||||
Polo: 894368,
|
||||
'T-Shirt': 1558919,
|
||||
Hoodie: 725973,
|
||||
|
@ -46,7 +48,7 @@ export default [
|
|||
Cap: 3596343,
|
||||
},
|
||||
{
|
||||
date: '2018-06-04',
|
||||
date: '2018-06-04T00:00:00',
|
||||
Polo: 737462,
|
||||
'T-Shirt': 1345341,
|
||||
Hoodie: 679201,
|
||||
|
|
|
@ -57,12 +57,12 @@ const orderedKeys = [
|
|||
},
|
||||
];
|
||||
const orderedDates = [
|
||||
'2018-05-30',
|
||||
'2018-05-31',
|
||||
'2018-06-01',
|
||||
'2018-06-02',
|
||||
'2018-06-03',
|
||||
'2018-06-04',
|
||||
'2018-05-30T00:00:00',
|
||||
'2018-05-31T00:00:00',
|
||||
'2018-06-01T00:00:00',
|
||||
'2018-06-02T00:00:00',
|
||||
'2018-06-03T00:00:00',
|
||||
'2018-06-04T00:00:00',
|
||||
];
|
||||
const testUniqueKeys = getUniqueKeys( dummyOrders );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
|
||||
|
@ -75,7 +75,7 @@ const testYScale = getYScale( 100, testYMax );
|
|||
|
||||
describe( 'parseDate', () => {
|
||||
it( 'correctly parse date in the expected format', () => {
|
||||
const testDate = parseDate( '2018-06-30' );
|
||||
const testDate = parseDate( '2018-06-30T00:00:00' );
|
||||
const expectedDate = new Date( Date.UTC( 2018, 5, 30 ) );
|
||||
expect( testDate.getTime() ).toEqual( expectedDate.getTime() );
|
||||
} );
|
||||
|
@ -182,13 +182,13 @@ describe( 'getYTickOffset', () => {
|
|||
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 ].date ).toEqual( '2018-05-30T00:00:00' );
|
||||
expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
|
||||
expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
|
||||
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02' );
|
||||
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
|
||||
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 ].date ).toEqual( '2018-06-04T00:00:00' );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
|
||||
} );
|
||||
|
|
|
@ -15,9 +15,9 @@ import {
|
|||
} from 'd3-scale';
|
||||
import { mouse as d3Mouse, select as d3Select } from 'd3-selection';
|
||||
import { line as d3Line } from 'd3-shape';
|
||||
import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format';
|
||||
import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||
|
||||
export const parseDate = d3UTCParse( '%Y-%m-%d' );
|
||||
export const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
||||
|
||||
/**
|
||||
* Describes `getUniqueKeys`
|
||||
|
@ -214,7 +214,7 @@ export const drawAxis = ( node, params ) => {
|
|||
.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 ) ) )
|
||||
.tickFormat( d => params.xFormat( d instanceof Date ? d : new Date( d ) ) )
|
||||
);
|
||||
|
||||
node
|
||||
|
@ -235,7 +235,7 @@ export const drawAxis = ( node, params ) => {
|
|||
.call(
|
||||
d3AxisLeft( params.yTickOffset )
|
||||
.tickValues( yGrids )
|
||||
.tickFormat( d => ( d !== 0 ? d3Format( '.3s' )( d ) : 0 ) )
|
||||
.tickFormat( d => ( d !== 0 ? d3Format( params.yFormat )( d ) : 0 ) )
|
||||
);
|
||||
|
||||
node
|
||||
|
@ -270,7 +270,7 @@ const showTooltip = ( node, params, d ) => {
|
|||
.style( 'top', yPosition + 'px' )
|
||||
.style( 'display', 'inline-block' ).html( `
|
||||
<div>
|
||||
<h4>${ d.date }</h4>
|
||||
<h4>${ params.tooltipFormat( d.date instanceof Date ? d.date : new Date( d.date ) ) }</h4>
|
||||
<ul>
|
||||
${ keys.join( '' ) }
|
||||
</ul>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Component, Fragment } from '@wordpress/element';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import ChartExample from 'components/chart/example-hour';
|
||||
import Header from 'layout/header';
|
||||
import StorePerformance from './store-performance';
|
||||
import TopSellingProducts from './top-selling-products';
|
||||
|
@ -19,6 +20,7 @@ export default class Dashboard extends Component {
|
|||
<Fragment>
|
||||
<Header sections={ [ __( 'Dashboard', 'wc-admin' ) ] } />
|
||||
<StorePerformance />
|
||||
<ChartExample />
|
||||
<div className="woocommerce-dashboard__columns">
|
||||
<div>
|
||||
<TopSellingProducts />
|
||||
|
|
Loading…
Reference in New Issue