Skip to content

Commit

Permalink
feat: percentage display in partitioning charts (opensearch-project#558)
Browse files Browse the repository at this point in the history
This commit adds the option to display a ratio or a percent rather than the value in data that's the basis for aggregation.
The ratio is computed as the value of the sector or treemap square divided by the total sum of the value.
The percentage can be displayed on the pie slice directly instead of the sector/rectangle value. On the tooltip both values are displayed.

Co-authored-by: Marco Vettorello <[email protected]>
  • Loading branch information
monfera and markov00 authored Mar 11, 2020
1 parent 4189573 commit 993a448
Show file tree
Hide file tree
Showing 18 changed files with 322 additions and 55 deletions.
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.
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 @@ -20,6 +20,8 @@ import { palettes } from '../../../../mocks/hierarchical/palettes';
import { Config, PartitionLayout, Numeric } from '../types/config_types';
import { GOLDEN_RATIO, TAU } from '../utils/math';
import { FONT_STYLES, FONT_VARIANTS } from '../types/types';
import { ShapeTreeNode } from '../types/viewmodel_types';
import { AGGREGATE_KEY, STATISTICS_KEY } from '../utils/group_by_rollup';

const log10 = Math.log(10);
function significantDigitCount(d: number): number {
Expand All @@ -29,18 +31,33 @@ function significantDigitCount(d: number): number {
return Math.floor(Math.log(n) / log10) + 1;
}

function defaultFormatter(d: any): string {
return typeof d === 'string'
? d
: typeof d === 'number'
? Math.abs(d) >= 10000000 || Math.abs(d) < 0.001
? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1)))
: d.toLocaleString(void 0, {
maximumSignificantDigits: 4,
maximumFractionDigits: 3,
useGrouping: true,
})
: String(d);
export function sumValueGetter(node: ShapeTreeNode): number {
return node[AGGREGATE_KEY];
}

export function percentValueGetter(node: ShapeTreeNode): number {
return (100 * node[AGGREGATE_KEY]) / node.parent[STATISTICS_KEY].globalAggregate;
}

export function ratioValueGetter(node: ShapeTreeNode): number {
return node[AGGREGATE_KEY] / node.parent[STATISTICS_KEY].globalAggregate;
}

export const VALUE_GETTERS = Object.freeze({ percent: percentValueGetter, ratio: ratioValueGetter } as const);
export type ValueGetterName = keyof typeof VALUE_GETTERS;

function defaultFormatter(d: number): string {
return Math.abs(d) >= 10000000 || Math.abs(d) < 0.001
? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1)))
: d.toLocaleString(void 0, {
maximumSignificantDigits: 4,
maximumFractionDigits: 3,
useGrouping: true,
});
}

export function percentFormatter(d: number): string {
return `${Math.round(d)}%`;
}

const valueFont = {
Expand Down Expand Up @@ -154,6 +171,10 @@ export const configMetadata = {
type: 'string',
values: FONT_VARIANTS,
},
valueGetter: {
dflt: sumValueGetter,
type: 'function',
},
valueFormatter: {
dflt: defaultFormatter,
type: 'function',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { Config } from './config_types';
import { Coordinate, Distance, Pixels, PointObject, PointTuple, Radian } from './geometry_types';
import { Font } from './types';
import { config } from '../config/config';
import { config, ValueGetterName } from '../config/config';
import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup';
import { Color } from '../../../../utils/commons';

Expand Down Expand Up @@ -124,4 +124,6 @@ export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY {
}

export type RawTextGetter = (node: ShapeTreeNode) => string;
export type ValueGetterFunction = (node: ShapeTreeNode) => number;
export type ValueGetter = ValueGetterFunction | ValueGetterName;
export type NodeColorAccessor = (d: ShapeTreeNode, index: number, array: HierarchyOfArrays) => string;
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,22 @@
import { Relation } from '../types/types';
import { Datum } from '../../../../utils/commons';

export const AGGREGATE_KEY = 'value'; // todo later switch back to 'aggregate'
export const AGGREGATE_KEY = 'value';
export const STATISTICS_KEY = 'statistics';
export const DEPTH_KEY = 'depth';
export const CHILDREN_KEY = 'children';
export const INPUT_KEY = 'inputIndex';
export const PARENT_KEY = 'parent';
export const SORT_INDEX_KEY = 'sortIndex';

interface Statistics {
globalAggregate: number;
}

interface NodeDescriptor {
[AGGREGATE_KEY]: number;
[DEPTH_KEY]: number;
[STATISTICS_KEY]: Statistics;
[INPUT_KEY]?: Array<number>;
}

Expand Down Expand Up @@ -82,7 +88,10 @@ export function groupByRollup(
identity: Function;
},
factTable: Relation,
) {
): HierarchyOfMaps {
const statistics: Statistics = {
globalAggregate: NaN,
};
const reductionMap = factTable.reduce((p: HierarchyOfMaps, n, index) => {
const keyCount = keyAccessors.length;
let pointer: HierarchyOfMaps = p;
Expand All @@ -97,6 +106,7 @@ export function groupByRollup(
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 }),
Expand All @@ -108,6 +118,9 @@ export function groupByRollup(
});
return p;
}, new Map());
if (reductionMap.get(null) !== void 0) {
statistics.globalAggregate = (reductionMap.get(null) as MapNode)[AGGREGATE_KEY];
}
return reductionMap;
}

Expand All @@ -127,6 +140,7 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc
const valueElement = value[CHILDREN_KEY];
const resultNode: ArrayNode = {
[AGGREGATE_KEY]: NaN,
[STATISTICS_KEY]: { globalAggregate: NaN },
[CHILDREN_KEY]: [],
[DEPTH_KEY]: NaN,
[SORT_INDEX_KEY]: NaN,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ import { wrapToTau } from '../geometry';
import { Coordinate, Distance, Pixels, Radian, Radius, RingSector } from '../types/geometry_types';
import { Config } from '../types/config_types';
import { logarithm, TAU, trueBearingToStandardPositionAngle } from '../utils/math';
import { QuadViewModel, RawTextGetter, RowBox, RowSet, RowSpace, ShapeTreeNode } from '../types/viewmodel_types';
import {
QuadViewModel,
RawTextGetter,
RowBox,
RowSet,
RowSpace,
ShapeTreeNode,
ValueGetterFunction,
} from '../types/viewmodel_types';
import { Box, Font, PartialFont, TextMeasure } from '../types/types';
import { AGGREGATE_KEY } from '../utils/group_by_rollup';
import { conjunctiveConstraint } from '../circline_geometry';
import { Layer } from '../../specs/index';
import { stringToRGB } from '../utils/d3_utils';
Expand Down Expand Up @@ -216,6 +223,7 @@ function identityRowSet(): RowSet {

function getAllBoxes(
rawTextGetter: RawTextGetter,
valueGetter: ValueGetterFunction,
valueFormatter: ValueFormatter,
sizeInvariantFontShorthand: Font,
valueFont: PartialFont,
Expand All @@ -225,7 +233,7 @@ function getAllBoxes(
.split(' ')
.map((text) => ({ text, ...sizeInvariantFontShorthand }))
.concat(
valueFormatter(node[AGGREGATE_KEY])
valueFormatter(valueGetter(node))
.split(' ')
.map((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont })),
);
Expand All @@ -241,7 +249,8 @@ function fill(
fontSizes: string | any[],
measure: TextMeasure,
rawTextGetter: RawTextGetter,
formatter: (value: number) => string,
valueGetter: ValueGetterFunction,
formatter: ValueFormatter,
textFillOrigins: any[],
shapeConstructor: (n: ShapeTreeNode) => any,
getShapeRowGeometry: (...args: any[]) => RowSpace,
Expand Down Expand Up @@ -278,7 +287,7 @@ function fill(
fontWeight,
fontFamily,
};
const allBoxes = getAllBoxes(rawTextGetter, valueFormatter, sizeInvariantFont, valueFont, node);
const allBoxes = getAllBoxes(rawTextGetter, valueGetter, valueFormatter, sizeInvariantFont, valueFont, node);
let rowSet = identityRowSet();
let completed = false;
const rotation = getRotation(node);
Expand Down Expand Up @@ -405,7 +414,8 @@ export function inSectorRotation(horizontalTextEnforcer: number, horizontalTextA
export function fillTextLayout(
measure: TextMeasure,
rawTextGetter: RawTextGetter,
valueFormatter: (value: number) => string,
valueGetter: ValueGetterFunction,
valueFormatter: ValueFormatter,
childNodes: QuadViewModel[],
config: Config,
layers: Layer[],
Expand Down Expand Up @@ -433,6 +443,7 @@ export function fillTextLayout(
fontSizes,
measure,
rawTextGetter,
valueGetter,
valueFormatter,
textFillOrigins,
shapeConstructor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,19 @@
import { Distance } from '../types/geometry_types';
import { Config } from '../types/config_types';
import { TAU, trueBearingToStandardPositionAngle } from '../utils/math';
import { LinkLabelVM, ShapeTreeNode } from '../types/viewmodel_types';
import { LinkLabelVM, ShapeTreeNode, ValueGetterFunction } from '../types/viewmodel_types';
import { meanAngle } from '../geometry';
import { TextMeasure } from '../types/types';
import { AGGREGATE_KEY } from '../utils/group_by_rollup';
import { ValueFormatter } from '../../../../utils/commons';

// todo modularize this large function
export function linkTextLayout(
measure: TextMeasure,
config: Config,
nodesWithoutRoom: ShapeTreeNode[],
currentY: Distance[],
anchorRadius: Distance,
rawTextGetter: Function,
valueGetter: ValueGetterFunction,
valueFormatter: ValueFormatter,
): LinkLabelVM[] {
const { linkLabel } = config;
Expand Down Expand Up @@ -83,7 +82,7 @@ export function linkTextLayout(
translate: [stemToX + west * (linkLabel.horizontalStemLength + linkLabel.gap), stemToY],
textAlign: side ? 'left' : 'right',
text,
valueText: valueFormatter(node[AGGREGATE_KEY]),
valueText: valueFormatter(valueGetter(node)),
width,
verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle`
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
RowSet,
ShapeTreeNode,
ShapeViewModel,
ValueGetterFunction,
} from '../types/viewmodel_types';
import { Layer } from '../../specs/index';
import {
Expand All @@ -61,7 +62,8 @@ import {
parentAccessor,
sortIndexAccessor,
} from '../utils/group_by_rollup';
import { ValueAccessor } from '../../../../utils/commons';
import { ValueAccessor, ValueFormatter } from '../../../../utils/commons';
import { percentValueGetter } from '../config/config';

function paddingAccessor(n: ArrayEntry) {
return entryValue(n).depth > 1 ? 1 : [0, 2][entryValue(n).depth];
Expand Down Expand Up @@ -145,7 +147,9 @@ export function shapeViewModel(
facts: Relation,
rawTextGetter: RawTextGetter,
valueAccessor: ValueAccessor,
valueFormatter: (value: number) => string,
specifiedValueFormatter: ValueFormatter,
specifiedPercentFormatter: ValueFormatter,
valueGetter: ValueGetterFunction,
groupByRollupAccessors: IndexedAccessorFn[],
): ShapeViewModel {
const {
Expand Down Expand Up @@ -247,9 +251,12 @@ export function shapeViewModel(

const textFillOrigins = nodesWithRoom.map(treemapLayout ? rectangleFillOrigins : sectorFillOrigins(fillOutside));

const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter;

const rowSets: RowSet[] = fillTextLayout(
textMeasure,
rawTextGetter,
valueGetter,
valueFormatter,
nodesWithRoom,
config,
Expand Down Expand Up @@ -283,6 +290,7 @@ export function shapeViewModel(
currentY,
outerRadius,
rawTextGetter,
valueGetter,
valueFormatter,
);

Expand Down
13 changes: 11 additions & 2 deletions packages/osd-charts/src/chart_types/partition_chart/specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
* under the License. */

import { ChartTypes } from '../../index';
import { config } from '../layout/config/config';
import { config, percentFormatter } from '../layout/config/config';
import { FunctionComponent } from 'react';
import { getConnect, specComponentFactory } from '../../../state/spec_factory';
import { IndexedAccessorFn } from '../../../utils/accessor';
import { Spec, SpecTypes } from '../../../specs/index';
import { Config, FillLabelConfig } from '../layout/types/config_types';
import { ShapeTreeNode, ValueGetter } from '../layout/types/viewmodel_types';
import { AGGREGATE_KEY } from '../layout/utils/group_by_rollup';
import { Datum, LabelAccessor, RecursivePartial, ValueAccessor, ValueFormatter } from '../../../utils/commons';
import { NodeColorAccessor } from '../layout/types/viewmodel_types';
import { PrimitiveValue } from '../layout/utils/group_by_rollup';
Expand All @@ -39,7 +41,9 @@ const defaultProps = {
specType: SpecTypes.Series,
config,
valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0),
valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY],
valueFormatter: (d: number): string => String(d),
percentFormatter,
layers: [
{
groupByRollup: (d: Datum, i: number) => i,
Expand All @@ -56,12 +60,17 @@ export interface PartitionSpec extends Spec {
data: Datum[];
valueAccessor: ValueAccessor;
valueFormatter: ValueFormatter;
valueGetter: ValueGetter;
percentFormatter: ValueFormatter;
layers: Layer[];
}

type SpecRequiredProps = Pick<PartitionSpec, 'id' | 'data'>;
type SpecOptionalProps = Partial<Omit<PartitionSpec, 'chartType' | 'specType' | 'id' | 'data'>>;

export const Partition: FunctionComponent<SpecRequiredProps & SpecOptionalProps> = getConnect()(
specComponentFactory<PartitionSpec, 'valueAccessor' | 'valueFormatter' | 'layers' | 'config'>(defaultProps),
specComponentFactory<
PartitionSpec,
'valueAccessor' | 'valueGetter' | 'valueFormatter' | 'layers' | 'config' | 'percentFormatter'
>(defaultProps),
);
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@
import { Dimensions } from '../../../../utils/dimensions';
import { shapeViewModel } from '../../layout/viewmodel/viewmodel';
import { measureText } from '../../layout/utils/measure';
import { ShapeTreeNode, ShapeViewModel, RawTextGetter, nullShapeViewModel } from '../../layout/types/viewmodel_types';
import {
ShapeTreeNode,
ShapeViewModel,
RawTextGetter,
nullShapeViewModel,
ValueGetter,
} from '../../layout/types/viewmodel_types';
import { DEPTH_KEY } from '../../layout/utils/group_by_rollup';
import { PartitionSpec, Layer } from '../../specs/index';
import { identity, mergePartial, RecursivePartial } from '../../../../utils/commons';
import { config as defaultConfig } from '../../layout/config/config';
import { config as defaultConfig, VALUE_GETTERS } from '../../layout/config/config';
import { Config } from '../../layout/types/config_types';

function rawTextGetter(layers: Layer[]): RawTextGetter {
Expand All @@ -33,6 +39,10 @@ function rawTextGetter(layers: Layer[]): RawTextGetter {
};
}

export function valueGetterFunction(valueGetter: ValueGetter) {
return typeof valueGetter === 'function' ? valueGetter : VALUE_GETTERS[valueGetter];
}

export function render(partitionSpec: PartitionSpec, parentDimensions: Dimensions): ShapeViewModel {
const { width, height } = parentDimensions;
const { layers, data: facts, config: specConfig } = partitionSpec;
Expand All @@ -43,6 +53,7 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension
if (!textMeasurerCtx) {
return nullShapeViewModel(config, { x: width / 2, y: height / 2 });
}
const valueGetter = valueGetterFunction(partitionSpec.valueGetter);
return shapeViewModel(
measureText(textMeasurerCtx),
config,
Expand All @@ -51,6 +62,8 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension
rawTextGetter(layers),
partitionSpec.valueAccessor,
partitionSpec.valueFormatter,
partitionSpec.percentFormatter,
valueGetter,
[() => null, ...layers.map(({ groupByRollup }) => groupByRollup)],
);
}
Loading

0 comments on commit 993a448

Please sign in to comment.