Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: percentage display in partitioning charts #558

Merged
merged 19 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
45 changes: 33 additions & 12 deletions src/chart_types/partition_chart/layout/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 @@ -11,18 +13,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) + '%';
monfera marked this conversation as resolved.
Show resolved Hide resolved
}

const valueFont = {
Expand Down Expand Up @@ -136,6 +153,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
@@ -1,7 +1,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 @@ -106,4 +106,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;
18 changes: 16 additions & 2 deletions src/chart_types/partition_chart/layout/utils/group_by_rollup.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,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 @@ -64,7 +70,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 @@ -79,6 +88,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 @@ -90,6 +100,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 @@ -109,6 +122,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 @@ -2,9 +2,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 @@ -198,6 +205,7 @@ function identityRowSet(): RowSet {

function getAllBoxes(
rawTextGetter: RawTextGetter,
valueGetter: ValueGetterFunction,
valueFormatter: ValueFormatter,
sizeInvariantFontShorthand: Font,
valueFont: PartialFont,
Expand All @@ -207,7 +215,7 @@ function getAllBoxes(
.split(' ')
.map((text) => ({ text, ...sizeInvariantFontShorthand }))
.concat(
valueFormatter(node[AGGREGATE_KEY])
valueFormatter(valueGetter(node))
.split(' ')
.map((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont })),
);
Expand All @@ -223,7 +231,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 @@ -260,7 +269,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 @@ -387,7 +396,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 @@ -415,6 +425,7 @@ export function fillTextLayout(
fontSizes,
measure,
rawTextGetter,
valueGetter,
valueFormatter,
textFillOrigins,
shapeConstructor,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,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 @@ -65,7 +64,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
11 changes: 9 additions & 2 deletions src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
RowSet,
ShapeTreeNode,
ShapeViewModel,
ValueGetterFunction,
} from '../types/viewmodel_types';
import { Layer } from '../../specs/index';
import {
Expand All @@ -43,7 +44,8 @@ import {
parentAccessor,
sortIndexAccessor,
} from '../utils/group_by_rollup';
import { ValueAccessor } from '../../../../utils/commons';
import { ValueAccessor, ValueFormatter } from '../../../../utils/commons';
import { percentFormatter, percentValueGetter } from '../config/config';

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

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

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

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

Expand Down
8 changes: 7 additions & 1 deletion src/chart_types/partition_chart/specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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 @@ -21,6 +23,7 @@ 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),
layers: [
{
Expand All @@ -38,12 +41,15 @@ export interface PartitionSpec extends Spec {
data: Datum[];
valueAccessor: ValueAccessor;
valueFormatter: ValueFormatter;
valueGetter: ValueGetter;
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'>(
defaultProps,
),
);
16 changes: 14 additions & 2 deletions src/chart_types/partition_chart/state/selectors/scenegraph.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,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 @@ -15,6 +21,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 @@ -25,6 +35,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 @@ -33,6 +44,7 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension
rawTextGetter(layers),
partitionSpec.valueAccessor,
partitionSpec.valueFormatter,
valueGetter,
[() => null, ...layers.map(({ groupByRollup }) => groupByRollup)],
);
}
Loading