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 allowedIntervals = getAllowedIntervalsForQuery( query );
const formats = getDateFormatsForInterval( currentInterval );
const formats = getDateFormatsForInterval( currentInterval, primaryData.data.intervals.length );
const { primary, secondary } = getCurrentDates( query );
const primaryKey = `${ primary.label } (${ primary.range })`;
const secondaryKey = `${ secondary.label } (${ secondary.range })`;

View File

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

View File

@ -20,6 +20,7 @@ import { format as formatDate } from '@wordpress/date';
* Internal dependencies
*/
import { formatCurrency } from 'lib/currency';
import { dayTicksThreshold } from 'lib/date';
/**
* Describes `smallestFactor`
@ -27,15 +28,16 @@ import { formatCurrency } from 'lib/currency';
* @returns {integer} smallest factor of num
*/
export const getFactors = inputNum => {
const num_factors = [];
const numFactors = [];
for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i += 1 ) {
if ( inputNum % i === 0 ) {
num_factors.push( i );
inputNum / i !== i && num_factors.push( inputNum / i );
numFactors.push( i );
inputNum / i !== i && numFactors.push( inputNum / i );
}
}
num_factors.sort( ( x, y ) => x - y ); // numeric sort
return num_factors;
numFactors.sort( ( x, y ) => x - y ); // numeric sort
return numFactors;
};
/**
@ -207,71 +209,113 @@ export const getLine = ( xLineScale, yScale ) =>
.y( d => yScale( d.value ) );
/**
* Describes getXTicks
* @param {array} uniqueDates - all the unique dates from the input data for the chart
* Calculate the maximum number of ticks allowed in the x-axis based on the width and layout of the chart
* @param {integer} width - calculated page width
* @param {string} layout - standard, comparison or compact chart types
* @returns {integer} number of x-axis ticks based on width and chart layout
*/
export const getXTicks = ( uniqueDates, 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;
const calculateMaxXTicks = ( width, layout ) => {
if ( width < 783 ) {
ticks = 7;
return 7;
} else if ( width >= 783 && width < 1129 ) {
ticks = 12;
return 12;
} else if ( width >= 1130 && width < 1365 ) {
if ( layout === 'standard' ) {
ticks = 16;
return 16;
} else if ( layout === 'comparison' ) {
ticks = 12;
return 12;
} else if ( layout === 'compact' ) {
ticks = 7;
return 7;
}
} else if ( width >= 1365 ) {
if ( layout === 'standard' ) {
ticks = 31;
return 31;
} else if ( layout === 'comparison' ) {
ticks = 16;
return 16;
} 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 i = 0;
// first we get all the factors of the length of the uniqieDates array
let i = 1;
// 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
// step down by 1 integer and try again
// step down by 1 integer and try again.
while ( factors.length <= 3 ) {
factors = getFactors( uniqueDates.length - ( 1 + i ) );
factors = getFactors( uniqueDates.length - i );
i += 1;
}
let newTicks = [];
let factorIndex = 0;
// 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 ) {
if ( newTicks.length > ticks ) {
factorIndex += 1;
newTicks = [];
}
for ( let idx = 0; idx < uniqueDates.length; idx = idx + factors[ factorIndex ] ) {
newTicks.push( uniqueDates[ idx ] );
}
return factors.find( f => uniqueDates.length / f < maxTicks );
};
/**
* Returns ticks for the x-axis.
* @param {array} uniqueDates - all the unique dates from the input data for the chart
* @param {integer} width - calculated page width
* @param {string} layout - standard, comparison or compact chart types
* @param {string} interval - string of the interval used in the graph (hour, day, week...)
* @returns {integer} number of x-axis ticks based on width and chart layout
*/
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 ( newTicks[ 0 ] !== uniqueDates[ 0 ] ) {
newTicks.unshift( uniqueDates[ 0 ] );
if ( uniqueDates.length <= maxTicks ) {
return uniqueDates;
}
if ( newTicks[ newTicks.length - 1 ] !== uniqueDates[ uniqueDates.length - 1 ] ) {
newTicks.push( uniqueDates[ uniqueDates.length - 1 ] );
}
return newTicks;
const incrementFactor = calculateXTicksIncrementFactor( uniqueDates, maxTicks );
return getXTicksFromIncrementFactor( uniqueDates, incrementFactor );
};
/**
@ -316,7 +360,7 @@ export const drawAxis = ( node, params ) => {
.append( 'g' )
.attr( 'class', 'axis' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(0,${ params.height })` )
.attr( 'transform', `translate(0, ${ params.height })` )
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
@ -327,7 +371,7 @@ export const drawAxis = ( node, params ) => {
.append( 'g' )
.attr( 'class', 'axis axis-month' )
.attr( 'aria-hidden', 'true' )
.attr( 'transform', `translate(3, ${ params.height + 20 })` )
.attr( 'transform', `translate(0, ${ params.height + 20 })` )
.call(
d3AxisBottom( xScale )
.tickValues( ticks )
@ -335,9 +379,7 @@ export const drawAxis = ( node, params ) => {
const monthDate = d instanceof Date ? d : new Date( d );
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
prevMonth = prevMonth instanceof Date ? prevMonth : new Date( prevMonth );
return monthDate.getDate() === 1 ||
i === 0 ||
params.x2Format( monthDate ) !== params.x2Format( prevMonth )
return i === 0 || params.x2Format( monthDate ) !== params.x2Format( prevMonth )
? params.x2Format( monthDate )
: '';
} )

View File

@ -389,14 +389,17 @@ export function getIntervalForQuery( query ) {
return current;
}
export const dayTicksThreshold = 180;
/**
* 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.
* @param {Int} [ticks] Number of ticks the axis will have.
* @return {String} Current interval.
*/
export function getDateFormatsForInterval( interval ) {
export function getDateFormatsForInterval( interval, ticks = 0 ) {
let pointLabelFormat = 'F j, Y';
let tooltipFormat = '%B %d %Y';
let xFormat = '%Y-%m-%d';
@ -411,7 +414,12 @@ export function getDateFormatsForInterval( interval ) {
tableFormat = 'h A';
break;
case 'day':
xFormat = '%d';
if ( ticks < dayTicksThreshold ) {
xFormat = '%d';
} else {
xFormat = '%b';
x2Format = '%Y';
}
break;
case 'week':
xFormat = '%d';