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:
Simon Brunel 2017-03-18 11:08:57 +01:00 committed by GitHub
parent 1ca0ffb5d5
commit 4b421a50bf
49 changed files with 1616 additions and 965 deletions

66
samples/area/analyser.js Normal file
View File

@ -0,0 +1,66 @@
/* global Chart */
'use strict';
(function() {
Chart.plugins.register({
id: 'samples_filler_analyser',
beforeInit: function(chart, options) {
this.element = document.getElementById(options.target);
},
afterUpdate: function(chart) {
var datasets = chart.data.datasets;
var element = this.element;
var stats = [];
var meta, i, ilen, dataset;
if (!element) {
return;
}
for (i=0, ilen=datasets.length; i<ilen; ++i) {
meta = chart.getDatasetMeta(i).$filler;
if (meta) {
dataset = datasets[i];
stats.push({
fill: dataset.fill,
target: meta.fill,
visible: meta.visible,
index: i
});
}
}
this.element.innerHTML = '<table>' +
'<tr>' +
'<th>Dataset</th>' +
'<th>Fill</th>' +
'<th>Target (visibility)</th>' +
'</tr>' +
stats.map(function(stat) {
var target = stat.target;
var row =
'<td><b>' + stat.index + '</b></td>' +
'<td>' + JSON.stringify(stat.fill) + '</td>';
if (target === false) {
target = 'none';
} else if (isFinite(target)) {
target = 'dataset ' + target;
} else {
target = 'boundary "' + target + '"';
}
if (stat.visible) {
row += '<td>' + target + '</td>';
} else {
row += '<td>(hidden)</td>';
}
return '<tr>' + row + '</tr>';
}).join('') + '</table>';
}
});
}());

View File

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>area > boundaries | Chart.js sample</title>
<link rel="stylesheet" type="text/css" href="../style.css">
<script src="../../dist/Chart.bundle.js"></script>
<script src="../utils.js"></script>
<script src="analyser.js"></script>
</head>
<body>
<div class="content">
<div class="wrapper col-2"><canvas id="chart-0"></canvas></div>
<div class="wrapper col-2"><canvas id="chart-1"></canvas></div>
<div class="wrapper col-2"><canvas id="chart-2"></canvas></div>
<div class="wrapper col-2"><canvas id="chart-3"></canvas></div>
<div class="toolbar">
<button onclick="toggleSmooth(this)">Smooth</button>
<button onclick="randomize(this)">Randomize</button>
</div>
</div>
<script>
var presets = window.chartColors;
var utils = Samples.utils;
var inputs = {
min: -100,
max: 100,
count: 8,
decimals: 2,
continuity: 1
};
function generateData(config) {
return utils.numbers(utils.merge(inputs, config || {}));
}
function generateLabels(config) {
return utils.months(utils.merge({
count: inputs.count,
section: 3
}, config || {}));
}
var options = {
maintainAspectRatio: false,
spanGaps: false,
elements: {
line: {
tension: 0.000001
}
},
plugins: {
filler: {
propagate: false
}
},
scales: {
xAxes: [{
ticks: {
autoSkip: false,
maxRotation: 0
}
}]
}
};
[false, 'origin', 'start', 'end'].forEach(function(boundary, index) {
// reset the random seed to generate the same data for all charts
utils.srand(8);
new Chart('chart-' + index, {
type: 'line',
data: {
labels: generateLabels(),
datasets: [{
backgroundColor: utils.transparentize(presets.red),
borderColor: presets.red,
data: generateData(),
label: 'Dataset',
fill: boundary
}]
},
options: utils.merge(options, {
title: {
text: 'fill: ' + boundary,
display: true
}
})
});
});
function toggleSmooth(btn) {
var value = btn.classList.toggle('btn-on');
Chart.helpers.each(Chart.instances, function(chart) {
chart.options.elements.line.tension = value? 0.4 : 0.000001;
chart.update();
});
}
function randomize() {
var seed = utils.rand();
Chart.helpers.each(Chart.instances, function(chart) {
utils.srand(seed);
chart.data.datasets.forEach(function(dataset) {
dataset.data = generateData();
});
chart.update();
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>area > datasets | Chart.js sample</title>
<link rel="stylesheet" type="text/css" href="../style.css">
<script src="../../dist/Chart.bundle.js"></script>
<script src="../utils.js"></script>
<script src="analyser.js"></script>
</head>
<body>
<div class="content">
<div class="wrapper">
<canvas id="chart-0"></canvas>
</div>
<div class="toolbar">
<button onclick="togglePropagate(this)">Propagate</button>
<button onclick="toggleSmooth(this)">Smooth</button>
<button onclick="randomize(this)">Randomize</button>
</div>
<div id="chart-analyser" class="analyser"></div>
</div>
<script>
var presets = window.chartColors;
var utils = Samples.utils;
var inputs = {
min: 20,
max: 80,
count: 8,
decimals: 2,
continuity: 1
};
function generateData() {
return utils.numbers(inputs);
}
function generateLabels(config) {
return utils.months({count: inputs.count});
}
utils.srand(42);
var data = {
labels: generateLabels(),
datasets: [{
backgroundColor: utils.transparentize(presets.red),
borderColor: presets.red,
data: generateData(),
hidden: true,
label: 'D0'
}, {
backgroundColor: utils.transparentize(presets.orange),
borderColor: presets.orange,
data: generateData(),
label: 'D1',
fill: '-1'
}, {
backgroundColor: utils.transparentize(presets.yellow),
borderColor: presets.yellow,
data: generateData(),
hidden: true,
label: 'D2',
fill: 1
}, {
backgroundColor: utils.transparentize(presets.green),
borderColor: presets.green,
data: generateData(),
label: 'D3',
fill: '-1'
}, {
backgroundColor: utils.transparentize(presets.blue),
borderColor: presets.blue,
data: generateData(),
label: 'D4',
fill: '-1'
}, {
backgroundColor: utils.transparentize(presets.grey),
borderColor: presets.grey,
data: generateData(),
label: 'D5',
fill: '+2'
}, {
backgroundColor: utils.transparentize(presets.purple),
borderColor: presets.purple,
data: generateData(),
label: 'D6',
fill: false
}, {
backgroundColor: utils.transparentize(presets.red),
borderColor: presets.red,
data: generateData(),
label: 'D7',
fill: 8
}, {
backgroundColor: utils.transparentize(presets.orange),
borderColor: presets.orange,
data: generateData(),
hidden: true,
label: 'D8',
fill: 'end'
}]
};
var options = {
maintainAspectRatio: false,
spanGaps: false,
elements: {
line: {
tension: 0.000001
}
},
scales: {
yAxes: [{
stacked: true
}]
},
plugins: {
filler: {
propagate: false
},
samples_filler_analyser: {
target: 'chart-analyser'
}
}
};
var chart = new Chart('chart-0', {
type: 'line',
data: data,
options: options
});
function togglePropagate(btn) {
var value = btn.classList.toggle('btn-on');
chart.options.plugins.filler.propagate = value;
chart.update();
}
function toggleSmooth(btn) {
var value = btn.classList.toggle('btn-on');
chart.options.elements.line.tension = value? 0.4 : 0.000001;
chart.update();
}
function randomize() {
chart.data.datasets.forEach(function(dataset) {
dataset.data = generateData();
});
chart.update();
}
</script>
</body>
</html>

136
samples/area/radar.html Normal file
View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>area > radar | Chart.js sample</title>
<link rel="stylesheet" type="text/css" href="../style.css">
<script src="../../dist/Chart.bundle.js"></script>
<script src="../utils.js"></script>
<script src="analyser.js"></script>
</head>
<body>
<div class="content">
<div class="wrapper" style="max-width: 512px; margin: auto">
<canvas id="chart-0"></canvas>
</div>
<div class="toolbar">
<button onclick="togglePropagate(this)">Propagate</button>
<button onclick="toggleSmooth(this)">Smooth</button>
<button onclick="randomize(this)">Randomize</button>
</div>
<div id="chart-analyser" class="analyser"></div>
</div>
<script>
var presets = window.chartColors;
var utils = Samples.utils;
var inputs = {
min: 8,
max: 16,
count: 8,
decimals: 2,
continuity: 1
};
function generateData() {
// radar chart doesn't support stacked values, let's do it manually
var values = utils.numbers(inputs);
inputs.from = values;
return values;
}
function generateLabels(config) {
return utils.months({count: inputs.count});
}
utils.srand(42);
var data = {
labels: generateLabels(),
datasets: [{
backgroundColor: utils.transparentize(presets.red),
borderColor: presets.red,
data: generateData(),
label: 'D0'
}, {
backgroundColor: utils.transparentize(presets.orange),
borderColor: presets.orange,
data: generateData(),
hidden: true,
label: 'D1',
fill: '-1'
}, {
backgroundColor: utils.transparentize(presets.yellow),
borderColor: presets.yellow,
data: generateData(),
label: 'D2',
fill: 1
}, {
backgroundColor: utils.transparentize(presets.green),
borderColor: presets.green,
data: generateData(),
label: 'D3',
fill: false
}, {
backgroundColor: utils.transparentize(presets.blue),
borderColor: presets.blue,
data: generateData(),
label: 'D4',
fill: '-1'
}, {
backgroundColor: utils.transparentize(presets.purple),
borderColor: presets.purple,
data: generateData(),
label: 'D5',
fill: '-1'
}]
};
var options = {
maintainAspectRatio: true,
spanGaps: false,
elements: {
line: {
tension: 0.000001
}
},
plugins: {
filler: {
propagate: false
},
samples_filler_analyser: {
target: 'chart-analyser'
}
}
};
var chart = new Chart('chart-0', {
type: 'radar',
data: data,
options: options
});
function togglePropagate(btn) {
var value = btn.classList.toggle('btn-on');
chart.options.plugins.filler.propagate = value;
chart.update();
}
function toggleSmooth(btn) {
var value = btn.classList.toggle('btn-on');
chart.options.elements.line.tension = value? 0.4 : 0.000001;
chart.update();
}
function randomize() {
inputs.from = [];
chart.data.datasets.forEach(function(dataset) {
dataset.data = generateData();
});
chart.update();
}
</script>
</body>
</html>

64
samples/style.css Normal file
View File

@ -0,0 +1,64 @@
body, html {
font-family: sans-serif;
padding: 0;
margin: 0;
}
.content {
max-width: 800px;
margin: auto;
padding: 16px;
}
.wrapper {
min-height: 400px;
padding: 16px 0;
position: relative;
}
.wrapper.col-2 {
display: inline-block;
min-height: 256px;
width: 49%;
}
@media (max-width: 400px) {
.wrapper.col-2 {
width: 100%
}
}
.wrapper canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
.toolbar {
display: flex;
}
.toolbar > * {
margin: 0 8px 0 0;
}
.btn-on {
border-style: inset;
}
.analyser table {
color: #333;
font-size: 0.9rem;
margin: 8px 0;
width: 100%
}
.analyser th {
background-color: #f0f0f0;
padding: 2px;
}
.analyser td {
padding: 2px;
text-align: center;
}

View File

@ -1,3 +1,7 @@
/* global Chart */
'use strict';
window.chartColors = {
red: 'rgb(255, 99, 132)',
orange: 'rgb(255, 159, 64)',
@ -5,9 +9,111 @@ window.chartColors = {
green: 'rgb(75, 192, 192)',
blue: 'rgb(54, 162, 235)',
purple: 'rgb(153, 102, 255)',
grey: 'rgb(231,233,237)'
grey: 'rgb(201, 203, 207)'
};
window.randomScalingFactor = function() {
return (Math.random() > 0.5 ? 1.0 : -1.0) * Math.round(Math.random() * 100);
}
};
(function(global) {
var Months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
var Samples = global.Samples || (global.Samples = {});
Samples.utils = {
// Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/
srand: function(seed) {
this._seed = seed;
},
rand: function(min, max) {
var seed = this._seed;
min = min === undefined? 0 : min;
max = max === undefined? 1 : max;
this._seed = (seed * 9301 + 49297) % 233280;
return min + (this._seed / 233280) * (max - min);
},
numbers: function(config) {
var cfg = config || {};
var min = cfg.min || 0;
var max = cfg.max || 1;
var from = cfg.from || [];
var count = cfg.count || 8;
var decimals = cfg.decimals || 8;
var continuity = cfg.continuity || 1;
var dfactor = Math.pow(10, decimals) || 0;
var data = [];
var i, value;
for (i=0; i<count; ++i) {
value = (from[i] || 0) + this.rand(min, max);
if (this.rand() <= continuity) {
data.push(Math.round(dfactor * value) / dfactor);
} else {
data.push(null);
}
}
return data;
},
labels: function(config) {
var cfg = config || {};
var min = cfg.min || 0;
var max = cfg.max || 100;
var count = cfg.count || 8;
var step = (max-min) / count;
var decimals = cfg.decimals || 8;
var dfactor = Math.pow(10, decimals) || 0;
var prefix = cfg.prefix || '';
var values = [];
var i;
for (i=min; i<max; i+=step) {
values.push(prefix + Math.round(dfactor * i) / dfactor);
}
return values;
},
months: function(config) {
var cfg = config || {};
var count = cfg.count || 12;
var section = cfg.section;
var values = [];
var i, value;
for (i=0; i<count; ++i) {
value = Months[Math.ceil(i)%12];
values.push(value.substring(0, section));
}
return values;
},
transparentize: function(color, opacity) {
var alpha = opacity === undefined? 0.5 : 1 - opacity;
return Chart.helpers.color(color).alpha(alpha).rgbString();
},
merge: Chart.helpers.configMerge
};
Samples.utils.srand(Date.now());
}(this));

View File

@ -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;

View File

@ -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();

View File

@ -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()
}
});

View File

@ -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;
};

View File

@ -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;
}

View 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);
}
}
};
};

View File

@ -40,7 +40,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "top",
"fill": "end",
"tension": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -40,7 +40,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "top",
"fill": "end",
"tension": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -40,7 +40,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "zero",
"fill": "origin",
"tension": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -41,7 +41,7 @@
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent",
"fill": "zero"
"fill": "origin"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -41,7 +41,7 @@
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent",
"fill": "zero"
"fill": "origin"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -42,7 +42,7 @@
"cubicInterpolationMode": "monotone",
"borderColor": "transparent",
"stepped": true,
"fill": "zero"
"fill": "origin"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -42,7 +42,7 @@
"cubicInterpolationMode": "monotone",
"borderColor": "transparent",
"stepped": true,
"fill": "zero"
"fill": "origin"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -40,7 +40,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "zero",
"fill": "origin",
"tension": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -40,7 +40,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "bottom",
"fill": "start",
"tension": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -40,7 +40,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "bottom",
"fill": "start",
"tension": 0
}
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,62 @@
{
"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": 1
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": "+1"
}, {
"backgroundColor": "rgba(0, 0, 255, 0.25)",
"data": [0, 2, 0, -2, 0, 2, 0, null, null],
"fill": 3
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": "-2"
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": "-1"
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"ticks": {
"display": false
}
}],
"yAxes": [{
"ticks": {
"display": false
}
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"borderColor": "transparent",
"tension": 0
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,62 @@
{
"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": 1
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": "+1"
}, {
"backgroundColor": "rgba(0, 0, 255, 0.25)",
"data": [0, 2, 0, -2, 0, 2, 0, null, null],
"fill": 3
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": "-2"
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": "-1"
}]
},
"options": {
"responsive": false,
"spanGaps": true,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"ticks": {
"display": false
}
}],
"yAxes": [{
"ticks": {
"display": false
}
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent"
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,62 @@
{
"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": 1
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": "+1"
}, {
"backgroundColor": "rgba(0, 0, 255, 0.25)",
"data": [0, 2, 0, -2, 0, 2, 0, null, null],
"fill": 3
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": "-2"
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": "-1"
}]
},
"options": {
"responsive": false,
"spanGaps": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"ticks": {
"display": false
}
}],
"yAxes": [{
"ticks": {
"display": false
}
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"cubicInterpolationMode": "monotone",
"borderColor": "transparent"
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,62 @@
{
"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": 1
}, {
"backgroundColor": "rgba(0, 255, 0, 0.25)",
"data": [1, 0, null, 1, 0, null, -1, 0, 1],
"fill": "+1"
}, {
"backgroundColor": "rgba(0, 0, 255, 0.25)",
"data": [0, 2, 0, -2, 0, 2, 0, null, null],
"fill": 3
}, {
"backgroundColor": "rgba(255, 0, 255, 0.25)",
"data": [2, 0, -2, 0, 2, 0, -2, 0, 2],
"fill": "-2"
}, {
"backgroundColor": "rgba(255, 255, 0, 0.25)",
"data": [3, 1, -1, -3, -1, 1, 3, 1, -1],
"fill": "-1"
}]
},
"options": {
"responsive": false,
"spanGaps": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"ticks": {
"display": false
}
}],
"yAxes": [{
"ticks": {
"display": false
}
}]
},
"elements": {
"point": {
"radius": 0
},
"line": {
"borderColor": "transparent",
"tension": 0
}
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -36,7 +36,7 @@
"line": {
"borderColor": "transparent",
"tension": 0.5,
"fill": "zero"
"fill": "origin"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -35,7 +35,7 @@
},
"line": {
"borderColor": "transparent",
"fill": "zero"
"fill": "origin"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -509,48 +509,6 @@ describe('Line controller tests', function() {
});
it('should find the correct scale zero when the data is all positive', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [{
data: [10, 15, 20, 20],
label: 'dataset1',
}],
labels: ['label1', 'label2', 'label3', 'label4']
},
});
var meta = chart.getDatasetMeta(0);
expect(meta.dataset._model).toEqual(jasmine.objectContaining({
scaleTop: 32,
scaleBottom: 484,
scaleZero: 484,
}));
});
it('should find the correct scale zero when the data is all negative', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [{
data: [-10, -15, -20, -20],
label: 'dataset1',
}],
labels: ['label1', 'label2', 'label3', 'label4']
},
});
var meta = chart.getDatasetMeta(0);
expect(meta.dataset._model).toEqual(jasmine.objectContaining({
scaleTop: 32,
scaleBottom: 484,
scaleZero: 32,
}));
});
it('should fall back to the line styles for points', function() {
var chart = window.acquireChart({
type: 'line',

View File

@ -111,10 +111,6 @@ describe('Radar controller tests', function() {
meta.controller.reset(); // reset first
// Line element
expect(meta.dataset._model.scaleTop).toBeCloseToPixel(32);
expect(meta.dataset._model.scaleBottom).toBeCloseToPixel(512);
expect(meta.dataset._model.scaleZero.x).toBeCloseToPixel(256);
expect(meta.dataset._model.scaleZero.y).toBeCloseToPixel(272);
expect(meta.dataset._model).toEqual(jasmine.objectContaining({
backgroundColor: 'rgb(255, 0, 0)',
borderCapStyle: 'round',
@ -198,10 +194,6 @@ describe('Radar controller tests', function() {
meta.controller.update();
expect(meta.dataset._model.scaleTop).toBeCloseToPixel(32);
expect(meta.dataset._model.scaleBottom).toBeCloseToPixel(512);
expect(meta.dataset._model.scaleZero.x).toBeCloseToPixel(256);
expect(meta.dataset._model.scaleZero.y).toBeCloseToPixel(272);
expect(meta.dataset._model).toEqual(jasmine.objectContaining({
backgroundColor: 'rgb(98, 98, 98)',
borderCapStyle: 'butt',
@ -262,10 +254,6 @@ describe('Radar controller tests', function() {
meta.controller.update();
expect(meta.dataset._model.scaleTop).toBeCloseToPixel(32);
expect(meta.dataset._model.scaleBottom).toBeCloseToPixel(512);
expect(meta.dataset._model.scaleZero.x).toBeCloseToPixel(256);
expect(meta.dataset._model.scaleZero.y).toBeCloseToPixel(272);
expect(meta.dataset._model).toEqual(jasmine.objectContaining({
backgroundColor: 'rgb(55, 55, 54)',
borderCapStyle: 'square',

File diff suppressed because it is too large Load Diff

View File

@ -86,6 +86,27 @@ describe('Deprecations', function() {
}, 200);
});
});
describe('Chart.elements.Line: fill option', function() {
it('should decode "zero", "top" and "bottom" as "origin", "start" and "end"', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 'zero'},
{fill: 'bottom'},
{fill: 'top'},
]
}
});
['origin', 'start', 'end'].forEach(function(expected, index) {
var meta = chart.getDatasetMeta(index);
expect(meta.$filler).toBeDefined();
expect(meta.$filler.fill).toBe(expected);
});
});
});
});
describe('Version 2.5.0', function() {

View File

@ -0,0 +1,265 @@
describe('Plugin.filler', function() {
function decodedFillValues(chart) {
return chart.data.datasets.map(function(dataset, index) {
var meta = chart.getDatasetMeta(index) || {};
expect(meta.$filler).toBeDefined();
return meta.$filler.fill;
});
}
describe('auto', jasmine.specsFromFixtures('plugin.filler'));
describe('dataset.fill', function() {
it('should support boundaries', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 'origin'},
{fill: 'start'},
{fill: 'end'},
]
}
});
expect(decodedFillValues(chart)).toEqual(['origin', 'start', 'end']);
});
it('should support absolute dataset index', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 1},
{fill: 3},
{fill: 0},
{fill: 2},
]
}
});
expect(decodedFillValues(chart)).toEqual([1, 3, 0, 2]);
});
it('should support relative dataset index', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: '+3'},
{fill: '-1'},
{fill: '+1'},
{fill: '-2'},
]
}
});
expect(decodedFillValues(chart)).toEqual([
3, // 0 + 3
0, // 1 - 1
3, // 2 + 1
1, // 3 - 2
]);
});
it('should handle default fill when true (origin)', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: true},
{fill: false},
]
}
});
expect(decodedFillValues(chart)).toEqual(['origin', false]);
});
it('should ignore self dataset index', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 0},
{fill: '-0'},
{fill: '+0'},
{fill: 3},
]
}
});
expect(decodedFillValues(chart)).toEqual([
false, // 0 === 0
false, // 1 === 1 - 0
false, // 2 === 2 + 0
false, // 3 === 3
]);
});
it('should ignore out of bounds dataset index', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: -2},
{fill: 4},
{fill: '-3'},
{fill: '+1'},
]
}
});
expect(decodedFillValues(chart)).toEqual([
false, // 0 - 2 < 0
false, // 1 + 4 > 3
false, // 2 - 3 < 0
false, // 3 + 1 > 3
]);
});
it('should ignore invalid values', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 'foo'},
{fill: '+foo'},
{fill: '-foo'},
{fill: '+1.1'},
{fill: '-2.2'},
{fill: 3.3},
{fill: -4.4},
{fill: NaN},
{fill: Infinity},
{fill: ''},
{fill: null},
{fill: []},
{fill: {}},
{fill: function() {}}
]
}
});
expect(decodedFillValues(chart)).toEqual([
false, // NaN (string)
false, // NaN (string)
false, // NaN (string)
false, // float (string)
false, // float (string)
false, // float (number)
false, // float (number)
false, // NaN
false, // !isFinite
false, // empty string
false, // null
false, // array
false, // object
false, // function
]);
});
});
describe('options.plugins.filler.propagate', function() {
it('should compute propagated fill targets if true', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 'start', hidden: true},
{fill: '-1', hidden: true},
{fill: 1, hidden: true},
{fill: '-2', hidden: true},
{fill: '+1'},
{fill: '+2'},
{fill: '-1'},
{fill: 'end', hidden: true},
]
},
options: {
plugins: {
filler: {
propagate: true
}
}
}
});
expect(decodedFillValues(chart)).toEqual([
'start', // 'start'
'start', // 1 - 1 -> 0 (hidden) -> 'start'
'start', // 1 (hidden) -> 0 (hidden) -> 'start'
'start', // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 'start'
5, // 4 + 1
'end', // 5 + 2 -> 7 (hidden) -> 'end'
5, // 6 - 1 -> 5
'end', // 'end'
]);
});
it('should preserve initial fill targets if false', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: 'start', hidden: true},
{fill: '-1', hidden: true},
{fill: 1, hidden: true},
{fill: '-2', hidden: true},
{fill: '+1'},
{fill: '+2'},
{fill: '-1'},
{fill: 'end', hidden: true},
]
},
options: {
plugins: {
filler: {
propagate: false
}
}
}
});
expect(decodedFillValues(chart)).toEqual([
'start', // 'origin'
0, // 1 - 1
1, // 1
1, // 3 - 2
5, // 4 + 1
7, // 5 + 2
5, // 6 - 1
'end', // 'end'
]);
});
it('should prevent recursive propagation', function() {
var chart = window.acquireChart({
type: 'line',
data: {
datasets: [
{fill: '+2', hidden: true},
{fill: '-1', hidden: true},
{fill: '-1', hidden: true},
{fill: '-2'}
]
},
options: {
plugins: {
filler: {
propagate: true
}
}
}
});
expect(decodedFillValues(chart)).toEqual([
false, // 0 + 2 -> 2 (hidden) -> 1 (hidden) -> 0 (loop)
false, // 1 - 1 -> 0 (hidden) -> 2 (hidden) -> 1 (loop)
false, // 2 - 1 -> 1 (hidden) -> 0 (hidden) -> 2 (loop)
false, // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 2 (hidden) -> 1 (loop)
]);
});
});
});