Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(partition): drilldown #995

Merged
merged 14 commits into from
Feb 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import React from 'react';

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

export class Playground extends React.Component {
render() {
Expand Down
6 changes: 3 additions & 3 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1435,7 +1435,7 @@ export const Partition: React.FunctionComponent<SpecRequiredProps_7 & SpecOption
//
// @public (undocumented)
export interface PartitionConfig extends StaticConfig {
// (undocumented)
// @alpha (undocumented)
animation: {
duration: TimeMs;
keyframes: Array<AnimKeyframe>;
Expand Down Expand Up @@ -2346,8 +2346,8 @@ export type YDomainRange = YDomainBase & DomainRange;
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// src/common/series_id.ts:40:3 - (ae-forgotten-export) The symbol "SeriesKey" needs to be exported by the entry point index.d.ts

// (No @packageDocumentation comment for this package)
Expand Down
4 changes: 4 additions & 0 deletions src/chart_types/partition_chart/layout/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ export const configMetadata: Record<string, ConfigItem> = {
type: 'string',
values: Object.keys(PartitionLayout),
},
drilldown: {
dflt: false,
type: 'boolean',
},

// fill text layout config
circlePadding: { dflt: 2, min: 0.0, max: 8, type: 'number' },
Expand Down
4 changes: 4 additions & 0 deletions src/chart_types/partition_chart/layout/types/config_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export interface StaticConfig extends FillFontSizeRange {
clockwiseSectors: boolean;
specialFirstInnermostSector: boolean;
partitionLayout: PartitionLayout;
/** @alpha */
drilldown: boolean;

// general text config
fontFamily: FontFamily;
Expand Down Expand Up @@ -115,13 +117,15 @@ export interface StaticConfig extends FillFontSizeRange {

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

/** @alpha */
export interface AnimKeyframe {
time: number;
easingFunction: EasingFunction;
keyframeConfig: Partial<StaticConfig>;
}

export interface Config extends StaticConfig {
/** @alpha */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving things back to an alpha version after exporting this as public is not a good practice (we should mark this as breaking change) but I don't think anyone is using that config at the moment so we leave with it now

animation: {
duration: TimeMs;
keyframes: Array<AnimKeyframe>;
Expand Down
49 changes: 30 additions & 19 deletions src/chart_types/partition_chart/layout/utils/group_by_rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,33 +108,44 @@ export function groupByRollup(
identity: () => any;
},
factTable: Relation,
drilldown: boolean,
drilldownSelection: CategoryKey[],
): HierarchyOfMaps {
const statistics: Statistics = {
globalAggregate: NaN,
};
const reductionMap: HierarchyOfMaps = factTable.reduce((p: HierarchyOfMaps, n, index) => {
const keyCount = keyAccessors.length;
let pointer: HierarchyOfMaps = p;
keyAccessors.forEach((keyAccessor, i) => {
const key: Key = keyAccessor(n, index);
const last = i === keyCount - 1;
const node = pointer.get(key);
const inputIndices = node?.[INPUT_KEY] ?? [];
const childrenMap = node?.[CHILDREN_KEY] ?? new Map();
const aggregate = node?.[AGGREGATE_KEY] ?? identity();
const reductionValue = reducer(aggregate, valueAccessor(n));
pointer.set(key, {
[AGGREGATE_KEY]: reductionValue,
[STATISTICS_KEY]: statistics,
[INPUT_KEY]: [...inputIndices, index],
[DEPTH_KEY]: i,
...(!last && { [CHILDREN_KEY]: childrenMap }),
keyAccessors
.filter(
() =>
!drilldown ||
keyAccessors
.slice(0, drilldownSelection.length)
.map((keyAccessor) => keyAccessor(n, index))
.join(' | ') === drilldownSelection.slice(0, drilldownSelection.length).join(' | '),
markov00 marked this conversation as resolved.
Show resolved Hide resolved
)
.forEach((keyAccessor, i) => {
const key: Key = keyAccessor(n, index);
const last = i === keyCount - 1;
const node = pointer.get(key);
const inputIndices = node?.[INPUT_KEY] ?? [];
const childrenMap = node?.[CHILDREN_KEY] ?? new Map();
const aggregate = node?.[AGGREGATE_KEY] ?? identity();
const reductionValue = reducer(aggregate, valueAccessor(n));
pointer.set(key, {
[AGGREGATE_KEY]: reductionValue,
[STATISTICS_KEY]: statistics,
[INPUT_KEY]: [...inputIndices, index],
[DEPTH_KEY]: i,
...(!last && { [CHILDREN_KEY]: childrenMap }),
});
if (childrenMap) {
// will always be true except when exiting from forEach, ie. upon encountering the leaf node
pointer = childrenMap;
}
});
if (childrenMap) {
// will always be true except when exiting from forEach, ie. upon encountering the leaf node
pointer = childrenMap;
}
});
return p;
}, new Map());
if (reductionMap.get(HIERARCHY_ROOT_KEY) !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const groupByRollupAccessors = [() => null, (d: any) => d.sitc1];

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

const results = outerResult[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/

import { CategoryKey } from '../../../../common/category';
import { LegendItemExtraValues } from '../../../../common/legend';
import { SeriesKey } from '../../../../common/series_id';
import { Relation } from '../../../../common/text_utils';
Expand Down Expand Up @@ -44,6 +45,8 @@ export function getHierarchyOfArrays(
valueAccessor: ValueAccessor,
groupByRollupAccessors: IndexedAccessorFn[],
sorter: Sorter | null = childOrders.descending,
drilldown: boolean,
drilldownSelection: CategoryKey[],
): HierarchyOfArrays {
const aggregator = aggregators.sum;

Expand All @@ -61,7 +64,7 @@ export function getHierarchyOfArrays(
// By introducing `scale`, we no longer need to deal with the dichotomy of
// size as data value vs size as number of pixels in the rectangle
return mapsToArrays(
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts),
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts, drilldown, drilldownSelection),
sorter && aggregateComparator(mapEntryValue, sorter),
);
}
Expand All @@ -73,6 +76,8 @@ export function partitionTree(
layers: Layer[],
defaultLayout: PartitionLayout,
layout: PartitionLayout = defaultLayout,
drilldown: boolean,
drilldownSelection: CategoryKey[],
) {
const sorter = isTreemap(layout) || isSunburst(layout) ? childOrders.descending : null;
return getHierarchyOfArrays(
Expand All @@ -81,6 +86,8 @@ export function partitionTree(
// eslint-disable-next-line no-shadow
[() => HIERARCHY_ROOT_KEY, ...layers.map(({ groupByRollup }) => groupByRollup)],
sorter,
drilldown,
drilldownSelection,
);
}

Expand Down
22 changes: 17 additions & 5 deletions src/chart_types/partition_chart/state/selectors/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,33 @@

import createCachedSelector from 're-reselect';

import { CategoryKey } from '../../../../common/category';
import { GlobalChartState } from '../../../../state/chart_state';
import { configMetadata } from '../../layout/config';
import { HierarchyOfArrays } from '../../layout/utils/group_by_rollup';
import { partitionTree } from '../../layout/viewmodel/hierarchy_of_arrays';
import { PartitionSpec } from '../../specs';
import { getPartitionSpecs } from './get_partition_specs';

function getTreeForSpec(spec: PartitionSpec) {
function getTreeForSpec(spec: PartitionSpec, drilldownSelection: CategoryKey[]) {
const { data, valueAccessor, layers, config } = spec;
return partitionTree(data, valueAccessor, layers, configMetadata.partitionLayout.dflt, config.partitionLayout);
return partitionTree(
data,
valueAccessor,
layers,
configMetadata.partitionLayout.dflt,
config.partitionLayout,
Boolean(config.drilldown),
drilldownSelection,
);
}

const getDrilldownSelection = (state: GlobalChartState) => state.interactions.drilldown || [];

/** @internal */
export const getTree = createCachedSelector(
[getPartitionSpecs],
(partitionSpecs): HierarchyOfArrays => {
return partitionSpecs.length > 0 ? getTreeForSpec(partitionSpecs[0]) : []; // singleton!
[getPartitionSpecs, getDrilldownSelection],
(partitionSpecs, drilldownSelection): HierarchyOfArrays => {
return partitionSpecs.length > 0 ? getTreeForSpec(partitionSpecs[0], drilldownSelection) : []; // singleton!
},
)((state) => state.chartId);
5 changes: 4 additions & 1 deletion src/state/chart_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { HeatmapState } from '../chart_types/heatmap/state/chart_state';
import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup';
import { PartitionState } from '../chart_types/partition_chart/state/chart_state';
import { XYAxisChartState } from '../chart_types/xy_chart/state/chart_state';
import { CategoryKey } from '../common/category';
import { LegendItem, LegendItemExtraValues } from '../common/legend';
import { SeriesIdentifier, SeriesKey } from '../common/series_id';
import { TooltipAnchorPosition, TooltipInfo } from '../components/tooltip/types';
Expand Down Expand Up @@ -186,6 +187,7 @@ export interface InteractionsState {
highlightedLegendPath: LegendPath;
deselectedDataSeries: SeriesIdentifier[];
hoveredDOMElement: DOMElement | null;
drilldown: CategoryKey[];
markov00 marked this conversation as resolved.
Show resolved Hide resolved
}

/** @internal */
Expand Down Expand Up @@ -275,6 +277,7 @@ export const getInitialState = (chartId: string): GlobalChartState => ({
highlightedLegendPath: [],
deselectedDataSeries: [],
hoveredDOMElement: null,
drilldown: [],
},
externalEvents: {
pointer: null,
Expand Down Expand Up @@ -391,7 +394,7 @@ export const chartStoreReducer = (chartId: string) => {
return getInternalIsInitializedSelector(state) === InitStatus.Initialized
? {
...state,
interactions: interactionsReducer(state.interactions, action, getLegendItemsSelector(state)),
interactions: interactionsReducer(state, action, getLegendItemsSelector(state)),
}
: state;
}
Expand Down
22 changes: 19 additions & 3 deletions src/state/reducers/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
* under the License.
*/

import { ChartTypes } from '../../chart_types';
import { getPickedShapesLayerValues } from '../../chart_types/partition_chart/state/selectors/picked_shapes';
import { getSeriesIndex } from '../../chart_types/xy_chart/utils/series';
import { LegendItem } from '../../common/legend';
import { SeriesIdentifier } from '../../common/series_id';
import { LayerValue } from '../../specs';
import { getDelta } from '../../utils/point';
import { DOMElementActions, ON_DOM_ELEMENT_ENTER, ON_DOM_ELEMENT_LEAVE } from '../actions/dom_element';
import { KeyActions, ON_KEY_UP } from '../actions/key';
Expand All @@ -31,7 +34,7 @@ import {
ToggleDeselectSeriesAction,
} from '../actions/legend';
import { MouseActions, ON_MOUSE_DOWN, ON_MOUSE_UP, ON_POINTER_MOVE } from '../actions/mouse';
import { InteractionsState } from '../chart_state';
import { GlobalChartState, InteractionsState } from '../chart_state';
import { getInitialPointerState } from '../utils';

/**
Expand All @@ -46,10 +49,11 @@ const DRAG_DETECTION_PIXEL_DELTA = 4;

/** @internal */
export function interactionsReducer(
state: InteractionsState,
globalState: GlobalChartState,
action: LegendActions | MouseActions | KeyActions | DOMElementActions,
legendItems: LegendItem[],
): InteractionsState {
const { interactions: state } = globalState;
switch (action.type) {
case ON_KEY_UP:
if (action.key === 'Escape') {
Expand Down Expand Up @@ -81,6 +85,7 @@ export function interactionsReducer(
case ON_MOUSE_DOWN:
return {
...state,
drilldown: getDrilldownData(globalState),
pointer: {
...state.pointer,
dragging: false,
Expand Down Expand Up @@ -169,7 +174,10 @@ export function interactionsReducer(
}
}

/** @internal */
/**
* Helper functions that currently depend on chart type eg. xy or partition
*/

function toggleDeselectedDataSeries(
{ legendItemId: id, negate }: ToggleDeselectSeriesAction,
deselectedDataSeries: SeriesIdentifier[],
Expand All @@ -194,3 +202,11 @@ function toggleDeselectedDataSeries(
}
return [...deselectedDataSeries, id];
}

function getDrilldownData(globalState: GlobalChartState) {
if (globalState.chartType !== ChartTypes.Partition) {
return [];
}
const layerValues: LayerValue[] = getPickedShapesLayerValues(globalState)[0];
return layerValues ? layerValues[layerValues.length - 1].path.map((n) => n.value) : [];
}
markov00 marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion stories/icicle/01_unix_icicle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Example = () => {
valueAccessor={(d: Datum) => d.value as number}
valueFormatter={() => ''}
layers={getLayerSpec(color)}
config={{ ...config, partitionLayout: PartitionLayout.icicle }}
config={{ ...config, partitionLayout: PartitionLayout.icicle, drilldown: true }}
/>
</Chart>
);
Expand Down
2 changes: 1 addition & 1 deletion stories/icicle/02_unix_flame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const Example = () => {
valueAccessor={(d: Datum) => d.value as number}
valueFormatter={() => ''}
layers={getLayerSpec(color)}
config={{ ...config, partitionLayout: PartitionLayout.flame }}
config={{ ...config, partitionLayout: PartitionLayout.flame, drilldown: true }}
/>
</Chart>
);
Expand Down