]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add `'single'` mode for stacking (#8586)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Sun, 7 Mar 2021 15:52:31 +0000 (17:52 +0200)
committerGitHub <noreply@github.com>
Sun, 7 Mar 2021 15:52:31 +0000 (10:52 -0500)
* Add `'single'` mode for stacking

* Update fixture

docs/docs/axes/_common.md
docs/docs/axes/index.mdx
docs/docs/charts/area.md
samples/charts/area/line-stacked.html
src/controllers/controller.bar.js
src/controllers/controller.line.js
src/core/core.datasetController.js
test/fixtures/controller.line/stacking/single.js [new file with mode: 0644]
test/fixtures/controller.line/stacking/single.png [new file with mode: 0644]
types/index.esm.d.ts

index b0ca57018853f313694f962b22c7c148c9978e55..9e35464611c34edbea4e1cc5669094d060f4ff76 100644 (file)
@@ -10,6 +10,7 @@ Namespace: `options.scales[scaleId]`
 | `min` | `number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](./index.mdx#axis-range-settings)
 | `max` | `number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](./index.mdx#axis-range-settings)
 | `reverse` | `boolean` | `false` | Reverse the scale.
+| `stacked` | `boolean`\|`string` | `false` | Should the data be stacked. [more...](./index.mdx#stacking)
 | `suggestedMax` | `number` | | Adjustment used when calculating the maximum data value. [more...](./index.mdx#axis-range-settings)
 | `suggestedMin` | `number` | | Adjustment used when calculating the minimum data value. [more...](./index.mdx#axis-range-settings)
 | `ticks` | `object` | | Tick configuration. [more...](#tick-configuration)
index 7a83229deee4f2ca76928a10600750fa2463555c..0d7be61cc380fdeb3145c7bdb1e1420212972b24 100644 (file)
@@ -60,7 +60,12 @@ let chart = new Chart(ctx, {
 
 In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible.
 
-### Callbacks
+## Stacking
+
+By default data is not stacked. If the `stacked` option of the value scale (y-axis on horizontal chart) is `true`, positive and negative values are stacked separately. Additionally a `stack` option can be defined per dataset to further divide into stack groups [more...](../general/data-structures/#dataset-configuration).
+For some charts, you might want to stack positive and negative values together. That can be achieved by specifying `stacked: 'single'`.
+
+## Callbacks
 
 There are a number of config callbacks that can be used to change parameters in the scale at different points in the update process. The options are supplied at the top level of the axis options.
 
index 31d0e4a8a68b3e85dd251eaeedacd2f53ff7d8da..a29b71e9a40230b7a53ad9ced8e24e1fea1bcb41 100644 (file)
@@ -14,7 +14,7 @@ Both [line](./line.mdx) and [radar](./radar.mdx) charts support a `fill` option
 | Relative dataset index | `string` | `'-1'`, `'-2'`, `'+1'`, ... |
 | Boundary | `string` | `'start'`, `'end'`, `'origin'` |
 | Disabled <sup>1</sup> | `boolean` | `false` |
-| Stacked value below <sup>4</sup> | `string` | `'stack'` |
+| Stacked value below | `string` | `'stack'` |
 | Axis value | `object` | `{ value: number; }` |
 
 > <sup>1</sup> for backward compatibility, `fill: true` is equivalent to `fill: 'origin'`<br/>
index d74db48763158eaf0b0241fb89cb07baadf760df..eaee5f0a45983106ba3336df148a59eafa26f1e0 100644 (file)
                <canvas id="canvas"></canvas>
        </div>
        <br>
+       <div style="width:75%;">
+               <canvas id="single"></canvas>
+       </div>
+       <br>
        <br>
        <button id="randomizeData">Randomize Data</button>
        <button id="addDataset">Add Dataset</button>
        <button id="removeData">Remove Data</button>
        <script>
                var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
-               var config = {
+               var data = {
+                       labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+                       datasets: [{
+                               label: 'My First dataset',
+                               borderColor: window.chartColors.red,
+                               backgroundColor: window.chartColors.red,
+                               data: [
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor()
+                               ],
+                               fill: true
+                       }, {
+                               label: 'My Second dataset',
+                               borderColor: window.chartColors.blue,
+                               backgroundColor: window.chartColors.blue,
+                               data: [
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor()
+                               ],
+                               fill: true
+                       }, {
+                               label: 'My Third dataset',
+                               borderColor: window.chartColors.green,
+                               backgroundColor: window.chartColors.green,
+                               data: [
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor()
+                               ],
+                               fill: true
+                       }, {
+                               label: 'My Fourth dataset',
+                               borderColor: window.chartColors.yellow,
+                               backgroundColor: window.chartColors.yellow,
+                               data: [
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor(),
+                                       randomScalingFactor()
+                               ],
+                               fill: true
+                       }]
+               };
+               var config = (stacked) => ({
                        type: 'line',
-                       data: {
-                               labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
-                               datasets: [{
-                                       label: 'My First dataset',
-                                       borderColor: window.chartColors.red,
-                                       backgroundColor: window.chartColors.red,
-                                       data: [
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor()
-                                       ],
-                                       fill: true
-                               }, {
-                                       label: 'My Second dataset',
-                                       borderColor: window.chartColors.blue,
-                                       backgroundColor: window.chartColors.blue,
-                                       data: [
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor()
-                                       ],
-                                       fill: true
-                               }, {
-                                       label: 'My Third dataset',
-                                       borderColor: window.chartColors.green,
-                                       backgroundColor: window.chartColors.green,
-                                       data: [
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor()
-                                       ],
-                                       fill: true
-                               }, {
-                                       label: 'My Fourth dataset',
-                                       borderColor: window.chartColors.yellow,
-                                       backgroundColor: window.chartColors.yellow,
-                                       data: [
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor(),
-                                               randomScalingFactor()
-                                       ],
-                                       fill: true
-                               }]
-                       },
+                       data,
                        options: {
                                responsive: true,
                                plugins: {
                                        title: {
                                                display: true,
-                                               text: 'Chart.js Line Chart - Stacked Area'
+                                               text: stacked === true ? 'Chart.js Line Chart - Stacked Area' : 'Same data, stacked=\'single\'',
                                        },
                                        tooltip: {
                                                mode: 'index',
                                        }
                                },
-                               hover: {
-                                       mode: 'index'
+                               interaction: {
+                                       mode: 'nearest',
+                                       axis: 'x',
+                                       intersect: false
                                },
                                scales: {
                                        x: {
                                                }
                                        },
                                        y: {
-                                               stacked: true,
+                                               stacked,
                                                title: {
                                                        display: true,
                                                        text: 'Value'
                                        }
                                }
                        }
-               };
+               });
 
                window.onload = function() {
-                       var ctx = document.getElementById('canvas').getContext('2d');
-                       window.myLine = new Chart(ctx, config);
+                       window.myLine = new Chart('canvas', config(true));
+                       window.myLine2 = new Chart('single', config('single'));
                };
 
                document.getElementById('randomizeData').addEventListener('click', function() {
-                       config.data.datasets.forEach(function(dataset) {
+                       data.datasets.forEach(function(dataset) {
                                dataset.data = dataset.data.map(function() {
                                        return randomScalingFactor();
                                });
                        });
 
                        window.myLine.update();
+                       window.myLine2.update();
                });
 
                var colorNames = Object.keys(window.chartColors);
                document.getElementById('addDataset').addEventListener('click', function() {
-                       var colorName = colorNames[config.data.datasets.length % colorNames.length];
+                       var colorName = colorNames[data.datasets.length % colorNames.length];
                        var newColor = window.chartColors[colorName];
                        var newDataset = {
-                               label: 'Dataset ' + config.data.datasets.length,
+                               label: 'Dataset ' + data.datasets.length,
                                borderColor: newColor,
                                backgroundColor: newColor,
                                data: [],
                        };
 
-                       for (var index = 0; index < config.data.labels.length; ++index) {
+                       for (var index = 0; index < data.labels.length; ++index) {
                                newDataset.data.push(randomScalingFactor());
                        }
 
-                       config.data.datasets.push(newDataset);
+                       data.datasets.push(newDataset);
                        window.myLine.update();
+                       window.myLine2.update();
                });
 
                document.getElementById('addData').addEventListener('click', function() {
-                       if (config.data.datasets.length > 0) {
-                               var month = MONTHS[config.data.labels.length % MONTHS.length];
-                               config.data.labels.push(month);
+                       if (data.datasets.length > 0) {
+                               var month = MONTHS[data.labels.length % MONTHS.length];
+                               data.labels.push(month);
 
-                               config.data.datasets.forEach(function(dataset) {
+                               data.datasets.forEach(function(dataset) {
                                        dataset.data.push(randomScalingFactor());
                                });
 
                                window.myLine.update();
+                               window.myLine2.update();
                        }
                });
 
                document.getElementById('removeDataset').addEventListener('click', function() {
-                       config.data.datasets.splice(0, 1);
+                       data.datasets.splice(0, 1);
                        window.myLine.update();
+                       window.myLine2.update();
                });
 
                document.getElementById('removeData').addEventListener('click', function() {
-                       config.data.labels.splice(-1, 1); // remove the label first
+                       data.labels.splice(-1, 1); // remove the label first
 
-                       config.data.datasets.forEach(function(dataset) {
+                       data.datasets.forEach(function(dataset) {
                                dataset.data.pop();
                        });
 
                        window.myLine.update();
+                       window.myLine2.update();
                });
        </script>
 </body>
index 9bf5ef32e4c3adbb39d2e0907be2e3c8ad5412ab..2a8c6c8e02dd1cec9f0333b1c653c2db6400335e 100644 (file)
@@ -401,15 +401,14 @@ export default class BarController extends DatasetController {
         */
   _calculateBarValuePixels(index) {
     const me = this;
-    const meta = me._cachedMeta;
-    const vScale = meta.vScale;
+    const {vScale, _stacked} = me._cachedMeta;
     const {base: baseValue, minBarLength} = me.options;
     const parsed = me.getParsed(index);
     const custom = parsed._custom;
     const floating = isFloatBar(custom);
     let value = parsed[vScale.axis];
     let start = 0;
-    let length = meta._stacked ? me.applyStack(vScale, parsed) : value;
+    let length = _stacked ? me.applyStack(vScale, parsed, _stacked) : value;
     let head, size;
 
     if (length !== value) {
index ca56046e802199fe13f603676e9e539fba59f3fb..82b8894dd34d3b28400c186cef87a06a48bee27d 100644 (file)
@@ -62,7 +62,7 @@ export default class LineController extends DatasetController {
       const parsed = me.getParsed(i);
       const properties = directUpdate ? point : {};
       const x = properties.x = xScale.getPixelForValue(parsed.x, i);
-      const y = properties.y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me.applyStack(yScale, parsed) : parsed.y, i);
+      const y = properties.y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(_stacked ? me.applyStack(yScale, parsed, _stacked) : parsed.y, i);
       properties.skip = isNaN(x) || isNaN(y);
       properties.stop = i > 0 && (parsed.x - prevParsed.x) > maxGapLength;
 
index ba1156444355c9a1acbc16b6ad4b8d33301b34d2..f33cb6da93ad5fb1fbabb66cdf889e57a1515903 100644 (file)
@@ -66,8 +66,9 @@ function getSortedDatasetIndices(chart, filterVisible) {
   return keys;
 }
 
-function applyStack(stack, value, dsIndex, allOther) {
+function applyStack(stack, value, dsIndex, options) {
   const keys = stack.keys;
+  const singleMode = options.mode === 'single';
   let i, ilen, datasetIndex, otherValue;
 
   if (value === null) {
@@ -77,13 +78,13 @@ function applyStack(stack, value, dsIndex, allOther) {
   for (i = 0, ilen = keys.length; i < ilen; ++i) {
     datasetIndex = +keys[i];
     if (datasetIndex === dsIndex) {
-      if (allOther) {
+      if (options.all) {
         continue;
       }
       break;
     }
     otherValue = stack.values[datasetIndex];
-    if (isFinite(otherValue) && (value === 0 || sign(value) === sign(otherValue))) {
+    if (isFinite(otherValue) && (singleMode || (value === 0 || sign(value) === sign(otherValue)))) {
       value += otherValue;
     }
   }
@@ -517,7 +518,7 @@ export default class DatasetController {
   /**
         * @protected
         */
-  applyStack(scale, parsed) {
+  applyStack(scale, parsed, mode) {
     const chart = this.chart;
     const meta = this._cachedMeta;
     const value = parsed[scale.axis];
@@ -525,7 +526,7 @@ export default class DatasetController {
       keys: getSortedDatasetIndices(chart, true),
       values: parsed._stacks[scale.axis]
     };
-    return applyStack(stack, value, meta.index);
+    return applyStack(stack, value, meta.index, {mode});
   }
 
   /**
@@ -541,7 +542,7 @@ export default class DatasetController {
       // in addition to the stacked value
       range.min = Math.min(range.min, value);
       range.max = Math.max(range.max, value);
-      value = applyStack(stack, parsedValue, this._cachedMeta.index, true);
+      value = applyStack(stack, parsedValue, this._cachedMeta.index, {all: true});
     }
     range.min = Math.min(range.min, value);
     range.max = Math.max(range.max, value);
diff --git a/test/fixtures/controller.line/stacking/single.js b/test/fixtures/controller.line/stacking/single.js
new file mode 100644 (file)
index 0000000..9251988
--- /dev/null
@@ -0,0 +1,51 @@
+module.exports = {
+  config: {
+    type: 'line',
+    data: {
+      labels: [0, 1, 2],
+      datasets: [
+        {
+          data: [0, -1, -1],
+          backgroundColor: '#ff0000',
+        },
+        {
+          data: [0, 2, 2],
+          backgroundColor: '#00ff00',
+        },
+        {
+          data: [0, 0, 1],
+          backgroundColor: '#0000ff',
+        }
+      ]
+    },
+    options: {
+      elements: {
+        line: {
+          fill: '-1',
+        },
+        point: {
+          radius: 0
+        }
+      },
+      layout: {
+        padding: 32
+      },
+      plugins: {
+        legend: false,
+        title: false,
+        tooltip: false,
+        filler: true
+      },
+      scales: {
+        x: {display: false},
+        y: {display: false, stacked: 'single'}
+      }
+    }
+  },
+  options: {
+    canvas: {
+      height: 256,
+      width: 512
+    }
+  }
+};
diff --git a/test/fixtures/controller.line/stacking/single.png b/test/fixtures/controller.line/stacking/single.png
new file mode 100644 (file)
index 0000000..595773c
Binary files /dev/null and b/test/fixtures/controller.line/stacking/single.png differ
index 4052e0d1f43f88e210217894bf3f7f3035b7ec98..7d59db582c032f0e2d00ff2f79d6b813fbab5efe 100644 (file)
@@ -427,7 +427,7 @@ export interface ChartMeta<TElement extends Element = Element, TDatasetElement e
   vScale?: Scale;
 
   _sorted: boolean;
-  _stacked: boolean;
+  _stacked: boolean | 'single';
   _parsed: unknown[];
 }
 
@@ -600,7 +600,7 @@ export class DatasetController<
     range: { min: number; max: number },
     scale: Scale,
     parsed: unknown[],
-    stack: boolean
+    stack: boolean | string
   ): void;
   protected getMinMax(scale: Scale, canStack?: boolean): { min: number; max: number };
 }