diff --git a/src/components/chartArea/__tests__/__snapshots__/chartArea.test.js.snap b/src/components/chartArea/__tests__/__snapshots__/chartArea.test.js.snap index affad1166..95f0d582c 100644 --- a/src/components/chartArea/__tests__/__snapshots__/chartArea.test.js.snap +++ b/src/components/chartArea/__tests__/__snapshots__/chartArea.test.js.snap @@ -704,33 +704,64 @@ exports[`ChartArea Component should allow tick formatting: y tick format 1`] = ` `; +exports[`ChartArea Component should handle custom chart legends: hide state 1`] = ` +Object { + "loremGraph": true, +} +`; + +exports[`ChartArea Component should handle custom chart legends: isToggled state 1`] = ` +Object { + "returnedToggleValue": true, + "state": Object { + "loremGraph": true, + }, +} +`; + exports[`ChartArea Component should handle custom chart legends: renderLegend: should return a custom legend 1`] = ` Object { - "dataSets": Array [ - Object { - "data": Array [ - Object { - "x": 1, - "xAxisLabel": "1 x axis label", - "y": 0, - }, - ], - "id": "loremGraph", - "interpolation": "natural", - "isStacked": true, - }, - Object { - "data": Array [ - Object { - "x": 1, - "xAxisLabel": "1 x axis label", - "y": 10, - }, - ], - "id": "ipsumGraph", - "isThreshold": true, - }, - ], + "chart": Object { + "hide": [Function], + "isToggled": [Function], + "revert": [Function], + "toggle": [Function], + }, + "datum": Object { + "dataSets": Array [ + Object { + "data": Array [ + Object { + "x": 1, + "xAxisLabel": "1 x axis label", + "y": 0, + }, + ], + "id": "loremGraph", + "interpolation": "natural", + "isStacked": true, + }, + Object { + "data": Array [ + Object { + "x": 1, + "xAxisLabel": "1 x axis label", + "y": 10, + }, + ], + "id": "ipsumGraph", + "isThreshold": true, + }, + ], + }, +} +`; + +exports[`ChartArea Component should handle custom chart legends: revert state 1`] = `Object {}`; + +exports[`ChartArea Component should handle custom chart legends: toggle state 1`] = ` +Object { + "loremGraph": false, } `; @@ -867,21 +898,13 @@ exports[`ChartArea Component should handle custom chart tooltips: renderTooltip: "y": -10, } } - labelComponent={ - } - > - } - /> - - } + labelComponent={} labels={[Function]} portalComponent={} portalZIndex={99} responsive={true} voronoiDimension="x" - voronoiPadding={50} + voronoiPadding={60} /> `; diff --git a/src/components/chartArea/__tests__/chartArea.test.js b/src/components/chartArea/__tests__/chartArea.test.js index 8ffb720eb..657647285 100644 --- a/src/components/chartArea/__tests__/chartArea.test.js +++ b/src/components/chartArea/__tests__/chartArea.test.js @@ -308,6 +308,7 @@ describe('ChartArea Component', () => { }); it('should handle custom chart legends', () => { + let chartMethods = {}; const props = { dataSets: [ { @@ -334,11 +335,26 @@ describe('ChartArea Component', () => { isThreshold: true } ], - chartLegend: propsObj => propsObj.datum + chartLegend: propsObj => { + chartMethods = propsObj.chart; + return propsObj; + } }; const component = shallow(); expect(component.instance().renderLegend()).toMatchSnapshot('renderLegend: should return a custom legend'); + + chartMethods.hide('loremGraph'); + expect(component.instance().dataSetsToggle).toMatchSnapshot('hide state'); + + const returnedToggleValue = chartMethods.isToggled('loremGraph'); + expect({ state: component.instance().dataSetsToggle, returnedToggleValue }).toMatchSnapshot('isToggled state'); + + chartMethods.toggle('loremGraph'); + expect(component.instance().dataSetsToggle).toMatchSnapshot('toggle state'); + + chartMethods.revert(); + expect(component.instance().dataSetsToggle).toMatchSnapshot('revert state'); }); it('should set initial width to zero and then resize', () => { diff --git a/src/components/chartArea/chartArea.js b/src/components/chartArea/chartArea.js index a445b66fc..20a8d46b4 100644 --- a/src/components/chartArea/chartArea.js +++ b/src/components/chartArea/chartArea.js @@ -1,12 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { createContainer, VictoryPortal } from 'victory'; +import { createContainer } from 'victory'; import { Chart, ChartAxis, ChartStack, ChartThreshold, - ChartTooltip, ChartThemeColor, ChartArea as PfChartArea } from '@patternfly/react-charts'; @@ -18,10 +17,16 @@ import { helpers } from '../../common'; * * @augments React.Component * @fires onResizeContainer + * @fires onHide + * @fires onRevert + * @fires onToggle + * @param id */ class ChartArea extends React.Component { state = { chartWidth: 0 }; + dataSetsToggle = {}; + containerRef = React.createRef(); tooltipRef = React.createRef(); @@ -48,6 +53,52 @@ class ChartArea extends React.Component { } }; + /** + * Consumer exposed, hides chart layer. + * + * @event onHide + * @param {string} id + */ + onHide = id => { + this.dataSetsToggle = { ...this.dataSetsToggle, [id]: true }; + this.forceUpdate(); + }; + + /** + * Consumer exposed, turns all chart layers back on. + * + * @event onRevert + */ + onRevert = () => { + this.dataSetsToggle = {}; + this.forceUpdate(); + }; + + /** + * Consumer exposed, turns chart layer on/off. + * + * @event onToggle + * @param {string} id + * @returns {boolean} + */ + onToggle = id => { + const updatedToggle = !this.dataSetsToggle[id]; + this.dataSetsToggle = { ...this.dataSetsToggle, [id]: updatedToggle }; + this.forceUpdate(); + + return updatedToggle; + }; + + /** + * Consumer exposed, determine if chart layer on/off. + * Note: Using "setState" as related to this exposed check gives the appearance of a race condition. + * Using a class property with forceUpdate to bypass. + * + * @param {string} id + * @returns {boolean} + */ + getIsToggled = id => this.dataSetsToggle[id] || false; + /** * Apply props, set x and y axis chart increments/ticks formatting. * @@ -131,7 +182,6 @@ class ChartArea extends React.Component { }; } - // ToDo: the domain range needs to be updated when additional datasets are added /** * Calculate and return the x and y domain range. * @@ -139,6 +189,7 @@ class ChartArea extends React.Component { * @returns {object} */ getChartDomain({ isXAxisTicks }) { + const { dataSetsToggle } = this; const { domain, dataSets } = this.props; if (Object.keys(domain).length) { @@ -153,7 +204,7 @@ class ChartArea extends React.Component { const stackedSets = dataSets.filter(set => set.isStacked === true); stackedSets.forEach(dataSet => { - if (dataSet.data) { + if (!dataSetsToggle[dataSet.id] && dataSet.data) { let dataSetMaxYStacked = 0; dataSet.data.forEach((value, index) => { @@ -167,7 +218,7 @@ class ChartArea extends React.Component { }); dataSets.forEach(dataSet => { - if (dataSet.data) { + if (!dataSetsToggle[dataSet.id] && dataSet.data) { dataSetMaxX = dataSet.data.length > dataSetMaxX ? dataSet.data.length : dataSetMaxX; dataSet.data.forEach(value => { @@ -199,6 +250,7 @@ class ChartArea extends React.Component { * @returns {Array} */ getTooltipData() { + const { dataSetsToggle } = this; const { dataSets, chartTooltip } = this.props; let tooltipDataSet = []; @@ -207,7 +259,7 @@ class ChartArea extends React.Component { const itemsByKey = {}; dataSets.forEach(data => { - if (data.data && data.data[index]) { + if (!dataSetsToggle[data.id] && data.data && data.data[index]) { itemsByKey[data.id] = { color: data.stroke || data.fill || data.color || '', data: _cloneDeep(data.data[index]) @@ -239,9 +291,10 @@ class ChartArea extends React.Component { * @returns {Node} */ renderTooltip() { - const { chartTooltip } = this.props; + const { dataSetsToggle } = this; + const { chartTooltip, dataSets } = this.props; - if (!chartTooltip) { + if (!chartTooltip || Object.values(dataSetsToggle).filter(v => v === true).length === dataSets.length) { return null; } @@ -273,7 +326,7 @@ class ChartArea extends React.Component { if ( globalContainerBounds.right < globalTooltipBounds.right || - obj.x + globalTooltipBounds.width * 3 > globalContainerBounds.right + obj.x + globalTooltipBounds.width > globalContainerBounds.right / 2 ) { xCoordinate = obj.x - 10 - globalTooltipBounds.width; } @@ -283,7 +336,7 @@ class ChartArea extends React.Component { if (htmlContent) { return ( - +
{htmlContent}
@@ -295,20 +348,13 @@ class ChartArea extends React.Component { return ; }; - // FixMe: "flyoutComponent" on ChartTooltip attribute requires very specific format. - const labelComponent = ( - - } /> - - ); - return ( ''} - labelComponent={labelComponent} + labelComponent={} voronoiDimension="x" - voronoiPadding={50} + voronoiPadding={60} /> ); } @@ -325,13 +371,19 @@ class ChartArea extends React.Component { return null; } - const mockDatum = { - datum: { dataSets: _cloneDeep(dataSets) } + const legendProps = { + datum: { dataSets: _cloneDeep(dataSets) }, + chart: { + hide: this.onHide, + revert: this.onRevert, + toggle: this.onToggle, + isToggled: this.getIsToggled + } }; return ( - (React.isValidElement(chartLegend) && React.cloneElement(chartLegend, { ...mockDatum })) || - chartLegend({ ...mockDatum }) + (React.isValidElement(chartLegend) && React.cloneElement(chartLegend, { ...legendProps })) || + chartLegend({ ...legendProps }) ); } @@ -342,6 +394,7 @@ class ChartArea extends React.Component { * @returns {Array} */ renderChart({ stacked = false }) { + const { dataSetsToggle } = this; const { dataSets } = this.props; const charts = []; const chartsStacked = []; @@ -407,7 +460,7 @@ class ChartArea extends React.Component { }; dataSets.forEach((dataSet, index) => { - if (dataSet.data && dataSet.data.length) { + if (!dataSetsToggle[dataSet.id] && dataSet.data && dataSet.data.length) { const updatedDataSet = (dataSet.isThreshold && thresholdChart(dataSet, index)) || areaChart(dataSet, index); if (dataSet.isStacked) { diff --git a/src/components/graphCard/__tests__/__snapshots__/graphCardChartLegend.test.js.snap b/src/components/graphCard/__tests__/__snapshots__/graphCardChartLegend.test.js.snap index a7b8dc3c0..8510fa639 100644 --- a/src/components/graphCard/__tests__/__snapshots__/graphCardChartLegend.test.js.snap +++ b/src/components/graphCard/__tests__/__snapshots__/graphCardChartLegend.test.js.snap @@ -1,5 +1,82 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`GraphCardChartLegend Component should handle a click event: click event post 1`] = ` + + } + isDisabled={false} + key="curiosity-button-loremIpsum" + onClick={[Function]} + onKeyPress={[Function]} + tabIndex={0} + variant="link" +> + t(curiosity-graph.loremIpsumLabel, [object Object]) + +`; + +exports[`GraphCardChartLegend Component should handle a click event: click event pre 1`] = ` + + } + isDisabled={false} + key="curiosity-button-loremIpsum" + onClick={[Function]} + onKeyPress={[Function]} + tabIndex={0} + variant="link" +> + t(curiosity-graph.loremIpsumLabel, [object Object]) + +`; + +exports[`GraphCardChartLegend Component should handle a click event: click event update 1`] = ` + + } + isDisabled={false} + key="curiosity-button-loremIpsum" + onClick={[Function]} + onKeyPress={[Function]} + tabIndex={0} + variant="link" +> + t(curiosity-graph.loremIpsumLabel, [object Object]) + +`; + exports[`GraphCardChartLegend Component should handle variations in data when returning legend items: legend item, MISSING tooltip content 1`] = ` } isDisabled={false} + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -69,13 +148,15 @@ exports[`GraphCardChartLegend Component should handle variations in data when re className="legend-icon" style={ Object { - "backgroundColor": "#ipsum", + "backgroundColor": "#000000", "visibility": "visible", } } /> } isDisabled={false} + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -97,6 +178,8 @@ exports[`GraphCardChartLegend Component should handle variations in data when re /> } isDisabled={true} + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -159,6 +242,8 @@ exports[`GraphCardChartLegend Component should render a basic component: basic 1 } isDisabled={false} key="curiosity-button-loremIpsum" + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -223,6 +308,8 @@ exports[`GraphCardChartLegend Component should render basic data: data 1`] = ` } isDisabled={false} key="curiosity-button-loremIpsum" + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -278,6 +365,8 @@ exports[`GraphCardChartLegend Component should render basic data: data 1`] = ` } isDisabled={true} key="curiosity-button-ametConsectetur" + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -337,6 +426,8 @@ exports[`GraphCardChartLegend Component should render basic data: data 1`] = ` } isDisabled={false} key="curiosity-button-dolorSit" + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > @@ -396,6 +487,8 @@ exports[`GraphCardChartLegend Component should render basic data: data 1`] = ` } isDisabled={false} key="curiosity-button-nonCursus" + onClick={[Function]} + onKeyPress={[Function]} tabIndex={0} variant="link" > diff --git a/src/components/graphCard/__tests__/graphCardChartLegend.test.js b/src/components/graphCard/__tests__/graphCardChartLegend.test.js index 0d55c1fcf..fca15cd1f 100644 --- a/src/components/graphCard/__tests__/graphCardChartLegend.test.js +++ b/src/components/graphCard/__tests__/graphCardChartLegend.test.js @@ -1,5 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { Button } from '@patternfly/react-core'; import { GraphCardChartLegend } from '../graphCardChartLegend'; describe('GraphCardChartLegend Component', () => { @@ -57,11 +58,76 @@ describe('GraphCardChartLegend Component', () => { expect(component).toMatchSnapshot('data'); }); + it('should handle a click event', () => { + const props = { + datum: { + dataSets: [ + { + stroke: '#000000', + id: 'loremIpsum', + isThreshold: false, + data: [{ y: 0, hasData: true }] + }, + { + stroke: '#ff0000', + id: 'dolorSit', + isThreshold: true, + data: [{ y: 0, isInfinite: false }] + } + ] + }, + legend: { + 'test-dolorSit': true + }, + product: 'test', + viewId: 'test' + }; + + const component = shallow(); + expect(component.find(Button).first()).toMatchSnapshot('click event pre'); + + component.find(Button).first().simulate('click'); + // emulate a Redux state update. + component.setProps({ + legend: { ...props.legend, ...{ 'test-loremIpsum': true } } + }); + expect(component.find(Button).first()).toMatchSnapshot('click event update'); + + component.find(Button).first().simulate('keyPress'); + // emulate a Redux state update. + component.setProps({ + legend: { ...props.legend, ...{ 'test-loremIpsum': false } } + }); + expect(component.find(Button).first()).toMatchSnapshot('click event post'); + }); + it('should handle variations in data when returning legend items', () => { + const props = { + datum: { + dataSets: [ + { + stroke: '#000000', + id: 'loremIpsum', + isThreshold: false, + data: [{ y: 0, hasData: true }] + }, + { + stroke: '#ff0000', + id: 'dolorSit', + isThreshold: true, + data: [{ y: 0, isInfinite: false }] + } + ] + }, + product: 'test' + }; + + const component = shallow(); + expect( - GraphCardChartLegend.renderLegendItem({ - chartId: 'lorem', - color: '#ipsum', + component.instance().renderLegendItem({ + chartId: 'loremIpsum', + color: '#000000', isDisabled: false, isThreshold: false, labelContent: 'lorem ispum', @@ -70,9 +136,9 @@ describe('GraphCardChartLegend Component', () => { ).toMatchSnapshot('legend item, WITH tooltip content'); expect( - GraphCardChartLegend.renderLegendItem({ - chartId: 'lorem', - color: '#ipsum', + component.instance().renderLegendItem({ + chartId: 'loremIpsum', + color: '#000000', isDisabled: false, isThreshold: false, labelContent: 'lorem ispum', @@ -81,9 +147,9 @@ describe('GraphCardChartLegend Component', () => { ).toMatchSnapshot('legend item, MISSING tooltip content'); expect( - GraphCardChartLegend.renderLegendItem({ - chartId: 'lorem', - color: '#ipsum', + component.instance().renderLegendItem({ + chartId: 'loremIpsum', + color: '#000000', isDisabled: true, isThreshold: false, labelContent: 'lorem ispum', diff --git a/src/components/graphCard/graphCard.js b/src/components/graphCard/graphCard.js index 9f25bba93..b0dd7c97c 100644 --- a/src/components/graphCard/graphCard.js +++ b/src/components/graphCard/graphCard.js @@ -83,7 +83,7 @@ class GraphCard extends React.Component { * @returns {Node} */ renderChart() { - const { filterGraphData, graphData, graphQuery, selectOptionsType, productShortLabel } = this.props; + const { filterGraphData, graphData, graphQuery, selectOptionsType, productShortLabel, viewId } = this.props; const graphGranularity = graphQuery && graphQuery[rhsmApiTypes.RHSM_API_QUERY_GRANULARITY]; const { selected } = graphCardTypes.getGranularityOptions(selectOptionsType); const updatedGranularity = graphGranularity || selected; @@ -141,7 +141,9 @@ class GraphCard extends React.Component { key={helpers.generateId()} {...chartAreaProps} dataSets={filteredGraphData(graphData)} - chartLegend={({ datum }) => } + chartLegend={({ chart, datum }) => ( + + )} chartTooltip={({ datum }) => ( )} diff --git a/src/components/graphCard/graphCardChartLegend.js b/src/components/graphCard/graphCardChartLegend.js index 9a1ab89da..1d8fbb5ba 100644 --- a/src/components/graphCard/graphCardChartLegend.js +++ b/src/components/graphCard/graphCardChartLegend.js @@ -1,19 +1,66 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { withTranslation } from 'react-i18next'; import { Button, Tooltip, TooltipPosition } from '@patternfly/react-core'; import { EyeSlashIcon } from '@patternfly/react-icons'; +import { connectTranslate, store, reduxTypes } from '../../redux'; import { helpers } from '../../common'; /** * A custom chart legend. * * @augments React.Component + * @fires onClick */ class GraphCardChartLegend extends React.Component { - static renderLegendItem({ chartId, color, isDisabled, isThreshold, labelContent, tooltipContent }) { + componentDidMount() { + const { chart, datum, legend, viewId } = this.props; + datum.dataSets.forEach(({ id }) => { + const checkIsToggled = legend[`${viewId}-${id}`] || chart.isToggled(id); + + if (checkIsToggled) { + chart.hide(id); + } + }); + } + + /** + * Toggle legend item and chart. + * + * @event onClick + * @param {string} id + */ + onClick = id => { + const { chart, viewId } = this.props; + const updatedToggle = chart.toggle(id); + + store.dispatch({ + type: reduxTypes.graph.SET_GRAPH_LEGEND, + legend: { + [`${viewId}-${id}`]: updatedToggle + } + }); + }; + + /** + * Return a legend item. + * + * @param {object} options + * @property {string} chartId + * @property {string} color + * @property {boolean} isDisabled + * @property {boolean} isThreshold + * @property {string} labelContent + * @property {string} tooltipContent + * @returns {Node} + */ + renderLegendItem({ chartId, color, isDisabled, isThreshold, labelContent, tooltipContent }) { + const { chart, legend, viewId } = this.props; + const checkIsToggled = legend[`${viewId}-${chartId}`] || chart.isToggled(chartId); + const button = (