From 08f8baf7a5971a7b98c58da3dbe0ae59ee96daba Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 15 Apr 2021 12:28:41 +0200 Subject: [PATCH] feat(partition): add debuggable state (#1117) Add the debug state for partition charts with the following type signature: fix #917 --- packages/osd-charts/api/charts.api.md | 4 + .../partition_chart/state/chart_state.tsx | 6 +- .../state/selectors/get_debug_state.test.ts | 139 ++++++++++++++++++ .../state/selectors/get_debug_state.ts | 72 +++++++++ .../src/components/chart_status.tsx | 6 +- packages/osd-charts/src/state/types.ts | 17 +++ .../stories/sunburst/10_2_slice.tsx | 3 +- .../stories/treemap/1_one_layer.tsx | 2 +- 8 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts diff --git a/packages/osd-charts/api/charts.api.md b/packages/osd-charts/api/charts.api.md index ae3330e87f5..3797a95ec77 100644 --- a/packages/osd-charts/api/charts.api.md +++ b/packages/osd-charts/api/charts.api.md @@ -582,6 +582,10 @@ export interface DebugState { // // (undocumented) lines?: DebugStateLine[]; + // Warning: (ae-forgotten-export) The symbol "PartitionDebugState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + partition?: PartitionDebugState[]; } // @public (undocumented) diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx index 5b07f9d404c..b9a39b0578f 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx +++ b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx @@ -27,6 +27,7 @@ import { DebugState } from '../../../state/types'; import { Dimensions } from '../../../utils/dimensions'; import { render } from '../renderer/dom/layered_partition_chart'; import { computeLegendSelector } from './selectors/compute_legend'; +import { getDebugStateSelector } from './selectors/get_debug_state'; import { getLegendItemsExtra } from './selectors/get_legend_items_extra'; import { getLegendItemsLabels } from './selectors/get_legend_items_labels'; import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible'; @@ -130,8 +131,7 @@ export class PartitionState implements InternalChartState { return null; } - // TODO - getDebugState(): DebugState { - return {}; + getDebugState(state: GlobalChartState): DebugState { + return getDebugStateSelector(state); } } diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts new file mode 100644 index 00000000000..712f52fd068 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.test.ts @@ -0,0 +1,139 @@ +/* + * 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 { Store } from 'redux'; + +import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs'; +import { MockStore } from '../../../../mocks/store/store'; +import { + HeatmapElementEvent, + LayerValue, + PartitionElementEvent, + XYChartElementEvent, +} from '../../../../specs/settings'; +import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse'; +import { GlobalChartState } from '../../../../state/chart_state'; +import { DebugState, PartitionDebugState, SinglePartitionDebugState } from '../../../../state/types'; +import { PartitionLayout } from '../../layout/types/config_types'; +import { isSunburst } from '../../layout/viewmodel/viewmodel'; +import { getDebugStateSelector } from './get_debug_state'; +import { createOnElementClickCaller } from './on_element_click_caller'; + +describe.each([ + [PartitionLayout.sunburst, 9, 9], + [PartitionLayout.treemap, 9, 6], + [PartitionLayout.flame, 9, 6], + [PartitionLayout.icicle, 9, 6], + [PartitionLayout.mosaic, 9, 6], +])('Partition - debug state %s', (partitionLayout, numberOfElements, numberOfCalls) => { + type TestDatum = { cat1: string; cat2: string; val: number }; + const specJSON = { + config: { + partitionLayout, + }, + data: [ + { cat1: 'Asia', cat2: 'Japan', val: 1 }, + { cat1: 'Asia', cat2: 'China', val: 1 }, + { cat1: 'Europe', cat2: 'Germany', val: 1 }, + { cat1: 'Europe', cat2: 'Italy', val: 1 }, + { cat1: 'North America', cat2: 'United States', val: 1 }, + { cat1: 'North America', cat2: 'Canada', val: 1 }, + ], + valueAccessor: (d: TestDatum) => d.val, + layers: [ + { + groupByRollup: (d: TestDatum) => d.cat1, + }, + { + groupByRollup: (d: TestDatum) => d.cat2, + }, + ], + }; + let store: Store; + let onClickListener: jest.Mock< + undefined, + Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]> + >; + let debugState: DebugState; + + beforeEach(() => { + onClickListener = jest.fn((): undefined => undefined); + store = MockStore.default({ width: 500, height: 500, top: 0, left: 0 }); + const onElementClickCaller = createOnElementClickCaller(); + store.subscribe(() => { + onElementClickCaller(store.getState()); + }); + MockStore.addSpecs( + [ + MockSeriesSpec.sunburst(specJSON), + MockGlobalSpec.settings({ debugState: true, onElementClick: onClickListener }), + ], + store, + ); + debugState = getDebugStateSelector(store.getState()); + }); + + it('can compute debug state', () => { + // small multiple panels + expect(debugState.partition).toHaveLength(1); + // partition sectors + expect(debugState.partition![0].partitions).toHaveLength(numberOfElements); + }); + + it('can click on every sector', () => { + const [{ partitions }] = debugState.partition as PartitionDebugState[]; + let counter = 0; + for (let index = 0; index < partitions.length; index++) { + const partition = partitions[index]; + if (!isSunburst(partitionLayout) && partition.depth < 2) { + continue; + } + expectCorrectClickInfo(store, onClickListener, partition, counter); + counter++; + } + expect(onClickListener).toBeCalledTimes(numberOfCalls); + }); +}); + +function expectCorrectClickInfo( + store: Store, + onClickListener: jest.Mock>, + partition: SinglePartitionDebugState, + index: number, +) { + const { + depth, + value, + name, + coords: [x, y], + } = partition; + + store.dispatch(onPointerMove({ x, y }, index * 3)); + store.dispatch(onMouseDown({ x, y }, index * 3 + 1)); + store.dispatch(onMouseUp({ x, y }, index * 3 + 2)); + + expect(onClickListener).toBeCalledTimes(index + 1); + const obj = onClickListener.mock.calls[index][0][0][0] as LayerValue[]; + // pick the last element of the path + expect(obj[obj.length - 1]).toMatchObject({ + depth, + groupByRollup: name, + value, + }); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts new file mode 100644 index 00000000000..21d51190d11 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_debug_state.ts @@ -0,0 +1,72 @@ +/* + * 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 { TAU } from '../../../../common/constants'; +import { Pixels, PointObject } from '../../../../common/geometry'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { DebugState, PartitionDebugState } from '../../../../state/types'; +import { QuadViewModel } from '../../layout/types/viewmodel_types'; +import { isSunburst } from '../../layout/viewmodel/viewmodel'; +import { partitionMultiGeometries } from './geometries'; + +/** @internal */ +export const getDebugStateSelector = createCachedSelector( + [partitionMultiGeometries], + (geoms): DebugState => { + return { + partition: geoms.reduce((acc, { panelTitle, config, quadViewModel, diskCenter }) => { + const partitions: PartitionDebugState['partitions'] = quadViewModel.map((model) => { + const { dataName, depth, fillColor, value } = model; + return { + name: dataName, + depth, + color: fillColor, + value, + coords: isSunburst(config.partitionLayout) + ? getCoordsForSector(model, diskCenter) + : getCoordsForRectangle(model, diskCenter), + }; + }); + acc.push({ + panelTitle, + partitions, + }); + return acc; + }, []), + }; + }, +)(getChartIdSelector); + +function getCoordsForSector({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] { + const X0 = x0 - TAU / 4; + const X1 = x1 - TAU / 4; + const cr = y0px + (y1px - y0px) / 2; + const angle = X0 + (X1 - X0) / 2; + const x = Math.round(Math.cos(angle) * cr + diskCenter.x); + const y = Math.round(Math.sin(angle) * cr + diskCenter.y); + return [x, y]; +} + +function getCoordsForRectangle({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] { + const y = Math.round(y0px + (y1px - y0px) / 2 + diskCenter.y); + const x = Math.round(x0 + (x1 - x0) / 2 + diskCenter.x); + return [x, y]; +} diff --git a/packages/osd-charts/src/components/chart_status.tsx b/packages/osd-charts/src/components/chart_status.tsx index e68d88573a7..6fa00baff75 100644 --- a/packages/osd-charts/src/components/chart_status.tsx +++ b/packages/osd-charts/src/components/chart_status.tsx @@ -66,13 +66,13 @@ class ChartStatusComponent extends React.Component { } const mapStateToProps = (state: GlobalChartState): ChartStatusStateProps => { - const settings = getSettingsSpecSelector(state); + const { onRenderChange, debugState } = getSettingsSpecSelector(state); return { rendered: state.chartRendered, renderedCount: state.chartRenderedCount, - onRenderChange: settings.onRenderChange, - debugState: settings.debugState ? getDebugStateSelector(state) : null, + onRenderChange, + debugState: debugState ? getDebugStateSelector(state) : null, }; }; diff --git a/packages/osd-charts/src/state/types.ts b/packages/osd-charts/src/state/types.ts index c38612512bd..acc01f92a2f 100644 --- a/packages/osd-charts/src/state/types.ts +++ b/packages/osd-charts/src/state/types.ts @@ -18,6 +18,7 @@ */ import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types'; +import { Pixels } from '../common/geometry'; import type { Position } from '../utils/common'; import type { GeometryValue } from '../utils/geometry'; @@ -97,6 +98,21 @@ type HeatmapDebugState = { }; }; +/** @public */ +export type SinglePartitionDebugState = { + name: string; + depth: number; + color: string; + value: number; + coords: [Pixels, Pixels]; +}; + +/** @public */ +export type PartitionDebugState = { + panelTitle: string; + partitions: Array; +}; + /** * Describes _visible_ chart state for use in functional tests * @@ -111,4 +127,5 @@ export interface DebugState { bars?: DebugStateBar[]; /** Heatmap chart debug state */ heatmap?: HeatmapDebugState; + partition?: PartitionDebugState[]; } diff --git a/packages/osd-charts/stories/sunburst/10_2_slice.tsx b/packages/osd-charts/stories/sunburst/10_2_slice.tsx index e8a75d87816..1faf69f682c 100644 --- a/packages/osd-charts/stories/sunburst/10_2_slice.tsx +++ b/packages/osd-charts/stories/sunburst/10_2_slice.tsx @@ -19,13 +19,14 @@ import React from 'react'; -import { Chart, Datum, Partition, PartitionLayout } from '../../src'; +import { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src'; import { config } from '../../src/chart_types/partition_chart/layout/config'; import { mocks } from '../../src/mocks/hierarchical'; import { indexInterpolatedFillColor, interpolatorCET2s, productLookup } from '../utils/utils'; export const Example = () => ( + (d: any, i: number, a: any[]) => c export const Example = () => ( - +