diff --git a/samples/area/analyser.js b/samples/area/analyser.js new file mode 100644 index 00000000000..f9d001af57e --- /dev/null +++ b/samples/area/analyser.js @@ -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' + + 'Dataset' + + 'Fill' + + 'Target (visibility)' + + '' + + stats.map(function(stat) { + var target = stat.target; + var row = + '' + stat.index + '' + + '' + JSON.stringify(stat.fill) + ''; + + if (target === false) { + target = 'none'; + } else if (isFinite(target)) { + target = 'dataset ' + target; + } else { + target = 'boundary "' + target + '"'; + } + + if (stat.visible) { + row += '' + target + ''; + } else { + row += '(hidden)'; + } + + return '' + row + ''; + }).join('') + ''; + } + }); +}()); diff --git a/samples/area/line-boundaries.html b/samples/area/line-boundaries.html new file mode 100644 index 00000000000..ca0aab3a79e --- /dev/null +++ b/samples/area/line-boundaries.html @@ -0,0 +1,120 @@ + + + + + + + area > boundaries | Chart.js sample + + + + + + +
+
+
+
+
+ +
+ + +
+
+ + + + diff --git a/samples/area/line-datasets.html b/samples/area/line-datasets.html new file mode 100644 index 00000000000..37719df26b3 --- /dev/null +++ b/samples/area/line-datasets.html @@ -0,0 +1,157 @@ + + + + + + + area > datasets | Chart.js sample + + + + + + +
+
+ +
+
+ + + +
+
+
+ + + + diff --git a/samples/area/radar.html b/samples/area/radar.html new file mode 100644 index 00000000000..23f2bdd3627 --- /dev/null +++ b/samples/area/radar.html @@ -0,0 +1,136 @@ + + + + + + + area > radar | Chart.js sample + + + + + + +
+
+ +
+
+ + + +
+
+
+ + + + diff --git a/samples/style.css b/samples/style.css new file mode 100644 index 00000000000..5b586506418 --- /dev/null +++ b/samples/style.css @@ -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; +} diff --git a/samples/utils.js b/samples/utils.js index 34965ce66ca..dc5bd12aea2 100644 --- a/samples/utils.js +++ b/samples/utils.js @@ -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); -} \ No newline at end of file +}; + +(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) { + 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; i0; --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 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) + ]); + }); + }); +});