/** * 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); } } } };