diff --git a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js
index 96b918442af..aadf29ef6ce 100644
--- a/plugins/woocommerce-admin/client/analytics/report/revenue/index.js
+++ b/plugins/woocommerce-admin/client/analytics/report/revenue/index.js
@@ -338,18 +338,16 @@ export class RevenueReport extends Component {
} );
return (
-
-
-
+
);
}
diff --git a/plugins/woocommerce-admin/client/components/calendar/style.scss b/plugins/woocommerce-admin/client/components/calendar/style.scss
index 7ebf90a3cef..08f746a81fa 100644
--- a/plugins/woocommerce-admin/client/components/calendar/style.scss
+++ b/plugins/woocommerce-admin/client/components/calendar/style.scss
@@ -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;
diff --git a/plugins/woocommerce-admin/client/components/chart/charts.js b/plugins/woocommerce-admin/client/components/chart/charts.js
index 92222f4653c..67d40f643a4 100644
--- a/plugins/woocommerce-admin/client/components/chart/charts.js
+++ b/plugins/woocommerce-admin/client/components/chart/charts.js
@@ -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,
diff --git a/plugins/woocommerce-admin/client/components/chart/index.js b/plugins/woocommerce-admin/client/components/chart/index.js
index 9bf0a0be2f9..df6fc9bbe57 100644
--- a/plugins/woocommerce-admin/client/components/chart/index.js
+++ b/plugins/woocommerce-admin/client/components/chart/index.js
@@ -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.
*/
diff --git a/plugins/woocommerce-admin/client/components/chart/style.scss b/plugins/woocommerce-admin/client/components/chart/style.scss
index 5db8d7e2723..604765c2731 100644
--- a/plugins/woocommerce-admin/client/components/chart/style.scss
+++ b/plugins/woocommerce-admin/client/components/chart/style.scss
@@ -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;
}
}
diff --git a/plugins/woocommerce-admin/client/components/chart/utils.js b/plugins/woocommerce-admin/client/components/chart/utils.js
index bde074cca9c..bce3971f0e5 100644
--- a/plugins/woocommerce-admin/client/components/chart/utils.js
+++ b/plugins/woocommerce-admin/client/components/chart/utils.js
@@ -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,25 +477,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' )
@@ -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 ) );
};
diff --git a/plugins/woocommerce-admin/client/dashboard/top-selling-products/__mocks__/mock-data.js b/plugins/woocommerce-admin/client/dashboard/top-selling-products/__mocks__/mock-data.js
index 98948fd3123..9f12de891dc 100644
--- a/plugins/woocommerce-admin/client/dashboard/top-selling-products/__mocks__/mock-data.js
+++ b/plugins/woocommerce-admin/client/dashboard/top-selling-products/__mocks__/mock-data.js
@@ -9,7 +9,8 @@ And as such will require data layer logic for products to fully build the table
"product_id": 20,
"items_sold": 100,
"gross_revenue": 999.99,
- "orders_count": 54,
+ "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: [
{
diff --git a/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js b/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js
index fe94943bae6..4232abe6719 100644
--- a/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js
+++ b/plugins/woocommerce-admin/client/dashboard/top-selling-products/index.js
@@ -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 = (
- { productName }
+ { name }
);
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 );
diff --git a/plugins/woocommerce-admin/client/dashboard/top-selling-products/test/index.js b/plugins/woocommerce-admin/client/dashboard/top-selling-products/test/index.js
index 7d5dcb62ba2..7d3e8261ff3 100644
--- a/plugins/woocommerce-admin/client/dashboard/top-selling-products/test/index.js
+++ b/plugins/woocommerce-admin/client/dashboard/top-selling-products/test/index.js
@@ -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 );