diff --git a/docs/docs/charts/area.md b/docs/docs/charts/area.md
index 31953d66a..c915208da 100644
--- a/docs/docs/charts/area.md
+++ b/docs/docs/charts/area.md
@@ -14,12 +14,15 @@ Both [line](line.md) and [radar](radar.md) charts support a `fill` option on the
| Relative dataset index 1 | `string` | `'-1'`, `'-2'`, `'+1'`, ... |
| Boundary 2 | `string` | `'start'`, `'end'`, `'origin'` |
| Disabled 3 | `boolean` | `false` |
+| Stacked value below 4 | `string` | `'stack'` |
> 1 dataset filling modes have been introduced in version 2.6.0
-> 2 prior version 2.6.0, boundary values was `'zero'`, `'top'`, `'bottom'` (deprecated)
+> 2 prior version 2.6.0, boundary values was `'zero'`, `'top'`, `'bottom'` (not supported anymore)
> 3 for backward compatibility, `fill: true` (default) is equivalent to `fill: 'origin'`
+> 4 stack mode has been introduced in version 3.0.0
**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
diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js
index 1ed214f18..afb2e3105 100644
--- a/src/plugins/plugin.filler.js
+++ b/src/plugins/plugin.filler.js
@@ -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
index 000000000..6e5e951f3
--- /dev/null
+++ b/test/fixtures/plugin.filler/fill-line-stack.json
@@ -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
index 000000000..6c18c54e7
Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-stack.png differ