From 0ba75c1f431415b9b50bd51216de252effa22b17 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Wed, 4 Mar 2020 10:16:51 +0100 Subject: [PATCH 1/7] feat(partition): add element click event This commit adds the element click event to the Partition chart, returning a groupByRollup value for each configured chart layer and the associated value --- .playground/playground.tsx | 133 +++++++++++++----- ...on-90-shows-tooltip-on-sunburst-1-snap.png | Bin 0 -> 50570 bytes integration/tests/interactions.test.ts | 9 ++ .../partition_chart/specs/index.ts | 4 + .../partition_chart/state/chart_state.tsx | 9 ++ .../selectors/on_element_click_caller.ts | 102 ++++++++++++++ .../state/selectors/picked_shapes.ts | 19 +++ .../state/selectors/pie_spec.ts | 10 ++ .../state/selectors/tooltip.ts | 27 +--- .../xy_chart/rendering/rendering.ts | 10 +- .../xy_chart/state/chart_state.tsx | 32 ++++- src/components/chart.tsx | 21 +-- src/specs/settings.tsx | 8 +- src/state/chart_state.ts | 2 + src/utils/geometry.ts | 6 +- .../interactions/4_sunburst_slice_clicks.tsx | 87 ++++++++++++ stories/interactions/interactions.stories.tsx | 1 + 17 files changed, 397 insertions(+), 83 deletions(-) create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png create mode 100644 src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts create mode 100644 src/chart_types/partition_chart/state/selectors/picked_shapes.ts create mode 100644 src/chart_types/partition_chart/state/selectors/pie_spec.ts create mode 100644 stories/interactions/4_sunburst_slice_clicks.tsx diff --git a/.playground/playground.tsx b/.playground/playground.tsx index f928282aac..b298856a9b 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -4,51 +4,116 @@ import { ScaleType, Position, Axis, - LineSeries, - LineAnnotation, - RectAnnotation, - AnnotationDomainTypes, - LineAnnotationDatum, - RectAnnotationDatum, + Settings, + PartitionElementEvent, + XYChartElementEvent, + Partition, + BarSeries, } from '../src'; -import { SeededDataGenerator } from '../src/mocks/utils'; +type PieDatum = [string, number, string, number]; +const pieData: Array = [ + ['CN', 301, 'CN', 64], + ['CN', 301, 'IN', 44], + ['CN', 301, 'US', 24], + ['CN', 301, 'ID', 13], + ['CN', 301, 'BR', 8], + ['IN', 245, 'IN', 41], + ['IN', 245, 'CN', 36], + ['IN', 245, 'US', 22], + ['IN', 245, 'BR', 11], + ['IN', 245, 'ID', 10], + ['US', 130, 'CN', 33], + ['US', 130, 'IN', 23], + ['US', 130, 'US', 9], + ['US', 130, 'ID', 7], + ['US', 130, 'BR', 5], + ['ID', 55, 'CN', 9], + ['ID', 55, 'IN', 8], + ['ID', 55, 'ID', 5], + ['ID', 55, 'BR', 4], + ['ID', 55, 'US', 3], + ['PK', 43, 'CN', 8], + ['PK', 43, 'IN', 5], + ['PK', 43, 'US', 5], + ['PK', 43, 'FR', 2], + ['PK', 43, 'PK', 2], +]; export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { + onClick = (elements: Array) => { + // eslint-disable-next-line no-console + console.log(elements[0]); + }; render() { - const dg = new SeededDataGenerator(); - const data = dg.generateGroupedSeries(10, 2).map((item) => ({ - ...item, - y1: item.y + 100, - })); - const lineDatum: LineAnnotationDatum[] = [{ dataValue: 321321 }]; - const rectDatum: RectAnnotationDatum[] = [{ coordinates: { x1: 100 } }]; - return ( <>
- - - - - + + + { + return `${d} $`; + }, + }} /> - +
+
+ + + { + return d[3]; + }} + layers={[ + { + groupByRollup: (d: PieDatum) => { + return d[0]; + }, + nodeLabel: (d) => { + return `dest: ${d}`; + }, + }, + { + groupByRollup: (d: PieDatum) => { + return d[2]; + }, + nodeLabel: (d) => { + return `source: ${d}`; + }, + }, + ]} /> - -
diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..2874bc10f6f76c0519cfbab970623c8edbf11628 GIT binary patch literal 50570 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 z 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

gvf{*glb%Z| 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 H^xys;FRz<6 literal 0 HcmV?d00001 diff --git a/integration/tests/interactions.test.ts b/integration/tests/interactions.test.ts index 69736b4fc8..c393dba4e3 100644 --- a/integration/tests/interactions.test.ts +++ b/integration/tests/interactions.test.ts @@ -76,5 +76,14 @@ describe.only('Tooltips', () => { }, ); }); + it('shows tooltip on sunburst', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/interactions--sunburst-slice-clicks', + { + x: 350, + y: 100, + }, + ); + }); }); }); diff --git a/src/chart_types/partition_chart/specs/index.ts b/src/chart_types/partition_chart/specs/index.ts index c2f8dbf765..64f2637c0c 100644 --- a/src/chart_types/partition_chart/specs/index.ts +++ b/src/chart_types/partition_chart/specs/index.ts @@ -16,6 +16,10 @@ export interface Layer { shape?: { fillColor: string | NodeColorAccessor }; } +export interface LayerValue { + groupByRollup: PrimitiveValue; + value: number; +} const defaultProps = { chartType: ChartTypes.Partition, specType: SpecTypes.Series, diff --git a/src/chart_types/partition_chart/state/chart_state.tsx b/src/chart_types/partition_chart/state/chart_state.tsx index c36f46c4a4..8b4810d26f 100644 --- a/src/chart_types/partition_chart/state/chart_state.tsx +++ b/src/chart_types/partition_chart/state/chart_state.tsx @@ -5,9 +5,15 @@ import { Partition } from '../renderer/canvas/partition'; import { isTooltipVisibleSelector } from '../state/selectors/is_tooltip_visible'; import { getTooltipInfoSelector } from '../state/selectors/tooltip'; import { Tooltip } from '../../../components/tooltip'; +import { createOnElementClickCaller } from './selectors/on_element_click_caller'; const EMPTY_MAP = new Map(); export class PartitionState implements InternalChartState { + onElementClickCaller: (state: GlobalChartState) => void; + + constructor() { + this.onElementClickCaller = createOnElementClickCaller(); + } chartType = ChartTypes.Partition; isBrushAvailable() { return false; @@ -49,4 +55,7 @@ export class PartitionState implements InternalChartState { y1: position.y, }; } + eventCallbacks(globalState: GlobalChartState) { + this.onElementClickCaller(globalState); + } } diff --git a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts new file mode 100644 index 0000000000..4364455bd2 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts @@ -0,0 +1,102 @@ +import createCachedSelector from 're-reselect'; +import { Selector } from 'reselect'; +import { GlobalChartState, PointerState } from '../../../../state/chart_state'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { SettingsSpec } from '../../../../specs'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { getPickedShapes } from './picked_shapes'; +import { getPieSpecOrNull } from './pie_spec'; +import { ChartTypes } from '../../..'; +import { PARENT_KEY, DEPTH_KEY, SORT_INDEX_KEY, AGGREGATE_KEY, CHILDREN_KEY } from '../../layout/utils/group_by_rollup'; +import { LayerValue } from '../../specs'; +import { SeriesIdentifier } from '../../../xy_chart/utils/series'; + +const getLastClickSelector = (state: GlobalChartState) => state.interactions.pointer.lastClick; + +interface Props { + settings: SettingsSpec | undefined; + lastClick: PointerState | null; + pickedShapes: QuadViewModel[]; +} + +function isClicking(prevProps: Props | null, nextProps: Props | null) { + if (nextProps === null) { + return false; + } + if (!nextProps.settings || !nextProps.settings.onElementClick || nextProps.pickedShapes.length === 0) { + return false; + } + const prevLastClick = prevProps !== null ? prevProps.lastClick : null; + const nextLastClick = nextProps !== null ? nextProps.lastClick : null; + + if (prevLastClick === null && nextLastClick !== null) { + return true; + } + if (prevLastClick !== null && nextLastClick !== null && prevLastClick.time !== nextLastClick.time) { + return true; + } + return false; +} + +/** + * Will call the onElementClick listener every time the following preconditions are met: + * - the onElementClick listener is available + * - we have at least one highlighted geometry + * - the pointer state goes from down state to up state + */ +export function createOnElementClickCaller(): (state: GlobalChartState) => void { + let prevProps: Props | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartTypes.Partition) { + selector = createCachedSelector( + [getPieSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapes], + (pieSpec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes: QuadViewModel[]): void => { + const nextProps = { + lastClick, + settings, + pickedShapes, + }; + if (!pieSpec) { + return; + } + if (isClicking(prevProps, nextProps)) { + if (settings && settings.onElementClick) { + const elements = pickedShapes.map<[Array, SeriesIdentifier]>((model) => { + const values: Array = []; + values.push({ + groupByRollup: model.dataName, + value: model.value, + }); + let parent = model.parent; + let index = model.parent.sortIndex; + while (parent[DEPTH_KEY] > 0) { + const value = parent[AGGREGATE_KEY]; + const dataName = parent[PARENT_KEY][CHILDREN_KEY][index][0]; + values.push({ groupByRollup: dataName, value }); + + parent = parent[PARENT_KEY]; + index = parent[SORT_INDEX_KEY]; + } + return [ + values.reverse(), + { + specId: pieSpec.id, + key: `spec{${pieSpec.id}}`, + }, + ]; + }); + settings.onElementClick(elements); + } + } + prevProps = nextProps; + }, + )({ + keySelector: (state: GlobalChartState) => state.chartId, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/src/chart_types/partition_chart/state/selectors/picked_shapes.ts b/src/chart_types/partition_chart/state/selectors/picked_shapes.ts new file mode 100644 index 0000000000..4436d0a97c --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/picked_shapes.ts @@ -0,0 +1,19 @@ +import createCachedSelector from 're-reselect'; +import { partitionGeometries } from './geometries'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { GlobalChartState } from '../../../../state/chart_state'; + +function getCurrentPointerPosition(state: GlobalChartState) { + return state.interactions.pointer.current.position; +} + +export const getPickedShapes = createCachedSelector( + [partitionGeometries, getCurrentPointerPosition], + (geoms, pointerPosition): Array => { + const picker = geoms.pickQuads; + const diskCenter = geoms.diskCenter; + const x = pointerPosition.x - diskCenter.x; + const y = pointerPosition.y - diskCenter.y; + return picker(x, y); + }, +)((state) => state.chartId); diff --git a/src/chart_types/partition_chart/state/selectors/pie_spec.ts b/src/chart_types/partition_chart/state/selectors/pie_spec.ts new file mode 100644 index 0000000000..0189c430b5 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/pie_spec.ts @@ -0,0 +1,10 @@ +import { GlobalChartState } from '../../../../state/chart_state'; +import { getSpecsFromStore } from '../../../../state/utils'; +import { PartitionSpec } from '../../specs'; +import { ChartTypes } from '../../..'; +import { SpecTypes } from '../../../../specs'; + +export function getPieSpecOrNull(state: GlobalChartState): PartitionSpec | null { + const pieSpecs = getSpecsFromStore(state.specs, ChartTypes.Partition, SpecTypes.Series); + return pieSpecs.length > 0 ? pieSpecs[0] : null; +} diff --git a/src/chart_types/partition_chart/state/selectors/tooltip.ts b/src/chart_types/partition_chart/state/selectors/tooltip.ts index 43da080d19..4645d7e27e 100644 --- a/src/chart_types/partition_chart/state/selectors/tooltip.ts +++ b/src/chart_types/partition_chart/state/selectors/tooltip.ts @@ -1,22 +1,9 @@ import createCachedSelector from 're-reselect'; import { GlobalChartState } from '../../../../state/chart_state'; -import { partitionGeometries } from './geometries'; import { INPUT_KEY } from '../../layout/utils/group_by_rollup'; -import { QuadViewModel } from '../../layout/types/viewmodel_types'; import { TooltipInfo } from '../../../../components/tooltip/types'; -import { ChartTypes } from '../../..'; -import { SpecTypes } from '../../../../specs'; -import { getSpecsFromStore } from '../../../../state/utils'; -import { PartitionSpec } from '../../specs'; - -function getCurrentPointerPosition(state: GlobalChartState) { - return state.interactions.pointer.current.position; -} - -function getPieSpecOrNull(state: GlobalChartState): PartitionSpec | null { - const pieSpecs = getSpecsFromStore(state.specs, ChartTypes.Partition, SpecTypes.Series); - return pieSpecs.length > 0 ? pieSpecs[0] : null; -} +import { getPieSpecOrNull } from './pie_spec'; +import { getPickedShapes } from './picked_shapes'; function getValueFormatter(state: GlobalChartState) { return getPieSpecOrNull(state)?.valueFormatter; @@ -32,16 +19,12 @@ const EMPTY_TOOLTIP = Object.freeze({ }); export const getTooltipInfoSelector = createCachedSelector( - [getPieSpecOrNull, partitionGeometries, getCurrentPointerPosition, getValueFormatter, getLabelFormatters], - (pieSpec, geoms, pointerPosition, valueFormatter, labelFormatters): TooltipInfo => { + [getPieSpecOrNull, getPickedShapes, getValueFormatter, getLabelFormatters], + (pieSpec, pickedShapes, valueFormatter, labelFormatters): TooltipInfo => { if (!pieSpec || !valueFormatter || !labelFormatters) { return EMPTY_TOOLTIP; } - const picker = geoms.pickQuads; - const diskCenter = geoms.diskCenter; - const x = pointerPosition.x - diskCenter.x; - const y = pointerPosition.y - diskCenter.y; - const pickedShapes: Array = picker(x, y); + const datumIndices = new Set(); const tooltipInfo: TooltipInfo = { header: null, diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index a1d087b2ec..125079eb96 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -150,7 +150,10 @@ function renderPoints( seriesIdentifier, styleOverrides, }; - mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, pointGeometry); + mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, { + ...pointGeometry, + datum: datum.datum, + }); // use the geometry only if the yDatum in contained in the current yScale domain const isHidden = yDatum === null || (isLogScale && yDatum <= 0); if (!isHidden && yScale.isValueInDomain(yDatum)) { @@ -293,7 +296,10 @@ export function renderBars( seriesIdentifier, seriesStyle, }; - mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, barGeometry); + mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, { + ...barGeometry, + datum: datum.datum, + }); barGeometries.push(barGeometry); }); diff --git a/src/chart_types/xy_chart/state/chart_state.tsx b/src/chart_types/xy_chart/state/chart_state.tsx index 4120f752ee..069edcbc34 100644 --- a/src/chart_types/xy_chart/state/chart_state.tsx +++ b/src/chart_types/xy_chart/state/chart_state.tsx @@ -19,10 +19,31 @@ import { htmlIdGenerator } from '../../../utils/commons'; import { Tooltip } from '../../../components/tooltip'; import { getTooltipAnchorPositionSelector } from './selectors/get_tooltip_position'; import { SeriesKey } from '../utils/series'; +import { createOnElementClickCaller } from './selectors/on_element_click_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; +import { createOnBrushEndCaller } from './selectors/on_brush_end_caller'; +import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller'; export class XYAxisChartState implements InternalChartState { - chartType = ChartTypes.XYAxis; - legendId: string = htmlIdGenerator()('legend'); + onElementClickCaller: (state: GlobalChartState) => void; + onElementOverCaller: (state: GlobalChartState) => void; + onElementOutCaller: (state: GlobalChartState) => void; + onBrushEndCaller: (state: GlobalChartState) => void; + onPointerMoveCaller: (state: GlobalChartState) => void; + chartType: ChartTypes; + legendId: string; + + constructor() { + this.onElementClickCaller = createOnElementClickCaller(); + this.onElementOverCaller = createOnElementOverCaller(); + this.onElementOutCaller = createOnElementOutCaller(); + this.onBrushEndCaller = createOnBrushEndCaller(); + this.onPointerMoveCaller = createOnPointerMoveCaller(); + this.chartType = ChartTypes.XYAxis; + this.legendId = htmlIdGenerator()('legend'); + } + isBrushAvailable(globalState: GlobalChartState) { return isBrushAvailableSelector(globalState); } @@ -62,4 +83,11 @@ export class XYAxisChartState implements InternalChartState { getTooltipAnchor(globalState: GlobalChartState) { return getTooltipAnchorPositionSelector(globalState); } + eventCallbacks(globalState: GlobalChartState) { + this.onElementOverCaller(globalState); + this.onElementOutCaller(globalState); + this.onElementClickCaller(globalState); + this.onBrushEndCaller(globalState); + this.onPointerMoveCaller(globalState); + } } diff --git a/src/components/chart.tsx b/src/components/chart.tsx index 5c702bc249..bb1868f29c 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -13,15 +13,9 @@ import { ChartSize, getChartSize } from '../utils/chart_size'; import { ChartStatus } from './chart_status'; import { chartStoreReducer, GlobalChartState } from '../state/chart_state'; import { isInitialized } from '../state/selectors/is_initialized'; -import { createOnElementOutCaller } from '../chart_types/xy_chart/state/selectors/on_element_out_caller'; -import { createOnElementOverCaller } from '../chart_types/xy_chart/state/selectors/on_element_over_caller'; -import { createOnElementClickCaller } from '../chart_types/xy_chart/state/selectors/on_element_click_caller'; -import { ChartTypes } from '../chart_types/index'; import { getSettingsSpecSelector } from '../state/selectors/get_settings_specs'; -import { createOnBrushEndCaller } from '../chart_types/xy_chart/state/selectors/on_brush_end_caller'; import { onExternalPointerEvent } from '../state/actions/events'; import { PointerEvent } from '../specs'; -import { createOnPointerMoveCaller } from '../chart_types/xy_chart/state/selectors/on_pointer_move_caller'; interface ChartProps { /** The type of rendered @@ -73,11 +67,6 @@ export class Chart extends React.Component { legendPosition: Position.Right, }; - const onElementClickCaller = createOnElementClickCaller(); - const onElementOverCaller = createOnElementOverCaller(); - const onElementOutCaller = createOnElementOutCaller(); - const onBrushEndCaller = createOnBrushEndCaller(); - const onPointerMoveCaller = createOnPointerMoveCaller(); this.unsubscribeToStore = this.chartStore.subscribe(() => { const state = this.chartStore.getState(); if (!isInitialized(state)) { @@ -90,15 +79,9 @@ export class Chart extends React.Component { legendPosition: settings.legendPosition, }); } - - if (state.chartType !== ChartTypes.XYAxis) { - return; + if (state.internalChartState) { + state.internalChartState.eventCallbacks(state); } - onElementOverCaller(state); - onElementOutCaller(state); - onElementClickCaller(state); - onBrushEndCaller(state); - onPointerMoveCaller(state); }); } diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 4725845889..f953e2f95f 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -13,9 +13,13 @@ import { XYChartSeriesIdentifier, SeriesIdentifier } from '../chart_types/xy_cha import { Accessor } from '../utils/accessor'; import { Position, Rendering, Rotation, Color } from '../utils/commons'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; +import { LayerValue } from '../chart_types/partition_chart/specs'; -export type ElementClickListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; -export type ElementOverListener = (elements: Array<[GeometryValue, XYChartSeriesIdentifier]>) => void; +export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; +export type PartitionElementEvent = [Array, SeriesIdentifier]; + +export type ElementClickListener = (elements: Array) => void; +export type ElementOverListener = (elements: Array) => void; export type BrushEndListener = (min: number, max: number) => void; export type LegendItemListener = (series: XYChartSeriesIdentifier | null) => void; export type PointerUpdateListener = (event: PointerEvent) => void; diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index c392883e36..24050fe912 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -83,6 +83,8 @@ export interface InternalChartState { * @param globalState */ getTooltipAnchor(globalState: GlobalChartState): TooltipAnchorPosition | null; + + eventCallbacks(globalState: GlobalChartState): void; } export interface SpecList { diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 47da1b8342..34fedb4384 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -1,7 +1,7 @@ import { $Values } from 'utility-types'; import { BarSeriesStyle, PointStyle, AreaStyle, LineStyle, ArcStyle } from './themes/theme'; import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; -import { Color } from './commons'; +import { Color, Datum } from './commons'; /** * The accessor type @@ -19,7 +19,9 @@ export interface GeometryValue { accessor: BandedAccessorType; } -export type IndexedGeometry = PointGeometry | BarGeometry; +export type IndexedGeometry = (PointGeometry | BarGeometry) & { + datum?: Datum; +}; /** * Array of **range** clippings [x1, x2] to be excluded during rendering diff --git a/stories/interactions/4_sunburst_slice_clicks.tsx b/stories/interactions/4_sunburst_slice_clicks.tsx new file mode 100644 index 0000000000..1b320191fa --- /dev/null +++ b/stories/interactions/4_sunburst_slice_clicks.tsx @@ -0,0 +1,87 @@ +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { Chart, Position, Settings, Partition } from '../../src'; +import { indexInterpolatedFillColor, interpolatorCET2s } from '../utils/utils'; + +const onElementListeners = { + onElementClick: action('onElementClick'), + onElementOver: action('onElementOver'), + onElementOut: action('onElementOut'), +}; +type PieDatum = [string, number, string, number]; +const pieData: Array = [ + ['CN', 301, 'IN', 44], + ['CN', 301, 'US', 24], + ['CN', 301, 'ID', 13], + ['CN', 301, 'BR', 8], + ['IN', 245, 'US', 22], + ['IN', 245, 'BR', 11], + ['IN', 245, 'ID', 10], + ['US', 130, 'CN', 33], + ['US', 130, 'IN', 23], + ['US', 130, 'US', 9], + ['US', 130, 'ID', 7], + ['US', 130, 'BR', 5], + ['ID', 55, 'BR', 4], + ['ID', 55, 'US', 3], + ['PK', 43, 'FR', 2], + ['PK', 43, 'PK', 2], +]; +export const example = () => { + return ( + + + { + return d[3]; + }} + layers={[ + { + groupByRollup: (d: PieDatum) => { + return d[0]; + }, + nodeLabel: (d) => { + return `dest: ${d}`; + }, + shape: { + fillColor: (d) => { + // pick color from color palette based on mean angle - rather distinct colors in the inner ring + return indexInterpolatedFillColor(interpolatorCET2s)(d, (d.x0 + d.x1) / 2 / (2 * Math.PI), []); + }, + }, + }, + { + groupByRollup: (d: PieDatum) => { + return d[2]; + }, + nodeLabel: (d) => { + return `source: ${d}`; + }, + shape: { + fillColor: (d) => { + // pick color from color palette based on mean angle - rather distinct colors in the inner ring + return indexInterpolatedFillColor(interpolatorCET2s)(d, (d.x0 + d.x1) / 2 / (2 * Math.PI), []); + }, + }, + }, + ]} + /> + + ); +}; + +example.story = { + parameters: { + info: { + text: `The \`onElementClick\` receive an argument with the following type definition: \`Array<[Array, SeriesIdentifier]>\`. + +Usually the outer array contains only one item but, in a near future, we will group smaller slices into a single one during the interaction. + +For every clicked slice, you will have an array of \`LayerValue\`s and a \`SeriesIdentifier\`. The array of \`LayerValues\` is sorted +in the same way as the \`layers\` props, and helps you to idenfity the \`groupByRollup\` value and the slice value on every sunburst level. + `, + }, + }, +}; diff --git a/stories/interactions/interactions.stories.tsx b/stories/interactions/interactions.stories.tsx index 4f32edd221..aa5252fb5d 100644 --- a/stories/interactions/interactions.stories.tsx +++ b/stories/interactions/interactions.stories.tsx @@ -11,6 +11,7 @@ export { example as barClicksAndHovers } from './1_bar_clicks'; export { example as areaPointClicksAndHovers } from './2_area_point_clicks'; export { example as linePointClicksAndHovers } from './3_line_point_clicks'; export { example as lineAreaBarPointClicksAndHovers } from './4_line_area_bar_clicks'; +export { example as sunburstSliceClicks } from './4_sunburst_slice_clicks'; export { example as clicksHoversOnLegendItemsBarChart } from './5_clicks_legend_items_bar'; export { example as clickHoversOnLegendItemsAreaChart } from './6_clicks_legend_items_area'; export { example as clickHoversOnLegendItemsLineChart } from './7_clicks_legend_items_line'; From dff69924e52f7249a5e19cf191944c8879b7c1e6 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Wed, 4 Mar 2020 10:33:40 +0100 Subject: [PATCH 2/7] fix: expose missing LayerValue type --- src/chart_types/partition_chart/specs/index.ts | 4 ---- .../state/selectors/on_element_click_caller.ts | 3 +-- src/specs/settings.tsx | 7 ++++++- stories/interactions/4_sunburst_slice_clicks.tsx | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/chart_types/partition_chart/specs/index.ts b/src/chart_types/partition_chart/specs/index.ts index 64f2637c0c..c2f8dbf765 100644 --- a/src/chart_types/partition_chart/specs/index.ts +++ b/src/chart_types/partition_chart/specs/index.ts @@ -16,10 +16,6 @@ export interface Layer { shape?: { fillColor: string | NodeColorAccessor }; } -export interface LayerValue { - groupByRollup: PrimitiveValue; - value: number; -} const defaultProps = { chartType: ChartTypes.Partition, specType: SpecTypes.Series, diff --git a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts index 4364455bd2..5db42e4d4d 100644 --- a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts +++ b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts @@ -2,13 +2,12 @@ import createCachedSelector from 're-reselect'; import { Selector } from 'reselect'; import { GlobalChartState, PointerState } from '../../../../state/chart_state'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { SettingsSpec } from '../../../../specs'; +import { SettingsSpec, LayerValue } from '../../../../specs'; import { QuadViewModel } from '../../layout/types/viewmodel_types'; import { getPickedShapes } from './picked_shapes'; import { getPieSpecOrNull } from './pie_spec'; import { ChartTypes } from '../../..'; import { PARENT_KEY, DEPTH_KEY, SORT_INDEX_KEY, AGGREGATE_KEY, CHILDREN_KEY } from '../../layout/utils/group_by_rollup'; -import { LayerValue } from '../../specs'; import { SeriesIdentifier } from '../../../xy_chart/utils/series'; const getLastClickSelector = (state: GlobalChartState) => state.interactions.pointer.lastClick; diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index f953e2f95f..a1eae7d21a 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -13,7 +13,12 @@ import { XYChartSeriesIdentifier, SeriesIdentifier } from '../chart_types/xy_cha import { Accessor } from '../utils/accessor'; import { Position, Rendering, Rotation, Color } from '../utils/commons'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; -import { LayerValue } from '../chart_types/partition_chart/specs'; +import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; + +export interface LayerValue { + groupByRollup: PrimitiveValue; + value: number; +} export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier]; export type PartitionElementEvent = [Array, SeriesIdentifier]; diff --git a/stories/interactions/4_sunburst_slice_clicks.tsx b/stories/interactions/4_sunburst_slice_clicks.tsx index 1b320191fa..dc49646c65 100644 --- a/stories/interactions/4_sunburst_slice_clicks.tsx +++ b/stories/interactions/4_sunburst_slice_clicks.tsx @@ -27,6 +27,7 @@ const pieData: Array = [ ['PK', 43, 'FR', 2], ['PK', 43, 'PK', 2], ]; + export const example = () => { return ( From 98c0a97c0793a9c1dcd2044a08529717315550f6 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Wed, 4 Mar 2020 15:06:46 +0100 Subject: [PATCH 3/7] test: add missing VRT baseline --- ...ice-clicks-visually-looks-correct-1-snap.png | Bin 0 -> 48155 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-sunburst-slice-clicks-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..04f85d76c2ead8ddd43c3bfec744053515bfc4b1 GIT binary patch literal 48155 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 literal 0 HcmV?d00001 From fe241741f3c66f10043fa4851ebf63a1aca8a18b Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 9 Mar 2020 12:36:05 +0100 Subject: [PATCH 4/7] refactor: cleanup callers --- .../selectors/on_element_click_caller.ts | 59 ++++++++----------- .../state/selectors/picked_shapes.ts | 18 ++++++ .../state/selectors/pie_spec.ts | 18 ++++++ .../xy_chart/rendering/rendering.ts | 10 +--- .../selectors/on_element_click_caller.ts | 41 ++----------- src/state/selectors/get_last_click.ts | 23 ++++++++ src/state/utils.ts | 18 +++++- src/utils/geometry.ts | 6 +- .../interactions/4_sunburst_slice_clicks.tsx | 18 ++++++ 9 files changed, 126 insertions(+), 85 deletions(-) create mode 100644 src/state/selectors/get_last_click.ts diff --git a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts index 5db42e4d4d..73a988dca0 100644 --- a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts +++ b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + import createCachedSelector from 're-reselect'; import { Selector } from 'reselect'; import { GlobalChartState, PointerState } from '../../../../state/chart_state'; @@ -9,33 +27,8 @@ import { getPieSpecOrNull } from './pie_spec'; import { ChartTypes } from '../../..'; import { PARENT_KEY, DEPTH_KEY, SORT_INDEX_KEY, AGGREGATE_KEY, CHILDREN_KEY } from '../../layout/utils/group_by_rollup'; import { SeriesIdentifier } from '../../../xy_chart/utils/series'; - -const getLastClickSelector = (state: GlobalChartState) => state.interactions.pointer.lastClick; - -interface Props { - settings: SettingsSpec | undefined; - lastClick: PointerState | null; - pickedShapes: QuadViewModel[]; -} - -function isClicking(prevProps: Props | null, nextProps: Props | null) { - if (nextProps === null) { - return false; - } - if (!nextProps.settings || !nextProps.settings.onElementClick || nextProps.pickedShapes.length === 0) { - return false; - } - const prevLastClick = prevProps !== null ? prevProps.lastClick : null; - const nextLastClick = nextProps !== null ? nextProps.lastClick : null; - - if (prevLastClick === null && nextLastClick !== null) { - return true; - } - if (prevLastClick !== null && nextLastClick !== null && prevLastClick.time !== nextLastClick.time) { - return true; - } - return false; -} +import { isClicking } from '../../../../state/utils'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; /** * Will call the onElementClick listener every time the following preconditions are met: @@ -44,22 +37,18 @@ function isClicking(prevProps: Props | null, nextProps: Props | null) { * - the pointer state goes from down state to up state */ export function createOnElementClickCaller(): (state: GlobalChartState) => void { - let prevProps: Props | null = null; + let prevClick: PointerState | null = null; let selector: Selector | null = null; return (state: GlobalChartState) => { if (selector === null && state.chartType === ChartTypes.Partition) { selector = createCachedSelector( [getPieSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapes], (pieSpec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes: QuadViewModel[]): void => { - const nextProps = { - lastClick, - settings, - pickedShapes, - }; if (!pieSpec) { return; } - if (isClicking(prevProps, nextProps)) { + const nextPickedShapesLength = pickedShapes.length; + if (nextPickedShapesLength > 0 && isClicking(prevClick, lastClick, settings)) { if (settings && settings.onElementClick) { const elements = pickedShapes.map<[Array, SeriesIdentifier]>((model) => { const values: Array = []; @@ -88,7 +77,7 @@ export function createOnElementClickCaller(): (state: GlobalChartState) => void settings.onElementClick(elements); } } - prevProps = nextProps; + prevClick = lastClick; }, )({ keySelector: (state: GlobalChartState) => state.chartId, diff --git a/src/chart_types/partition_chart/state/selectors/picked_shapes.ts b/src/chart_types/partition_chart/state/selectors/picked_shapes.ts index 4436d0a97c..9f4a235929 100644 --- a/src/chart_types/partition_chart/state/selectors/picked_shapes.ts +++ b/src/chart_types/partition_chart/state/selectors/picked_shapes.ts @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + import createCachedSelector from 're-reselect'; import { partitionGeometries } from './geometries'; import { QuadViewModel } from '../../layout/types/viewmodel_types'; diff --git a/src/chart_types/partition_chart/state/selectors/pie_spec.ts b/src/chart_types/partition_chart/state/selectors/pie_spec.ts index 0189c430b5..7eebf7a3ef 100644 --- a/src/chart_types/partition_chart/state/selectors/pie_spec.ts +++ b/src/chart_types/partition_chart/state/selectors/pie_spec.ts @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + import { GlobalChartState } from '../../../../state/chart_state'; import { getSpecsFromStore } from '../../../../state/utils'; import { PartitionSpec } from '../../specs'; diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index beaa54f2d6..f70f76d46e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -168,10 +168,7 @@ function renderPoints( seriesIdentifier, styleOverrides, }; - mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, { - ...pointGeometry, - datum: datum.datum, - }); + mutableIndexedGeometryMapUpsert(indexedGeometries, xValue, pointGeometry); // use the geometry only if the yDatum in contained in the current yScale domain const isHidden = yDatum === null || (isLogScale && yDatum <= 0); if (!isHidden && yScale.isValueInDomain(yDatum)) { @@ -314,10 +311,7 @@ export function renderBars( seriesIdentifier, seriesStyle, }; - mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, { - ...barGeometry, - datum: datum.datum, - }); + mutableIndexedGeometryMapUpsert(indexedGeometries, datum.x, barGeometry); barGeometries.push(barGeometry); }); diff --git a/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts b/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts index 41c0a091ad..992e7acd86 100644 --- a/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts +++ b/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts @@ -25,33 +25,8 @@ import { SettingsSpec } from '../../../../specs'; import { IndexedGeometry, GeometryValue } from '../../../../utils/geometry'; import { ChartTypes } from '../../../index'; import { XYChartSeriesIdentifier } from '../../utils/series'; - -const getLastClickSelector = (state: GlobalChartState) => state.interactions.pointer.lastClick; - -interface Props { - settings: SettingsSpec | undefined; - lastClick: PointerState | null; - indexedGeometries: IndexedGeometry[]; -} - -function isClicking(prevProps: Props | null, nextProps: Props | null) { - if (nextProps === null) { - return false; - } - if (!nextProps.settings || !nextProps.settings.onElementClick || nextProps.indexedGeometries.length === 0) { - return false; - } - const prevLastClick = prevProps !== null ? prevProps.lastClick : null; - const nextLastClick = nextProps !== null ? nextProps.lastClick : null; - - if (prevLastClick === null && nextLastClick !== null) { - return true; - } - if (prevLastClick !== null && nextLastClick !== null && prevLastClick.time !== nextLastClick.time) { - return true; - } - return false; -} +import { isClicking } from '../../../../state/utils'; +import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; /** * Will call the onElementClick listener every time the following preconditions are met: @@ -60,20 +35,14 @@ function isClicking(prevProps: Props | null, nextProps: Props | null) { * - the pointer state goes from down state to up state */ export function createOnElementClickCaller(): (state: GlobalChartState) => void { - let prevProps: Props | null = null; + let prevClick: PointerState | null = null; let selector: Selector | null = null; return (state: GlobalChartState) => { if (selector === null && state.chartType === ChartTypes.XYAxis) { selector = createCachedSelector( [getLastClickSelector, getSettingsSpecSelector, getHighlightedGeomsSelector], (lastClick: PointerState | null, settings: SettingsSpec, indexedGeometries: IndexedGeometry[]): void => { - const nextProps = { - lastClick, - settings, - indexedGeometries, - }; - - if (isClicking(prevProps, nextProps)) { + if (indexedGeometries.length > 0 && isClicking(prevClick, lastClick, settings)) { if (settings && settings.onElementClick) { const elements = indexedGeometries.map<[GeometryValue, XYChartSeriesIdentifier]>( ({ value, seriesIdentifier }) => [value, seriesIdentifier], @@ -81,7 +50,7 @@ export function createOnElementClickCaller(): (state: GlobalChartState) => void settings.onElementClick(elements); } } - prevProps = nextProps; + prevClick = lastClick; }, )({ keySelector: (state: GlobalChartState) => state.chartId, diff --git a/src/state/selectors/get_last_click.ts b/src/state/selectors/get_last_click.ts new file mode 100644 index 0000000000..e93d517321 --- /dev/null +++ b/src/state/selectors/get_last_click.ts @@ -0,0 +1,23 @@ +import { GlobalChartState } from '../chart_state'; + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +export function getLastClickSelector(state: GlobalChartState) { + return state.interactions.pointer.lastClick; +} diff --git a/src/state/utils.ts b/src/state/utils.ts index caa43fd717..a81ea692f9 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { SpecList } from './chart_state'; -import { Spec } from '../specs'; +import { SpecList, PointerState } from './chart_state'; +import { Spec, SettingsSpec } from '../specs'; import { ChartTypes } from '../chart_types'; export function getSpecsFromStore(specs: SpecList, chartType: ChartTypes, specType?: string): U[] { @@ -32,3 +32,17 @@ export function getSpecsFromStore(specs: SpecList, chartType: Ch return specs[specId] as U; }); } + +export function isClicking(prevClick: PointerState | null, lastClick: PointerState | null, settings: SettingsSpec) { + if (!settings.onElementClick) { + return false; + } + + if (prevClick === null && lastClick !== null) { + return true; + } + if (prevClick !== null && lastClick !== null && prevClick.time !== lastClick.time) { + return true; + } + return false; +} diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 449e58309d..c43042fd6d 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -19,7 +19,7 @@ import { $Values } from 'utility-types'; import { BarSeriesStyle, PointStyle, AreaStyle, LineStyle, ArcStyle } from './themes/theme'; import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; -import { Color, Datum } from './commons'; +import { Color } from './commons'; /** * The accessor type @@ -37,9 +37,7 @@ export interface GeometryValue { accessor: BandedAccessorType; } -export type IndexedGeometry = (PointGeometry | BarGeometry) & { - datum?: Datum; -}; +export type IndexedGeometry = PointGeometry | BarGeometry; /** * Array of **range** clippings [x1, x2] to be excluded during rendering diff --git a/stories/interactions/4_sunburst_slice_clicks.tsx b/stories/interactions/4_sunburst_slice_clicks.tsx index dc49646c65..85f759eefe 100644 --- a/stories/interactions/4_sunburst_slice_clicks.tsx +++ b/stories/interactions/4_sunburst_slice_clicks.tsx @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + import { action } from '@storybook/addon-actions'; import React from 'react'; import { Chart, Position, Settings, Partition } from '../../src'; From 981c66a9bee9181f0a2b7e7037d0d74029558ec1 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 9 Mar 2020 17:04:45 +0100 Subject: [PATCH 5/7] feat: add elementOut and elementOver events --- .../layout/types/config_types.ts | 4 +- .../partition_chart/state/chart_state.tsx | 8 ++ .../selectors/on_element_click_caller.ts | 32 ++----- .../state/selectors/on_element_out_caller.ts | 62 ++++++++++++ .../state/selectors/on_element_over_caller.ts | 95 +++++++++++++++++++ .../state/selectors/picked_shapes.ts | 31 +++++- .../selectors/on_element_click_caller.ts | 5 +- src/state/utils.ts | 8 +- 8 files changed, 211 insertions(+), 34 deletions(-) create mode 100644 src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts create mode 100644 src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts diff --git a/src/chart_types/partition_chart/layout/types/config_types.ts b/src/chart_types/partition_chart/layout/types/config_types.ts index 1e611b54fd..5af1490afb 100644 --- a/src/chart_types/partition_chart/layout/types/config_types.ts +++ b/src/chart_types/partition_chart/layout/types/config_types.ts @@ -22,8 +22,8 @@ import { $Values as Values } from 'utility-types'; import { Color, ValueFormatter } from '../../../../utils/commons'; export const PartitionLayout = Object.freeze({ - sunburst: 'sunburst', - treemap: 'treemap', + sunburst: 'sunburst' as 'sunburst', + treemap: 'treemap' as 'treemap', }); export type PartitionLayout = Values; // could use ValuesType diff --git a/src/chart_types/partition_chart/state/chart_state.tsx b/src/chart_types/partition_chart/state/chart_state.tsx index fb38f2b32e..870cac84f5 100644 --- a/src/chart_types/partition_chart/state/chart_state.tsx +++ b/src/chart_types/partition_chart/state/chart_state.tsx @@ -24,13 +24,19 @@ import { isTooltipVisibleSelector } from '../state/selectors/is_tooltip_visible' import { getTooltipInfoSelector } from '../state/selectors/tooltip'; import { Tooltip } from '../../../components/tooltip'; import { createOnElementClickCaller } from './selectors/on_element_click_caller'; +import { createOnElementOverCaller } from './selectors/on_element_over_caller'; +import { createOnElementOutCaller } from './selectors/on_element_out_caller'; const EMPTY_MAP = new Map(); export class PartitionState implements InternalChartState { onElementClickCaller: (state: GlobalChartState) => void; + onElementOverCaller: (state: GlobalChartState) => void; + onElementOutCaller: (state: GlobalChartState) => void; constructor() { this.onElementClickCaller = createOnElementClickCaller(); + this.onElementOverCaller = createOnElementOverCaller(); + this.onElementOutCaller = createOnElementOutCaller(); } chartType = ChartTypes.Partition; isBrushAvailable() { @@ -74,6 +80,8 @@ export class PartitionState implements InternalChartState { }; } eventCallbacks(globalState: GlobalChartState) { + this.onElementOverCaller(globalState); + this.onElementOutCaller(globalState); this.onElementClickCaller(globalState); } } diff --git a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts index 73a988dca0..fa7670c8ef 100644 --- a/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts +++ b/src/chart_types/partition_chart/state/selectors/on_element_click_caller.ts @@ -21,11 +21,9 @@ import { Selector } from 'reselect'; import { GlobalChartState, PointerState } from '../../../../state/chart_state'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { SettingsSpec, LayerValue } from '../../../../specs'; -import { QuadViewModel } from '../../layout/types/viewmodel_types'; -import { getPickedShapes } from './picked_shapes'; +import { getPickedShapesLayerValues } from './picked_shapes'; import { getPieSpecOrNull } from './pie_spec'; import { ChartTypes } from '../../..'; -import { PARENT_KEY, DEPTH_KEY, SORT_INDEX_KEY, AGGREGATE_KEY, CHILDREN_KEY } from '../../layout/utils/group_by_rollup'; import { SeriesIdentifier } from '../../../xy_chart/utils/series'; import { isClicking } from '../../../../state/utils'; import { getLastClickSelector } from '../../../../state/selectors/get_last_click'; @@ -42,32 +40,20 @@ export function createOnElementClickCaller(): (state: GlobalChartState) => void return (state: GlobalChartState) => { if (selector === null && state.chartType === ChartTypes.Partition) { selector = createCachedSelector( - [getPieSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapes], - (pieSpec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes: QuadViewModel[]): void => { + [getPieSpecOrNull, getLastClickSelector, getSettingsSpecSelector, getPickedShapesLayerValues], + (pieSpec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes): void => { if (!pieSpec) { return; } + if (!settings.onElementClick) { + return; + } const nextPickedShapesLength = pickedShapes.length; - if (nextPickedShapesLength > 0 && isClicking(prevClick, lastClick, settings)) { + if (nextPickedShapesLength > 0 && isClicking(prevClick, lastClick)) { if (settings && settings.onElementClick) { - const elements = pickedShapes.map<[Array, SeriesIdentifier]>((model) => { - const values: Array = []; - values.push({ - groupByRollup: model.dataName, - value: model.value, - }); - let parent = model.parent; - let index = model.parent.sortIndex; - while (parent[DEPTH_KEY] > 0) { - const value = parent[AGGREGATE_KEY]; - const dataName = parent[PARENT_KEY][CHILDREN_KEY][index][0]; - values.push({ groupByRollup: dataName, value }); - - parent = parent[PARENT_KEY]; - index = parent[SORT_INDEX_KEY]; - } + const elements = pickedShapes.map<[Array, SeriesIdentifier]>((values) => { return [ - values.reverse(), + values, { specId: pieSpec.id, key: `spec{${pieSpec.id}}`, diff --git a/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts b/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts new file mode 100644 index 0000000000..5260644303 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/on_element_out_caller.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import createCachedSelector from 're-reselect'; +import { getPickedShapesLayerValues } from './picked_shapes'; +import { getPieSpecOrNull } from './pie_spec'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { Selector } from 'react-redux'; +import { ChartTypes } from '../../../index'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; + +/** + * Will call the onElementOut listener every time the following preconditions are met: + * - the onElementOut listener is available + * - the highlighted geometries list goes from a list of at least one object to an empty one + */ +export function createOnElementOutCaller(): (state: GlobalChartState) => void { + let prevPickedShapes: number | null = null; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartTypes.Partition) { + selector = createCachedSelector( + [getPieSpecOrNull, getPickedShapesLayerValues, getSettingsSpecSelector], + (pieSpec, pickedShapes, settings): void => { + if (!pieSpec) { + return; + } + if (!settings.onElementOut) { + return; + } + const nextPickedShapes = pickedShapes.length; + + if (prevPickedShapes !== null && prevPickedShapes > 0 && nextPickedShapes === 0) { + settings.onElementOut(); + } + prevPickedShapes = nextPickedShapes; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts b/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts new file mode 100644 index 0000000000..6a6a247d73 --- /dev/null +++ b/src/chart_types/partition_chart/state/selectors/on_element_over_caller.ts @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import createCachedSelector from 're-reselect'; +import { LayerValue } from '../../../../specs'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { Selector } from 'react-redux'; +import { ChartTypes } from '../../../index'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getPieSpecOrNull } from './pie_spec'; +import { getPickedShapesLayerValues } from './picked_shapes'; +import { SeriesIdentifier } from '../../../xy_chart/utils/series'; + +function isOverElement(prevPickedShapes: Array> = [], nextPickedShapes: Array>) { + if (nextPickedShapes.length === 0) { + return; + } + if (nextPickedShapes.length !== prevPickedShapes.length) { + return true; + } + return !nextPickedShapes.every((nextPickedShapeValues, index) => { + const prevPickedShapeValues = prevPickedShapes[index]; + if (prevPickedShapeValues === null) { + return false; + } + if (prevPickedShapeValues.length !== nextPickedShapeValues.length) { + return false; + } + return nextPickedShapeValues.every((layerValue, i) => { + const prevPickedValue = prevPickedShapeValues[i]; + if (!prevPickedValue) { + return false; + } + return layerValue.value === prevPickedValue.value && layerValue.groupByRollup === prevPickedValue.groupByRollup; + }); + }); +} + +/** + * Will call the onElementOver listener every time the following preconditions are met: + * - the onElementOver listener is available + * - we have a new set of highlighted geometries on our state + */ +export function createOnElementOverCaller(): (state: GlobalChartState) => void { + let prevPickedShapes: Array> = []; + let selector: Selector | null = null; + return (state: GlobalChartState) => { + if (selector === null && state.chartType === ChartTypes.Partition) { + selector = createCachedSelector( + [getPieSpecOrNull, getPickedShapesLayerValues, getSettingsSpecSelector], + (pieSpec, nextPickedShapes, settings): void => { + if (!pieSpec) { + return; + } + if (!settings.onElementOver) { + return; + } + + if (isOverElement(prevPickedShapes, nextPickedShapes)) { + const elements = nextPickedShapes.map<[Array, SeriesIdentifier]>((values) => [ + values, + { + specId: pieSpec.id, + key: `spec{${pieSpec.id}}`, + }, + ]); + settings.onElementOver(elements); + } + prevPickedShapes = nextPickedShapes; + }, + )({ + keySelector: getChartIdSelector, + }); + } + if (selector) { + selector(state); + } + }; +} diff --git a/src/chart_types/partition_chart/state/selectors/picked_shapes.ts b/src/chart_types/partition_chart/state/selectors/picked_shapes.ts index 9f4a235929..621c644ecc 100644 --- a/src/chart_types/partition_chart/state/selectors/picked_shapes.ts +++ b/src/chart_types/partition_chart/state/selectors/picked_shapes.ts @@ -18,8 +18,10 @@ import createCachedSelector from 're-reselect'; import { partitionGeometries } from './geometries'; -import { QuadViewModel } from '../../layout/types/viewmodel_types'; import { GlobalChartState } from '../../../../state/chart_state'; +import { LayerValue } from '../../../../specs'; +import { PARENT_KEY, DEPTH_KEY, AGGREGATE_KEY, CHILDREN_KEY, SORT_INDEX_KEY } from '../../layout/utils/group_by_rollup'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; function getCurrentPointerPosition(state: GlobalChartState) { return state.interactions.pointer.current.position; @@ -27,7 +29,7 @@ function getCurrentPointerPosition(state: GlobalChartState) { export const getPickedShapes = createCachedSelector( [partitionGeometries, getCurrentPointerPosition], - (geoms, pointerPosition): Array => { + (geoms, pointerPosition): QuadViewModel[] => { const picker = geoms.pickQuads; const diskCenter = geoms.diskCenter; const x = pointerPosition.x - diskCenter.x; @@ -35,3 +37,28 @@ export const getPickedShapes = createCachedSelector( return picker(x, y); }, )((state) => state.chartId); + +export const getPickedShapesLayerValues = createCachedSelector( + [getPickedShapes], + (pickedShapes): Array> => { + const elements = pickedShapes.map>((model) => { + const values: Array = []; + values.push({ + groupByRollup: model.dataName, + value: model.value, + }); + let parent = model[PARENT_KEY]; + let index = model[PARENT_KEY].sortIndex; + while (parent[DEPTH_KEY] > 0) { + const value = parent[AGGREGATE_KEY]; + const dataName = parent[PARENT_KEY][CHILDREN_KEY][index][0]; + values.push({ groupByRollup: dataName, value }); + + parent = parent[PARENT_KEY]; + index = parent[SORT_INDEX_KEY]; + } + return values.reverse(); + }); + return elements; + }, +)((state) => state.chartId); diff --git a/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts b/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts index 992e7acd86..a8d2ec6d0c 100644 --- a/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts +++ b/src/chart_types/xy_chart/state/selectors/on_element_click_caller.ts @@ -42,7 +42,10 @@ export function createOnElementClickCaller(): (state: GlobalChartState) => void selector = createCachedSelector( [getLastClickSelector, getSettingsSpecSelector, getHighlightedGeomsSelector], (lastClick: PointerState | null, settings: SettingsSpec, indexedGeometries: IndexedGeometry[]): void => { - if (indexedGeometries.length > 0 && isClicking(prevClick, lastClick, settings)) { + if (!settings.onElementClick) { + return; + } + if (indexedGeometries.length > 0 && isClicking(prevClick, lastClick)) { if (settings && settings.onElementClick) { const elements = indexedGeometries.map<[GeometryValue, XYChartSeriesIdentifier]>( ({ value, seriesIdentifier }) => [value, seriesIdentifier], diff --git a/src/state/utils.ts b/src/state/utils.ts index a81ea692f9..3b57807ef1 100644 --- a/src/state/utils.ts +++ b/src/state/utils.ts @@ -17,7 +17,7 @@ * under the License. */ import { SpecList, PointerState } from './chart_state'; -import { Spec, SettingsSpec } from '../specs'; +import { Spec } from '../specs'; import { ChartTypes } from '../chart_types'; export function getSpecsFromStore(specs: SpecList, chartType: ChartTypes, specType?: string): U[] { @@ -33,11 +33,7 @@ export function getSpecsFromStore(specs: SpecList, chartType: Ch }); } -export function isClicking(prevClick: PointerState | null, lastClick: PointerState | null, settings: SettingsSpec) { - if (!settings.onElementClick) { - return false; - } - +export function isClicking(prevClick: PointerState | null, lastClick: PointerState | null) { if (prevClick === null && lastClick !== null) { return true; } From 14a4da43701ee4fbeafd5c686141d7381bec8285 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 10 Mar 2020 10:29:39 +0100 Subject: [PATCH 6/7] chore: removed partition from playground --- .playground/playground.tsx | 59 -------------------------------------- 1 file changed, 59 deletions(-) diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 99023395c6..deed09e964 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -25,38 +25,9 @@ import { Settings, PartitionElementEvent, XYChartElementEvent, - Partition, BarSeries, } from '../src'; -type PieDatum = [string, number, string, number]; -const pieData: Array = [ - ['CN', 301, 'CN', 64], - ['CN', 301, 'IN', 44], - ['CN', 301, 'US', 24], - ['CN', 301, 'ID', 13], - ['CN', 301, 'BR', 8], - ['IN', 245, 'IN', 41], - ['IN', 245, 'CN', 36], - ['IN', 245, 'US', 22], - ['IN', 245, 'BR', 11], - ['IN', 245, 'ID', 10], - ['US', 130, 'CN', 33], - ['US', 130, 'IN', 23], - ['US', 130, 'US', 9], - ['US', 130, 'ID', 7], - ['US', 130, 'BR', 5], - ['ID', 55, 'CN', 9], - ['ID', 55, 'IN', 8], - ['ID', 55, 'ID', 5], - ['ID', 55, 'BR', 4], - ['ID', 55, 'US', 3], - ['PK', 43, 'CN', 8], - ['PK', 43, 'IN', 5], - ['PK', 43, 'US', 5], - ['PK', 43, 'FR', 2], - ['PK', 43, 'PK', 2], -]; export class Playground extends React.Component<{}, { isSunburstShown: boolean }> { onClick = (elements: Array) => { // eslint-disable-next-line no-console @@ -104,36 +75,6 @@ export class Playground extends React.Component<{}, { isSunburstShown: boolean } /> -

); } From 6a5d8ca1a25d902ffb11a86c08f4d19f8b392bb5 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 10 Mar 2020 10:31:59 +0100 Subject: [PATCH 7/7] fix: move jest-env as top comment --- .eslintrc.js | 2 +- src/components/chart.test.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d80a2b9b4f..c83645ca79 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -93,7 +93,7 @@ module.exports = { 'under the License.', ], 'block', - ['-\\*-(.*)-\\*-', 'eslint(.*)'], + ['-\\*-(.*)-\\*-', 'eslint(.*)', '@jest-environment'], ], }, settings: { diff --git a/src/components/chart.test.tsx b/src/components/chart.test.tsx index 4dc02b80b9..6be95918b7 100644 --- a/src/components/chart.test.tsx +++ b/src/components/chart.test.tsx @@ -1,3 +1,6 @@ +/** + * @jest-environment node + */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -15,9 +18,6 @@ * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -/** - * @jest-environment node - */ // Using node env because JSDOM environment throws warnings: // Jest doesn't work well with the environment detection hack that react-redux uses internally. // https://github.com/reduxjs/react-redux/issues/1373