Update comparison chart design and show it on the Product report (https://github.com/woocommerce/woocommerce-admin/pull/816)

* Display comparison chart in Product detail report

* Make legend to overflow in comparison charts

* Show comparison chart also when comparing products

* Update comparison chart legend design

* Send itemsLabel and comparisonChart as props to the ReportChart component

* Update styles and create a legend.scss file

* Minor cleanup

* Fix legend test

* Sort props alphabetically
This commit is contained in:
Albert Juhé Lluveras 2018-11-12 15:41:33 -06:00 committed by GitHub
parent 3637ae5054
commit 5033a1ba5c
7 changed files with 325 additions and 195 deletions

View File

@ -29,7 +29,15 @@ import ReportError from 'analytics/components/report-error';
class ReportChart extends Component { class ReportChart extends Component {
render() { render() {
const { path, primaryData, secondaryData, selectedChart, query } = this.props; const {
comparisonChart,
query,
itemsLabel,
path,
primaryData,
secondaryData,
selectedChart,
} = this.props;
if ( primaryData.isError || secondaryData.isError ) { if ( primaryData.isError || secondaryData.isError ) {
return <ReportError isError />; return <ReportError isError />;
@ -84,7 +92,9 @@ class ReportChart extends Component {
title={ selectedChart.label } title={ selectedChart.label }
interval={ currentInterval } interval={ currentInterval }
allowedIntervals={ allowedIntervals } allowedIntervals={ allowedIntervals }
mode="time-comparison" itemsLabel={ itemsLabel }
layout={ comparisonChart ? 'comparison' : 'standard' }
mode={ comparisonChart ? 'item-comparison' : 'time-comparison' }
pointLabelFormat={ formats.pointLabelFormat } pointLabelFormat={ formats.pointLabelFormat }
tooltipTitle={ selectedChart.label } tooltipTitle={ selectedChart.label }
xFormat={ formats.xFormat } xFormat={ formats.xFormat }
@ -97,6 +107,8 @@ class ReportChart extends Component {
} }
ReportChart.propTypes = { ReportChart.propTypes = {
comparisonChart: PropTypes.bool,
itemsLabel: PropTypes.string,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
primaryData: PropTypes.object.isRequired, primaryData: PropTypes.object.isRequired,
query: PropTypes.object.isRequired, query: PropTypes.object.isRequired,

View File

@ -2,6 +2,7 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@ -23,6 +24,11 @@ export default class ProductsReport extends Component {
render() { render() {
const { path, query } = this.props; const { path, query } = this.props;
const itemsLabel =
'single_product' === query.filter && !! query.products
? __( '%s variations', 'wc-admin' )
: __( '%s products', 'wc-admin' );
return ( return (
<Fragment> <Fragment>
<ReportFilters query={ query } path={ path } filters={ filters } /> <ReportFilters query={ query } path={ path } filters={ filters } />
@ -33,8 +39,10 @@ export default class ProductsReport extends Component {
selectedChart={ getSelectedChart( query.chart, charts ) } selectedChart={ getSelectedChart( query.chart, charts ) }
/> />
<ReportChart <ReportChart
comparisonChart
charts={ charts } charts={ charts }
endpoint="products" endpoint="products"
itemsLabel={ itemsLabel }
path={ path } path={ path }
query={ query } query={ query }
selectedChart={ getSelectedChart( query.chart, charts ) } selectedChart={ getSelectedChart( query.chart, charts ) }

View File

@ -192,6 +192,7 @@ class Chart extends Component {
const { orderedKeys, type, visibleData, width } = this.state; const { orderedKeys, type, visibleData, width } = this.state;
const { const {
dateParser, dateParser,
itemsLabel,
layout, layout,
mode, mode,
pointLabelFormat, pointLabelFormat,
@ -211,12 +212,12 @@ class Chart extends Component {
chartHeight = width <= 783 ? 180 : chartHeight; chartHeight = width <= 783 ? 180 : chartHeight;
const legend = ( const legend = (
<Legend <Legend
className={ 'woocommerce-chart__legend' }
colorScheme={ d3InterpolateViridis } colorScheme={ d3InterpolateViridis }
data={ orderedKeys } data={ orderedKeys }
handleLegendHover={ this.handleLegendHover } handleLegendHover={ this.handleLegendHover }
handleLegendToggle={ this.handleLegendToggle } handleLegendToggle={ this.handleLegendToggle }
legendDirection={ legendDirection } legendDirection={ legendDirection }
itemsLabel={ itemsLabel }
valueType={ valueType } valueType={ valueType }
/> />
); );

View File

@ -3,8 +3,9 @@
* External dependencies * External dependencies
*/ */
import classNames from 'classnames'; import classNames from 'classnames';
import { Component } from '@wordpress/element'; import { Component, createRef } from '@wordpress/element';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { sprintf } from '@wordpress/i18n';
/** /**
* WooCommerce dependencies * WooCommerce dependencies
@ -14,7 +15,7 @@ import { formatCurrency } from '@woocommerce/currency';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './style.scss'; import './legend.scss';
import { getColor } from './utils'; import { getColor } from './utils';
function getFormatedTotal( total, valueType ) { function getFormatedTotal( total, valueType ) {
@ -36,28 +37,67 @@ function getFormatedTotal( total, valueType ) {
* A legend specifically designed for the WooCommerce admin charts. * A legend specifically designed for the WooCommerce admin charts.
*/ */
class Legend extends Component { class Legend extends Component {
constructor() {
super();
this.listRef = createRef();
this.state = {
isScrollable: false,
};
}
componentDidMount() {
this.updateListScroll();
window.addEventListener( 'resize', this.updateListScroll );
}
componentWillUnmount() {
window.removeEventListener( 'resize', this.updateListScroll );
}
updateListScroll = () => {
const list = this.listRef.current;
const scrolledToEnd = list.scrollHeight - list.scrollTop <= list.offsetHeight;
this.setState( {
isScrollable: ! scrolledToEnd,
} );
};
render() { render() {
const { const {
colorScheme, colorScheme,
data, data,
handleLegendHover, handleLegendHover,
handleLegendToggle, handleLegendToggle,
itemsLabel,
legendDirection, legendDirection,
valueType, valueType,
} = this.props; } = this.props;
const { isScrollable } = this.state;
const colorParams = { const colorParams = {
orderedKeys: data, orderedKeys: data,
colorScheme, colorScheme,
}; };
const numberOfRowsVisible = data.filter( row => row.visible ).length; const numberOfRowsVisible = data.filter( row => row.visible ).length;
const showTotalLabel = legendDirection === 'column' && data.length > 5 && itemsLabel;
return ( return (
<ul <div
className={ classNames( className={ classNames(
'woocommerce-legend', 'woocommerce-legend',
`woocommerce-legend__direction-${ legendDirection }`, `woocommerce-legend__direction-${ legendDirection }`,
{
'has-total': showTotalLabel,
'is-scrollable': isScrollable,
},
this.props.className this.props.className
) } ) }
>
<ul
className="woocommerce-legend__list"
ref={ this.listRef }
onScroll={ showTotalLabel ? this.updateListScroll : null }
> >
{ data.map( row => ( { data.map( row => (
<li <li
@ -95,6 +135,10 @@ class Legend extends Component {
</li> </li>
) ) } ) ) }
</ul> </ul>
{ showTotalLabel && (
<div className="woocommerce-legend__total">{ sprintf( itemsLabel, data.length ) }</div>
) }
</div>
); );
} }
} }
@ -124,6 +168,11 @@ Legend.propTypes = {
* Display legend items as a `row` or `column` inside a flex-box. * Display legend items as a `row` or `column` inside a flex-box.
*/ */
legendDirection: PropTypes.oneOf( [ 'row', 'column' ] ), legendDirection: PropTypes.oneOf( [ 'row', 'column' ] ),
/**
* Label to describe the legend items. It will be displayed in the legend of
* comparison charts when there are many.
*/
itemsLabel: PropTypes.string,
/** /**
* What type of data is to be displayed? Number, Average, String? * What type of data is to be displayed? Number, Average, String?
*/ */

View File

@ -0,0 +1,212 @@
/** @format */
.woocommerce-legend {
&.has-total {
padding-bottom: 50px;
position: relative;
}
&.woocommerce-legend__direction-column {
border-right: 1px solid $core-grey-light-700;
.woocommerce-chart__footer & {
border-right: none;
}
}
}
.woocommerce-legend__list {
color: $black;
display: flex;
height: 100%;
margin: 0;
.woocommerce-legend__direction-column & {
flex-direction: column;
height: 300px;
min-width: 320px;
overflow: auto;
.woocommerce-chart__footer & {
border-top: 1px solid $core-grey-light-700;
height: 100%;
max-height: none;
min-height: none;
}
}
.has-total.woocommerce-legend__direction-column & {
.woocommerce-chart__footer & {
height: auto;
max-height: 220px;
min-height: none;
}
}
.woocommerce-legend__direction-row & {
flex-direction: row;
}
}
.woocommerce-legend__item {
& > button {
display: flex;
justify-content: center;
align-items: center;
background-color: $white;
color: $core-grey-dark-500;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
border: none;
padding: 0;
.woocommerce-legend__item-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
position: relative;
padding: 3px 0 3px 24px;
cursor: pointer;
font-size: 13px;
user-select: none;
width: 100%;
&:hover {
input {
~ .woocommerce-legend__item-checkmark {
background-color: $core-grey-light-200;
}
}
}
.woocommerce-legend__item-checkmark {
border: 1px solid $core-grey-light-900;
position: absolute;
top: 4px;
left: 0;
height: 16px;
width: 16px;
background-color: $white;
&::after {
content: '';
position: absolute;
display: none;
}
&.woocommerce-legend__item-checkmark-checked {
background-color: currentColor;
border-color: currentColor;
&::after {
display: block;
left: 5px;
top: 2px;
width: 3px;
height: 6px;
border: solid $white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
}
.woocommerce-legend__item-total {
font-weight: bold;
}
}
&:focus {
outline: none;
.woocommerce-legend__item-container {
.woocommerce-legend__item-checkmark {
outline: 2px solid $core-grey-light-900;
}
}
}
&:hover {
background-color: $core-grey-light-100;
}
}
.woocommerce-legend__direction-column & {
margin: 2px 0;
padding: 0;
& > button {
height: 32px;
padding: 0 17px;
}
&:first-child {
margin-top: $gap-small;
}
&:last-child::after {
content: '';
display: block;
height: $gap-small;
width: 100%;
}
}
.woocommerce-legend__direction-row & {
padding: 0;
margin: 0;
& > button {
padding: 0 17px;
.woocommerce-legend__item-container {
height: 50px;
align-items: center;
.woocommerce-legend__item-checkmark {
top: 17px;
}
.woocommerce-legend__item-title {
margin-right: 17px;
}
}
}
}
}
.woocommerce-legend__total {
align-items: center;
background: $white;
border-top: 1px solid $core-grey-light-700;
bottom: 0;
color: $core-grey-dark-500;
display: flex;
height: 50px;
justify-content: center;
left: 0;
position: absolute;
right: 0;
text-transform: uppercase;
&::before {
background: linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2));
bottom: 100%;
content: '';
height: 20px;
left: 0;
opacity: 0;
pointer-events: none;
position: absolute;
right: 0;
transition: opacity 0.3s;
}
.is-scrollable &::before {
opacity: 1;
}
}

View File

@ -61,15 +61,6 @@
.woocommerce-chart__footer { .woocommerce-chart__footer {
width: 100%; width: 100%;
.woocommerce-legend {
&.woocommerce-legend__direction-column {
height: 100%;
min-height: none;
border-right: none;
margin-bottom: $gap;
}
}
} }
svg { svg {
@ -252,146 +243,3 @@
position: absolute; position: absolute;
} }
} }
.woocommerce-legend {
color: $black;
display: flex;
height: 100%;
margin: 0;
&.woocommerce-legend__direction-column {
flex-direction: column;
border-right: 1px solid $core-grey-light-700;
height: 300px;
min-width: 320px;
li {
margin: 0;
padding: 0;
button {
height: 32px;
padding: 0 17px;
}
&:first-child {
margin-top: 17px;
}
}
}
&.woocommerce-legend__direction-row {
flex-direction: row;
li {
padding: 0;
margin: 0;
button {
padding: 0 17px;
.woocommerce-legend__item-container {
height: 50px;
align-items: center;
.woocommerce-legend__item-checkmark {
top: 17px;
}
.woocommerce-legend__item-title {
margin-right: 17px;
}
}
}
}
}
li {
&.woocommerce-legend__item {
button {
&:hover {
background-color: $core-grey-light-100;
}
}
}
button {
background-color: $white;
color: $core-grey-dark-500;
display: inline-flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
width: 100%;
border: none;
padding: 0;
.woocommerce-legend__item-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
position: relative;
padding: 3px 0 3px 24px;
cursor: pointer;
font-size: 13px;
user-select: none;
width: 100%;
&:hover {
input {
~ .woocommerce-legend__item-checkmark {
background-color: $core-grey-light-200;
}
}
}
.woocommerce-legend__item-checkmark {
border: 1px solid $core-grey-light-900;
position: absolute;
top: 2px;
left: 0;
height: 16px;
width: 16px;
background-color: $white;
&::after {
content: '';
position: absolute;
display: none;
}
&.woocommerce-legend__item-checkmark-checked {
background-color: currentColor;
border-color: currentColor;
&::after {
display: block;
left: 5px;
top: 2px;
width: 3px;
height: 6px;
border: solid $white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
}
}
.woocommerce-legend__item-total {
font-weight: bold;
}
}
&:focus {
outline: none;
.woocommerce-legend__item-container {
.woocommerce-legend__item-checkmark {
outline: 2px solid $core-grey-light-900;
}
}
}
}
}
}

View File

@ -3,7 +3,7 @@
* *
* @format * @format
*/ */
import { shallow } from 'enzyme'; import { mount } from 'enzyme';
/** /**
* Internal dependencies * Internal dependencies
@ -26,7 +26,7 @@ const data = [
describe( 'Legend', () => { describe( 'Legend', () => {
test( 'should not disable any button if more than one is active', () => { test( 'should not disable any button if more than one is active', () => {
const topSellingProducts = shallow( <Legend colorScheme={ colorScheme } data={ data } /> ); const topSellingProducts = mount( <Legend colorScheme={ colorScheme } data={ data } /> );
expect( topSellingProducts.find( 'button' ).get( 0 ).props.disabled ).toBeFalsy(); expect( topSellingProducts.find( 'button' ).get( 0 ).props.disabled ).toBeFalsy();
expect( topSellingProducts.find( 'button' ).get( 1 ).props.disabled ).toBeFalsy(); expect( topSellingProducts.find( 'button' ).get( 1 ).props.disabled ).toBeFalsy();
@ -35,7 +35,7 @@ describe( 'Legend', () => {
test( 'should disable the last active button', () => { test( 'should disable the last active button', () => {
data[ 1 ].visible = false; data[ 1 ].visible = false;
const topSellingProducts = shallow( <Legend colorScheme={ colorScheme } data={ data } /> ); const topSellingProducts = mount( <Legend colorScheme={ colorScheme } data={ data } /> );
expect( topSellingProducts.find( 'button' ).get( 0 ).props.disabled ).toBeTruthy(); expect( topSellingProducts.find( 'button' ).get( 0 ).props.disabled ).toBeTruthy();
expect( topSellingProducts.find( 'button' ).get( 1 ).props.disabled ).toBeFalsy(); expect( topSellingProducts.find( 'button' ).get( 1 ).props.disabled ).toBeFalsy();