From 5e194021c7b6ec21566e64dd671441b7797c35fb 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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 literal 0 HcmV?d00001