Skip to content

Commit d6aa8d7

Browse files
monferamarkov00
andauthored
feat: percentage display in partitioning charts (#558)
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 <vettorello.marco@gmail.com>
1 parent 103df02 commit d6aa8d7

18 files changed

+322
-55
lines changed
Loading
Loading
552 Bytes
Loading

src/chart_types/partition_chart/layout/config/config.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { palettes } from '../../../../mocks/hierarchical/palettes';
2020
import { Config, PartitionLayout, Numeric } from '../types/config_types';
2121
import { GOLDEN_RATIO, TAU } from '../utils/math';
2222
import { FONT_STYLES, FONT_VARIANTS } from '../types/types';
23+
import { ShapeTreeNode } from '../types/viewmodel_types';
24+
import { AGGREGATE_KEY, STATISTICS_KEY } from '../utils/group_by_rollup';
2325

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

32-
function defaultFormatter(d: any): string {
33-
return typeof d === 'string'
34-
? d
35-
: typeof d === 'number'
36-
? Math.abs(d) >= 10000000 || Math.abs(d) < 0.001
37-
? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1)))
38-
: d.toLocaleString(void 0, {
39-
maximumSignificantDigits: 4,
40-
maximumFractionDigits: 3,
41-
useGrouping: true,
42-
})
43-
: String(d);
34+
export function sumValueGetter(node: ShapeTreeNode): number {
35+
return node[AGGREGATE_KEY];
36+
}
37+
38+
export function percentValueGetter(node: ShapeTreeNode): number {
39+
return (100 * node[AGGREGATE_KEY]) / node.parent[STATISTICS_KEY].globalAggregate;
40+
}
41+
42+
export function ratioValueGetter(node: ShapeTreeNode): number {
43+
return node[AGGREGATE_KEY] / node.parent[STATISTICS_KEY].globalAggregate;
44+
}
45+
46+
export const VALUE_GETTERS = Object.freeze({ percent: percentValueGetter, ratio: ratioValueGetter } as const);
47+
export type ValueGetterName = keyof typeof VALUE_GETTERS;
48+
49+
function defaultFormatter(d: number): string {
50+
return Math.abs(d) >= 10000000 || Math.abs(d) < 0.001
51+
? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1)))
52+
: d.toLocaleString(void 0, {
53+
maximumSignificantDigits: 4,
54+
maximumFractionDigits: 3,
55+
useGrouping: true,
56+
});
57+
}
58+
59+
export function percentFormatter(d: number): string {
60+
return `${Math.round(d)}%`;
4461
}
4562

4663
const valueFont = {
@@ -154,6 +171,10 @@ export const configMetadata = {
154171
type: 'string',
155172
values: FONT_VARIANTS,
156173
},
174+
valueGetter: {
175+
dflt: sumValueGetter,
176+
type: 'function',
177+
},
157178
valueFormatter: {
158179
dflt: defaultFormatter,
159180
type: 'function',

src/chart_types/partition_chart/layout/types/viewmodel_types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import { Config } from './config_types';
2020
import { Coordinate, Distance, Pixels, PointObject, PointTuple, Radian } from './geometry_types';
2121
import { Font } from './types';
22-
import { config } from '../config/config';
22+
import { config, ValueGetterName } from '../config/config';
2323
import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup';
2424
import { Color } from '../../../../utils/commons';
2525

@@ -124,4 +124,6 @@ export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY {
124124
}
125125

126126
export type RawTextGetter = (node: ShapeTreeNode) => string;
127+
export type ValueGetterFunction = (node: ShapeTreeNode) => number;
128+
export type ValueGetter = ValueGetterFunction | ValueGetterName;
127129
export type NodeColorAccessor = (d: ShapeTreeNode, index: number, array: HierarchyOfArrays) => string;

src/chart_types/partition_chart/layout/utils/group_by_rollup.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,22 @@
1919
import { Relation } from '../types/types';
2020
import { Datum } from '../../../../utils/commons';
2121

22-
export const AGGREGATE_KEY = 'value'; // todo later switch back to 'aggregate'
22+
export const AGGREGATE_KEY = 'value';
23+
export const STATISTICS_KEY = 'statistics';
2324
export const DEPTH_KEY = 'depth';
2425
export const CHILDREN_KEY = 'children';
2526
export const INPUT_KEY = 'inputIndex';
2627
export const PARENT_KEY = 'parent';
2728
export const SORT_INDEX_KEY = 'sortIndex';
2829

30+
interface Statistics {
31+
globalAggregate: number;
32+
}
33+
2934
interface NodeDescriptor {
3035
[AGGREGATE_KEY]: number;
3136
[DEPTH_KEY]: number;
37+
[STATISTICS_KEY]: Statistics;
3238
[INPUT_KEY]?: Array<number>;
3339
}
3440

@@ -82,7 +88,10 @@ export function groupByRollup(
8288
identity: Function;
8389
},
8490
factTable: Relation,
85-
) {
91+
): HierarchyOfMaps {
92+
const statistics: Statistics = {
93+
globalAggregate: NaN,
94+
};
8695
const reductionMap = factTable.reduce((p: HierarchyOfMaps, n, index) => {
8796
const keyCount = keyAccessors.length;
8897
let pointer: HierarchyOfMaps = p;
@@ -97,6 +106,7 @@ export function groupByRollup(
97106
const reductionValue = reducer(aggregate, valueAccessor(n));
98107
pointer.set(key, {
99108
[AGGREGATE_KEY]: reductionValue,
109+
[STATISTICS_KEY]: statistics,
100110
[INPUT_KEY]: [...inputIndices, index],
101111
[DEPTH_KEY]: i,
102112
...(!last && { [CHILDREN_KEY]: childrenMap }),
@@ -108,6 +118,9 @@ export function groupByRollup(
108118
});
109119
return p;
110120
}, new Map());
121+
if (reductionMap.get(null) !== void 0) {
122+
statistics.globalAggregate = (reductionMap.get(null) as MapNode)[AGGREGATE_KEY];
123+
}
111124
return reductionMap;
112125
}
113126

@@ -127,6 +140,7 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc
127140
const valueElement = value[CHILDREN_KEY];
128141
const resultNode: ArrayNode = {
129142
[AGGREGATE_KEY]: NaN,
143+
[STATISTICS_KEY]: { globalAggregate: NaN },
130144
[CHILDREN_KEY]: [],
131145
[DEPTH_KEY]: NaN,
132146
[SORT_INDEX_KEY]: NaN,

src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ import { wrapToTau } from '../geometry';
2020
import { Coordinate, Distance, Pixels, Radian, Radius, RingSector } from '../types/geometry_types';
2121
import { Config } from '../types/config_types';
2222
import { logarithm, TAU, trueBearingToStandardPositionAngle } from '../utils/math';
23-
import { QuadViewModel, RawTextGetter, RowBox, RowSet, RowSpace, ShapeTreeNode } from '../types/viewmodel_types';
23+
import {
24+
QuadViewModel,
25+
RawTextGetter,
26+
RowBox,
27+
RowSet,
28+
RowSpace,
29+
ShapeTreeNode,
30+
ValueGetterFunction,
31+
} from '../types/viewmodel_types';
2432
import { Box, Font, PartialFont, TextMeasure } from '../types/types';
25-
import { AGGREGATE_KEY } from '../utils/group_by_rollup';
2633
import { conjunctiveConstraint } from '../circline_geometry';
2734
import { Layer } from '../../specs/index';
2835
import { stringToRGB } from '../utils/d3_utils';
@@ -216,6 +223,7 @@ function identityRowSet(): RowSet {
216223

217224
function getAllBoxes(
218225
rawTextGetter: RawTextGetter,
226+
valueGetter: ValueGetterFunction,
219227
valueFormatter: ValueFormatter,
220228
sizeInvariantFontShorthand: Font,
221229
valueFont: PartialFont,
@@ -225,7 +233,7 @@ function getAllBoxes(
225233
.split(' ')
226234
.map((text) => ({ text, ...sizeInvariantFontShorthand }))
227235
.concat(
228-
valueFormatter(node[AGGREGATE_KEY])
236+
valueFormatter(valueGetter(node))
229237
.split(' ')
230238
.map((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont })),
231239
);
@@ -241,7 +249,8 @@ function fill(
241249
fontSizes: string | any[],
242250
measure: TextMeasure,
243251
rawTextGetter: RawTextGetter,
244-
formatter: (value: number) => string,
252+
valueGetter: ValueGetterFunction,
253+
formatter: ValueFormatter,
245254
textFillOrigins: any[],
246255
shapeConstructor: (n: ShapeTreeNode) => any,
247256
getShapeRowGeometry: (...args: any[]) => RowSpace,
@@ -278,7 +287,7 @@ function fill(
278287
fontWeight,
279288
fontFamily,
280289
};
281-
const allBoxes = getAllBoxes(rawTextGetter, valueFormatter, sizeInvariantFont, valueFont, node);
290+
const allBoxes = getAllBoxes(rawTextGetter, valueGetter, valueFormatter, sizeInvariantFont, valueFont, node);
282291
let rowSet = identityRowSet();
283292
let completed = false;
284293
const rotation = getRotation(node);
@@ -405,7 +414,8 @@ export function inSectorRotation(horizontalTextEnforcer: number, horizontalTextA
405414
export function fillTextLayout(
406415
measure: TextMeasure,
407416
rawTextGetter: RawTextGetter,
408-
valueFormatter: (value: number) => string,
417+
valueGetter: ValueGetterFunction,
418+
valueFormatter: ValueFormatter,
409419
childNodes: QuadViewModel[],
410420
config: Config,
411421
layers: Layer[],
@@ -433,6 +443,7 @@ export function fillTextLayout(
433443
fontSizes,
434444
measure,
435445
rawTextGetter,
446+
valueGetter,
436447
valueFormatter,
437448
textFillOrigins,
438449
shapeConstructor,

src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,19 @@
1919
import { Distance } from '../types/geometry_types';
2020
import { Config } from '../types/config_types';
2121
import { TAU, trueBearingToStandardPositionAngle } from '../utils/math';
22-
import { LinkLabelVM, ShapeTreeNode } from '../types/viewmodel_types';
22+
import { LinkLabelVM, ShapeTreeNode, ValueGetterFunction } from '../types/viewmodel_types';
2323
import { meanAngle } from '../geometry';
2424
import { TextMeasure } from '../types/types';
25-
import { AGGREGATE_KEY } from '../utils/group_by_rollup';
2625
import { ValueFormatter } from '../../../../utils/commons';
2726

28-
// todo modularize this large function
2927
export function linkTextLayout(
3028
measure: TextMeasure,
3129
config: Config,
3230
nodesWithoutRoom: ShapeTreeNode[],
3331
currentY: Distance[],
3432
anchorRadius: Distance,
3533
rawTextGetter: Function,
34+
valueGetter: ValueGetterFunction,
3635
valueFormatter: ValueFormatter,
3736
): LinkLabelVM[] {
3837
const { linkLabel } = config;
@@ -83,7 +82,7 @@ export function linkTextLayout(
8382
translate: [stemToX + west * (linkLabel.horizontalStemLength + linkLabel.gap), stemToY],
8483
textAlign: side ? 'left' : 'right',
8584
text,
86-
valueText: valueFormatter(node[AGGREGATE_KEY]),
85+
valueText: valueFormatter(valueGetter(node)),
8786
width,
8887
verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle`
8988
};

src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
RowSet,
3636
ShapeTreeNode,
3737
ShapeViewModel,
38+
ValueGetterFunction,
3839
} from '../types/viewmodel_types';
3940
import { Layer } from '../../specs/index';
4041
import {
@@ -61,7 +62,8 @@ import {
6162
parentAccessor,
6263
sortIndexAccessor,
6364
} from '../utils/group_by_rollup';
64-
import { ValueAccessor } from '../../../../utils/commons';
65+
import { ValueAccessor, ValueFormatter } from '../../../../utils/commons';
66+
import { percentValueGetter } from '../config/config';
6567

6668
function paddingAccessor(n: ArrayEntry) {
6769
return entryValue(n).depth > 1 ? 1 : [0, 2][entryValue(n).depth];
@@ -145,7 +147,9 @@ export function shapeViewModel(
145147
facts: Relation,
146148
rawTextGetter: RawTextGetter,
147149
valueAccessor: ValueAccessor,
148-
valueFormatter: (value: number) => string,
150+
specifiedValueFormatter: ValueFormatter,
151+
specifiedPercentFormatter: ValueFormatter,
152+
valueGetter: ValueGetterFunction,
149153
groupByRollupAccessors: IndexedAccessorFn[],
150154
): ShapeViewModel {
151155
const {
@@ -247,9 +251,12 @@ export function shapeViewModel(
247251

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

254+
const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter;
255+
250256
const rowSets: RowSet[] = fillTextLayout(
251257
textMeasure,
252258
rawTextGetter,
259+
valueGetter,
253260
valueFormatter,
254261
nodesWithRoom,
255262
config,
@@ -283,6 +290,7 @@ export function shapeViewModel(
283290
currentY,
284291
outerRadius,
285292
rawTextGetter,
293+
valueGetter,
286294
valueFormatter,
287295
);
288296

src/chart_types/partition_chart/specs/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
* under the License. */
1818

1919
import { ChartTypes } from '../../index';
20-
import { config } from '../layout/config/config';
20+
import { config, percentFormatter } from '../layout/config/config';
2121
import { FunctionComponent } from 'react';
2222
import { getConnect, specComponentFactory } from '../../../state/spec_factory';
2323
import { IndexedAccessorFn } from '../../../utils/accessor';
2424
import { Spec, SpecTypes } from '../../../specs/index';
2525
import { Config, FillLabelConfig } from '../layout/types/config_types';
26+
import { ShapeTreeNode, ValueGetter } from '../layout/types/viewmodel_types';
27+
import { AGGREGATE_KEY } from '../layout/utils/group_by_rollup';
2628
import { Datum, LabelAccessor, RecursivePartial, ValueAccessor, ValueFormatter } from '../../../utils/commons';
2729
import { NodeColorAccessor } from '../layout/types/viewmodel_types';
2830
import { PrimitiveValue } from '../layout/utils/group_by_rollup';
@@ -39,7 +41,9 @@ const defaultProps = {
3941
specType: SpecTypes.Series,
4042
config,
4143
valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0),
44+
valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY],
4245
valueFormatter: (d: number): string => String(d),
46+
percentFormatter,
4347
layers: [
4448
{
4549
groupByRollup: (d: Datum, i: number) => i,
@@ -56,12 +60,17 @@ export interface PartitionSpec extends Spec {
5660
data: Datum[];
5761
valueAccessor: ValueAccessor;
5862
valueFormatter: ValueFormatter;
63+
valueGetter: ValueGetter;
64+
percentFormatter: ValueFormatter;
5965
layers: Layer[];
6066
}
6167

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

6571
export const Partition: FunctionComponent<SpecRequiredProps & SpecOptionalProps> = getConnect()(
66-
specComponentFactory<PartitionSpec, 'valueAccessor' | 'valueFormatter' | 'layers' | 'config'>(defaultProps),
72+
specComponentFactory<
73+
PartitionSpec,
74+
'valueAccessor' | 'valueGetter' | 'valueFormatter' | 'layers' | 'config' | 'percentFormatter'
75+
>(defaultProps),
6776
);

0 commit comments

Comments
 (0)