mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-24 00:56:51 +01:00
Add support to fill between datasets (#4008)
The `fill` option now accepts the index of the target dataset (number) or a string starting by "+" or "-" followed by a number representing the dataset index relative to the current one (e.g. `fill: "-2"` on dataset at index 3 will fill to dataset at index 1). It's also possible to "propagate" the filling to the target of an hidden dataset (`options.plugins.filler.propagate`). Fill boundaries `zero`, `top` and `bottom` have been deprecated and replaced by `origin`, `start` and `end`. Implementation has been moved out of the line element into a new plugin (`src/plugins/plugin.filler.js`) and does not rely anymore on the deprecated model `scaleTop`, `scaleBottom` and `scaleZero` values. Drawing Bézier splines has been refactored in the canvas helpers (note that `Chart.helpers.canvas` is now an alias of `Chart.canvasHelpers`). Add 3 new examples and extend utils with a pseudo-random number generator that can be initialized with `srand`. That makes possible to design examples starting always with the same initial data.
This commit is contained in:
@@ -49,4 +49,11 @@ require('./charts/Chart.PolarArea')(Chart);
|
||||
require('./charts/Chart.Radar')(Chart);
|
||||
require('./charts/Chart.Scatter')(Chart);
|
||||
|
||||
// Loading built-it plugins
|
||||
var plugins = [];
|
||||
|
||||
plugins.push(require('./plugins/plugin.filler.js')(Chart));
|
||||
|
||||
Chart.plugins.register(plugins);
|
||||
|
||||
window.Chart = module.exports = Chart;
|
||||
|
||||
@@ -78,10 +78,6 @@ module.exports = function(Chart) {
|
||||
fill: custom.fill ? custom.fill : (dataset.fill !== undefined ? dataset.fill : lineElementOptions.fill),
|
||||
steppedLine: custom.steppedLine ? custom.steppedLine : helpers.getValueOrDefault(dataset.steppedLine, lineElementOptions.stepped),
|
||||
cubicInterpolationMode: custom.cubicInterpolationMode ? custom.cubicInterpolationMode : helpers.getValueOrDefault(dataset.cubicInterpolationMode, lineElementOptions.cubicInterpolationMode),
|
||||
// Scale
|
||||
scaleTop: scale.top,
|
||||
scaleBottom: scale.bottom,
|
||||
scaleZero: scale.getBasePixel()
|
||||
};
|
||||
|
||||
line.pivot();
|
||||
|
||||
@@ -42,6 +42,7 @@ module.exports = function(Chart) {
|
||||
helpers.extend(meta.dataset, {
|
||||
// Utility
|
||||
_datasetIndex: me.index,
|
||||
_scale: scale,
|
||||
// Data
|
||||
_children: points,
|
||||
_loop: true,
|
||||
@@ -57,11 +58,6 @@ module.exports = function(Chart) {
|
||||
borderDash: custom.borderDash ? custom.borderDash : (dataset.borderDash || lineElementOptions.borderDash),
|
||||
borderDashOffset: custom.borderDashOffset ? custom.borderDashOffset : (dataset.borderDashOffset || lineElementOptions.borderDashOffset),
|
||||
borderJoinStyle: custom.borderJoinStyle ? custom.borderJoinStyle : (dataset.borderJoinStyle || lineElementOptions.borderJoinStyle),
|
||||
|
||||
// Scale
|
||||
scaleTop: scale.top,
|
||||
scaleBottom: scale.bottom,
|
||||
scaleZero: scale.getBasePosition()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -121,4 +121,26 @@ module.exports = function(Chart) {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
helpers.lineTo = function(ctx, previous, target, flip) {
|
||||
if (target.steppedLine) {
|
||||
ctx.lineTo(target.x, previous.y);
|
||||
ctx.lineTo(target.x, target.y);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.tension) {
|
||||
ctx.lineTo(target.x, target.y);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.bezierCurveTo(
|
||||
flip? previous.controlPointPreviousX : previous.controlPointNextX,
|
||||
flip? previous.controlPointPreviousY : previous.controlPointNextY,
|
||||
flip? target.controlPointNextX : target.controlPointPreviousX,
|
||||
flip? target.controlPointNextY : target.controlPointPreviousY,
|
||||
target.x,
|
||||
target.y);
|
||||
};
|
||||
|
||||
Chart.helpers.canvas = helpers;
|
||||
};
|
||||
|
||||
@@ -22,118 +22,21 @@ module.exports = function(Chart) {
|
||||
draw: function() {
|
||||
var me = this;
|
||||
var vm = me._view;
|
||||
var spanGaps = vm.spanGaps;
|
||||
var fillPoint = vm.scaleZero;
|
||||
var loop = me._loop;
|
||||
|
||||
// Handle different fill modes for cartesian lines
|
||||
if (!loop) {
|
||||
if (vm.fill === 'top') {
|
||||
fillPoint = vm.scaleTop;
|
||||
} else if (vm.fill === 'bottom') {
|
||||
fillPoint = vm.scaleBottom;
|
||||
}
|
||||
}
|
||||
|
||||
var ctx = me._chart.ctx;
|
||||
ctx.save();
|
||||
|
||||
// Helper function to draw a line to a point
|
||||
function lineToPoint(previousPoint, point) {
|
||||
var pointVM = point._view;
|
||||
if (point._view.steppedLine === true) {
|
||||
ctx.lineTo(pointVM.x, previousPoint._view.y);
|
||||
ctx.lineTo(pointVM.x, pointVM.y);
|
||||
} else if (point._view.tension === 0) {
|
||||
ctx.lineTo(pointVM.x, pointVM.y);
|
||||
} else {
|
||||
ctx.bezierCurveTo(
|
||||
previousPoint._view.controlPointNextX,
|
||||
previousPoint._view.controlPointNextY,
|
||||
pointVM.controlPointPreviousX,
|
||||
pointVM.controlPointPreviousY,
|
||||
pointVM.x,
|
||||
pointVM.y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var spanGaps = vm.spanGaps;
|
||||
var points = me._children.slice(); // clone array
|
||||
var globalOptionLineElements = globalDefaults.elements.line;
|
||||
var lastDrawnIndex = -1;
|
||||
var index, current, previous, currentVM;
|
||||
|
||||
// If we are looping, adding the first point again
|
||||
if (loop && points.length) {
|
||||
if (me._loop && points.length) {
|
||||
points.push(points[0]);
|
||||
}
|
||||
|
||||
var index, current, previous, currentVM;
|
||||
|
||||
// Fill Line
|
||||
if (points.length && vm.fill) {
|
||||
ctx.beginPath();
|
||||
|
||||
for (index = 0; index < points.length; ++index) {
|
||||
current = points[index];
|
||||
previous = helpers.previousItem(points, index);
|
||||
currentVM = current._view;
|
||||
|
||||
// First point moves to it's starting position no matter what
|
||||
if (index === 0) {
|
||||
if (loop) {
|
||||
ctx.moveTo(fillPoint.x, fillPoint.y);
|
||||
} else {
|
||||
ctx.moveTo(currentVM.x, fillPoint);
|
||||
}
|
||||
|
||||
if (!currentVM.skip) {
|
||||
lastDrawnIndex = index;
|
||||
ctx.lineTo(currentVM.x, currentVM.y);
|
||||
}
|
||||
} else {
|
||||
previous = lastDrawnIndex === -1 ? previous : points[lastDrawnIndex];
|
||||
|
||||
if (currentVM.skip) {
|
||||
// Only do this if this is the first point that is skipped
|
||||
if (!spanGaps && lastDrawnIndex === (index - 1)) {
|
||||
if (loop) {
|
||||
ctx.lineTo(fillPoint.x, fillPoint.y);
|
||||
} else {
|
||||
ctx.lineTo(previous._view.x, fillPoint);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (lastDrawnIndex !== (index - 1)) {
|
||||
// There was a gap and this is the first point after the gap. If we've never drawn a point, this is a special case.
|
||||
// If the first data point is NaN, then there is no real gap to skip
|
||||
if (spanGaps && lastDrawnIndex !== -1) {
|
||||
// We are spanning the gap, so simple draw a line to this point
|
||||
lineToPoint(previous, current);
|
||||
} else if (loop) {
|
||||
ctx.lineTo(currentVM.x, currentVM.y);
|
||||
} else {
|
||||
ctx.lineTo(currentVM.x, fillPoint);
|
||||
ctx.lineTo(currentVM.x, currentVM.y);
|
||||
}
|
||||
} else {
|
||||
// Line to next point
|
||||
lineToPoint(previous, current);
|
||||
}
|
||||
lastDrawnIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!loop && lastDrawnIndex !== -1) {
|
||||
ctx.lineTo(points[lastDrawnIndex]._view.x, fillPoint);
|
||||
}
|
||||
|
||||
ctx.fillStyle = vm.backgroundColor || globalDefaults.defaultColor;
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.save();
|
||||
|
||||
// Stroke Line Options
|
||||
var globalOptionLineElements = globalDefaults.elements.line;
|
||||
ctx.lineCap = vm.borderCapStyle || globalOptionLineElements.borderCapStyle;
|
||||
|
||||
// IE 9 and 10 do not support line dash
|
||||
@@ -170,7 +73,7 @@ module.exports = function(Chart) {
|
||||
ctx.moveTo(currentVM.x, currentVM.y);
|
||||
} else {
|
||||
// Line to next point
|
||||
lineToPoint(previous, current);
|
||||
helpers.canvas.lineTo(ctx, previous._view, current._view);
|
||||
}
|
||||
lastDrawnIndex = index;
|
||||
}
|
||||
|
||||
309
src/plugins/plugin.filler.js
Normal file
309
src/plugins/plugin.filler.js
Normal file
@@ -0,0 +1,309 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(Chart) {
|
||||
/**
|
||||
* Plugin based on discussion from the following Chart.js issues:
|
||||
* @see https://github.com/chartjs/Chart.js/issues/2380#issuecomment-279961569
|
||||
* @see https://github.com/chartjs/Chart.js/issues/2440#issuecomment-256461897
|
||||
*/
|
||||
Chart.defaults.global.plugins.filler = {
|
||||
propagate: true
|
||||
};
|
||||
|
||||
var defaults = Chart.defaults;
|
||||
var helpers = Chart.helpers;
|
||||
var mappers = {
|
||||
dataset: function(source) {
|
||||
var index = source.fill;
|
||||
var chart = source.chart;
|
||||
var meta = chart.getDatasetMeta(index);
|
||||
var visible = meta && chart.isDatasetVisible(index);
|
||||
var points = (visible && meta.dataset._children) || [];
|
||||
|
||||
return !points.length? null : function(point, i) {
|
||||
return points[i]._view || null;
|
||||
};
|
||||
},
|
||||
|
||||
boundary: function(source) {
|
||||
var boundary = source.boundary;
|
||||
var x = boundary? boundary.x : null;
|
||||
var y = boundary? boundary.y : null;
|
||||
|
||||
return function(point) {
|
||||
return {
|
||||
x: x === null? point.x : x,
|
||||
y: y === null? point.y : y,
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// @todo if (fill[0] === '#')
|
||||
function decodeFill(el, index, count) {
|
||||
var model = el._model || {};
|
||||
var fill = model.fill;
|
||||
var target;
|
||||
|
||||
if (fill === undefined) {
|
||||
fill = !!model.backgroundColor;
|
||||
}
|
||||
|
||||
if (fill === false || fill === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fill === true) {
|
||||
return 'origin';
|
||||
}
|
||||
|
||||
target = parseFloat(fill, 10);
|
||||
if (isFinite(target) && Math.floor(target) === target) {
|
||||
if (fill[0] === '-' || fill[0] === '+') {
|
||||
target = index + target;
|
||||
}
|
||||
|
||||
if (target === index || target < 0 || target >= count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
switch (fill) {
|
||||
// compatibility
|
||||
case 'bottom':
|
||||
return 'start';
|
||||
case 'top':
|
||||
return 'end';
|
||||
case 'zero':
|
||||
return 'origin';
|
||||
// supported boundaries
|
||||
case 'origin':
|
||||
case 'start':
|
||||
case 'end':
|
||||
return fill;
|
||||
// invalid fill values
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function computeBoundary(source) {
|
||||
var model = source.el._model || {};
|
||||
var scale = source.el._scale || {};
|
||||
var fill = source.fill;
|
||||
var target = null;
|
||||
var horizontal;
|
||||
|
||||
if (isFinite(fill)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Backward compatibility: until v3, we still need to support boundary values set on
|
||||
// the model (scaleTop, scaleBottom and scaleZero) because some external plugins and
|
||||
// controllers might still use it (e.g. the Smith chart).
|
||||
|
||||
if (fill === 'start') {
|
||||
target = model.scaleBottom === undefined? scale.bottom : model.scaleBottom;
|
||||
} else if (fill === 'end') {
|
||||
target = model.scaleTop === undefined? scale.top : model.scaleTop;
|
||||
} else if (model.scaleZero !== undefined) {
|
||||
target = model.scaleZero;
|
||||
} else if (scale.getBasePosition) {
|
||||
target = scale.getBasePosition();
|
||||
} else if (scale.getBasePixel) {
|
||||
target = scale.getBasePixel();
|
||||
}
|
||||
|
||||
if (target !== undefined && target !== null) {
|
||||
if (target.x !== undefined && target.y !== undefined) {
|
||||
return target;
|
||||
}
|
||||
|
||||
if (typeof target === 'number' && isFinite(target)) {
|
||||
horizontal = scale.isHorizontal();
|
||||
return {
|
||||
x: horizontal? target : null,
|
||||
y: horizontal? null : target
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveTarget(sources, index, propagate) {
|
||||
var source = sources[index];
|
||||
var fill = source.fill;
|
||||
var visited = [index];
|
||||
var target;
|
||||
|
||||
if (!propagate) {
|
||||
return fill;
|
||||
}
|
||||
|
||||
while (fill !== false && visited.indexOf(fill) === -1) {
|
||||
if (!isFinite(fill)) {
|
||||
return fill;
|
||||
}
|
||||
|
||||
target = sources[fill];
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.visible) {
|
||||
return fill;
|
||||
}
|
||||
|
||||
visited.push(fill);
|
||||
fill = target.fill;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function createMapper(source) {
|
||||
var fill = source.fill;
|
||||
var type = 'dataset';
|
||||
|
||||
if (fill === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isFinite(fill)) {
|
||||
type = 'boundary';
|
||||
}
|
||||
|
||||
return mappers[type](source);
|
||||
}
|
||||
|
||||
function isDrawable(point) {
|
||||
return point && !point.skip;
|
||||
}
|
||||
|
||||
function drawArea(ctx, curve0, curve1, len0, len1) {
|
||||
var i;
|
||||
|
||||
if (!len0 || !len1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// building first area curve (normal)
|
||||
ctx.moveTo(curve0[0].x, curve0[0].y);
|
||||
for (i=1; i<len0; ++i) {
|
||||
helpers.canvas.lineTo(ctx, curve0[i-1], curve0[i]);
|
||||
}
|
||||
|
||||
// joining the two area curves
|
||||
ctx.lineTo(curve1[len1-1].x, curve1[len1-1].y);
|
||||
|
||||
// building opposite area curve (reverse)
|
||||
for (i=len1-1; i>0; --i) {
|
||||
helpers.canvas.lineTo(ctx, curve1[i], curve1[i-1], true);
|
||||
}
|
||||
}
|
||||
|
||||
function doFill(ctx, points, mapper, view, color, loop) {
|
||||
var count = points.length;
|
||||
var span = view.spanGaps;
|
||||
var curve0 = [];
|
||||
var curve1 = [];
|
||||
var len0 = 0;
|
||||
var len1 = 0;
|
||||
var i, ilen, index, p0, p1, d0, d1;
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
for (i = 0, ilen = (count + !!loop); i < ilen; ++i) {
|
||||
index = i%count;
|
||||
p0 = points[index]._view;
|
||||
p1 = mapper(p0, index, view);
|
||||
d0 = isDrawable(p0);
|
||||
d1 = isDrawable(p1);
|
||||
|
||||
if (d0 && d1) {
|
||||
len0 = curve0.push(p0);
|
||||
len1 = curve1.push(p1);
|
||||
} else if (len0 && len1) {
|
||||
if (!span) {
|
||||
drawArea(ctx, curve0, curve1, len0, len1);
|
||||
len0 = len1 = 0;
|
||||
curve0 = [];
|
||||
curve1 = [];
|
||||
} else {
|
||||
if (d0) {
|
||||
curve0.push(p0);
|
||||
}
|
||||
if (d1) {
|
||||
curve1.push(p1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawArea(ctx, curve0, curve1, len0, len1);
|
||||
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'filler',
|
||||
|
||||
afterDatasetsUpdate: function(chart, options) {
|
||||
var count = (chart.data.datasets || []).length;
|
||||
var propagate = options.propagate;
|
||||
var sources = [];
|
||||
var meta, i, el, source;
|
||||
|
||||
for (i = 0; i < count; ++i) {
|
||||
meta = chart.getDatasetMeta(i);
|
||||
el = meta.dataset;
|
||||
source = null;
|
||||
|
||||
if (el && el._model && el instanceof Chart.elements.Line) {
|
||||
source = {
|
||||
visible: chart.isDatasetVisible(i),
|
||||
fill: decodeFill(el, i, count),
|
||||
chart: chart,
|
||||
el: el
|
||||
};
|
||||
}
|
||||
|
||||
meta.$filler = source;
|
||||
sources.push(source);
|
||||
}
|
||||
|
||||
for (i=0; i<count; ++i) {
|
||||
source = sources[i];
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
source.fill = resolveTarget(sources, i, propagate);
|
||||
source.boundary = computeBoundary(source);
|
||||
source.mapper = createMapper(source);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDatasetDraw: function(chart, args) {
|
||||
var meta = args.meta.$filler;
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
var el = meta.el;
|
||||
var view = el._view;
|
||||
var points = el._children || [];
|
||||
var mapper = meta.mapper;
|
||||
var color = view.backgroundColor || defaults.global.defaultColor;
|
||||
|
||||
if (mapper && color && points.length) {
|
||||
doFill(chart.ctx, points, mapper, view, color, el._loop);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user