From: L M Date: Thu, 21 Nov 2019 18:48:31 +0000 (+0100) Subject: Allow filling above and below with different colors (#6318) X-Git-Tag: v3.0.0-alpha~220 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=18e3bc06240b8444a6ed3335b8cd510bed23f6c9;p=thirdparty%2FChart.js.git 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 --- diff --git a/docs/charts/area.md b/docs/charts/area.md index 824a469e8..d18f6bdb8 100644 --- a/docs/charts/area.md +++ b/docs/charts/area.md @@ -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 | | :--- | :--- | :--- | :--- | diff --git a/src/plugins/plugin.filler.js b/src/plugins/plugin.filler.js index 8f652aba8..7209a104b 100644 --- a/src/plugins/plugin.filler.js +++ b/src/plugins/plugin.filler.js @@ -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); + 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 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.lineTo(target.x, target.y); + 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, el) { +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); } } diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-span-dual.json b/test/fixtures/plugin.filler/fill-line-boundary-origin-span-dual.json new file mode 100644 index 000000000..968c4ae87 --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin-span-dual.json @@ -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 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-span-dual.png b/test/fixtures/plugin.filler/fill-line-boundary-origin-span-dual.png new file mode 100644 index 000000000..eeecfcc1f Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-boundary-origin-span-dual.png differ diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-above.json b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-above.json new file mode 100644 index 000000000..751945e2e --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-above.json @@ -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 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-above.png b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-above.png new file mode 100644 index 000000000..c0b25469d Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-boundary-origin-spline-above.png differ diff --git a/test/fixtures/plugin.filler/fill-line-dataset-dual.json b/test/fixtures/plugin.filler/fill-line-dataset-dual.json new file mode 100644 index 000000000..6ccdf228f --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset-dual.json @@ -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 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset-dual.png b/test/fixtures/plugin.filler/fill-line-dataset-dual.png new file mode 100644 index 000000000..ae2c77dff Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-dataset-dual.png differ diff --git a/test/fixtures/plugin.filler/fill-line-dataset-span-dual.json b/test/fixtures/plugin.filler/fill-line-dataset-span-dual.json new file mode 100644 index 000000000..2ca91fac8 --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset-span-dual.json @@ -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 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset-span-dual.png b/test/fixtures/plugin.filler/fill-line-dataset-span-dual.png new file mode 100644 index 000000000..d5600b767 Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-dataset-span-dual.png differ diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span-above.json b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-above.json new file mode 100644 index 000000000..70b80f6fa --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-above.json @@ -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 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span-above.png b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-above.png new file mode 100644 index 000000000..838a24b5d Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-above.png differ diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.json b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.json new file mode 100644 index 000000000..87314029c --- /dev/null +++ b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.json @@ -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 + } + } +} diff --git a/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.png b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.png new file mode 100644 index 000000000..45e15c5d8 Binary files /dev/null and b/test/fixtures/plugin.filler/fill-line-dataset-spline-span-below.png differ diff --git a/test/specs/plugin.filler.tests.js b/test/specs/plugin.filler.tests.js index 0164dc1dd..9b9eb214d 100644 --- a/test/specs/plugin.filler.tests.js +++ b/test/specs/plugin.filler.tests.js @@ -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 ]); }); });