Hide day from X axis when the time period is too long (https://github.com/woocommerce/woocommerce-admin/pull/525)

* Refactor getXTicks

* Hide day from X axis when the time period is too long

* Fix X axis labels misaligned in charts

* Align axis ticks to the first day of the month when not displaying the day number

* Store threshold in a variable

* Add missing JSDoc

* Fix charts hanging when the number of days selected was exactly 180

* Improve function naming

* Fix dayTicksThreshold mismatch between date format and chart layout
This commit is contained in:
Albert Juhé Lluveras 2018-10-17 15:44:43 +02:00 committed by GitHub
parent e8fe6bac83
commit 36c37afe49
4 changed files with 105 additions and 54 deletions

View File

@ -40,7 +40,7 @@ class ReportChart extends Component {
const currentInterval = getIntervalForQuery( query ); const currentInterval = getIntervalForQuery( query );
const allowedIntervals = getAllowedIntervalsForQuery( query ); const allowedIntervals = getAllowedIntervalsForQuery( query );
const formats = getDateFormatsForInterval( currentInterval ); const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
const { primary, secondary } = getCurrentDates( query ); const { primary, secondary } = getCurrentDates( query );
const primaryKey = `${ primary.label } (${ primary.range })`; const primaryKey = `${ primary.label } (${ primary.range })`;
const secondaryKey = `${ secondary.label } (${ secondary.range })`; const secondaryKey = `${ secondary.label } (${ secondary.range })`;

View File

@ -99,6 +99,7 @@ class D3Chart extends Component {
dateParser, dateParser,
height, height,
layout, layout,
interval,
margin, margin,
mode, mode,
orderedKeys, orderedKeys,
@ -125,7 +126,7 @@ class D3Chart extends Component {
const uniqueDates = getUniqueDates( lineData, parseDate ); const uniqueDates = getUniqueDates( lineData, parseDate );
const xLineScale = getXLineScale( uniqueDates, adjWidth ); const xLineScale = getXLineScale( uniqueDates, adjWidth );
const xScale = getXScale( uniqueDates, adjWidth ); const xScale = getXScale( uniqueDates, adjWidth );
const xTicks = getXTicks( uniqueDates, adjWidth, layout ); const xTicks = getXTicks( uniqueDates, adjWidth, layout, interval );
return { return {
colorScheme, colorScheme,
dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ), dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ),

View File

@ -20,6 +20,7 @@ import { format as formatDate } from '@wordpress/date';
* Internal dependencies * Internal dependencies
*/ */
import { formatCurrency } from 'lib/currency'; import { formatCurrency } from 'lib/currency';
import { dayTicksThreshold } from 'lib/date';
/** /**
* Describes `smallestFactor` * Describes `smallestFactor`
@ -27,15 +28,16 @@ import { formatCurrency } from 'lib/currency';
* @returns {integer} smallest factor of num * @returns {integer} smallest factor of num
*/ */
export const getFactors = inputNum => { export const getFactors = inputNum => {
const num_factors = []; const numFactors = [];
for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i += 1 ) { for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i += 1 ) {
if ( inputNum % i === 0 ) { if ( inputNum % i === 0 ) {
num_factors.push( i ); numFactors.push( i );
inputNum / i !== i && num_factors.push( inputNum / i ); inputNum / i !== i && numFactors.push( inputNum / i );
} }
} }
num_factors.sort( ( x, y ) => x - y ); // numeric sort numFactors.sort( ( x, y ) => x - y ); // numeric sort
return num_factors;
return numFactors;
}; };
/** /**
@ -207,71 +209,113 @@ export const getLine = ( xLineScale, yScale ) =>
.y( d => yScale( d.value ) ); .y( d => yScale( d.value ) );
/** /**
* Describes getXTicks * Calculate the maximum number of ticks allowed in the x-axis based on the width and layout of the chart
* @param {array} uniqueDates - all the unique dates from the input data for the chart
* @param {integer} width - calculated page width * @param {integer} width - calculated page width
* @param {string} layout - standard, comparison or compact chart types * @param {string} layout - standard, comparison or compact chart types
* @returns {integer} number of x-axis ticks based on width and chart layout * @returns {integer} number of x-axis ticks based on width and chart layout
*/ */
export const getXTicks = ( uniqueDates, width, layout ) => { const calculateMaxXTicks = ( width, layout ) => {
// caluclate the maximum number of ticks allowed in the x-axis based on the width
// and layout of the chart
let ticks = 16;
if ( width < 783 ) { if ( width < 783 ) {
ticks = 7; return 7;
} else if ( width >= 783 && width < 1129 ) { } else if ( width >= 783 && width < 1129 ) {
ticks = 12; return 12;
} else if ( width >= 1130 && width < 1365 ) { } else if ( width >= 1130 && width < 1365 ) {
if ( layout === 'standard' ) { if ( layout === 'standard' ) {
ticks = 16; return 16;
} else if ( layout === 'comparison' ) { } else if ( layout === 'comparison' ) {
ticks = 12; return 12;
} else if ( layout === 'compact' ) { } else if ( layout === 'compact' ) {
ticks = 7; return 7;
} }
} else if ( width >= 1365 ) { } else if ( width >= 1365 ) {
if ( layout === 'standard' ) { if ( layout === 'standard' ) {
ticks = 31; return 31;
} else if ( layout === 'comparison' ) { } else if ( layout === 'comparison' ) {
ticks = 16; return 16;
} else if ( layout === 'compact' ) { } else if ( layout === 'compact' ) {
ticks = 12; return 12;
} }
} }
if ( uniqueDates.length <= ticks ) {
return uniqueDates; return 16;
};
/**
* Filter out irrelevant dates so only the first date of each month is kept.
* @param {array} dates - string dates.
* @returns {array} Filtered dates.
*/
const getFirstDatePerMonth = dates => {
return dates.filter(
( date, i ) => i === 0 || new Date( date ).getMonth() !== new Date( dates[ i - 1 ] ).getMonth()
);
};
/**
* Get x-axis ticks given the unique dates and the increment factor.
* @param {array} uniqueDates - all the unique dates from the input data for the chart
* @param {integer} incrementFactor - increment factor for the visible ticks.
* @returns {array} Ticks for the x-axis.
*/
const getXTicksFromIncrementFactor = ( uniqueDates, incrementFactor ) => {
const ticks = [];
for ( let idx = 0; idx < uniqueDates.length; idx = idx + incrementFactor ) {
ticks.push( uniqueDates[ idx ] );
} }
// If the first or last date is missing from the ticks array, add it back in.
if ( ticks[ 0 ] !== uniqueDates[ 0 ] ) {
ticks.unshift( uniqueDates[ 0 ] );
}
if ( ticks[ ticks.length - 1 ] !== uniqueDates[ uniqueDates.length - 1 ] ) {
ticks.push( uniqueDates[ uniqueDates.length - 1 ] );
}
return ticks;
};
/**
* Calculates the increment factor between ticks so there aren't more than maxTicks.
* @param {array} uniqueDates - all the unique dates from the input data for the chart
* @param {integer} maxTicks - maximum number of ticks that can be displayed in the x-axis
* @returns {integer} x-axis ticks increment factor
*/
const calculateXTicksIncrementFactor = ( uniqueDates, maxTicks ) => {
let factors = []; let factors = [];
let i = 0; let i = 1;
// first we get all the factors of the length of the uniqieDates array // First we get all the factors of the length of the uniqueDates array
// if the number is a prime number or near prime (with 3 factors) then we // if the number is a prime number or near prime (with 3 factors) then we
// step down by 1 integer and try again // step down by 1 integer and try again.
while ( factors.length <= 3 ) { while ( factors.length <= 3 ) {
factors = getFactors( uniqueDates.length - ( 1 + i ) ); factors = getFactors( uniqueDates.length - i );
i += 1; i += 1;
} }
let newTicks = [];
let factorIndex = 0; return factors.find( f => uniqueDates.length / f < maxTicks );
// newTicks is the first tick plus the smallest factor (initiallY) etc. };
// however, if we still end up with too many ticks we look at the next factor
// and try again unttil we have fewer ticks than the max /**
while ( newTicks.length > ticks || newTicks.length === 0 ) { * Returns ticks for the x-axis.
if ( newTicks.length > ticks ) { * @param {array} uniqueDates - all the unique dates from the input data for the chart
factorIndex += 1; * @param {integer} width - calculated page width
newTicks = []; * @param {string} layout - standard, comparison or compact chart types
} * @param {string} interval - string of the interval used in the graph (hour, day, week...)
for ( let idx = 0; idx < uniqueDates.length; idx = idx + factors[ factorIndex ] ) { * @returns {integer} number of x-axis ticks based on width and chart layout
newTicks.push( uniqueDates[ idx ] ); */
} export const getXTicks = ( uniqueDates, width, layout, interval ) => {
const maxTicks = calculateMaxXTicks( width, layout );
if ( uniqueDates.length >= dayTicksThreshold && interval === 'day' ) {
uniqueDates = getFirstDatePerMonth( uniqueDates );
} }
// if, for some reason, the first or last date is missing from the newTicks array, add it back in if ( uniqueDates.length <= maxTicks ) {
if ( newTicks[ 0 ] !== uniqueDates[ 0 ] ) { return uniqueDates;
newTicks.unshift( uniqueDates[ 0 ] );
} }
if ( newTicks[ newTicks.length - 1 ] !== uniqueDates[ uniqueDates.length - 1 ] ) {
newTicks.push( uniqueDates[ uniqueDates.length - 1 ] ); const incrementFactor = calculateXTicksIncrementFactor( uniqueDates, maxTicks );
}
return newTicks; return getXTicksFromIncrementFactor( uniqueDates, incrementFactor );
}; };
/** /**
@ -316,7 +360,7 @@ export const drawAxis = ( node, params ) => {
.append( 'g' ) .append( 'g' )
.attr( 'class', 'axis' ) .attr( 'class', 'axis' )
.attr( 'aria-hidden', 'true' ) .attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(0,${ params.height })` ) .attr( 'transform', `translate(0, ${ params.height })` )
.call( .call(
d3AxisBottom( xScale ) d3AxisBottom( xScale )
.tickValues( ticks ) .tickValues( ticks )
@ -327,7 +371,7 @@ export const drawAxis = ( node, params ) => {
.append( 'g' ) .append( 'g' )
.attr( 'class', 'axis axis-month' ) .attr( 'class', 'axis axis-month' )
.attr( 'aria-hidden', 'true' ) .attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(3, ${ params.height + 20 })` ) .attr( 'transform', `translate(0, ${ params.height + 20 })` )
.call( .call(
d3AxisBottom( xScale ) d3AxisBottom( xScale )
.tickValues( ticks ) .tickValues( ticks )
@ -335,9 +379,7 @@ export const drawAxis = ( node, params ) => {
const monthDate = d instanceof Date ? d : new Date( d ); const monthDate = d instanceof Date ? d : new Date( d );
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ]; let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
prevMonth = prevMonth instanceof Date ? prevMonth : new Date( prevMonth ); prevMonth = prevMonth instanceof Date ? prevMonth : new Date( prevMonth );
return monthDate.getDate() === 1 || return i === 0 || params.x2Format( monthDate ) !== params.x2Format( prevMonth )
i === 0 ||
params.x2Format( monthDate ) !== params.x2Format( prevMonth )
? params.x2Format( monthDate ) ? params.x2Format( monthDate )
: ''; : '';
} ) } )

View File

@ -389,14 +389,17 @@ export function getIntervalForQuery( query ) {
return current; return current;
} }
export const dayTicksThreshold = 180;
/** /**
* Returns date formats for the current interval. * Returns date formats for the current interval.
* See https://github.com/d3/d3-time-format for chart formats. * See https://github.com/d3/d3-time-format for chart formats.
* *
* @param {String} interval Interval to get date formats for. * @param {String} interval Interval to get date formats for.
* @param {Int} [ticks] Number of ticks the axis will have.
* @return {String} Current interval. * @return {String} Current interval.
*/ */
export function getDateFormatsForInterval( interval ) { export function getDateFormatsForInterval( interval, ticks = 0 ) {
let pointLabelFormat = 'F j, Y'; let pointLabelFormat = 'F j, Y';
let tooltipFormat = '%B %d %Y'; let tooltipFormat = '%B %d %Y';
let xFormat = '%Y-%m-%d'; let xFormat = '%Y-%m-%d';
@ -411,7 +414,12 @@ export function getDateFormatsForInterval( interval ) {
tableFormat = 'h A'; tableFormat = 'h A';
break; break;
case 'day': case 'day':
xFormat = '%d'; if ( ticks < dayTicksThreshold ) {
xFormat = '%d';
} else {
xFormat = '%b';
x2Format = '%Y';
}
break; break;
case 'week': case 'week':
xFormat = '%d'; xFormat = '%d';