Allow filling above and below with different colors (#6318)

Two colors allowed : first one to fill above the target, second to fill below
Tests added
Docs edited
This commit is contained in:
L M 2019-11-21 19:48:31 +01:00 committed by Evert Timberg
parent ea90365972
commit 18e3bc0624
15 changed files with 497 additions and 30 deletions

View File

@ -32,6 +32,31 @@ new Chart(ctx, {
});
```
If you need to support multiple colors when filling from one dataset to another, you may specify an object with the following option :
| Param | Type | Description |
| :--- | :--- | :--- |
| `target` | `number`, `string`, `boolean` | The accepted values are the same as the filling mode values, so you may use absolute and relative dataset indexes and/or boundaries. |
| `above` | `Color` | If no color is set, the default color will be the background color of the chart. |
| `below` | `Color` | Same as the above. |
**Example**
```javascript
new Chart(ctx, {
data: {
datasets: [
{
fill: {
target: 'origin',
above: 'rgb(255, 0, 0)', // Area will be red above the origin
below: 'rgb(0, 0, 255)' // And blue below the origin
}
}
]
}
});
```
## Configuration
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |

View File

@ -56,7 +56,8 @@ var mappers = {
// @todo if (fill[0] === '#')
function decodeFill(el, index, count) {
var model = el._model || {};
var fill = model.fill;
var fillOption = model.fill;
var fill = fillOption && typeof fillOption.target !== 'undefined' ? fillOption.target : fillOption;
var target;
if (fill === undefined) {
@ -235,50 +236,121 @@ function isDrawable(point) {
return point && !point.skip;
}
function drawArea(ctx, curve0, curve1, len0, len1, stepped, tension) {
const lineTo = stepped ? helpers.canvas._steppedLineTo : helpers.canvas._bezierCurveTo;
let i, cx, cy, r, target;
function fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets) {
const fillAreaPointsSet = [];
const clipAboveAreaPointsSet = [];
const clipBelowAreaPointsSet = [];
const radialSet = [];
const jointPoint = {};
let i, cx, cy, r;
if (!len0 || !len1) {
return;
}
clipAboveAreaPointsSet.push({x: curve1[len1 - 1].x, y: area.top});
clipBelowAreaPointsSet.push({x: curve0[0].x, y: area.top});
clipBelowAreaPointsSet.push(curve0[0]);
// building first area curve (normal)
ctx.moveTo(curve0[0].x, curve0[0].y);
fillAreaPointsSet.push(curve0[0]);
for (i = 1; i < len0; ++i) {
target = curve0[i];
if (!target.boundary && (tension || stepped)) {
lineTo(ctx, curve0[i - 1], target, false, stepped);
} else {
ctx.lineTo(target.x, target.y);
}
curve0[i].flip = false;
fillAreaPointsSet.push(curve0[i]);
clipBelowAreaPointsSet.push(curve0[i]);
}
if (curve1[0].angle !== undefined) {
pointSets.fill.push(fillAreaPointsSet);
cx = curve1[0].cx;
cy = curve1[0].cy;
r = Math.sqrt(Math.pow(curve1[0].x - cx, 2) + Math.pow(curve1[0].y - cy, 2));
for (i = len1 - 1; i > 0; --i) {
ctx.arc(cx, cy, r, curve1[i].angle, curve1[i - 1].angle, true);
radialSet.push({cx: cx, cy: cy, radius: r, startAngle: curve1[i].angle, endAngle: curve1[i - 1].angle});
}
if (radialSet.length) {
pointSets.fill.push(radialSet);
}
return;
}
// joining the two area curves
ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y);
for (var key in curve1[len1 - 1]) {
if (Object.prototype.hasOwnProperty.call(curve1[len1 - 1], key)) {
jointPoint[key] = curve1[len1 - 1][key];
}
}
jointPoint.joint = true;
fillAreaPointsSet.push(jointPoint);
// building opposite area curve (reverse)
for (i = len1 - 1; i > 0; --i) {
target = curve1[i - 1];
if (!target.boundary && (tension || stepped)) {
lineTo(ctx, curve1[i], target, true, stepped);
} else {
ctx.lineTo(target.x, target.y);
}
curve1[i].flip = true;
clipAboveAreaPointsSet.push(curve1[i]);
curve1[i - 1].flip = true;
fillAreaPointsSet.push(curve1[i - 1]);
}
clipAboveAreaPointsSet.push(curve1[0]);
clipAboveAreaPointsSet.push({x: curve1[0].x, y: area.top});
clipBelowAreaPointsSet.push({x: curve0[len0 - 1].x, y: area.top});
pointSets.clipAbove.push(clipAboveAreaPointsSet);
pointSets.clipBelow.push(clipBelowAreaPointsSet);
pointSets.fill.push(fillAreaPointsSet);
}
function doFill(ctx, points, mapper, el) {
function clipAndFill(ctx, clippingPointsSets, fillingPointsSets, color, stepped, tension) {
const lineTo = stepped ? helpers.canvas._steppedLineTo : helpers.canvas._bezierCurveTo;
let i, ilen, j, jlen, set, target;
if (clippingPointsSets) {
ctx.save();
ctx.beginPath();
for (i = 0, ilen = clippingPointsSets.length; i < ilen; i++) {
set = clippingPointsSets[i];
// Have edge lines straight
ctx.moveTo(set[0].x, set[0].y);
ctx.lineTo(set[1].x, set[1].y);
for (j = 2, jlen = set.length; j < jlen - 1; j++) {
target = set[j];
if (!target.boundary && (tension || stepped)) {
lineTo(ctx, set[j - 1], target, target.flip, stepped);
} else {
ctx.lineTo(target.x, target.y);
}
}
ctx.lineTo(set[j].x, set[j].y);
}
ctx.closePath();
ctx.clip();
ctx.beginPath();
}
for (i = 0, ilen = fillingPointsSets.length; i < ilen; i++) {
set = fillingPointsSets[i];
if (set[0].startAngle !== undefined) {
for (j = 0, jlen = set.length; j < jlen; j++) {
ctx.arc(set[j].cx, set[j].cy, set[j].radius, set[j].startAngle, set[j].endAngle, true);
}
} else {
ctx.moveTo(set[0].x, set[0].y);
for (j = 1, jlen = set.length; j < jlen; j++) {
if (set[j].joint) {
ctx.lineTo(set[j].x, set[j].y);
} else {
target = set[j];
if (!target.boundary && (tension || stepped)) {
lineTo(ctx, set[j - 1], target, target.flip, stepped);
} else {
ctx.lineTo(target.x, target.y);
}
}
}
}
}
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
}
function doFill(ctx, points, mapper, colors, el, area) {
const count = points.length;
const view = el._view;
const loop = el._loop;
@ -289,8 +361,10 @@ function doFill(ctx, points, mapper, el) {
let curve1 = [];
let len0 = 0;
let len1 = 0;
let pointSets = {clipBelow: [], clipAbove: [], fill: []};
let i, ilen, index, p0, p1, d0, d1, loopOffset;
ctx.save();
ctx.beginPath();
for (i = 0, ilen = count; i < ilen; ++i) {
@ -310,7 +384,7 @@ function doFill(ctx, points, mapper, el) {
len1 = curve1.push(p1);
} else if (len0 && len1) {
if (!span) {
drawArea(ctx, curve0, curve1, len0, len1, stepped, tension);
fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets);
len0 = len1 = 0;
curve0 = [];
curve1 = [];
@ -325,11 +399,14 @@ function doFill(ctx, points, mapper, el) {
}
}
drawArea(ctx, curve0, curve1, len0, len1, stepped, tension);
fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets);
ctx.closePath();
ctx.fillStyle = view.backgroundColor;
ctx.fill();
if (colors.below !== colors.above) {
clipAndFill(ctx, pointSets.clipAbove, pointSets.fill, colors.above, stepped, tension);
clipAndFill(ctx, pointSets.clipBelow, pointSets.fill, colors.below, stepped, tension);
} else {
clipAndFill(ctx, false, pointSets.fill, colors.above, stepped, tension);
}
}
module.exports = {
@ -375,7 +452,7 @@ module.exports = {
beforeDatasetsDraw: function(chart) {
var metasets = chart._getSortedVisibleDatasetMetas();
var ctx = chart.ctx;
var meta, i, el, points, mapper;
var meta, i, el, view, points, mapper, color, colors, fillOption;
for (i = metasets.length - 1; i >= 0; --i) {
meta = metasets[i].$filler;
@ -385,12 +462,20 @@ module.exports = {
}
el = meta.el;
view = el._view;
points = el._children || [];
mapper = meta.mapper;
fillOption = meta.el._model.fill;
color = view.backgroundColor || defaults.global.defaultColor;
colors = {above: color, below: color};
if (fillOption && typeof fillOption === 'object') {
colors.above = fillOption.above || color;
colors.below = fillOption.below || color;
}
if (mapper && points.length) {
helpers.canvas.clipArea(ctx, chart.chartArea);
doFill(ctx, points, mapper, el);
doFill(ctx, points, mapper, colors, el, chart.chartArea);
helpers.canvas.unclipArea(ctx);
}
}

View File

@ -0,0 +1,54 @@
{
"config": {
"type": "line",
"data": {
"labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
"datasets": [{
"backgroundColor": "rgba(0, 0, 192, 0.25)",
"data": [null, null, 2, 3, 4, -4, -2, 1, 0]
}, {
"backgroundColor": "rgba(0, 192, 0, 0.25)",
"data": [6, 2, null, 4, 5, null, null, 2, 1]
}, {
"backgroundColor": "rgba(192, 0, 0, 0.25)",
"data": [7, 3, 4, 5, 6, 1, 4, null, null]
}, {
"backgroundColor": "rgba(0, 64, 192, 0.25)",
"data": [8, 7, 6, -6, -4, -6, 4, 5, 8]
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"display": false
}],
"yAxes": [{
"display": false
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"borderColor": "transparent",
"fill": {
"target": "origin",
"below": "rgba(255, 0, 0, 0.25)"
},
"tension": 0
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,54 @@
{
"config": {
"type": "line",
"data": {
"labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
"datasets": [{
"backgroundColor": "rgba(0, 0, 192, 0.25)",
"data": [null, null, 2, 4, 2, 1, -1, 1, 2]
}, {
"backgroundColor": "rgba(0, 192, 0, 0.25)",
"data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3]
}, {
"backgroundColor": "rgba(192, 0, 0, 0.25)",
"data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null]
}, {
"backgroundColor": "rgba(128, 0, 128, 0.25)",
"data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5]
}]
},
"options": {
"responsive": false,
"spanGaps": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"display": false
}],
"yAxes": [{
"display": false
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent",
"fill": {
"target": "origin",
"below": "transparent"
}
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -0,0 +1,48 @@
{
"config": {
"type": "line",
"data": {
"labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
"datasets": [{
"backgroundColor": "rgba(255, 0, 0, 0.25)",
"data": [0, 1, 2, -1, 0, 2, 1, -1, -2],
"fill": {
"target": "+1",
"above": "rgba(255, 0, 0, 0.25)",
"below": "rgba(0, 0, 255, 0.25)"
}
}, {
"data": [0, 0, 0, 0, 0, 0, 0, 0, 0]
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"display": false
}],
"yAxes": [{
"display": false
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"borderColor": "transparent",
"tension": 0
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,71 @@
{
"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, 0, -1, 0],
"fill": {
"target": 1,
"above": "rgba(255, 0, 0, 0.25)",
"below": "rgba(122, 0, 0, 0.25)"
}
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": {
"target": "+1",
"above": "rgba(0, 255, 0, 0.25)",
"below": "rgba(0, 255, 120, 0.25)"
}
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": {
"target": "-2",
"above": "rgba(255, 0, 255, 0.25)",
"below": "rgba(255, 0, 120, 0.25)"
}
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": {
"target": "-1",
"above": "rgba(255, 255, 0, 0.25)",
"below": "rgba(255, 120, 0, 0.25)"
}
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"display": false
}],
"yAxes": [{
"display": false
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent",
"tension": 0
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,66 @@
{
"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, 0, -1, 0],
"fill": {
"target": 1,
"below": "transparent"
}
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": {
"target": "+1",
"below": "transparent"
}
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": {
"target": "-2",
"below": "transparent"
}
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": {
"target": "-1",
"below": "transparent"
}
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"display": false
}],
"yAxes": [{
"display": false
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent"
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,66 @@
{
"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, 0, -1, 0],
"fill": {
"target": 1,
"above": "transparent"
}
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": {
"target": "+1",
"above": "transparent"
}
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": {
"target": "-2",
"above": "transparent"
}
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": {
"target": "-1",
"above": "transparent"
}
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"display": false
}],
"yAxes": [{
"display": false
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent"
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -135,7 +135,6 @@ describe('Plugin.filler', function() {
{fill: ''},
{fill: null},
{fill: []},
{fill: {}},
]
}
});
@ -153,7 +152,6 @@ describe('Plugin.filler', function() {
false, // empty string
false, // null
false, // array
false, // object
]);
});
});