From 8ea47cad19ec894319f74fde340e8bc8d79fc370 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Thu, 5 Jun 2025 17:27:56 +0200 Subject: [PATCH] Fix: display stacked bar with multiple x-Axis (#12070) --- src/controllers/controller.bar.js | 32 +++++++-- .../stacking/stacked-and-multiple-axis.js | 64 ++++++++++++++++++ .../stacking/stacked-and-multiple-axis.png | Bin 0 -> 14649 bytes 3 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js create mode 100644 test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.png diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 82138f3fb..554497b30 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -486,6 +486,27 @@ export default class BarController extends DatasetController { return this._getStacks(undefined, index).length; } + _getAxisCount() { + return this._getAxis().length; + } + + getFirstScaleIdForIndexAxis() { + const scales = this.chart.scales; + const indexScaleId = this.chart.options.indexAxis; + return Object.keys(scales).filter(key => scales[key].axis === indexScaleId).shift(); + } + + _getAxis() { + const axis = {}; + const firstScaleAxisId = this.getFirstScaleIdForIndexAxis(); + for (const dataset of this.chart.data.datasets) { + axis[valueOrDefault( + this.chart.options.indexAxis === 'x' ? dataset.xAxisID : dataset.yAxisID, firstScaleAxisId + )] = true; + } + return Object.keys(axis); + } + /** * Returns the stack index for the given dataset based on groups and bar visibility. * @param {number} [datasetIndex] - The dataset index @@ -618,13 +639,15 @@ export default class BarController extends DatasetController { const skipNull = options.skipNull; const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity); let center, size; + const axisCount = this._getAxisCount(); if (ruler.grouped) { const stackCount = skipNull ? this._getStackCount(index) : ruler.stackCount; const range = options.barThickness === 'flex' - ? computeFlexCategoryTraits(index, ruler, options, stackCount) - : computeFitCategoryTraits(index, ruler, options, stackCount); - - const stackIndex = this._getStackIndex(this.index, this._cachedMeta.stack, skipNull ? index : undefined); + ? computeFlexCategoryTraits(index, ruler, options, stackCount * axisCount) + : computeFitCategoryTraits(index, ruler, options, stackCount * axisCount); + const axisID = this.chart.options.indexAxis === 'x' ? this.getDataset().xAxisID : this.getDataset().yAxisID; + const axisNumber = this._getAxis().indexOf(valueOrDefault(axisID, this.getFirstScaleIdForIndexAxis())); + const stackIndex = this._getStackIndex(this.index, this._cachedMeta.stack, skipNull ? index : undefined) + axisNumber; center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); size = Math.min(maxBarThickness, range.chunk * range.ratio); } else { @@ -633,6 +656,7 @@ export default class BarController extends DatasetController { size = Math.min(maxBarThickness, ruler.min * ruler.ratio); } + return { base: center - size / 2, head: center + size / 2, diff --git a/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js new file mode 100644 index 000000000..ca5490a92 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js @@ -0,0 +1,64 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [ + { + label: 'Dataset 1', + data: [100, 90, 100, 50, 99, 87, 34], + backgroundColor: 'rgba(255,99,132,0.8)', + stack: 'a', + xAxisID: 'x' + }, + { + label: 'Dataset 2', + data: [20, 25, 30, 32, 58, 14, 12], + backgroundColor: 'rgba(54,162,235,0.8)', + stack: 'b', + xAxisID: 'x2' + }, + { + label: 'Dataset 3', + data: [80, 30, 40, 60, 70, 80, 47], + backgroundColor: 'rgba(75,192,192,0.8)', + stack: 'a', + xAxisID: 'x3' + }, + { + label: 'Dataset 4', + data: [80, 30, 40, 60, 70, 80, 47], + backgroundColor: 'rgba(54,162,235,0.8)', + stack: 'a', + xAxisID: 'x3' + }, + ] + }, + options: { + plugins: false, + barThickness: 'flex', + scales: { + x: { + stacked: true, + display: false, + }, + x2: { + labels: ['January 2024', 'February 2024', 'March 2024', 'April 2024', 'May 2024', 'June 2024', 'July 2024'], + stacked: true, + display: false, + }, + x3: { + labels: ['January 2025', 'February 2025', 'March 2025', 'April 2025', 'May 2025', 'June 2025', 'July 2025'], + stacked: true, + display: false, + }, + y: { + stacked: true, + display: false, + } + } + } + }, + options: { + } +}; diff --git a/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.png b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.png new file mode 100644 index 0000000000000000000000000000000000000000..ea571109e90cad2295defb015a131c52b009c9a3 GIT binary patch literal 14649 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 VV?@l>7Q_?a