From 455a0339095e72daadbe135cb20a3ccbbe07f935 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Sun, 3 Feb 2019 21:20:41 -0800 Subject: [PATCH 01/46] feat(legend/series): add hover interaction on legend items addresses #24 --- src/components/legend.tsx | 12 ++++- .../react_canvas/bar_geometries.tsx | 33 +++++++++++-- .../react_canvas/reactive_chart.tsx | 5 ++ src/lib/series/series_utils.test.ts | 47 +++++++++++++++++++ src/lib/series/series_utils.ts | 31 ++++++++++++ src/state/chart_state.ts | 8 ++++ 6 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 src/lib/series/series_utils.test.ts create mode 100644 src/lib/series/series_utils.ts diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 02c33f7ff7..c5b7526c0b 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -86,8 +86,14 @@ class LegendComponent extends React.Component { responsive={false} > {legendItems.map((item, index) => { + const legendItemProps = { + key: index, + className: 'euiChartLegendList__item', + onMouseOver: this.onLegendItemMouseover(index), + }; + return ( - + ); @@ -97,6 +103,10 @@ class LegendComponent extends React.Component { ); } + + private onLegendItemMouseover = (legendItemIndex: number) => () => { + this.props.chartStore!.updateHighlightedLegendItem(legendItemIndex); + } } function LegendElement({ color, label }: Partial) { return ( diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index cdfb5d7cb5..65baf07164 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -3,7 +3,9 @@ import { IAction } from 'mobx'; import React from 'react'; import { Group, Rect } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; +import { LegendItem } from '../../lib/series/legend'; import { BarGeometry, GeometryValue } from '../../lib/series/rendering'; +import { belongsToDataSeries } from '../../lib/series/series_utils'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; interface BarGeometriesDataProps { @@ -12,6 +14,7 @@ interface BarGeometriesDataProps { onElementClick?: ElementClickListener; onElementOver: ((tooltip: TooltipData) => void) & IAction; onElementOut: (() => void) & IAction; + highlightedLegendItem: LegendItem | null; } interface BarGeometriesDataState { overBar?: BarGeometry; @@ -19,7 +22,7 @@ interface BarGeometriesDataState { export class BarGeometries extends React.PureComponent< BarGeometriesDataProps, BarGeometriesDataState -> { + > { static defaultProps: Partial = { animated: false, }; @@ -70,14 +73,34 @@ export class BarGeometries extends React.PureComponent< }); onElementOut(); } + + private computeBarOpacity = (bar: BarGeometry, overBar: BarGeometry | undefined): number => { + const { highlightedLegendItem } = this.props; + + // There are two elements that might be hovered over that could affect this: + // a specific bar element or a legend item; thus, we handle these states as mutually exclusive. + if (overBar) { + if (overBar !== bar) { + return 0.6; + } + return 1; + } else if (highlightedLegendItem != null) { + const isPartOfHighlightedSeries = belongsToDataSeries(bar.value, highlightedLegendItem.value); + + if (isPartOfHighlightedSeries) { + return 1; + } + + return 0.6; + } + return 1; + } + private renderBarGeoms = (bars: BarGeometry[]): JSX.Element[] => { const { overBar } = this.state; return bars.map((bar, i) => { const { x, y, width, height, color, value } = bar; - let opacity = 1; - if (overBar && overBar !== bar) { - opacity = 0.6; - } + const opacity = this.computeBarOpacity(bar, overBar); if (this.props.animated) { return ( diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index eb0a329717..51e0a1dea1 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -67,10 +67,14 @@ class Chart extends React.Component { onOverElement, onOutElement, onElementClickListener, + highlightedLegendItemIndex, + legendItems, } = this.props.chartStore!; if (!geometries) { return; } + const highlightedLegendItem = highlightedLegendItemIndex ? legendItems[highlightedLegendItemIndex] : null; + return ( { onElementOver={onOverElement} onElementOut={onOutElement} onElementClick={onElementClickListener} + highlightedLegendItem={highlightedLegendItem} /> ); } diff --git a/src/lib/series/series_utils.test.ts b/src/lib/series/series_utils.test.ts new file mode 100644 index 0000000000..ac97156731 --- /dev/null +++ b/src/lib/series/series_utils.test.ts @@ -0,0 +1,47 @@ +import { getSpecId } from '../utils/ids'; +import { GeometryValue } from './rendering'; +import { DataSeriesColorsValues } from './series'; +import { belongsToDataSeries, isEqualSeriesKey } from './series_utils'; + +describe('Series utility functions', () => { + test('can compare series keys for identity', () => { + const seriesKeyA = ['a', 'b', 'c']; + const seriesKeyB = ['a', 'b', 'c']; + const seriesKeyC = ['a', 'b', 'd']; + const seriesKeyD = ['d']; + const seriesKeyE = ['b', 'a', 'c']; + + expect(isEqualSeriesKey(seriesKeyA, seriesKeyB)).toBe(true); + expect(isEqualSeriesKey(seriesKeyB, seriesKeyC)).toBe(false); + expect(isEqualSeriesKey(seriesKeyA, seriesKeyD)).toBe(false); + expect(isEqualSeriesKey(seriesKeyA, seriesKeyE)).toBe(false); + expect(isEqualSeriesKey(seriesKeyA, [])).toBe(false); + }); + + test('can determine if a geometry value belongs to a data series', () => { + const geometryValueA: GeometryValue = { + specId: getSpecId('a'), + datum: null, + seriesKey: ['a', 'b', 'c'], + }; + + const dataSeriesValuesA: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesB: DataSeriesColorsValues = { + specId: getSpecId('b'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesC: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'd'], + }; + + expect(belongsToDataSeries(geometryValueA, dataSeriesValuesA)).toBe(true); + expect(belongsToDataSeries(geometryValueA, dataSeriesValuesB)).toBe(false); + expect(belongsToDataSeries(geometryValueA, dataSeriesValuesC)).toBe(false); + }); +}); diff --git a/src/lib/series/series_utils.ts b/src/lib/series/series_utils.ts new file mode 100644 index 0000000000..dbea45cbc6 --- /dev/null +++ b/src/lib/series/series_utils.ts @@ -0,0 +1,31 @@ +import { GeometryValue } from './rendering'; +import { DataSeriesColorsValues } from './series'; + +export function isEqualSeriesKey(a: any[], b: any[]): boolean { + if (a.length !== b.length) { + return false; + } + + let ret = true; + + a.forEach((aVal: any, idx: number) => { + if (aVal !== b[idx]) { + ret = false; + } + }); + + return ret; +} + +export function belongsToDataSeries(geometryValue: GeometryValue, dataSeriesValues: DataSeriesColorsValues): boolean { + const legendItemSeriesKey = dataSeriesValues.colorValues; + const legendItemSpecId = dataSeriesValues.specId; + + const geometrySeriesKey = geometryValue.seriesKey; + const geometrySpecId = geometryValue.specId; + + const hasSameSpecId = legendItemSpecId === geometrySpecId; + const hasSameSeriesKey = isEqualSeriesKey(legendItemSeriesKey, geometrySeriesKey); + + return hasSameSpecId && hasSameSeriesKey; +} diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index a1fa2d2512..19f386a64d 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -123,6 +123,7 @@ export class ChartStore { yScales?: Map; legendItems: LegendItem[] = []; + highlightedLegendItemIndex: number | null = null; tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -229,6 +230,13 @@ export class ChartStore { return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener); } + updateHighlightedLegendItem(legendItemIndex: number) { + if (legendItemIndex !== this.highlightedLegendItemIndex) { + this.highlightedLegendItemIndex = legendItemIndex; + this.computeChart(); + } + } + updateParentDimensions(width: number, height: number, top: number, left: number) { let isChanged = false; if (width !== this.parentDimensions.width) { From a0637178b4351a9802d077db3eb489dd38532be6 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Sun, 3 Feb 2019 22:13:15 -0800 Subject: [PATCH 02/46] feat(legend/series): set highlighted legend item to null on mouseout addresses #24 --- src/components/legend.tsx | 5 +++++ src/state/chart_state.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index c5b7526c0b..ba07c3f441 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -90,6 +90,7 @@ class LegendComponent extends React.Component { key: index, className: 'euiChartLegendList__item', onMouseOver: this.onLegendItemMouseover(index), + onMouseOut: this.onLegendItemMouseout, }; return ( @@ -107,6 +108,10 @@ class LegendComponent extends React.Component { private onLegendItemMouseover = (legendItemIndex: number) => () => { this.props.chartStore!.updateHighlightedLegendItem(legendItemIndex); } + + private onLegendItemMouseout = () => { + this.props.chartStore!.updateHighlightedLegendItem(null); + } } function LegendElement({ color, label }: Partial) { return ( diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 19f386a64d..e541206fd7 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -230,7 +230,7 @@ export class ChartStore { return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener); } - updateHighlightedLegendItem(legendItemIndex: number) { + updateHighlightedLegendItem(legendItemIndex: number | null) { if (legendItemIndex !== this.highlightedLegendItemIndex) { this.highlightedLegendItemIndex = legendItemIndex; this.computeChart(); From 94d650800f40fc8a2a1ee0636720ce3dfe96fef3 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Sun, 3 Feb 2019 22:28:49 -0800 Subject: [PATCH 03/46] feat(legend/series): compare highlightedLegendItemIndex with null addresses #24 --- src/components/react_canvas/reactive_chart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 51e0a1dea1..0612c222a3 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -73,7 +73,7 @@ class Chart extends React.Component { if (!geometries) { return; } - const highlightedLegendItem = highlightedLegendItemIndex ? legendItems[highlightedLegendItemIndex] : null; + const highlightedLegendItem = highlightedLegendItemIndex != null ? legendItems[highlightedLegendItemIndex] : null; return ( Date: Sun, 3 Feb 2019 22:49:44 -0800 Subject: [PATCH 04/46] feat(legend/series): add legend hover interaction to line series addresses #24 --- .../react_canvas/line_geometries.tsx | 29 +++++++++++++++++-- .../react_canvas/reactive_chart.tsx | 18 ++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index acf3b37f37..cc3acdd2fb 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -3,7 +3,9 @@ import { IAction } from 'mobx'; import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; +import { LegendItem } from '../../lib/series/legend'; import { GeometryValue, LineGeometry, PointGeometry } from '../../lib/series/rendering'; +import { belongsToDataSeries } from '../../lib/series/series_utils'; import { LineSeriesStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; @@ -14,6 +16,7 @@ interface LineGeometriesDataProps { onElementClick?: ElementClickListener; onElementOver: ((tooltip: TooltipData) => void) & IAction; onElementOut: (() => void) & IAction; + highlightedLegendItem: LegendItem | null; } interface LineGeometriesDataState { overPoint?: PointGeometry; @@ -21,7 +24,7 @@ interface LineGeometriesDataState { export class LineGeometries extends React.PureComponent< LineGeometriesDataProps, LineGeometriesDataState -> { + > { static defaultProps: Partial = { animated: false, }; @@ -129,10 +132,31 @@ export class LineGeometries extends React.PureComponent< ); }); } + + private computeLineOpacity = (point: PointGeometry): number => { + const { highlightedLegendItem } = this.props; + + if (highlightedLegendItem != null) { + const isPartOfHighlightedSeries = belongsToDataSeries(point.value, highlightedLegendItem.value); + + if (isPartOfHighlightedSeries) { + return 1; + } + + return 0.25; + } + return 1; + } + private renderLineGeoms = (): JSX.Element[] => { const { style, lines } = this.props; return lines.map((glyph, i) => { - const { line, color, transform } = glyph; + const { line, color, transform, points } = glyph; + + // TODO: May want to consider a way to get GeometryValue from LineGeometry instead of + // PointGeometry (which is why we currently use the first point) + const opacity = this.computeLineOpacity(points[0]); + if (this.props.animated) { return ( @@ -146,6 +170,7 @@ export class LineGeometries extends React.PureComponent< listening={false} lineCap="round" lineJoin="round" + opacity={opacity} /> )} diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 0612c222a3..47521e03e0 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -67,13 +67,11 @@ class Chart extends React.Component { onOverElement, onOutElement, onElementClickListener, - highlightedLegendItemIndex, - legendItems, } = this.props.chartStore!; if (!geometries) { return; } - const highlightedLegendItem = highlightedLegendItemIndex != null ? legendItems[highlightedLegendItemIndex] : null; + const highlightedLegendItem = this.getHighlightedLegendItem(); return ( { if (!geometries) { return; } + + const highlightedLegendItem = this.getHighlightedLegendItem(); + return ( { onElementOver={onOverElement} onElementOut={onOutElement} onElementClick={onElementClickListener} + highlightedLegendItem={highlightedLegendItem} /> ); } @@ -358,6 +360,16 @@ class Chart extends React.Component { /> ); } + + private getHighlightedLegendItem = () => { + const { + highlightedLegendItemIndex, + legendItems, + } = this.props.chartStore!; + const highlightedLegendItem = highlightedLegendItemIndex != null ? legendItems[highlightedLegendItemIndex] : null; + + return highlightedLegendItem; + } } export const ReactiveChart = inject('chartStore')(observer(Chart)); From ae5dcafc18d280dea2da6812d0a910a914690a77 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Sun, 3 Feb 2019 22:56:32 -0800 Subject: [PATCH 05/46] feat(legend/series): add legend hover interaction to area chart addresses #24 --- .../react_canvas/area_geometries.tsx | 38 ++++++++++++++++--- .../react_canvas/bar_geometries.tsx | 2 +- .../react_canvas/line_geometries.tsx | 1 + .../react_canvas/reactive_chart.tsx | 4 ++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx index 866bd1b5d0..9b23241ae2 100644 --- a/src/components/react_canvas/area_geometries.tsx +++ b/src/components/react_canvas/area_geometries.tsx @@ -3,7 +3,9 @@ import { IAction } from 'mobx'; import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; +import { LegendItem } from '../../lib/series/legend'; import { AreaGeometry, GeometryValue, PointGeometry } from '../../lib/series/rendering'; +import { belongsToDataSeries } from '../../lib/series/series_utils'; import { AreaSeriesStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; @@ -15,6 +17,7 @@ interface AreaGeometriesDataProps { onElementClick?: ElementClickListener; onElementOver: ((tooltip: TooltipData) => void) & IAction; onElementOut: (() => void) & IAction; + highlightedLegendItem: LegendItem | null; } interface AreaGeometriesDataState { overPoint?: PointGeometry; @@ -22,7 +25,7 @@ interface AreaGeometriesDataState { export class AreaGeometries extends React.PureComponent< AreaGeometriesDataProps, AreaGeometriesDataState -> { + > { static defaultProps: Partial = { animated: false, num: 1, @@ -131,10 +134,31 @@ export class AreaGeometries extends React.PureComponent< ); }); } + + private computeAreaOpacity = (point: PointGeometry): number => { + const { highlightedLegendItem } = this.props; + + if (highlightedLegendItem != null) { + const isPartOfHighlightedSeries = belongsToDataSeries(point.value, highlightedLegendItem.value); + + if (isPartOfHighlightedSeries) { + return 1; + } + + return 0.25; + } + return 1; + } + private renderAreaGeoms = (): JSX.Element[] => { const { areas } = this.props; return areas.map((glyph, i) => { - const { area, color, transform } = glyph; + const { area, color, transform, points } = glyph; + + // TODO: May want to consider a way to get GeometryValue from LineGeometry instead of + // PointGeometry (which is why we currently use the first point) + const opacity = this.computeAreaOpacity(points[0]); + if (this.props.animated) { return ( @@ -145,8 +169,9 @@ export class AreaGeometries extends React.PureComponent< data={props.area} fill={color} listening={false} - // areaCap="round" - // areaJoin="round" + opacity={opacity} + // areaCap="round" + // areaJoin="round" /> )} @@ -159,8 +184,9 @@ export class AreaGeometries extends React.PureComponent< data={area} fill={color} listening={false} - // areaCap="round" - // areaJoin="round" + opacity={opacity} + // areaCap="round" + // areaJoin="round" /> ); } diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index 65baf07164..c11ae705fe 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -91,7 +91,7 @@ export class BarGeometries extends React.PureComponent< return 1; } - return 0.6; + return 0.25; } return 1; } diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index cc3acdd2fb..c8ffb41c2b 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -186,6 +186,7 @@ export class LineGeometries extends React.PureComponent< listening={false} lineCap="round" lineJoin="round" + opacity={opacity} /> ); } diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 47521e03e0..42740c985c 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -123,6 +123,9 @@ class Chart extends React.Component { if (!geometries) { return; } + + const highlightedLegendItem = this.getHighlightedLegendItem(); + return ( { onElementOver={onOverElement} onElementOut={onOutElement} onElementClick={onElementClickListener} + highlightedLegendItem={highlightedLegendItem} /> ); } From d6d88a9f5492a9b4ad23a398f43588363da5bf4e Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Wed, 6 Feb 2019 14:10:26 -0800 Subject: [PATCH 06/46] refactor(geometry): allow all geometries to have specId & seriesKey Previously, LineGeometry and AreaGeometry did not have specId and seriesKey but rather had those properties within each of their points. This meant that there wasn't a way to use specId and seriesKey on the whole geometry, but required accessing a point within the geometry. --- .../react_canvas/area_geometries.tsx | 12 +++++----- .../react_canvas/bar_geometries.tsx | 2 +- .../react_canvas/line_geometries.tsx | 12 +++++----- src/lib/series/rendering.ts | 22 +++++++++++++++++-- src/lib/series/series_utils.test.ts | 13 +++++------ src/lib/series/series_utils.ts | 4 ++-- 6 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx index 9b23241ae2..d188978783 100644 --- a/src/components/react_canvas/area_geometries.tsx +++ b/src/components/react_canvas/area_geometries.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { AreaGeometry, GeometryValue, PointGeometry } from '../../lib/series/rendering'; +import { AreaGeometry, GeometryId, GeometryValue, PointGeometry } from '../../lib/series/rendering'; import { belongsToDataSeries } from '../../lib/series/series_utils'; import { AreaSeriesStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; @@ -135,11 +135,11 @@ export class AreaGeometries extends React.PureComponent< }); } - private computeAreaOpacity = (point: PointGeometry): number => { + private computeAreaOpacity = (geometryId: GeometryId): number => { const { highlightedLegendItem } = this.props; if (highlightedLegendItem != null) { - const isPartOfHighlightedSeries = belongsToDataSeries(point.value, highlightedLegendItem.value); + const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); if (isPartOfHighlightedSeries) { return 1; @@ -153,11 +153,9 @@ export class AreaGeometries extends React.PureComponent< private renderAreaGeoms = (): JSX.Element[] => { const { areas } = this.props; return areas.map((glyph, i) => { - const { area, color, transform, points } = glyph; + const { area, color, transform, geometryId } = glyph; - // TODO: May want to consider a way to get GeometryValue from LineGeometry instead of - // PointGeometry (which is why we currently use the first point) - const opacity = this.computeAreaOpacity(points[0]); + const opacity = this.computeAreaOpacity(geometryId); if (this.props.animated) { return ( diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index c11ae705fe..0864e3d2a8 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -85,7 +85,7 @@ export class BarGeometries extends React.PureComponent< } return 1; } else if (highlightedLegendItem != null) { - const isPartOfHighlightedSeries = belongsToDataSeries(bar.value, highlightedLegendItem.value); + const isPartOfHighlightedSeries = belongsToDataSeries(bar.geometryId, highlightedLegendItem.value); if (isPartOfHighlightedSeries) { return 1; diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index c8ffb41c2b..8899c52d13 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { GeometryValue, LineGeometry, PointGeometry } from '../../lib/series/rendering'; +import { GeometryId, GeometryValue, LineGeometry, PointGeometry } from '../../lib/series/rendering'; import { belongsToDataSeries } from '../../lib/series/series_utils'; import { LineSeriesStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; @@ -133,11 +133,11 @@ export class LineGeometries extends React.PureComponent< }); } - private computeLineOpacity = (point: PointGeometry): number => { + private computeLineOpacity = (geometryId: GeometryId): number => { const { highlightedLegendItem } = this.props; if (highlightedLegendItem != null) { - const isPartOfHighlightedSeries = belongsToDataSeries(point.value, highlightedLegendItem.value); + const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); if (isPartOfHighlightedSeries) { return 1; @@ -151,11 +151,9 @@ export class LineGeometries extends React.PureComponent< private renderLineGeoms = (): JSX.Element[] => { const { style, lines } = this.props; return lines.map((glyph, i) => { - const { line, color, transform, points } = glyph; + const { line, color, transform, geometryId } = glyph; - // TODO: May want to consider a way to get GeometryValue from LineGeometry instead of - // PointGeometry (which is why we currently use the first point) - const opacity = this.computeLineOpacity(points[0]); + const opacity = this.computeLineOpacity(geometryId); if (this.props.animated) { return ( diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index a8d8e11053..a08a43a649 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -4,11 +4,14 @@ import { Scale } from '../utils/scales/scales'; import { CurveType, getCurveFactory } from './curves'; import { DataSeriesDatum } from './series'; -export interface GeometryValue { +export interface GeometryId { specId: SpecId; - datum: any; seriesKey: any[]; } + +export interface GeometryValue extends GeometryId { + datum: any; +} export interface PointGeometry { x: number; y: number; @@ -26,6 +29,7 @@ export interface BarGeometry { height: number; color: string; value: GeometryValue; + geometryId: GeometryId; } export interface LineGeometry { line: string; @@ -35,6 +39,7 @@ export interface LineGeometry { x: number; y: number; }; + geometryId: GeometryId; } export interface AreaGeometry { area: string; @@ -45,6 +50,7 @@ export interface AreaGeometry { x: number; y: number; }; + geometryId: GeometryId; } export function renderPoints( @@ -95,6 +101,10 @@ export function renderBars( datum: datum.datum, seriesKey, }, + geometryId: { + specId, + seriesKey, + }, }; }); } @@ -123,6 +133,10 @@ export function renderLine( x, y, }, + geometryId: { + specId, + seriesKey, + }, }; } @@ -148,5 +162,9 @@ export function renderArea( points: lineGeometry.points, color, transform: lineGeometry.transform, + geometryId: { + specId, + seriesKey, + }, }; } diff --git a/src/lib/series/series_utils.test.ts b/src/lib/series/series_utils.test.ts index ac97156731..d098eab356 100644 --- a/src/lib/series/series_utils.test.ts +++ b/src/lib/series/series_utils.test.ts @@ -1,5 +1,5 @@ import { getSpecId } from '../utils/ids'; -import { GeometryValue } from './rendering'; +import { GeometryId } from './rendering'; import { DataSeriesColorsValues } from './series'; import { belongsToDataSeries, isEqualSeriesKey } from './series_utils'; @@ -18,10 +18,9 @@ describe('Series utility functions', () => { expect(isEqualSeriesKey(seriesKeyA, [])).toBe(false); }); - test('can determine if a geometry value belongs to a data series', () => { - const geometryValueA: GeometryValue = { + test('can determine if a geometry id belongs to a data series', () => { + const geometryIdA: GeometryId = { specId: getSpecId('a'), - datum: null, seriesKey: ['a', 'b', 'c'], }; @@ -40,8 +39,8 @@ describe('Series utility functions', () => { colorValues: ['a', 'b', 'd'], }; - expect(belongsToDataSeries(geometryValueA, dataSeriesValuesA)).toBe(true); - expect(belongsToDataSeries(geometryValueA, dataSeriesValuesB)).toBe(false); - expect(belongsToDataSeries(geometryValueA, dataSeriesValuesC)).toBe(false); + expect(belongsToDataSeries(geometryIdA, dataSeriesValuesA)).toBe(true); + expect(belongsToDataSeries(geometryIdA, dataSeriesValuesB)).toBe(false); + expect(belongsToDataSeries(geometryIdA, dataSeriesValuesC)).toBe(false); }); }); diff --git a/src/lib/series/series_utils.ts b/src/lib/series/series_utils.ts index dbea45cbc6..6c42cacbf0 100644 --- a/src/lib/series/series_utils.ts +++ b/src/lib/series/series_utils.ts @@ -1,4 +1,4 @@ -import { GeometryValue } from './rendering'; +import { GeometryId } from './rendering'; import { DataSeriesColorsValues } from './series'; export function isEqualSeriesKey(a: any[], b: any[]): boolean { @@ -17,7 +17,7 @@ export function isEqualSeriesKey(a: any[], b: any[]): boolean { return ret; } -export function belongsToDataSeries(geometryValue: GeometryValue, dataSeriesValues: DataSeriesColorsValues): boolean { +export function belongsToDataSeries(geometryValue: GeometryId, dataSeriesValues: DataSeriesColorsValues): boolean { const legendItemSeriesKey = dataSeriesValues.colorValues; const legendItemSpecId = dataSeriesValues.specId; From c062b2eaf7790c2852951dd90b5dfbf72c177663 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 7 Feb 2019 10:47:54 -0800 Subject: [PATCH 07/46] refactor(geometries): share common styles with different geometries --- .../react_canvas/area_geometries.tsx | 24 +++------------ .../react_canvas/bar_geometries.tsx | 29 +++++++------------ .../react_canvas/line_geometries.tsx | 24 +++------------ src/lib/series/rendering.ts | 24 +++++++++++++++ 4 files changed, 42 insertions(+), 59 deletions(-) diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx index d188978783..b0c0a7aef3 100644 --- a/src/components/react_canvas/area_geometries.tsx +++ b/src/components/react_canvas/area_geometries.tsx @@ -4,8 +4,7 @@ import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { AreaGeometry, GeometryId, GeometryValue, PointGeometry } from '../../lib/series/rendering'; -import { belongsToDataSeries } from '../../lib/series/series_utils'; +import { AreaGeometry, GeometryValue, getGeometryStyle, PointGeometry } from '../../lib/series/rendering'; import { AreaSeriesStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; @@ -135,27 +134,12 @@ export class AreaGeometries extends React.PureComponent< }); } - private computeAreaOpacity = (geometryId: GeometryId): number => { - const { highlightedLegendItem } = this.props; - - if (highlightedLegendItem != null) { - const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); - - if (isPartOfHighlightedSeries) { - return 1; - } - - return 0.25; - } - return 1; - } - private renderAreaGeoms = (): JSX.Element[] => { const { areas } = this.props; return areas.map((glyph, i) => { const { area, color, transform, geometryId } = glyph; - const opacity = this.computeAreaOpacity(geometryId); + const geometryStyle = getGeometryStyle(geometryId, this.props.highlightedLegendItem); if (this.props.animated) { return ( @@ -167,7 +151,7 @@ export class AreaGeometries extends React.PureComponent< data={props.area} fill={color} listening={false} - opacity={opacity} + {...geometryStyle} // areaCap="round" // areaJoin="round" /> @@ -182,7 +166,7 @@ export class AreaGeometries extends React.PureComponent< data={area} fill={color} listening={false} - opacity={opacity} + {...geometryStyle} // areaCap="round" // areaJoin="round" /> diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index 0864e3d2a8..5cb602c23b 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -4,8 +4,7 @@ import React from 'react'; import { Group, Rect } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { BarGeometry, GeometryValue } from '../../lib/series/rendering'; -import { belongsToDataSeries } from '../../lib/series/series_utils'; +import { BarGeometry, GeometryValue, getGeometryStyle } from '../../lib/series/rendering'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; interface BarGeometriesDataProps { @@ -75,23 +74,8 @@ export class BarGeometries extends React.PureComponent< } private computeBarOpacity = (bar: BarGeometry, overBar: BarGeometry | undefined): number => { - const { highlightedLegendItem } = this.props; - - // There are two elements that might be hovered over that could affect this: - // a specific bar element or a legend item; thus, we handle these states as mutually exclusive. - if (overBar) { - if (overBar !== bar) { - return 0.6; - } - return 1; - } else if (highlightedLegendItem != null) { - const isPartOfHighlightedSeries = belongsToDataSeries(bar.geometryId, highlightedLegendItem.value); - - if (isPartOfHighlightedSeries) { - return 1; - } - - return 0.25; + if (overBar && overBar !== bar) { + return 0.6; } return 1; } @@ -100,7 +84,12 @@ export class BarGeometries extends React.PureComponent< const { overBar } = this.state; return bars.map((bar, i) => { const { x, y, width, height, color, value } = bar; + + // This sets the opacity if any bars within the chart are hovered over const opacity = this.computeBarOpacity(bar, overBar); + + const geometryStyle = getGeometryStyle(bar.geometryId, this.props.highlightedLegendItem); + if (this.props.animated) { return ( @@ -119,6 +108,7 @@ export class BarGeometries extends React.PureComponent< onMouseOver={this.onOverBar(bar)} onMouseLeave={this.onOutBar} onClick={this.onElementClick(value)} + {...geometryStyle} /> )} @@ -139,6 +129,7 @@ export class BarGeometries extends React.PureComponent< onMouseOver={this.onOverBar(bar)} onMouseLeave={this.onOutBar} onClick={this.onElementClick(bar.value)} + {...geometryStyle} /> ); } diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index 8899c52d13..8058c6f1c9 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -4,8 +4,7 @@ import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { animated, Spring } from 'react-spring/konva'; import { LegendItem } from '../../lib/series/legend'; -import { GeometryId, GeometryValue, LineGeometry, PointGeometry } from '../../lib/series/rendering'; -import { belongsToDataSeries } from '../../lib/series/series_utils'; +import { GeometryValue, getGeometryStyle, LineGeometry, PointGeometry } from '../../lib/series/rendering'; import { LineSeriesStyle } from '../../lib/themes/theme'; import { ElementClickListener, TooltipData } from '../../state/chart_state'; @@ -133,27 +132,12 @@ export class LineGeometries extends React.PureComponent< }); } - private computeLineOpacity = (geometryId: GeometryId): number => { - const { highlightedLegendItem } = this.props; - - if (highlightedLegendItem != null) { - const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); - - if (isPartOfHighlightedSeries) { - return 1; - } - - return 0.25; - } - return 1; - } - private renderLineGeoms = (): JSX.Element[] => { const { style, lines } = this.props; return lines.map((glyph, i) => { const { line, color, transform, geometryId } = glyph; - const opacity = this.computeLineOpacity(geometryId); + const geometryStyle = getGeometryStyle(geometryId, this.props.highlightedLegendItem); if (this.props.animated) { return ( @@ -168,7 +152,7 @@ export class LineGeometries extends React.PureComponent< listening={false} lineCap="round" lineJoin="round" - opacity={opacity} + {...geometryStyle} /> )} @@ -184,7 +168,7 @@ export class LineGeometries extends React.PureComponent< listening={false} lineCap="round" lineJoin="round" - opacity={opacity} + {...geometryStyle} /> ); } diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index a08a43a649..978dcf6a6d 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -2,7 +2,9 @@ import { area, line } from 'd3-shape'; import { SpecId } from '../utils/ids'; import { Scale } from '../utils/scales/scales'; import { CurveType, getCurveFactory } from './curves'; +import { LegendItem } from './legend'; import { DataSeriesDatum } from './series'; +import { belongsToDataSeries } from './series_utils'; export interface GeometryId { specId: SpecId; @@ -12,6 +14,12 @@ export interface GeometryId { export interface GeometryValue extends GeometryId { datum: any; } + +/** Shared style properties for varies geometries */ +export interface GeometryStyle { + opacity: number; +} + export interface PointGeometry { x: number; y: number; @@ -168,3 +176,19 @@ export function renderArea( }, }; } + +export function getGeometryStyle( + geometryId: GeometryId, + highlightedLegendItem: LegendItem | null, +): GeometryStyle { + let opacity = 1; + if (highlightedLegendItem != null) { + const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); + + if (!isPartOfHighlightedSeries) { + opacity = 0.25; + } + } + + return { opacity }; +} From 409097e50362de316fc5d22d7b0db4a29b206836 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 7 Feb 2019 10:57:22 -0800 Subject: [PATCH 08/46] refactor(geometry): add GEOMETRY_STYLES constant --- src/lib/series/rendering.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index 978dcf6a6d..8bd312d839 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -181,14 +181,23 @@ export function getGeometryStyle( geometryId: GeometryId, highlightedLegendItem: LegendItem | null, ): GeometryStyle { - let opacity = 1; if (highlightedLegendItem != null) { const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); - if (!isPartOfHighlightedSeries) { - opacity = 0.25; - } + return isPartOfHighlightedSeries ? GEOMETRY_STYLES.highlighted : GEOMETRY_STYLES.unhighlighted; } - return { opacity }; + return GEOMETRY_STYLES.default; } + +const GEOMETRY_STYLES: { [key: string]: GeometryStyle } = { + default: { + opacity: 1, + }, + highlighted: { + opacity: 1, + }, + unhighlighted: { + opacity: 0.25, + }, +}; From 65b59f87f723f71d5721ca2760c7c06ef46839a9 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 08:32:50 -0800 Subject: [PATCH 09/46] docs(legend): add interaction stories for each chart type --- stories/interactions.tsx | 193 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 1 deletion(-) diff --git a/stories/interactions.tsx b/stories/interactions.tsx index 1b404c953d..993bb17bf8 100644 --- a/stories/interactions.tsx +++ b/stories/interactions.tsx @@ -14,6 +14,8 @@ import { import { boolean } from '@storybook/addon-knobs'; import { DateTime } from 'luxon'; +import { CurveType } from '../src/lib/series/curves'; +import * as TestDatasets from '../src/lib/series/utils/test_dataset'; import { AreaSeries, LineSeries } from '../src/specs'; import { niceTimeFormatter } from '../src/utils/data/formatters'; @@ -111,7 +113,196 @@ storiesOf('Interactions', module) ); }) - .add('click/hovers on legend items (TO DO)', () =>

TO DO

) + .add('click/hovers on legend items [bar chart] (TO DO click)', () => { + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); + }) + .add('click/hovers on legend items [area chart] (TO DO click)', () => { + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); + }) + .add('click/hovers on legend items [line chart] (TO DO click)', () => { + return ( + + + + Number(d).toFixed(2)} + /> + + + + + + + + + ); + }) + .add('click/hovers on legend items [mixed chart] (TO DO click)', () => { + return ( + + + + Number(d).toFixed(2)} + /> + + + + + ); + }) .add('brush selection tool on linear', () => { return ( From eec61b31cdc8c8981958ce6661967945a538bdc4 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 09:01:02 -0800 Subject: [PATCH 10/46] refactor(legend): move getter to store --- src/components/react_canvas/reactive_chart.tsx | 8 +------- src/state/chart_state.ts | 7 +++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 42740c985c..7affc98feb 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -366,13 +366,7 @@ class Chart extends React.Component { } private getHighlightedLegendItem = () => { - const { - highlightedLegendItemIndex, - legendItems, - } = this.props.chartStore!; - const highlightedLegendItem = highlightedLegendItemIndex != null ? legendItems[highlightedLegendItemIndex] : null; - - return highlightedLegendItem; + return this.props.chartStore!.getHighlightedLegendItem(); } } diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index e541206fd7..db33b7d5c8 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -237,6 +237,13 @@ export class ChartStore { } } + getHighlightedLegendItem(): LegendItem | null { + if (this.highlightedLegendItemIndex != null) { + return this.legendItems[this.highlightedLegendItemIndex]; + } + return null; + } + updateParentDimensions(width: number, height: number, top: number, left: number) { let isChanged = false; if (width !== this.parentDimensions.width) { From 98d5cece7ec29c829bf89ffced3f2a910a6090b8 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 10:58:59 -0800 Subject: [PATCH 11/46] refactor: move shared geometry style to theme --- src/lib/series/rendering.ts | 18 ++++-------------- src/lib/themes/theme.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index 8bd312d839..db5429b503 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -1,4 +1,5 @@ import { area, line } from 'd3-shape'; +import { DEFAULT_THEME } from '../themes/theme'; import { SpecId } from '../utils/ids'; import { Scale } from '../utils/scales/scales'; import { CurveType, getCurveFactory } from './curves'; @@ -181,23 +182,12 @@ export function getGeometryStyle( geometryId: GeometryId, highlightedLegendItem: LegendItem | null, ): GeometryStyle { + const { shared } = DEFAULT_THEME.chart.styles; if (highlightedLegendItem != null) { const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); - return isPartOfHighlightedSeries ? GEOMETRY_STYLES.highlighted : GEOMETRY_STYLES.unhighlighted; + return isPartOfHighlightedSeries ? shared.highlighted : shared.unhighlighted; } - return GEOMETRY_STYLES.default; + return shared.default; } - -const GEOMETRY_STYLES: { [key: string]: GeometryStyle } = { - default: { - opacity: 1, - }, - highlighted: { - opacity: 1, - }, - unhighlighted: { - opacity: 0.25, - }, -}; diff --git a/src/lib/themes/theme.ts b/src/lib/themes/theme.ts index ab1b13779f..34bad0574c 100644 --- a/src/lib/themes/theme.ts +++ b/src/lib/themes/theme.ts @@ -1,3 +1,4 @@ +import { GeometryStyle } from '../series/rendering'; import { Margins } from '../utils/dimensions'; export interface ChartConfig { @@ -11,6 +12,7 @@ export interface ChartConfig { styles: { lineSeries: LineSeriesStyle; areaSeries: AreaSeriesStyle; + shared: { [key: string]: GeometryStyle }; }; } export interface AxisConfig { @@ -91,6 +93,18 @@ export const DEFAULT_GRID_LINE_CONFIG: GridLineConfig = { opacity: 1, }; +export const GEOMETRY_STYLES: { [key: string]: GeometryStyle } = { + default: { + opacity: 1, + }, + highlighted: { + opacity: 1, + }, + unhighlighted: { + opacity: 0.25, + }, +}; + export const DEFAULT_THEME: Theme = { chart: { paddings: { @@ -130,6 +144,7 @@ export const DEFAULT_THEME: Theme = { dataPointsStroke: 'white', dataPointsStrokeWidth: 1, }, + shared: GEOMETRY_STYLES, }, }, scales: { From 7d12db7382912bcc2de6daef67f1676593d0925a Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 11:04:34 -0800 Subject: [PATCH 12/46] refactor(legend): use observable for legend item state --- src/state/chart_state.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index db33b7d5c8..3b4e7b7134 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -1,4 +1,4 @@ -import { action, observable } from 'mobx'; +import { action, IObservableValue, observable } from 'mobx'; import { AxisLinePosition, AxisTick, @@ -123,7 +123,7 @@ export class ChartStore { yScales?: Map; legendItems: LegendItem[] = []; - highlightedLegendItemIndex: number | null = null; + highlightedLegendItemIndex: IObservableValue = observable.box(null); tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -231,17 +231,12 @@ export class ChartStore { } updateHighlightedLegendItem(legendItemIndex: number | null) { - if (legendItemIndex !== this.highlightedLegendItemIndex) { - this.highlightedLegendItemIndex = legendItemIndex; - this.computeChart(); - } + this.highlightedLegendItemIndex.set(legendItemIndex); } getHighlightedLegendItem(): LegendItem | null { - if (this.highlightedLegendItemIndex != null) { - return this.legendItems[this.highlightedLegendItemIndex]; - } - return null; + const index = this.highlightedLegendItemIndex.get(); + return index == null ? null : this.legendItems[index]; } updateParentDimensions(width: number, height: number, top: number, left: number) { From 61a24431115939bfdec933fa33d2b3b489e3b46a Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 11:16:51 -0800 Subject: [PATCH 13/46] refactor: use for loop for series key array comparison --- src/lib/series/series_utils.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/lib/series/series_utils.ts b/src/lib/series/series_utils.ts index 6c42cacbf0..657266a367 100644 --- a/src/lib/series/series_utils.ts +++ b/src/lib/series/series_utils.ts @@ -6,15 +6,13 @@ export function isEqualSeriesKey(a: any[], b: any[]): boolean { return false; } - let ret = true; - - a.forEach((aVal: any, idx: number) => { - if (aVal !== b[idx]) { - ret = false; + for (let i = 0, l = a.length; i < l; i++) { + if (a[i] !== b[i]) { + return false; } - }); + } - return ret; + return true; } export function belongsToDataSeries(geometryValue: GeometryId, dataSeriesValues: DataSeriesColorsValues): boolean { From 77e101f83c11280e0d07ec944aeee2ad988eb10e Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 11:37:48 -0800 Subject: [PATCH 14/46] refactor(geometry): move bar hover opacity logic to shared logic --- .../react_canvas/bar_geometries.tsx | 20 ++++++++----------- src/lib/series/rendering.ts | 10 ++++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index 5cb602c23b..66afadc51f 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -73,22 +73,20 @@ export class BarGeometries extends React.PureComponent< onElementOut(); } - private computeBarOpacity = (bar: BarGeometry, overBar: BarGeometry | undefined): number => { - if (overBar && overBar !== bar) { - return 0.6; - } - return 1; - } - private renderBarGeoms = (bars: BarGeometry[]): JSX.Element[] => { const { overBar } = this.state; return bars.map((bar, i) => { const { x, y, width, height, color, value } = bar; - // This sets the opacity if any bars within the chart are hovered over - const opacity = this.computeBarOpacity(bar, overBar); + // Properties to determine if we need to highlight individual bars depending on hover state + const hasGeometryHover = overBar != null; + const hasHighlight = overBar === bar; + const individualHighlight = { + hasGeometryHover, + hasHighlight, + }; - const geometryStyle = getGeometryStyle(bar.geometryId, this.props.highlightedLegendItem); + const geometryStyle = getGeometryStyle(bar.geometryId, this.props.highlightedLegendItem, individualHighlight); if (this.props.animated) { return ( @@ -103,7 +101,6 @@ export class BarGeometries extends React.PureComponent< height={props.height} fill={color} strokeWidth={0} - opacity={opacity} perfectDrawEnabled={true} onMouseOver={this.onOverBar(bar)} onMouseLeave={this.onOutBar} @@ -124,7 +121,6 @@ export class BarGeometries extends React.PureComponent< height={height} fill={color} strokeWidth={0} - opacity={opacity} perfectDrawEnabled={false} onMouseOver={this.onOverBar(bar)} onMouseLeave={this.onOutBar} diff --git a/src/lib/series/rendering.ts b/src/lib/series/rendering.ts index db5429b503..bd0e5e7cf1 100644 --- a/src/lib/series/rendering.ts +++ b/src/lib/series/rendering.ts @@ -181,13 +181,23 @@ export function renderArea( export function getGeometryStyle( geometryId: GeometryId, highlightedLegendItem: LegendItem | null, + individualHighlight?: { [key: string]: boolean }, ): GeometryStyle { const { shared } = DEFAULT_THEME.chart.styles; + if (highlightedLegendItem != null) { const isPartOfHighlightedSeries = belongsToDataSeries(geometryId, highlightedLegendItem.value); return isPartOfHighlightedSeries ? shared.highlighted : shared.unhighlighted; } + if (individualHighlight) { + const { hasHighlight, hasGeometryHover } = individualHighlight; + if (!hasGeometryHover) { + return shared.highlighted; + } + return hasHighlight ? shared.highlighted : shared.unhighlighted; + } + return shared.default; } From 4e4dafd020197bfd1d27fd13a6096823ae770ef5 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 14:26:56 -0800 Subject: [PATCH 15/46] refactor: use computed for current highlighted legend item --- src/components/react_canvas/reactive_chart.tsx | 2 +- src/state/chart_state.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 7affc98feb..0f1cd7aac3 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -366,7 +366,7 @@ class Chart extends React.Component { } private getHighlightedLegendItem = () => { - return this.props.chartStore!.getHighlightedLegendItem(); + return this.props.chartStore!.highlightedLegendItem.get(); } } diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 3b4e7b7134..15848b206c 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -1,4 +1,4 @@ -import { action, IObservableValue, observable } from 'mobx'; +import { action, computed, IObservableValue, observable } from 'mobx'; import { AxisLinePosition, AxisTick, @@ -187,6 +187,11 @@ export class ChartStore { this.showLegend.set(showLegend); }); + highlightedLegendItem = computed(() => { + const index = this.highlightedLegendItemIndex.get(); + return index == null ? null : this.legendItems[index]; + }); + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } @@ -234,11 +239,6 @@ export class ChartStore { this.highlightedLegendItemIndex.set(legendItemIndex); } - getHighlightedLegendItem(): LegendItem | null { - const index = this.highlightedLegendItemIndex.get(); - return index == null ? null : this.legendItems[index]; - } - updateParentDimensions(width: number, height: number, top: number, left: number) { let isChanged = false; if (width !== this.parentDimensions.width) { From 26ff575a843f22214fe274819f337cbe32d3ddfc Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 15:32:49 -0800 Subject: [PATCH 16/46] feat(legend): add mouse over/out listeners from store --- src/components/legend.tsx | 1 + src/specs/settings.tsx | 11 +++++++++++ src/state/chart_state.ts | 28 ++++++++++++++++++++++++++++ stories/interactions.tsx | 7 ++++++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index ba07c3f441..377cf66b9c 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -106,6 +106,7 @@ class LegendComponent extends React.Component { } private onLegendItemMouseover = (legendItemIndex: number) => () => { + this.props.chartStore!.onLegendItemOver(); this.props.chartStore!.updateHighlightedLegendItem(legendItemIndex); } diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 4dd3006a7c..fd4af60508 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -7,6 +7,7 @@ import { ChartStore, ElementClickListener, ElementOverListener, + LegendItemListener, } from '../state/chart_state'; interface SettingSpecProps { @@ -22,6 +23,8 @@ interface SettingSpecProps { onElementOver?: ElementOverListener; onElementOut?: () => undefined; onBrushEnd?: BrushEndListener; + onLegendItemOver?: LegendItemListener; + onLegendItemOut?: LegendItemListener; } function updateChartStore(props: SettingSpecProps) { @@ -37,6 +40,8 @@ function updateChartStore(props: SettingSpecProps) { onElementOver, onElementOut, onBrushEnd, + onLegendItemOver, + onLegendItemOut, debug, } = props; if (!chartStore) { @@ -63,6 +68,12 @@ function updateChartStore(props: SettingSpecProps) { if (onBrushEnd) { chartStore.setOnBrushEndListener(onBrushEnd); } + if (onLegendItemOver) { + chartStore.setOnLegendItemOverListener(onLegendItemOver); + } + if (onLegendItemOut) { + chartStore.setOnLegendItemOutListener(onLegendItemOut); + } } export class SettingsComponent extends PureComponent { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 15848b206c..d8d699dc2d 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -76,6 +76,7 @@ export interface SeriesDomainsAndData { export type ElementClickListener = (value: GeometryValue) => void; export type ElementOverListener = (value: GeometryValue) => void; export type BrushEndListener = (min: number, max: number) => void; +export type LegendItemListener = () => void; // const MAX_ANIMATABLE_GLYPHS = 500; export class ChartStore { @@ -134,6 +135,9 @@ export class ChartStore { onElementOutListener?: () => undefined; onBrushEndListener?: BrushEndListener; + onLegendItemOverListener?: LegendItemListener; + onLegendItemOutListener?: LegendItemListener; + geometries: { points: PointGeometry[]; bars: BarGeometry[]; @@ -192,6 +196,18 @@ export class ChartStore { return index == null ? null : this.legendItems[index]; }); + onLegendItemOver = action(() => { + if (this.onLegendItemOverListener) { + this.onLegendItemOverListener(); + } + }); + + onLegendItemOut = action(() => { + if (this.onLegendItemOutListener) { + this.onLegendItemOutListener(); + } + }); + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } @@ -204,6 +220,12 @@ export class ChartStore { setOnBrushEndListener(listener: BrushEndListener) { this.onBrushEndListener = listener; } + setOnLegendItemOverListener(listener: LegendItemListener) { + this.onLegendItemOverListener = listener; + } + setOnLegendItemOutListener(listener: LegendItemListener) { + this.onLegendItemOutListener = listener; + } removeElementClickListener() { this.onElementClickListener = undefined; } @@ -213,6 +235,12 @@ export class ChartStore { removeElementOutListener() { this.onElementOutListener = undefined; } + removeOnLegendItemOverListener() { + this.onLegendItemOverListener = undefined; + } + removeOnLegendItemOutListener() { + this.onLegendItemOutListener = undefined; + } onBrushEnd(start: Point, end: Point) { if (!this.onBrushEndListener) { return; diff --git a/stories/interactions.tsx b/stories/interactions.tsx index 993bb17bf8..5d9752bf6e 100644 --- a/stories/interactions.tsx +++ b/stories/interactions.tsx @@ -25,6 +25,11 @@ const onElementListeners = { onElementOut: action('onElementOut'), }; +const onLegendItemListeners = { + onLegendItemOver: action('onLegendItemOver'), + onLegendItemOut: action('onLegendItemOut'), +}; + storiesOf('Interactions', module) .add('bar clicks and hovers', () => { return ( @@ -116,7 +121,7 @@ storiesOf('Interactions', module) .add('click/hovers on legend items [bar chart] (TO DO click)', () => { return ( - + Date: Fri, 8 Feb 2019 16:44:29 -0800 Subject: [PATCH 17/46] refactor: pass only the necessary values to legend mouseover listener --- src/components/legend.tsx | 5 ++--- src/specs/settings.tsx | 2 +- src/state/chart_state.ts | 18 +++++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 377cf66b9c..151ca80df9 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -106,12 +106,11 @@ class LegendComponent extends React.Component { } private onLegendItemMouseover = (legendItemIndex: number) => () => { - this.props.chartStore!.onLegendItemOver(); - this.props.chartStore!.updateHighlightedLegendItem(legendItemIndex); + this.props.chartStore!.onLegendItemOver(legendItemIndex); } private onLegendItemMouseout = () => { - this.props.chartStore!.updateHighlightedLegendItem(null); + this.props.chartStore!.onLegendItemOut(); } } function LegendElement({ color, label }: Partial) { diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index fd4af60508..19bc95098e 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -24,7 +24,7 @@ interface SettingSpecProps { onElementOut?: () => undefined; onBrushEnd?: BrushEndListener; onLegendItemOver?: LegendItemListener; - onLegendItemOut?: LegendItemListener; + onLegendItemOut?: () => undefined; } function updateChartStore(props: SettingSpecProps) { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index d8d699dc2d..3cb563f377 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -76,7 +76,7 @@ export interface SeriesDomainsAndData { export type ElementClickListener = (value: GeometryValue) => void; export type ElementOverListener = (value: GeometryValue) => void; export type BrushEndListener = (min: number, max: number) => void; -export type LegendItemListener = () => void; +export type LegendItemListener = (dataSeriesIdentifiers: DataSeriesColorsValues | null) => void; // const MAX_ANIMATABLE_GLYPHS = 500; export class ChartStore { @@ -136,7 +136,7 @@ export class ChartStore { onBrushEndListener?: BrushEndListener; onLegendItemOverListener?: LegendItemListener; - onLegendItemOutListener?: LegendItemListener; + onLegendItemOutListener?: () => undefined; geometries: { points: PointGeometry[]; @@ -196,13 +196,17 @@ export class ChartStore { return index == null ? null : this.legendItems[index]; }); - onLegendItemOver = action(() => { + onLegendItemOver = action((legendItemIndex: number) => { + this.highlightedLegendItemIndex.set(legendItemIndex); if (this.onLegendItemOverListener) { - this.onLegendItemOverListener(); + const currentLegendItem = this.highlightedLegendItem.get(); + const listenerData = currentLegendItem ? currentLegendItem.value : null; + this.onLegendItemOverListener(listenerData); } }); onLegendItemOut = action(() => { + this.highlightedLegendItemIndex.set(null); if (this.onLegendItemOutListener) { this.onLegendItemOutListener(); } @@ -223,7 +227,7 @@ export class ChartStore { setOnLegendItemOverListener(listener: LegendItemListener) { this.onLegendItemOverListener = listener; } - setOnLegendItemOutListener(listener: LegendItemListener) { + setOnLegendItemOutListener(listener: () => undefined) { this.onLegendItemOutListener = listener; } removeElementClickListener() { @@ -263,10 +267,6 @@ export class ChartStore { return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener); } - updateHighlightedLegendItem(legendItemIndex: number | null) { - this.highlightedLegendItemIndex.set(legendItemIndex); - } - updateParentDimensions(width: number, height: number, top: number, left: number) { let isChanged = false; if (width !== this.parentDimensions.width) { From af649f019f5283fdf47a81a9ad71f3461e24db92 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Mon, 4 Feb 2019 10:19:55 -0800 Subject: [PATCH 18/46] feat(legend): add scaffolding for legend title click interaction --- src/components/_legend.scss | 11 +++++-- src/components/legend.tsx | 66 ++++++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/components/_legend.scss b/src/components/_legend.scss index 43b60c3312..ed8044654d 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -48,7 +48,7 @@ $euiChartLegendMaxHeight: $euiSize * 4 + $euiSize; top: 0; bottom: 0; left: 0; - width: $euiChartLegendMaxWidth; + width: $euiChartLegendMaxWidth; order: 1; .euiChartLegend__listItem { min-width: 100%; @@ -58,7 +58,7 @@ $euiChartLegendMaxHeight: $euiSize * 4 + $euiSize; top: 0; bottom: 0; right: 0; - width: $euiChartLegendMaxWidth; + width: $euiChartLegendMaxWidth; .euiChartLegend__listItem { min-width: 100%; } @@ -90,7 +90,14 @@ $euiChartLegendMaxHeight: $euiSize * 4 + $euiSize; overflow-x: hidden; height: 100%; } +.euiChartLegendList__item { + cursor: pointer; +} .euiChartLegendListItem__title { width: $euiChartLegendMaxWidth - 4 * $euiSize; max-width: $euiChartLegendMaxWidth - 4 * $euiSize; + + &:hover { + text-decoration: underline; + } } \ No newline at end of file diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 151ca80df9..aae52b7d2b 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -93,9 +93,11 @@ class LegendComponent extends React.Component { onMouseOut: this.onLegendItemMouseout, }; + const { color, label } = item; + return ( - + {this.renderLegendElement({ color, label }, index)} ); })} @@ -105,6 +107,11 @@ class LegendComponent extends React.Component { ); } + private onLegendTitleClick = (legendItemIndex: number) => () => { + // tslint:disable-next-line:no-console + console.log(legendItemIndex); + } + private onLegendItemMouseover = (legendItemIndex: number) => () => { this.props.chartStore!.onLegendItemOver(legendItemIndex); } @@ -112,24 +119,45 @@ class LegendComponent extends React.Component { private onLegendItemMouseout = () => { this.props.chartStore!.onLegendItemOut(); } + + private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { + const onTitleClick = this.onLegendTitleClick(legendItemIndex); + + return ( + + + + + + {label}}> + + + {label} + + + + + + ); + } } -function LegendElement({ color, label }: Partial) { - return ( - - - - - - {label}}> - - - {label} - - - - - - ); -} +// function LegendElement({ color, label }: Partial) { +// return ( +// +// +// +// +// +// {label}}> +// +// +// {label} +// +// +// +// +// +// ); +// } export const Legend = inject('chartStore')(observer(LegendComponent)); From b929d29809ce2e64ca2c879a71e0215abce22d6b Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 5 Feb 2019 08:27:24 -0800 Subject: [PATCH 19/46] feat(legend): update state store on legend item click --- src/components/legend.tsx | 3 +-- src/state/chart_state.ts | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index aae52b7d2b..2db8b6516f 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -108,8 +108,7 @@ class LegendComponent extends React.Component { } private onLegendTitleClick = (legendItemIndex: number) => () => { - // tslint:disable-next-line:no-console - console.log(legendItemIndex); + this.props.chartStore!.updateSelectedLegendItem(legendItemIndex); } private onLegendItemMouseover = (legendItemIndex: number) => () => { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 3cb563f377..30003bc3bd 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -125,6 +125,7 @@ export class ChartStore { legendItems: LegendItem[] = []; highlightedLegendItemIndex: IObservableValue = observable.box(null); + selectedLegendItemIndex: number | null = null; tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -267,6 +268,13 @@ export class ChartStore { return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener); } + updateSelectedLegendItem(legendItemIndex: number | null) { + if (legendItemIndex !== this.selectedLegendItemIndex) { + this.selectedLegendItemIndex = legendItemIndex; + this.computeChart(); + } + } + updateParentDimensions(width: number, height: number, top: number, left: number) { let isChanged = false; if (width !== this.parentDimensions.width) { From 69c164a007464fbe937fa7b88b8bd7c8fc950272 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 7 Feb 2019 17:22:22 -0800 Subject: [PATCH 20/46] feat(legend/series): filter raw dataseries by current legend item --- src/lib/series/domains/x_domain.test.ts | 20 ++++++++++---------- src/lib/series/series.test.ts | 4 ++-- src/lib/series/series.ts | 18 +++++++++++++++++- src/state/chart_state.ts | 11 +++++++++-- src/state/utils.test.ts | 4 ++-- src/state/utils.ts | 3 ++- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/lib/series/domains/x_domain.test.ts b/src/lib/series/domains/x_domain.test.ts index d072ea7322..302ac37328 100644 --- a/src/lib/series/domains/x_domain.test.ts +++ b/src/lib/series/domains/x_domain.test.ts @@ -217,7 +217,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -255,7 +255,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -293,7 +293,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -335,7 +335,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -377,7 +377,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -419,7 +419,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -461,7 +461,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -503,7 +503,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -545,7 +545,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { @@ -589,7 +589,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries); + const { xValues } = getSplittedSeries(specDataSeries, null); const mergedDomain = mergeXDomain( [ { diff --git a/src/lib/series/series.test.ts b/src/lib/series/series.test.ts index ca0b420fe6..45e1eeadb7 100644 --- a/src/lib/series/series.test.ts +++ b/src/lib/series/series.test.ts @@ -193,7 +193,7 @@ describe('Series', () => { }; seriesSpecs.set(spec1.id, spec1); seriesSpecs.set(spec2.id, spec2); - const splittedDataSeries = getSplittedSeries(seriesSpecs); + const splittedDataSeries = getSplittedSeries(seriesSpecs, null); expect(splittedDataSeries.splittedSeries.get(getSpecId('spec1'))).toMatchSnapshot(); expect(splittedDataSeries.splittedSeries.get(getSpecId('spec2'))).toMatchSnapshot(); }); @@ -224,7 +224,7 @@ describe('Series', () => { }; seriesSpecs.set(spec1.id, spec1); seriesSpecs.set(spec2.id, spec2); - const splittedDataSeries = getSplittedSeries(seriesSpecs); + const splittedDataSeries = getSplittedSeries(seriesSpecs, null); const stackedDataSeries = getFormattedDataseries( [spec1, spec2], splittedDataSeries.splittedSeries, diff --git a/src/lib/series/series.ts b/src/lib/series/series.ts index d06e856b17..695e77b04e 100644 --- a/src/lib/series/series.ts +++ b/src/lib/series/series.ts @@ -2,6 +2,7 @@ import { ColorConfig } from '../themes/theme'; import { Accessor } from '../utils/accessor'; import { GroupId, SpecId } from '../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from './domains/y_domain'; +import { isEqualSeriesKey } from './series_utils'; import { BasicSeriesSpec, Datum, SeriesAccessors } from './specs'; export interface RawDataSeriesDatum { @@ -341,6 +342,7 @@ export function formatStackedDataSeriesValues( export function getSplittedSeries( seriesSpecs: Map, + selectedDataSeries: DataSeriesColorsValues | null, ): { splittedSeries: Map; seriesColors: Map; @@ -349,15 +351,29 @@ export function getSplittedSeries( const splittedSeries = new Map(); const seriesColors = new Map(); const xValues: Set = new Set(); + for (const [specId, spec] of seriesSpecs) { const dataSeries = splitSeries(spec.data, spec, specId); - splittedSeries.set(specId, dataSeries.rawDataSeries); + + let currentRawDataSeries = dataSeries.rawDataSeries; + if (selectedDataSeries) { + const selectedSpecId = selectedDataSeries.specId; + const selectedSeriesKey = selectedDataSeries.colorValues; + + currentRawDataSeries = dataSeries.rawDataSeries.filter((series): boolean => { + return (selectedSpecId === specId) && isEqualSeriesKey(selectedSeriesKey, series.key); + }); + } + + splittedSeries.set(specId, currentRawDataSeries); + dataSeries.colorsValues.forEach((colorValues, key) => { seriesColors.set(key, { specId, colorValues, }); }); + for (const xValue of dataSeries.xValues) { xValues.add(xValue); } diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 30003bc3bd..f1ef31e66c 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -271,8 +271,10 @@ export class ChartStore { updateSelectedLegendItem(legendItemIndex: number | null) { if (legendItemIndex !== this.selectedLegendItemIndex) { this.selectedLegendItemIndex = legendItemIndex; - this.computeChart(); + } else { + this.selectedLegendItemIndex = null; } + this.computeChart(); } updateParentDimensions(width: number, height: number, top: number, left: number) { @@ -325,7 +327,12 @@ export class ChartStore { return; } - const seriesDomains = computeSeriesDomains(this.seriesSpecs); + const selectedDataSeries: DataSeriesColorsValues | null = + this.selectedLegendItemIndex != null + ? this.legendItems[this.selectedLegendItemIndex].value + : null; + + const seriesDomains = computeSeriesDomains(this.seriesSpecs, selectedDataSeries); this.seriesDomainsAndData = seriesDomains; // tslint:disable-next-line:no-console // console.log({colors: seriesDomains.seriesColors}); diff --git a/src/state/utils.test.ts b/src/state/utils.test.ts index 81b77b518c..74ce284281 100644 --- a/src/state/utils.test.ts +++ b/src/state/utils.test.ts @@ -31,7 +31,7 @@ describe('Chart State utils', () => { const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); - const domains = computeSeriesDomains(specs); + const domains = computeSeriesDomains(specs, null); expect(domains.xDomain).toEqual({ domain: [0, 3], isBandScale: false, @@ -87,7 +87,7 @@ describe('Chart State utils', () => { const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); - const domains = computeSeriesDomains(specs); + const domains = computeSeriesDomains(specs, null); expect(domains.xDomain).toEqual({ domain: [0, 3], isBandScale: false, diff --git a/src/state/utils.ts b/src/state/utils.ts index 776cfc22ef..8230cb5703 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -47,6 +47,7 @@ export interface BrushExtent { export function computeSeriesDomains( seriesSpecs: Map, + selectedDataSeries: DataSeriesColorsValues | null, ): { xDomain: XDomain; yDomain: YDomain[]; @@ -57,7 +58,7 @@ export function computeSeriesDomains( }; seriesColors: Map; } { - const { splittedSeries, xValues, seriesColors } = getSplittedSeries(seriesSpecs); + const { splittedSeries, xValues, seriesColors } = getSplittedSeries(seriesSpecs, selectedDataSeries); // tslint:disable-next-line:no-console // console.log({ splittedSeries, xValues, seriesColors }); const splittedDataSeries = [...splittedSeries.values()]; From d28d3d16928b43b9135c4f707d33ca436ebc320e Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 7 Feb 2019 18:00:47 -0800 Subject: [PATCH 21/46] feat(legend): update highlighted and selected dependent on state --- src/state/chart_state.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index f1ef31e66c..a89e1d243d 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -268,9 +268,19 @@ export class ChartStore { return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener); } + /* Keeping this here so I remember to use this logic :P + updateHighlightedLegendItem(legendItemIndex: number | null) { + if (this.selectedLegendItemIndex == null && legendItemIndex !== this.highlightedLegendItemIndex) { + this.highlightedLegendItemIndex = legendItemIndex; + this.computeChart(); + } + } + */ + updateSelectedLegendItem(legendItemIndex: number | null) { if (legendItemIndex !== this.selectedLegendItemIndex) { this.selectedLegendItemIndex = legendItemIndex; + this.highlightedLegendItemIndex.set(null); } else { this.selectedLegendItemIndex = null; } From 40e6ed5a2801e43a52036770fad7f31137634505 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 7 Feb 2019 18:24:52 -0800 Subject: [PATCH 22/46] feat(legend): add style for selected item title --- src/components/_legend.scss | 2 +- src/components/legend.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/_legend.scss b/src/components/_legend.scss index ed8044654d..d776834f68 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -97,7 +97,7 @@ $euiChartLegendMaxHeight: $euiSize * 4 + $euiSize; width: $euiChartLegendMaxWidth - 4 * $euiSize; max-width: $euiChartLegendMaxWidth - 4 * $euiSize; - &:hover { + &:hover, &.euiChartLegendListItem__title--selected { text-decoration: underline; } } \ No newline at end of file diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 2db8b6516f..65aafa8fce 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -122,6 +122,10 @@ class LegendComponent extends React.Component { private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { const onTitleClick = this.onLegendTitleClick(legendItemIndex); + const titleClassNames = classNames({ + ['euiChartLegendListItem__title--selected']: legendItemIndex === this.props.chartStore!.selectedLegendItemIndex, + }, 'euiChartLegendListItem__title'); + return ( @@ -129,7 +133,7 @@ class LegendComponent extends React.Component { {label}}> - + {label} From c5aebdc02adb07514bece59ee2d950345b971cfc Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 07:52:40 -0800 Subject: [PATCH 23/46] fix(legend): remove direct filtering of legend click with data --- src/lib/series/domains/x_domain.test.ts | 20 ++++++++++---------- src/lib/series/series.test.ts | 4 ++-- src/lib/series/series.ts | 14 +------------- src/state/chart_state.ts | 9 ++------- src/state/utils.test.ts | 4 ++-- src/state/utils.ts | 3 +-- 6 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/lib/series/domains/x_domain.test.ts b/src/lib/series/domains/x_domain.test.ts index 302ac37328..d072ea7322 100644 --- a/src/lib/series/domains/x_domain.test.ts +++ b/src/lib/series/domains/x_domain.test.ts @@ -217,7 +217,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -255,7 +255,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -293,7 +293,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -335,7 +335,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -377,7 +377,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -419,7 +419,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -461,7 +461,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -503,7 +503,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -545,7 +545,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -589,7 +589,7 @@ describe('X Domain', () => { const specDataSeries = new Map(); specDataSeries.set(ds1.id, ds1); specDataSeries.set(ds2.id, ds2); - const { xValues } = getSplittedSeries(specDataSeries, null); + const { xValues } = getSplittedSeries(specDataSeries); const mergedDomain = mergeXDomain( [ { diff --git a/src/lib/series/series.test.ts b/src/lib/series/series.test.ts index 45e1eeadb7..ca0b420fe6 100644 --- a/src/lib/series/series.test.ts +++ b/src/lib/series/series.test.ts @@ -193,7 +193,7 @@ describe('Series', () => { }; seriesSpecs.set(spec1.id, spec1); seriesSpecs.set(spec2.id, spec2); - const splittedDataSeries = getSplittedSeries(seriesSpecs, null); + const splittedDataSeries = getSplittedSeries(seriesSpecs); expect(splittedDataSeries.splittedSeries.get(getSpecId('spec1'))).toMatchSnapshot(); expect(splittedDataSeries.splittedSeries.get(getSpecId('spec2'))).toMatchSnapshot(); }); @@ -224,7 +224,7 @@ describe('Series', () => { }; seriesSpecs.set(spec1.id, spec1); seriesSpecs.set(spec2.id, spec2); - const splittedDataSeries = getSplittedSeries(seriesSpecs, null); + const splittedDataSeries = getSplittedSeries(seriesSpecs); const stackedDataSeries = getFormattedDataseries( [spec1, spec2], splittedDataSeries.splittedSeries, diff --git a/src/lib/series/series.ts b/src/lib/series/series.ts index 695e77b04e..31f7c3ef6d 100644 --- a/src/lib/series/series.ts +++ b/src/lib/series/series.ts @@ -2,7 +2,6 @@ import { ColorConfig } from '../themes/theme'; import { Accessor } from '../utils/accessor'; import { GroupId, SpecId } from '../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from './domains/y_domain'; -import { isEqualSeriesKey } from './series_utils'; import { BasicSeriesSpec, Datum, SeriesAccessors } from './specs'; export interface RawDataSeriesDatum { @@ -342,7 +341,6 @@ export function formatStackedDataSeriesValues( export function getSplittedSeries( seriesSpecs: Map, - selectedDataSeries: DataSeriesColorsValues | null, ): { splittedSeries: Map; seriesColors: Map; @@ -355,17 +353,7 @@ export function getSplittedSeries( for (const [specId, spec] of seriesSpecs) { const dataSeries = splitSeries(spec.data, spec, specId); - let currentRawDataSeries = dataSeries.rawDataSeries; - if (selectedDataSeries) { - const selectedSpecId = selectedDataSeries.specId; - const selectedSeriesKey = selectedDataSeries.colorValues; - - currentRawDataSeries = dataSeries.rawDataSeries.filter((series): boolean => { - return (selectedSpecId === specId) && isEqualSeriesKey(selectedSeriesKey, series.key); - }); - } - - splittedSeries.set(specId, currentRawDataSeries); + splittedSeries.set(specId, dataSeries.rawDataSeries); dataSeries.colorsValues.forEach((colorValues, key) => { seriesColors.set(key, { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index a89e1d243d..76d0b01606 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -135,9 +135,9 @@ export class ChartStore { onElementOverListener?: ElementOverListener; onElementOutListener?: () => undefined; onBrushEndListener?: BrushEndListener; - onLegendItemOverListener?: LegendItemListener; onLegendItemOutListener?: () => undefined; + onLegendItemClickListener?: LegendItemListener; geometries: { points: PointGeometry[]; @@ -337,12 +337,7 @@ export class ChartStore { return; } - const selectedDataSeries: DataSeriesColorsValues | null = - this.selectedLegendItemIndex != null - ? this.legendItems[this.selectedLegendItemIndex].value - : null; - - const seriesDomains = computeSeriesDomains(this.seriesSpecs, selectedDataSeries); + const seriesDomains = computeSeriesDomains(this.seriesSpecs); this.seriesDomainsAndData = seriesDomains; // tslint:disable-next-line:no-console // console.log({colors: seriesDomains.seriesColors}); diff --git a/src/state/utils.test.ts b/src/state/utils.test.ts index 74ce284281..81b77b518c 100644 --- a/src/state/utils.test.ts +++ b/src/state/utils.test.ts @@ -31,7 +31,7 @@ describe('Chart State utils', () => { const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); - const domains = computeSeriesDomains(specs, null); + const domains = computeSeriesDomains(specs); expect(domains.xDomain).toEqual({ domain: [0, 3], isBandScale: false, @@ -87,7 +87,7 @@ describe('Chart State utils', () => { const specs = new Map(); specs.set(spec1.id, spec1); specs.set(spec2.id, spec2); - const domains = computeSeriesDomains(specs, null); + const domains = computeSeriesDomains(specs); expect(domains.xDomain).toEqual({ domain: [0, 3], isBandScale: false, diff --git a/src/state/utils.ts b/src/state/utils.ts index 8230cb5703..776cfc22ef 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -47,7 +47,6 @@ export interface BrushExtent { export function computeSeriesDomains( seriesSpecs: Map, - selectedDataSeries: DataSeriesColorsValues | null, ): { xDomain: XDomain; yDomain: YDomain[]; @@ -58,7 +57,7 @@ export function computeSeriesDomains( }; seriesColors: Map; } { - const { splittedSeries, xValues, seriesColors } = getSplittedSeries(seriesSpecs, selectedDataSeries); + const { splittedSeries, xValues, seriesColors } = getSplittedSeries(seriesSpecs); // tslint:disable-next-line:no-console // console.log({ splittedSeries, xValues, seriesColors }); const splittedDataSeries = [...splittedSeries.values()]; From e2070ea26fc2e9552fbe1a5b7bd9f0f7a8336e61 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 08:10:41 -0800 Subject: [PATCH 24/46] refactor(legend): set up legend item click listeners --- src/components/legend.tsx | 1 + src/specs/settings.tsx | 5 +++++ src/state/chart_state.ts | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 65aafa8fce..63d78c48ce 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -108,6 +108,7 @@ class LegendComponent extends React.Component { } private onLegendTitleClick = (legendItemIndex: number) => () => { + console.log(this.props.chartStore); this.props.chartStore!.updateSelectedLegendItem(legendItemIndex); } diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 19bc95098e..566b59586f 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -25,6 +25,7 @@ interface SettingSpecProps { onBrushEnd?: BrushEndListener; onLegendItemOver?: LegendItemListener; onLegendItemOut?: () => undefined; + onLegendItemClick?: LegendItemListener; } function updateChartStore(props: SettingSpecProps) { @@ -42,6 +43,7 @@ function updateChartStore(props: SettingSpecProps) { onBrushEnd, onLegendItemOver, onLegendItemOut, + onLegendItemClick, debug, } = props; if (!chartStore) { @@ -74,6 +76,9 @@ function updateChartStore(props: SettingSpecProps) { if (onLegendItemOut) { chartStore.setOnLegendItemOutListener(onLegendItemOut); } + if (onLegendItemClick) { + chartStore.setOnLegendItemClickListener(onLegendItemClick); + } } export class SettingsComponent extends PureComponent { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 76d0b01606..cebc490b84 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -231,6 +231,9 @@ export class ChartStore { setOnLegendItemOutListener(listener: () => undefined) { this.onLegendItemOutListener = listener; } + setOnLegendItemClickListener(listener: LegendItemListener) { + this.onLegendItemClickListener = listener; + } removeElementClickListener() { this.onElementClickListener = undefined; } From 08c051fbecb724f40a55a5c9dc1789b280390c8d Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 17:28:03 -0800 Subject: [PATCH 25/46] feat(legend/click): add listener logic and interactions story --- src/components/legend.tsx | 24 +++-------------------- src/state/chart_state.ts | 40 +++++++++++++++++++-------------------- stories/interactions.tsx | 9 +++++---- 3 files changed, 28 insertions(+), 45 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 63d78c48ce..edc8267b4c 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -108,8 +108,7 @@ class LegendComponent extends React.Component { } private onLegendTitleClick = (legendItemIndex: number) => () => { - console.log(this.props.chartStore); - this.props.chartStore!.updateSelectedLegendItem(legendItemIndex); + this.props.chartStore!.onLegendItemClick(legendItemIndex); } private onLegendItemMouseover = (legendItemIndex: number) => () => { @@ -123,8 +122,9 @@ class LegendComponent extends React.Component { private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { const onTitleClick = this.onLegendTitleClick(legendItemIndex); + const isSelected = legendItemIndex === this.props.chartStore!.selectedLegendItemIndex.get(); const titleClassNames = classNames({ - ['euiChartLegendListItem__title--selected']: legendItemIndex === this.props.chartStore!.selectedLegendItemIndex, + ['euiChartLegendListItem__title--selected']: isSelected, }, 'euiChartLegendListItem__title'); return ( @@ -145,23 +145,5 @@ class LegendComponent extends React.Component { ); } } -// function LegendElement({ color, label }: Partial) { -// return ( -// -// -// -// -// -// {label}}> -// -// -// {label} -// -// -// -// -// -// ); -// } export const Legend = inject('chartStore')(observer(LegendComponent)); diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index cebc490b84..2f590b6c16 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -125,7 +125,7 @@ export class ChartStore { legendItems: LegendItem[] = []; highlightedLegendItemIndex: IObservableValue = observable.box(null); - selectedLegendItemIndex: number | null = null; + selectedLegendItemIndex: IObservableValue = observable.box(null); tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -197,6 +197,11 @@ export class ChartStore { return index == null ? null : this.legendItems[index]; }); + selectedLegendItem = computed(() => { + const index = this.selectedLegendItemIndex.get(); + return index == null ? null : this.legendItems[index]; + }); + onLegendItemOver = action((legendItemIndex: number) => { this.highlightedLegendItemIndex.set(legendItemIndex); if (this.onLegendItemOverListener) { @@ -213,6 +218,20 @@ export class ChartStore { } }); + onLegendItemClick = action((legendItemIndex: number) => { + if (legendItemIndex !== this.selectedLegendItemIndex.get()) { + this.selectedLegendItemIndex.set(legendItemIndex); + } else { + this.selectedLegendItemIndex.set(null); + } + + if (this.onLegendItemClickListener) { + const currentLegendItem = this.selectedLegendItem.get(); + const listenerData = currentLegendItem ? currentLegendItem.value : null; + this.onLegendItemClickListener(listenerData); + } + }); + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } @@ -271,25 +290,6 @@ export class ChartStore { return this.xScale.type !== ScaleType.Ordinal && Boolean(this.onBrushEndListener); } - /* Keeping this here so I remember to use this logic :P - updateHighlightedLegendItem(legendItemIndex: number | null) { - if (this.selectedLegendItemIndex == null && legendItemIndex !== this.highlightedLegendItemIndex) { - this.highlightedLegendItemIndex = legendItemIndex; - this.computeChart(); - } - } - */ - - updateSelectedLegendItem(legendItemIndex: number | null) { - if (legendItemIndex !== this.selectedLegendItemIndex) { - this.selectedLegendItemIndex = legendItemIndex; - this.highlightedLegendItemIndex.set(null); - } else { - this.selectedLegendItemIndex = null; - } - this.computeChart(); - } - updateParentDimensions(width: number, height: number, top: number, left: number) { let isChanged = false; if (width !== this.parentDimensions.width) { diff --git a/stories/interactions.tsx b/stories/interactions.tsx index 5d9752bf6e..1125b96ed2 100644 --- a/stories/interactions.tsx +++ b/stories/interactions.tsx @@ -28,6 +28,7 @@ const onElementListeners = { const onLegendItemListeners = { onLegendItemOver: action('onLegendItemOver'), onLegendItemOut: action('onLegendItemOut'), + onLegendItemClick: action('onLegendItemClick'), }; storiesOf('Interactions', module) @@ -118,7 +119,7 @@ storiesOf('Interactions', module) ); }) - .add('click/hovers on legend items [bar chart] (TO DO click)', () => { + .add('click/hovers on legend items [bar chart]', () => { return ( @@ -148,7 +149,7 @@ storiesOf('Interactions', module) ); }) - .add('click/hovers on legend items [area chart] (TO DO click)', () => { + .add('click/hovers on legend items [area chart]', () => { return ( @@ -188,7 +189,7 @@ storiesOf('Interactions', module) ); }) - .add('click/hovers on legend items [line chart] (TO DO click)', () => { + .add('click/hovers on legend items [line chart]', () => { return ( @@ -268,7 +269,7 @@ storiesOf('Interactions', module) ); }) - .add('click/hovers on legend items [mixed chart] (TO DO click)', () => { + .add('click/hovers on legend items [mixed chart]', () => { return ( From 2a696f47441e5e8729f3148768b9f2cd45b4bbba Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 8 Feb 2019 19:07:14 -0800 Subject: [PATCH 26/46] feat(legend): add plus/minus buttons to legend item panel --- src/components/legend.tsx | 50 ++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index edc8267b4c..f96375df2b 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -1,10 +1,12 @@ import { EuiButtonIcon, + EuiContextMenuPanel, EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiPopover, EuiText, - EuiToolTip, + // EuiToolTip, } from '@elastic/eui'; import classNames from 'classnames'; import { inject, observer } from 'mobx-react'; @@ -119,6 +121,11 @@ class LegendComponent extends React.Component { this.props.chartStore!.onLegendItemOut(); } + private onLegendItemPanelClose = () => { + // tslint:disable-next-line:no-console + console.log('close'); + } + private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { const onTitleClick = this.onLegendTitleClick(legendItemIndex); @@ -133,13 +140,44 @@ class LegendComponent extends React.Component {
- {label}}> - - + {/* {label}}> */} + + {/* {label} - - + {this.renderLegendItemDropdown(isSelected)} */} + + {label} + ) + } + isOpen={isSelected} + closePopover={this.onLegendItemPanelClose} + panelPaddingSize="s" + anchorPosition="downCenter" + > + + + + window.alert('Button clicked')} + iconType="plusInCircle" + aria-label="Show this group only" + /> + + + window.alert('Button clicked')} + iconType="minusInCircle" + aria-label="Remove this group" + /> + + + + + + {/* */} ); From bc0eeddabdfca9544aa4bfb6989fd0aaa457f772 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 14 Feb 2019 17:00:03 -0800 Subject: [PATCH 27/46] feat(legend): add placeholders for legend panel --- src/components/legend.tsx | 95 ++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index dd1563530f..f4eaef5d06 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -1,5 +1,8 @@ import { EuiButtonIcon, + // TODO: remove ts-ignore below once typings file is included in eui for color picker + // @ts-ignore + EuiColorPicker, EuiContextMenuPanel, EuiFlexGroup, EuiFlexItem, @@ -125,6 +128,29 @@ class LegendComponent extends React.Component { console.log('close'); } + private onColorPickerClose = () => { + // tslint:disable-next-line:no-console + console.log('color picker close'); + } + + private renderPlusButton = () => { + return ( + window.alert('Button clicked')} + iconType="plusInCircle" + aria-label="Show this group only" + />); + } + + private renderMinusButton = () => { + return ( + window.alert('Button clicked')} + iconType="plusInCircle" + aria-label="Show this group only" + />); + } + private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { const onTitleClick = this.onLegendTitleClick(legendItemIndex); @@ -134,49 +160,38 @@ class LegendComponent extends React.Component { }, 'euiChartLegendListItem__title'); return ( - + - - {/* {label}}> */} - - {/* - {label} - - {this.renderLegendItemDropdown(isSelected)} */} - - {label} - ) - } - isOpen={isSelected} - closePopover={this.onLegendItemPanelClose} - panelPaddingSize="s" - anchorPosition="downCenter" - > - - - - window.alert('Button clicked')} - iconType="plusInCircle" - aria-label="Show this group only" - /> - - - window.alert('Button clicked')} - iconType="minusInCircle" - aria-label="Remove this group" - /> - - - - - - {/* */} + + + {label} + ) + } + isOpen={isSelected} + closePopover={this.onLegendItemPanelClose} + panelPaddingSize="s" + anchorPosition="downCenter" + > + + + + {this.renderPlusButton()} + + + {this.renderMinusButton()} + + + + + + + + + ); From 71556477d2e639aa9e0e403364c1b19c1977921d Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 14 Feb 2019 17:17:23 -0800 Subject: [PATCH 28/46] feat(legend): add legend item +/- listeners --- src/components/legend.tsx | 6 +++--- src/specs/settings.tsx | 10 ++++++++++ src/state/chart_state.ts | 30 ++++++++++++++++++++++++++++++ stories/interactions.tsx | 2 ++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index f4eaef5d06..2555fc846c 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -136,7 +136,7 @@ class LegendComponent extends React.Component { private renderPlusButton = () => { return ( window.alert('Button clicked')} + onClick={this.props.chartStore!.onLegendItemPlusClick} iconType="plusInCircle" aria-label="Show this group only" />); @@ -145,8 +145,8 @@ class LegendComponent extends React.Component { private renderMinusButton = () => { return ( window.alert('Button clicked')} - iconType="plusInCircle" + onClick={this.props.chartStore!.onLegendItemMinusClick} + iconType="minusInCircle" aria-label="Show this group only" />); } diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 566b59586f..e13bc3dc4e 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -26,6 +26,8 @@ interface SettingSpecProps { onLegendItemOver?: LegendItemListener; onLegendItemOut?: () => undefined; onLegendItemClick?: LegendItemListener; + onLegendItemPlusClick?: LegendItemListener; + onLegendItemMinusClick?: LegendItemListener; } function updateChartStore(props: SettingSpecProps) { @@ -44,6 +46,8 @@ function updateChartStore(props: SettingSpecProps) { onLegendItemOver, onLegendItemOut, onLegendItemClick, + onLegendItemMinusClick, + onLegendItemPlusClick, debug, } = props; if (!chartStore) { @@ -79,6 +83,12 @@ function updateChartStore(props: SettingSpecProps) { if (onLegendItemClick) { chartStore.setOnLegendItemClickListener(onLegendItemClick); } + if (onLegendItemPlusClick) { + chartStore.setOnLegendItemPlusClickListener(onLegendItemPlusClick); + } + if (onLegendItemMinusClick) { + chartStore.setOnLegendItemMinusClickListener(onLegendItemMinusClick); + } } export class SettingsComponent extends PureComponent { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 2f590b6c16..1afc49a178 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -138,6 +138,8 @@ export class ChartStore { onLegendItemOverListener?: LegendItemListener; onLegendItemOutListener?: () => undefined; onLegendItemClickListener?: LegendItemListener; + onLegendItemPlusClickListener?: LegendItemListener; + onLegendItemMinusClickListener?: LegendItemListener; geometries: { points: PointGeometry[]; @@ -232,6 +234,22 @@ export class ChartStore { } }); + onLegendItemPlusClick = action(() => { + if (this.onLegendItemPlusClickListener) { + const currentLegendItem = this.selectedLegendItem.get(); + const listenerData = currentLegendItem ? currentLegendItem.value : null; + this.onLegendItemPlusClickListener(listenerData); + } + }); + + onLegendItemMinusClick = action(() => { + if (this.onLegendItemMinusClickListener) { + const currentLegendItem = this.selectedLegendItem.get(); + const listenerData = currentLegendItem ? currentLegendItem.value : null; + this.onLegendItemMinusClickListener(listenerData); + } + }); + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } @@ -253,6 +271,12 @@ export class ChartStore { setOnLegendItemClickListener(listener: LegendItemListener) { this.onLegendItemClickListener = listener; } + setOnLegendItemPlusClickListener(listener: LegendItemListener) { + this.onLegendItemPlusClickListener = listener; + } + setOnLegendItemMinusClickListener(listener: LegendItemListener) { + this.onLegendItemMinusClickListener = listener; + } removeElementClickListener() { this.onElementClickListener = undefined; } @@ -268,6 +292,12 @@ export class ChartStore { removeOnLegendItemOutListener() { this.onLegendItemOutListener = undefined; } + removeOnLegendItemPlusClickListener() { + this.onLegendItemPlusClickListener = undefined; + } + removeOnLegendItemMinusClickListener() { + this.onLegendItemMinusClickListener = undefined; + } onBrushEnd(start: Point, end: Point) { if (!this.onBrushEndListener) { return; diff --git a/stories/interactions.tsx b/stories/interactions.tsx index 1125b96ed2..a14cd47038 100644 --- a/stories/interactions.tsx +++ b/stories/interactions.tsx @@ -29,6 +29,8 @@ const onLegendItemListeners = { onLegendItemOver: action('onLegendItemOver'), onLegendItemOut: action('onLegendItemOut'), onLegendItemClick: action('onLegendItemClick'), + onLegendItemPlusClick: action('onLegendItemPlusClick'), + onLegendItemMinusClick: action('onLegendItemMinusClick'), }; storiesOf('Interactions', module) From ae2bb812922cbaa469d53da0daba34a9da400489 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 19 Feb 2019 08:55:59 -0800 Subject: [PATCH 29/46] fix(legend): fix legend item hover style --- src/components/_legend.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/_legend.scss b/src/components/_legend.scss index 644d8bfea6..985ed70dc9 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -89,6 +89,14 @@ $elasticChartsLegendMaxHeight: $euiSize * 4; @include euiScrollBar; } +.elasticChartsLegendList__item { + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + .elasticChartsLegendListItem__title { width: $elasticChartsLegendMaxWidth - 4 * $euiSize; max-width: $elasticChartsLegendMaxWidth - 4 * $euiSize; From 0ee4860ddceaa001457bb4186d8f213c84fe5b2f Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 19 Feb 2019 12:07:07 -0800 Subject: [PATCH 30/46] fix(legend): fix hover & selected style for legend item title --- src/components/_legend.scss | 8 ++++++-- src/components/legend.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/_legend.scss b/src/components/_legend.scss index 985ed70dc9..f91ba35c19 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -93,7 +93,9 @@ $elasticChartsLegendMaxHeight: $euiSize * 4; cursor: pointer; &:hover { - text-decoration: underline; + .elasticChartsLegendListItem__title { + text-decoration: underline; + } } } @@ -102,7 +104,9 @@ $elasticChartsLegendMaxHeight: $euiSize * 4; max-width: $elasticChartsLegendMaxWidth - 4 * $euiSize; &.elasticChartsLegendListItem__title--selected { - text-decoration: underline; + .elasticChartsLegendListItem__title { + text-decoration: underline; + } } } diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 67b1b1f155..afd6335534 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -155,7 +155,7 @@ class LegendComponent extends React.Component { + button={( {label} ) } From 44d1da1d9600959accd3d75145ef7b5b444c3321 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 19 Feb 2019 14:24:27 -0800 Subject: [PATCH 31/46] feat(series): use selected data series to show/hide series --- src/lib/series/series.ts | 14 +++++++++++++- src/state/chart_state.ts | 7 +++++++ src/state/utils.ts | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/lib/series/series.ts b/src/lib/series/series.ts index 31f7c3ef6d..704bdc2918 100644 --- a/src/lib/series/series.ts +++ b/src/lib/series/series.ts @@ -2,6 +2,7 @@ import { ColorConfig } from '../themes/theme'; import { Accessor } from '../utils/accessor'; import { GroupId, SpecId } from '../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from './domains/y_domain'; +import { isEqualSeriesKey } from './series_utils'; import { BasicSeriesSpec, Datum, SeriesAccessors } from './specs'; export interface RawDataSeriesDatum { @@ -341,6 +342,7 @@ export function formatStackedDataSeriesValues( export function getSplittedSeries( seriesSpecs: Map, + selectedDataSeries?: DataSeriesColorsValues | null, ): { splittedSeries: Map; seriesColors: Map; @@ -353,7 +355,17 @@ export function getSplittedSeries( for (const [specId, spec] of seriesSpecs) { const dataSeries = splitSeries(spec.data, spec, specId); - splittedSeries.set(specId, dataSeries.rawDataSeries); + let currentRawDataSeries = dataSeries.rawDataSeries; + if (selectedDataSeries) { + const selectedSpecId = selectedDataSeries.specId; + const selectedSeriesKey = selectedDataSeries.colorValues; + + currentRawDataSeries = dataSeries.rawDataSeries.filter((series): boolean => { + return (selectedSpecId === specId) && isEqualSeriesKey(selectedSeriesKey, series.key); + }); + } + + splittedSeries.set(specId, currentRawDataSeries); dataSeries.colorsValues.forEach((colorValues, key) => { seriesColors.set(key, { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index e5d84461b3..874241abc0 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -371,6 +371,13 @@ export class ChartStore { return; } + // for passing in selectedDataSeries info to show/hide a series + // commenting out for now as we consider how to show/hide a series + // const selectedDataSeries: DataSeriesColorsValues | null = + // this.selectedLegendItem.get() + // ? this.selectedLegendItem.get()!.value + // : null; + const seriesDomains = computeSeriesDomains(this.seriesSpecs); this.seriesDomainsAndData = seriesDomains; // tslint:disable-next-line:no-console diff --git a/src/state/utils.ts b/src/state/utils.ts index 776cfc22ef..3009ce3a5d 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -47,6 +47,7 @@ export interface BrushExtent { export function computeSeriesDomains( seriesSpecs: Map, + selectedDataSeries?: DataSeriesColorsValues | null, ): { xDomain: XDomain; yDomain: YDomain[]; @@ -57,7 +58,7 @@ export function computeSeriesDomains( }; seriesColors: Map; } { - const { splittedSeries, xValues, seriesColors } = getSplittedSeries(seriesSpecs); + const { splittedSeries, xValues, seriesColors } = getSplittedSeries(seriesSpecs, selectedDataSeries); // tslint:disable-next-line:no-console // console.log({ splittedSeries, xValues, seriesColors }); const splittedDataSeries = [...splittedSeries.values()]; From 99f37937b0e42239f5e6e37aee1ce16a58c275c0 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 19 Feb 2019 17:17:23 -0800 Subject: [PATCH 32/46] feat(legend): add color picker state for legend item --- src/components/legend.tsx | 82 +--------------- src/components/legend_element.tsx | 155 ++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 79 deletions(-) create mode 100644 src/components/legend_element.tsx diff --git a/src/components/legend.tsx b/src/components/legend.tsx index afd6335534..4d1a194116 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -1,14 +1,9 @@ import { - EuiButtonIcon, // TODO: remove ts-ignore below once typings file is included in eui for color picker // @ts-ignore EuiColorPicker, - EuiContextMenuPanel, EuiFlexGroup, EuiFlexItem, - EuiIcon, - EuiPopover, - EuiText, } from '@elastic/eui'; import classNames from 'classnames'; import { inject, observer } from 'mobx-react'; @@ -16,6 +11,7 @@ import React from 'react'; import { isVertical } from '../lib/axes/axis_utils'; import { LegendItem } from '../lib/series/legend'; import { ChartStore } from '../state/chart_state'; +import { LegendElement } from './legend_element'; interface ReactiveChartProps { chartStore?: ChartStore; // FIX until we find a better way on ts mobx @@ -99,10 +95,6 @@ class LegendComponent extends React.Component { ); } - private onLegendTitleClick = (legendItemIndex: number) => () => { - this.props.chartStore!.onLegendItemClick(legendItemIndex); - } - private onLegendItemMouseover = (legendItemIndex: number) => () => { this.props.chartStore!.onLegendItemOver(legendItemIndex); } @@ -111,78 +103,10 @@ class LegendComponent extends React.Component { this.props.chartStore!.onLegendItemOut(); } - private onLegendItemPanelClose = () => { - // tslint:disable-next-line:no-console - console.log('close'); - } - - private onColorPickerClose = () => { - // tslint:disable-next-line:no-console - console.log('color picker close'); - } - - private renderPlusButton = () => { - return ( - ); - } - - private renderMinusButton = () => { - return ( - ); - } - private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { - const onTitleClick = this.onLegendTitleClick(legendItemIndex); - - const isSelected = legendItemIndex === this.props.chartStore!.selectedLegendItemIndex.get(); - const titleClassNames = classNames({ - ['elasticChartsLegendListItem__title--selected']: isSelected, - }, 'elasticChartsLegendListItem__title'); + const props = { color, label, index: legendItemIndex }; - return ( - - - - - - - {label} - ) - } - isOpen={isSelected} - closePopover={this.onLegendItemPanelClose} - panelPaddingSize="s" - anchorPosition="downCenter" - > - - - - {this.renderPlusButton()} - - - {this.renderMinusButton()} - - - - - - - - - - - - ); + return ; } } diff --git a/src/components/legend_element.tsx b/src/components/legend_element.tsx new file mode 100644 index 0000000000..a3a2937b70 --- /dev/null +++ b/src/components/legend_element.tsx @@ -0,0 +1,155 @@ +import { + EuiButtonIcon, + // TODO: remove ts-ignore below once typings file is included in eui for color picker + // @ts-ignore + EuiColorPicker, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiText, +} from '@elastic/eui'; +import classNames from 'classnames'; +import { inject, observer } from 'mobx-react'; +import React from 'react'; + +import { ChartStore } from '../state/chart_state'; + +interface LegendElementProps { + chartStore?: ChartStore; // FIX until we find a better way on ts mobx + index: number; + color: string | undefined; + label: string | undefined; +} + +interface LegendElementState { + isColorPickerOpen: boolean; +} + +class LegendElementComponent extends React.PureComponent { + static displayName = 'LegendElement'; + + constructor(props: LegendElementProps) { + super(props); + this.state = { + isColorPickerOpen: false, + }; + } + + closeColorPicker = () => { + this.setState({ + isColorPickerOpen: false, + }); + } + + toggleColorPicker = () => { + this.setState({ + isColorPickerOpen: !this.state.isColorPickerOpen, + }); + } + + render() { + const legendItemIndex = this.props.index; + const { color, label } = this.props; + + const onTitleClick = this.onLegendTitleClick(legendItemIndex); + + const isSelected = legendItemIndex === this.props.chartStore!.selectedLegendItemIndex.get(); + const titleClassNames = classNames({ + ['elasticChartsLegendListItem__title--selected']: isSelected, + }, 'elasticChartsLegendListItem__title'); + + const colorDotProps = { + color, + onClick: this.toggleColorPicker, + }; + + const colorDot = ; + + return ( + + + + + + + + + + + {label} + ) + } + isOpen={isSelected} + closePopover={this.onLegendItemPanelClose} + panelPaddingSize="s" + anchorPosition="downCenter" + > + + + + {this.renderPlusButton()} + + + {this.renderMinusButton()} + + + + + + + ); + } + + private onLegendTitleClick = (legendItemIndex: number) => () => { + this.props.chartStore!.onLegendItemClick(legendItemIndex); + } + + // private onLegendItemMouseover = (legendItemIndex: number) => () => { + // this.props.chartStore!.onLegendItemOver(legendItemIndex); + // } + + // private onLegendItemMouseout = () => { + // this.props.chartStore!.onLegendItemOut(); + // } + + private onLegendItemPanelClose = () => { + // tslint:disable-next-line:no-console + console.log('close'); + } + + private onColorPickerChange = () => { + // tslint:disable-next-line:no-console + console.log('color picker close'); + } + + private renderPlusButton = () => { + return ( + ); + } + + private renderMinusButton = () => { + return ( + ); + } +} + +export const LegendElement = inject('chartStore')(observer(LegendElementComponent)); From 370f8b4c393f8906756db84396889a2de1a84201 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Mon, 25 Feb 2019 16:54:22 -0800 Subject: [PATCH 33/46] feat(legend/series): toggle visibility of series from legend --- src/components/_legend.scss | 8 ++++ src/components/legend.tsx | 8 ++-- src/components/legend_element.tsx | 29 ++++++++++-- src/lib/series/legend.test.ts | 39 +++++++++++++--- src/lib/series/legend.ts | 3 ++ src/lib/series/series.ts | 14 +++--- src/state/chart_state.ts | 18 +++++++- src/state/utils.test.ts | 75 ++++++++++++++++++++++++++++++- src/state/utils.ts | 32 ++++++++++++- 9 files changed, 203 insertions(+), 23 deletions(-) diff --git a/src/components/_legend.scss b/src/components/_legend.scss index f91ba35c19..d4a40915f4 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -110,6 +110,14 @@ $elasticChartsLegendMaxHeight: $euiSize * 4; } } +.elasticChartsLegendListItem__vibilityIndicator { + opacity: 0.5; + + &.elasticChartsLegendListItem__vibilityIndicator--visible { + opacity: 1; + } +} + .elasticChartsLegend__toggle { border-radius: $euiBorderRadius; border-bottom-right-radius: 0; diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 4d1a194116..4f3957aac2 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -81,11 +81,11 @@ class LegendComponent extends React.Component { onMouseLeave: this.onLegendItemMouseout, }; - const { color, label } = item; + const { color, label, isVisible } = item; return ( - {this.renderLegendElement({ color, label }, index)} + {this.renderLegendElement({ color, label, isVisible }, index)} ); })} @@ -103,8 +103,8 @@ class LegendComponent extends React.Component { this.props.chartStore!.onLegendItemOut(); } - private renderLegendElement = ({ color, label }: Partial, legendItemIndex: number) => { - const props = { color, label, index: legendItemIndex }; + private renderLegendElement = ({ color, label, isVisible }: Partial, legendItemIndex: number) => { + const props = { color, label, isVisible, index: legendItemIndex }; return ; } diff --git a/src/components/legend_element.tsx b/src/components/legend_element.tsx index a3a2937b70..9fd9b179d4 100644 --- a/src/components/legend_element.tsx +++ b/src/components/legend_element.tsx @@ -21,13 +21,14 @@ interface LegendElementProps { index: number; color: string | undefined; label: string | undefined; + isVisible?: boolean; } interface LegendElementState { isColorPickerOpen: boolean; } -class LegendElementComponent extends React.PureComponent { +class LegendElementComponent extends React.Component { static displayName = 'LegendElement'; constructor(props: LegendElementProps) { @@ -51,7 +52,7 @@ class LegendElementComponent extends React.PureComponent + + {this.renderVisibilityButton(legendItemIndex, isVisible)} + ); } @@ -147,9 +151,26 @@ class LegendElementComponent extends React.PureComponent); } + + private onVisibilityClick = (legendItemIndex: number) => () => { + this.props.chartStore!.toggleVisibility(legendItemIndex); + } + + private renderVisibilityButton = (legendItemIndex: number, isVisible: boolean = true) => { + const className = classNames('elasticChartsLegendListItem__vibilityIndicator', { + 'elasticChartsLegendListItem__vibilityIndicator--visible': isVisible, + }); + + return ; + } } export const LegendElement = inject('chartStore')(observer(LegendElementComponent)); diff --git a/src/lib/series/legend.test.ts b/src/lib/series/legend.test.ts index 496c8325b2..8d09e50cda 100644 --- a/src/lib/series/legend.test.ts +++ b/src/lib/series/legend.test.ts @@ -60,7 +60,7 @@ describe('Legends', () => { seriesColor.set('colorSeries1a', colorValues1a); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -69,8 +69,8 @@ describe('Legends', () => { seriesColor.set('colorSeries1b', colorValues1b); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } }, - { color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' } }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, + { color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -79,8 +79,8 @@ describe('Legends', () => { seriesColor.set('colorSeries2a', colorValues2a); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' } }, - { color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' } }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, + { color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -94,8 +94,35 @@ describe('Legends', () => { const emptyColorMap = new Map(); const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet'); const expected = [ - { color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' } }, + { color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); + it('sets all series legend items to visible when specs are not initialized yet', () => { + seriesColor.set('colorSeries1a', colorValues1a); + seriesColor.set('colorSeries1b', colorValues1b); + seriesColor.set('colorSeries2a', colorValues2a); + seriesColor.set('colorSeries2b', colorValues2b); + + const emptyColorMap = new Map(); + + const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', null); + + const visibility = legend.map((item) => item.isVisible); + + expect(visibility).toEqual([true, true, true, true]); + }); + it('selectively sets series to visible when specs are already initialized yet', () => { + seriesColor.set('colorSeries1a', colorValues1a); + seriesColor.set('colorSeries1b', colorValues1b); + seriesColor.set('colorSeries2a', colorValues2a); + seriesColor.set('colorSeries2b', colorValues2b); + + const emptyColorMap = new Map(); + + const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', null); + + const visibility = legend.map((item) => item.isVisible); + expect(visibility).toEqual([true, true, true, true]); + }); }); diff --git a/src/lib/series/legend.ts b/src/lib/series/legend.ts index 39460c6cb6..fc668449fb 100644 --- a/src/lib/series/legend.ts +++ b/src/lib/series/legend.ts @@ -5,12 +5,14 @@ export interface LegendItem { color: string; label: string; value: DataSeriesColorsValues; + isVisible?: boolean; } export function computeLegend( seriesColor: Map, seriesColorMap: Map, specs: Map, defaultColor: string, + seriesForFiltering: DataSeriesColorsValues[] | null = null, ): LegendItem[] { const legendItems: LegendItem[] = []; seriesColor.forEach((series, key) => { @@ -30,6 +32,7 @@ export function computeLegend( color, label, value: series, + isVisible: true, }); }); return legendItems; diff --git a/src/lib/series/series.ts b/src/lib/series/series.ts index 704bdc2918..fcab8d23b5 100644 --- a/src/lib/series/series.ts +++ b/src/lib/series/series.ts @@ -1,8 +1,8 @@ +import { findSelectedDataSeries } from '../../state/utils'; import { ColorConfig } from '../themes/theme'; import { Accessor } from '../utils/accessor'; import { GroupId, SpecId } from '../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from './domains/y_domain'; -import { isEqualSeriesKey } from './series_utils'; import { BasicSeriesSpec, Datum, SeriesAccessors } from './specs'; export interface RawDataSeriesDatum { @@ -342,7 +342,7 @@ export function formatStackedDataSeriesValues( export function getSplittedSeries( seriesSpecs: Map, - selectedDataSeries?: DataSeriesColorsValues | null, + selectedDataSeries?: DataSeriesColorsValues[] | null, ): { splittedSeries: Map; seriesColors: Map; @@ -357,11 +357,13 @@ export function getSplittedSeries( let currentRawDataSeries = dataSeries.rawDataSeries; if (selectedDataSeries) { - const selectedSpecId = selectedDataSeries.specId; - const selectedSeriesKey = selectedDataSeries.colorValues; - currentRawDataSeries = dataSeries.rawDataSeries.filter((series): boolean => { - return (selectedSpecId === specId) && isEqualSeriesKey(selectedSeriesKey, series.key); + const seriesValues = { + specId, + colorValues: series.key, + }; + + return findSelectedDataSeries(selectedDataSeries, seriesValues) > -1; }); } diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 874241abc0..5276737792 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -47,7 +47,9 @@ import { computeSeriesDomains, computeSeriesGeometries, getAxesSpecForSpecId, + getLegendItemByIndex, Transform, + updateSelectedDataSeries, } from './utils'; export interface TooltipPosition { top?: number; @@ -127,6 +129,7 @@ export class ChartStore { legendItems: LegendItem[] = []; highlightedLegendItemIndex: IObservableValue = observable.box(null); selectedLegendItemIndex: IObservableValue = observable.box(null); + selectedDataSeries: DataSeriesColorsValues[] = []; tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -141,6 +144,7 @@ export class ChartStore { onLegendItemClickListener?: LegendItemListener; onLegendItemPlusClickListener?: LegendItemListener; onLegendItemMinusClickListener?: LegendItemListener; + onLegendItemVisibilityToggleClickListener?: LegendItemListener; geometries: { points: PointGeometry[]; @@ -251,6 +255,15 @@ export class ChartStore { } }); + toggleVisibility = action((legendItemIndex: number) => { + const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); + + if (legendItem) { + this.selectedDataSeries = updateSelectedDataSeries(this.selectedDataSeries, legendItem.value); + this.computeChart(); + } + }); + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } @@ -371,6 +384,8 @@ export class ChartStore { return; } + const isSpecsInitialized = this.specsInitialized.get(); + const seriesForFiltering = isSpecsInitialized ? this.selectedDataSeries : null; // for passing in selectedDataSeries info to show/hide a series // commenting out for now as we consider how to show/hide a series // const selectedDataSeries: DataSeriesColorsValues | null = @@ -378,7 +393,7 @@ export class ChartStore { // ? this.selectedLegendItem.get()!.value // : null; - const seriesDomains = computeSeriesDomains(this.seriesSpecs); + const seriesDomains = computeSeriesDomains(this.seriesSpecs, seriesForFiltering); this.seriesDomainsAndData = seriesDomains; // tslint:disable-next-line:no-console // console.log({colors: seriesDomains.seriesColors}); @@ -391,6 +406,7 @@ export class ChartStore { seriesColorMap, this.seriesSpecs, this.chartTheme.colors.defaultVizColor, + seriesForFiltering, ); // tslint:disable-next-line:no-console // console.log({ legendItems: this.legendItems }); diff --git a/src/state/utils.test.ts b/src/state/utils.test.ts index 81b77b518c..48b2ab5c75 100644 --- a/src/state/utils.test.ts +++ b/src/state/utils.test.ts @@ -1,8 +1,12 @@ +import { LegendItem } from '../lib/series/legend'; +import { DataSeriesColorsValues } from '../lib/series/series'; import { BasicSeriesSpec } from '../lib/series/specs'; + import { BARCHART_1Y0G, BARCHART_1Y1G } from '../lib/series/utils/test_dataset'; + import { getGroupId, getSpecId, SpecId } from '../lib/utils/ids'; import { ScaleType } from '../lib/utils/scales/scales'; -import { computeSeriesDomains } from './utils'; +import { computeSeriesDomains, findSelectedDataSeries, getLegendItemByIndex, updateSelectedDataSeries } from './utils'; describe('Chart State utils', () => { it('should compute and format specifications for non stacked chart', () => { @@ -114,4 +118,73 @@ describe('Chart State utils', () => { expect(domains.formattedDataSeries.stacked).toMatchSnapshot(); expect(domains.formattedDataSeries.nonStacked).toMatchSnapshot(); }); + it('should get a legend item by index', () => { + const dataSeriesColorValues = { + specId: getSpecId('foo'), + colorValues: [], + }; + + const firstItem = { + color: 'foo', + label: 'foo', + value: dataSeriesColorValues, + }; + + const secondItem = { + color: 'bar', + label: 'bar', + value: dataSeriesColorValues, + }; + + const legendItems: LegendItem[] = [firstItem, secondItem]; + const legendItemIndex = 1; + + expect(getLegendItemByIndex([], legendItemIndex)).toBe(null); + expect(getLegendItemByIndex(legendItems, 2)).toEqual(null); + expect(getLegendItemByIndex(legendItems, legendItemIndex)).toEqual(secondItem); + }); + it('should check if a DataSeriesColorValues item exists in a list of DataSeriesColorValues', () => { + const dataSeriesValuesA: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesB: DataSeriesColorsValues = { + specId: getSpecId('b'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesC: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'd'], + }; + + const selectedSeries = [dataSeriesValuesA, dataSeriesValuesB]; + + expect(findSelectedDataSeries(selectedSeries, dataSeriesValuesA)).toBe(0); + expect(findSelectedDataSeries(selectedSeries, dataSeriesValuesC)).toBe(-1); + }); + it('should update a list of DataSeriesColorsValues given a selected DataSeriesColorValues item', () => { + const dataSeriesValuesA: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesB: DataSeriesColorsValues = { + specId: getSpecId('b'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesC: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'd'], + }; + + const selectedSeries = [dataSeriesValuesA, dataSeriesValuesB]; + const addedSelectedSeries = [dataSeriesValuesA, dataSeriesValuesB, dataSeriesValuesC]; + const removedSelectedSeries = [dataSeriesValuesB]; + + expect(updateSelectedDataSeries([...selectedSeries], dataSeriesValuesC)).toEqual(addedSelectedSeries); + expect(updateSelectedDataSeries([...selectedSeries], dataSeriesValuesA)).toEqual(removedSelectedSeries); + }); }); diff --git a/src/state/utils.ts b/src/state/utils.ts index 3009ce3a5d..0e52693daf 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -2,6 +2,7 @@ import { isVertical } from '../lib/axes/axis_utils'; import { CurveType } from '../lib/series/curves'; import { mergeXDomain, XDomain } from '../lib/series/domains/x_domain'; import { mergeYDomain, YDomain } from '../lib/series/domains/y_domain'; +import { LegendItem } from '../lib/series/legend'; import { AreaGeometry, BarGeometry, @@ -21,6 +22,7 @@ import { getSplittedSeries, RawDataSeries, } from '../lib/series/series'; +import { isEqualSeriesKey } from '../lib/series/series_utils'; import { AreaSeriesSpec, AxisSpec, @@ -45,9 +47,37 @@ export interface BrushExtent { maxY: number; } +export function getLegendItemByIndex(items: LegendItem[], index: number): LegendItem | null { + if (index < 0 || index >= items.length) { + return null; + } + return items[index]; +} + +export function findSelectedDataSeries(series: DataSeriesColorsValues[], value: DataSeriesColorsValues): number { + return series.findIndex((item: DataSeriesColorsValues) => { + return isEqualSeriesKey(item.colorValues, value.colorValues) && item.specId === value.specId; + }); +} + +export function updateSelectedDataSeries( + series: DataSeriesColorsValues[], + value: DataSeriesColorsValues, +): DataSeriesColorsValues[] { + const seriesIndex = findSelectedDataSeries(series, value); + const updatedSeries = [...series]; + + if (seriesIndex > -1) { + updatedSeries.splice(seriesIndex, 1); + } else { + updatedSeries.push(value); + } + return updatedSeries; +} + export function computeSeriesDomains( seriesSpecs: Map, - selectedDataSeries?: DataSeriesColorsValues | null, + selectedDataSeries?: DataSeriesColorsValues[] | null, ): { xDomain: XDomain; yDomain: YDomain[]; From c052f363c3c5e2a459a91b86bd874a07b1e8d5f6 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 26 Feb 2019 08:51:58 -0800 Subject: [PATCH 34/46] feat(legend/series): init selectedDateSeries & set legend item selection --- src/lib/series/legend.test.ts | 12 +++++++----- src/lib/series/legend.ts | 9 +++++++-- src/state/chart_state.test.ts | 6 ++++++ src/state/chart_state.ts | 26 ++++++++++++++------------ src/state/utils.test.ts | 27 ++++++++++++++++++++++++++- src/state/utils.ts | 6 ++++++ 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/lib/series/legend.test.ts b/src/lib/series/legend.test.ts index 8d09e50cda..483944f19f 100644 --- a/src/lib/series/legend.test.ts +++ b/src/lib/series/legend.test.ts @@ -98,31 +98,33 @@ describe('Legends', () => { ]; expect(legend).toEqual(expected); }); - it('sets all series legend items to visible when specs are not initialized yet', () => { + it('sets all series legend items to visible when selectedDataSeries is null', () => { seriesColor.set('colorSeries1a', colorValues1a); seriesColor.set('colorSeries1b', colorValues1b); seriesColor.set('colorSeries2a', colorValues2a); seriesColor.set('colorSeries2b', colorValues2b); const emptyColorMap = new Map(); + const selectedDataSeries = null; - const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', null); + const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries); const visibility = legend.map((item) => item.isVisible); expect(visibility).toEqual([true, true, true, true]); }); - it('selectively sets series to visible when specs are already initialized yet', () => { + it('selectively sets series to visible when there are selectedDataSeries items', () => { seriesColor.set('colorSeries1a', colorValues1a); seriesColor.set('colorSeries1b', colorValues1b); seriesColor.set('colorSeries2a', colorValues2a); seriesColor.set('colorSeries2b', colorValues2b); const emptyColorMap = new Map(); + const selectedDataSeries = [colorValues1a, colorValues1b]; - const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', null); + const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries); const visibility = legend.map((item) => item.isVisible); - expect(visibility).toEqual([true, true, true, true]); + expect(visibility).toEqual([true, true, false, false]); }); }); diff --git a/src/lib/series/legend.ts b/src/lib/series/legend.ts index fc668449fb..6e9f38fad2 100644 --- a/src/lib/series/legend.ts +++ b/src/lib/series/legend.ts @@ -1,6 +1,8 @@ +import { findSelectedDataSeries } from '../../state/utils'; import { SpecId } from '../utils/ids'; import { DataSeriesColorsValues } from './series'; import { BasicSeriesSpec } from './specs'; + export interface LegendItem { color: string; label: string; @@ -12,7 +14,7 @@ export function computeLegend( seriesColorMap: Map, specs: Map, defaultColor: string, - seriesForFiltering: DataSeriesColorsValues[] | null = null, + selectedDataSeries?: DataSeriesColorsValues[] | null, ): LegendItem[] { const legendItems: LegendItem[] = []; seriesColor.forEach((series, key) => { @@ -28,11 +30,14 @@ export function computeLegend( } else { label = series.colorValues.join(' - '); } + + const isVisible = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; + legendItems.push({ color, label, value: series, - isVisible: true, + isVisible, }); }); return legendItems; diff --git a/src/state/chart_state.test.ts b/src/state/chart_state.test.ts index 204090cd90..a56ae84b5a 100644 --- a/src/state/chart_state.test.ts +++ b/src/state/chart_state.test.ts @@ -50,6 +50,12 @@ describe('Chart Store', () => { expect(seriesDomainsAndData).not.toBeUndefined(); }); + test('can initialize selectedDataSeries depending on previous state', () => { + store.selectedDataSeries = null; + store.computeChart(); + expect(store.selectedDataSeries).toEqual([{ specId: SPEC_ID, colorValues: [] }]); + }); + test('can add an axis', () => { const axisSpec: AxisSpec = { id: AXIS_ID, diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 5276737792..ef96515e04 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -46,6 +46,7 @@ import { computeChartTransform, computeSeriesDomains, computeSeriesGeometries, + getAllDataSeriesColorValues, getAxesSpecForSpecId, getLegendItemByIndex, Transform, @@ -129,7 +130,7 @@ export class ChartStore { legendItems: LegendItem[] = []; highlightedLegendItemIndex: IObservableValue = observable.box(null); selectedLegendItemIndex: IObservableValue = observable.box(null); - selectedDataSeries: DataSeriesColorsValues[] = []; + selectedDataSeries: DataSeriesColorsValues[] | null = null; tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -259,7 +260,7 @@ export class ChartStore { const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); if (legendItem) { - this.selectedDataSeries = updateSelectedDataSeries(this.selectedDataSeries, legendItem.value); + this.selectedDataSeries = updateSelectedDataSeries(this.selectedDataSeries || [], legendItem.value); this.computeChart(); } }); @@ -384,17 +385,18 @@ export class ChartStore { return; } - const isSpecsInitialized = this.specsInitialized.get(); - const seriesForFiltering = isSpecsInitialized ? this.selectedDataSeries : null; - // for passing in selectedDataSeries info to show/hide a series - // commenting out for now as we consider how to show/hide a series - // const selectedDataSeries: DataSeriesColorsValues | null = - // this.selectedLegendItem.get() - // ? this.selectedLegendItem.get()!.value - // : null; + if (!this.specsInitialized.get()) { + this.selectedDataSeries = null; + } - const seriesDomains = computeSeriesDomains(this.seriesSpecs, seriesForFiltering); + const seriesDomains = computeSeriesDomains(this.seriesSpecs, this.selectedDataSeries); this.seriesDomainsAndData = seriesDomains; + + // If this.selectedDataSeries is null, initialize with all split series + if (!this.selectedDataSeries) { + this.selectedDataSeries = getAllDataSeriesColorValues(seriesDomains.seriesColors); + } + // tslint:disable-next-line:no-console // console.log({colors: seriesDomains.seriesColors}); @@ -406,7 +408,7 @@ export class ChartStore { seriesColorMap, this.seriesSpecs, this.chartTheme.colors.defaultVizColor, - seriesForFiltering, + this.selectedDataSeries, ); // tslint:disable-next-line:no-console // console.log({ legendItems: this.legendItems }); diff --git a/src/state/utils.test.ts b/src/state/utils.test.ts index 48b2ab5c75..00bdd866c4 100644 --- a/src/state/utils.test.ts +++ b/src/state/utils.test.ts @@ -6,7 +6,13 @@ import { BARCHART_1Y0G, BARCHART_1Y1G } from '../lib/series/utils/test_dataset'; import { getGroupId, getSpecId, SpecId } from '../lib/utils/ids'; import { ScaleType } from '../lib/utils/scales/scales'; -import { computeSeriesDomains, findSelectedDataSeries, getLegendItemByIndex, updateSelectedDataSeries } from './utils'; +import { + computeSeriesDomains, + findSelectedDataSeries, + getAllDataSeriesColorValues, + getLegendItemByIndex, + updateSelectedDataSeries, +} from './utils'; describe('Chart State utils', () => { it('should compute and format specifications for non stacked chart', () => { @@ -187,4 +193,23 @@ describe('Chart State utils', () => { expect(updateSelectedDataSeries([...selectedSeries], dataSeriesValuesC)).toEqual(addedSelectedSeries); expect(updateSelectedDataSeries([...selectedSeries], dataSeriesValuesA)).toEqual(removedSelectedSeries); }); + it('should return all of the DataSeriesColorValues on initialization', () => { + const dataSeriesValuesA: DataSeriesColorsValues = { + specId: getSpecId('a'), + colorValues: ['a', 'b', 'c'], + }; + + const dataSeriesValuesB: DataSeriesColorsValues = { + specId: getSpecId('b'), + colorValues: ['a', 'b', 'c'], + }; + + const colorMap = new Map(); + colorMap.set('a', dataSeriesValuesA); + colorMap.set('b', dataSeriesValuesB); + + const expected = [dataSeriesValuesA, dataSeriesValuesB]; + + expect(getAllDataSeriesColorValues(colorMap)).toEqual(expected); + }); }); diff --git a/src/state/utils.ts b/src/state/utils.ts index 0e52693daf..eacd5933ce 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -60,6 +60,12 @@ export function findSelectedDataSeries(series: DataSeriesColorsValues[], value: }); } +export function getAllDataSeriesColorValues( + seriesColors: Map, +): DataSeriesColorsValues[] { + return Array.from(seriesColors.values()); +} + export function updateSelectedDataSeries( series: DataSeriesColorsValues[], value: DataSeriesColorsValues, From e25233317056739b7580d96b4e4fece6b43366a3 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 26 Feb 2019 09:00:16 -0800 Subject: [PATCH 35/46] feat(legend): set legend item icon based on item status --- src/components/_legend.scss | 8 -------- src/components/legend_element.tsx | 7 ++----- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/_legend.scss b/src/components/_legend.scss index d4a40915f4..f91ba35c19 100644 --- a/src/components/_legend.scss +++ b/src/components/_legend.scss @@ -110,14 +110,6 @@ $elasticChartsLegendMaxHeight: $euiSize * 4; } } -.elasticChartsLegendListItem__vibilityIndicator { - opacity: 0.5; - - &.elasticChartsLegendListItem__vibilityIndicator--visible { - opacity: 1; - } -} - .elasticChartsLegend__toggle { border-radius: $euiBorderRadius; border-bottom-right-radius: 0; diff --git a/src/components/legend_element.tsx b/src/components/legend_element.tsx index 9fd9b179d4..fefc67f633 100644 --- a/src/components/legend_element.tsx +++ b/src/components/legend_element.tsx @@ -160,14 +160,11 @@ class LegendElementComponent extends React.Component { - const className = classNames('elasticChartsLegendListItem__vibilityIndicator', { - 'elasticChartsLegendListItem__vibilityIndicator--visible': isVisible, - }); + const iconType = isVisible ? 'eye' : 'eyeClosed'; return ; } From 5857a639477539bb79d80f6ca8d25677846cd26f Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 26 Feb 2019 10:06:58 -0800 Subject: [PATCH 36/46] refactor(legend): change property to better clarify use --- src/components/legend.tsx | 8 ++++---- src/components/legend_element.tsx | 4 ++-- src/lib/series/legend.test.ts | 16 ++++++++-------- src/lib/series/legend.ts | 6 +++--- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 4f3957aac2..a61f6616b4 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -81,11 +81,11 @@ class LegendComponent extends React.Component { onMouseLeave: this.onLegendItemMouseout, }; - const { color, label, isVisible } = item; + const { color, label, isSelected } = item; return ( - {this.renderLegendElement({ color, label, isVisible }, index)} + {this.renderLegendElement({ color, label, isSelected }, index)} ); })} @@ -103,8 +103,8 @@ class LegendComponent extends React.Component { this.props.chartStore!.onLegendItemOut(); } - private renderLegendElement = ({ color, label, isVisible }: Partial, legendItemIndex: number) => { - const props = { color, label, isVisible, index: legendItemIndex }; + private renderLegendElement = ({ color, label, isSelected }: Partial, legendItemIndex: number) => { + const props = { color, label, isSelected, index: legendItemIndex }; return ; } diff --git a/src/components/legend_element.tsx b/src/components/legend_element.tsx index fefc67f633..eeb12b1965 100644 --- a/src/components/legend_element.tsx +++ b/src/components/legend_element.tsx @@ -159,8 +159,8 @@ class LegendElementComponent extends React.Component { - const iconType = isVisible ? 'eye' : 'eyeClosed'; + private renderVisibilityButton = (legendItemIndex: number, isSelected: boolean = true) => { + const iconType = isSelected ? 'eye' : 'eyeClosed'; return { seriesColor.set('colorSeries1a', colorValues1a); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, ]; expect(legend).toEqual(expected); }); @@ -69,8 +69,8 @@ describe('Legends', () => { seriesColor.set('colorSeries1b', colorValues1b); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, - { color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isVisible: true }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, + { color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isSelected: true }, ]; expect(legend).toEqual(expected); }); @@ -79,8 +79,8 @@ describe('Legends', () => { seriesColor.set('colorSeries2a', colorValues2a); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, - { color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isVisible: true }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, + { color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isSelected: true }, ]; expect(legend).toEqual(expected); }); @@ -94,7 +94,7 @@ describe('Legends', () => { const emptyColorMap = new Map(); const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet'); const expected = [ - { color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, + { color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, ]; expect(legend).toEqual(expected); }); @@ -109,7 +109,7 @@ describe('Legends', () => { const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries); - const visibility = legend.map((item) => item.isVisible); + const visibility = legend.map((item) => item.isSelected); expect(visibility).toEqual([true, true, true, true]); }); @@ -124,7 +124,7 @@ describe('Legends', () => { const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries); - const visibility = legend.map((item) => item.isVisible); + const visibility = legend.map((item) => item.isSelected); expect(visibility).toEqual([true, true, false, false]); }); }); diff --git a/src/lib/series/legend.ts b/src/lib/series/legend.ts index 6e9f38fad2..4cfb6ff714 100644 --- a/src/lib/series/legend.ts +++ b/src/lib/series/legend.ts @@ -7,7 +7,7 @@ export interface LegendItem { color: string; label: string; value: DataSeriesColorsValues; - isVisible?: boolean; + isSelected?: boolean; } export function computeLegend( seriesColor: Map, @@ -31,13 +31,13 @@ export function computeLegend( label = series.colorValues.join(' - '); } - const isVisible = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; + const isSelected = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; legendItems.push({ color, label, value: series, - isVisible, + isSelected, }); }); return legendItems; From 3c081f4d82d0f66992b5b004fe65b641ea2e62fe Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 26 Feb 2019 11:28:25 -0800 Subject: [PATCH 37/46] feat(legend/series): toggle individual series visibility --- src/components/legend.tsx | 8 +++----- src/components/legend_element.tsx | 12 ++++++++---- src/lib/series/legend.test.ts | 16 ++++++++-------- src/lib/series/legend.ts | 6 +++--- src/state/chart_state.ts | 20 +++++++++++++++++++- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index a61f6616b4..b38645049f 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -81,11 +81,9 @@ class LegendComponent extends React.Component { onMouseLeave: this.onLegendItemMouseout, }; - const { color, label, isSelected } = item; - return ( - {this.renderLegendElement({ color, label, isSelected }, index)} + {this.renderLegendElement(item, index)} ); })} @@ -103,8 +101,8 @@ class LegendComponent extends React.Component { this.props.chartStore!.onLegendItemOut(); } - private renderLegendElement = ({ color, label, isSelected }: Partial, legendItemIndex: number) => { - const props = { color, label, isSelected, index: legendItemIndex }; + private renderLegendElement = ({ color, label, isVisible }: Partial, legendItemIndex: number) => { + const props = { color, label, isVisible, index: legendItemIndex }; return ; } diff --git a/src/components/legend_element.tsx b/src/components/legend_element.tsx index eeb12b1965..d3e2ced49d 100644 --- a/src/components/legend_element.tsx +++ b/src/components/legend_element.tsx @@ -155,12 +155,16 @@ class LegendElementComponent extends React.Component); } - private onVisibilityClick = (legendItemIndex: number) => () => { - this.props.chartStore!.toggleVisibility(legendItemIndex); + private onVisibilityClick = (legendItemIndex: number) => (event: React.MouseEvent) => { + if (event.shiftKey) { + this.props.chartStore!.toggleSingleSeries(legendItemIndex); + } else { + this.props.chartStore!.toggleSeriesVisibility(legendItemIndex); + } } - private renderVisibilityButton = (legendItemIndex: number, isSelected: boolean = true) => { - const iconType = isSelected ? 'eye' : 'eyeClosed'; + private renderVisibilityButton = (legendItemIndex: number, isVisible: boolean = true) => { + const iconType = isVisible ? 'eye' : 'eyeClosed'; return { seriesColor.set('colorSeries1a', colorValues1a); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -69,8 +69,8 @@ describe('Legends', () => { seriesColor.set('colorSeries1b', colorValues1b); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, - { color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isSelected: true }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, + { color: 'blue', label: 'a - b', value: { colorValues: ['a', 'b'], specId: 'spec1' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -79,8 +79,8 @@ describe('Legends', () => { seriesColor.set('colorSeries2a', colorValues2a); const legend = computeLegend(seriesColor, seriesColorMap, specs, 'violet'); const expected = [ - { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, - { color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isSelected: true }, + { color: 'red', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, + { color: 'green', label: 'spec2', value: { colorValues: [], specId: 'spec2' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -94,7 +94,7 @@ describe('Legends', () => { const emptyColorMap = new Map(); const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet'); const expected = [ - { color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isSelected: true }, + { color: 'violet', label: 'spec1', value: { colorValues: [], specId: 'spec1' }, isVisible: true }, ]; expect(legend).toEqual(expected); }); @@ -109,7 +109,7 @@ describe('Legends', () => { const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries); - const visibility = legend.map((item) => item.isSelected); + const visibility = legend.map((item) => item.isVisible); expect(visibility).toEqual([true, true, true, true]); }); @@ -124,7 +124,7 @@ describe('Legends', () => { const legend = computeLegend(seriesColor, emptyColorMap, specs, 'violet', selectedDataSeries); - const visibility = legend.map((item) => item.isSelected); + const visibility = legend.map((item) => item.isVisible); expect(visibility).toEqual([true, true, false, false]); }); }); diff --git a/src/lib/series/legend.ts b/src/lib/series/legend.ts index 4cfb6ff714..6e9f38fad2 100644 --- a/src/lib/series/legend.ts +++ b/src/lib/series/legend.ts @@ -7,7 +7,7 @@ export interface LegendItem { color: string; label: string; value: DataSeriesColorsValues; - isSelected?: boolean; + isVisible?: boolean; } export function computeLegend( seriesColor: Map, @@ -31,13 +31,13 @@ export function computeLegend( label = series.colorValues.join(' - '); } - const isSelected = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; + const isVisible = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; legendItems.push({ color, label, value: series, - isSelected, + isVisible, }); }); return legendItems; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index ef96515e04..38dc215c4f 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -46,6 +46,7 @@ import { computeChartTransform, computeSeriesDomains, computeSeriesGeometries, + findSelectedDataSeries, getAllDataSeriesColorValues, getAxesSpecForSpecId, getLegendItemByIndex, @@ -256,7 +257,24 @@ export class ChartStore { } }); - toggleVisibility = action((legendItemIndex: number) => { + toggleSingleSeries = action((legendItemIndex: number) => { + const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); + + if (legendItem) { + if (this.selectedDataSeries && findSelectedDataSeries(this.selectedDataSeries, legendItem.value) > -1) { + this.selectedDataSeries = + this.legendItems + .filter((item: LegendItem, idx: number) => idx !== legendItemIndex) + .map((item: LegendItem) => item.value); + } else { + this.selectedDataSeries = [legendItem.value]; + } + + this.computeChart(); + } + }); + + toggleSeriesVisibility = action((legendItemIndex: number) => { const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); if (legendItem) { From 1d9a5d1c7d25e65119da24b92572d7c057be6010 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Tue, 26 Feb 2019 15:15:50 -0800 Subject: [PATCH 38/46] test(chart_state): add tests for new legend interactions --- src/state/chart_state.test.ts | 105 ++++++++++++++++++++++++++++++++++ src/state/chart_state.ts | 4 +- src/state/utils.test.ts | 6 +- src/state/utils.ts | 14 ++++- 4 files changed, 122 insertions(+), 7 deletions(-) diff --git a/src/state/chart_state.test.ts b/src/state/chart_state.test.ts index 12160f1e31..dd398fe349 100644 --- a/src/state/chart_state.test.ts +++ b/src/state/chart_state.test.ts @@ -222,6 +222,105 @@ describe('Chart Store', () => { expect(outListener.mock.calls.length).toBe(1); }); + test('can respond to legend item click event', () => { + const legendListener = jest.fn((ds: DataSeriesColorsValues | null): void => { return; }); + + store.legendItems = [firstLegendItem, secondLegendItem]; + store.selectedLegendItemIndex.set(null); + store.onLegendItemClickListener = undefined; + + store.onLegendItemClick(0); + expect(store.selectedLegendItemIndex.get()).toBe(0); + expect(legendListener).not.toBeCalled(); + + store.setOnLegendItemClickListener(legendListener); + store.onLegendItemClick(0); + expect(store.selectedLegendItemIndex.get()).toBe(null); + expect(legendListener).toBeCalledWith(null); + + store.setOnLegendItemClickListener(legendListener); + store.onLegendItemClick(1); + expect(store.selectedLegendItemIndex.get()).toBe(1); + expect(legendListener).toBeCalledWith(secondLegendItem.value); + }); + + test('can respond to a legend item plus click event', () => { + const legendListener = jest.fn((ds: DataSeriesColorsValues | null): void => { return; }); + + store.legendItems = [firstLegendItem, secondLegendItem]; + store.selectedLegendItemIndex.set(null); + store.onLegendItemPlusClickListener = undefined; + + store.onLegendItemPlusClick(); + expect(legendListener).not.toBeCalled(); + + store.setOnLegendItemPlusClickListener(legendListener); + store.onLegendItemPlusClick(); + expect(legendListener).toBeCalledWith(null); + + store.selectedLegendItemIndex.set(0); + store.onLegendItemPlusClick(); + expect(legendListener).toBeCalledWith(firstLegendItem.value); + }); + + test('can respond to a legend item minus click event', () => { + const legendListener = jest.fn((ds: DataSeriesColorsValues | null): void => { return; }); + + store.legendItems = [firstLegendItem, secondLegendItem]; + store.selectedLegendItemIndex.set(null); + store.onLegendItemMinusClickListener = undefined; + + store.onLegendItemMinusClick(); + expect(legendListener).not.toBeCalled(); + + store.setOnLegendItemMinusClickListener(legendListener); + store.onLegendItemMinusClick(); + expect(legendListener).toBeCalledWith(null); + + store.selectedLegendItemIndex.set(0); + store.onLegendItemMinusClick(); + expect(legendListener).toBeCalledWith(firstLegendItem.value); + }); + + test('can toggle series visibility', () => { + const computeChart = jest.fn((): void => { return; }); + + store.legendItems = [firstLegendItem, secondLegendItem]; + store.selectedDataSeries = null; + store.computeChart = computeChart; + + store.toggleSeriesVisibility(3); + expect(store.selectedDataSeries).toEqual(null); + expect(computeChart).not.toBeCalled(); + + store.selectedDataSeries = [firstLegendItem.value, secondLegendItem.value]; + store.toggleSeriesVisibility(0); + expect(store.selectedDataSeries).toEqual([secondLegendItem.value]); + expect(computeChart).toBeCalled(); + + store.selectedDataSeries = [firstLegendItem.value]; + store.toggleSeriesVisibility(0); + expect(store.selectedDataSeries).toEqual([]); + }); + + test('can toggle single series visibility', () => { + const computeChart = jest.fn((): void => { return; }); + + store.legendItems = [firstLegendItem, secondLegendItem]; + store.selectedDataSeries = null; + store.computeChart = computeChart; + + store.toggleSingleSeries(3); + expect(store.selectedDataSeries).toEqual(null); + expect(computeChart).not.toBeCalled(); + + store.toggleSingleSeries(0); + expect(store.selectedDataSeries).toEqual([firstLegendItem.value]); + + store.toggleSingleSeries(0); + expect(store.selectedDataSeries).toEqual([secondLegendItem.value]); + }); + test('can set an element click listener', () => { const clickListener = (value: GeometryValue): void => { return; }; store.setOnElementClickListener(clickListener); @@ -248,6 +347,12 @@ describe('Chart Store', () => { store.removeOnLegendItemOverListener(); expect(store.onLegendItemOverListener).toEqual(undefined); + + store.removeOnLegendItemPlusClickListener(); + expect(store.onLegendItemPlusClickListener).toEqual(undefined); + + store.removeOnLegendItemMinusClickListener(); + expect(store.onLegendItemMinusClickListener).toEqual(undefined); }); test('can respond to a brush end event', () => { diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index bd97f31be7..5036fbbc81 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -266,7 +266,7 @@ export class ChartStore { const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); if (legendItem) { - if (this.selectedDataSeries && findSelectedDataSeries(this.selectedDataSeries, legendItem.value) > -1) { + if (findSelectedDataSeries(this.selectedDataSeries, legendItem.value) > -1) { this.selectedDataSeries = this.legendItems .filter((item: LegendItem, idx: number) => idx !== legendItemIndex) @@ -283,7 +283,7 @@ export class ChartStore { const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); if (legendItem) { - this.selectedDataSeries = updateSelectedDataSeries(this.selectedDataSeries || [], legendItem.value); + this.selectedDataSeries = updateSelectedDataSeries(this.selectedDataSeries, legendItem.value); this.computeChart(); } }); diff --git a/src/state/utils.test.ts b/src/state/utils.test.ts index 00bdd866c4..d7259bb62d 100644 --- a/src/state/utils.test.ts +++ b/src/state/utils.test.ts @@ -169,6 +169,7 @@ describe('Chart State utils', () => { expect(findSelectedDataSeries(selectedSeries, dataSeriesValuesA)).toBe(0); expect(findSelectedDataSeries(selectedSeries, dataSeriesValuesC)).toBe(-1); + expect(findSelectedDataSeries(null, dataSeriesValuesA)).toBe(-1); }); it('should update a list of DataSeriesColorsValues given a selected DataSeriesColorValues item', () => { const dataSeriesValuesA: DataSeriesColorsValues = { @@ -190,8 +191,9 @@ describe('Chart State utils', () => { const addedSelectedSeries = [dataSeriesValuesA, dataSeriesValuesB, dataSeriesValuesC]; const removedSelectedSeries = [dataSeriesValuesB]; - expect(updateSelectedDataSeries([...selectedSeries], dataSeriesValuesC)).toEqual(addedSelectedSeries); - expect(updateSelectedDataSeries([...selectedSeries], dataSeriesValuesA)).toEqual(removedSelectedSeries); + expect(updateSelectedDataSeries(selectedSeries, dataSeriesValuesC)).toEqual(addedSelectedSeries); + expect(updateSelectedDataSeries(selectedSeries, dataSeriesValuesA)).toEqual(removedSelectedSeries); + expect(updateSelectedDataSeries(null, dataSeriesValuesA)).toEqual([dataSeriesValuesA]); }); it('should return all of the DataSeriesColorValues on initialization', () => { const dataSeriesValuesA: DataSeriesColorsValues = { diff --git a/src/state/utils.ts b/src/state/utils.ts index eacd5933ce..997ae8863c 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -54,7 +54,14 @@ export function getLegendItemByIndex(items: LegendItem[], index: number): Legend return items[index]; } -export function findSelectedDataSeries(series: DataSeriesColorsValues[], value: DataSeriesColorsValues): number { +export function findSelectedDataSeries( + series: DataSeriesColorsValues[] | null, + value: DataSeriesColorsValues, +): number { + if (!series) { + return -1; + } + return series.findIndex((item: DataSeriesColorsValues) => { return isEqualSeriesKey(item.colorValues, value.colorValues) && item.specId === value.specId; }); @@ -67,11 +74,12 @@ export function getAllDataSeriesColorValues( } export function updateSelectedDataSeries( - series: DataSeriesColorsValues[], + series: DataSeriesColorsValues[] | null, value: DataSeriesColorsValues, ): DataSeriesColorsValues[] { + const seriesIndex = findSelectedDataSeries(series, value); - const updatedSeries = [...series]; + const updatedSeries = series ? [...series] : []; if (seriesIndex > -1) { updatedSeries.splice(seriesIndex, 1); From 28a811badcd5c8baa2a1f7e0b6026874754cd7ad Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Wed, 27 Feb 2019 12:58:07 -0800 Subject: [PATCH 39/46] feat(legend/series): update series color on color picker change --- src/components/legend.tsx | 4 ++- src/components/legend_element.tsx | 15 ++-------- src/lib/series/legend.ts | 37 ++++++++++++++++-------- src/lib/series/series.test.ts | 47 +++++++++++++++++++++++++++++++ src/lib/series/series.ts | 13 ++++++++- src/state/chart_state.ts | 20 ++++++++++++- 6 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index b38645049f..4f3957aac2 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -81,9 +81,11 @@ class LegendComponent extends React.Component { onMouseLeave: this.onLegendItemMouseout, }; + const { color, label, isVisible } = item; + return ( - {this.renderLegendElement(item, index)} + {this.renderLegendElement({ color, label, isVisible }, index)} ); })} diff --git a/src/components/legend_element.tsx b/src/components/legend_element.tsx index d3e2ced49d..39a900f120 100644 --- a/src/components/legend_element.tsx +++ b/src/components/legend_element.tsx @@ -80,7 +80,7 @@ class LegendElementComponent extends React.Component - + @@ -119,22 +119,13 @@ class LegendElementComponent extends React.Component () => { - // this.props.chartStore!.onLegendItemOver(legendItemIndex); - // } - - // private onLegendItemMouseout = () => { - // this.props.chartStore!.onLegendItemOut(); - // } - private onLegendItemPanelClose = () => { // tslint:disable-next-line:no-console console.log('close'); } - private onColorPickerChange = () => { - // tslint:disable-next-line:no-console - console.log('color picker close'); + private onColorPickerChange = (legendItemIndex: number) => (color: string) => { + this.props.chartStore!.setSeriesColor(legendItemIndex, color); } private renderPlusButton = () => { diff --git a/src/lib/series/legend.ts b/src/lib/series/legend.ts index 6e9f38fad2..c8f8f7bd50 100644 --- a/src/lib/series/legend.ts +++ b/src/lib/series/legend.ts @@ -18,21 +18,17 @@ export function computeLegend( ): LegendItem[] { const legendItems: LegendItem[] = []; seriesColor.forEach((series, key) => { + const spec = specs.get(series.specId); + const color = seriesColorMap.get(key) || defaultColor; - let label = ''; + const hasSingleSeries = seriesColor.size === 1; + const label = getSeriesColorLabel(series, hasSingleSeries, spec); + const isVisible = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; - if (seriesColor.size === 1 || series.colorValues.length === 0 || !series.colorValues[0]) { - const spec = specs.get(series.specId); - if (!spec) { - return; - } - label = `${spec.id}`; - } else { - label = series.colorValues.join(' - '); + if (!label) { + return; } - const isVisible = selectedDataSeries ? findSelectedDataSeries(selectedDataSeries, series) > -1 : true; - legendItems.push({ color, label, @@ -42,3 +38,22 @@ export function computeLegend( }); return legendItems; } + +export function getSeriesColorLabel( + series: DataSeriesColorsValues, + hasSingleSeries: boolean, + spec: BasicSeriesSpec | undefined, +): string | undefined { + let label = ''; + + if (hasSingleSeries || series.colorValues.length === 0 || !series.colorValues[0]) { + if (!spec) { + return; + } + label = `${spec.id}`; + } else { + label = series.colorValues.join(' - '); + } + + return label; +} diff --git a/src/lib/series/series.test.ts b/src/lib/series/series.test.ts index ca0b420fe6..43164d69b1 100644 --- a/src/lib/series/series.test.ts +++ b/src/lib/series/series.test.ts @@ -1,8 +1,11 @@ +import { ColorConfig } from '../themes/theme'; import { getGroupId, getSpecId, SpecId } from '../utils/ids'; import { ScaleType } from '../utils/scales/scales'; import { + DataSeriesColorsValues, formatStackedDataSeriesValues, getFormattedDataseries, + getSeriesColorMap, getSplittedSeries, RawDataSeries, splitSeries, @@ -231,4 +234,48 @@ describe('Series', () => { ); expect(stackedDataSeries.stacked).toMatchSnapshot(); }); + test('should get series color map', () => { + const spec1: BasicSeriesSpec = { + id: getSpecId('spec1'), + groupId: getGroupId('group'), + seriesType: 'line', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + yScaleToDataExtent: false, + data: TestDataset.BARCHART_1Y0G, + }; + + const specs = new Map(); + specs.set(spec1.id, spec1); + + const dataSeriesValuesA: DataSeriesColorsValues = { + specId: getSpecId('spec1'), + colorValues: ['a', 'b', 'c'], + }; + + const chartColors: ColorConfig = { + vizColors: ['elastic_charts_c1', 'elastic_charts_c2'], + defaultVizColor: 'elastic_charts', + }; + + const seriesColors = new Map(); + seriesColors.set('spec1', dataSeriesValuesA); + + const emptyCustomColors = new Map(); + + const defaultColorMap = getSeriesColorMap(seriesColors, chartColors, emptyCustomColors, specs); + const expectedDefaultColorMap = new Map(); + expectedDefaultColorMap.set('spec1', 'elastic_charts_c1'); + expect(defaultColorMap).toEqual(expectedDefaultColorMap); + + const customColors: Map = new Map(); + customColors.set('spec1', 'custom_color'); + + const customizedColorMap = getSeriesColorMap(seriesColors, chartColors, customColors, specs); + const expectedCustomizedColorMap = new Map(); + expectedCustomizedColorMap.set('spec1', 'custom_color'); + expect(customizedColorMap).toEqual(expectedCustomizedColorMap); + }); }); diff --git a/src/lib/series/series.ts b/src/lib/series/series.ts index fcab8d23b5..6baf74f7b1 100644 --- a/src/lib/series/series.ts +++ b/src/lib/series/series.ts @@ -3,6 +3,7 @@ import { ColorConfig } from '../themes/theme'; import { Accessor } from '../utils/accessor'; import { GroupId, SpecId } from '../utils/ids'; import { splitSpecsByGroupId, YBasicSeriesSpec } from './domains/y_domain'; +import { getSeriesColorLabel } from './legend'; import { BasicSeriesSpec, Datum, SeriesAccessors } from './specs'; export interface RawDataSeriesDatum { @@ -390,13 +391,23 @@ export function getSplittedSeries( export function getSeriesColorMap( seriesColors: Map, chartColors: ColorConfig, + customColors: Map, + specs: Map, ): Map { const seriesColorMap = new Map(); let counter = 0; + seriesColors.forEach((value, seriesColorKey) => { + const spec = specs.get(value.specId); + const hasSingleSeries = seriesColors.size === 1; + const seriesLabel = getSeriesColorLabel(value, hasSingleSeries, spec); + + const color = (seriesLabel && customColors.get(seriesLabel)) || + chartColors.vizColors[counter % chartColors.vizColors.length]; + seriesColorMap.set( seriesColorKey, - chartColors.vizColors[counter % chartColors.vizColors.length], + color, ); counter++; }); diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 5036fbbc81..a93923d18c 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -132,6 +132,7 @@ export class ChartStore { highlightedLegendItemIndex: IObservableValue = observable.box(null); selectedLegendItemIndex: IObservableValue = observable.box(null); selectedDataSeries: DataSeriesColorsValues[] | null = null; + customSeriesColors: Map = new Map(); tooltipData = observable.box | null>(null); tooltipPosition = observable.box<{ x: number; y: number } | null>(); @@ -288,6 +289,16 @@ export class ChartStore { } }); + setSeriesColor = action((legendItemIndex: number, color: string) => { + const legendItem = getLegendItemByIndex(this.legendItems, legendItemIndex); + + if (legendItem) { + const key = legendItem.label; + this.customSeriesColors.set(key, color); + this.computeChart(); + } + }); + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } @@ -408,6 +419,7 @@ export class ChartStore { return; } + // When specs are not initialized, reset selectedDataSeries to null if (!this.specsInitialized.get()) { this.selectedDataSeries = null; } @@ -425,7 +437,13 @@ export class ChartStore { // tslint:disable-next-line:no-console // console.log({ seriesDomains }); - const seriesColorMap = getSeriesColorMap(seriesDomains.seriesColors, this.chartTheme.colors); + const seriesColorMap = getSeriesColorMap( + seriesDomains.seriesColors, + this.chartTheme.colors, + this.customSeriesColors, + this.seriesSpecs, + ); + this.legendItems = computeLegend( seriesDomains.seriesColors, seriesColorMap, From 82e597ee98abb52784d5f2ac9c201d447bce9c72 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Wed, 27 Feb 2019 14:48:48 -0800 Subject: [PATCH 40/46] test(chart_state): test for selecting & customizing data series --- src/state/chart_state.test.ts | 41 ++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/state/chart_state.test.ts b/src/state/chart_state.test.ts index dd398fe349..fac49daaa5 100644 --- a/src/state/chart_state.test.ts +++ b/src/state/chart_state.test.ts @@ -67,9 +67,16 @@ describe('Chart Store', () => { }); test('can initialize selectedDataSeries depending on previous state', () => { + const selectedDataSeries = [{ specId: SPEC_ID, colorValues: [] }]; + store.selectedDataSeries = null; store.computeChart(); - expect(store.selectedDataSeries).toEqual([{ specId: SPEC_ID, colorValues: [] }]); + expect(store.selectedDataSeries).toEqual(selectedDataSeries); + + store.selectedDataSeries = selectedDataSeries; + store.specsInitialized.set(true); + store.computeChart(); + expect(store.selectedDataSeries).toEqual(selectedDataSeries); }); test('can add an axis', () => { @@ -455,4 +462,36 @@ describe('Chart Store', () => { localStore.computeChart(); expect(localStore.initialized.get()).toBe(false); }); + + test('only computes chart if series specs exist', () => { + const localStore = new ChartStore(); + + localStore.parentDimensions = { + width: 100, + height: 100, + top: 0, + left: 0, + }; + + localStore.seriesSpecs = new Map(); + localStore.computeChart(); + expect(localStore.initialized.get()).toBe(false); + }); + + test('can set the color for a series', () => { + const computeChart = jest.fn((): void => { return; }); + store.computeChart = computeChart; + store.legendItems = [firstLegendItem, secondLegendItem]; + + const expectedCustomColors = new Map(); + expectedCustomColors.set(firstLegendItem.label, 'foo'); + + store.setSeriesColor(-1, 'foo'); + expect(computeChart).not.toBeCalled(); + expect(store.customSeriesColors).toEqual(new Map()); + + store.setSeriesColor(0, 'foo'); + expect(computeChart).toBeCalled(); + expect(store.customSeriesColors).toEqual(expectedCustomColors); + }); }); From e6ba6eadeae998e2e6948c6dd6e72cd69aa66516 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 28 Feb 2019 08:21:49 -0800 Subject: [PATCH 41/46] test(series): add test for splitting series for only selected --- src/lib/series/series.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/lib/series/series.test.ts b/src/lib/series/series.test.ts index 43164d69b1..961a1be4b0 100644 --- a/src/lib/series/series.test.ts +++ b/src/lib/series/series.test.ts @@ -278,4 +278,36 @@ describe('Series', () => { expectedCustomizedColorMap.set('spec1', 'custom_color'); expect(customizedColorMap).toEqual(expectedCustomizedColorMap); }); + test('should only include selectedDataSeries when splitting series if selectedDataSeries is defined', () => { + const seriesSpecs = new Map(); + const specId = getSpecId('splitSpec'); + + const splitSpec: BasicSeriesSpec = { + id: specId, + groupId: getGroupId('group'), + seriesType: 'line', + yScaleType: ScaleType.Log, + xScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y1', 'y2'], + stackAccessors: ['x'], + yScaleToDataExtent: false, + data: TestDataset.BARCHART_2Y0G, + }; + + seriesSpecs.set(splitSpec.id, splitSpec); + + const allSeries = getSplittedSeries(seriesSpecs, null); + expect(allSeries.splittedSeries.get(specId)!.length).toBe(2); + + const emptySplit = getSplittedSeries(seriesSpecs, []); + expect(emptySplit.splittedSeries.get(specId)!.length).toBe(0); + + const selectedDataSeries: DataSeriesColorsValues[] = [{ + specId, + colorValues: ['y1'], + }]; + const subsetSplit = getSplittedSeries(seriesSpecs, selectedDataSeries); + expect(subsetSplit.splittedSeries.get(specId)!.length).toBe(1); + }); }); From 46a8ea77253d87085214f3b863daae431e67fd9c Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 28 Feb 2019 12:42:33 -0800 Subject: [PATCH 42/46] fix(series): reset selectedDataSeries on spec update --- src/specs/specs_parser.tsx | 1 + src/state/chart_state.test.ts | 6 ++++++ src/state/chart_state.ts | 4 ++++ stories/legend.tsx | 36 ++++++++++++++++++++++++++++++++++- 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/specs/specs_parser.tsx b/src/specs/specs_parser.tsx index e33ab9fe36..f13560fa22 100644 --- a/src/specs/specs_parser.tsx +++ b/src/specs/specs_parser.tsx @@ -18,6 +18,7 @@ export class SpecsSpecRootComponent extends PureComponent { } componentDidUpdate() { this.props.chartStore!.specsInitialized.set(true); + this.props.chartStore!.resetSelectedDataSeries(); this.props.chartStore!.computeChart(); } componentWillUnmount() { diff --git a/src/state/chart_state.test.ts b/src/state/chart_state.test.ts index fac49daaa5..c224f1d18f 100644 --- a/src/state/chart_state.test.ts +++ b/src/state/chart_state.test.ts @@ -494,4 +494,10 @@ describe('Chart Store', () => { expect(computeChart).toBeCalled(); expect(store.customSeriesColors).toEqual(expectedCustomColors); }); + + test('can reset selectedDataSeries', () => { + store.selectedDataSeries = [firstLegendItem.value]; + store.resetSelectedDataSeries(); + expect(store.selectedDataSeries).toBe(null); + }); }); diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index a93923d18c..948ba5dea9 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -299,6 +299,10 @@ export class ChartStore { } }); + resetSelectedDataSeries() { + this.selectedDataSeries = null; + } + setOnElementClickListener(listener: ElementClickListener) { this.onElementClickListener = listener; } diff --git a/stories/legend.tsx b/stories/legend.tsx index 9aa109613e..1ca2c4db01 100644 --- a/stories/legend.tsx +++ b/stories/legend.tsx @@ -11,6 +11,7 @@ import { Settings, } from '../src/'; import * as TestDatasets from '../src/lib/series/utils/test_dataset'; +import { boolean } from '@storybook/addon-knobs'; storiesOf('Legend', module) .add('right', () => { @@ -75,7 +76,7 @@ storiesOf('Legend', module) }) .add('left', () => { return ( - + ); + }) + .add('changing specs', () => { + const splitSeries = boolean('split series', true) ? + ['g1', 'g2'] : undefined; + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); }); + From 4b3fe494e02b206b66985b1b652cf7cb3d5c1957 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 28 Feb 2019 15:33:23 -0800 Subject: [PATCH 43/46] test(specs_parser): add tests for lifecycle methods --- src/specs/specs_parser.test.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/specs/specs_parser.test.tsx b/src/specs/specs_parser.test.tsx index f8ae9c8149..74ab4feaf1 100644 --- a/src/specs/specs_parser.test.tsx +++ b/src/specs/specs_parser.test.tsx @@ -11,4 +11,21 @@ describe('Specs parser', () => { mount(component); expect(chartStore.specsInitialized.get()).toBe(true); }); + test('resets selectedDataSeries on component update', () => { + const chartStore = new ChartStore(); + const reset = jest.fn((): void => { return; }); + chartStore.resetSelectedDataSeries = reset; + + const component = mount(); + component.update(); + component.setState({ foo: 'bar' }); + expect(reset).toBeCalled(); + }); + test('updates initialization state on unmount', () => { + const chartStore = new ChartStore(); + chartStore.initialized.set(true); + const component = mount(); + component.unmount(); + expect(chartStore.initialized.get()).toBe(false); + }); }); From b9d714009193e6b9674772177a66e31599138232 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 28 Feb 2019 15:36:07 -0800 Subject: [PATCH 44/46] style(legend): remove unused import --- src/components/legend.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/legend.tsx b/src/components/legend.tsx index 4f3957aac2..3dbc79320e 100644 --- a/src/components/legend.tsx +++ b/src/components/legend.tsx @@ -1,7 +1,4 @@ import { - // TODO: remove ts-ignore below once typings file is included in eui for color picker - // @ts-ignore - EuiColorPicker, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; From f7f8d93418e35a9e2ccb2647cb4a8897266046c2 Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Thu, 28 Feb 2019 16:38:46 -0800 Subject: [PATCH 45/46] style(chart_state): add comments around series logic --- src/state/chart_state.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 948ba5dea9..40cd6f0c91 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -428,10 +428,12 @@ export class ChartStore { this.selectedDataSeries = null; } + // The second argument is optional; if not supplied, then all series will be factored into computations + // Otherwise, selectedDataSeries is used to restrict the computation for just the selected series const seriesDomains = computeSeriesDomains(this.seriesSpecs, this.selectedDataSeries); this.seriesDomainsAndData = seriesDomains; - // If this.selectedDataSeries is null, initialize with all split series + // If this.selectedDataSeries is null, initialize with all series if (!this.selectedDataSeries) { this.selectedDataSeries = getAllDataSeriesColorValues(seriesDomains.seriesColors); } From 58f7e3df1e4978fa0cf0972c6019f12608f83d0b Mon Sep 17 00:00:00 2001 From: Emma Cunningham Date: Fri, 1 Mar 2019 11:36:18 -0800 Subject: [PATCH 46/46] fix(line_geometries): use opacity for spring animation --- src/components/react_canvas/line_geometries.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index 9f408e29cd..32fc76649e 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -29,7 +29,7 @@ interface LineGeometriesDataState { export class LineGeometries extends React.PureComponent< LineGeometriesDataProps, LineGeometriesDataState -> { + > { static defaultProps: Partial = { animated: false, }; @@ -41,6 +41,7 @@ export class LineGeometries extends React.PureComponent< overPoint: undefined, }; } + render() { return ( @@ -153,11 +154,12 @@ export class LineGeometries extends React.PureComponent< if (this.props.animated) { return ( - - {(props: { line: string }) => ( + + {(props: { opacity: number }) => (