Skip to content

Commit

Permalink
feat(partition): drilldown (opensearch-project#995)
Browse files Browse the repository at this point in the history
Co-authored-by: Nick Partridge <nick.ryan.partridge@gmail.com>
  • Loading branch information
monfera and nickofthyme authored Feb 10, 2021
1 parent 89e8a64 commit de0cba6
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 36 deletions.
2 changes: 1 addition & 1 deletion packages/osd-charts/.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 packages/osd-charts/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
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
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 */
animation: {
duration: TimeMs;
keyframes: Array<AnimKeyframe>;
Expand Down
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(' | '),
)
.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
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 packages/osd-charts/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[];
}

/** @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 packages/osd-charts/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) : [];
}
2 changes: 1 addition & 1 deletion packages/osd-charts/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 packages/osd-charts/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

0 comments on commit de0cba6

Please sign in to comment.