From bfea129c15dffde12348df795c4acd7fbf84bf7e Mon Sep 17 00:00:00 2001 From: Andy Stabler Date: Fri, 17 Feb 2017 11:22:26 +0000 Subject: [PATCH] Update Chart.js to version 2.5.0 --- README.md | 2 +- lib/chart-js-rails/version.rb | 2 +- vendor/assets/javascripts/Chart.js | 2770 +++++++++++++++++----------- 3 files changed, 1743 insertions(+), 1031 deletions(-) diff --git a/README.md b/README.md index 1601836..343185b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Chart.js for Rails -Integrate Chart.js into Rails Asset Pipeline (current version is 2.4.0) +Integrate Chart.js into Rails Asset Pipeline (current version is 2.5.0) ## Installation diff --git a/lib/chart-js-rails/version.rb b/lib/chart-js-rails/version.rb index 68b0c84..b7c76a5 100644 --- a/lib/chart-js-rails/version.rb +++ b/lib/chart-js-rails/version.rb @@ -1,5 +1,5 @@ module ChartJs module Rails - VERSION = '0.1.1'.freeze + VERSION = '0.1.2'.freeze end end diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js index c66671c..56e06df 100644 --- a/vendor/assets/javascripts/Chart.js +++ b/vendor/assets/javascripts/Chart.js @@ -1,9 +1,9 @@ /*! * Chart.js * http://chartjs.org/ - * Version: 2.4.0 + * Version: 2.5.0 * - * Copyright 2016 Nick Downie + * Copyright 2017 Nick Downie * Released under the MIT license * https://github.com/chartjs/Chart.js/blob/master/LICENSE.md */ @@ -1668,14 +1668,15 @@ module.exports = { var Chart = require(28)(); require(26)(Chart); +require(42)(Chart); require(22)(Chart); +require(31)(Chart); require(25)(Chart); require(21)(Chart); require(23)(Chart); require(24)(Chart); require(29)(Chart); require(33)(Chart); -require(31)(Chart); require(34)(Chart); require(32)(Chart); require(35)(Chart); @@ -1688,12 +1689,12 @@ require(38)(Chart); require(39)(Chart); require(40)(Chart); +require(45)(Chart); require(43)(Chart); -require(41)(Chart); -require(42)(Chart); require(44)(Chart); -require(45)(Chart); require(46)(Chart); +require(47)(Chart); +require(48)(Chart); // Controllers must be loaded after elements // See Chart.core.datasetController.dataElementType @@ -1714,7 +1715,7 @@ require(14)(Chart); window.Chart = module.exports = Chart; -},{"10":10,"11":11,"12":12,"13":13,"14":14,"15":15,"16":16,"17":17,"18":18,"19":19,"20":20,"21":21,"22":22,"23":23,"24":24,"25":25,"26":26,"27":27,"28":28,"29":29,"30":30,"31":31,"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"41":41,"42":42,"43":43,"44":44,"45":45,"46":46,"8":8,"9":9}],8:[function(require,module,exports){ +},{"10":10,"11":11,"12":12,"13":13,"14":14,"15":15,"16":16,"17":17,"18":18,"19":19,"20":20,"21":21,"22":22,"23":23,"24":24,"25":25,"26":26,"27":27,"28":28,"29":29,"30":30,"31":31,"32":32,"33":33,"34":34,"35":35,"36":36,"37":37,"38":38,"39":39,"40":40,"42":42,"43":43,"44":44,"45":45,"46":46,"47":47,"48":48,"8":8,"9":9}],8:[function(require,module,exports){ 'use strict'; module.exports = function(Chart) { @@ -1878,21 +1879,33 @@ module.exports = function(Chart) { initialize: function(chart, datasetIndex) { Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex); + var me = this; + var meta = me.getMeta(); + var dataset = me.getDataset(); + + meta.stack = dataset.stack; // Use this to indicate that this is a bar dataset. - this.getMeta().bar = true; + meta.bar = true; }, - // Get the number of datasets that display bars. We use this to correctly calculate the bar width - getBarCount: function() { + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { var me = this; - var barCount = 0; + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); + + var stacks = []; helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { - var meta = me.chart.getDatasetMeta(datasetIndex); - if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) { - ++barCount; + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } }, me); - return barCount; + + return stacks.length; }, update: function(reset) { @@ -1917,7 +1930,7 @@ module.exports = function(Chart) { rectangle._datasetIndex = me.index; rectangle._index = index; - var ruler = me.getRuler(index); + var ruler = me.getRuler(index); // The index argument for compatible rectangle._model = { x: me.calculateBarX(index, me.index, ruler), y: reset ? scaleBase : me.calculateBarY(index, me.index), @@ -1927,6 +1940,7 @@ module.exports = function(Chart) { datasetLabel: dataset.label, // Appearance + horizontal: false, base: reset ? scaleBase : me.calculateBarBase(me.index, index), width: me.calculateBarWidth(ruler), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), @@ -1942,9 +1956,11 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var base = 0; + var base = yScale.getBaseValue(); + var original = base; - if (yScale.options.stacked) { + if ((yScale.options.stacked === true) || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -1952,9 +1968,10 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); + base += value < 0 ? Math.min(currentVal, original) : Math.max(currentVal, original); } } @@ -1964,34 +1981,22 @@ module.exports = function(Chart) { return yScale.getBasePixel(); }, - getRuler: function(index) { + getRuler: function() { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var datasetCount = me.getBarCount(); - - var tickWidth; + var stackCount = me.getStackCount(); - if (xScale.options.type === 'category') { - tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index); - } else { - // Average width - tickWidth = xScale.width / xScale.ticks.length; - } + var tickWidth = xScale.width / xScale.ticks.length; var categoryWidth = tickWidth * xScale.options.categoryPercentage; var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2; - var fullBarWidth = categoryWidth / datasetCount; - - if (xScale.ticks.length !== me.chart.data.labels.length) { - var perc = xScale.ticks.length / me.chart.data.labels.length; - fullBarWidth = fullBarWidth * perc; - } + var fullBarWidth = categoryWidth / stackCount; var barWidth = fullBarWidth * xScale.options.barPercentage; var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickWidth: tickWidth, categoryWidth: categoryWidth, categorySpacing: categorySpacing, @@ -2002,46 +2007,50 @@ module.exports = function(Chart) { }, calculateBarWidth: function(ruler) { - var xScale = this.getScaleForId(this.getMeta().xAxisID); + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); if (xScale.options.barThickness) { return xScale.options.barThickness; } - return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth; + return ruler.barWidth; }, - // Get bar index from the given dataset index accounting for the fact that not all bars are visible - getBarIndex: function(datasetIndex) { - var barIndex = 0; - var meta, j; + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var yScale = me.getScaleForId(meta.yAxisID); + var dsMeta, j; + var stacks = [meta.stack]; for (j = 0; j < datasetIndex; ++j) { - meta = this.chart.getDatasetMeta(j); - if (meta.bar && this.chart.isDatasetVisible(j)) { - ++barIndex; + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (yScale.options.stacked === false || + (yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); } } - return barIndex; + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex, ruler) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; - if (xScale.options.stacked) { - return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; - } - return leftTick + (ruler.barWidth / 2) + ruler.categorySpacing + - (ruler.barWidth * barIndex) + + (ruler.barWidth * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); }, calculateBarY: function(index, datasetIndex) { @@ -2050,15 +2059,17 @@ module.exports = function(Chart) { var yScale = me.getScaleForId(meta.yAxisID); var value = Number(me.getDataset().data[index]); - if (yScale.options.stacked) { - - var sumPos = 0, - sumNeg = 0; + if (yScale.options.stacked || + (yScale.options.stacked === undefined && meta.stack !== undefined)) { + var base = yScale.getBaseValue(); + var sumPos = base, + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -2084,12 +2095,14 @@ module.exports = function(Chart) { var dataset = me.getDataset(); var i, len; + Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); for (i = 0, len = metaData.length; i < len; ++i) { var d = dataset.data[i]; if (d !== null && d !== undefined && !isNaN(d)) { metaData[i].transition(easingDecimal).draw(); } } + Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); }, setHoverStyle: function(rectangle) { @@ -2174,6 +2187,27 @@ module.exports = function(Chart) { }; Chart.controllers.horizontalBar = Chart.controllers.bar.extend({ + + // Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible + getStackCount: function() { + var me = this; + var meta = me.getMeta(); + var xScale = me.getScaleForId(meta.xAxisID); + + var stacks = []; + helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) { + var dsMeta = me.chart.getDatasetMeta(datasetIndex); + if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + }, me); + + return stacks.length; + }, + updateElement: function(rectangle, index, reset) { var me = this; var meta = me.getMeta(); @@ -2189,7 +2223,7 @@ module.exports = function(Chart) { rectangle._datasetIndex = me.index; rectangle._index = index; - var ruler = me.getRuler(index); + var ruler = me.getRuler(index); // The index argument for compatible rectangle._model = { x: reset ? scaleBase : me.calculateBarX(index, me.index), y: me.calculateBarY(index, me.index, ruler), @@ -2199,6 +2233,7 @@ module.exports = function(Chart) { datasetLabel: dataset.label, // Appearance + horizontal: true, base: reset ? scaleBase : me.calculateBarBase(me.index, index), height: me.calculateBarHeight(ruler), backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor), @@ -2206,62 +2241,6 @@ module.exports = function(Chart) { borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor), borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth) }; - rectangle.draw = function() { - var ctx = this._chart.ctx; - var vm = this._view; - - var halfHeight = vm.height / 2, - topY = vm.y - halfHeight, - bottomY = vm.y + halfHeight, - right = vm.base - (vm.base - vm.x), - halfStroke = vm.borderWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - topY += halfStroke; - bottomY -= halfStroke; - right += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // Corner points, from bottom-left to bottom-right clockwise - // | 1 2 | - // | 0 3 | - var corners = [ - [vm.base, bottomY], - [vm.base, topY], - [right, topY], - [right, bottomY] - ]; - - // Find first (starting) corner with fallback to 'bottom' - var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); - if (startCorner === -1) { - startCorner = 0; - } - - function cornerAt(cornerIndex) { - return corners[(startCorner + cornerIndex) % 4]; - } - - // Draw rectangle from 'startCorner' - ctx.moveTo.apply(ctx, cornerAt(0)); - for (var i = 1; i < 4; i++) { - ctx.lineTo.apply(ctx, cornerAt(i)); - } - - ctx.fill(); - if (vm.borderWidth) { - ctx.stroke(); - } - }; rectangle.pivot(); }, @@ -2270,9 +2249,11 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var xScale = me.getScaleForId(meta.xAxisID); - var base = 0; + var base = xScale.getBaseValue(); + var originalBase = base; - if (xScale.options.stacked) { + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { var chart = me.chart; var datasets = chart.data.datasets; var value = Number(datasets[datasetIndex].data[index]); @@ -2280,9 +2261,10 @@ module.exports = function(Chart) { for (var i = 0; i < datasetIndex; i++) { var currentDs = datasets[i]; var currentDsMeta = chart.getDatasetMeta(i); - if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i)) { + if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i) && + meta.stack === currentDsMeta.stack) { var currentVal = Number(currentDs.data[index]); - base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0); + base += value < 0 ? Math.min(currentVal, originalBase) : Math.max(currentVal, originalBase); } } @@ -2292,33 +2274,22 @@ module.exports = function(Chart) { return xScale.getBasePixel(); }, - getRuler: function(index) { + getRuler: function() { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var datasetCount = me.getBarCount(); + var stackCount = me.getStackCount(); - var tickHeight; - if (yScale.options.type === 'category') { - tickHeight = yScale.getPixelForTick(index + 1) - yScale.getPixelForTick(index); - } else { - // Average width - tickHeight = yScale.width / yScale.ticks.length; - } + var tickHeight = yScale.height / yScale.ticks.length; var categoryHeight = tickHeight * yScale.options.categoryPercentage; var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2; - var fullBarHeight = categoryHeight / datasetCount; - - if (yScale.ticks.length !== me.chart.data.labels.length) { - var perc = yScale.ticks.length / me.chart.data.labels.length; - fullBarHeight = fullBarHeight * perc; - } + var fullBarHeight = categoryHeight / stackCount; var barHeight = fullBarHeight * yScale.options.barPercentage; var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage); return { - datasetCount: datasetCount, + stackCount: stackCount, tickHeight: tickHeight, categoryHeight: categoryHeight, categorySpacing: categorySpacing, @@ -2330,11 +2301,33 @@ module.exports = function(Chart) { calculateBarHeight: function(ruler) { var me = this; - var yScale = me.getScaleForId(me.getMeta().yAxisID); + var meta = me.getMeta(); + var yScale = me.getScaleForId(meta.yAxisID); if (yScale.options.barThickness) { return yScale.options.barThickness; } - return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight; + return ruler.barHeight; + }, + + // Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible + getStackIndex: function(datasetIndex) { + var me = this; + var meta = me.chart.getDatasetMeta(datasetIndex); + var xScale = me.getScaleForId(meta.xAxisID); + var dsMeta, j; + var stacks = [meta.stack]; + + for (j = 0; j < datasetIndex; ++j) { + dsMeta = this.chart.getDatasetMeta(j); + if (dsMeta.bar && this.chart.isDatasetVisible(j) && + (xScale.options.stacked === false || + (xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) || + (xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) { + stacks.push(dsMeta.stack); + } + } + + return stacks.length - 1; }, calculateBarX: function(index, datasetIndex) { @@ -2343,15 +2336,17 @@ module.exports = function(Chart) { var xScale = me.getScaleForId(meta.xAxisID); var value = Number(me.getDataset().data[index]); - if (xScale.options.stacked) { - - var sumPos = 0, - sumNeg = 0; + if (xScale.options.stacked || + (xScale.options.stacked === undefined && meta.stack !== undefined)) { + var base = xScale.getBaseValue(); + var sumPos = base, + sumNeg = base; for (var i = 0; i < datasetIndex; i++) { var ds = me.chart.data.datasets[i]; var dsMeta = me.chart.getDatasetMeta(i); - if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) { + if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i) && + meta.stack === dsMeta.stack) { var stackedVal = Number(ds.data[index]); if (stackedVal < 0) { sumNeg += stackedVal || 0; @@ -2374,20 +2369,16 @@ module.exports = function(Chart) { var me = this; var meta = me.getMeta(); var yScale = me.getScaleForId(meta.yAxisID); - var barIndex = me.getBarIndex(datasetIndex); + var stackIndex = me.getStackIndex(datasetIndex); var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0; - if (yScale.options.stacked) { - return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing; - } - return topTick + (ruler.barHeight / 2) + ruler.categorySpacing + - (ruler.barHeight * barIndex) + + (ruler.barHeight * stackIndex) + (ruler.barSpacing / 2) + - (ruler.barSpacing * barIndex); + (ruler.barSpacing * stackIndex); } }); }; @@ -2702,7 +2693,7 @@ module.exports = function(Chart) { chart.borderWidth = me.getMaxBorderWidth(meta.data); chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0); - chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 1, 0); + chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0); chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount(); chart.offsetX = offset.x * chart.outerRadius; chart.offsetY = offset.y * chart.outerRadius; @@ -2710,7 +2701,7 @@ module.exports = function(Chart) { meta.total = me.calculateTotal(); me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index)); - me.innerRadius = me.outerRadius - chart.radiusLength; + me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0); helpers.each(meta.data, function(arc, index) { me.updateElement(arc, index, reset); @@ -2963,11 +2954,11 @@ module.exports = function(Chart) { var dataset = this.getDataset(); var custom = point.custom || {}; - if (custom.borderWidth) { + if (!isNaN(custom.borderWidth)) { borderWidth = custom.borderWidth; - } else if (dataset.pointBorderWidth) { + } else if (!isNaN(dataset.pointBorderWidth)) { borderWidth = helpers.getValueAtIndexOrDefault(dataset.pointBorderWidth, index, borderWidth); - } else if (dataset.borderWidth) { + } else if (!isNaN(dataset.borderWidth)) { borderWidth = dataset.borderWidth; } @@ -3116,14 +3107,16 @@ module.exports = function(Chart) { points[i].transition(easingDecimal); } + Chart.canvasHelpers.clipArea(me.chart.chart.ctx, me.chart.chartArea); // Transition and Draw the line if (lineEnabled(me.getDataset(), me.chart.options)) { meta.dataset.transition(easingDecimal).draw(); } + Chart.canvasHelpers.unclipArea(me.chart.chart.ctx); // Draw the points for (i=0, ilen=points.length; i x)); + }; helpers.max = function(array) { return array.reduce(function(max, value) { if (!isNaN(value)) { @@ -5385,7 +5351,10 @@ module.exports = function(Chart) { pointBefore = i > 0 ? pointsWithTangents[i - 1] : null; pointAfter = i < pointsLen - 1 ? pointsWithTangents[i + 1] : null; if (pointAfter && !pointAfter.model.skip) { - pointCurrent.deltaK = (pointAfter.model.y - pointCurrent.model.y) / (pointAfter.model.x - pointCurrent.model.x); + var slopeDeltaX = (pointAfter.model.x - pointCurrent.model.x); + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + pointCurrent.deltaK = slopeDeltaX !== 0 ? (pointAfter.model.y - pointCurrent.model.y) / slopeDeltaX : 0; } if (!pointBefore || pointBefore.model.skip) { @@ -5695,16 +5664,6 @@ module.exports = function(Chart) { return window.setTimeout(callback, 1000 / 60); }; }()); - helpers.cancelAnimFrame = (function() { - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - }()); // -- DOM methods helpers.getRelativePosition = function(evt, chart) { var mouseX, mouseY; @@ -5761,23 +5720,6 @@ module.exports = function(Chart) { node['on' + eventType] = helpers.noop; } }; - helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) { - // Create the events object if it's not already present - var events = chartInstance.events = chartInstance.events || {}; - - helpers.each(arrayOfEvents, function(eventName) { - events[eventName] = function() { - handler.apply(chartInstance, arguments); - }; - helpers.addEvent(chartInstance.chart.canvas, eventName, events[eventName]); - }); - }; - helpers.unbindEvents = function(chartInstance, arrayOfEvents) { - var canvas = chartInstance.chart.canvas; - helpers.each(arrayOfEvents, function(handler, eventName) { - helpers.removeEvent(canvas, eventName, handler); - }); - }; // Private helper function to convert max-width/max-height values that may be percentages into a number function parseMaxStyle(styleValue, node, parentProperty) { @@ -5968,73 +5910,6 @@ module.exports = function(Chart) { return color(c); }; - helpers.addResizeListener = function(node, callback) { - var iframe = document.createElement('iframe'); - iframe.className = 'chartjs-hidden-iframe'; - iframe.style.cssText = - 'display:block;'+ - 'overflow:hidden;'+ - 'border:0;'+ - 'margin:0;'+ - 'top:0;'+ - 'left:0;'+ - 'bottom:0;'+ - 'right:0;'+ - 'height:100%;'+ - 'width:100%;'+ - 'position:absolute;'+ - 'pointer-events:none;'+ - 'z-index:-1;'; - - // Prevent the iframe to gain focus on tab. - // https://github.com/chartjs/Chart.js/issues/3090 - iframe.tabIndex = -1; - - // Let's keep track of this added iframe and thus avoid DOM query when removing it. - var stub = node._chartjs = { - resizer: iframe, - ticking: false - }; - - // Throttle the callback notification until the next animation frame. - var notify = function() { - if (!stub.ticking) { - stub.ticking = true; - helpers.requestAnimFrame.call(window, function() { - if (stub.resizer) { - stub.ticking = false; - return callback(); - } - }); - } - }; - - // If the iframe is re-attached to the DOM, the resize listener is removed because the - // content is reloaded, so make sure to install the handler after the iframe is loaded. - // https://github.com/chartjs/Chart.js/issues/3521 - helpers.addEvent(iframe, 'load', function() { - helpers.addEvent(iframe.contentWindow || iframe, 'resize', notify); - - // The iframe size might have changed while loading, which can also - // happen if the size has been changed while detached from the DOM. - notify(); - }); - - node.insertBefore(iframe, node.firstChild); - }; - helpers.removeResizeListener = function(node) { - if (!node || !node._chartjs) { - return; - } - - var iframe = node._chartjs.resizer; - if (iframe) { - iframe.parentNode.removeChild(iframe); - node._chartjs.resizer = null; - } - - delete node._chartjs; - }; helpers.isArray = Array.isArray? function(obj) { return Array.isArray(obj); @@ -6085,6 +5960,23 @@ module.exports = function(Chart) { module.exports = function(Chart) { var helpers = Chart.helpers; + /** + * Helper function to get relative position for an event + * @param {Event|IEvent} event - The event to get the position for + * @param {Chart} chart - The chart + * @returns {Point} the event position + */ + function getRelativePosition(e, chart) { + if (e.native) { + return { + x: e.x, + y: e.y + }; + } + + return helpers.getRelativePosition(e, chart); + } + /** * Helper function to traverse all of the visible elements in the chart * @param chart {chart} the chart @@ -6164,7 +6056,7 @@ module.exports = function(Chart) { } function indexMode(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var distanceMetric = function(pt1, pt2) { return Math.abs(pt1.x - pt2.x); }; @@ -6200,14 +6092,14 @@ module.exports = function(Chart) { */ /** - * @namespace Chart.Interaction * Contains interaction related functions + * @namespace Chart.Interaction */ Chart.Interaction = { // Helper function for different modes modes: { single: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var elements = []; parseVisibleItems(chart, function(element) { @@ -6248,7 +6140,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ dataset: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = options.intersect ? getIntersectItems(chart, position) : getNearestItems(chart, position, false); if (items.length > 0) { @@ -6275,7 +6167,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ point: function(chart, e) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); return getIntersectItems(chart, position); }, @@ -6288,7 +6180,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ nearest: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var nearestItems = getNearestItems(chart, position, options.intersect); // We have multiple items at the same distance from the event. Now sort by smallest @@ -6320,7 +6212,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ x: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; @@ -6351,7 +6243,7 @@ module.exports = function(Chart) { * @return {Chart.Element[]} Array of elements that are under the point. If none are found, an empty array is returned */ y: function(chart, e, options) { - var position = helpers.getRelativePosition(e, chart.chart); + var position = getRelativePosition(e, chart.chart); var items = []; var intersectsItem = false; @@ -6585,15 +6477,36 @@ module.exports = function(Chart) { minBoxSizes.push({ horizontal: isHorizontal, minSize: minSize, - box: box + box: box, }); } helpers.each(leftBoxes.concat(rightBoxes, topBoxes, bottomBoxes), getMinimumBoxSize); + // If a horizontal box has padding, we move the left boxes over to avoid ugly charts (see issue #2478) + var maxHorizontalLeftPadding = 0; + var maxHorizontalRightPadding = 0; + var maxVerticalTopPadding = 0; + var maxVerticalBottomPadding = 0; + + helpers.each(topBoxes.concat(bottomBoxes), function(horizontalBox) { + if (horizontalBox.getPadding) { + var boxPadding = horizontalBox.getPadding(); + maxHorizontalLeftPadding = Math.max(maxHorizontalLeftPadding, boxPadding.left); + maxHorizontalRightPadding = Math.max(maxHorizontalRightPadding, boxPadding.right); + } + }); + + helpers.each(leftBoxes.concat(rightBoxes), function(verticalBox) { + if (verticalBox.getPadding) { + var boxPadding = verticalBox.getPadding(); + maxVerticalTopPadding = Math.max(maxVerticalTopPadding, boxPadding.top); + maxVerticalBottomPadding = Math.max(maxVerticalBottomPadding, boxPadding.bottom); + } + }); + // At this point, maxChartAreaHeight and maxChartAreaWidth are the size the chart area could // be if the axes are drawn at their minimum sizes. - // Steps 5 & 6 var totalLeftBoxesWidth = leftPadding; var totalRightBoxesWidth = rightPadding; @@ -6609,8 +6522,8 @@ module.exports = function(Chart) { if (minBoxSize) { if (box.isHorizontal()) { var scaleMargin = { - left: totalLeftBoxesWidth, - right: totalRightBoxesWidth, + left: Math.max(totalLeftBoxesWidth, maxHorizontalLeftPadding), + right: Math.max(totalRightBoxesWidth, maxHorizontalRightPadding), top: 0, bottom: 0 }; @@ -6688,6 +6601,15 @@ module.exports = function(Chart) { totalBottomBoxesHeight += box.height; }); + // We may be adding some padding to account for rotated x axis labels + var leftPaddingAddition = Math.max(maxHorizontalLeftPadding - totalLeftBoxesWidth, 0); + totalLeftBoxesWidth += leftPaddingAddition; + totalRightBoxesWidth += Math.max(maxHorizontalRightPadding - totalRightBoxesWidth, 0); + + var topPaddingAddition = Math.max(maxVerticalTopPadding - totalTopBoxesHeight, 0); + totalTopBoxesHeight += topPaddingAddition; + totalBottomBoxesHeight += Math.max(maxVerticalBottomPadding - totalBottomBoxesHeight, 0); + // Figure out if our chart area changed. This would occur if the dataset layout label rotation // changed due to the application of the margins in step 6. Since we can only get bigger, this is safe to do // without calling `fit` again @@ -6720,8 +6642,8 @@ module.exports = function(Chart) { } // Step 7 - Position the boxes - var left = leftPadding; - var top = topPadding; + var left = leftPadding + leftPaddingAddition; + var top = topPadding + topPaddingAddition; function placeBox(box) { if (box.isHorizontal()) { @@ -6940,10 +6862,20 @@ module.exports = function(Chart) { beforeBuildLabels: noop, buildLabels: function() { var me = this; - me.legendItems = me.options.labels.generateLabels.call(me, me.chart); + var labelOpts = me.options.labels; + var legendItems = labelOpts.generateLabels.call(me, me.chart); + + if (labelOpts.filter) { + legendItems = legendItems.filter(function(item) { + return labelOpts.filter(item, me.chart.data); + }); + } + if (me.options.reverse) { - me.legendItems.reverse(); + legendItems.reverse(); } + + me.legendItems = legendItems; }, afterBuildLabels: noop, @@ -7182,7 +7114,7 @@ module.exports = function(Chart) { } } else if (y + itemHeight > me.bottom) { x = cursor.x = x + me.columnWidths[cursor.line] + labelOpts.padding; - y = cursor.y = me.top; + y = cursor.y = me.top + labelOpts.padding; cursor.line++; } @@ -7207,7 +7139,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param {IEvent} event - The event to handle * @return {Boolean} true if a change occured */ handleEvent: function(e) { @@ -7228,9 +7160,9 @@ module.exports = function(Chart) { return; } - var position = helpers.getRelativePosition(e, me.chart.chart), - x = position.x, - y = position.y; + // Chart event already has relative position in it + var x = e.x, + y = e.y; if (x >= me.left && x <= me.right && y >= me.top && y <= me.bottom) { // See if we are touching one of the dataset boxes @@ -7241,11 +7173,13 @@ module.exports = function(Chart) { if (x >= hitBox.left && x <= hitBox.left + hitBox.width && y >= hitBox.top && y <= hitBox.top + hitBox.height) { // Touching an element if (type === 'click') { - opts.onClick.call(me, e, me.legendItems[i]); + // use e.native for backwards compatibility + opts.onClick.call(me, e.native, me.legendItems[i]); changed = true; break; } else if (type === 'mousemove') { - opts.onHover.call(me, e, me.legendItems[i]); + // use e.native for backwards compatibility + opts.onHover.call(me, e.native, me.legendItems[i]); changed = true; break; } @@ -7257,20 +7191,45 @@ module.exports = function(Chart) { } }); + function createNewLegendAndAttach(chartInstance, legendOpts) { + var legend = new Chart.Legend({ + ctx: chartInstance.chart.ctx, + options: legendOpts, + chart: chartInstance + }); + chartInstance.legend = legend; + Chart.layoutService.addBox(chartInstance, legend); + } + // Register the legend plugin Chart.plugins.register({ beforeInit: function(chartInstance) { - var opts = chartInstance.options; - var legendOpts = opts.legend; + var legendOpts = chartInstance.options.legend; if (legendOpts) { - chartInstance.legend = new Chart.Legend({ - ctx: chartInstance.chart.ctx, - options: legendOpts, - chart: chartInstance - }); + createNewLegendAndAttach(chartInstance, legendOpts); + } + }, + beforeUpdate: function(chartInstance) { + var legendOpts = chartInstance.options.legend; - Chart.layoutService.addBox(chartInstance, chartInstance.legend); + if (legendOpts) { + legendOpts = helpers.configMerge(Chart.defaults.global.legend, legendOpts); + + if (chartInstance.legend) { + chartInstance.legend.options = legendOpts; + } else { + createNewLegendAndAttach(chartInstance, legendOpts); + } + } else { + Chart.layoutService.removeBox(chartInstance, chartInstance.legend); + delete chartInstance.legend; + } + }, + afterEvent: function(chartInstance, e) { + var legend = chartInstance.legend; + if (legend) { + legend.handleEvent(e); } } }); @@ -7281,7 +7240,9 @@ module.exports = function(Chart) { module.exports = function(Chart) { - var noop = Chart.helpers.noop; + var helpers = Chart.helpers; + + Chart.defaults.global.plugins = {}; /** * The plugin service singleton @@ -7289,8 +7250,20 @@ module.exports = function(Chart) { * @since 2.1.0 */ Chart.plugins = { + /** + * Globally registered plugins. + * @private + */ _plugins: [], + /** + * This identifier is used to invalidate the descriptors cache attached to each chart + * when a global plugin is registered or unregistered. In this case, the cache ID is + * incremented and descriptors are regenerated during following API calls. + * @private + */ + _cacheId: 0, + /** * Registers the given plugin(s) if not already registered. * @param {Array|Object} plugins plugin instance(s). @@ -7302,6 +7275,8 @@ module.exports = function(Chart) { p.push(plugin); } }); + + this._cacheId++; }, /** @@ -7316,6 +7291,8 @@ module.exports = function(Chart) { p.splice(idx, 1); } }); + + this._cacheId++; }, /** @@ -7324,6 +7301,7 @@ module.exports = function(Chart) { */ clear: function() { this._plugins = []; + this._cacheId++; }, /** @@ -7345,66 +7323,243 @@ module.exports = function(Chart) { }, /** - * Calls registered plugins on the specified extension, with the given args. This - * method immediately returns as soon as a plugin explicitly returns false. The + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The * returned value can be used, for instance, to interrupt the current action. - * @param {String} extension the name of the plugin method to call (e.g. 'beforeUpdate'). - * @param {Array} [args] extra arguments to apply to the extension call. + * @param {Object} chart - The chart instance for which plugins should be called. + * @param {String} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Array} [args] - Extra arguments to apply to the hook call. * @returns {Boolean} false if any of the plugins return false, else returns true. */ - notify: function(extension, args) { - var plugins = this._plugins; - var ilen = plugins.length; - var i, plugin; + notify: function(chart, hook, args) { + var descriptors = this.descriptors(chart); + var ilen = descriptors.length; + var i, descriptor, plugin, params, method; for (i=0; i tickWidth && me.labelRotation < optionTicks.maxRotation) { - cosRotation = Math.cos(helpers.toRadians(me.labelRotation)); - sinRotation = Math.sin(helpers.toRadians(me.labelRotation)); - - firstRotated = cosRotation * firstWidth; - - // We're right aligning the text now. - if (firstRotated + tickFontSize / 2 > me.yLabelWidth) { - me.paddingLeft = firstRotated + tickFontSize / 2; - } - - me.paddingRight = tickFontSize / 2; - - if (sinRotation * originalLabelWidth > me.maxHeight) { - // go back one step - me.labelRotation--; - break; - } + // Max label rotation can be set or default to 90 - also act as a loop counter + while (labelWidth > tickWidth && labelRotation < tickOpts.maxRotation) { + var angleRadians = helpers.toRadians(labelRotation); + cosRotation = Math.cos(angleRadians); + sinRotation = Math.sin(angleRadians); - me.labelRotation++; - labelWidth = cosRotation * originalLabelWidth; + if (sinRotation * originalLabelWidth > me.maxHeight) { + // go back one step + labelRotation--; + break; } + + labelRotation++; + labelWidth = cosRotation * originalLabelWidth; } } - if (me.margins) { - me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); - me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); - } + me.labelRotation = labelRotation; }, afterCalculateTickRotation: function() { helpers.callCallback(this.options.afterCalculateTickRotation, [this]); @@ -7677,20 +7839,14 @@ module.exports = function(Chart) { }; var opts = me.options; - var globalDefaults = Chart.defaults.global; var tickOpts = opts.ticks; var scaleLabelOpts = opts.scaleLabel; var gridLineOpts = opts.gridLines; var display = opts.display; var isHorizontal = me.isHorizontal(); - var tickFontSize = helpers.getValueOrDefault(tickOpts.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(tickOpts.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(tickOpts.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - - var scaleLabelFontSize = helpers.getValueOrDefault(scaleLabelOpts.fontSize, globalDefaults.defaultFontSize); - + var tickFont = parseFontOptions(tickOpts); + var scaleLabelFontSize = parseFontOptions(scaleLabelOpts).size * 1.5; var tickMarkLength = opts.gridLines.tickMarkLength; // Width @@ -7711,78 +7867,84 @@ module.exports = function(Chart) { // Are we showing a title for the scale? if (scaleLabelOpts.display && display) { if (isHorizontal) { - minSize.height += (scaleLabelFontSize * 1.5); + minSize.height += scaleLabelFontSize; } else { - minSize.width += (scaleLabelFontSize * 1.5); + minSize.width += scaleLabelFontSize; } } + // Don't bother fitting the ticks if we are not showing them if (tickOpts.display && display) { - // Don't bother fitting the ticks if we are not showing them - if (!me.longestTextCache) { - me.longestTextCache = {}; - } - - var largestTextWidth = helpers.longestText(me.ctx, tickLabelFont, me.ticks, me.longestTextCache); + var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, me.ticks, me.longestTextCache); var tallestLabelHeightInLines = helpers.numberOfLabelLines(me.ticks); - var lineSpace = tickFontSize * 0.5; + var lineSpace = tickFont.size * 0.5; if (isHorizontal) { // A horizontal axis is more constrained by the height. me.longestLabelWidth = largestTextWidth; + var angleRadians = helpers.toRadians(me.labelRotation); + var cosRotation = Math.cos(angleRadians); + var sinRotation = Math.sin(angleRadians); + // TODO - improve this calculation - var labelHeight = (Math.sin(helpers.toRadians(me.labelRotation)) * me.longestLabelWidth) + (tickFontSize * tallestLabelHeightInLines) + (lineSpace * tallestLabelHeightInLines); + var labelHeight = (sinRotation * largestTextWidth) + + (tickFont.size * tallestLabelHeightInLines) + + (lineSpace * tallestLabelHeightInLines); minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight); - me.ctx.font = tickLabelFont; + me.ctx.font = tickFont.font; - var firstLabelWidth = me.ctx.measureText(me.ticks[0]).width; - var lastLabelWidth = me.ctx.measureText(me.ticks[me.ticks.length - 1]).width; + var firstTick = me.ticks[0]; + var firstLabelWidth = computeTextSize(me.ctx, firstTick, tickFont.font); + + var lastTick = me.ticks[me.ticks.length - 1]; + var lastLabelWidth = computeTextSize(me.ctx, lastTick, tickFont.font); // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated // by the font height - var cosRotation = Math.cos(helpers.toRadians(me.labelRotation)); - var sinRotation = Math.sin(helpers.toRadians(me.labelRotation)); - me.paddingLeft = me.labelRotation !== 0 ? (cosRotation * firstLabelWidth) + 3 : firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges - me.paddingRight = me.labelRotation !== 0 ? (sinRotation * (tickFontSize / 2)) + 3 : lastLabelWidth / 2 + 3; // when rotated + if (me.labelRotation !== 0) { + me.paddingLeft = opts.position === 'bottom'? (cosRotation * firstLabelWidth) + 3: (cosRotation * lineSpace) + 3; // add 3 px to move away from canvas edges + me.paddingRight = opts.position === 'bottom'? (cosRotation * lineSpace) + 3: (cosRotation * lastLabelWidth) + 3; + } else { + me.paddingLeft = firstLabelWidth / 2 + 3; // add 3 px to move away from canvas edges + me.paddingRight = lastLabelWidth / 2 + 3; + } } else { // A vertical axis is more constrained by the width. Labels are the dominant factor here, so get that length first - var maxLabelWidth = me.maxWidth - minSize.width; - // Account for padding - var mirror = tickOpts.mirror; - if (!mirror) { - largestTextWidth += me.options.ticks.padding; - } else { - // If mirrored text is on the inside so don't expand - largestTextWidth = 0; - } - if (largestTextWidth < maxLabelWidth) { - // We don't need all the room - minSize.width += largestTextWidth; + if (tickOpts.mirror) { + largestTextWidth = 0; } else { - // Expand to max size - minSize.width = me.maxWidth; + largestTextWidth += me.options.ticks.padding; } - - me.paddingTop = tickFontSize / 2; - me.paddingBottom = tickFontSize / 2; + minSize.width += largestTextWidth; + me.paddingTop = tickFont.size / 2; + me.paddingBottom = tickFont.size / 2; } } + me.handleMargins(); + + me.width = minSize.width; + me.height = minSize.height; + }, + + /** + * Handle margins and padding interactions + * @private + */ + handleMargins: function() { + var me = this; if (me.margins) { me.paddingLeft = Math.max(me.paddingLeft - me.margins.left, 0); me.paddingTop = Math.max(me.paddingTop - me.margins.top, 0); me.paddingRight = Math.max(me.paddingRight - me.margins.right, 0); me.paddingBottom = Math.max(me.paddingBottom - me.margins.bottom, 0); } - - me.width = minSize.width; - me.height = minSize.height; - }, + afterFit: function() { helpers.callCallback(this.options.afterFit, [this]); }, @@ -7862,15 +8024,18 @@ module.exports = function(Chart) { }, getBasePixel: function() { + return this.getPixelForValue(this.getBaseValue()); + }, + + getBaseValue: function() { var me = this; var min = me.min; var max = me.max; - return me.getPixelForValue( - me.beginAtZero? 0: + return me.beginAtZero ? 0: min < 0 && max < 0? max : min > 0 && max > 0? min : - 0); + 0; }, // Actually draw the scale on the canvas @@ -7900,19 +8065,14 @@ module.exports = function(Chart) { } var tickFontColor = helpers.getValueOrDefault(optionTicks.fontColor, globalDefaults.defaultFontColor); - var tickFontSize = helpers.getValueOrDefault(optionTicks.fontSize, globalDefaults.defaultFontSize); - var tickFontStyle = helpers.getValueOrDefault(optionTicks.fontStyle, globalDefaults.defaultFontStyle); - var tickFontFamily = helpers.getValueOrDefault(optionTicks.fontFamily, globalDefaults.defaultFontFamily); - var tickLabelFont = helpers.fontString(tickFontSize, tickFontStyle, tickFontFamily); - var tl = gridLines.tickMarkLength; + var tickFont = parseFontOptions(optionTicks); + + var tl = gridLines.drawTicks ? gridLines.tickMarkLength : 0; var borderDash = helpers.getValueOrDefault(gridLines.borderDash, globalDefaults.borderDash); var borderDashOffset = helpers.getValueOrDefault(gridLines.borderDashOffset, globalDefaults.borderDashOffset); var scaleLabelFontColor = helpers.getValueOrDefault(scaleLabel.fontColor, globalDefaults.defaultFontColor); - var scaleLabelFontSize = helpers.getValueOrDefault(scaleLabel.fontSize, globalDefaults.defaultFontSize); - var scaleLabelFontStyle = helpers.getValueOrDefault(scaleLabel.fontStyle, globalDefaults.defaultFontStyle); - var scaleLabelFontFamily = helpers.getValueOrDefault(scaleLabel.fontFamily, globalDefaults.defaultFontFamily); - var scaleLabelFont = helpers.fontString(scaleLabelFontSize, scaleLabelFontStyle, scaleLabelFontFamily); + var scaleLabelFont = parseFontOptions(scaleLabel); var labelRotationRadians = helpers.toRadians(me.labelRotation); var cosRotation = Math.cos(labelRotationRadians); @@ -7988,15 +8148,21 @@ module.exports = function(Chart) { var textBaseline = 'middle'; if (isHorizontal) { - if (!isRotated) { - textBaseline = options.position === 'top' ? 'bottom' : 'top'; - } - textAlign = isRotated ? 'right' : 'center'; + if (options.position === 'bottom') { + // bottom + textBaseline = !isRotated? 'top':'middle'; + textAlign = !isRotated? 'center': 'right'; + labelY = me.top + tl; + } else { + // top + textBaseline = !isRotated? 'bottom':'middle'; + textAlign = !isRotated? 'center': 'left'; + labelY = me.bottom - tl; + } var xLineValue = me.getPixelForTick(index) + helpers.aliasPixel(lineWidth); // xvalues for grid lines labelX = me.getPixelForTick(index, gridLines.offsetGridLines) + optionTicks.labelOffset; // x values for optionTicks (need to consider offsetLabel option) - labelY = (isRotated) ? me.top + 12 : options.position === 'top' ? me.bottom - tl : me.top + tl; tx1 = tx2 = x1 = x2 = xLineValue; ty1 = yTickStart; @@ -8004,23 +8170,20 @@ module.exports = function(Chart) { y1 = chartArea.top; y2 = chartArea.bottom; } else { - if (options.position === 'left') { - if (optionTicks.mirror) { - labelX = me.right + optionTicks.padding; - textAlign = 'left'; - } else { - labelX = me.right - optionTicks.padding; - textAlign = 'right'; - } - // right side - } else if (optionTicks.mirror) { - labelX = me.left - optionTicks.padding; - textAlign = 'right'; + var isLeft = options.position === 'left'; + var tickPadding = optionTicks.padding; + var labelXOffset; + + if (optionTicks.mirror) { + textAlign = isLeft ? 'left' : 'right'; + labelXOffset = tickPadding; } else { - labelX = me.left + optionTicks.padding; - textAlign = 'left'; + textAlign = isLeft ? 'right' : 'left'; + labelXOffset = tl + tickPadding; } + labelX = isLeft ? me.right - labelXOffset : me.left + labelXOffset; + var yLineValue = me.getPixelForTick(index); // xvalues for grid lines yLineValue += helpers.aliasPixel(lineWidth); labelY = me.getPixelForTick(index, gridLines.offsetGridLines); @@ -8085,17 +8248,17 @@ module.exports = function(Chart) { context.save(); context.translate(itemToDraw.labelX, itemToDraw.labelY); context.rotate(itemToDraw.rotation); - context.font = tickLabelFont; + context.font = tickFont.font; context.textBaseline = itemToDraw.textBaseline; context.textAlign = itemToDraw.textAlign; var label = itemToDraw.label; if (helpers.isArray(label)) { - for (var i = 0, y = -(label.length - 1)*tickFontSize*0.75; i < label.length; ++i) { + for (var i = 0, y = 0; i < label.length; ++i) { // We just make sure the multiline element is a string here.. context.fillText('' + label[i], 0, y); // apply same lineSpacing as calculated @ L#320 - y += (tickFontSize * 1.5); + y += (tickFont.size * 1.5); } } else { context.fillText(label, 0, 0); @@ -8112,10 +8275,10 @@ module.exports = function(Chart) { if (isHorizontal) { scaleLabelX = me.left + ((me.right - me.left) / 2); // midpoint of the width - scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFontSize / 2) : me.top + (scaleLabelFontSize / 2); + scaleLabelY = options.position === 'bottom' ? me.bottom - (scaleLabelFont.size / 2) : me.top + (scaleLabelFont.size / 2); } else { var isLeft = options.position === 'left'; - scaleLabelX = isLeft ? me.left + (scaleLabelFontSize / 2) : me.right - (scaleLabelFontSize / 2); + scaleLabelX = isLeft ? me.left + (scaleLabelFont.size / 2) : me.right - (scaleLabelFont.size / 2); scaleLabelY = me.top + ((me.bottom - me.top) / 2); rotation = isLeft ? -0.5 * Math.PI : 0.5 * Math.PI; } @@ -8126,7 +8289,7 @@ module.exports = function(Chart) { context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillStyle = scaleLabelFontColor; // render in correct colour - context.font = scaleLabelFont; + context.font = scaleLabelFont.font; context.fillText(scaleLabel.labelString, 0, 0); context.restore(); } @@ -8272,8 +8435,8 @@ module.exports = function(Chart) { // If min, max and stepSize is set and they make an evenly spaced scale use it. if (generationOptions.min && generationOptions.max && generationOptions.stepSize) { - var minMaxDeltaDivisibleByStepSize = ((generationOptions.max - generationOptions.min) % generationOptions.stepSize) === 0; - if (minMaxDeltaDivisibleByStepSize) { + // If very close to our whole number, use it. + if (helpers.almostWhole((generationOptions.max - generationOptions.min) / generationOptions.stepSize, spacing / 1000)) { niceMin = generationOptions.min; niceMax = generationOptions.max; } @@ -8314,27 +8477,33 @@ module.exports = function(Chart) { // the graph var tickVal = getValueOrDefault(generationOptions.min, Math.pow(10, Math.floor(helpers.log10(dataRange.min)))); - while (tickVal < dataRange.max) { - ticks.push(tickVal); + var endExp = Math.floor(helpers.log10(dataRange.max)); + var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp)); + var exp; + var significand; - var exp; - var significand; + if (tickVal === 0) { + exp = Math.floor(helpers.log10(dataRange.minNotZero)); + significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp)); - if (tickVal === 0) { - exp = Math.floor(helpers.log10(dataRange.minNotZero)); - significand = Math.round(dataRange.minNotZero / Math.pow(10, exp)); - } else { - exp = Math.floor(helpers.log10(tickVal)); - significand = Math.floor(tickVal / Math.pow(10, exp)) + 1; - } + ticks.push(tickVal); + tickVal = significand * Math.pow(10, exp); + } else { + exp = Math.floor(helpers.log10(tickVal)); + significand = Math.floor(tickVal / Math.pow(10, exp)); + } + + do { + ticks.push(tickVal); + ++significand; if (significand === 10) { significand = 1; ++exp; } tickVal = significand * Math.pow(10, exp); - } + } while (exp < endExp || (exp === endExp && significand < endSignificand)); var lastTick = getValueOrDefault(generationOptions.max, tickVal); ticks.push(lastTick); @@ -8431,7 +8600,6 @@ module.exports = function(Chart) { initialize: function(config) { var me = this; helpers.extend(me, config); - me.options = helpers.configMerge(Chart.defaults.global.title, config.options); // Contains hit boxes for each dataset (in dataset order) me.legendHitBoxes = []; @@ -8439,12 +8607,7 @@ module.exports = function(Chart) { // These methods are ordered by lifecycle. Utilities then follow. - beforeUpdate: function() { - var chartOpts = this.chart.options; - if (chartOpts && chartOpts.title) { - this.options = helpers.configMerge(Chart.defaults.global.title, chartOpts.title); - } - }, + beforeUpdate: noop, update: function(maxWidth, maxHeight, margins) { var me = this; @@ -8596,20 +8759,39 @@ module.exports = function(Chart) { } }); + function createNewTitleBlockAndAttach(chartInstance, titleOpts) { + var title = new Chart.Title({ + ctx: chartInstance.chart.ctx, + options: titleOpts, + chart: chartInstance + }); + chartInstance.titleBlock = title; + Chart.layoutService.addBox(chartInstance, title); + } + // Register the title plugin Chart.plugins.register({ beforeInit: function(chartInstance) { - var opts = chartInstance.options; - var titleOpts = opts.title; + var titleOpts = chartInstance.options.title; if (titleOpts) { - chartInstance.titleBlock = new Chart.Title({ - ctx: chartInstance.chart.ctx, - options: titleOpts, - chart: chartInstance - }); + createNewTitleBlockAndAttach(chartInstance, titleOpts); + } + }, + beforeUpdate: function(chartInstance) { + var titleOpts = chartInstance.options.title; - Chart.layoutService.addBox(chartInstance, chartInstance.titleBlock); + if (titleOpts) { + titleOpts = helpers.configMerge(Chart.defaults.global.title, titleOpts); + + if (chartInstance.titleBlock) { + chartInstance.titleBlock.options = titleOpts; + } else { + createNewTitleBlockAndAttach(chartInstance, titleOpts); + } + } else { + Chart.layoutService.removeBox(chartInstance, chartInstance.titleBlock); + delete chartInstance.titleBlock; } } }); @@ -9381,7 +9563,7 @@ module.exports = function(Chart) { /** * Handle an event * @private - * @param e {Event} the event to handle + * @param {IEvent} event - The event to handle * @returns {Boolean} true if the tooltip changed */ handleEvent: function(e) { @@ -9403,7 +9585,10 @@ module.exports = function(Chart) { me._lastActive = me._active; if (options.enabled || options.custom) { - me._eventPosition = helpers.getRelativePosition(e, me._chart); + me._eventPosition = { + x: e.x, + y: e.y + }; var model = me._model; me.update(true); @@ -9845,13 +10030,17 @@ module.exports = function(Chart) { padding: vm.radius + vm.borderWidth }; }, - draw: function() { + draw: function(chartArea) { var vm = this._view; + var model = this._model; var ctx = this._chart.ctx; var pointStyle = vm.pointStyle; var radius = vm.radius; var x = vm.x; var y = vm.y; + var color = Chart.helpers.color; + var errMargin = 1.01; // 1.01 is margin for Accumulated error. (Especially Edge, IE.) + var ratio = 0; if (vm.skip) { return; @@ -9861,6 +10050,24 @@ module.exports = function(Chart) { ctx.lineWidth = helpers.getValueOrDefault(vm.borderWidth, globalOpts.elements.point.borderWidth); ctx.fillStyle = vm.backgroundColor || defaultColor; + // Cliping for Points. + // going out from inner charArea? + if ((chartArea !== undefined) && ((model.x < chartArea.left) || (chartArea.right*errMargin < model.x) || (model.y < chartArea.top) || (chartArea.bottom*errMargin < model.y))) { + // Point fade out + if (model.x < chartArea.left) { + ratio = (x - model.x) / (chartArea.left - model.x); + } else if (chartArea.right*errMargin < model.x) { + ratio = (model.x - x) / (model.x - chartArea.right); + } else if (model.y < chartArea.top) { + ratio = (y - model.y) / (chartArea.top - model.y); + } else if (chartArea.bottom*errMargin < model.y) { + ratio = (model.y - y) / (model.y - chartArea.bottom); + } + ratio = Math.round(ratio*100) / 100; + ctx.strokeStyle = color(ctx.strokeStyle).alpha(ratio).rgbString(); + ctx.fillStyle = color(ctx.fillStyle).alpha(ratio).rgbString(); + } + Chart.canvasHelpers.drawPoint(ctx, pointStyle, radius, x, y); } }); @@ -9922,39 +10129,71 @@ module.exports = function(Chart) { draw: function() { var ctx = this._chart.ctx; var vm = this._view; - - var halfWidth = vm.width / 2, - leftX = vm.x - halfWidth, - rightX = vm.x + halfWidth, - top = vm.base - (vm.base - vm.y), - halfStroke = vm.borderWidth / 2; + var left, right, top, bottom, signX, signY, borderSkipped; + var borderWidth = vm.borderWidth; + + if (!vm.horizontal) { + // bar + left = vm.x - vm.width / 2; + right = vm.x + vm.width / 2; + top = vm.y; + bottom = vm.base; + signX = 1; + signY = bottom > top? 1: -1; + borderSkipped = vm.borderSkipped || 'bottom'; + } else { + // horizontal bar + left = vm.base; + right = vm.x; + top = vm.y - vm.height / 2; + bottom = vm.y + vm.height / 2; + signX = right > left? 1: -1; + signY = 1; + borderSkipped = vm.borderSkipped || 'left'; + } // Canvas doesn't allow us to stroke inside the width so we can // adjust the sizes to fit if we're setting a stroke on the line - if (vm.borderWidth) { - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; + if (borderWidth) { + // borderWidth shold be less than bar width and bar height. + var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom)); + borderWidth = borderWidth > barSize? barSize: borderWidth; + var halfStroke = borderWidth / 2; + // Adjust borderWidth when bar top position is near vm.base(zero). + var borderLeft = left + (borderSkipped !== 'left'? halfStroke * signX: 0); + var borderRight = right + (borderSkipped !== 'right'? -halfStroke * signX: 0); + var borderTop = top + (borderSkipped !== 'top'? halfStroke * signY: 0); + var borderBottom = bottom + (borderSkipped !== 'bottom'? -halfStroke * signY: 0); + // not become a vertical line? + if (borderLeft !== borderRight) { + top = borderTop; + bottom = borderBottom; + } + // not become a horizontal line? + if (borderTop !== borderBottom) { + left = borderLeft; + right = borderRight; + } } ctx.beginPath(); ctx.fillStyle = vm.backgroundColor; ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; + ctx.lineWidth = borderWidth; // Corner points, from bottom-left to bottom-right clockwise // | 1 2 | // | 0 3 | var corners = [ - [leftX, vm.base], - [leftX, top], - [rightX, top], - [rightX, vm.base] + [left, bottom], + [left, top], + [right, top], + [right, bottom] ]; // Find first (starting) corner with fallback to 'bottom' var borders = ['bottom', 'left', 'top', 'right']; - var startCorner = borders.indexOf(vm.borderSkipped, 0); + var startCorner = borders.indexOf(borderSkipped, 0); if (startCorner === -1) { startCorner = 0; } @@ -9973,7 +10212,7 @@ module.exports = function(Chart) { } ctx.fill(); - if (vm.borderWidth) { + if (borderWidth) { ctx.stroke(); } }, @@ -10047,6 +10286,356 @@ module.exports = function(Chart) { },{}],41:[function(require,module,exports){ 'use strict'; +// Chart.Platform implementation for targeting a web browser +module.exports = function(Chart) { + var helpers = Chart.helpers; + + // DOM event types -> Chart.js event types. + // Note: only events with different types are mapped. + // https://developer.mozilla.org/en-US/docs/Web/Events + var eventTypeMap = { + // Touch events + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + + // Pointer events + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' + }; + + /** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns {Number} Size in pixels or undefined if unknown. + */ + function readUsedSize(element, property) { + var value = helpers.getStyle(element, property); + var matches = value && value.match(/(\d+)px/); + return matches? Number(matches[1]) : undefined; + } + + /** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + */ + function initCanvas(canvas, config) { + var style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + var renderHeight = canvas.getAttribute('height'); + var renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas._chartjs = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + + if (renderWidth === null || renderWidth === '') { + var displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (renderHeight === null || renderHeight === '') { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (config.options.aspectRatio || 2); + } else { + var displayHeight = readUsedSize(canvas, 'height'); + if (displayWidth !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; + } + + function createEvent(type, chart, x, y, native) { + return { + type: type, + chart: chart, + native: native || null, + x: x !== undefined? x : null, + y: y !== undefined? y : null, + }; + } + + function fromNativeEvent(event, chart) { + var type = eventTypeMap[event.type] || event.type; + var pos = helpers.getRelativePosition(event, chart); + return createEvent(type, chart, pos.x, pos.y, event); + } + + function createResizer(handler) { + var iframe = document.createElement('iframe'); + iframe.className = 'chartjs-hidden-iframe'; + iframe.style.cssText = + 'display:block;'+ + 'overflow:hidden;'+ + 'border:0;'+ + 'margin:0;'+ + 'top:0;'+ + 'left:0;'+ + 'bottom:0;'+ + 'right:0;'+ + 'height:100%;'+ + 'width:100%;'+ + 'position:absolute;'+ + 'pointer-events:none;'+ + 'z-index:-1;'; + + // Prevent the iframe to gain focus on tab. + // https://github.com/chartjs/Chart.js/issues/3090 + iframe.tabIndex = -1; + + // If the iframe is re-attached to the DOM, the resize listener is removed because the + // content is reloaded, so make sure to install the handler after the iframe is loaded. + // https://github.com/chartjs/Chart.js/issues/3521 + helpers.addEvent(iframe, 'load', function() { + helpers.addEvent(iframe.contentWindow || iframe, 'resize', handler); + + // The iframe size might have changed while loading, which can also + // happen if the size has been changed while detached from the DOM. + handler(); + }); + + return iframe; + } + + function addResizeListener(node, listener, chart) { + var stub = node._chartjs = { + ticking: false + }; + + // Throttle the callback notification until the next animation frame. + var notify = function() { + if (!stub.ticking) { + stub.ticking = true; + helpers.requestAnimFrame.call(window, function() { + if (stub.resizer) { + stub.ticking = false; + return listener(createEvent('resize', chart)); + } + }); + } + }; + + // Let's keep track of this added iframe and thus avoid DOM query when removing it. + stub.resizer = createResizer(notify); + + node.insertBefore(stub.resizer, node.firstChild); + } + + function removeResizeListener(node) { + if (!node || !node._chartjs) { + return; + } + + var resizer = node._chartjs.resizer; + if (resizer) { + resizer.parentNode.removeChild(resizer); + node._chartjs.resizer = null; + } + + delete node._chartjs; + } + + return { + acquireContext: function(item, config) { + if (typeof item === 'string') { + item = document.getElementById(item); + } else if (item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + + if (item instanceof HTMLCanvasElement) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + var context = item.getContext && item.getContext('2d'); + if (context instanceof CanvasRenderingContext2D) { + initCanvas(item, config); + return context; + } + } + + return null; + }, + + releaseContext: function(context) { + var canvas = context.canvas; + if (!canvas._chartjs) { + return; + } + + var initial = canvas._chartjs.initial; + ['height', 'width'].forEach(function(prop) { + var value = initial[prop]; + if (value === undefined || value === null) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + + helpers.each(initial.style || {}, function(value, key) { + canvas.style[key] = value; + }); + + // The canvas render size might have been changed (and thus the state stack discarded), + // we can't use save() and restore() to restore the initial state. So make sure that at + // least the canvas context is reset to the default state by setting the canvas width. + // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + canvas.width = canvas.width; + + delete canvas._chartjs; + }, + + addEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + addResizeListener(canvas.parentNode, listener, chart.chart); + return; + } + + var stub = listener._chartjs || (listener._chartjs = {}); + var proxies = stub.proxies || (stub.proxies = {}); + var proxy = proxies[chart.id + '_' + type] = function(event) { + listener(fromNativeEvent(event, chart.chart)); + }; + + helpers.addEvent(canvas, type, proxy); + }, + + removeEventListener: function(chart, type, listener) { + var canvas = chart.chart.canvas; + if (type === 'resize') { + // Note: the resize event is not supported on all browsers. + removeResizeListener(canvas.parentNode, listener); + return; + } + + var stub = listener._chartjs || {}; + var proxies = stub.proxies || {}; + var proxy = proxies[chart.id + '_' + type]; + if (!proxy) { + return; + } + + helpers.removeEvent(canvas, type, proxy); + } + }; +}; + +},{}],42:[function(require,module,exports){ +'use strict'; + +// By default, select the browser (DOM) platform. +// @TODO Make possible to select another platform at build time. +var implementation = require(41); + +module.exports = function(Chart) { + /** + * @namespace Chart.platform + * @see https://chartjs.gitbooks.io/proposals/content/Platform.html + * @since 2.4.0 + */ + Chart.platform = { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {*} item - The native item from which to acquire context (platform specific) + * @param {Object} options - The chart options + * @returns {CanvasRenderingContext2D} context2d instance + */ + acquireContext: function() {}, + + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {Boolean} true if the method succeeded, else false + */ + releaseContext: function() {}, + + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {String} type - The ({@link IEvent}) type to listen for + * @param {Function} listener - Receives a notification (an object that implements + * the {@link IEvent} interface) when an event of the specified type occurs. + */ + addEventListener: function() {}, + + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart -Chart from which to remove the listener + * @param {String} type - The ({@link IEvent}) type to remove + * @param {Function} listener - The listener function to remove from the event target. + */ + removeEventListener: function() {} + }; + + /** + * @interface IPlatform + * Allows abstracting platform dependencies away from the chart + * @borrows Chart.platform.acquireContext as acquireContext + * @borrows Chart.platform.releaseContext as releaseContext + * @borrows Chart.platform.addEventListener as addEventListener + * @borrows Chart.platform.removeEventListener as removeEventListener + */ + + /** + * @interface IEvent + * @prop {String} type - The event type name, possible values are: + * 'contextmenu', 'mouseenter', 'mousedown', 'mousemove', 'mouseup', 'mouseout', + * 'click', 'dblclick', 'keydown', 'keypress', 'keyup' and 'resize' + * @prop {*} native - The original native event (null for emulated events, e.g. 'resize') + * @prop {Number} x - The mouse x position, relative to the canvas (null for incompatible events) + * @prop {Number} y - The mouse y position, relative to the canvas (null for incompatible events) + */ + + Chart.helpers.extend(Chart.platform, implementation(Chart)); +}; + +},{"41":41}],43:[function(require,module,exports){ +'use strict'; + module.exports = function(Chart) { var helpers = Chart.helpers; @@ -10101,10 +10690,10 @@ module.exports = function(Chart) { var data = me.chart.data; var isHorizontal = me.isHorizontal(); - if ((data.xLabels && isHorizontal) || (data.yLabels && !isHorizontal)) { + if (data.yLabels && !isHorizontal) { return me.getRightValue(data.datasets[datasetIndex].data[index]); } - return me.ticks[index]; + return me.ticks[index - me.minIndex]; }, // Used to get data value locations. Value can either be an index or a numerical value @@ -10120,9 +10709,8 @@ module.exports = function(Chart) { } if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueWidth = innerWidth / offsetAmt; - var widthOffset = (valueWidth * (index - me.minIndex)) + me.paddingLeft; + var valueWidth = me.width / offsetAmt; + var widthOffset = (valueWidth * (index - me.minIndex)); if (me.options.gridLines.offsetGridLines && includeOffset || me.maxIndex === me.minIndex && includeOffset) { widthOffset += (valueWidth / 2); @@ -10130,9 +10718,8 @@ module.exports = function(Chart) { return me.left + Math.round(widthOffset); } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - var valueHeight = innerHeight / offsetAmt; - var heightOffset = (valueHeight * (index - me.minIndex)) + me.paddingTop; + var valueHeight = me.height / offsetAmt; + var heightOffset = (valueHeight * (index - me.minIndex)); if (me.options.gridLines.offsetGridLines && includeOffset) { heightOffset += (valueHeight / 2); @@ -10148,15 +10735,13 @@ module.exports = function(Chart) { var value; var offsetAmt = Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1); var horz = me.isHorizontal(); - var innerDimension = horz ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); - var valueDimension = innerDimension / offsetAmt; + var valueDimension = (horz ? me.width : me.height) / offsetAmt; pixel -= horz ? me.left : me.top; if (me.options.gridLines.offsetGridLines) { pixel -= (valueDimension / 2); } - pixel -= horz ? me.paddingLeft : me.paddingTop; if (pixel <= 0) { value = 0; @@ -10175,7 +10760,7 @@ module.exports = function(Chart) { }; -},{}],42:[function(require,module,exports){ +},{}],44:[function(require,module,exports){ 'use strict'; module.exports = function(Chart) { @@ -10206,21 +10791,43 @@ module.exports = function(Chart) { me.min = null; me.max = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = { + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = { positiveValues: [], negativeValues: [] }; } // Store these per type - var positiveValues = valuesPerType[meta.type].positiveValues; - var negativeValues = valuesPerType[meta.type].negativeValues; + var positiveValues = valuesPerStack[key].positiveValues; + var negativeValues = valuesPerStack[key].negativeValues; if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue, index) { @@ -10243,7 +10850,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var values = valuesForType.positiveValues.concat(valuesForType.negativeValues); var minVal = helpers.min(values); var maxVal = helpers.max(values); @@ -10310,31 +10917,25 @@ module.exports = function(Chart) { // This must be called after fit has been run so that // this.left, this.top, this.right, and this.bottom have been defined var me = this; - var paddingLeft = me.paddingLeft; - var paddingBottom = me.paddingBottom; var start = me.start; var rightValue = +me.getRightValue(value); var pixel; - var innerDimension; var range = me.end - start; if (me.isHorizontal()) { - innerDimension = me.width - (paddingLeft + me.paddingRight); - pixel = me.left + (innerDimension / range * (rightValue - start)); - return Math.round(pixel + paddingLeft); + pixel = me.left + (me.width / range * (rightValue - start)); + return Math.round(pixel); } - innerDimension = me.height - (me.paddingTop + paddingBottom); - pixel = (me.bottom - paddingBottom) - (innerDimension / range * (rightValue - start)); + + pixel = me.bottom - (me.height / range * (rightValue - start)); return Math.round(pixel); }, getValueForPixel: function(pixel) { var me = this; var isHorizontal = me.isHorizontal(); - var paddingLeft = me.paddingLeft; - var paddingBottom = me.paddingBottom; - var innerDimension = isHorizontal ? me.width - (paddingLeft + me.paddingRight) : me.height - (me.paddingTop + paddingBottom); - var offset = (isHorizontal ? pixel - me.left - paddingLeft : me.bottom - paddingBottom - pixel) / innerDimension; + var innerDimension = isHorizontal ? me.width : me.height; + var offset = (isHorizontal ? pixel - me.left : me.bottom - pixel) / innerDimension; return me.start + ((me.end - me.start) * offset); }, getPixelForTick: function(index) { @@ -10345,7 +10946,7 @@ module.exports = function(Chart) { }; -},{}],43:[function(require,module,exports){ +},{}],45:[function(require,module,exports){ 'use strict'; module.exports = function(Chart) { @@ -10445,7 +11046,7 @@ module.exports = function(Chart) { }); }; -},{}],44:[function(require,module,exports){ +},{}],46:[function(require,module,exports){ 'use strict'; module.exports = function(Chart) { @@ -10480,18 +11081,40 @@ module.exports = function(Chart) { me.max = null; me.minNotZero = null; - if (opts.stacked) { - var valuesPerType = {}; + var hasStacks = opts.stacked; + if (hasStacks === undefined) { + helpers.each(datasets, function(dataset, datasetIndex) { + if (hasStacks) { + return; + } + + var meta = chart.getDatasetMeta(datasetIndex); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) && + meta.stack !== undefined) { + hasStacks = true; + } + }); + } + + if (opts.stacked || hasStacks) { + var valuesPerStack = {}; helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); + var key = [ + meta.type, + // we have a separate stack for stack=undefined datasets when the opts.stacked is undefined + ((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''), + meta.stack + ].join('.'); + if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { - if (valuesPerType[meta.type] === undefined) { - valuesPerType[meta.type] = []; + if (valuesPerStack[key] === undefined) { + valuesPerStack[key] = []; } helpers.each(dataset.data, function(rawValue, index) { - var values = valuesPerType[meta.type]; + var values = valuesPerStack[key]; var value = +me.getRightValue(rawValue); if (isNaN(value) || meta.data[index].hidden) { return; @@ -10509,7 +11132,7 @@ module.exports = function(Chart) { } }); - helpers.each(valuesPerType, function(valuesForType) { + helpers.each(valuesPerStack, function(valuesForType) { var minVal = helpers.min(valuesForType); var maxVal = helpers.max(valuesForType); me.min = me.min === null ? minVal : Math.min(me.min, minVal); @@ -10610,46 +11233,42 @@ module.exports = function(Chart) { var start = me.start; var newVal = +me.getRightValue(value); var range; - var paddingTop = me.paddingTop; - var paddingBottom = me.paddingBottom; - var paddingLeft = me.paddingLeft; var opts = me.options; var tickOpts = opts.ticks; if (me.isHorizontal()) { range = helpers.log10(me.end) - helpers.log10(start); // todo: if start === 0 if (newVal === 0) { - pixel = me.left + paddingLeft; + pixel = me.left; } else { - innerDimension = me.width - (paddingLeft + me.paddingRight); + innerDimension = me.width; pixel = me.left + (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); - pixel += paddingLeft; } } else { // Bottom - top since pixels increase downward on a screen - innerDimension = me.height - (paddingTop + paddingBottom); + innerDimension = me.height; if (start === 0 && !tickOpts.reverse) { range = helpers.log10(me.end) - helpers.log10(me.minNotZero); if (newVal === start) { - pixel = me.bottom - paddingBottom; + pixel = me.bottom; } else if (newVal === me.minNotZero) { - pixel = me.bottom - paddingBottom - innerDimension * 0.02; + pixel = me.bottom - innerDimension * 0.02; } else { - pixel = me.bottom - paddingBottom - innerDimension * 0.02 - (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + pixel = me.bottom - innerDimension * 0.02 - (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); } } else if (me.end === 0 && tickOpts.reverse) { range = helpers.log10(me.start) - helpers.log10(me.minNotZero); if (newVal === me.end) { - pixel = me.top + paddingTop; + pixel = me.top; } else if (newVal === me.minNotZero) { - pixel = me.top + paddingTop + innerDimension * 0.02; + pixel = me.top + innerDimension * 0.02; } else { - pixel = me.top + paddingTop + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); + pixel = me.top + innerDimension * 0.02 + (innerDimension * 0.98/ range * (helpers.log10(newVal)-helpers.log10(me.minNotZero))); } } else { range = helpers.log10(me.end) - helpers.log10(start); - innerDimension = me.height - (paddingTop + paddingBottom); - pixel = (me.bottom - paddingBottom) - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); + innerDimension = me.height; + pixel = me.bottom - (innerDimension / range * (helpers.log10(newVal) - helpers.log10(start))); } } return pixel; @@ -10660,11 +11279,11 @@ module.exports = function(Chart) { var value, innerDimension; if (me.isHorizontal()) { - innerDimension = me.width - (me.paddingLeft + me.paddingRight); - value = me.start * Math.pow(10, (pixel - me.left - me.paddingLeft) * range / innerDimension); + innerDimension = me.width; + value = me.start * Math.pow(10, (pixel - me.left) * range / innerDimension); } else { // todo: if start === 0 - innerDimension = me.height - (me.paddingTop + me.paddingBottom); - value = Math.pow(10, (me.bottom - me.paddingBottom - pixel) * range / innerDimension) / me.start; + innerDimension = me.height; + value = Math.pow(10, (me.bottom - pixel) * range / innerDimension) / me.start; } return value; } @@ -10673,7 +11292,7 @@ module.exports = function(Chart) { }; -},{}],45:[function(require,module,exports){ +},{}],47:[function(require,module,exports){ 'use strict'; module.exports = function(Chart) { @@ -10723,10 +11342,266 @@ module.exports = function(Chart) { } }; + function getValueCount(scale) { + return !scale.options.lineArc ? scale.chart.data.labels.length : 0; + } + + function getPointLabelFontOptions(scale) { + var pointLabelOptions = scale.options.pointLabels; + var fontSize = helpers.getValueOrDefault(pointLabelOptions.fontSize, globalDefaults.defaultFontSize); + var fontStyle = helpers.getValueOrDefault(pointLabelOptions.fontStyle, globalDefaults.defaultFontStyle); + var fontFamily = helpers.getValueOrDefault(pointLabelOptions.fontFamily, globalDefaults.defaultFontFamily); + var font = helpers.fontString(fontSize, fontStyle, fontFamily); + + return { + size: fontSize, + style: fontStyle, + family: fontFamily, + font: font + }; + } + + function measureLabelSize(ctx, fontSize, label) { + if (helpers.isArray(label)) { + return { + w: helpers.longestText(ctx, ctx.font, label), + h: (label.length * fontSize) + ((label.length - 1) * 1.5 * fontSize) + }; + } + + return { + w: ctx.measureText(label).width, + h: fontSize + }; + } + + function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size - 5, + end: pos + }; + } + + return { + start: pos, + end: pos + size + 5 + }; + } + + /** + * Helper function to fit a radial linear scale with point labels + */ + function fitWithPointLabels(scale) { + /* + * Right, this is really confusing and there is a lot of maths going on here + * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + * + * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + * + * Solution: + * + * We assume the radius of the polygon is half the size of the canvas at first + * at each index we check if the text overlaps. + * + * Where it does, we store that angle and that index. + * + * After finding the largest index and angle we calculate how much we need to remove + * from the shape radius to move the point inwards by that x. + * + * We average the left and right distances to get the maximum shape radius that can fit in the box + * along with labels. + * + * Once we have that, we can find the centre point for the chart, by taking the x text protrusion + * on each side, removing that from the size, halving it and adding the left x protrusion width. + * + * This will mean we have a shape fitted to the canvas, as large as it can be with the labels + * and position it in the most space efficient manner + * + * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + */ + + var plFont = getPointLabelFontOptions(scale); + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); + var furthestLimits = { + l: scale.width, + r: 0, + t: scale.height, + b: 0 + }; + var furthestAngles = {}; + var i; + var textSize; + var pointPosition; + + scale.ctx.font = plFont.font; + scale._pointLabelSizes = []; + + var valueCount = getValueCount(scale); + for (i = 0; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, largestPossibleRadius); + textSize = measureLabelSize(scale.ctx, plFont.size, scale.pointLabels[i] || ''); + scale._pointLabelSizes[i] = textSize; + + // Add quarter circle to make degree 0 mean top of circle + var angleRadians = scale.getIndexAngle(i); + var angle = helpers.toDegrees(angleRadians) % 360; + var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + + if (hLimits.start < furthestLimits.l) { + furthestLimits.l = hLimits.start; + furthestAngles.l = angleRadians; + } + + if (hLimits.end > furthestLimits.r) { + furthestLimits.r = hLimits.end; + furthestAngles.r = angleRadians; + } + + if (vLimits.start < furthestLimits.t) { + furthestLimits.t = vLimits.start; + furthestAngles.t = angleRadians; + } + + if (vLimits.end > furthestLimits.b) { + furthestLimits.b = vLimits.end; + furthestAngles.b = angleRadians; + } + } + + scale.setReductions(largestPossibleRadius, furthestLimits, furthestAngles); + } + + /** + * Helper function to fit a radial linear scale with no point labels + */ + function fit(scale) { + var largestPossibleRadius = Math.min(scale.height / 2, scale.width / 2); + scale.drawingArea = Math.round(largestPossibleRadius); + scale.setCenterPoint(0, 0, 0, 0); + } + + function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + + return 'right'; + } + + function fillText(ctx, text, position, fontSize) { + if (helpers.isArray(text)) { + var y = position.y; + var spacing = 1.5 * fontSize; + + for (var i = 0; i < text.length; ++i) { + ctx.fillText(text[i], position.x, y); + y+= spacing; + } + } else { + ctx.fillText(text, position.x, position.y); + } + } + + function adjustPointPositionForLabelHeight(angle, textSize, position) { + if (angle === 90 || angle === 270) { + position.y -= (textSize.h / 2); + } else if (angle > 270 || angle < 90) { + position.y -= textSize.h; + } + } + + function drawPointLabels(scale) { + var ctx = scale.ctx; + var getValueOrDefault = helpers.getValueOrDefault; + var opts = scale.options; + var angleLineOpts = opts.angleLines; + var pointLabelOpts = opts.pointLabels; + + ctx.lineWidth = angleLineOpts.lineWidth; + ctx.strokeStyle = angleLineOpts.color; + + var outerDistance = scale.getDistanceFromCenterForValue(opts.reverse ? scale.min : scale.max); + + // Point Label Font + var plFont = getPointLabelFontOptions(scale); + + ctx.textBaseline = 'top'; + + for (var i = getValueCount(scale) - 1; i >= 0; i--) { + if (angleLineOpts.display) { + var outerPosition = scale.getPointPosition(i, outerDistance); + ctx.beginPath(); + ctx.moveTo(scale.xCenter, scale.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = scale.getPointPosition(i, outerDistance + 5); + + // Keep this in loop since we may support array properties here + var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); + ctx.font = plFont.font; + ctx.fillStyle = pointLabelFontColor; + + var angleRadians = scale.getIndexAngle(i); + var angle = helpers.toDegrees(angleRadians); + ctx.textAlign = getTextAlignForAngle(angle); + adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition); + fillText(ctx, scale.pointLabels[i] || '', pointLabelPosition, plFont.size); + } + } + + function drawRadiusLine(scale, gridLineOpts, radius, index) { + var ctx = scale.ctx; + ctx.strokeStyle = helpers.getValueAtIndexOrDefault(gridLineOpts.color, index - 1); + ctx.lineWidth = helpers.getValueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); + + if (scale.options.lineArc) { + // Draw circular arcs between the points + ctx.beginPath(); + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + } else { + // Draw straight lines connecting each index + var valueCount = getValueCount(scale); + + if (valueCount === 0) { + return; + } + + ctx.beginPath(); + var pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + + for (var i = 1; i < valueCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + + ctx.closePath(); + ctx.stroke(); + } + } + + function numberOrZero(param) { + return helpers.isNumber(param) ? param : 0; + } + var LinearRadialScale = Chart.LinearScaleBase.extend({ - getValueCount: function() { - return this.chart.data.labels.length; - }, setDimensions: function() { var me = this; var opts = me.options; @@ -10744,9 +11619,8 @@ module.exports = function(Chart) { determineDataLimits: function() { var me = this; var chart = me.chart; - me.min = null; - me.max = null; - + var min = Number.POSITIVE_INFINITY; + var max = Number.NEGATIVE_INFINITY; helpers.each(chart.data.datasets, function(dataset, datasetIndex) { if (chart.isDatasetVisible(datasetIndex)) { @@ -10758,21 +11632,15 @@ module.exports = function(Chart) { return; } - if (me.min === null) { - me.min = value; - } else if (value < me.min) { - me.min = value; - } - - if (me.max === null) { - me.max = value; - } else if (value > me.max) { - me.max = value; - } + min = Math.min(value, min); + max = Math.max(value, max); }); } }); + me.min = (min === Number.POSITIVE_INFINITY ? 0 : min); + me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max); + // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero me.handleTickRangeOptions(); }, @@ -10792,122 +11660,46 @@ module.exports = function(Chart) { return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]); }, fit: function() { - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - var pointLabels = this.options.pointLabels; - var pointLabelFontSize = helpers.getValueOrDefault(pointLabels.fontSize, globalDefaults.defaultFontSize); - var pointLabeFontStyle = helpers.getValueOrDefault(pointLabels.fontStyle, globalDefaults.defaultFontStyle); - var pointLabeFontFamily = helpers.getValueOrDefault(pointLabels.fontFamily, globalDefaults.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = helpers.min([(this.height / 2 - pointLabelFontSize - 5), this.width / 2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft; - this.ctx.font = pointLabeFont; - - for (i = 0; i < this.getValueCount(); i++) { - // 5px to space the text slightly out - similar to what we do in the draw function. - pointPosition = this.getPointPosition(i, largestPossibleRadius); - textWidth = this.ctx.measureText(this.pointLabels[i] ? this.pointLabels[i] : '').width + 5; - - // Add quarter circle to make degree 0 mean top of circle - var angleRadians = this.getIndexAngle(i) + (Math.PI / 2); - var angle = (angleRadians * 360 / (2 * Math.PI)) % 360; - - if (angle === 0 || angle === 180) { - // At angle 0 and 180, we're at exactly the top/bottom - // of the radar chart, so text will be aligned centrally, so we'll half it and compare - // w/left and right text sizes - halfTextWidth = textWidth / 2; - if (pointPosition.x + halfTextWidth > furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } else if (angle < 180) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - // More than half the values means we'll right align the text - } else if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } + if (this.options.lineArc) { + fit(this); + } else { + fitWithPointLabels(this); } - - xProtrusionLeft = furthestLeft; - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI / 2); - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI / 2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (helpers.isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (helpers.isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2); - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); }, - setCenterPoint: function(leftMovement, rightMovement) { + /** + * Set radius reductions and determine new radius and center point + * @private + */ + setReductions: function(largestPossibleRadius, furthestLimits, furthestAngles) { + var me = this; + var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l); + var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r); + var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t); + var radiusReductionBottom = -Math.max(furthestLimits.b - me.height, 0) / Math.cos(furthestAngles.b); + + radiusReductionLeft = numberOrZero(radiusReductionLeft); + radiusReductionRight = numberOrZero(radiusReductionRight); + radiusReductionTop = numberOrZero(radiusReductionTop); + radiusReductionBottom = numberOrZero(radiusReductionBottom); + + me.drawingArea = Math.min( + Math.round(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2), + Math.round(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2)); + me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom); + }, + setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) { var me = this; var maxRight = me.width - rightMovement - me.drawingArea, - maxLeft = leftMovement + me.drawingArea; + maxLeft = leftMovement + me.drawingArea, + maxTop = topMovement + me.drawingArea, + maxBottom = me.height - bottomMovement - me.drawingArea; me.xCenter = Math.round(((maxLeft + maxRight) / 2) + me.left); - // Always vertically in the centre as the text height doesn't change - me.yCenter = Math.round((me.height / 2) + me.top); + me.yCenter = Math.round(((maxTop + maxBottom) / 2) + me.top); }, getIndexAngle: function(index) { - var angleMultiplier = (Math.PI * 2) / this.getValueCount(); + var angleMultiplier = (Math.PI * 2) / getValueCount(this); var startAngle = this.chart.options && this.chart.options.startAngle ? this.chart.options.startAngle : 0; @@ -10915,7 +11707,7 @@ module.exports = function(Chart) { var startAngleRadians = startAngle * Math.PI * 2 / 360; // Start from the top instead of right, so remove a quarter of the circle - return index * angleMultiplier - (Math.PI / 2) + startAngleRadians; + return index * angleMultiplier + startAngleRadians; }, getDistanceFromCenterForValue: function(value) { var me = this; @@ -10933,7 +11725,7 @@ module.exports = function(Chart) { }, getPointPosition: function(index, distanceFromCenter) { var me = this; - var thisAngle = me.getIndexAngle(index); + var thisAngle = me.getIndexAngle(index) - (Math.PI / 2); return { x: Math.round(Math.cos(thisAngle) * distanceFromCenter) + me.xCenter, y: Math.round(Math.sin(thisAngle) * distanceFromCenter) + me.yCenter @@ -10960,8 +11752,6 @@ module.exports = function(Chart) { var opts = me.options; var gridLineOpts = opts.gridLines; var tickOpts = opts.ticks; - var angleLineOpts = opts.angleLines; - var pointLabelOpts = opts.pointLabels; var getValueOrDefault = helpers.getValueOrDefault; if (opts.display) { @@ -10981,29 +11771,7 @@ module.exports = function(Chart) { // Draw circular lines around the scale if (gridLineOpts.display && index !== 0) { - ctx.strokeStyle = helpers.getValueAtIndexOrDefault(gridLineOpts.color, index - 1); - ctx.lineWidth = helpers.getValueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1); - - if (opts.lineArc) { - // Draw circular arcs between the points - ctx.beginPath(); - ctx.arc(me.xCenter, me.yCenter, yCenterOffset, 0, Math.PI * 2); - ctx.closePath(); - ctx.stroke(); - } else { - // Draw straight lines connecting each index - ctx.beginPath(); - for (var i = 0; i < me.getValueCount(); i++) { - var pointPosition = me.getPointPosition(i, yCenterOffset); - if (i === 0) { - ctx.moveTo(pointPosition.x, pointPosition.y); - } else { - ctx.lineTo(pointPosition.x, pointPosition.y); - } - } - ctx.closePath(); - ctx.stroke(); - } + drawRadiusLine(me, gridLineOpts, yCenterOffset, index); } if (tickOpts.display) { @@ -11030,59 +11798,7 @@ module.exports = function(Chart) { }); if (!opts.lineArc) { - ctx.lineWidth = angleLineOpts.lineWidth; - ctx.strokeStyle = angleLineOpts.color; - - var outerDistance = me.getDistanceFromCenterForValue(opts.reverse ? me.min : me.max); - - // Point Label Font - var pointLabelFontSize = getValueOrDefault(pointLabelOpts.fontSize, globalDefaults.defaultFontSize); - var pointLabeFontStyle = getValueOrDefault(pointLabelOpts.fontStyle, globalDefaults.defaultFontStyle); - var pointLabeFontFamily = getValueOrDefault(pointLabelOpts.fontFamily, globalDefaults.defaultFontFamily); - var pointLabeFont = helpers.fontString(pointLabelFontSize, pointLabeFontStyle, pointLabeFontFamily); - - for (var i = me.getValueCount() - 1; i >= 0; i--) { - if (angleLineOpts.display) { - var outerPosition = me.getPointPosition(i, outerDistance); - ctx.beginPath(); - ctx.moveTo(me.xCenter, me.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = me.getPointPosition(i, outerDistance + 5); - - // Keep this in loop since we may support array properties here - var pointLabelFontColor = getValueOrDefault(pointLabelOpts.fontColor, globalDefaults.defaultFontColor); - ctx.font = pointLabeFont; - ctx.fillStyle = pointLabelFontColor; - - var pointLabels = me.pointLabels; - - // Add quarter circle to make degree 0 mean top of circle - var angleRadians = this.getIndexAngle(i) + (Math.PI / 2); - var angle = (angleRadians * 360 / (2 * Math.PI)) % 360; - - if (angle === 0 || angle === 180) { - ctx.textAlign = 'center'; - } else if (angle < 180) { - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (angle === 90 || angle === 270) { - ctx.textBaseline = 'middle'; - } else if (angle > 270 || angle < 90) { - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(pointLabels[i] ? pointLabels[i] : '', pointLabelPosition.x, pointLabelPosition.y); - } + drawPointLabels(me); } } } @@ -11091,7 +11807,7 @@ module.exports = function(Chart) { }; -},{}],46:[function(require,module,exports){ +},{}],48:[function(require,module,exports){ /* global window: false */ 'use strict'; @@ -11334,7 +12050,7 @@ module.exports = function(Chart) { me.unitScale = helpers.getValueOrDefault(me.options.time.unitStepSize, 1); } else { // Determine the smallest needed unit of the time - var innerWidth = me.isHorizontal() ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); + var innerWidth = me.isHorizontal() ? me.width : me.height; // Crude approximation of what the label length might be var tempFirstLabel = me.tickFormatFunction(me.firstTick, 0, []); @@ -11418,7 +12134,7 @@ module.exports = function(Chart) { me.ticks.push(me.firstTick.clone()); // For every unit in between the first and last moment, create a moment and add it to the ticks tick - for (var i = 1; i <= me.scaleSizeInUnits; ++i) { + for (var i = me.unitScale; i <= me.scaleSizeInUnits; i += me.unitScale) { var newTick = roundedStart.clone().add(i, me.tickUnit); // Are we greater than the max time @@ -11426,9 +12142,7 @@ module.exports = function(Chart) { break; } - if (i % me.unitScale === 0) { - me.ticks.push(newTick); - } + me.ticks.push(newTick); } // Always show the right tick @@ -11454,9 +12168,10 @@ module.exports = function(Chart) { getLabelForIndex: function(index, datasetIndex) { var me = this; var label = me.chart.data.labels && index < me.chart.data.labels.length ? me.chart.data.labels[index] : ''; + var value = me.chart.data.datasets[datasetIndex].data[index]; - if (typeof me.chart.data.datasets[datasetIndex].data[0] === 'object') { - label = me.getRightValue(me.chart.data.datasets[datasetIndex].data[index]); + if (value !== null && typeof value === 'object') { + label = me.getRightValue(value); } // Format nicely @@ -11503,14 +12218,11 @@ module.exports = function(Chart) { var decimal = offset !== 0 ? offset / me.scaleSizeInUnits : offset; if (me.isHorizontal()) { - var innerWidth = me.width - (me.paddingLeft + me.paddingRight); - var valueOffset = (innerWidth * decimal) + me.paddingLeft; - + var valueOffset = (me.width * decimal); return me.left + Math.round(valueOffset); } - var innerHeight = me.height - (me.paddingTop + me.paddingBottom); - var heightOffset = (innerHeight * decimal) + me.paddingTop; + var heightOffset = (me.height * decimal); return me.top + Math.round(heightOffset); } }, @@ -11519,8 +12231,8 @@ module.exports = function(Chart) { }, getValueForPixel: function(pixel) { var me = this; - var innerDimension = me.isHorizontal() ? me.width - (me.paddingLeft + me.paddingRight) : me.height - (me.paddingTop + me.paddingBottom); - var offset = (pixel - (me.isHorizontal() ? me.left + me.paddingLeft : me.top + me.paddingTop)) / innerDimension; + var innerDimension = me.isHorizontal() ? me.width : me.height; + var offset = (pixel - (me.isHorizontal() ? me.left : me.top)) / innerDimension; offset *= me.scaleSizeInUnits; return me.firstTick.clone().add(moment.duration(offset, me.tickUnit).asSeconds(), 'seconds'); },