Merge pull request woocommerce/woocommerce-admin#359 from woocommerce/add/revenue-report-interval-handling
Add interval selection to the revenue report
This commit is contained in:
commit
176754c85f
|
@ -29,7 +29,14 @@ import { downloadCSVFile, generateCSVDataFromTable, generateCSVFileName } from '
|
|||
import { formatCurrency, getCurrencyFormatDecimal } from 'lib/currency';
|
||||
import { getAdminLink, getNewPath, updateQueryString } from 'lib/nav-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';
|
||||
|
||||
export class RevenueReport extends Component {
|
||||
|
@ -126,6 +133,10 @@ export class RevenueReport extends Component {
|
|||
}
|
||||
|
||||
getRowsContent( data = [] ) {
|
||||
const { query } = this.props;
|
||||
const currentInterval = getIntervalForQuery( query );
|
||||
const formats = getDateFormatsForInterval( currentInterval );
|
||||
|
||||
return map( data, row => {
|
||||
const {
|
||||
coupons,
|
||||
|
@ -150,7 +161,7 @@ export class RevenueReport extends Component {
|
|||
);
|
||||
return [
|
||||
{
|
||||
display: formatDate( 'm/d/Y', row.date_start ),
|
||||
display: formatDate( formats.tableFormat, row.date_start ),
|
||||
value: row.date_start,
|
||||
},
|
||||
{
|
||||
|
@ -280,23 +291,46 @@ export class RevenueReport extends Component {
|
|||
|
||||
renderChart() {
|
||||
const { primaryData, secondaryData, query } = this.props;
|
||||
|
||||
const currentInterval = getIntervalForQuery( query );
|
||||
const allowedIntervals = getAllowedIntervalsForQuery( query );
|
||||
const formats = getDateFormatsForInterval( currentInterval );
|
||||
|
||||
const { primary, secondary } = getCurrentDates( query );
|
||||
const selectedChart = this.getSelectedChart();
|
||||
|
||||
const primaryKey = `${ primary.label } (${ primary.range })`;
|
||||
const secondaryKey = `${ secondary.label } (${ secondary.range })`;
|
||||
|
||||
const difference = getDateDifferenceInDays( primary.after, secondary.after );
|
||||
|
||||
const chartData = primaryData.data.intervals.map( function( interval ) {
|
||||
const date = formatDate( 'Y-m-d', interval.date_start );
|
||||
const secondaryDate = getPreviousDate( date, difference, query.compare );
|
||||
const secondaryInterval = find( secondaryData.data.intervals, {
|
||||
date_start: secondaryDate.format( isoDateFormat ) + ' 00:00:00',
|
||||
} );
|
||||
|
||||
const chartData = primaryData.data.intervals.map( function( interval, index ) {
|
||||
const secondaryDate = getPreviousDate(
|
||||
formatDate( 'Y-m-d', interval.date_start ),
|
||||
primary.after,
|
||||
secondary.after,
|
||||
query.compare,
|
||||
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 {
|
||||
date,
|
||||
date: formatDate( 'Y-m-d\\TH:i:s', interval.date_start ),
|
||||
[ primaryKey ]: interval.subtotals[ selectedChart.key ] || 0,
|
||||
[ secondaryKey ]:
|
||||
( secondaryInterval && secondaryInterval.subtotals[ selectedChart.key ] ) || 0,
|
||||
|
@ -305,7 +339,15 @@ export class RevenueReport extends Component {
|
|||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
@ -433,9 +475,10 @@ export default compose(
|
|||
withSelect( ( select, props ) => {
|
||||
const { query } = props;
|
||||
const datesFromQuery = getCurrentDates( query );
|
||||
const interval = getIntervalForQuery( query );
|
||||
const baseArgs = {
|
||||
order: 'asc',
|
||||
interval: 'day', // TODO support other intervals
|
||||
interval: interval,
|
||||
per_page: MAX_PER_PAGE,
|
||||
};
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@ D3Chart.propTypes = {
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import classNames from 'classnames';
|
||||
import { isEqual, partial } from 'lodash';
|
||||
import { Component, createRef } from '@wordpress/element';
|
||||
import { IconButton } from '@wordpress/components';
|
||||
import { IconButton, SelectControl } from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { interpolateViridis as d3InterpolateViridis } from 'd3-scale-chromatic';
|
||||
import Gridicon from 'gridicons';
|
||||
|
@ -16,6 +17,7 @@ import Gridicon from 'gridicons';
|
|||
import D3Chart from './charts';
|
||||
import Legend from './legend';
|
||||
import { gap, gaplarge } from 'stylesheets/abstracts/_variables.scss';
|
||||
import { updateQueryString } from 'lib/nav-utils';
|
||||
|
||||
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() {
|
||||
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 chartDirection = layout === 'comparison' && width > WIDE_BREAKPOINT ? 'row' : 'column';
|
||||
const legend = (
|
||||
|
@ -165,6 +208,7 @@ class Chart extends Component {
|
|||
<div className="woocommerce-chart__header">
|
||||
<span className="woocommerce-chart__title">{ title }</span>
|
||||
{ width > WIDE_BREAKPOINT && legendDirection === 'row' && legend }
|
||||
{ this.renderIntervalSelector() }
|
||||
<div className="woocommerce-chart__types">
|
||||
<IconButton
|
||||
className={ classNames( 'woocommerce-chart__type-button', {
|
||||
|
@ -198,6 +242,7 @@ class Chart extends Component {
|
|||
orderedKeys={ orderedKeys }
|
||||
tooltipFormat={ tooltipFormat }
|
||||
type={ type }
|
||||
interval={ interval }
|
||||
width={ chartDirection === 'row' ? width - 320 : width }
|
||||
xFormat={ xFormat }
|
||||
x2Format={ x2Format }
|
||||
|
@ -247,6 +292,18 @@ Chart.propTypes = {
|
|||
* Chart type of either `line` or `bar`.
|
||||
*/
|
||||
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 = {
|
||||
|
@ -258,6 +315,7 @@ Chart.defaultProps = {
|
|||
yFormat: '$.3s',
|
||||
layout: 'standard',
|
||||
type: 'line',
|
||||
interval: 'day',
|
||||
};
|
||||
|
||||
export default Chart;
|
||||
|
|
|
@ -199,6 +199,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-chart__interval-select {
|
||||
margin: 0;
|
||||
|
||||
.components-select-control__input {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-chart__container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
|
@ -300,18 +300,137 @@ export const getDateDifferenceInDays = ( date, date2 ) => {
|
|||
* Get the previous date for either the previous period of year.
|
||||
*
|
||||
* @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} interval - interval
|
||||
* @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 _date1 = toMoment( isoDateFormat, formatDate( 'Y-m-d', date1 ) );
|
||||
const _date2 = toMoment( isoDateFormat, formatDate( 'Y-m-d', date2 ) );
|
||||
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' );
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* we can use that data and enhance it with additional translations
|
||||
|
|
|
@ -681,12 +681,43 @@ describe( 'getDateDifferenceInDays', () => {
|
|||
} );
|
||||
|
||||
describe( 'getPreviousDate', () => {
|
||||
it( 'should return valid date for previous period', () => {
|
||||
const previousDate = getPreviousDate( '2018-08-21', 92, 'previous_period' );
|
||||
expect( previousDate.format( isoDateFormat ) ).toBe( '2018-05-21' );
|
||||
it( 'should return valid date for previous period by days', () => {
|
||||
const date = '2018-08-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', () => {
|
||||
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' );
|
||||
} );
|
||||
} );
|
||||
|
|
Loading…
Reference in New Issue