Skip to content

Commit 2c1d224

Browse files
authored
fix(treemap): align onElementClick parameters to sunburst (elastic#636)
This commit align the shape of passed parameter of the onElementClick listener to the one passed by a sunburst. For each single hovered shape (usually only one shape at time) it returns the values for each layer of the treemap fix elastic#624
1 parent e9b09ce commit 2c1d224

File tree

7 files changed

+305
-37
lines changed

7 files changed

+305
-37
lines changed

.playground/index.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
/*display: inline-block;
3030
position: relative;
3131
*/
32-
width: 800px;
33-
height: 400px;
32+
width: 300px;
33+
height: 300px;
3434
margin: 20px;
3535
}
3636
</style>

.playground/playground.tsx

+35-2
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,43 @@
1717
* under the License. */
1818

1919
import React from 'react';
20-
import { example } from '../stories/sunburst/12_very_small';
20+
import { Chart, Partition, Settings, PartitionLayout, XYChartElementEvent, PartitionElementEvent } from '../src';
2121

2222
export class Playground extends React.Component {
23+
onElementClick = (elements: (XYChartElementEvent | PartitionElementEvent)[]) => {
24+
// eslint-disable-next-line no-console
25+
console.log(elements);
26+
};
2327
render() {
24-
return <div className="chart">{example()}</div>;
28+
return (
29+
<div className="chart">
30+
<Chart>
31+
<Settings onElementClick={this.onElementClick} />
32+
<Partition
33+
id="111"
34+
config={{
35+
partitionLayout: PartitionLayout.treemap,
36+
}}
37+
valueAccessor={(d: { v: number }) => {
38+
return d.v;
39+
}}
40+
data={[
41+
{ g1: 'a', g2: 'a', v: 1 },
42+
{ g1: 'a', g2: 'b', v: 1 },
43+
{ g1: 'b', g2: 'a', v: 1 },
44+
{ g1: 'b', g2: 'b', v: 1 },
45+
]}
46+
layers={[
47+
{
48+
groupByRollup: (datum: { g1: string }) => datum.g1,
49+
},
50+
{
51+
groupByRollup: (datum: { g2: string }) => datum.g2,
52+
},
53+
]}
54+
/>
55+
</Chart>
56+
</div>
57+
);
2558
}
2659
}

src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts

+22-20
Original file line numberDiff line numberDiff line change
@@ -202,27 +202,9 @@ export function shapeViewModel(
202202
const innerRadius: Radius = outerRadius - (1 - emptySizeRatio) * outerRadius;
203203
const treeHeight = shownChildNodes.reduce((p: number, n: any) => Math.max(p, entryValue(n.node).depth), 0); // 1: pie, 2: two-ring donut etc.
204204
const ringThickness = (outerRadius - innerRadius) / treeHeight;
205-
205+
const partToShapeFn = partToShapeTreeNode(treemapLayout, innerRadius, ringThickness);
206206
const quadViewModel = makeQuadViewModel(
207-
shownChildNodes.slice(1).map(
208-
(n: Part): ShapeTreeNode => {
209-
const node: ArrayEntry = n.node;
210-
return {
211-
dataName: entryKey(node),
212-
depth: depthAccessor(node),
213-
value: aggregateAccessor(node),
214-
parent: parentAccessor(node),
215-
sortIndex: sortIndexAccessor(node),
216-
x0: n.x0,
217-
x1: n.x1,
218-
y0: n.y0,
219-
y1: n.y1,
220-
y0px: treemapLayout ? n.y0 : innerRadius + n.y0 * ringThickness,
221-
y1px: treemapLayout ? n.y1 : innerRadius + n.y1 * ringThickness,
222-
yMidPx: treemapLayout ? (n.y0 + n.y1) / 2 : innerRadius + ((n.y0 + n.y1) / 2) * ringThickness,
223-
};
224-
},
225-
),
207+
shownChildNodes.slice(1).map(partToShapeFn),
226208
layers,
227209
config.sectorLineWidth,
228210
config.sectorLineStroke,
@@ -308,3 +290,23 @@ export function shapeViewModel(
308290
outerRadius,
309291
};
310292
}
293+
294+
function partToShapeTreeNode(treemapLayout: boolean, innerRadius: Radius, ringThickness: number) {
295+
return (n: Part): ShapeTreeNode => {
296+
const node: ArrayEntry = n.node;
297+
return {
298+
dataName: entryKey(node),
299+
depth: depthAccessor(node),
300+
value: aggregateAccessor(node),
301+
parent: parentAccessor(node),
302+
sortIndex: sortIndexAccessor(node),
303+
x0: n.x0,
304+
x1: n.x1,
305+
y0: n.y0,
306+
y1: n.y1,
307+
y0px: treemapLayout ? n.y0 : innerRadius + n.y0 * ringThickness,
308+
y1px: treemapLayout ? n.y1 : innerRadius + n.y1 * ringThickness,
309+
yMidPx: treemapLayout ? (n.y0 + n.y1) / 2 : innerRadius + ((n.y0 + n.y1) / 2) * ringThickness,
310+
};
311+
};
312+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
import { chartStoreReducer, GlobalChartState } from '../../../../state/chart_state';
20+
import { createStore, Store } from 'redux';
21+
import { PartitionSpec } from '../../specs';
22+
import { upsertSpec, specParsed, specParsing } from '../../../../state/actions/specs';
23+
import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs';
24+
import { updateParentDimensions } from '../../../../state/actions/chart_settings';
25+
import { partitionGeometries } from './geometries';
26+
import { onMouseDown, onMouseUp, onPointerMove } from '../../../../state/actions/mouse';
27+
import { createOnElementClickCaller } from './on_element_click_caller';
28+
import { SettingsSpec, XYChartElementEvent, PartitionElementEvent } from '../../../../specs';
29+
30+
describe('Picked shapes selector', () => {
31+
function initStore() {
32+
const storeReducer = chartStoreReducer('chartId');
33+
return createStore(storeReducer);
34+
}
35+
function addSeries(store: Store<GlobalChartState>, spec: PartitionSpec, settings?: Partial<SettingsSpec>) {
36+
store.dispatch(specParsing());
37+
store.dispatch(upsertSpec(MockGlobalSpec.settings(settings)));
38+
store.dispatch(upsertSpec(spec));
39+
store.dispatch(specParsed());
40+
store.dispatch(updateParentDimensions({ width: 300, height: 300, top: 0, left: 0 }));
41+
}
42+
let store: Store<GlobalChartState>;
43+
let treemapSpec: PartitionSpec;
44+
let sunburstSpec: PartitionSpec;
45+
beforeEach(() => {
46+
store = initStore();
47+
const common = {
48+
valueAccessor: (d: { v: number }) => {
49+
return d.v;
50+
},
51+
data: [
52+
{ g1: 'a', g2: 'a', v: 1 },
53+
{ g1: 'a', g2: 'b', v: 1 },
54+
{ g1: 'b', g2: 'a', v: 1 },
55+
{ g1: 'b', g2: 'b', v: 1 },
56+
],
57+
layers: [
58+
{
59+
groupByRollup: (datum: { g1: string }) => datum.g1,
60+
},
61+
{
62+
groupByRollup: (datum: { g2: string }) => datum.g2,
63+
},
64+
],
65+
};
66+
treemapSpec = MockSeriesSpec.treemap(common);
67+
sunburstSpec = MockSeriesSpec.sunburst(common);
68+
});
69+
test('check initial geoms', () => {
70+
addSeries(store, treemapSpec);
71+
const treemapGeometries = partitionGeometries(store.getState());
72+
expect(treemapGeometries.quadViewModel).toHaveLength(6);
73+
74+
addSeries(store, sunburstSpec);
75+
const sunburstGeometries = partitionGeometries(store.getState());
76+
expect(sunburstGeometries.quadViewModel).toHaveLength(6);
77+
});
78+
test('treemap check picked geometries', () => {
79+
const onClickListener = jest.fn<undefined, Array<(XYChartElementEvent | PartitionElementEvent)[]>>(
80+
(): undefined => undefined,
81+
);
82+
addSeries(store, treemapSpec, {
83+
onElementClick: onClickListener,
84+
});
85+
const geometries = partitionGeometries(store.getState());
86+
expect(geometries.quadViewModel).toHaveLength(6);
87+
88+
const onElementClickCaller = createOnElementClickCaller();
89+
store.subscribe(() => {
90+
onElementClickCaller(store.getState());
91+
});
92+
store.dispatch(onPointerMove({ x: 200, y: 200 }, 0));
93+
store.dispatch(onMouseDown({ x: 200, y: 200 }, 1));
94+
store.dispatch(onMouseUp({ x: 200, y: 200 }, 2));
95+
expect(onClickListener).toBeCalled();
96+
expect(onClickListener.mock.calls[0][0]).toEqual([
97+
[
98+
[
99+
{ groupByRollup: 'b', value: 2 },
100+
{ groupByRollup: 'b', value: 1 },
101+
],
102+
{
103+
specId: treemapSpec.id,
104+
key: `spec{${treemapSpec.id}}`,
105+
},
106+
],
107+
]);
108+
});
109+
test('sunburst check picked geometries', () => {
110+
const onClickListener = jest.fn<undefined, Array<(XYChartElementEvent | PartitionElementEvent)[]>>(
111+
(): undefined => undefined,
112+
);
113+
addSeries(store, sunburstSpec, {
114+
onElementClick: onClickListener,
115+
});
116+
const geometries = partitionGeometries(store.getState());
117+
expect(geometries.quadViewModel).toHaveLength(6);
118+
119+
const onElementClickCaller = createOnElementClickCaller();
120+
store.subscribe(() => {
121+
onElementClickCaller(store.getState());
122+
});
123+
store.dispatch(onPointerMove({ x: 200, y: 200 }, 0));
124+
store.dispatch(onMouseDown({ x: 200, y: 200 }, 1));
125+
store.dispatch(onMouseUp({ x: 200, y: 200 }, 2));
126+
expect(onClickListener).toBeCalled();
127+
expect(onClickListener.mock.calls[0][0]).toEqual([
128+
[
129+
[
130+
{ groupByRollup: 'b', value: 2 },
131+
{ groupByRollup: 'b', value: 1 },
132+
],
133+
{
134+
specId: sunburstSpec.id,
135+
key: `spec{${sunburstSpec.id}}`,
136+
},
137+
],
138+
]);
139+
});
140+
});

src/chart_types/partition_chart/state/selectors/picked_shapes.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,17 @@ export const getPickedShapes = createCachedSelector(
4242
/** @internal */
4343
export const getPickedShapesLayerValues = createCachedSelector(
4444
[getPickedShapes],
45-
(pickedShapes): Array<Array<LayerValue>> => {
46-
const elements = pickedShapes.map<Array<LayerValue>>((model) => {
45+
pickShapesLayerValues,
46+
)((state) => state.chartId);
47+
48+
/** @internal */
49+
export function pickShapesLayerValues(pickedShapes: QuadViewModel[]): Array<Array<LayerValue>> {
50+
const maxDepth = pickedShapes.reduce((acc, curr) => {
51+
return Math.max(acc, curr.depth);
52+
}, 0);
53+
const elements = pickedShapes
54+
.filter(({ depth }) => depth === maxDepth)
55+
.map<Array<LayerValue>>((model) => {
4756
const values: Array<LayerValue> = [];
4857
values.push({
4958
groupByRollup: model.dataName,
@@ -61,6 +70,5 @@ export const getPickedShapesLayerValues = createCachedSelector(
6170
}
6271
return values.reverse();
6372
});
64-
return elements;
65-
},
66-
)((state) => state.chartId);
73+
return elements;
74+
}

src/mocks/specs/specs.ts

+65-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ import { ScaleType } from '../../scales';
3232
import { ChartTypes } from '../../chart_types';
3333
import { SettingsSpec, SpecTypes, TooltipType } from '../../specs';
3434
import { LIGHT_THEME } from '../../utils/themes/light_theme';
35+
import { PartitionSpec } from '../../chart_types/partition_chart/specs';
36+
import { config, percentFormatter } from '../../chart_types/partition_chart/layout/config/config';
37+
import { ShapeTreeNode } from '../../chart_types/partition_chart/layout/types/viewmodel_types';
38+
import { Datum } from '../../utils/commons';
39+
import { AGGREGATE_KEY, PrimitiveValue } from '../../chart_types/partition_chart/layout/utils/group_by_rollup';
40+
import { PartitionLayout } from '../../chart_types/partition_chart/layout/types/config_types';
3541

3642
/** @internal */
3743
export class MockSeriesSpec {
@@ -100,6 +106,52 @@ export class MockSeriesSpec {
100106
data: [],
101107
};
102108

109+
private static readonly sunburstBase: PartitionSpec = {
110+
chartType: ChartTypes.Partition,
111+
specType: SpecTypes.Series,
112+
id: 'spec1',
113+
config: {
114+
...config,
115+
partitionLayout: PartitionLayout.sunburst,
116+
},
117+
valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0),
118+
valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY],
119+
valueFormatter: (d: number): string => String(d),
120+
percentFormatter,
121+
layers: [
122+
{
123+
groupByRollup: (d: Datum, i: number) => i,
124+
nodeLabel: (d: PrimitiveValue) => String(d),
125+
showAccessor: () => true,
126+
fillLabel: {},
127+
},
128+
],
129+
data: [],
130+
};
131+
132+
private static readonly treemapBase: PartitionSpec = {
133+
chartType: ChartTypes.Partition,
134+
specType: SpecTypes.Series,
135+
id: 'spec1',
136+
config: {
137+
...config,
138+
partitionLayout: PartitionLayout.treemap,
139+
},
140+
valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0),
141+
valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY],
142+
valueFormatter: (d: number): string => String(d),
143+
percentFormatter,
144+
layers: [
145+
{
146+
groupByRollup: (d: Datum, i: number) => i,
147+
nodeLabel: (d: PrimitiveValue) => String(d),
148+
showAccessor: () => true,
149+
fillLabel: {},
150+
},
151+
],
152+
data: [],
153+
};
154+
103155
static bar(partial?: Partial<BarSeriesSpec>): BarSeriesSpec {
104156
return mergePartial<BarSeriesSpec>(MockSeriesSpec.barBase, partial as RecursivePartial<BarSeriesSpec>, {
105157
mergeOptionalPartialValues: true,
@@ -128,14 +180,25 @@ export class MockSeriesSpec {
128180
});
129181
}
130182

183+
static sunburst(partial?: Partial<PartitionSpec>): PartitionSpec {
184+
return mergePartial<PartitionSpec>(MockSeriesSpec.sunburstBase, partial as RecursivePartial<PartitionSpec>, {
185+
mergeOptionalPartialValues: true,
186+
});
187+
}
188+
189+
static treemap(partial?: Partial<PartitionSpec>): PartitionSpec {
190+
return mergePartial<PartitionSpec>(MockSeriesSpec.treemapBase, partial as RecursivePartial<PartitionSpec>, {
191+
mergeOptionalPartialValues: true,
192+
});
193+
}
194+
131195
static byType(type?: 'line' | 'bar' | 'area'): BasicSeriesSpec {
132196
switch (type) {
133197
case 'line':
134198
return MockSeriesSpec.lineBase;
135-
case 'bar':
136-
return MockSeriesSpec.barBase;
137199
case 'area':
138200
return MockSeriesSpec.areaBase;
201+
case 'bar':
139202
default:
140203
return MockSeriesSpec.barBase;
141204
}

0 commit comments

Comments
 (0)