382 lines
8.0 KiB
JavaScript
382 lines
8.0 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var defaults = require('../core/core.defaults');
|
|
var elements = require('../elements/index');
|
|
var helpers = require('../helpers/index');
|
|
|
|
defaults._set('global', {
|
|
plugins: {
|
|
filler: {
|
|
propagate: true
|
|
}
|
|
}
|
|
});
|
|
|
|
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) || [];
|
|
var length = points.length || 0;
|
|
|
|
return !length ? null : function(point, i) {
|
|
return (i < length && points[i]._view) || null;
|
|
};
|
|
},
|
|
|
|
boundary: function(source) {
|
|
var boundary = source.boundary;
|
|
var x = boundary ? boundary.x : null;
|
|
var y = boundary ? boundary.y : null;
|
|
|
|
if (helpers.isArray(boundary)) {
|
|
return function(point, i) {
|
|
return boundary[i];
|
|
};
|
|
}
|
|
|
|
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 computeLinearBoundary(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.getBasePixel) {
|
|
target = scale.getBasePixel();
|
|
}
|
|
|
|
if (target !== undefined && target !== null) {
|
|
if (target.x !== undefined && target.y !== undefined) {
|
|
return target;
|
|
}
|
|
|
|
if (helpers.isFinite(target)) {
|
|
horizontal = scale.isHorizontal();
|
|
return {
|
|
x: horizontal ? target : null,
|
|
y: horizontal ? null : target
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function computeCircularBoundary(source) {
|
|
var scale = source.el._scale;
|
|
var options = scale.options;
|
|
var length = scale.chart.data.labels.length;
|
|
var fill = source.fill;
|
|
var target = [];
|
|
var start, end, center, i, point;
|
|
|
|
if (!length) {
|
|
return null;
|
|
}
|
|
|
|
start = options.ticks.reverse ? scale.max : scale.min;
|
|
end = options.ticks.reverse ? scale.min : scale.max;
|
|
center = scale.getPointPositionForValue(0, start);
|
|
for (i = 0; i < length; ++i) {
|
|
point = fill === 'start' || fill === 'end'
|
|
? scale.getPointPositionForValue(i, fill === 'start' ? start : end)
|
|
: scale.getBasePosition(i);
|
|
if (options.gridLines.circular) {
|
|
point.cx = center.x;
|
|
point.cy = center.y;
|
|
point.angle = scale.getIndexAngle(i) - Math.PI / 2;
|
|
}
|
|
target.push(point);
|
|
}
|
|
return target;
|
|
}
|
|
|
|
function computeBoundary(source) {
|
|
var scale = source.el._scale || {};
|
|
|
|
if (scale.getPointPositionForValue) {
|
|
return computeCircularBoundary(source);
|
|
}
|
|
return computeLinearBoundary(source);
|
|
}
|
|
|
|
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, cx, cy, r;
|
|
|
|
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]);
|
|
}
|
|
|
|
if (curve1[0].angle !== undefined) {
|
|
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);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// 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, loopOffset;
|
|
|
|
ctx.beginPath();
|
|
|
|
for (i = 0, ilen = count; i < ilen; ++i) {
|
|
index = i % count;
|
|
p0 = points[index]._view;
|
|
p1 = mapper(p0, index, view);
|
|
d0 = isDrawable(p0);
|
|
d1 = isDrawable(p1);
|
|
|
|
if (loop && loopOffset === undefined && d0) {
|
|
loopOffset = i + 1;
|
|
ilen = count + loopOffset;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
module.exports = {
|
|
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 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);
|
|
}
|
|
},
|
|
|
|
beforeDatasetsDraw: function(chart) {
|
|
var metasets = chart._getSortedVisibleDatasetMetas();
|
|
var ctx = chart.ctx;
|
|
var meta, i, el, view, points, mapper, color;
|
|
|
|
for (i = metasets.length - 1; i >= 0; --i) {
|
|
meta = metasets[i].$filler;
|
|
|
|
if (!meta || !meta.visible) {
|
|
continue;
|
|
}
|
|
|
|
el = meta.el;
|
|
view = el._view;
|
|
points = el._children || [];
|
|
mapper = meta.mapper;
|
|
color = view.backgroundColor || defaults.global.defaultColor;
|
|
|
|
if (mapper && color && points.length) {
|
|
helpers.canvas.clipArea(ctx, chart.chartArea);
|
|
doFill(ctx, points, mapper, view, color, el._loop);
|
|
helpers.canvas.unclipArea(ctx);
|
|
}
|
|
}
|
|
}
|
|
};
|