Skip to content

Commit 18e3bc0

Browse files
charlesmassetimberg
authored andcommitted
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
1 parent ea90365 commit 18e3bc0

15 files changed

+495
-28
lines changed

docs/charts/area.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ new Chart(ctx, {
3232
});
3333
```
3434

35+
If you need to support multiple colors when filling from one dataset to another, you may specify an object with the following option :
36+
37+
| Param | Type | Description |
38+
| :--- | :--- | :--- |
39+
| `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. |
40+
| `above` | `Color` | If no color is set, the default color will be the background color of the chart. |
41+
| `below` | `Color` | Same as the above. |
42+
43+
**Example**
44+
```javascript
45+
new Chart(ctx, {
46+
data: {
47+
datasets: [
48+
{
49+
fill: {
50+
target: 'origin',
51+
above: 'rgb(255, 0, 0)', // Area will be red above the origin
52+
below: 'rgb(0, 0, 255)' // And blue below the origin
53+
}
54+
}
55+
]
56+
}
57+
});
58+
```
59+
3560
## Configuration
3661
| Option | Type | Default | Description |
3762
| :--- | :--- | :--- | :--- |

src/plugins/plugin.filler.js

Lines changed: 111 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ var mappers = {
5656
// @todo if (fill[0] === '#')
5757
function decodeFill(el, index, count) {
5858
var model = el._model || {};
59-
var fill = model.fill;
59+
var fillOption = model.fill;
60+
var fill = fillOption && typeof fillOption.target !== 'undefined' ? fillOption.target : fillOption;
6061
var target;
6162

6263
if (fill === undefined) {
@@ -235,50 +236,121 @@ function isDrawable(point) {
235236
return point && !point.skip;
236237
}
237238

238-
function drawArea(ctx, curve0, curve1, len0, len1, stepped, tension) {
239-
const lineTo = stepped ? helpers.canvas._steppedLineTo : helpers.canvas._bezierCurveTo;
240-
let i, cx, cy, r, target;
239+
function fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets) {
240+
const fillAreaPointsSet = [];
241+
const clipAboveAreaPointsSet = [];
242+
const clipBelowAreaPointsSet = [];
243+
const radialSet = [];
244+
const jointPoint = {};
245+
let i, cx, cy, r;
241246

242247
if (!len0 || !len1) {
243248
return;
244249
}
250+
clipAboveAreaPointsSet.push({x: curve1[len1 - 1].x, y: area.top});
251+
clipBelowAreaPointsSet.push({x: curve0[0].x, y: area.top});
252+
clipBelowAreaPointsSet.push(curve0[0]);
245253

246254
// building first area curve (normal)
247-
ctx.moveTo(curve0[0].x, curve0[0].y);
255+
fillAreaPointsSet.push(curve0[0]);
248256
for (i = 1; i < len0; ++i) {
249-
target = curve0[i];
250-
if (!target.boundary && (tension || stepped)) {
251-
lineTo(ctx, curve0[i - 1], target, false, stepped);
252-
} else {
253-
ctx.lineTo(target.x, target.y);
254-
}
257+
curve0[i].flip = false;
258+
fillAreaPointsSet.push(curve0[i]);
259+
clipBelowAreaPointsSet.push(curve0[i]);
255260
}
256261

257262
if (curve1[0].angle !== undefined) {
263+
pointSets.fill.push(fillAreaPointsSet);
258264
cx = curve1[0].cx;
259265
cy = curve1[0].cy;
260266
r = Math.sqrt(Math.pow(curve1[0].x - cx, 2) + Math.pow(curve1[0].y - cy, 2));
261267
for (i = len1 - 1; i > 0; --i) {
262-
ctx.arc(cx, cy, r, curve1[i].angle, curve1[i - 1].angle, true);
268+
radialSet.push({cx: cx, cy: cy, radius: r, startAngle: curve1[i].angle, endAngle: curve1[i - 1].angle});
269+
}
270+
if (radialSet.length) {
271+
pointSets.fill.push(radialSet);
263272
}
264273
return;
265274
}
266-
267275
// joining the two area curves
268-
ctx.lineTo(curve1[len1 - 1].x, curve1[len1 - 1].y);
276+
for (var key in curve1[len1 - 1]) {
277+
if (Object.prototype.hasOwnProperty.call(curve1[len1 - 1], key)) {
278+
jointPoint[key] = curve1[len1 - 1][key];
279+
}
280+
}
281+
jointPoint.joint = true;
282+
fillAreaPointsSet.push(jointPoint);
269283

270284
// building opposite area curve (reverse)
271285
for (i = len1 - 1; i > 0; --i) {
272-
target = curve1[i - 1];
273-
if (!target.boundary && (tension || stepped)) {
274-
lineTo(ctx, curve1[i], target, true, stepped);
286+
curve1[i].flip = true;
287+
clipAboveAreaPointsSet.push(curve1[i]);
288+
curve1[i - 1].flip = true;
289+
fillAreaPointsSet.push(curve1[i - 1]);
290+
}
291+
clipAboveAreaPointsSet.push(curve1[0]);
292+
clipAboveAreaPointsSet.push({x: curve1[0].x, y: area.top});
293+
clipBelowAreaPointsSet.push({x: curve0[len0 - 1].x, y: area.top});
294+
295+
pointSets.clipAbove.push(clipAboveAreaPointsSet);
296+
pointSets.clipBelow.push(clipBelowAreaPointsSet);
297+
pointSets.fill.push(fillAreaPointsSet);
298+
}
299+
300+
function clipAndFill(ctx, clippingPointsSets, fillingPointsSets, color, stepped, tension) {
301+
const lineTo = stepped ? helpers.canvas._steppedLineTo : helpers.canvas._bezierCurveTo;
302+
let i, ilen, j, jlen, set, target;
303+
if (clippingPointsSets) {
304+
ctx.save();
305+
ctx.beginPath();
306+
for (i = 0, ilen = clippingPointsSets.length; i < ilen; i++) {
307+
set = clippingPointsSets[i];
308+
// Have edge lines straight
309+
ctx.moveTo(set[0].x, set[0].y);
310+
ctx.lineTo(set[1].x, set[1].y);
311+
for (j = 2, jlen = set.length; j < jlen - 1; j++) {
312+
target = set[j];
313+
if (!target.boundary && (tension || stepped)) {
314+
lineTo(ctx, set[j - 1], target, target.flip, stepped);
315+
} else {
316+
ctx.lineTo(target.x, target.y);
317+
}
318+
}
319+
ctx.lineTo(set[j].x, set[j].y);
320+
}
321+
ctx.closePath();
322+
ctx.clip();
323+
ctx.beginPath();
324+
}
325+
for (i = 0, ilen = fillingPointsSets.length; i < ilen; i++) {
326+
set = fillingPointsSets[i];
327+
if (set[0].startAngle !== undefined) {
328+
for (j = 0, jlen = set.length; j < jlen; j++) {
329+
ctx.arc(set[j].cx, set[j].cy, set[j].radius, set[j].startAngle, set[j].endAngle, true);
330+
}
275331
} else {
276-
ctx.lineTo(target.x, target.y);
332+
ctx.moveTo(set[0].x, set[0].y);
333+
for (j = 1, jlen = set.length; j < jlen; j++) {
334+
if (set[j].joint) {
335+
ctx.lineTo(set[j].x, set[j].y);
336+
} else {
337+
target = set[j];
338+
if (!target.boundary && (tension || stepped)) {
339+
lineTo(ctx, set[j - 1], target, target.flip, stepped);
340+
} else {
341+
ctx.lineTo(target.x, target.y);
342+
}
343+
}
344+
}
277345
}
278346
}
347+
ctx.closePath();
348+
ctx.fillStyle = color;
349+
ctx.fill();
350+
ctx.restore();
279351
}
280352

281-
function doFill(ctx, points, mapper, el) {
353+
function doFill(ctx, points, mapper, colors, el, area) {
282354
const count = points.length;
283355
const view = el._view;
284356
const loop = el._loop;
@@ -289,8 +361,10 @@ function doFill(ctx, points, mapper, el) {
289361
let curve1 = [];
290362
let len0 = 0;
291363
let len1 = 0;
364+
let pointSets = {clipBelow: [], clipAbove: [], fill: []};
292365
let i, ilen, index, p0, p1, d0, d1, loopOffset;
293366

367+
ctx.save();
294368
ctx.beginPath();
295369

296370
for (i = 0, ilen = count; i < ilen; ++i) {
@@ -310,7 +384,7 @@ function doFill(ctx, points, mapper, el) {
310384
len1 = curve1.push(p1);
311385
} else if (len0 && len1) {
312386
if (!span) {
313-
drawArea(ctx, curve0, curve1, len0, len1, stepped, tension);
387+
fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets);
314388
len0 = len1 = 0;
315389
curve0 = [];
316390
curve1 = [];
@@ -325,11 +399,14 @@ function doFill(ctx, points, mapper, el) {
325399
}
326400
}
327401

328-
drawArea(ctx, curve0, curve1, len0, len1, stepped, tension);
402+
fillPointsSets(ctx, curve0, curve1, len0, len1, area, pointSets);
329403

330-
ctx.closePath();
331-
ctx.fillStyle = view.backgroundColor;
332-
ctx.fill();
404+
if (colors.below !== colors.above) {
405+
clipAndFill(ctx, pointSets.clipAbove, pointSets.fill, colors.above, stepped, tension);
406+
clipAndFill(ctx, pointSets.clipBelow, pointSets.fill, colors.below, stepped, tension);
407+
} else {
408+
clipAndFill(ctx, false, pointSets.fill, colors.above, stepped, tension);
409+
}
333410
}
334411

335412
module.exports = {
@@ -375,7 +452,7 @@ module.exports = {
375452
beforeDatasetsDraw: function(chart) {
376453
var metasets = chart._getSortedVisibleDatasetMetas();
377454
var ctx = chart.ctx;
378-
var meta, i, el, points, mapper;
455+
var meta, i, el, view, points, mapper, color, colors, fillOption;
379456

380457
for (i = metasets.length - 1; i >= 0; --i) {
381458
meta = metasets[i].$filler;
@@ -385,12 +462,20 @@ module.exports = {
385462
}
386463

387464
el = meta.el;
465+
view = el._view;
388466
points = el._children || [];
389467
mapper = meta.mapper;
468+
fillOption = meta.el._model.fill;
469+
color = view.backgroundColor || defaults.global.defaultColor;
390470

471+
colors = {above: color, below: color};
472+
if (fillOption && typeof fillOption === 'object') {
473+
colors.above = fillOption.above || color;
474+
colors.below = fillOption.below || color;
475+
}
391476
if (mapper && points.length) {
392477
helpers.canvas.clipArea(ctx, chart.chartArea);
393-
doFill(ctx, points, mapper, el);
478+
doFill(ctx, points, mapper, colors, el, chart.chartArea);
394479
helpers.canvas.unclipArea(ctx);
395480
}
396481
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"config": {
3+
"type": "line",
4+
"data": {
5+
"labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
6+
"datasets": [{
7+
"backgroundColor": "rgba(0, 0, 192, 0.25)",
8+
"data": [null, null, 2, 3, 4, -4, -2, 1, 0]
9+
}, {
10+
"backgroundColor": "rgba(0, 192, 0, 0.25)",
11+
"data": [6, 2, null, 4, 5, null, null, 2, 1]
12+
}, {
13+
"backgroundColor": "rgba(192, 0, 0, 0.25)",
14+
"data": [7, 3, 4, 5, 6, 1, 4, null, null]
15+
}, {
16+
"backgroundColor": "rgba(0, 64, 192, 0.25)",
17+
"data": [8, 7, 6, -6, -4, -6, 4, 5, 8]
18+
}]
19+
},
20+
"options": {
21+
"responsive": false,
22+
"spanGaps": true,
23+
"legend": false,
24+
"title": false,
25+
"scales": {
26+
"xAxes": [{
27+
"display": false
28+
}],
29+
"yAxes": [{
30+
"display": false
31+
}]
32+
},
33+
"elements": {
34+
"point": {
35+
"radius": 0
36+
},
37+
"line": {
38+
"borderColor": "transparent",
39+
"fill": {
40+
"target": "origin",
41+
"below": "rgba(255, 0, 0, 0.25)"
42+
},
43+
"tension": 0
44+
}
45+
}
46+
}
47+
},
48+
"options": {
49+
"canvas": {
50+
"height": 256,
51+
"width": 512
52+
}
53+
}
54+
}
Loading
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"config": {
3+
"type": "line",
4+
"data": {
5+
"labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"],
6+
"datasets": [{
7+
"backgroundColor": "rgba(0, 0, 192, 0.25)",
8+
"data": [null, null, 2, 4, 2, 1, -1, 1, 2]
9+
}, {
10+
"backgroundColor": "rgba(0, 192, 0, 0.25)",
11+
"data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3]
12+
}, {
13+
"backgroundColor": "rgba(192, 0, 0, 0.25)",
14+
"data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null]
15+
}, {
16+
"backgroundColor": "rgba(128, 0, 128, 0.25)",
17+
"data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5]
18+
}]
19+
},
20+
"options": {
21+
"responsive": false,
22+
"spanGaps": false,
23+
"legend": false,
24+
"title": false,
25+
"scales": {
26+
"xAxes": [{
27+
"display": false
28+
}],
29+
"yAxes": [{
30+
"display": false
31+
}]
32+
},
33+
"elements": {
34+
"point": {
35+
"radius": 0
36+
},
37+
"line": {
38+
"cubicInterpolationMode": "monotone",
39+
"borderColor": "transparent",
40+
"fill": {
41+
"target": "origin",
42+
"below": "transparent"
43+
}
44+
}
45+
}
46+
}
47+
},
48+
"options": {
49+
"canvas": {
50+
"height": 256,
51+
"width": 512
52+
}
53+
}
54+
}
Loading

0 commit comments

Comments
 (0)