From 1451e5aee6f9231ad4de88bee6aa64b3c52a0e18 Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Sat, 3 Apr 2021 16:23:35 +0300 Subject: [PATCH] Add ability to stack different dataset types (#8797) --- docs/.vuepress/config.js | 1 + docs/charts/bar.md | 2 +- docs/general/data-structures.md | 2 +- docs/samples/other-charts/stacked-bar-line.md | 120 ++++++++++++++++++ src/core/core.datasetController.js | 2 +- test/fixtures/mixed/bar+line-stacked.js | 40 ++++++ test/fixtures/mixed/bar+line-stacked.png | Bin 0 -> 17971 bytes test/specs/core.datasetController.tests.js | 16 +-- 8 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 docs/samples/other-charts/stacked-bar-line.md create mode 100644 test/fixtures/mixed/bar+line-stacked.js create mode 100644 test/fixtures/mixed/bar+line-stacked.png diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 679ac299a67..6a5595cb5bc 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -126,6 +126,7 @@ module.exports = { 'other-charts/radar', 'other-charts/radar-skip-points', 'other-charts/combo-bar-line', + 'other-charts/stacked-bar-line', ] }, { diff --git a/docs/charts/bar.md b/docs/charts/bar.md index d71b553deca..d2ea8ba45d6 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -286,7 +286,7 @@ The following dataset properties are specific to stacked bar charts. | Name | Type | Description | ---- | ---- | ----------- -| `stack` | `string` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). +| `stack` | `string` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). Defaults to `bar`. ## Horizontal Bar Chart diff --git a/docs/general/data-structures.md b/docs/general/data-structures.md index b66f9e2a597..778aa295ab6 100644 --- a/docs/general/data-structures.md +++ b/docs/general/data-structures.md @@ -67,7 +67,7 @@ In this mode, property name is used for `index` scale and value for `value` scal | `label` | `string` | The label for the dataset which appears in the legend and tooltips. | `clip` | `number`\|`object` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: clip: {left: 5, top: false, right: -2, bottom: 0} | `order` | `number` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. -| `stack` | `string` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). +| `stack` | `string` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). Defaults to dataset `type`. | `parsing` | `boolean`\|`object` | How to parse the dataset. The parsing can be disabled by specifying parsing: false at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. | `hidden` | `boolean` | Configure the visibility of the dataset. Using `hidden: true` will hide the dataset from being rendered in the Chart. diff --git a/docs/samples/other-charts/stacked-bar-line.md b/docs/samples/other-charts/stacked-bar-line.md new file mode 100644 index 00000000000..1afbbd4aaee --- /dev/null +++ b/docs/samples/other-charts/stacked-bar-line.md @@ -0,0 +1,120 @@ +# Stacked bar/line + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + borderWidth: 1, + stack: 'combined', + data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (var index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + stack: 'combined', + type: 'bar' + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + stack: 'combined' + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Chart.js Stacked Line/Bar Chart' + } + }, + scales: { + y: { + stacked: true + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index e1a0f48e912..8d8e9a95ec9 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -111,7 +111,7 @@ function isStacked(scale, meta) { } function getStackKey(indexScale, valueScale, meta) { - return indexScale.id + '.' + valueScale.id + '.' + meta.stack + '.' + meta.type; + return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`; } function getUserBounds(scale) { diff --git a/test/fixtures/mixed/bar+line-stacked.js b/test/fixtures/mixed/bar+line-stacked.js new file mode 100644 index 00000000000..17f28aa0ba1 --- /dev/null +++ b/test/fixtures/mixed/bar+line-stacked.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + data: { + datasets: [ + { + type: 'bar', + stack: 'mixed', + data: [5, 20, 1, 10], + backgroundColor: '#00ff00', + borderColor: '#ff0000' + }, + { + type: 'line', + stack: 'mixed', + data: [6, 16, 3, 19], + borderColor: '#0000ff', + fill: false + }, + ] + }, + options: { + scales: { + x: { + axis: 'y', + labels: ['a', 'b', 'c', 'd'] + }, + y: { + stacked: true + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/mixed/bar+line-stacked.png b/test/fixtures/mixed/bar+line-stacked.png new file mode 100644 index 0000000000000000000000000000000000000000..4768675e3da2c8238371ede7840ef1b2fbc1b4a8 GIT binary patch literal 17971 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 zat