Add interval handling to the revenue report.

This commit is contained in:
Justin Shreve 2018-09-04 14:03:52 -04:00 committed by Paul Sealock
parent ed1b847067
commit baf44d030f
6 changed files with 284 additions and 24 deletions

View File

@ -29,7 +29,14 @@ import { downloadCSVFile, generateCSVDataFromTable, generateCSVFileName } from '
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency'; import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
import { getAdminLink, getNewPath, updateQueryString } from 'lib/nav-utils'; import { getAdminLink, getNewPath, updateQueryString } from 'lib/nav-utils';
import { getAllReportData, isReportDataEmpty } from 'store/reports/utils'; import { getAllReportData, isReportDataEmpty } from 'store/reports/utils';
import { getCurrentDates, isoDateFormat, getPreviousDate, getDateDifferenceInDays } from 'lib/date'; import {
getCurrentDates,
isoDateFormat,
getPreviousDate,
getIntervalForQuery,
getAllowedIntervalsForQuery,
getDateFormatsForInterval,
} from 'lib/date';
import { MAX_PER_PAGE } from 'store/constants'; import { MAX_PER_PAGE } from 'store/constants';
export class RevenueReport extends Component { export class RevenueReport extends Component {
@ -126,6 +133,10 @@ export class RevenueReport extends Component {
} }
getRowsContent( data = [] ) { getRowsContent( data = [] ) {
const { query } = this.props;
const currentInterval = getIntervalForQuery( query );
const formats = getDateFormatsForInterval( currentInterval );
return map( data, row => { return map( data, row => {
const { const {
coupons, coupons,
@ -150,7 +161,7 @@ export class RevenueReport extends Component {
); );
return [ return [
{ {
display: formatDate( 'm/d/Y', row.date_start ), display: formatDate( formats.tableFormat, row.date_start ),
value: row.date_start, value: row.date_start,
}, },
{ {
@ -280,23 +291,46 @@ export class RevenueReport extends Component {
renderChart() { renderChart() {
const { primaryData, secondaryData, query } = this.props; const { primaryData, secondaryData, query } = this.props;
const currentInterval = getIntervalForQuery( query );
const allowedIntervals = getAllowedIntervalsForQuery( query );
const formats = getDateFormatsForInterval( currentInterval );
const { primary, secondary } = getCurrentDates( query ); const { primary, secondary } = getCurrentDates( query );
const selectedChart = this.getSelectedChart(); const selectedChart = this.getSelectedChart();
const primaryKey = `${ primary.label } (${ primary.range })`; const primaryKey = `${ primary.label } (${ primary.range })`;
const secondaryKey = `${ secondary.label } (${ secondary.range })`; const secondaryKey = `${ secondary.label } (${ secondary.range })`;
const difference = getDateDifferenceInDays( primary.after, secondary.after ); const chartData = primaryData.data.intervals.map( function( interval, index ) {
const secondaryDate = getPreviousDate(
const chartData = primaryData.data.intervals.map( function( interval ) { formatDate( 'Y-m-d', interval.date_start ),
const date = formatDate( 'Y-m-d', interval.date_start ); primary.after,
const secondaryDate = getPreviousDate( date, difference, query.compare ); secondary.after,
const secondaryInterval = find( secondaryData.data.intervals, { query.compare,
date_start: secondaryDate.format( isoDateFormat ) + ' 00:00:00', currentInterval
} ); );
/**
* When looking at weeks, getting the previous date doesn't always work
* because subtracting the correct number of weeks from `interval.date_start`
* yeilds the start of the week correct week, but not necessarily the of the
* period in question.
*
* When https://github.com/woocommerce/woocommerce/issues/21298 is resolved and
* data will be zero-filled, there is a strong argument for all this logic to be
* removed in favor of simply matching up the indices in each array of data.
*/
const secondaryInterval =
0 === index && 'week' === currentInterval
? secondaryData.data.intervals[ 0 ]
: find( secondaryData.data.intervals, {
date_start:
secondaryDate.format( isoDateFormat ) +
' ' +
formatDate( 'H:i:s', interval.date_start ),
} );
return { return {
date, date: formatDate( 'Y-m-d\\TH:i:s', interval.date_start ),
[ primaryKey ]: interval.subtotals[ selectedChart.key ] || 0, [ primaryKey ]: interval.subtotals[ selectedChart.key ] || 0,
[ secondaryKey ]: [ secondaryKey ]:
( secondaryInterval && secondaryInterval.subtotals[ selectedChart.key ] ) || 0, ( secondaryInterval && secondaryInterval.subtotals[ selectedChart.key ] ) || 0,
@ -305,7 +339,15 @@ export class RevenueReport extends Component {
return ( return (
<Card title=""> <Card title="">
<Chart data={ chartData } title={ selectedChart.label } dateParser={ '%Y-%m-%d' } /> <Chart
data={ chartData }
title={ selectedChart.label }
interval={ currentInterval }
allowedIntervals={ allowedIntervals }
tooltipFormat={ formats.tooltipFormat }
xFormat={ formats.xFormat }
dateParser={ '%Y-%m-%d' }
/>
</Card> </Card>
); );
} }
@ -433,9 +475,10 @@ export default compose(
withSelect( ( select, props ) => { withSelect( ( select, props ) => {
const { query } = props; const { query } = props;
const datesFromQuery = getCurrentDates( query ); const datesFromQuery = getCurrentDates( query );
const interval = getIntervalForQuery( query );
const baseArgs = { const baseArgs = {
order: 'asc', order: 'asc',
interval: 'day', // TODO support other intervals interval: interval,
per_page: MAX_PER_PAGE, per_page: MAX_PER_PAGE,
}; };

View File

@ -194,7 +194,7 @@ D3Chart.propTypes = {
/** /**
* Interval specification (hourly, daily, weekly etc.) * Interval specification (hourly, daily, weekly etc.)
*/ */
interval: PropTypes.oneOf( [ 'hour', 'day', 'week', 'month', 'quater', 'year' ] ), interval: PropTypes.oneOf( [ 'hour', 'day', 'week', 'month', 'quarter', 'year' ] ),
/** /**
* Margins for axis and chart padding. * Margins for axis and chart padding.
*/ */

View File

@ -2,10 +2,11 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEqual, partial } from 'lodash'; import { isEqual, partial } from 'lodash';
import { Component, createRef } from '@wordpress/element'; import { Component, createRef } from '@wordpress/element';
import { IconButton } from '@wordpress/components'; import { IconButton, SelectControl } from '@wordpress/components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic'; import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
import Gridicon from 'gridicons'; import Gridicon from 'gridicons';
@ -16,6 +17,7 @@ import Gridicon from 'gridicons';
import D3Chart from './charts'; import D3Chart from './charts';
import Legend from './legend'; import Legend from './legend';
import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss'; import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss';
import { updateQueryString } from 'lib/nav-utils';
const WIDE_BREAKPOINT = 1100; const WIDE_BREAKPOINT = 1100;
@ -139,9 +141,50 @@ class Chart extends Component {
} ); } );
} }
setInterval( interval ) {
updateQueryString( { interval } );
}
renderIntervalSelector() {
const { interval, allowedIntervals } = this.props;
if ( ! allowedIntervals || allowedIntervals.length < 1 ) {
return null;
}
const intervalLabels = {
hour: __( 'By hour', 'wc-admin' ),
day: __( 'By day', 'wc-admin' ),
week: __( 'By week', 'wc-admin' ),
month: __( 'By month', 'wc-admin' ),
quarter: __( 'By quarter', 'wc-admin' ),
year: __( 'By year', 'wc-admin' ),
};
return (
<SelectControl
className="woocommerce-chart__interval-select"
value={ interval }
options={ allowedIntervals.map( allowedInterval => ( {
value: allowedInterval,
label: intervalLabels[ allowedInterval ],
} ) ) }
onChange={ this.setInterval }
/>
);
}
render() { render() {
const { orderedKeys, type, visibleData, width } = this.state; const { orderedKeys, type, visibleData, width } = this.state;
const { dateParser, layout, title, tooltipFormat, xFormat, x2Format, yFormat } = this.props; const {
dateParser,
layout,
title,
tooltipFormat,
xFormat,
x2Format,
yFormat,
interval,
} = this.props;
const legendDirection = layout === 'standard' && width > WIDE_BREAKPOINT ? 'row' : 'column'; const legendDirection = layout === 'standard' && width > WIDE_BREAKPOINT ? 'row' : 'column';
const chartDirection = layout === 'comparison' && width > WIDE_BREAKPOINT ? 'row' : 'column'; const chartDirection = layout === 'comparison' && width > WIDE_BREAKPOINT ? 'row' : 'column';
const legend = ( const legend = (
@ -165,6 +208,7 @@ class Chart extends Component {
<div className="woocommerce-chart__header"> <div className="woocommerce-chart__header">
<span className="woocommerce-chart__title">{ title }</span> <span className="woocommerce-chart__title">{ title }</span>
{ width > WIDE_BREAKPOINT && legendDirection === 'row' && legend } { width > WIDE_BREAKPOINT && legendDirection === 'row' && legend }
{ this.renderIntervalSelector() }
<div className="woocommerce-chart__types"> <div className="woocommerce-chart__types">
<IconButton <IconButton
className={ classNames( 'woocommerce-chart__type-button', { className={ classNames( 'woocommerce-chart__type-button', {
@ -198,6 +242,7 @@ class Chart extends Component {
orderedKeys={ orderedKeys } orderedKeys={ orderedKeys }
tooltipFormat={ tooltipFormat } tooltipFormat={ tooltipFormat }
type={ type } type={ type }
interval={ interval }
width={ chartDirection === 'row' ? width - 320 : width } width={ chartDirection === 'row' ? width - 320 : width }
xFormat={ xFormat } xFormat={ xFormat }
x2Format={ x2Format } x2Format={ x2Format }
@ -247,6 +292,18 @@ Chart.propTypes = {
* Chart type of either `line` or `bar`. * Chart type of either `line` or `bar`.
*/ */
type: PropTypes.oneOf( [ 'bar', 'line' ] ), type: PropTypes.oneOf( [ 'bar', 'line' ] ),
/**
* Information about the currently selected interval, and set of allowed intervals for the chart. See `getIntervalsForQuery`.
*/
intervalData: PropTypes.object,
/**
* Interval specification (hourly, daily, weekly etc).
*/
interval: PropTypes.oneOf( [ 'hour', 'day', 'week', 'month', 'quarter', 'year' ] ),
/**
* Allowed intervals to show in a dropdown.
*/
allowedIntervals: PropTypes.array,
}; };
Chart.defaultProps = { Chart.defaultProps = {
@ -258,6 +315,7 @@ Chart.defaultProps = {
yFormat: '$.3s', yFormat: '$.3s',
layout: 'standard', layout: 'standard',
type: 'line', type: 'line',
interval: 'day',
}; };
export default Chart; export default Chart;

View File

@ -199,6 +199,15 @@
} }
} }
.woocommerce-chart__interval-select {
margin: 0;
.components-select-control__input {
border: 0;
box-shadow: none;
}
}
.woocommerce-chart__container { .woocommerce-chart__container {
position: relative; position: relative;
width: 100%; width: 100%;

View File

@ -300,18 +300,137 @@ export const getDateDifferenceInDays = ( date, date2 ) => {
* Get the previous date for either the previous period of year. * Get the previous date for either the previous period of year.
* *
* @param {String} date - Base date * @param {String} date - Base date
* @param {Int} difference - The difference in days for the previous period. See `getDateDifferenceInDays`. * @param {String} date1 - primary start
* @param {String} date2 - secondary start
* @param {String} compare - `previous_period` or `previous_year` * @param {String} compare - `previous_period` or `previous_year`
* @param {String} interval - interval
* @return {String} - Calculated date * @return {String} - Calculated date
*/ */
export const getPreviousDate = ( date, difference, compare ) => { export const getPreviousDate = ( date, date1, date2, compare, interval ) => {
const dateMoment = toMoment( isoDateFormat, formatDate( 'Y-m-d', date ) ); const dateMoment = toMoment( isoDateFormat, formatDate( 'Y-m-d', date ) );
const _date1 = toMoment( isoDateFormat, formatDate( 'Y-m-d', date1 ) );
const _date2 = toMoment( isoDateFormat, formatDate( 'Y-m-d', date2 ) );
if ( 'previous_period' === compare ) { if ( 'previous_period' === compare ) {
return dateMoment.clone().subtract( difference, 'days' ); const difference = _date1.diff( _date2, interval );
return dateMoment.clone().subtract( difference, interval );
} }
return dateMoment.clone().subtract( 1, 'years' ); return dateMoment.clone().subtract( 1, 'years' );
}; };
/**
* Returns the allowed selectable intervals for a specific query.
*
* @param {Object} query Current query
* @return {Array} Array containing allowed intervals.
*/
export function getAllowedIntervalsForQuery( query ) {
let allowed = [];
if ( 'custom' === query.period ) {
const { primary } = getCurrentDates( query );
const differenceInDays = getDateDifferenceInDays( primary.before, primary.after );
if ( differenceInDays > 728 ) {
allowed = [ 'day', 'week', 'month', 'quarter', 'year' ];
} else if ( differenceInDays > 364 ) {
allowed = [ 'day', 'week', 'month', 'quarter' ];
} else if ( differenceInDays > 90 ) {
allowed = [ 'day', 'week', 'month' ];
} else if ( differenceInDays > 7 ) {
allowed = [ 'day', 'week' ];
} else if ( differenceInDays > 1 && differenceInDays <= 7 ) {
allowed = [ 'day' ];
} else if ( differenceInDays <= 1 ) {
allowed = [ 'hour' ];
} else {
allowed = [ 'day' ];
}
} else {
switch ( query.period ) {
case 'today':
case 'yesterday':
allowed = [ 'hour' ];
break;
case 'week':
case 'last_week':
allowed = [ 'day' ];
break;
case 'month':
case 'last_month':
allowed = [ 'day', 'week' ];
break;
case 'quarter':
case 'last_quarter':
allowed = [ 'day', 'week', 'month' ];
break;
case 'year':
case 'last_year':
allowed = [ 'day', 'week', 'month', 'quarter' ];
break;
default:
allowed = [ 'day' ];
break;
}
}
return allowed;
}
/**
* Returns the current interval to use.
*
* @param {Object} query Current query
* @return {String} Current interval.
*/
export function getIntervalForQuery( query ) {
const allowed = getAllowedIntervalsForQuery( query );
const defaultInterval = allowed[ 0 ];
let current = query.interval || defaultInterval;
if ( query.interval && ! allowed.includes( query.interval ) ) {
current = defaultInterval;
}
return current;
}
/**
* Returns date formats for the current interval.
* See https://github.com/d3/d3-time-format for chart formats.
*
* @param {String} interval Interval to get date formats for.
* @return {String} Current interval.
*/
export function getDateFormatsForInterval( interval ) {
let tooltipFormat = '%B %d %Y';
let xFormat = '%Y-%m-%d';
let tableFormat = 'm/d/Y';
switch ( interval ) {
case 'hour':
tooltipFormat = '%I %p';
xFormat = '%I %p';
tableFormat = 'h A';
break;
case 'week':
tooltipFormat = __( 'Week of %B %d %Y', 'wc-admin' );
break;
case 'quarter':
case 'month':
tooltipFormat = '%B %Y';
xFormat = '%b %y';
tableFormat = 'M Y';
break;
case 'year':
tooltipFormat = '%Y';
xFormat = '%Y';
tableFormat = 'Y';
break;
}
return {
tooltipFormat,
xFormat,
tableFormat,
};
}
/** /**
* Gutenberg's moment instance is loaded with i18n values. If the locale isn't english * Gutenberg's moment instance is loaded with i18n values. If the locale isn't english
* we can use that data and enhance it with additional translations * we can use that data and enhance it with additional translations

View File

@ -681,12 +681,43 @@ describe( 'getDateDifferenceInDays', () => {
} ); } );
describe( 'getPreviousDate', () => { describe( 'getPreviousDate', () => {
it( 'should return valid date for previous period', () => { it( 'should return valid date for previous period by days', () => {
const previousDate = getPreviousDate( '2018-08-21', 92, 'previous_period' ); const date = '2018-08-21';
expect( previousDate.format( isoDateFormat ) ).toBe( '2018-05-21' ); const primaryStart = '2018-08-25';
const secondaryStart = '2018-08-15';
const previousDate = getPreviousDate(
date,
primaryStart,
secondaryStart,
'previous_period',
'day'
);
expect( previousDate.format( isoDateFormat ) ).toBe( '2018-08-11' );
} );
it( 'should return valid date for previous period by months', () => {
const date = '2018-08-21';
const primaryStart = '2018-08-01';
const secondaryStart = '2018-07-01';
const previousDate = getPreviousDate(
date,
primaryStart,
secondaryStart,
'previous_period',
'month'
);
expect( previousDate.format( isoDateFormat ) ).toBe( '2018-07-21' );
} ); } );
it( 'should return valid date for previous year', () => { it( 'should return valid date for previous year', () => {
const previousDate = getPreviousDate( '2018-08-21', 92, 'previous_year' ); const date = '2018-08-21';
const primaryStart = '2018-08-01';
const secondaryStart = '2018-07-01';
const previousDate = getPreviousDate(
date,
primaryStart,
secondaryStart,
'previous_year',
'day'
);
expect( previousDate.format( isoDateFormat ) ).toBe( '2017-08-21' ); expect( previousDate.format( isoDateFormat ) ).toBe( '2017-08-21' );
} ); } );
} ); } );