]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
'stack' mode for filler (#7705)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 11 Aug 2020 13:31:18 +0000 (16:31 +0300)
committerGitHub <noreply@github.com>
Tue, 11 Aug 2020 13:31:18 +0000 (09:31 -0400)
'stack' mode for filler

docs/docs/charts/area.md
src/plugins/plugin.filler.js
test/fixtures/plugin.filler/fill-line-stack.json [new file with mode: 0644]
test/fixtures/plugin.filler/fill-line-stack.png [new file with mode: 0644]

index 31953d66a5916b9d854d3d327f905436418c9e34..c915208daa1512cd1641486304c95881e38d6386 100644 (file)
@@ -14,12 +14,15 @@ Both [line](line.md) and [radar](radar.md) charts support a `fill` option on the
 | Relative dataset index <sup>1</sup> | `string` | `'-1'`, `'-2'`, `'+1'`, ... |
 | Boundary <sup>2</sup> | `string` | `'start'`, `'end'`, `'origin'` |
 | Disabled <sup>3</sup> | `boolean` | `false` |
+| Stacked value below <sup>4</sup> | `string` | `'stack'` |
 
 > <sup>1</sup> dataset filling modes have been introduced in version 2.6.0<br/>
-> <sup>2</sup> prior version 2.6.0, boundary values was `'zero'`, `'top'`, `'bottom'` (deprecated)<br/>
+> <sup>2</sup> prior version 2.6.0, boundary values was `'zero'`, `'top'`, `'bottom'` (not supported anymore)<br/>
 > <sup>3</sup> for backward compatibility, `fill: true` (default) is equivalent to `fill: 'origin'`<br/>
+> <sup>4</sup> stack mode has been introduced in version 3.0.0<br/>
 
 **Example**
+
 ```javascript
 new Chart(ctx, {
     data: {
@@ -43,6 +46,7 @@ If you need to support multiple colors when filling from one dataset to another,
 | `below` | `Color` | Same as the above. |
 
 **Example**
+
 ```javascript
 new Chart(ctx, {
     data: {
@@ -60,16 +64,19 @@ new Chart(ctx, {
 ```
 
 ## Configuration
+
 | Option | Type | Default | Description |
 | :--- | :--- | :--- | :--- |
 | [`plugins.filler.propagate`](#propagate) | `boolean` | `true` | Fill propagation when target is hidden.
 
 ### propagate
+
 `propagate` takes a `boolean` value (default: `true`).
 
 If `true`, the fill area will be recursively extended to the visible target defined by the `fill` value of hidden dataset targets:
 
 **Example**
+
 ```javascript
 new Chart(ctx, {
     data: {
@@ -92,8 +99,8 @@ new Chart(ctx, {
 ```
 
 `propagate: true`:
-- if dataset 2 is hidden, dataset 4 will fill to dataset 1
-- if dataset 2 and 1 are hidden, dataset 4 will fill to `'origin'`
+-if dataset 2 is hidden, dataset 4 will fill to dataset 1
+-if dataset 2 and 1 are hidden, dataset 4 will fill to `'origin'`
 
 `propagate: false`:
-- if dataset 2 and/or 4 are hidden, dataset 4 will not be filled
+-if dataset 2 and/or 4 are hidden, dataset 4 will not be filled
index 1ed214f182cef8a7eeaec0d128d59c5ae47cce50..afb2e31057650534cf6e792b30244e275f37a563 100644 (file)
@@ -10,12 +10,25 @@ import {clipArea, unclipArea} from '../helpers/helpers.canvas';
 import {isArray, isFinite, valueOrDefault} from '../helpers/helpers.core';
 import {_normalizeAngle} from '../helpers/helpers.math';
 
+/**
+ * @typedef { import('../core/core.controller').default } Chart
+ * @typedef { import('../core/core.scale').default } Scale
+ * @typedef { import("../elements/element.point").default } Point
+ */
+
+/**
+ * @param {Chart} chart
+ * @param {number} index
+ */
 function getLineByIndex(chart, index) {
        const meta = chart.getDatasetMeta(index);
        const visible = meta && chart.isDatasetVisible(index);
        return visible ? meta.dataset : null;
 }
 
+/**
+ * @param {Line} line
+ */
 function parseFillOption(line) {
        const options = line.options;
        const fillOption = options.fill;
@@ -35,7 +48,11 @@ function parseFillOption(line) {
        return fill;
 }
 
-// @todo if (fill[0] === '#')
+/**
+ * @param {Line} line
+ * @param {number} index
+ * @param {number} count
+ */
 function decodeFill(line, index, count) {
        const fill = parseFillOption(line);
        let target = parseFloat(fill);
@@ -52,7 +69,7 @@ function decodeFill(line, index, count) {
                return target;
        }
 
-       return ['origin', 'start', 'end'].indexOf(fill) >= 0 ? fill : false;
+       return ['origin', 'start', 'end', 'stack'].indexOf(fill) >= 0 && fill;
 }
 
 function computeLinearBoundary(source) {
@@ -163,6 +180,103 @@ function pointsFromSegments(boundary, line) {
        return points;
 }
 
+/**
+ * @param {{ chart: Chart; scale: Scale; index: number; line: Line; }} source
+ * @return {Line}
+ */
+function buildStackLine(source) {
+       const {chart, scale, index, line} = source;
+       const linesBelow = getLinesBelow(chart, index);
+       const points = [];
+       const segments = line.segments;
+       const sourcePoints = line.points;
+       const startPoints = [];
+       sourcePoints.forEach(point => startPoints.push({x: point.x, y: scale.bottom, _prop: 'x', _ref: point}));
+       linesBelow.push(new Line({points: startPoints, options: {}}));
+
+       for (let i = 0; i < segments.length; i++) {
+               const segment = segments[i];
+               for (let j = segment.start; j <= segment.end; j++) {
+                       addPointsBelow(points, sourcePoints[j], linesBelow);
+               }
+       }
+       return new Line({points, options: {}, _refPoints: true});
+}
+
+/**
+ * @param {Chart} chart
+ * @param {number} index
+ * @return {Line[]}
+ */
+function getLinesBelow(chart, index) {
+       const below = [];
+       const metas = chart.getSortedVisibleDatasetMetas();
+       for (let i = 0; i < metas.length; i++) {
+               const meta = metas[i];
+               if (meta.index === index) {
+                       break;
+               }
+               if (meta.type === 'line') {
+                       below.unshift(meta.dataset);
+               }
+       }
+       return below;
+}
+
+/**
+ * @param {Point[]} points
+ * @param {Point} sourcePoint
+ * @param {Line[]} linesBelow
+ */
+function addPointsBelow(points, sourcePoint, linesBelow) {
+       const postponed = [];
+       for (let j = 0; j < linesBelow.length; j++) {
+               const line = linesBelow[j];
+               const {first, last, point} = findPoint(line, sourcePoint, 'x');
+
+               if (!point || (first && last)) {
+                       continue;
+               }
+               if (first) {
+                       // First point of an segment -> need to add another point before this,
+                       // from next line below.
+                       postponed.unshift(point);
+               } else {
+                       points.push(point);
+                       if (!last) {
+                               // In the middle of an segment, no need to add more points.
+                               break;
+                       }
+               }
+       }
+       points.push(...postponed);
+}
+
+/**
+ * @param {Line} line
+ * @param {Point} sourcePoint
+ * @param {string} property
+ * @returns {{point?: Point, first?: boolean, last?: boolean}}
+ */
+function findPoint(line, sourcePoint, property) {
+       const segments = line.segments;
+       const linePoints = line.points;
+       for (let i = 0; i < segments.length; i++) {
+               const segment = segments[i];
+               for (let j = segment.start; j <= segment.end; j++) {
+                       const point = linePoints[j];
+                       if (sourcePoint[property] === point[property]) {
+                               return {
+                                       first: j === segment.start,
+                                       last: j === segment.end,
+                                       point
+                               };
+                       }
+               }
+       }
+       return {};
+}
+
 function getTarget(source) {
        const {chart, fill, line} = source;
 
@@ -170,15 +284,29 @@ function getTarget(source) {
                return getLineByIndex(chart, fill);
        }
 
+       if (fill === 'stack') {
+               return buildStackLine(source);
+       }
+
        const boundary = computeBoundary(source);
-       let points = [];
-       let _loop = false;
-       let _refPoints = false;
 
        if (boundary instanceof simpleArc) {
                return boundary;
        }
 
+       return createBoundaryLine(boundary, line);
+}
+
+/**
+ * @param {Point[] | { x: number; y: number; }} boundary
+ * @param {Line} line
+ * @return {Line?}
+ */
+function createBoundaryLine(boundary, line) {
+       let points = [];
+       let _loop = false;
+       let _refPoints = false;
+
        if (isArray(boundary)) {
                _loop = true;
                // @ts-ignore
@@ -187,6 +315,7 @@ function getTarget(source) {
                points = pointsFromSegments(boundary, line);
                _refPoints = true;
        }
+
        return points.length ? new Line({
                points,
                options: {tension: 0},
@@ -402,6 +531,7 @@ export default {
                        if (line && line.options && line instanceof Line) {
                                source = {
                                        visible: chart.isDatasetVisible(i),
+                                       index: i,
                                        fill: decodeFill(line, i, count),
                                        chart,
                                        scale: meta.vScale,
diff --git a/test/fixtures/plugin.filler/fill-line-stack.json b/test/fixtures/plugin.filler/fill-line-stack.json
new file mode 100644 (file)
index 0000000..6e5e951
--- /dev/null
@@ -0,0 +1,64 @@
+{
+    "config": {
+        "type": "line",
+        "data": {
+            "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
+            "datasets": [{
+                "backgroundColor": "rgba(255, 0, 0, 0.25)",
+                "data": [null, null, 0, 1, 0, 1, null, 0, 1],
+                "fill": "stack"
+            }, {
+                "backgroundColor": "rgba(0, 255, 0, 0.25)",
+                "data": [1, 1, null, 1, 0, null, 1, 1, 0],
+                "fill": "stack"
+            }, {
+                "backgroundColor": "rgba(0, 0, 255, 0.25)",
+                "data": [0, 2, null, 2, 0, 2, 0],
+                "fill": "stack"
+            }, {
+                "backgroundColor": "rgba(255, 0, 255, 0.25)",
+                "data": [2, 0, null, 0, 2, 0, 2, 0, 2],
+                "fill": "stack"
+                       }, {
+                               "backgroundColor": "rgba(0, 0, 0, 0.25)",
+                               "data": [null, null, null, 2, null, 2, 2],
+                               "fill": "stack"
+                       }, {
+                "backgroundColor": "rgba(255, 255, 0, 0.25)",
+                "data": [3, 1, 1, 3, 1, 1, 3, 1, 1],
+                "fill": "stack"
+            }]
+        },
+        "options": {
+            "responsive": false,
+            "spanGaps": false,
+            "legend": false,
+            "title": false,
+            "scales": {
+                "x": {
+                    "display": false
+                },
+                "y": {
+                                       "display": false,
+                                       "stacked": true,
+                                       "min": 0
+                }
+            },
+            "elements": {
+                "point": {
+                    "radius": 0
+                },
+                "line": {
+                    "borderColor": "transparent",
+                    "tension": 0
+                }
+            }
+        }
+    },
+    "options": {
+        "canvas": {
+            "height": 256,
+            "width": 512
+        }
+    }
+}
diff --git a/test/fixtures/plugin.filler/fill-line-stack.png b/test/fixtures/plugin.filler/fill-line-stack.png
new file mode 100644 (file)
index 0000000..6c18c54
Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-stack.png differ