Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
116 changes: 116 additions & 0 deletions demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -3466,6 +3466,122 @@ var demos = {
}
}
],
DataStackNormalizedGroup: [
{
options: {
title: {
text: "Normalize per group - Multiple groups"
},
data: {
columns: [
["data1", 100, 200, 150, 300],
["data2", 200, 400, 350, 200],
["data3", 50, 100, 80, 120],
["data4", 150, 200, 220, 180]
],
type: "bar",
groups: [
["data1", "data2"],
["data3", "data4"]
],
stack: {
normalize: {
perGroup: true
}
}
},
axis: {
y: {
label: {
text: "Percentage (%)",
position: "outer-middle"
}
}
},
tooltip: {
format: {
title: function(x) { return "Index " + x; }
}
}
}
},
{
options: {
title: {
text: "Normalize per group - With non-grouped data"
},
data: {
columns: [
["data1", 100, 200, 150, 300],
["data2", 200, 400, 350, 200],
["data3", 50, 100, 80, 120]
],
type: "bar",
types: {
"data3": "line"
},
groups: [
["data1", "data2"]
],
stack: {
normalize: {
perGroup: true
}
},
axes: {
data3: "y2"
}
},
axis: {
y: {
label: {
text: "Grouped: % | Non-grouped: Absolute",
position: "outer-middle"
}
},
y2: {
show: true
}
},
tooltip: {
format: {
title: function(x) { return "Index " + x; }
}
}
}
},
{
options: {
title: {
text: "Normalize per group - Area chart"
},
data: {
columns: [
["data1", 30, 280, 951, 400, 150],
["data2", 130, 357, 751, 400, 150],
["data3", 50, 100, 200, 150, 80],
["data4", 100, 200, 300, 250, 120]
],
types: {
data1: "area",
data2: "area",
data3: "bar",
data4: "bar",
},
groups: [
["data1", "data2"],
["data3", "data4"]
],
stack: {
normalize: {
perGroup: true
}
}
},
clipPath: false
}
}
],
DataXSort: [
{
options: {
Expand Down
2 changes: 1 addition & 1 deletion src/ChartInternal/Axis/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ class Axis {
// Set tick
axis.tickFormat(
tickFormat || (
!isX && ($$.isStackNormalized() && (x => `${x}%`))
!isX && ($$.isStackNormalized() && $$.hasAxisGroupedData(id) && (x => `${x}%`))
)
);

Expand Down
107 changes: 95 additions & 12 deletions src/ChartInternal/data/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,26 @@ export default {
isStackNormalized(): boolean {
const {config} = this;

return !!(config.data_stack_normalize && config.data_groups.length);
return !!(
(config.data_stack_normalize === true ||
isObjectType(config.data_stack_normalize)) &&
config.data_groups.length
);
},

/**
* Check if stack normalization should be applied per group
* @returns {boolean}
* @private
*/
isStackNormalizedPerGroup(): boolean {
const {config} = this;

return !!(
isObjectType(config.data_stack_normalize) &&
config.data_stack_normalize?.perGroup &&
config.data_groups.length
);
},

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

/**
* Check if the given axis has any grouped data
* @param {string} axisId Axis ID (e.g., "y", "y2")
* @returns {boolean} true if axis has grouped data
* @private
*/
hasAxisGroupedData(axisId: "y" | "y2"): boolean {
const $$ = this;
const {axis} = $$;
const targets = $$.data.targets;

// Get all data IDs that belong to this axis
const axisDataIds = targets
.filter(t => axis.getId(t.id) === axisId)
.map(t => t.id);

// Check if any of the axis data IDs are in groups
return axisDataIds.some(id => $$.isGrouped(id));
},

getXKey(id) {
const $$ = this;
const {config} = $$;
Expand Down Expand Up @@ -325,18 +364,37 @@ export default {

/**
* Get sum of data per index
* @param {string} targetId Target ID to get total for (only for normalized stack per group)
* @private
* @returns {Array}
*/
getTotalPerIndex() {
getTotalPerIndex(targetId?: string) {
const $$ = this;
const cacheKey = KEY.dataTotalPerIndex;
const {config} = $$;
const cacheKey = targetId ? `${KEY.dataTotalPerIndex}-${targetId}` : KEY.dataTotalPerIndex;
let sum = $$.cache.get(cacheKey);

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

$$.data.targets.forEach(row => {
// When normalize per group is enabled and targetId is provided,
// only sum data within the same group
let {targets} = $$.data;

if ($$.isStackNormalizedPerGroup() && targetId) {
// Find which group the target belongs to
const group = config.data_groups.find(g => g.indexOf(targetId) >= 0);

if (group) {
// Only sum targets in the same group
targets = targets.filter(t => group.indexOf(t.id) >= 0);
} else {
// If target is not in any group, return null to indicate no normalization
return null;
}
}

targets.forEach(row => {
row.values.forEach((v, i) => {
if (!sum[i]) {
sum[i] = 0;
Expand All @@ -345,6 +403,8 @@ export default {
sum[i] += ~~v.value;
});
});

$$.cache.add(cacheKey, sum);
}

return sum;
Expand Down Expand Up @@ -489,7 +549,7 @@ export default {
},

/**
* Add to the state target Ids
* Add to thetarget Ids
* @param {string} type State's prop name
* @param {Array|string} targetIds Target ids array
* @private
Expand Down Expand Up @@ -1014,16 +1074,39 @@ export default {
}
} else if (type === "index") {
const dataValues = api.data.values.bind(api);
let total = this.getTotalPerIndex();
const {hiddenTargetIds} = state;

if (state.hiddenTargetIds.length) {
let hiddenSum = dataValues(state.hiddenTargetIds, false);
// For normalized stack per group, get total per group
let total = this.getTotalPerIndex(
$$.isStackNormalizedPerGroup() ? d.id : undefined
);

// If total is null, the data is not in any group - don't normalize
if (total === null) {
return ratio;
}

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

total = total.map((v, i) => v - hiddenSum[i]);
if ($$.isStackNormalizedPerGroup() && d.id) {
const group = config.data_groups.find(g => g.indexOf(d.id) >= 0);
if (group) {
// Only consider hidden IDs in the same group
hiddenIds = hiddenIds.filter(id => group.indexOf(id) >= 0);
}
}

if (hiddenIds.length) {
let hiddenSum = dataValues(hiddenIds, false);

if (hiddenSum.length) {
hiddenSum = hiddenSum
.reduce((acc, curr) => acc.map((v, i) => ~~v + curr[i]));

total = total.map((v, i) => v - hiddenSum[i]);
}
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/ChartInternal/internals/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,20 @@ export default {
const {axis, config, scale} = $$;
const pfx = `axis_${axisId}`;

// Check if stack normalization should be applied for this axis
if ($$.isStackNormalized()) {
return [0, 100];
// Get all data IDs that belong to this axis
const axisDataIds = targets
.filter(t => axis.getId(t.id) === axisId)
.map(t => t.id);

// Check if any of the axis data IDs are in groups
const hasGroupedData = axisDataIds.some(id => $$.isGrouped(id));

// Apply normalization only if this axis has grouped data
if (hasGroupedData) {
return [0, 100];
}
}

const isLog = scale?.[axisId] && scale[axisId].type === "log";
Expand Down
31 changes: 20 additions & 11 deletions src/ChartInternal/internals/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,23 @@ export default {
// determine fotmatter function with sanitization
const titleFormat = (...arg) => sanitize((titleFn || defaultTitleFormat)(...arg));
const nameFormat = (...arg) => sanitize((nameFn || (name => name))(...arg));
const valueFormat = (...arg) => {
const fn = valueFn || (
state.hasTreemap || $$.isStackNormalized() ?
(v, ratio) => `${(ratio * 100).toFixed(2)}%` :
defaultValueFormat
);
const valueFormat = (v, ratio, id, index) => {
let fn = valueFn;

if (!fn) {
// For normalize per group, only show percentage for data in groups
if (
state.hasTreemap ||
($$.isStackNormalized() &&
(!$$.isStackNormalizedPerGroup() || $$.isGrouped(id)))
) {
fn = (v, ratio) => `${(ratio * 100).toFixed(2)}%`;
} else {
fn = defaultValueFormat;
}
}

return sanitize(fn(...arg));
return sanitize(fn(v, ratio, id, index));
};

const order = config.tooltip_order;
Expand Down Expand Up @@ -207,9 +216,9 @@ export default {

if ($$.isAreaRangeType(row)) {
const [high, low] = ["high", "low"].map(v =>
valueFormat($$.getRangedData(row, v), ...param)
valueFormat($$.getRangedData(row, v), ...param as [number, string, number])
);
const mid = valueFormat(getRowValue(row), ...param);
const mid = valueFormat(getRowValue(row), ...param as [number, string, number]);

value = `<b>Mid:</b> ${mid} <b>High:</b> ${high} <b>Low:</b> ${low}`;
} else if ($$.isCandlestickType(row)) {
Expand All @@ -220,7 +229,7 @@ export default {
return value ?
valueFormat(
$$.getRangedData(row, v, "candlestick"),
...param
...param as [number, string, number]
) :
undefined;
});
Expand All @@ -234,7 +243,7 @@ export default {

value = `${valueFormat(rangeValue, undefined, id, index)}`;
} else {
value = valueFormat(getRowValue(row), ...param);
value = valueFormat(getRowValue(row), ...param as [number, string, number]);
}

if (value !== undefined) {
Expand Down
Loading