From: Jukka Kurkela Date: Tue, 11 Aug 2020 13:31:18 +0000 (+0300) Subject: 'stack' mode for filler (#7705) X-Git-Tag: v3.0.0-beta.2~16 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=6f83c55be5417e75beb50fc12b39bc6c145f5dbf;p=thirdparty%2FChart.js.git 'stack' mode for filler (#7705) 'stack' mode for filler --- 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