Merge branch 'add/chart-second-x' of github.com:woocommerce/wc-admin into add/chart-second-x
This commit is contained in:
commit
dbca60a660
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ) );
|
||||
};
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 );
|
||||
|
|
Loading…
Reference in New Issue