Skip to content

Commit 4379e29

Browse files
authored
feat(data): Intent to ship data.stack.normalize.perGroup
Implement normalize per group option Close #4060
1 parent 76151ad commit 4379e29

File tree

8 files changed

+377
-30
lines changed

8 files changed

+377
-30
lines changed

demo/demo.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3466,6 +3466,122 @@ var demos = {
34663466
}
34673467
}
34683468
],
3469+
DataStackNormalizedGroup: [
3470+
{
3471+
options: {
3472+
title: {
3473+
text: "Normalize per group - Multiple groups"
3474+
},
3475+
data: {
3476+
columns: [
3477+
["data1", 100, 200, 150, 300],
3478+
["data2", 200, 400, 350, 200],
3479+
["data3", 50, 100, 80, 120],
3480+
["data4", 150, 200, 220, 180]
3481+
],
3482+
type: "bar",
3483+
groups: [
3484+
["data1", "data2"],
3485+
["data3", "data4"]
3486+
],
3487+
stack: {
3488+
normalize: {
3489+
perGroup: true
3490+
}
3491+
}
3492+
},
3493+
axis: {
3494+
y: {
3495+
label: {
3496+
text: "Percentage (%)",
3497+
position: "outer-middle"
3498+
}
3499+
}
3500+
},
3501+
tooltip: {
3502+
format: {
3503+
title: function(x) { return "Index " + x; }
3504+
}
3505+
}
3506+
}
3507+
},
3508+
{
3509+
options: {
3510+
title: {
3511+
text: "Normalize per group - With non-grouped data"
3512+
},
3513+
data: {
3514+
columns: [
3515+
["data1", 100, 200, 150, 300],
3516+
["data2", 200, 400, 350, 200],
3517+
["data3", 50, 100, 80, 120]
3518+
],
3519+
type: "bar",
3520+
types: {
3521+
"data3": "line"
3522+
},
3523+
groups: [
3524+
["data1", "data2"]
3525+
],
3526+
stack: {
3527+
normalize: {
3528+
perGroup: true
3529+
}
3530+
},
3531+
axes: {
3532+
data3: "y2"
3533+
}
3534+
},
3535+
axis: {
3536+
y: {
3537+
label: {
3538+
text: "Grouped: % | Non-grouped: Absolute",
3539+
position: "outer-middle"
3540+
}
3541+
},
3542+
y2: {
3543+
show: true
3544+
}
3545+
},
3546+
tooltip: {
3547+
format: {
3548+
title: function(x) { return "Index " + x; }
3549+
}
3550+
}
3551+
}
3552+
},
3553+
{
3554+
options: {
3555+
title: {
3556+
text: "Normalize per group - Area chart"
3557+
},
3558+
data: {
3559+
columns: [
3560+
["data1", 30, 280, 951, 400, 150],
3561+
["data2", 130, 357, 751, 400, 150],
3562+
["data3", 50, 100, 200, 150, 80],
3563+
["data4", 100, 200, 300, 250, 120]
3564+
],
3565+
types: {
3566+
data1: "area",
3567+
data2: "area",
3568+
data3: "bar",
3569+
data4: "bar",
3570+
},
3571+
groups: [
3572+
["data1", "data2"],
3573+
["data3", "data4"]
3574+
],
3575+
stack: {
3576+
normalize: {
3577+
perGroup: true
3578+
}
3579+
}
3580+
},
3581+
clipPath: false
3582+
}
3583+
}
3584+
],
34693585
DataXSort: [
34703586
{
34713587
options: {

src/ChartInternal/Axis/Axis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ class Axis {
356356
// Set tick
357357
axis.tickFormat(
358358
tickFormat || (
359-
!isX && ($$.isStackNormalized() && (x => `${x}%`))
359+
!isX && ($$.isStackNormalized() && $$.hasAxisGroupedData(id) && (x => `${x}%`))
360360
)
361361
);
362362

src/ChartInternal/data/data.ts

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,26 @@ export default {
4646
isStackNormalized(): boolean {
4747
const {config} = this;
4848

49-
return !!(config.data_stack_normalize && config.data_groups.length);
49+
return !!(
50+
(config.data_stack_normalize === true ||
51+
isObjectType(config.data_stack_normalize)) &&
52+
config.data_groups.length
53+
);
54+
},
55+
56+
/**
57+
* Check if stack normalization should be applied per group
58+
* @returns {boolean}
59+
* @private
60+
*/
61+
isStackNormalizedPerGroup(): boolean {
62+
const {config} = this;
63+
64+
return !!(
65+
isObjectType(config.data_stack_normalize) &&
66+
config.data_stack_normalize?.perGroup &&
67+
config.data_groups.length
68+
);
5069
},
5170

5271
/**
@@ -61,6 +80,26 @@ export default {
6180
return id ? groups.some(v => v.indexOf(id) >= 0 && v.length > 1) : groups.length > 0;
6281
},
6382

83+
/**
84+
* Check if the given axis has any grouped data
85+
* @param {string} axisId Axis ID (e.g., "y", "y2")
86+
* @returns {boolean} true if axis has grouped data
87+
* @private
88+
*/
89+
hasAxisGroupedData(axisId: "y" | "y2"): boolean {
90+
const $$ = this;
91+
const {axis} = $$;
92+
const targets = $$.data.targets;
93+
94+
// Get all data IDs that belong to this axis
95+
const axisDataIds = targets
96+
.filter(t => axis.getId(t.id) === axisId)
97+
.map(t => t.id);
98+
99+
// Check if any of the axis data IDs are in groups
100+
return axisDataIds.some(id => $$.isGrouped(id));
101+
},
102+
64103
getXKey(id) {
65104
const $$ = this;
66105
const {config} = $$;
@@ -325,18 +364,37 @@ export default {
325364

326365
/**
327366
* Get sum of data per index
367+
* @param {string} targetId Target ID to get total for (only for normalized stack per group)
328368
* @private
329369
* @returns {Array}
330370
*/
331-
getTotalPerIndex() {
371+
getTotalPerIndex(targetId?: string) {
332372
const $$ = this;
333-
const cacheKey = KEY.dataTotalPerIndex;
373+
const {config} = $$;
374+
const cacheKey = targetId ? `${KEY.dataTotalPerIndex}-${targetId}` : KEY.dataTotalPerIndex;
334375
let sum = $$.cache.get(cacheKey);
335376

336377
if (($$.config.data_groups.length || $$.isStackNormalized()) && !sum) {
337378
sum = [];
338379

339-
$$.data.targets.forEach(row => {
380+
// When normalize per group is enabled and targetId is provided,
381+
// only sum data within the same group
382+
let {targets} = $$.data;
383+
384+
if ($$.isStackNormalizedPerGroup() && targetId) {
385+
// Find which group the target belongs to
386+
const group = config.data_groups.find(g => g.indexOf(targetId) >= 0);
387+
388+
if (group) {
389+
// Only sum targets in the same group
390+
targets = targets.filter(t => group.indexOf(t.id) >= 0);
391+
} else {
392+
// If target is not in any group, return null to indicate no normalization
393+
return null;
394+
}
395+
}
396+
397+
targets.forEach(row => {
340398
row.values.forEach((v, i) => {
341399
if (!sum[i]) {
342400
sum[i] = 0;
@@ -345,6 +403,8 @@ export default {
345403
sum[i] += ~~v.value;
346404
});
347405
});
406+
407+
$$.cache.add(cacheKey, sum);
348408
}
349409

350410
return sum;
@@ -489,7 +549,7 @@ export default {
489549
},
490550

491551
/**
492-
* Add to the state target Ids
552+
* Add to thetarget Ids
493553
* @param {string} type State's prop name
494554
* @param {Array|string} targetIds Target ids array
495555
* @private
@@ -1014,16 +1074,39 @@ export default {
10141074
}
10151075
} else if (type === "index") {
10161076
const dataValues = api.data.values.bind(api);
1017-
let total = this.getTotalPerIndex();
1077+
const {hiddenTargetIds} = state;
10181078

1019-
if (state.hiddenTargetIds.length) {
1020-
let hiddenSum = dataValues(state.hiddenTargetIds, false);
1079+
// For normalized stack per group, get total per group
1080+
let total = this.getTotalPerIndex(
1081+
$$.isStackNormalizedPerGroup() ? d.id : undefined
1082+
);
1083+
1084+
// If total is null, the data is not in any group - don't normalize
1085+
if (total === null) {
1086+
return ratio;
1087+
}
10211088

1022-
if (hiddenSum.length) {
1023-
hiddenSum = hiddenSum
1024-
.reduce((acc, curr) => acc.map((v, i) => ~~v + curr[i]));
1089+
if (hiddenTargetIds.length) {
1090+
// When normalized per group, only subtract hidden data from the same group
1091+
let hiddenIds = hiddenTargetIds;
10251092

1026-
total = total.map((v, i) => v - hiddenSum[i]);
1093+
if ($$.isStackNormalizedPerGroup() && d.id) {
1094+
const group = config.data_groups.find(g => g.indexOf(d.id) >= 0);
1095+
if (group) {
1096+
// Only consider hidden IDs in the same group
1097+
hiddenIds = hiddenIds.filter(id => group.indexOf(id) >= 0);
1098+
}
1099+
}
1100+
1101+
if (hiddenIds.length) {
1102+
let hiddenSum = dataValues(hiddenIds, false);
1103+
1104+
if (hiddenSum.length) {
1105+
hiddenSum = hiddenSum
1106+
.reduce((acc, curr) => acc.map((v, i) => ~~v + curr[i]));
1107+
1108+
total = total.map((v, i) => v - hiddenSum[i]);
1109+
}
10271110
}
10281111
}
10291112

src/ChartInternal/internals/domain.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,20 @@ export default {
8787
const {axis, config, scale} = $$;
8888
const pfx = `axis_${axisId}`;
8989

90+
// Check if stack normalization should be applied for this axis
9091
if ($$.isStackNormalized()) {
91-
return [0, 100];
92+
// Get all data IDs that belong to this axis
93+
const axisDataIds = targets
94+
.filter(t => axis.getId(t.id) === axisId)
95+
.map(t => t.id);
96+
97+
// Check if any of the axis data IDs are in groups
98+
const hasGroupedData = axisDataIds.some(id => $$.isGrouped(id));
99+
100+
// Apply normalization only if this axis has grouped data
101+
if (hasGroupedData) {
102+
return [0, 100];
103+
}
92104
}
93105

94106
const isLog = scale?.[axisId] && scale[axisId].type === "log";

src/ChartInternal/internals/tooltip.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,23 @@ export default {
119119
// determine fotmatter function with sanitization
120120
const titleFormat = (...arg) => sanitize((titleFn || defaultTitleFormat)(...arg));
121121
const nameFormat = (...arg) => sanitize((nameFn || (name => name))(...arg));
122-
const valueFormat = (...arg) => {
123-
const fn = valueFn || (
124-
state.hasTreemap || $$.isStackNormalized() ?
125-
(v, ratio) => `${(ratio * 100).toFixed(2)}%` :
126-
defaultValueFormat
127-
);
122+
const valueFormat = (v, ratio, id, index) => {
123+
let fn = valueFn;
124+
125+
if (!fn) {
126+
// For normalize per group, only show percentage for data in groups
127+
if (
128+
state.hasTreemap ||
129+
($$.isStackNormalized() &&
130+
(!$$.isStackNormalizedPerGroup() || $$.isGrouped(id)))
131+
) {
132+
fn = (v, ratio) => `${(ratio * 100).toFixed(2)}%`;
133+
} else {
134+
fn = defaultValueFormat;
135+
}
136+
}
128137

129-
return sanitize(fn(...arg));
138+
return sanitize(fn(v, ratio, id, index));
130139
};
131140

132141
const order = config.tooltip_order;
@@ -207,9 +216,9 @@ export default {
207216

208217
if ($$.isAreaRangeType(row)) {
209218
const [high, low] = ["high", "low"].map(v =>
210-
valueFormat($$.getRangedData(row, v), ...param)
219+
valueFormat($$.getRangedData(row, v), ...param as [number, string, number])
211220
);
212-
const mid = valueFormat(getRowValue(row), ...param);
221+
const mid = valueFormat(getRowValue(row), ...param as [number, string, number]);
213222

214223
value = `<b>Mid:</b> ${mid} <b>High:</b> ${high} <b>Low:</b> ${low}`;
215224
} else if ($$.isCandlestickType(row)) {
@@ -220,7 +229,7 @@ export default {
220229
return value ?
221230
valueFormat(
222231
$$.getRangedData(row, v, "candlestick"),
223-
...param
232+
...param as [number, string, number]
224233
) :
225234
undefined;
226235
});
@@ -234,7 +243,7 @@ export default {
234243

235244
value = `${valueFormat(rangeValue, undefined, id, index)}`;
236245
} else {
237-
value = valueFormat(getRowValue(row), ...param);
246+
value = valueFormat(getRowValue(row), ...param as [number, string, number]);
238247
}
239248

240249
if (value !== undefined) {

0 commit comments

Comments
 (0)