Skip to content

Commit

Permalink
feat(partition): order slices and sectors (#1112)
Browse files Browse the repository at this point in the history
  • Loading branch information
monfera authored Apr 12, 2021
1 parent ec17cb2 commit 74df29b
Show file tree
Hide file tree
Showing 14 changed files with 239 additions and 50 deletions.
12 changes: 10 additions & 2 deletions 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.
33 changes: 16 additions & 17 deletions src/chart_types/partition_chart/layout/utils/group_by_rollup.ts
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
40 changes: 34 additions & 6 deletions src/chart_types/partition_chart/layout/utils/treemap.ts
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
19 changes: 13 additions & 6 deletions src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts
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 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 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 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 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 74df29b

Please sign in to comment.