From baf44d030fecb78a9c32a4db25128687367f8c96 Mon Sep 17 00:00:00 2001 From: Justin Shreve Date: Tue, 4 Sep 2018 14:03:52 -0400 Subject: [PATCH] Add interval handling to the revenue report. --- .../client/analytics/report/revenue/index.js | 71 ++++++++-- .../client/components/chart/charts.js | 2 +- .../client/components/chart/index.js | 62 ++++++++- .../client/components/chart/style.scss | 9 ++ .../client/lib/date/index.js | 125 +++++++++++++++++- .../client/lib/date/test/index.js | 39 +++++- 6 files changed, 284 insertions(+), 24 deletions(-) diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js index 83a79d9a4c7..8c864472b0e 100644 --- a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js +++ b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js @@ -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 ( - + ); } @@ -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, }; diff --git a/plugins/woocommerce-admin/client/components/chart/charts.js b/plugins/woocommerce-admin/client/components/chart/charts.js index ce144fd071d..92222f4653c 100644 --- a/plugins/woocommerce-admin/client/components/chart/charts.js +++ b/plugins/woocommerce-admin/client/components/chart/charts.js @@ -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. */ diff --git a/plugins/woocommerce-admin/client/components/chart/index.js b/plugins/woocommerce-admin/client/components/chart/index.js index 747541b8df4..04cd2203cca 100644 --- a/plugins/woocommerce-admin/client/components/chart/index.js +++ b/plugins/woocommerce-admin/client/components/chart/index.js @@ -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 ( + ( { + 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 {
{ title } { width > WIDE_BREAKPOINT && legendDirection === 'row' && legend } + { this.renderIntervalSelector() }
{ * 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 diff --git a/plugins/woocommerce-admin/client/lib/date/test/index.js b/plugins/woocommerce-admin/client/lib/date/test/index.js index bedba741d3d..273ce2ad60d 100644 --- a/plugins/woocommerce-admin/client/lib/date/test/index.js +++ b/plugins/woocommerce-admin/client/lib/date/test/index.js @@ -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' ); } ); } );