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

View File

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

View File

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

View File

@ -3,8 +3,9 @@
* External dependencies
*/
import classNames from 'classnames';
import { Component } from '@wordpress/element';
import { Component, createRef } from '@wordpress/element';
import PropTypes from 'prop-types';
import { sprintf } from '@wordpress/i18n';
/**
* WooCommerce dependencies
@ -14,7 +15,7 @@ import { formatCurrency } from '@woocommerce/currency';
/**
* Internal dependencies
*/
import './style.scss';
import './legend.scss';
import { getColor } from './utils';
function getFormatedTotal( total, valueType ) {
@ -36,65 +37,108 @@ function getFormatedTotal( total, valueType ) {
* A legend specifically designed for the WooCommerce admin charts.
*/
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() {
const {
colorScheme,
data,
handleLegendHover,
handleLegendToggle,
itemsLabel,
legendDirection,
valueType,
} = this.props;
const { isScrollable } = this.state;
const colorParams = {
orderedKeys: data,
colorScheme,
};
const numberOfRowsVisible = data.filter( row => row.visible ).length;
const showTotalLabel = legendDirection === 'column' && data.length > 5 && itemsLabel;
return (
<ul
<div
className={ classNames(
'woocommerce-legend',
`woocommerce-legend__direction-${ legendDirection }`,
{
'has-total': showTotalLabel,
'is-scrollable': isScrollable,
},
this.props.className
) }
>
{ data.map( row => (
<li
className={ classNames( 'woocommerce-legend__item', {
'woocommerce-legend__item-checked': row.visible,
} ) }
key={ row.key }
id={ row.key }
onMouseEnter={ handleLegendHover }
onMouseLeave={ handleLegendHover }
onBlur={ handleLegendHover }
onFocus={ handleLegendHover }
>
<button
onClick={ handleLegendToggle }
<ul
className="woocommerce-legend__list"
ref={ this.listRef }
onScroll={ showTotalLabel ? this.updateListScroll : null }
>
{ data.map( row => (
<li
className={ classNames( 'woocommerce-legend__item', {
'woocommerce-legend__item-checked': row.visible,
} ) }
key={ row.key }
id={ row.key }
disabled={ row.visible && numberOfRowsVisible <= 1 }
onMouseEnter={ handleLegendHover }
onMouseLeave={ handleLegendHover }
onBlur={ handleLegendHover }
onFocus={ handleLegendHover }
>
<div className="woocommerce-legend__item-container" id={ row.key }>
<span
className={ classNames( 'woocommerce-legend__item-checkmark', {
'woocommerce-legend__item-checkmark-checked': row.visible,
} ) }
id={ row.key }
style={ { color: getColor( row.key, colorParams ) } }
/>
<span className="woocommerce-legend__item-title" id={ row.key }>
{ row.key }
</span>
<span className="woocommerce-legend__item-total" id={ row.key }>
{ getFormatedTotal( row.total, valueType ) }
</span>
</div>
</button>
</li>
) ) }
</ul>
<button
onClick={ handleLegendToggle }
id={ row.key }
disabled={ row.visible && numberOfRowsVisible <= 1 }
>
<div className="woocommerce-legend__item-container" id={ row.key }>
<span
className={ classNames( 'woocommerce-legend__item-checkmark', {
'woocommerce-legend__item-checkmark-checked': row.visible,
} ) }
id={ row.key }
style={ { color: getColor( row.key, colorParams ) } }
/>
<span className="woocommerce-legend__item-title" id={ row.key }>
{ row.key }
</span>
<span className="woocommerce-legend__item-total" id={ row.key }>
{ getFormatedTotal( row.total, valueType ) }
</span>
</div>
</button>
</li>
) ) }
</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.
*/
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?
*/

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 {
width: 100%;
.woocommerce-legend {
&.woocommerce-legend__direction-column {
height: 100%;
min-height: none;
border-right: none;
margin-bottom: $gap;
}
}
}
svg {
@ -252,146 +243,3 @@
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
*/
import { shallow } from 'enzyme';
import { mount } from 'enzyme';
/**
* Internal dependencies
@ -26,7 +26,7 @@ const data = [
describe( 'Legend', () => {
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( 1 ).props.disabled ).toBeFalsy();
@ -35,7 +35,7 @@ describe( 'Legend', () => {
test( 'should disable the last active button', () => {
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( 1 ).props.disabled ).toBeFalsy();