Skip to content

Commit

Permalink
feat(partition): order slices and sectors (opensearch-project#1112)
Browse files Browse the repository at this point in the history
  • Loading branch information
monfera authored Apr 12, 2021
1 parent 065673c commit 72c0d1b
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 50 deletions.
12 changes: 10 additions & 2 deletions packages/osd-charts/api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export type AccessorFn = UnaryAccessorFn;
// @public
export type AccessorObjectKey = string;

// @public
export type AdditiveNumber = number;

// @public (undocumented)
export const AGGREGATE_KEY = "value";

Expand Down Expand Up @@ -1295,6 +1298,9 @@ export interface NodeDescriptor {
[AGGREGATE_KEY]: number;
}

// @public
export type NodeSorter = (a: ArrayEntry, b: ArrayEntry) => number;

// @public (undocumented)
export type NonAny = number | boolean | string | symbol | null;

Expand Down Expand Up @@ -1356,7 +1362,7 @@ export interface PartitionFillLabel extends LabelConfig {
clipText: boolean;
}

// @public (undocumented)
// @public
export interface PartitionLayer {
// Warning: (ae-forgotten-export) The symbol "ExtendedFillLabelConfig" needs to be exported by the entry point index.d.ts
//
Expand All @@ -1372,6 +1378,8 @@ export interface PartitionLayer {
};
// (undocumented)
showAccessor?: ShowAccessor;
// (undocumented)
sortPredicate?: NodeSorter | null;
}

// @public (undocumented)
Expand Down Expand Up @@ -2055,7 +2063,7 @@ export type UnboundedDomainWithInterval = DomainBase;
export type UpperBoundedDomain = DomainBase & UpperBound;

// @public (undocumented)
export type ValueAccessor = (d: Datum) => number;
export type ValueAccessor = (d: Datum) => AdditiveNumber;

// @public (undocumented)
export type ValueFormatter = (value: number) => string;
Expand Down
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 @@ -79,7 +79,12 @@ export type PrimitiveValue = string | number | null; // there could be more but
export type Key = CategoryKey;
/** @public */
export type Sorter = (a: number, b: number) => number;
type NodeSorter = (a: ArrayEntry, b: ArrayEntry) => number;

/**
* Binary predicate function used for `[].sort`ing partitions represented as ArrayEntries
* @public
*/
export type NodeSorter = (a: ArrayEntry, b: ArrayEntry) => number;

/** @public */
export const entryKey = ([key]: ArrayEntry) => key;
Expand Down Expand Up @@ -109,8 +114,6 @@ export function sortIndexAccessor(n: ArrayEntry) {
export function pathAccessor(n: ArrayEntry) {
return entryValue(n)[PATH_KEY];
}
const ascending: Sorter = (a, b) => a - b;
const descending: Sorter = (a, b) => b - a;

/** @public */
export function getNodeName(node: ArrayNode) {
Expand Down Expand Up @@ -182,7 +185,7 @@ function getRootArrayNode(): ArrayNode {
}

/** @internal */
export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter | null): HierarchyOfArrays {
export function mapsToArrays(root: HierarchyOfMaps, sortSpecs: (NodeSorter | null)[]): HierarchyOfArrays {
const groupByMap = (node: HierarchyOfMaps, parent: ArrayNode) => {
const items = Array.from(
node,
Expand All @@ -206,8 +209,15 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter | null):
return [key, newValue];
},
);
if (sorter !== null) {
items.sort(sorter);
if (sortSpecs.some((s) => s !== null)) {
items.sort((e1: ArrayEntry, e2: ArrayEntry) => {
const node1 = e1[1];
const node2 = e2[1];
if (node1[DEPTH_KEY] !== node2[DEPTH_KEY]) return node1[DEPTH_KEY] - node2[DEPTH_KEY];
const depth = node1[DEPTH_KEY];
const sorterWithinLayer = sortSpecs[depth];
return sorterWithinLayer ? sorterWithinLayer(e1, e2) : node2.value - node1.value;
});
}
return items.map((n: ArrayEntry, i) => {
entryValue(n).sortIndex = i;
Expand All @@ -229,17 +239,6 @@ export function mapEntryValue(entry: ArrayEntry) {
return entryValue(entry)[AGGREGATE_KEY];
}

/** @internal */
export function aggregateComparator(accessor: (v: any) => any, sorter: Sorter): NodeSorter {
return (a, b) => sorter(accessor(a), accessor(b));
}

/** @internal */
export const childOrders = {
ascending,
descending,
};

// type MeanReduction = { sum: number; count: number };
// type MedianReduction = Array<number>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
* under the License.
*/

import { $Values as Values } from 'utility-types';

import { GOLDEN_RATIO } from '../../../../common/constants';
import { Pixels } from '../../../../common/geometry';
import { Part } from '../../../../common/text_utils';
import { ArrayEntry, CHILDREN_KEY, entryValue, HierarchyOfArrays } from './group_by_rollup';
import { ArrayEntry, CHILDREN_KEY, DEPTH_KEY, entryValue, HierarchyOfArrays } from './group_by_rollup';

const MAX_U_PADDING_RATIO = 0.0256197; // this limits area distortion to <10% (which occurs due to pixel padding) with very small rectangles
const MAX_TOP_PADDING_RATIO = 0.33; // this limits further area distortion to ~33%
Expand Down Expand Up @@ -62,7 +64,28 @@ const NullLayoutElement: LayoutElement = {
sectionOffsets: [],
};

function bestVector(nodes: HierarchyOfArrays, height: number, areaAccessor: (e: ArrayEntry) => number): LayoutElement {
/**
* Specifies whether partitions are laid out horizontally, vertically or treemap-like tiling for preferably squarish aspect ratios
* @public
*/
export const LayerLayout = Object.freeze({
horizontal: 'horizontal' as const,
vertical: 'vertical' as const,
squarifying: 'squarifying' as const,
});

/**
* Specifies whether partitions are laid out horizontally, vertically or treemap-like tiling for preferably squarish aspect ratios
* @public
*/
export type LayerLayout = Values<typeof LayerLayout>; // could use ValuesType<typeof HierarchicalChartTypes>

function bestVector(
nodes: HierarchyOfArrays,
height: number,
areaAccessor: (e: ArrayEntry) => number,
layout: LayerLayout,
): LayoutElement {
let previousWorstAspectRatio = -1;
let currentWorstAspectRatio = 0;

Expand All @@ -75,9 +98,9 @@ function bestVector(nodes: HierarchyOfArrays, height: number, areaAccessor: (e:
previousWorstAspectRatio = currentWorstAspectRatio;
currentVectorLayout = layVector(nodes.slice(0, currentCount), height, areaAccessor);
currentWorstAspectRatio = leastSquarishAspectRatio(currentVectorLayout);
} while (currentCount++ < nodes.length && currentWorstAspectRatio > previousWorstAspectRatio);
} while (currentCount++ < nodes.length && (layout || currentWorstAspectRatio > previousWorstAspectRatio));

return currentWorstAspectRatio >= previousWorstAspectRatio ? currentVectorLayout : previousVectorLayout;
return layout || currentWorstAspectRatio >= previousWorstAspectRatio ? currentVectorLayout : previousVectorLayout;
}

function vectorNodeCoordinates(vectorLayout: LayoutElement, x0Base: number, y0Base: number, vertical: boolean) {
Expand Down Expand Up @@ -107,12 +130,15 @@ export function treemap(
width: outerWidth,
height: outerHeight,
}: { x0: number; y0: number; width: number; height: number },
layouts: LayerLayout[],
): Array<Part> {
if (nodes.length === 0) return [];
// some bias toward horizontal rectangles with a golden ratio of width to height
const vertical = outerWidth / GOLDEN_RATIO <= outerHeight;
const depth = nodes[0][1][DEPTH_KEY] - 1;
const layerLayout = layouts[depth] ?? null;
const vertical = layerLayout === LayerLayout.vertical || (!layerLayout && outerWidth / GOLDEN_RATIO <= outerHeight);
const independentSize = vertical ? outerWidth : outerHeight;
const vectorElements = bestVector(nodes, independentSize, areaAccessor);
const vectorElements = bestVector(nodes, independentSize, areaAccessor, layerLayout);
const vector = vectorNodeCoordinates(vectorElements, outerX0, outerY0, vertical);
const { dependentSize } = vectorElements;
return vector
Expand Down Expand Up @@ -143,6 +169,7 @@ export function treemap(
width,
height,
},
layouts,
);
}),
)
Expand All @@ -155,6 +182,7 @@ export function treemap(
vertical
? { x0: outerX0, y0: outerY0 + dependentSize, width: outerWidth, height: outerHeight - dependentSize }
: { x0: outerX0 + dependentSize, y0: outerY0, width: outerWidth - dependentSize, height: outerHeight },
layouts,
),
);
}
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, null);
const outerResult = getHierarchyOfArrays(rawFacts, valueAccessor, groupByRollupAccessors, []);
expect(outerResult.length).toBe(1);

const results = outerResult[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,40 @@ import { Datum, ValueAccessor, ValueFormatter } from '../../../../utils/common';
import { Layer } from '../../specs';
import { PartitionLayout } from '../types/config_types';
import {
aggregateComparator,
aggregators,
childOrders,
CHILDREN_KEY,
groupByRollup,
HIERARCHY_ROOT_KEY,
HierarchyOfArrays,
mapEntryValue,
mapsToArrays,
NodeSorter,
Sorter,
} from '../utils/group_by_rollup';
import { isSunburst, isTreemap } from './viewmodel';

function aggregateComparator(accessor: (v: any) => any, sorter: Sorter): NodeSorter {
return (a, b) => sorter(accessor(a), accessor(b));
}

const ascending: Sorter = (a, b) => a - b;
const descending: Sorter = (a, b) => b - a;

const childOrders = {
ascending,
descending,
};

const descendingValueNodes = aggregateComparator(mapEntryValue, childOrders.descending);

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

Expand All @@ -62,27 +75,26 @@ export function getHierarchyOfArrays(
// We can precompute things invariant of how the rectangle is divvied up.
// 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),
sorter && aggregateComparator(mapEntryValue, sorter),
);
return mapsToArrays(groupByRollup(groupByRollupAccessors, valueAccessor, aggregator, facts), sortSpecs);
}

const sorter = (layout: PartitionLayout) => ({ sortPredicate }: Layer) =>
sortPredicate || (isTreemap(layout) || isSunburst(layout) ? descendingValueNodes : null);

/** @internal */
export function partitionTree(
data: Datum[],
valueAccessor: ValueAccessor,
layers: Layer[],
defaultLayout: PartitionLayout,
layout: PartitionLayout = defaultLayout,
partitionLayout: PartitionLayout = defaultLayout,
) {
const sorter = isTreemap(layout) || isSunburst(layout) ? childOrders.descending : null;
return getHierarchyOfArrays(
data,
valueAccessor,
// eslint-disable-next-line no-shadow
[() => HIERARCHY_ROOT_KEY, ...layers.map(({ groupByRollup }) => groupByRollup)],
sorter,
[null, ...layers.map(sorter(partitionLayout))],
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,19 @@ const rawChildNodes = (
const treemapInnerArea = isTreemap(partitionLayout) ? width * height : 1; // assuming 1 x 1 unit square
const treemapValueToAreaScale = treemapInnerArea / totalValue;
const treemapAreaAccessor = (e: ArrayEntry) => treemapValueToAreaScale * mapEntryValue(e);
return treemap(tree, treemapAreaAccessor, topGrooveAccessor(topGroove), grooveAccessor, {
x0: 0,
y0: 0,
width,
height,
});
return treemap(
tree,
treemapAreaAccessor,
topGrooveAccessor(topGroove),
grooveAccessor,
{
x0: 0,
y0: 0,
width,
height,
},
[],
);

case PartitionLayout.icicle:
case PartitionLayout.flame:
Expand Down
15 changes: 11 additions & 4 deletions packages/osd-charts/src/chart_types/partition_chart/specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ import {
} from '../../../utils/common';
import { config, percentFormatter } from '../layout/config';
import { Config, FillFontSizeRange, FillLabelConfig } from '../layout/types/config_types';
import { ShapeTreeNode, ValueGetter, NodeColorAccessor } from '../layout/types/viewmodel_types';
import { AGGREGATE_KEY, PrimitiveValue } from '../layout/utils/group_by_rollup';
import { NodeColorAccessor, ShapeTreeNode, ValueGetter } from '../layout/types/viewmodel_types';
import { AGGREGATE_KEY, NodeSorter, PrimitiveValue } from '../layout/utils/group_by_rollup';

interface ExtendedFillLabelConfig extends FillLabelConfig, FillFontSizeRange {}

/** @public */
/**
* Specification for a given layer in the partition chart
* @public
*/
export interface Layer {
groupByRollup: IndexedAccessorFn;
sortPredicate?: NodeSorter | null;
nodeLabel?: LabelAccessor;
fillLabel?: Partial<ExtendedFillLabelConfig>;
showAccessor?: ShowAccessor;
Expand All @@ -69,7 +73,10 @@ const defaultProps = {
],
};

/** @public */
/**
* Specifies the partition chart
* @public
*/
export interface PartitionSpec extends Spec {
specType: typeof SpecType.Series;
chartType: typeof ChartType.Partition;
Expand Down
1 change: 1 addition & 0 deletions packages/osd-charts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ export * from './utils/themes/merge_utils';
export { MODEL_KEY } from './chart_types/partition_chart/layout/config';
export { LegendStrategy } from './chart_types/partition_chart/layout/utils/highlighted_geoms';
export { Ratio } from './common/geometry';
export { AdditiveNumber } from './utils/accessor';
9 changes: 6 additions & 3 deletions packages/osd-charts/src/utils/accessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,6 @@ export function getAccessorFormatLabel(accessor: AccessorFormat, label: string):

/**
* Helper function to get accessor value from string, number or function
*
* @param {Datum} datum
* @param {AccessorString|AccessorFn} accessor
* @internal
*/
export function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn) {
Expand All @@ -112,3 +109,9 @@ export function getAccessorValue(datum: Datum, accessor: Accessor | AccessorFn)

return datum[accessor];
}

/**
* Additive numbers: numbers whose semantics are conducive to addition; eg. counts and sums are additive, but averages aren't
* @public
*/
export type AdditiveNumber = number;
3 changes: 2 additions & 1 deletion packages/osd-charts/src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { $Values } from 'utility-types';
import { v1 as uuidV1 } from 'uuid';

import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/group_by_rollup';
import { AdditiveNumber } from './accessor';
import { Point } from './point';

/** @public */
Expand Down Expand Up @@ -465,7 +466,7 @@ export function getUniqueValues<T>(fullArray: T[], uniqueProperty: keyof T, filt
/** @public */
export type ValueFormatter = (value: number) => string;
/** @public */
export type ValueAccessor = (d: Datum) => number;
export type ValueAccessor = (d: Datum) => AdditiveNumber;
/** @public */
export type LabelAccessor = (value: PrimitiveValue) => string;
/** @public */
Expand Down
Loading

0 comments on commit 72c0d1b

Please sign in to comment.