Skip to content

Commit

Permalink
feat(partition): Flame and icicle chart (opensearch-project#965)
Browse files Browse the repository at this point in the history
* feat: flame and icicle chart
* chore: no longer shadow variables in treemap and solving other partitioning warnings

Co-authored-by: Nick Partridge <nick.ryan.partridge@gmail.com>
Co-authored-by: Marco Vettorello <vettorello.marco@gmail.com>
  • Loading branch information
3 people committed Jan 13, 2021
1 parent 9bc4b09 commit 9e8b1f7
Show file tree
Hide file tree
Showing 30 changed files with 552 additions and 176 deletions.
5 changes: 3 additions & 2 deletions packages/osd-charts/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ module.exports = {
'global-require': 1,
'import/no-dynamic-require': 1,
'no-shadow': 1,
'no-param-reassign': 1,
'no-param-reassign': [1, { props: false }],
'@typescript-eslint/comma-spacing': 0,
'react/no-array-index-key': 1,
'react/prefer-stateless-function': 1,
'react/require-default-props': 'off',
Expand Down Expand Up @@ -342,7 +343,7 @@ module.exports = {
'prefer-destructuring': [
'warn',
{
array: true,
array: false,
object: true,
},
{
Expand Down
33 changes: 2 additions & 31 deletions packages/osd-charts/.playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,39 +38,10 @@

import React from 'react';

import { Chart, Settings, Partition, PartitionLayout } from '../src';
import { Example } from '../stories/icicle/01_unix_icicle';

export class Playground extends React.Component {
render() {
return (
<div className="chart">
<Chart className="story-chart">
<Settings showLegend flatLegend={false} />
<Partition
id="spec_1"
data={[
{ cat1: 'A', cat2: 'A', val: 1 },
{ cat1: 'A', cat2: 'B', val: 1 },
{ cat1: 'B', cat2: 'A', val: 1 },
{ cat1: 'B', cat2: 'B', val: 1 },
{ cat1: 'C', cat2: 'A', val: 1 },
{ cat1: 'C', cat2: 'B', val: 1 },
]}
valueAccessor={(d: any) => d.val as number}
layers={[
{
groupByRollup: (d: any) => d.cat1,
},
{
groupByRollup: (d: any) => d.cat2,
},
]}
config={{
partitionLayout: PartitionLayout.sunburst,
}}
/>
</Chart>
</div>
);
return <Example />;
}
}
6 changes: 4 additions & 2 deletions packages/osd-charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,8 @@ export interface PartitionLayer {
export const PartitionLayout: Readonly<{
sunburst: "sunburst";
treemap: "treemap";
icicle: "icicle";
flame: "flame";
}>;

// @public (undocumented)
Expand Down Expand Up @@ -1958,8 +1960,8 @@ export type YDomainRange = YDomainBase & DomainRange;
// src/chart_types/heatmap/layout/types/config_types.ts:28: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:60: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:61: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:128: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:129:5 - (ae-forgotten-export) The symbol "AnimKeyframe" needs to be exported by the entry point index.d.ts
// src/chart_types/partition_chart/specs/index.ts:48:13 - (ae-forgotten-export) The symbol "NodeColorAccessor" needs to be exported by the entry point index.d.ts
// src/commons/series_id.ts:39:3 - (ae-forgotten-export) The symbol "SeriesKey" needs to be exported by the entry point index.d.ts

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { Font, FontFamily, PartialFont } from './types';
export const PartitionLayout = Object.freeze({
sunburst: 'sunburst' as const,
treemap: 'treemap' as const,
icicle: 'icicle' as const,
flame: 'flame' as const,
});

/** @public */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ interface MapNode extends NodeDescriptor {

export type PrimitiveValue = string | number | null; // there could be more but sufficient for now
type Key = PrimitiveValue;
type Sorter = (a: number, b: number) => number;

export type Sorter = (a: number, b: number) => number;

type NodeSorter = (a: ArrayEntry, b: ArrayEntry) => number;

export const entryKey = ([key]: ArrayEntry) => key;
Expand Down Expand Up @@ -99,7 +101,7 @@ export function groupByRollup(
const statistics: Statistics = {
globalAggregate: NaN,
};
const reductionMap = factTable.reduce((p: HierarchyOfMaps, n, index) => {
const reductionMap: HierarchyOfMaps = factTable.reduce((p: HierarchyOfMaps, n, index) => {
const keyCount = keyAccessors.length;
let pointer: HierarchyOfMaps = p;
keyAccessors.forEach((keyAccessor, i) => {
Expand Down Expand Up @@ -132,22 +134,22 @@ export function groupByRollup(

function getRootArrayNode(): ArrayNode {
const children: HierarchyOfArrays = [];
const bootstrap = {
const bootstrap: Omit<ArrayNode, typeof PARENT_KEY> = {
[AGGREGATE_KEY]: NaN,
[DEPTH_KEY]: NaN,
[CHILDREN_KEY]: children,
[INPUT_KEY]: [] as number[],
[PATH_KEY]: [] as number[],
[SORT_INDEX_KEY]: 0,
[STATISTICS_KEY]: { globalAggregate: 0 },
};
Object.assign(bootstrap, { [PARENT_KEY]: bootstrap });
const result: ArrayNode = bootstrap as ArrayNode;
return result;
return { ...bootstrap, [PARENT_KEY]: bootstrap } as ArrayNode; // TS doesn't yet handle bootstrapping but the `Omit` above retains guarantee for all props except `[PARENT_KEY`
}

/** @internal */
export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): HierarchyOfArrays {
const groupByMap = (node: HierarchyOfMaps, parent: ArrayNode) =>
Array.from(
export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter | null): HierarchyOfArrays {
const groupByMap = (node: HierarchyOfMaps, parent: ArrayNode) => {
const items = Array.from(
node,
([key, value]: [Key, MapNode]): ArrayEntry => {
const valueElement = value[CHILDREN_KEY];
Expand All @@ -168,12 +170,15 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc
);
return [key, newValue];
},
)
.sort(sorter)
.map((n: ArrayEntry, i) => {
entryValue(n).sortIndex = i;
return n;
}); // with the current algo, decreasing order is important
);
if (sorter !== null) {
items.sort(sorter);
}
return items.map((n: ArrayEntry, i) => {
entryValue(n).sortIndex = i;
return n;
});
}; // with the current algo, decreasing order is important
const tree = groupByMap(root, getRootArrayNode());
const buildPaths = ([, mapNode]: ArrayEntry, currentPath: number[]) => {
const newPath = [...currentPath, mapNode[SORT_INDEX_KEY]];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { ArrayEntry, childrenAccessor, HierarchyOfArrays } from './group_by_roll

/** @internal */
export function sunburst(
nodes: HierarchyOfArrays,
outerNodes: HierarchyOfArrays,
areaAccessor: (e: ArrayEntry) => number,
{ x0, y0 }: Origin,
{ x0: outerX0, y0: outerY0 }: Origin,
clockwiseSectors: boolean,
specialFirstInnermostSector: boolean,
heightStep: number = 1,
): Array<Part> {
const result: Array<Part> = [];
const laySubtree = (nodes: HierarchyOfArrays, { x0, y0 }: Origin, depth: number) => {
Expand All @@ -36,14 +37,14 @@ export function sunburst(
const index = clockwiseSectors ? i : nodeCount - i - 1;
const node = nodes[depth === 1 && specialFirstInnermostSector ? (index + 1) % nodeCount : index];
const area = areaAccessor(node);
result.push({ node, x0: currentOffsetX, y0, x1: currentOffsetX + area, y1: y0 + 1 });
result.push({ node, x0: currentOffsetX, y0, x1: currentOffsetX + area, y1: y0 + heightStep });
const children = childrenAccessor(node);
if (children && children.length) {
laySubtree(children, { x0: currentOffsetX, y0: y0 + 1 }, depth + 1);
if (children.length > 0) {
laySubtree(children, { x0: currentOffsetX, y0: y0 + heightStep }, depth + 1);
}
currentOffsetX += area;
}
};
laySubtree(nodes, { x0, y0 }, 0);
laySubtree(outerNodes, { x0: outerX0, y0: outerY0 }, 0);
return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,25 @@ export function treemap(
areaAccessor: (e: ArrayEntry) => number,
topPaddingAccessor: (e: ArrayEntry) => number,
paddingAccessor: (e: ArrayEntry) => number,
{ x0, y0, width, height }: { x0: number; y0: number; width: number; height: number },
{
x0: outerX0,
y0: outerY0,
width: outerWidth,
height: outerHeight,
}: { x0: number; y0: number; width: number; height: number },
): Array<Part> {
if (nodes.length === 0) return [];
// some bias toward horizontal rectangles with a golden ratio of width to height
const vertical = width / GOLDEN_RATIO <= height;
const independentSize = vertical ? width : height;
const vertical = outerWidth / GOLDEN_RATIO <= outerHeight;
const independentSize = vertical ? outerWidth : outerHeight;
const vectorElements = bestVector(nodes, independentSize, areaAccessor);
const vector = vectorNodeCoordinates(vectorElements, x0, y0, vertical);
const vector = vectorNodeCoordinates(vectorElements, outerX0, outerY0, vertical);
const { dependentSize } = vectorElements;
return vector
.concat(
...vector.map(({ node, x0, y0, x1, y1 }) => {
const childrenNodes = entryValue(node)[CHILDREN_KEY];
if (!childrenNodes || !childrenNodes.length) {
if (childrenNodes.length === 0) {
return [];
}
const fullWidth = x1 - x0;
Expand Down Expand Up @@ -148,8 +153,8 @@ export function treemap(
topPaddingAccessor,
paddingAccessor,
vertical
? { x0, y0: y0 + dependentSize, width, height: height - dependentSize }
: { x0: x0 + dependentSize, y0, width: width - dependentSize, height },
? { x0: outerX0, y0: outerY0 + dependentSize, width: outerWidth, height: outerHeight - dependentSize }
: { x0: outerX0 + dependentSize, y0: outerY0, width: outerWidth - dependentSize, height: outerHeight },
),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import {
groupByRollup,
mapEntryValue,
mapsToArrays,
Sorter,
} from '../utils/group_by_rollup';

export function getHierarchyOfArrays(
rawFacts: Relation,
valueAccessor: ValueAccessor,
groupByRollupAccessors: IndexedAccessorFn[],
sorter: Sorter | null = childOrders.descending,
): HierarchyOfArrays {
const aggregator = aggregators.sum;

Expand All @@ -52,6 +54,6 @@ export function getHierarchyOfArrays(
// size as data value vs size as number of pixels in the rectangle
return mapsToArrays(
groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts),
aggregateComparator(mapEntryValue, childOrders.descending),
sorter && aggregateComparator(mapEntryValue, sorter),
);
}
Loading

0 comments on commit 9e8b1f7

Please sign in to comment.