From 6f83c55be5417e75beb50fc12b39bc6c145f5dbf Mon Sep 17 00:00:00 2001 From: Jukka Kurkela Date: Tue, 11 Aug 2020 16:31:18 +0300 Subject: [PATCH] 'stack' mode for filler (#7705) 'stack' mode for filler --- docs/docs/charts/area.md | 15 +- src/plugins/plugin.filler.js | 140 +++++++++++++++++- .../plugin.filler/fill-line-stack.json | 64 ++++++++ .../plugin.filler/fill-line-stack.png | Bin 0 -> 16184 bytes 4 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 test/fixtures/plugin.filler/fill-line-stack.json create mode 100644 test/fixtures/plugin.filler/fill-line-stack.png 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 0000000000000000000000000000000000000000..6c18c54e7e51e3c6b87cbb43d35cde81baa20495 GIT binary patch literal 16184 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 VF1^$4f&l)}(K66{t>Jk0{{zvnNjLxi literal 0 Hc-jL100001 -- 2.47.3