Skip to content

Commit d7fc206

Browse files
authored
feat(partition): add debuggable state (#1117)
Add the debug state for partition charts with the following type signature: fix #917
1 parent c1b59f2 commit d7fc206

File tree

8 files changed

+241
-8
lines changed

8 files changed

+241
-8
lines changed

api/charts.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,10 @@ export interface DebugState {
582582
//
583583
// (undocumented)
584584
lines?: DebugStateLine[];
585+
// Warning: (ae-forgotten-export) The symbol "PartitionDebugState" needs to be exported by the entry point index.d.ts
586+
//
587+
// (undocumented)
588+
partition?: PartitionDebugState[];
585589
}
586590

587591
// @public (undocumented)

src/chart_types/partition_chart/state/chart_state.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { DebugState } from '../../../state/types';
2727
import { Dimensions } from '../../../utils/dimensions';
2828
import { render } from '../renderer/dom/layered_partition_chart';
2929
import { computeLegendSelector } from './selectors/compute_legend';
30+
import { getDebugStateSelector } from './selectors/get_debug_state';
3031
import { getLegendItemsExtra } from './selectors/get_legend_items_extra';
3132
import { getLegendItemsLabels } from './selectors/get_legend_items_labels';
3233
import { isTooltipVisibleSelector } from './selectors/is_tooltip_visible';
@@ -130,8 +131,7 @@ export class PartitionState implements InternalChartState {
130131
return null;
131132
}
132133

133-
// TODO
134-
getDebugState(): DebugState {
135-
return {};
134+
getDebugState(state: GlobalChartState): DebugState {
135+
return getDebugStateSelector(state);
136136
}
137137
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { Store } from 'redux';
21+
22+
import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs';
23+
import { MockStore } from '../../../../mocks/store/store';
24+
import {
25+
HeatmapElementEvent,
26+
LayerValue,
27+
PartitionElementEvent,
28+
XYChartElementEvent,
29+
} from '../../../../specs/settings';
30+
import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse';
31+
import { GlobalChartState } from '../../../../state/chart_state';
32+
import { DebugState, PartitionDebugState, SinglePartitionDebugState } from '../../../../state/types';
33+
import { PartitionLayout } from '../../layout/types/config_types';
34+
import { isSunburst } from '../../layout/viewmodel/viewmodel';
35+
import { getDebugStateSelector } from './get_debug_state';
36+
import { createOnElementClickCaller } from './on_element_click_caller';
37+
38+
describe.each([
39+
[PartitionLayout.sunburst, 9, 9],
40+
[PartitionLayout.treemap, 9, 6],
41+
[PartitionLayout.flame, 9, 6],
42+
[PartitionLayout.icicle, 9, 6],
43+
[PartitionLayout.mosaic, 9, 6],
44+
])('Partition - debug state %s', (partitionLayout, numberOfElements, numberOfCalls) => {
45+
type TestDatum = { cat1: string; cat2: string; val: number };
46+
const specJSON = {
47+
config: {
48+
partitionLayout,
49+
},
50+
data: [
51+
{ cat1: 'Asia', cat2: 'Japan', val: 1 },
52+
{ cat1: 'Asia', cat2: 'China', val: 1 },
53+
{ cat1: 'Europe', cat2: 'Germany', val: 1 },
54+
{ cat1: 'Europe', cat2: 'Italy', val: 1 },
55+
{ cat1: 'North America', cat2: 'United States', val: 1 },
56+
{ cat1: 'North America', cat2: 'Canada', val: 1 },
57+
],
58+
valueAccessor: (d: TestDatum) => d.val,
59+
layers: [
60+
{
61+
groupByRollup: (d: TestDatum) => d.cat1,
62+
},
63+
{
64+
groupByRollup: (d: TestDatum) => d.cat2,
65+
},
66+
],
67+
};
68+
let store: Store<GlobalChartState>;
69+
let onClickListener: jest.Mock<
70+
undefined,
71+
Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]>
72+
>;
73+
let debugState: DebugState;
74+
75+
beforeEach(() => {
76+
onClickListener = jest.fn((): undefined => undefined);
77+
store = MockStore.default({ width: 500, height: 500, top: 0, left: 0 });
78+
const onElementClickCaller = createOnElementClickCaller();
79+
store.subscribe(() => {
80+
onElementClickCaller(store.getState());
81+
});
82+
MockStore.addSpecs(
83+
[
84+
MockSeriesSpec.sunburst(specJSON),
85+
MockGlobalSpec.settings({ debugState: true, onElementClick: onClickListener }),
86+
],
87+
store,
88+
);
89+
debugState = getDebugStateSelector(store.getState());
90+
});
91+
92+
it('can compute debug state', () => {
93+
// small multiple panels
94+
expect(debugState.partition).toHaveLength(1);
95+
// partition sectors
96+
expect(debugState.partition![0].partitions).toHaveLength(numberOfElements);
97+
});
98+
99+
it('can click on every sector', () => {
100+
const [{ partitions }] = debugState.partition as PartitionDebugState[];
101+
let counter = 0;
102+
for (let index = 0; index < partitions.length; index++) {
103+
const partition = partitions[index];
104+
if (!isSunburst(partitionLayout) && partition.depth < 2) {
105+
continue;
106+
}
107+
expectCorrectClickInfo(store, onClickListener, partition, counter);
108+
counter++;
109+
}
110+
expect(onClickListener).toBeCalledTimes(numberOfCalls);
111+
});
112+
});
113+
114+
function expectCorrectClickInfo(
115+
store: Store<GlobalChartState>,
116+
onClickListener: jest.Mock<undefined, Array<(XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent)[]>>,
117+
partition: SinglePartitionDebugState,
118+
index: number,
119+
) {
120+
const {
121+
depth,
122+
value,
123+
name,
124+
coords: [x, y],
125+
} = partition;
126+
127+
store.dispatch(onPointerMove({ x, y }, index * 3));
128+
store.dispatch(onMouseDown({ x, y }, index * 3 + 1));
129+
store.dispatch(onMouseUp({ x, y }, index * 3 + 2));
130+
131+
expect(onClickListener).toBeCalledTimes(index + 1);
132+
const obj = onClickListener.mock.calls[index][0][0][0] as LayerValue[];
133+
// pick the last element of the path
134+
expect(obj[obj.length - 1]).toMatchObject({
135+
depth,
136+
groupByRollup: name,
137+
value,
138+
});
139+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import createCachedSelector from 're-reselect';
21+
22+
import { TAU } from '../../../../common/constants';
23+
import { Pixels, PointObject } from '../../../../common/geometry';
24+
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
25+
import { DebugState, PartitionDebugState } from '../../../../state/types';
26+
import { QuadViewModel } from '../../layout/types/viewmodel_types';
27+
import { isSunburst } from '../../layout/viewmodel/viewmodel';
28+
import { partitionMultiGeometries } from './geometries';
29+
30+
/** @internal */
31+
export const getDebugStateSelector = createCachedSelector(
32+
[partitionMultiGeometries],
33+
(geoms): DebugState => {
34+
return {
35+
partition: geoms.reduce<PartitionDebugState[]>((acc, { panelTitle, config, quadViewModel, diskCenter }) => {
36+
const partitions: PartitionDebugState['partitions'] = quadViewModel.map((model) => {
37+
const { dataName, depth, fillColor, value } = model;
38+
return {
39+
name: dataName,
40+
depth,
41+
color: fillColor,
42+
value,
43+
coords: isSunburst(config.partitionLayout)
44+
? getCoordsForSector(model, diskCenter)
45+
: getCoordsForRectangle(model, diskCenter),
46+
};
47+
});
48+
acc.push({
49+
panelTitle,
50+
partitions,
51+
});
52+
return acc;
53+
}, []),
54+
};
55+
},
56+
)(getChartIdSelector);
57+
58+
function getCoordsForSector({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] {
59+
const X0 = x0 - TAU / 4;
60+
const X1 = x1 - TAU / 4;
61+
const cr = y0px + (y1px - y0px) / 2;
62+
const angle = X0 + (X1 - X0) / 2;
63+
const x = Math.round(Math.cos(angle) * cr + diskCenter.x);
64+
const y = Math.round(Math.sin(angle) * cr + diskCenter.y);
65+
return [x, y];
66+
}
67+
68+
function getCoordsForRectangle({ x0, x1, y1px, y0px }: QuadViewModel, diskCenter: PointObject): [Pixels, Pixels] {
69+
const y = Math.round(y0px + (y1px - y0px) / 2 + diskCenter.y);
70+
const x = Math.round(x0 + (x1 - x0) / 2 + diskCenter.x);
71+
return [x, y];
72+
}

src/components/chart_status.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ class ChartStatusComponent extends React.Component<ChartStatusStateProps> {
6666
}
6767

6868
const mapStateToProps = (state: GlobalChartState): ChartStatusStateProps => {
69-
const settings = getSettingsSpecSelector(state);
69+
const { onRenderChange, debugState } = getSettingsSpecSelector(state);
7070

7171
return {
7272
rendered: state.chartRendered,
7373
renderedCount: state.chartRenderedCount,
74-
onRenderChange: settings.onRenderChange,
75-
debugState: settings.debugState ? getDebugStateSelector(state) : null,
74+
onRenderChange,
75+
debugState: debugState ? getDebugStateSelector(state) : null,
7676
};
7777
};
7878

src/state/types.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import type { Cell } from '../chart_types/heatmap/layout/types/viewmodel_types';
21+
import { Pixels } from '../common/geometry';
2122
import type { Position } from '../utils/common';
2223
import type { GeometryValue } from '../utils/geometry';
2324

@@ -97,6 +98,21 @@ type HeatmapDebugState = {
9798
};
9899
};
99100

101+
/** @public */
102+
export type SinglePartitionDebugState = {
103+
name: string;
104+
depth: number;
105+
color: string;
106+
value: number;
107+
coords: [Pixels, Pixels];
108+
};
109+
110+
/** @public */
111+
export type PartitionDebugState = {
112+
panelTitle: string;
113+
partitions: Array<SinglePartitionDebugState>;
114+
};
115+
100116
/**
101117
* Describes _visible_ chart state for use in functional tests
102118
*
@@ -111,4 +127,5 @@ export interface DebugState {
111127
bars?: DebugStateBar[];
112128
/** Heatmap chart debug state */
113129
heatmap?: HeatmapDebugState;
130+
partition?: PartitionDebugState[];
114131
}

stories/sunburst/10_2_slice.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919

2020
import React from 'react';
2121

22-
import { Chart, Datum, Partition, PartitionLayout } from '../../src';
22+
import { Chart, Datum, Partition, PartitionLayout, Settings } from '../../src';
2323
import { config } from '../../src/chart_types/partition_chart/layout/config';
2424
import { mocks } from '../../src/mocks/hierarchical';
2525
import { indexInterpolatedFillColor, interpolatorCET2s, productLookup } from '../utils/utils';
2626

2727
export const Example = () => (
2828
<Chart className="story-chart">
29+
<Settings debugState />
2930
<Partition
3031
id="spec_1"
3132
data={mocks.pie.slice(0, 2)}

stories/treemap/1_one_layer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const defaultFillColor = (colorMaker: any) => (d: any, i: number, a: any[]) => c
3636

3737
export const Example = () => (
3838
<Chart className="story-chart">
39-
<Settings theme={STORYBOOK_LIGHT_THEME} />
39+
<Settings theme={STORYBOOK_LIGHT_THEME} debugState />
4040
<Partition
4141
id="spec_1"
4242
data={mocks.pie}

0 commit comments

Comments
 (0)