Merge branch 'add/chart-second-x' of github.com:woocommerce/wc-admin into add/chart-second-x

This commit is contained in:
Robert Elliott 2018-09-17 10:51:44 +02:00
commit dbca60a660
9 changed files with 195 additions and 66 deletions

View File

@ -338,7 +338,6 @@ export class RevenueReport extends Component {
} );
return (
<Card title="">
<Chart
data={ chartData }
title={ selectedChart.label }
@ -349,7 +348,6 @@ export class RevenueReport extends Component {
x2Format={ formats.x2Format }
dateParser={ '%Y-%m-%dT%H:%M:%S' }
/>
</Card>
);
}

View File

@ -79,6 +79,7 @@
display: flex;
align-items: center;
justify-content: center;
grid-column-start: 2;
}
.woocommerce-calendar__input {
@ -95,6 +96,14 @@
}
}
&:first-child {
grid-column-start: 1;
}
&:last-child {
grid-column-start: 3;
}
&.is-empty {
.dashicons-calendar path {
fill: $core-grey-dark-300;

View File

@ -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,

View File

@ -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.
*/

View File

@ -1,15 +1,22 @@
/** @format */
.woocommerce-chart {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
margin: -$gap;
border-top: 1px solid $core-grey-light-700;
margin-top: -$gap;
margin-bottom: $gap-large;
background: white;
border: 1px solid $core-grey-light-700;
border-top: 0;
@include breakpoint( '<782px' ) {
margin-left: -16px;
margin-right: -16px;
margin-bottom: $gap-small;
border-left: none;
border-right: none;
width: auto;
}
.woocommerce-chart__header {
min-height: 50px;
border-top: 1px solid $core-grey-light-700;
border-bottom: 1px solid $core-grey-light-700;
display: flex;
flex-wrap: nowrap;
@ -181,13 +188,9 @@
&.is-placeholder {
@include placeholder();
display: inline-block;
height: 200px;
width: 100%;
margin: 0;
padding: 0;
margin-bottom: $gap;
border: 1px solid $core-grey-light-700;
}
}

View File

@ -13,13 +13,30 @@ import {
scaleLinear as d3ScaleLinear,
scaleTime as d3ScaleTime,
} from 'd3-scale';
import { mouse as d3Mouse, select as d3Select } from 'd3-selection';
import { event as d3Event, mouse as d3Mouse, select as d3Select } from 'd3-selection';
import { line as d3Line } from 'd3-shape';
/**
* Internal dependencies
*/
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 );
let prevMonth = i !== 0 ? params.uniqueDates[ i - 1 ] : params.uniqueDates[ i ];
@ -263,7 +350,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( '' )
);
@ -303,9 +390,9 @@ export const drawAxis = ( node, params ) => {
.remove();
};
const showTooltip = ( node, params, d ) => {
const showTooltip = ( node, params, d, position ) => {
const chartCoords = node.node().getBoundingClientRect();
let [ xPosition, yPosition ] = d3Mouse( node.node() );
let [ xPosition, yPosition ] = position ? position : d3Mouse( node.node() );
xPosition = xPosition > chartCoords.width - 340 ? xPosition - 340 : xPosition + 100;
yPosition = yPosition > chartCoords.height - 150 ? yPosition - 200 : yPosition + 20;
const keys = params.orderedKeys.filter( row => row.visible ).map(
@ -333,25 +420,25 @@ const showTooltip = ( node, params, d ) => {
` );
};
const handleMouseOverBarChart = ( d, i, nodes, node, data, params ) => {
const handleMouseOverBarChart = ( d, i, nodes, node, data, params, position ) => {
d3Select( nodes[ i ].parentNode )
.select( '.barfocus' )
.attr( 'opacity', '0.1' );
showTooltip( node, params, d );
showTooltip( node, params, d, position );
};
const handleMouseOutBarChart = ( d, i, nodes, params ) => {
d3Select( nodes[ i ].parentNode )
.select( '.barfocus' )
.attr( 'opacity', '0' );
params.tooltip.style( 'display', 'flex' );
params.tooltip.style( 'display', 'none' );
};
const handleMouseOverLineChart = ( d, i, nodes, node, data, params ) => {
const handleMouseOverLineChart = ( d, i, nodes, node, data, params, position ) => {
d3Select( nodes[ i ].parentNode )
.select( '.focus-grid' )
.attr( 'opacity', '1' );
showTooltip( node, params, data.find( e => e.date === d.date ) );
showTooltip( node, params, data.find( e => e.date === d.date ), position );
};
const handleMouseOutLineChart = ( d, i, nodes, params ) => {
@ -361,6 +448,12 @@ const handleMouseOutLineChart = ( d, i, nodes, params ) => {
params.tooltip.style( 'display', 'none' );
};
const calculatePositionInChart = ( element, chart ) => {
const elementCoords = element.getBoundingClientRect();
const chartCoords = chart.getBoundingClientRect();
return [ elementCoords.x - chartCoords.x, elementCoords.y - chartCoords.y ];
};
export const drawLines = ( node, data, params ) => {
const series = node
.append( 'g' )
@ -384,6 +477,7 @@ export const drawLines = ( node, data, params ) => {
} )
.attr( 'd', d => params.line( d.values ) );
params.uniqueDates.length < 50 &&
series
.selectAll( 'circle' )
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
@ -430,10 +524,15 @@ export const drawLines = ( node, data, params ) => {
.attr( 'width', d => d.width )
.attr( 'height', params.height )
.attr( 'opacity', 0 )
.attr( 'tabindex', '0' )
.on( 'mouseover', ( d, i, nodes ) =>
handleMouseOverLineChart( d, i, nodes, node, data, params )
)
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
.on( 'focus', ( d, i, nodes ) => {
const position = calculatePositionInChart( d3Event.target, node.node() );
handleMouseOverLineChart( d, i, nodes, node, data, params, position );
} )
.on( 'mouseout blur', ( d, i, nodes ) => handleMouseOutLineChart( d, i, nodes, params ) );
};
export const drawBars = ( node, data, params ) => {
@ -491,8 +590,13 @@ export const drawBars = ( node, data, params ) => {
.attr( 'width', params.xGroupScale.range()[ 1 ] )
.attr( 'height', params.height )
.attr( 'opacity', '0' )
.attr( 'tabindex', '0' )
.on( 'mouseover', ( d, i, nodes ) =>
handleMouseOverBarChart( d, i, nodes, node, data, params )
)
.on( 'mouseout', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, params ) );
.on( 'focus', ( d, i, nodes ) => {
const position = calculatePositionInChart( d3Event.target, node.node() );
handleMouseOverBarChart( d, i, nodes, node, data, params, position );
} )
.on( 'mouseout blur', ( d, i, nodes ) => handleMouseOutBarChart( d, i, nodes, params ) );
};

View File

@ -10,6 +10,7 @@ And as such will require data layer logic for products to fully build the table
"items_sold": 100,
"gross_revenue": 999.99,
"orders_count": 54,
"name": 'Product name',
"_links": {
"product": [
{
@ -27,6 +28,7 @@ export default [
items_sold: 1000,
gross_revenue: 999.99,
orders_count: 54,
name: 'awesome shirt',
_links: {
product: [
{
@ -40,6 +42,7 @@ export default [
items_sold: 90,
gross_revenue: 875,
orders_count: 41,
name: 'awesome pants',
_links: {
product: [
{
@ -53,6 +56,7 @@ export default [
items_sold: 55,
gross_revenue: 75.75,
orders_count: 28,
name: 'awesome hat',
_links: {
product: [
{
@ -66,6 +70,7 @@ export default [
items_sold: 10,
gross_revenue: 24.5,
orders_count: 14,
name: 'awesome sticker',
_links: {
product: [
{
@ -79,6 +84,7 @@ export default [
items_sold: 1,
gross_revenue: 0.99,
orders_count: 1,
name: 'awesome button',
_links: {
product: [
{

View File

@ -53,17 +53,15 @@ export class TopSellingProducts extends Component {
getRowsContent( data ) {
return map( data, row => {
const { product_id, items_sold, gross_revenue, orders_count } = row;
const { product_id, items_sold, gross_revenue, orders_count, name } = row;
// @TODO We also will need to have product data to properly display the product name here
const productName = `Product ${ product_id }`;
const productLink = (
<a href={ getAdminLink( `/post.php?post=${ product_id }&action=edit` ) }>{ productName }</a>
<a href={ getAdminLink( `/post.php?post=${ product_id }&action=edit` ) }>{ name }</a>
);
return [
{
display: productLink,
value: productName,
value: name,
},
{
display: numberFormat( items_sold ),
@ -123,7 +121,7 @@ export default compose(
const endpoint = NAMESPACE + 'reports/products';
// @TODO We will need to add the date parameters from the Date Picker
// { after: '2018-04-22', before: '2018-05-06' }
const query = { orderby: 'items_sold', per_page: 5 };
const query = { orderby: 'items_sold', per_page: 5, extended_product_info: 1 };
const stats = getReportStats( endpoint, query );
const isRequesting = isReportStatsRequesting( endpoint, query );

View File

@ -43,7 +43,7 @@ describe( 'TopSellingProducts', () => {
const table = topSellingProducts.find( 'Table' );
const firstRow = table.props().rows[ 0 ];
expect( firstRow[ 0 ].value ).toBe( `Product ${ mockData[ 0 ].product_id }` );
expect( firstRow[ 0 ].value ).toBe( mockData[ 0 ].name );
expect( firstRow[ 1 ].display ).toBe( numberFormat( mockData[ 0 ].items_sold ) );
expect( firstRow[ 1 ].value ).toBe( mockData[ 0 ].items_sold );
expect( firstRow[ 2 ].display ).toBe( numberFormat( mockData[ 0 ].orders_count ) );
@ -73,7 +73,7 @@ describe( 'TopSellingProducts', () => {
const topSellingProducts = topSellingProductsWrapper.root.findByType( TopSellingProducts );
const endpoint = '/wc/v3/reports/products';
const query = { orderby: 'items_sold', per_page: 5 };
const query = { orderby: 'items_sold', per_page: 5, extended_product_info: 1 };
expect( getReportStatsMock.mock.calls[ 0 ][ 1 ] ).toBe( endpoint );
expect( getReportStatsMock.mock.calls[ 0 ][ 2 ] ).toEqual( query );