Skip to content

Commit 20bbdae

Browse files
monferanickofthyme
andauthored
feat(partition): drilldown (#995)
Co-authored-by: Nick Partridge <nick.ryan.partridge@gmail.com>
1 parent 9694797 commit 20bbdae

12 files changed

Lines changed: 93 additions & 36 deletions

File tree

.playground/playground.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import React from 'react';
2121

22-
import { Example } from '../stories/sunburst/9_sunburst_three_layers';
22+
import { Example } from '../stories/icicle/02_unix_flame';
2323

2424
export class Playground extends React.Component {
2525
render() {

api/charts.api.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,7 +1435,7 @@ export const Partition: React.FunctionComponent<SpecRequiredProps_7 & SpecOption
14351435
//
14361436
// @public (undocumented)
14371437
export interface PartitionConfig extends StaticConfig {
1438-
// (undocumented)
1438+
// @alpha (undocumented)
14391439
animation: {
14401440
duration: TimeMs;
14411441
keyframes: Array<AnimKeyframe>;
@@ -2346,8 +2346,8 @@ export type YDomainRange = YDomainBase & DomainRange;
23462346
// src/chart_types/heatmap/layout/types/config_types.ts:29:13 - (ae-forgotten-export) The symbol "SizeRatio" needs to be exported by the entry point index.d.ts
23472347
// src/chart_types/heatmap/layout/types/config_types.ts:61:5 - (ae-forgotten-export) The symbol "TextAlign" needs to be exported by the entry point index.d.ts
23482348
// src/chart_types/heatmap/layout/types/config_types.ts:62:5 - (ae-forgotten-export) The symbol "TextBaseline" needs to be exported by the entry point index.d.ts
2349-
// src/chart_types/partition_chart/layout/types/config_types.ts:126:5 - (ae-forgotten-export) The symbol "TimeMs" needs to be exported by the entry point index.d.ts
2350-
// src/chart_types/partition_chart/layout/types/config_types.ts:127:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts
2349+
// src/chart_types/partition_chart/layout/types/config_types.ts:130:5 - (ae-forgotten-export) The symbol "TimeMs" needs to be exported by the entry point index.d.ts
2350+
// src/chart_types/partition_chart/layout/types/config_types.ts:131:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts
23512351
// src/common/series_id.ts:40:3 - (ae-forgotten-export) The symbol "SeriesKey" needs to be exported by the entry point index.d.ts
23522352

23532353
// (No @packageDocumentation comment for this package)

src/chart_types/partition_chart/layout/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ export const configMetadata: Record<string, ConfigItem> = {
181181
type: 'string',
182182
values: Object.keys(PartitionLayout),
183183
},
184+
drilldown: {
185+
dflt: false,
186+
type: 'boolean',
187+
},
184188

185189
// fill text layout config
186190
circlePadding: { dflt: 2, min: 0.0, max: 8, type: 'number' },

src/chart_types/partition_chart/layout/types/config_types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ export interface StaticConfig extends FillFontSizeRange {
8888
clockwiseSectors: boolean;
8989
specialFirstInnermostSector: boolean;
9090
partitionLayout: PartitionLayout;
91+
/** @alpha */
92+
drilldown: boolean;
9193

9294
// general text config
9395
fontFamily: FontFamily;
@@ -115,13 +117,15 @@ export interface StaticConfig extends FillFontSizeRange {
115117

116118
export type EasingFunction = (x: Ratio) => Ratio;
117119

120+
/** @alpha */
118121
export interface AnimKeyframe {
119122
time: number;
120123
easingFunction: EasingFunction;
121124
keyframeConfig: Partial<StaticConfig>;
122125
}
123126

124127
export interface Config extends StaticConfig {
128+
/** @alpha */
125129
animation: {
126130
duration: TimeMs;
127131
keyframes: Array<AnimKeyframe>;

src/chart_types/partition_chart/layout/utils/group_by_rollup.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,33 +108,44 @@ export function groupByRollup(
108108
identity: () => any;
109109
},
110110
factTable: Relation,
111+
drilldown: boolean,
112+
drilldownSelection: CategoryKey[],
111113
): HierarchyOfMaps {
112114
const statistics: Statistics = {
113115
globalAggregate: NaN,
114116
};
115117
const reductionMap: HierarchyOfMaps = factTable.reduce((p: HierarchyOfMaps, n, index) => {
116118
const keyCount = keyAccessors.length;
117119
let pointer: HierarchyOfMaps = p;
118-
keyAccessors.forEach((keyAccessor, i) => {
119-
const key: Key = keyAccessor(n, index);
120-
const last = i === keyCount - 1;
121-
const node = pointer.get(key);
122-
const inputIndices = node?.[INPUT_KEY] ?? [];
123-
const childrenMap = node?.[CHILDREN_KEY] ?? new Map();
124-
const aggregate = node?.[AGGREGATE_KEY] ?? identity();
125-
const reductionValue = reducer(aggregate, valueAccessor(n));
126-
pointer.set(key, {
127-
[AGGREGATE_KEY]: reductionValue,
128-
[STATISTICS_KEY]: statistics,
129-
[INPUT_KEY]: [...inputIndices, index],
130-
[DEPTH_KEY]: i,
131-
...(!last && { [CHILDREN_KEY]: childrenMap }),
120+
keyAccessors
121+
.filter(
122+
() =>
123+
!drilldown ||
124+
keyAccessors
125+
.slice(0, drilldownSelection.length)
126+
.map((keyAccessor) => keyAccessor(n, index))
127+
.join(' | ') === drilldownSelection.slice(0, drilldownSelection.length).join(' | '),
128+
)
129+
.forEach((keyAccessor, i) => {
130+
const key: Key = keyAccessor(n, index);
131+
const last = i === keyCount - 1;
132+
const node = pointer.get(key);
133+
const inputIndices = node?.[INPUT_KEY] ?? [];
134+
const childrenMap = node?.[CHILDREN_KEY] ?? new Map();
135+
const aggregate = node?.[AGGREGATE_KEY] ?? identity();
136+
const reductionValue = reducer(aggregate, valueAccessor(n));
137+
pointer.set(key, {
138+
[AGGREGATE_KEY]: reductionValue,
139+
[STATISTICS_KEY]: statistics,
140+
[INPUT_KEY]: [...inputIndices, index],
141+
[DEPTH_KEY]: i,
142+
...(!last && { [CHILDREN_KEY]: childrenMap }),
143+
});
144+
if (childrenMap) {
145+
// will always be true except when exiting from forEach, ie. upon encountering the leaf node
146+
pointer = childrenMap;
147+
}
132148
});
133-
if (childrenMap) {
134-
// will always be true except when exiting from forEach, ie. upon encountering the leaf node
135-
pointer = childrenMap;
136-
}
137-
});
138149
return p;
139150
}, new Map());
140151
if (reductionMap.get(HIERARCHY_ROOT_KEY) !== undefined) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const groupByRollupAccessors = [() => null, (d: any) => d.sitc1];
3232

3333
describe('Test', () => {
3434
test('getHierarchyOfArrays should omit zero and negative values', () => {
35-
const outerResult = getHierarchyOfArrays(rawFacts, valueAccessor, groupByRollupAccessors);
35+
const outerResult = getHierarchyOfArrays(rawFacts, valueAccessor, groupByRollupAccessors, null, false, []);
3636
expect(outerResult.length).toBe(1);
3737

3838
const results = outerResult[0];

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919

20+
import { CategoryKey } from '../../../../common/category';
2021
import { LegendItemExtraValues } from '../../../../common/legend';
2122
import { SeriesKey } from '../../../../common/series_id';
2223
import { Relation } from '../../../../common/text_utils';
@@ -44,6 +45,8 @@ export function getHierarchyOfArrays(
4445
valueAccessor: ValueAccessor,
4546
groupByRollupAccessors: IndexedAccessorFn[],
4647
sorter: Sorter | null = childOrders.descending,
48+
drilldown: boolean,
49+
drilldownSelection: CategoryKey[],
4750
): HierarchyOfArrays {
4851
const aggregator = aggregators.sum;
4952

@@ -61,7 +64,7 @@ export function getHierarchyOfArrays(
6164
// By introducing `scale`, we no longer need to deal with the dichotomy of
6265
// size as data value vs size as number of pixels in the rectangle
6366
return mapsToArrays(
64-
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts),
67+
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts, drilldown, drilldownSelection),
6568
sorter && aggregateComparator(mapEntryValue, sorter),
6669
);
6770
}
@@ -73,6 +76,8 @@ export function partitionTree(
7376
layers: Layer[],
7477
defaultLayout: PartitionLayout,
7578
layout: PartitionLayout = defaultLayout,
79+
drilldown: boolean,
80+
drilldownSelection: CategoryKey[],
7681
) {
7782
const sorter = isTreemap(layout) || isSunburst(layout) ? childOrders.descending : null;
7883
return getHierarchyOfArrays(
@@ -81,6 +86,8 @@ export function partitionTree(
8186
// eslint-disable-next-line no-shadow
8287
[() => HIERARCHY_ROOT_KEY, ...layers.map(({ groupByRollup }) => groupByRollup)],
8388
sorter,
89+
drilldown,
90+
drilldownSelection,
8491
);
8592
}
8693

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,33 @@
1919

2020
import createCachedSelector from 're-reselect';
2121

22+
import { CategoryKey } from '../../../../common/category';
23+
import { GlobalChartState } from '../../../../state/chart_state';
2224
import { configMetadata } from '../../layout/config';
2325
import { HierarchyOfArrays } from '../../layout/utils/group_by_rollup';
2426
import { partitionTree } from '../../layout/viewmodel/hierarchy_of_arrays';
2527
import { PartitionSpec } from '../../specs';
2628
import { getPartitionSpecs } from './get_partition_specs';
2729

28-
function getTreeForSpec(spec: PartitionSpec) {
30+
function getTreeForSpec(spec: PartitionSpec, drilldownSelection: CategoryKey[]) {
2931
const { data, valueAccessor, layers, config } = spec;
30-
return partitionTree(data, valueAccessor, layers, configMetadata.partitionLayout.dflt, config.partitionLayout);
32+
return partitionTree(
33+
data,
34+
valueAccessor,
35+
layers,
36+
configMetadata.partitionLayout.dflt,
37+
config.partitionLayout,
38+
Boolean(config.drilldown),
39+
drilldownSelection,
40+
);
3141
}
3242

43+
const getDrilldownSelection = (state: GlobalChartState) => state.interactions.drilldown || [];
44+
3345
/** @internal */
3446
export const getTree = createCachedSelector(
35-
[getPartitionSpecs],
36-
(partitionSpecs): HierarchyOfArrays => {
37-
return partitionSpecs.length > 0 ? getTreeForSpec(partitionSpecs[0]) : []; // singleton!
47+
[getPartitionSpecs, getDrilldownSelection],
48+
(partitionSpecs, drilldownSelection): HierarchyOfArrays => {
49+
return partitionSpecs.length > 0 ? getTreeForSpec(partitionSpecs[0], drilldownSelection) : []; // singleton!
3850
},
3951
)((state) => state.chartId);

src/state/chart_state.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { HeatmapState } from '../chart_types/heatmap/state/chart_state';
2525
import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup';
2626
import { PartitionState } from '../chart_types/partition_chart/state/chart_state';
2727
import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state';
28+
import { CategoryKey } from '../common/category';
2829
import { LegendItem, LegendItemExtraValues } from '../common/legend';
2930
import { SeriesIdentifier, SeriesKey } from '../common/series_id';
3031
import { TooltipAnchorPosition, TooltipInfo } from '../components/tooltip/types';
@@ -186,6 +187,7 @@ export interface InteractionsState {
186187
highlightedLegendPath: LegendPath;
187188
deselectedDataSeries: SeriesIdentifier[];
188189
hoveredDOMElement: DOMElement | null;
190+
drilldown: CategoryKey[];
189191
}
190192

191193
/** @internal */
@@ -275,6 +277,7 @@ export const getInitialState = (chartId: string): GlobalChartState => ({
275277
highlightedLegendPath: [],
276278
deselectedDataSeries: [],
277279
hoveredDOMElement: null,
280+
drilldown: [],
278281
},
279282
externalEvents: {
280283
pointer: null,
@@ -391,7 +394,7 @@ export const chartStoreReducer = (chartId: string) => {
391394
return getInternalIsInitializedSelector(state) === InitStatus.Initialized
392395
? {
393396
...state,
394-
interactions: interactionsReducer(state.interactions, action, getLegendItemsSelector(state)),
397+
interactions: interactionsReducer(state, action, getLegendItemsSelector(state)),
395398
}
396399
: state;
397400
}

src/state/reducers/interactions.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
* under the License.
1818
*/
1919

20+
import { ChartTypes } from '../../chart_types';
21+
import { getPickedShapesLayerValues } from '../../chart_types/partition_chart/state/selectors/picked_shapes';
2022
import { getSeriesIndex } from '../../chart_types/xy_chart/utils/series';
2123
import { LegendItem } from '../../common/legend';
2224
import { SeriesIdentifier } from '../../common/series_id';
25+
import { LayerValue } from '../../specs';
2326
import { getDelta } from '../../utils/point';
2427
import { DOMElementActions, ON_DOM_ELEMENT_ENTER, ON_DOM_ELEMENT_LEAVE } from '../actions/dom_element';
2528
import { KeyActions, ON_KEY_UP } from '../actions/key';
@@ -31,7 +34,7 @@ import {
3134
ToggleDeselectSeriesAction,
3235
} from '../actions/legend';
3336
import { MouseActions, ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE } from '../actions/mouse';
34-
import { InteractionsState } from '../chart_state';
37+
import { GlobalChartState, InteractionsState } from '../chart_state';
3538
import { getInitialPointerState } from '../utils';
3639

3740
/**
@@ -46,10 +49,11 @@ const DRAG_DETECTION_PIXEL_DELTA = 4;
4649

4750
/** @internal */
4851
export function interactionsReducer(
49-
state: InteractionsState,
52+
globalState: GlobalChartState,
5053
action: LegendActions | MouseActions | KeyActions | DOMElementActions,
5154
legendItems: LegendItem[],
5255
): InteractionsState {
56+
const { interactions: state } = globalState;
5357
switch (action.type) {
5458
case ON_KEY_UP:
5559
if (action.key === 'Escape') {
@@ -81,6 +85,7 @@ export function interactionsReducer(
8185
case ON_MOUSE_DOWN:
8286
return {
8387
...state,
88+
drilldown: getDrilldownData(globalState),
8489
pointer: {
8590
...state.pointer,
8691
dragging: false,
@@ -169,7 +174,10 @@ export function interactionsReducer(
169174
}
170175
}
171176

172-
/** @internal */
177+
/**
178+
* Helper functions that currently depend on chart type eg. xy or partition
179+
*/
180+
173181
function toggleDeselectedDataSeries(
174182
{ legendItemId: id, negate }: ToggleDeselectSeriesAction,
175183
deselectedDataSeries: SeriesIdentifier[],
@@ -194,3 +202,11 @@ function toggleDeselectedDataSeries(
194202
}
195203
return [...deselectedDataSeries, id];
196204
}
205+
206+
function getDrilldownData(globalState: GlobalChartState) {
207+
if (globalState.chartType !== ChartTypes.Partition) {
208+
return [];
209+
}
210+
const layerValues: LayerValue[] = getPickedShapesLayerValues(globalState)[0];
211+
return layerValues ? layerValues[layerValues.length - 1].path.map((n) => n.value) : [];
212+
}

0 commit comments

Comments
 (0)