Component - Chart: factorial spacing of x-axis ticks (https://github.com/woocommerce/woocommerce-admin/pull/398)
* remove circles on more than 50 x points * x-axis interval spacing * factorial spacing of x-axis ticks * limit pipes too * catch recursion infinity issue and add layout
This commit is contained in:
parent
8eb0906287
commit
ac8952d7ec
|
@ -23,6 +23,7 @@ import {
|
|||
getOrderedKeys,
|
||||
getLine,
|
||||
getLineData,
|
||||
getXTicks,
|
||||
getUniqueKeys,
|
||||
getUniqueDates,
|
||||
getXScale,
|
||||
|
@ -97,6 +98,7 @@ class D3Chart extends Component {
|
|||
data,
|
||||
dateParser,
|
||||
height,
|
||||
layout,
|
||||
margin,
|
||||
orderedKeys,
|
||||
tooltipFormat,
|
||||
|
@ -120,6 +122,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 );
|
||||
return {
|
||||
colorScheme,
|
||||
dateSpaces: getDateSpaces( uniqueDates, adjWidth, xLineScale ),
|
||||
|
@ -139,6 +142,7 @@ class D3Chart extends Component {
|
|||
x2Format: d3TimeFormat( x2Format ),
|
||||
xGroupScale: getXGroupScale( orderedKeys, xScale ),
|
||||
xLineScale,
|
||||
xTicks,
|
||||
xScale,
|
||||
yMax,
|
||||
yScale,
|
||||
|
@ -195,6 +199,11 @@ D3Chart.propTypes = {
|
|||
* Interval specification (hourly, daily, weekly etc.)
|
||||
*/
|
||||
interval: PropTypes.oneOf( [ 'hour', 'day', 'week', 'month', 'quarter', 'year' ] ),
|
||||
/**
|
||||
* `standard` (default) legend layout in the header or `comparison` moves legend layout
|
||||
* to the left or 'compact' has the legend below
|
||||
*/
|
||||
layout: PropTypes.oneOf( [ 'standard', 'comparison', 'compact' ] ),
|
||||
/**
|
||||
* Margins for axis and chart padding.
|
||||
*/
|
||||
|
@ -244,6 +253,7 @@ D3Chart.defaultProps = {
|
|||
right: 0,
|
||||
top: 20,
|
||||
},
|
||||
layout: 'standard',
|
||||
tooltipFormat: '%Y-%m-%d',
|
||||
type: 'line',
|
||||
width: 600,
|
||||
|
|
|
@ -291,9 +291,10 @@ Chart.propTypes = {
|
|||
*/
|
||||
yFormat: PropTypes.string,
|
||||
/**
|
||||
* `standard` (default) legend layout in the header or `comparison` moves legend layout to the left
|
||||
* `standard` (default) legend layout in the header or `comparison` moves legend layout
|
||||
* to the left or 'compact' has the legend below
|
||||
*/
|
||||
layout: PropTypes.oneOf( [ 'standard', 'comparison' ] ),
|
||||
layout: PropTypes.oneOf( [ 'standard', 'comparison', 'compact' ] ),
|
||||
/**
|
||||
* A title describing this chart.
|
||||
*/
|
||||
|
|
|
@ -20,6 +20,23 @@ import { line as d3Line } from 'd3-shape';
|
|||
*/
|
||||
import { formatCurrency } from 'lib/currency';
|
||||
|
||||
/**
|
||||
* Describes `smallestFactor`
|
||||
* @param {number} inputNum - any double or integer
|
||||
* @returns {integer} smallest factor of num
|
||||
*/
|
||||
export const getFactors = inputNum => {
|
||||
const num_factors = [];
|
||||
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 );
|
||||
}
|
||||
}
|
||||
num_factors.sort( ( x, y ) => x - y ); // numeric sort
|
||||
return num_factors;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes `getUniqueKeys`
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
|
@ -187,6 +204,74 @@ export const getLine = ( xLineScale, yScale ) =>
|
|||
.x( d => xLineScale( new Date( d.date ) ) )
|
||||
.y( d => yScale( d.value ) );
|
||||
|
||||
/**
|
||||
* Describes getXTicks
|
||||
* @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
|
||||
* @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;
|
||||
if ( width < 783 ) {
|
||||
ticks = 7;
|
||||
} else if ( width >= 783 && width < 1129 ) {
|
||||
ticks = 12;
|
||||
} else if ( width >= 1130 && width < 1365 ) {
|
||||
if ( layout === 'standard' ) {
|
||||
ticks = 16;
|
||||
} else if ( layout === 'comparison' ) {
|
||||
ticks = 12;
|
||||
} else if ( layout === 'compact' ) {
|
||||
ticks = 7;
|
||||
}
|
||||
} else if ( width >= 1365 ) {
|
||||
if ( layout === 'standard' ) {
|
||||
ticks = 31;
|
||||
} else if ( layout === 'comparison' ) {
|
||||
ticks = 16;
|
||||
} else if ( layout === 'compact' ) {
|
||||
ticks = 12;
|
||||
}
|
||||
}
|
||||
if ( uniqueDates.length <= ticks ) {
|
||||
return uniqueDates;
|
||||
}
|
||||
let factors = [];
|
||||
let i = 0;
|
||||
// first we get all the factors of the length of the uniqieDates array
|
||||
// if the number is a prime number or near prime (with 3 factors) then we
|
||||
// step down by 1 integer and try again
|
||||
while ( factors.length <= 3 ) {
|
||||
factors = getFactors( uniqueDates.length - ( 1 + 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 ] );
|
||||
}
|
||||
}
|
||||
// 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 ( newTicks[ newTicks.length - 1 ] !== uniqueDates[ uniqueDates.length - 1 ] ) {
|
||||
newTicks.push( uniqueDates[ uniqueDates.length - 1 ] );
|
||||
}
|
||||
return newTicks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes getDateSpaces
|
||||
* @param {array} uniqueDates - from `getUniqueDates`
|
||||
|
@ -223,13 +308,15 @@ export const drawAxis = ( node, params ) => {
|
|||
yGrids.push( i / 3 * params.yMax );
|
||||
}
|
||||
|
||||
const ticks = params.xTicks.map( d => ( params.type === 'line' ? new Date( d ) : d ) );
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis' )
|
||||
.attr( 'transform', `translate(0,${ params.height })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
.tickValues( params.uniqueDates.map( d => ( params.type === 'line' ? new Date( d ) : d ) ) )
|
||||
.tickValues( ticks )
|
||||
.tickFormat( d => params.xFormat( d instanceof Date ? d : new Date( d ) ) )
|
||||
);
|
||||
|
||||
|
@ -239,7 +326,7 @@ export const drawAxis = ( node, params ) => {
|
|||
.attr( 'transform', `translate(3, ${ params.height + 20 })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
.tickValues( params.uniqueDates.map( d => ( params.type === 'line' ? new Date( d ) : d ) ) )
|
||||
.tickValues( ticks )
|
||||
.tickFormat( ( d, i ) => {
|
||||
const monthDate = d instanceof Date ? d : new Date( d );
|
||||
return monthDate.getDate() === 1 || i === 0 ? params.x2Format( monthDate ) : '';
|
||||
|
@ -257,7 +344,7 @@ export const drawAxis = ( node, params ) => {
|
|||
.attr( 'transform', `translate(0, ${ params.height })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
.tickValues( params.uniqueDates.map( d => ( params.type === 'line' ? new Date( d ) : d ) ) )
|
||||
.tickValues( ticks )
|
||||
.tickSize( 5 )
|
||||
.tickFormat( '' )
|
||||
);
|
||||
|
@ -384,25 +471,26 @@ export const drawLines = ( node, data, params ) => {
|
|||
} )
|
||||
.attr( 'd', d => params.line( d.values ) );
|
||||
|
||||
series
|
||||
.selectAll( 'circle' )
|
||||
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
|
||||
.enter()
|
||||
.append( 'circle' )
|
||||
.attr( 'r', 6 )
|
||||
.attr( 'fill', d => getColor( d.key, params ) )
|
||||
.attr( 'stroke', '#fff' )
|
||||
.attr( 'stroke-width', 3 )
|
||||
.style( 'opacity', d => {
|
||||
const opacity = d.focus ? 1 : 0.1;
|
||||
return d.visible ? opacity : 0;
|
||||
} )
|
||||
.attr( 'cx', d => params.xLineScale( new Date( d.date ) ) )
|
||||
.attr( 'cy', d => params.yScale( d.value ) )
|
||||
.on( 'mouseover', ( d, i, nodes ) =>
|
||||
handleMouseOverLineChart( d, i, nodes, node, data, params )
|
||||
)
|
||||
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
|
||||
params.uniqueDates.length < 50 &&
|
||||
series
|
||||
.selectAll( 'circle' )
|
||||
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
|
||||
.enter()
|
||||
.append( 'circle' )
|
||||
.attr( 'r', 6 )
|
||||
.attr( 'fill', d => getColor( d.key, params ) )
|
||||
.attr( 'stroke', '#fff' )
|
||||
.attr( 'stroke-width', 3 )
|
||||
.style( 'opacity', d => {
|
||||
const opacity = d.focus ? 1 : 0.1;
|
||||
return d.visible ? opacity : 0;
|
||||
} )
|
||||
.attr( 'cx', d => params.xLineScale( new Date( d.date ) ) )
|
||||
.attr( 'cy', d => params.yScale( d.value ) )
|
||||
.on( 'mouseover', ( d, i, nodes ) =>
|
||||
handleMouseOverLineChart( d, i, nodes, node, data, params )
|
||||
)
|
||||
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
|
||||
|
||||
const focus = node
|
||||
.append( 'g' )
|
||||
|
|
Loading…
Reference in New Issue