'stack' mode for filler (#7705)

'stack' mode for filler
This commit is contained in:
Jukka Kurkela
2020-08-11 16:31:18 +03:00
committed by Evert Timberg
parent 483d06eebd
commit 5e194021c7
4 changed files with 210 additions and 9 deletions

View 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

View 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,

View File

@@ -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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB