chart.js
Version:
Simple HTML5 charts using the canvas element.
322 lines (265 loc) • 6.85 kB
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
*/
;
var defaults = require('../core/core.defaults');
var elements = require('../elements/index');
var helpers = require('../helpers/index');
defaults._set('global', {
plugins: {
filler: {
propagate: true
}
}
});
module.exports = function() {
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;
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 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 ctx = chart.ctx;
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) {
helpers.canvas.clipArea(ctx, chart.chartArea);
doFill(ctx, points, mapper, view, color, el._loop);
helpers.canvas.unclipArea(ctx);
}
}
};
};