From: Tanner Linsley Date: Sat, 16 May 2015 02:54:01 +0000 (-0600) Subject: Prettified, and half way through doughut X-Git-Tag: v2.0-alpha~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1d8d19d404070dc62590f7fd86c4a385ea30b270;p=thirdparty%2FChart.js.git Prettified, and half way through doughut --- diff --git a/samples/bar.html b/samples/bar.html index c081ea826..eea5b659b 100644 --- a/samples/bar.html +++ b/samples/bar.html @@ -1,5 +1,6 @@ + Bar Chart @@ -12,8 +13,8 @@ + diff --git a/samples/doughnut.html b/samples/doughnut.html index 879f72f68..125beaf46 100644 --- a/samples/doughnut.html +++ b/samples/doughnut.html @@ -1,80 +1,79 @@ - - Doughnut Chart - - - - - -
- -
- + + Doughnut Chart + + + + - + - $('#randomizeData').click(function(){ - $.each(doughnutData, function(i, piece){ - doughnutData[i].value = randomScalingFactor(); - doughnutData[i].color = 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',.7)'; - }); - window.myDoughnut.update(); - }); - - - - - diff --git a/samples/line.html b/samples/line.html index b3d6c8534..b6c2ae8e7 100644 --- a/samples/line.html +++ b/samples/line.html @@ -1,66 +1,64 @@ - - Line Chart - - - - -
-
- -
-
- + + Line Chart + + + - + - - - diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js index 71b6451f5..8eb436a4d 100644 --- a/src/Chart.Bar.js +++ b/src/Chart.Bar.js @@ -1,369 +1,365 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - var defaultConfig = { - //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero : true, - - //Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Number - Pixel width of the bar border - barBorderWidth : 2, - - //Number - Spacing between each of the X value sets - barValueSpacing : 5, - - //Number - Spacing between data sets within X values - barDatasetSpacing : 1, - - //String - A legend template - legendTemplate : "" - - }; - - - Chart.Type.extend({ - name: "Bar", - defaults : defaultConfig, - initialize: function(data){ - - // Save data as a source for updating of values & methods - this.data = data; - - var options = this.options; - - // Custom Scale Methods and Options - this.ScaleClass = Chart.Scale.extend({ - offsetGridLines : true, - calculateBarX : function(datasetCount, datasetIndex, elementIndex){ - //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.calculateX(elementIndex) - (xWidth/2), - barWidth = this.calculateBarWidth(datasetCount); - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; - }, - calculateBaseWidth : function(){ - return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); - }, - calculateBarWidth : function(datasetCount){ - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); - - return (baseWidth / datasetCount); - } - }); - - // Events - helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); - - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.BarClass = Chart.Rectangle.extend({ - ctx : this.chart.ctx, - }); - - // Build Scale - this.buildScale(this.data.labels); - - //Create a new bar for each piece of data - helpers.each(this.data.datasets,function(dataset,datasetIndex){ - dataset.metaData = []; - helpers.each(dataset.data,function(dataPoint,index){ - dataset.metaData.push(new this.BarClass()); - },this); - },this); - - // Set defaults for bars - this.eachElement(function(bar, index, datasetIndex){ - helpers.extend(bar, { - width : this.scale.calculateBarWidth(this.data.datasets.length), - x: this.scale.calculateBarX(this.data.datasets.length, datasetIndex, index), - y: this.calculateBaseY(), - _datasetIndex: datasetIndex, - _index: index, - }); - // Copy to view model - bar.save(); - }, this); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - // Update the chart with the latest data. - this.update(); - }, - onHover: function(e){ - - - // If exiting chart - if(e.type == 'mouseout'){ - return this; - } - - this.lastActive = this.lastActive || []; - - // Find Active Elements - this.active = function(){ - switch(this.options.hoverMode){ - case 'single': - return this.getElementAtEvent(e); - case 'label': - return this.getElementsAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - - // On Hover hook - if(this.options.onHover){ - this.options.onHover.call(this, this.active); - } - - // Remove styling for last active (even if it may still be active) - if(this.lastActive.length){ - switch(this.options.hoverMode){ - case 'single': - this.lastActive[0].backgroundColor = this.data.datasets[this.lastActive[0]._datasetIndex].backgroundColor; - this.lastActive[0].borderColor = this.data.datasets[this.lastActive[0]._datasetIndex].borderColor; - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].backgroundColor; - this.lastActive[i].borderColor = this.data.datasets[this.lastActive[i]._datasetIndex].borderColor; - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if(this.active.length && this.options.hoverMode){ - switch(this.options.hoverMode){ - case 'single': - this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.5).darken(0.35).rgbString(); - this.active[0].borderColor = this.data.datasets[this.active[0]._datasetIndex].hoverBorderColor || helpers.color(this.active[0].borderColor).saturate(0.5).darken(0.35).rgbString(); - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - this.active[i].backgroundColor = this.data.datasets[this.active[i]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[i].backgroundColor).saturate(0.5).darken(0.35).rgbString(); - this.active[i].borderColor = this.data.datasets[this.active[i]._datasetIndex].hoverBorderColor || helpers.color(this.active[i].borderColor).saturate(0.5).darken(0.35).rgbString(); - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if(this.options.showTooltips){ - - // The usual updates - this.tooltip.initialize(); - - // Active - if(this.active.length){ - helpers.extend(this.tooltip, { - opacity: 1, - _active: this.active, - }); - - this.tooltip.update(); - } - else{ - // Inactive - helpers.extend(this.tooltip, { - opacity: 0, - }); - } - } - - - this.tooltip.pivot(); - - // Hover animations - if(!this.animating){ - var changed; - - helpers.each(this.active, function(element, index){ - if (element !== this.lastActive[index]){ - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length)|| - (this.lastActive.length && this.active.length && changed)){ - - this.stop(); - this.render(this.options.hoverAnimationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - // Calculate the base point for the bar. - calculateBaseY: function() { - var base = this.scale.endPoint; - - if (this.scale.beginAtZero || ((this.scale.min <= 0 && this.scale.max >= 0) || (this.scale.min >= 0 && this.scale.max <= 0))) - { - base = this.scale.calculateY(0); - base += this.options.scaleGridLineWidth; - } - else if (this.scale.min < 0 && this.scale.max < 0) - { - // All values are negative. Use the top as the base - base = this.scale.startPoint; - } - - return base; - }, - update : function(){ - - this.scale.update(); - - this.eachElement(function(bar, index, dataset, datasetIndex){ - helpers.extend(bar, { - width : this.scale.calculateBarWidth(this.data.datasets.length), - x: this.scale.calculateBarX(this.data.datasets.length, datasetIndex, index), - y: this.scale.calculateY(this.data.datasets[datasetIndex].data[index]), - value : this.data.datasets[datasetIndex].data[index], - label : this.data.labels[index], - datasetLabel: this.data.datasets[datasetIndex].label, - borderColor : this.data.datasets[datasetIndex].borderColor, - borderWidth : this.data.datasets[datasetIndex].borderWidth, - backgroundColor : this.data.datasets[datasetIndex].backgroundColor, - _datasetIndex: datasetIndex, - _index: index, - }); - bar.pivot(); - }, this); - - this.render(); - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachValue(function(value){ - values.push(value); - }); - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding : (this.options.showScale) ? 0 : this.options.borderWidth, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - this.scale = new this.ScaleClass(scaleOptions); - }, - // This should be incorportated into the init as something like a default value. "Reflow" seems like a weird word for a fredraw function - redraw : function(){ - var base = this.calculateBaseY(); - this.eachElement(function(element, index, datasetIndex){ - helpers.extend(element,{ - y: base, - base : base - }); - }); - this.render(); - }, - draw : function(ease){ - - var easingDecimal = ease || 1; - this.clear(); - - this.scale.draw(easingDecimal); - - //Draw all the bars for each dataset - this.eachElement(function(bar, index, datasetIndex){ - if (bar.hasValue()){ - // Update the bar basepoint - bar.base = this.calculateBaseY(); - //Transition - bar.transition(easingDecimal).draw(); - } - }, this); - - // Finally draw the tooltip - this.tooltip.transition(easingDecimal).draw(); - } - }); +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + + var defaultConfig = { + //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: true, + + //Boolean - Whether grid lines are shown across the chart + scaleShowGridLines: true, + + //String - Colour of the grid lines + scaleGridLineColor: "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth: 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Number - Pixel width of the bar border + barBorderWidth: 2, + + //Number - Spacing between each of the X value sets + barValueSpacing: 5, + + //Number - Spacing between data sets within X values + barDatasetSpacing: 1, + + //String - A legend template + legendTemplate: "" + + }; + + + Chart.Type.extend({ + name: "Bar", + defaults: defaultConfig, + initialize: function(data) { + + // Save data as a source for updating of values & methods + this.data = data; + + var options = this.options; + + // Custom Scale Methods and Options + this.ScaleClass = Chart.Scale.extend({ + offsetGridLines: true, + calculateBarX: function(datasetCount, datasetIndex, elementIndex) { + //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar + var xWidth = this.calculateBaseWidth(), + xAbsolute = this.calculateX(elementIndex) - (xWidth / 2), + barWidth = this.calculateBarWidth(datasetCount); + + return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth / 2; + }, + calculateBaseWidth: function() { + return (this.calculateX(1) - this.calculateX(0)) - (2 * options.barValueSpacing); + }, + calculateBarWidth: function(datasetCount) { + //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset + var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); + + return (baseWidth / datasetCount); + } + }); + + // Events + helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + + //Declare the extension of the default point, to cater for the options passed in to the constructor + this.BarClass = Chart.Rectangle.extend({ + ctx: this.chart.ctx, + }); + + // Build Scale + this.buildScale(this.data.labels); + + //Create a new bar for each piece of data + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + dataset.metaData = []; + helpers.each(dataset.data, function(dataPoint, index) { + dataset.metaData.push(new this.BarClass()); + }, this); + }, this); + + // Set defaults for bars + this.eachElement(function(bar, index, datasetIndex) { + helpers.extend(bar, { + width: this.scale.calculateBarWidth(this.data.datasets.length), + x: this.scale.calculateBarX(this.data.datasets.length, datasetIndex, index), + y: this.calculateBaseY(), + _datasetIndex: datasetIndex, + _index: index, + }); + // Copy to view model + bar.save(); + }, this); + + // Create tooltip instance exclusively for this chart with some defaults. + this.tooltip = new Chart.Tooltip({ + _chart: this.chart, + _data: this.data, + _options: this.options, + }, this); + + // Update the chart with the latest data. + this.update(); + }, + onHover: function(e) { + + + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.hoverMode) { + case 'single': + return this.getElementAtEvent(e); + case 'label': + return this.getElementsAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + + // On Hover hook + if (this.options.onHover) { + this.options.onHover.call(this, this.active); + } + + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hoverMode) { + case 'single': + this.lastActive[0].backgroundColor = this.data.datasets[this.lastActive[0]._datasetIndex].backgroundColor; + this.lastActive[0].borderColor = this.data.datasets[this.lastActive[0]._datasetIndex].borderColor; + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].backgroundColor; + this.lastActive[i].borderColor = this.data.datasets[this.lastActive[i]._datasetIndex].borderColor; + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hoverMode) { + switch (this.options.hoverMode) { + case 'single': + this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + this.active[0].borderColor = this.data.datasets[this.active[0]._datasetIndex].hoverBorderColor || helpers.color(this.active[0].borderColor).saturate(0.5).darken(0.35).rgbString(); + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + this.active[i].backgroundColor = this.data.datasets[this.active[i]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[i].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + this.active[i].borderColor = this.data.datasets[this.active[i]._datasetIndex].hoverBorderColor || helpers.color(this.active[i].borderColor).saturate(0.5).darken(0.35).rgbString(); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + + // Built in Tooltips + if (this.options.showTooltips) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + helpers.extend(this.tooltip, { + opacity: 1, + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + helpers.extend(this.tooltip, { + opacity: 0, + }); + } + } + + + this.tooltip.pivot(); + + // Hover animations + if (!this.animating) { + var changed; + + helpers.each(this.active, function(element, index) { + if (element !== this.lastActive[index]) { + changed = true; + } + }, this); + + // If entering, leaving, or changing elements, animate the change via pivot + if ((!this.lastActive.length && this.active.length) || + (this.lastActive.length && !this.active.length) || + (this.lastActive.length && this.active.length && changed)) { + + this.stop(); + this.render(this.options.hoverAnimationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + }, + // Calculate the base point for the bar. + calculateBaseY: function() { + var base = this.scale.endPoint; + + if (this.scale.beginAtZero || ((this.scale.min <= 0 && this.scale.max >= 0) || (this.scale.min >= 0 && this.scale.max <= 0))) { + base = this.scale.calculateY(0); + base += this.options.scaleGridLineWidth; + } else if (this.scale.min < 0 && this.scale.max < 0) { + // All values are negative. Use the top as the base + base = this.scale.startPoint; + } + + return base; + }, + update: function() { + + this.scale.update(); + + this.eachElement(function(bar, index, dataset, datasetIndex) { + helpers.extend(bar, { + width: this.scale.calculateBarWidth(this.data.datasets.length), + x: this.scale.calculateBarX(this.data.datasets.length, datasetIndex, index), + y: this.scale.calculateY(this.data.datasets[datasetIndex].data[index]), + value: this.data.datasets[datasetIndex].data[index], + label: this.data.labels[index], + datasetLabel: this.data.datasets[datasetIndex].label, + borderColor: this.data.datasets[datasetIndex].borderColor, + borderWidth: this.data.datasets[datasetIndex].borderWidth, + backgroundColor: this.data.datasets[datasetIndex].backgroundColor, + _datasetIndex: datasetIndex, + _index: index, + }); + bar.pivot(); + }, this); + + this.render(); + }, + buildScale: function(labels) { + var self = this; + + var dataTotal = function() { + var values = []; + self.eachValue(function(value) { + values.push(value); + }); + return values; + }; + + var scaleOptions = { + templateString: this.options.scaleLabel, + height: this.chart.height, + width: this.chart.width, + ctx: this.chart.ctx, + textColor: this.options.scaleFontColor, + fontSize: this.options.scaleFontSize, + fontStyle: this.options.scaleFontStyle, + fontFamily: this.options.scaleFontFamily, + valuesCount: labels.length, + beginAtZero: this.options.scaleBeginAtZero, + integersOnly: this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight) { + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels: labels, + font: helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth: this.options.scaleLineWidth, + lineColor: this.options.scaleLineColor, + showHorizontalLines: this.options.scaleShowHorizontalLines, + showVerticalLines: this.options.scaleShowVerticalLines, + gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.borderWidth, + showLabels: this.options.scaleShowLabels, + display: this.options.showScale + }; + + if (this.options.scaleOverride) { + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new this.ScaleClass(scaleOptions); + }, + // This should be incorportated into the init as something like a default value. "Reflow" seems like a weird word for a fredraw function + redraw: function() { + var base = this.calculateBaseY(); + this.eachElement(function(element, index, datasetIndex) { + helpers.extend(element, { + y: base, + base: base + }); + }); + this.render(); + }, + draw: function(ease) { + + var easingDecimal = ease || 1; + this.clear(); + + this.scale.draw(easingDecimal); + + //Draw all the bars for each dataset + this.eachElement(function(bar, index, datasetIndex) { + if (bar.hasValue()) { + // Update the bar basepoint + bar.base = this.calculateBaseY(); + //Transition + bar.transition(easingDecimal).draw(); + } + }, this); + + // Finally draw the tooltip + this.tooltip.transition(easingDecimal).draw(); + } + }); }).call(this); diff --git a/src/Chart.Core.js b/src/Chart.Core.js index 0fa13465d..1d6651ab7 100755 --- a/src/Chart.Core.js +++ b/src/Chart.Core.js @@ -9,1724 +9,1720 @@ */ -(function(){ +(function() { - "use strict"; + "use strict"; - //Declare root variable - window in the browser, global on the server - var root = this, - previous = root.Chart; + //Declare root variable - window in the browser, global on the server + var root = this, + previous = root.Chart; - //Occupy the global variable of Chart, and create a simple base class - var Chart = function(context){ - var chart = this; - this.canvas = context.canvas; + //Occupy the global variable of Chart, and create a simple base class + var Chart = function(context) { + var chart = this; + this.canvas = context.canvas; - this.ctx = context; + this.ctx = context; - //Variables global to the chart - var computeDimension = function(element,dimension) - { - if (element['offset'+dimension]) - { - return element['offset'+dimension]; - } - else - { - return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); - } - }; + //Variables global to the chart + var computeDimension = function(element, dimension) { + if (element['offset' + dimension]) { + return element['offset' + dimension]; + } else { + return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); + } + }; - var width = this.width = computeDimension(context.canvas,'Width') || context.canvas.width; - var height = this.height = computeDimension(context.canvas,'Height') || context.canvas.height; + var width = this.width = computeDimension(context.canvas, 'Width') || context.canvas.width; + var height = this.height = computeDimension(context.canvas, 'Height') || context.canvas.height; - // Firefox requires this to work correctly - context.canvas.width = width; - context.canvas.height = height; + // Firefox requires this to work correctly + context.canvas.width = width; + context.canvas.height = height; - width = this.width = context.canvas.width; - height = this.height = context.canvas.height; - this.aspectRatio = this.width / this.height; - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(this); + width = this.width = context.canvas.width; + height = this.height = context.canvas.height; + this.aspectRatio = this.width / this.height; + //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. + helpers.retinaScale(this); - return this; - }; - //Globally expose the defaults to allow for user updating/changing - Chart.defaults = { - global: { - // Boolean - Whether to animate the chart - animation: true, + return this; + }; + //Globally expose the defaults to allow for user updating/changing + Chart.defaults = { + global: { + // Boolean - Whether to animate the chart + animation: true, - // Number - Number of animation steps - animationDuration: 1000, + // Number - Number of animation steps + animationDuration: 1000, - // String - Animation easing effect - animationEasing: "easeOutQuart", + // String - Animation easing effect + animationEasing: "easeOutQuart", - // Boolean - If we should show the scale at all - showScale: true, + // Boolean - If we should show the scale at all + showScale: true, - // Boolean - If we want to override with a hard coded scale - scaleOverride: false, + // Boolean - If we want to override with a hard coded scale + scaleOverride: false, - // ** Required if scaleOverride is true ** - // Number - The number of steps in a hard coded scale - scaleSteps: null, - // Number - The value jump in the hard coded scale - scaleStepWidth: null, - // Number - The scale starting value - scaleStartValue: null, + // ** Required if scaleOverride is true ** + // Number - The number of steps in a hard coded scale + scaleSteps: null, + // Number - The value jump in the hard coded scale + scaleStepWidth: null, + // Number - The scale starting value + scaleStartValue: null, - // String - Colour of the scale line - scaleLineColor: "rgba(0,0,0,.1)", + // String - Colour of the scale line + scaleLineColor: "rgba(0,0,0,.1)", - // Number - Pixel width of the scale line - scaleLineWidth: 1, + // Number - Pixel width of the scale line + scaleLineWidth: 1, - // Boolean - Whether to show labels on the scale - scaleShowLabels: true, + // Boolean - Whether to show labels on the scale + scaleShowLabels: true, - // Interpolated JS string - can access value - scaleLabel: "<%=value%>", + // Interpolated JS string - can access value + scaleLabel: "<%=value%>", - // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there - scaleIntegersOnly: true, + // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there + scaleIntegersOnly: true, - // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero: false, + // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value + scaleBeginAtZero: false, - // String - Scale label font declaration for the scale label - scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + // String - Scale label font declaration for the scale label + scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - // Number - Scale label font size in pixels - scaleFontSize: 12, + // Number - Scale label font size in pixels + scaleFontSize: 12, - // String - Scale label font weight style - scaleFontStyle: "normal", + // String - Scale label font weight style + scaleFontStyle: "normal", - // String - Scale label font colour - scaleFontColor: "#666", + // String - Scale label font colour + scaleFontColor: "#666", - // Boolean - whether or not the chart should be responsive and resize when the browser does. - responsive: false, + // Boolean - whether or not the chart should be responsive and resize when the browser does. + responsive: false, - // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container - maintainAspectRatio: true, + // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container + maintainAspectRatio: true, - //String / Boolean - Hover mode for events. - hoverMode : 'single', // 'label', 'dataset', 'false' + //String / Boolean - Hover mode for events. + hoverMode: 'single', // 'label', 'dataset', 'false' - //Function - Custom hover handler - onHover : null, + //Function - Custom hover handler + onHover: null, - //Function - Custom hover handler - hoverAnimationDuration : 400, + //Function - Custom hover handler + hoverAnimationDuration: 400, - // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove - showTooltips: true, + // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove + showTooltips: true, - // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function - customTooltips: false, + // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function + customTooltips: false, - // Array - Array of string names to attach tooltip events - tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], + // Array - Array of string names to attach tooltip events + tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], - // String - Tooltip background colour - tooltipBackgroundColor: "rgba(0,0,0,0.8)", + // String - Tooltip background colour + tooltipBackgroundColor: "rgba(0,0,0,0.8)", - // String - Tooltip label font declaration for the scale label - tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + // String - Tooltip label font declaration for the scale label + tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - // Number - Tooltip label font size in pixels - tooltipFontSize: 14, - - // String - Tooltip font weight style - tooltipFontStyle: "normal", - - // String - Tooltip label font colour - tooltipFontColor: "#fff", - - // String - Tooltip title font declaration for the scale label - tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip title font size in pixels - tooltipTitleFontSize: 14, - - // String - Tooltip title font weight style - tooltipTitleFontStyle: "bold", - - // String - Tooltip title font colour - tooltipTitleFontColor: "#fff", - - // Number - pixel width of padding around tooltip text - tooltipYPadding: 6, - - // Number - pixel width of padding around tooltip text - tooltipXPadding: 6, - - // Number - Size of the caret on the tooltip - tooltipCaretSize: 8, - - // Number - Pixel radius of the tooltip border - tooltipCornerRadius: 6, - - // Number - Pixel offset from point x to tooltip edge - tooltipXOffset: 10, - - // String - Template string for single tooltips - tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", - - // String - Template string for single tooltips - multiTooltipTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>", - - // String - Colour behind the legend colour block - multiTooltipKeyBackground: '#fff', - - // Function - Will fire on animation progression. - onAnimationProgress: function(){}, - - // Function - Will fire on animation completion. - onAnimationComplete: function(){}, - - // Color String - Used for undefined Colros - colorFallback: 'rgba(0,0,0,0.1)', - - } - }; - - //Create a dictionary of chart types, to allow for extension of existing types - Chart.types = {}; - - //Global Chart helpers object for utility methods and classes - var helpers = Chart.helpers = {}; - - //-- Basic js utility methods - var each = helpers.each = function(loopable,callback,self){ - var additionalArgs = Array.prototype.slice.call(arguments, 3); - // Check to see if null or undefined firstly. - if (loopable){ - if (loopable.length === +loopable.length){ - var i; - for (i=0; i= 0; i--) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)){ - return currentItem; - } - } - }, - inherits = helpers.inherits = function(extensions){ - //Basic javascript inheritance based on the model created in Backbone.js - var parent = this; - var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; - - var Surrogate = function(){ this.constructor = ChartElement;}; - Surrogate.prototype = parent.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = inherits; - - if (extensions) extend(ChartElement.prototype, extensions); - - ChartElement.__super__ = parent.prototype; - - return ChartElement; - }, - noop = helpers.noop = function(){}, - uid = helpers.uid = (function(){ - var id=0; - return function(){ - return "chart-" + id++; - }; - })(), - warn = helpers.warn = function(str){ - //Method for warning of errors - if (window.console && typeof window.console.warn === "function") console.warn(str); - }, - amd = helpers.amd = (typeof define === 'function' && define.amd), - //-- Math methods - isNumber = helpers.isNumber = function(n){ - return !isNaN(parseFloat(n)) && isFinite(n); - }, - max = helpers.max = function(array){ - return Math.max.apply( Math, array ); - }, - min = helpers.min = function(array){ - return Math.min.apply( Math, array ); - }, - cap = helpers.cap = function(valueToCap,maxValue,minValue){ - if(isNumber(maxValue)) { - if( valueToCap > maxValue ) { - return maxValue; - } - } - else if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; - } - } - return valueToCap; - }, - getDecimalPlaces = helpers.getDecimalPlaces = function(num){ - if (num%1!==0 && isNumber(num)){ - var s = num.toString(); - if(s.indexOf("e-") < 0){ - // no exponent, e.g. 0.01 - return s.split(".")[1].length; - } - else if(s.indexOf(".") < 0) { - // no decimal point, e.g. 1e-9 - return parseInt(s.split("e-")[1]); - } - else { - // exponent and decimal point, e.g. 1.23e-9 - var parts = s.split(".")[1].split("e-"); - return parts[0].length + parseInt(parts[1]); - } - } - else { - return 0; - } - }, - toRadians = helpers.radians = function(degrees){ - return degrees * (Math.PI/180); - }, - // Gets the angle from vertical upright to the point about a centre. - getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ - var distanceFromXCenter = anglePoint.x - centrePoint.x, - distanceFromYCenter = anglePoint.y - centrePoint.y, - radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); - - - var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); - - //If the segment is in the top left quadrant, we need to add another rotation to the angle - if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ - angle += Math.PI*2; - } - - return { - angle: angle, - distance: radialDistanceFromCenter - }; - }, - aliasPixel = helpers.aliasPixel = function(pixelWidth){ - return (pixelWidth % 2 === 0) ? 0 : 0.5; - }, - splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ - //Props to Rob Spencer at scaled innovation for his post on splining between points - //http://scaledinnovation.com/analytics/splines/aboutSplines.html - var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), - d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), - fa=t*d01/(d01+d12),// scaling factor for triangle Ta - fb=t*d12/(d01+d12); - return { - next : { - x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) - }, - previous : { - x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) - } - }; - }, - calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ - return Math.floor(Math.log(val) / Math.LN10); - }, - calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ - - //Set a minimum step of two - a point at the top of the graph, and a point at the base - var minSteps = 2, - maxSteps = Math.floor(drawingSize/(textSize * 1.5)), - skipFitting = (minSteps >= maxSteps); - - var maxValue = max(valuesArray), - minValue = min(valuesArray); - - // We need some degree of seperation here to calculate the scales if all the values are the same - // Adding/minusing 0.5 will give us a range of 1. - if (maxValue === minValue){ - maxValue += 0.5; - // So we don't end up with a graph with a negative start value if we've said always start from zero - if (minValue >= 0.5 && !startFromZero){ - minValue -= 0.5; - } - else{ - // Make up a whole number above the values - maxValue += 0.5; - } - } - - var valueRange = Math.abs(maxValue - minValue), - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphRange = graphMax - graphMin, - stepValue = Math.pow(10, rangeOrderOfMagnitude), - numberOfSteps = Math.round(graphRange / stepValue); - - //If we have more space on the graph we'll use it to give more definition to the data - while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { - if(numberOfSteps > maxSteps){ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. - if (numberOfSteps % 1 !== 0){ - skipFitting = true; - } - } - //We can fit in double the amount of scale points on the scale - else{ - //If user has declared ints only, and the step value isn't a decimal - if (integersOnly && rangeOrderOfMagnitude >= 0){ - //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float - if(stepValue/2 % 1 === 0){ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - //If it would make it a float break out of the loop - else{ - break; - } - } - //If the scale doesn't have to be an int, make the scale more granular anyway. - else{ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - - } - } - - if (skipFitting){ - numberOfSteps = minSteps; - stepValue = graphRange / numberOfSteps; - } - return { - steps : numberOfSteps, - stepValue : stepValue, - min : graphMin, - max : graphMin + (numberOfSteps * stepValue) - }; - - }, - /* jshint ignore:start */ - // Blows up jshint errors based on the new Function constructor - //Templating methods - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - template = helpers.template = function(templateString, valuesObject){ - - // If templateString is function rather than string-template - call the function for valuesObject - - if(templateString instanceof Function){ - return templateString(valuesObject); - } - - var cache = {}; - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") + - "');}return p.join('');" - ); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - } - return tmpl(templateString,valuesObject); - }, - /* jshint ignore:end */ - generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ - var labelsArray = new Array(numberOfSteps); - if (templateString){ - each(labelsArray,function(val,index){ - labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); - }); - } - return labelsArray; - }, - //--Animation methods - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - easingEffects = helpers.easingEffects = { - linear: function (t) { - return t; - }, - easeInQuad: function (t) { - return t * t; - }, - easeOutQuad: function (t) { - return -1 * t * (t - 2); - }, - easeInOutQuad: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t; - } - return -1 / 2 * ((--t) * (t - 2) - 1); - }, - easeInCubic: function (t) { - return t * t * t; - }, - easeOutCubic: function (t) { - return 1 * ((t = t / 1 - 1) * t * t + 1); - }, - easeInOutCubic: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t * t; - } - return 1 / 2 * ((t -= 2) * t * t + 2); - }, - easeInQuart: function (t) { - return t * t * t * t; - }, - easeOutQuart: function (t) { - return -1 * ((t = t / 1 - 1) * t * t * t - 1); - }, - easeInOutQuart: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t * t * t; - } - return -1 / 2 * ((t -= 2) * t * t * t - 2); - }, - easeInQuint: function (t) { - return 1 * (t /= 1) * t * t * t * t; - }, - easeOutQuint: function (t) { - return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); - }, - easeInOutQuint: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t * t * t * t; - } - return 1 / 2 * ((t -= 2) * t * t * t * t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t / 1 * (Math.PI / 2)); - }, - easeInOutSine: function (t) { - return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); - }, - easeInExpo: function (t) { - return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); - }, - easeOutExpo: function (t) { - return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); - }, - easeInOutExpo: function (t) { - if (t === 0){ - return 0; - } - if (t === 1){ - return 1; - } - if ((t /= 1 / 2) < 1){ - return 1 / 2 * Math.pow(2, 10 * (t - 1)); - } - return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (t) { - if (t >= 1){ - return t; - } - return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); - }, - easeOutCirc: function (t) { - return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); - }, - easeInOutCirc: function (t) { - if ((t /= 1 / 2) < 1){ - return -1 / 2 * (Math.sqrt(1 - t * t) - 1); - } - return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); - }, - easeInElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0){ - return 0; - } - if ((t /= 1) == 1){ - return 1; - } - if (!p){ - p = 1 * 0.3; - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else{ - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - }, - easeOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0){ - return 0; - } - if ((t /= 1) == 1){ - return 1; - } - if (!p){ - p = 1 * 0.3; - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else{ - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; - }, - easeInOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0){ - return 0; - } - if ((t /= 1 / 2) == 2){ - return 1; - } - if (!p){ - p = 1 * (0.3 * 1.5); - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else { - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - if (t < 1){ - return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));} - return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1 * (t /= 1) * t * ((s + 1) * t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - if ((t /= 1 / 2) < 1){ - return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); - } - return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); - }, - easeInBounce: function (t) { - return 1 - easingEffects.easeOutBounce(1 - t); - }, - easeOutBounce: function (t) { - if ((t /= 1) < (1 / 2.75)) { - return 1 * (7.5625 * t * t); - } else if (t < (2 / 2.75)) { - return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); - } else if (t < (2.5 / 2.75)) { - return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); - } else { - return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1 / 2){ - return easingEffects.easeInBounce(t * 2) * 0.5; - } - return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; - } - }, - //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - requestAnimFrame = helpers.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, 1000 / 60); - }; - })(), - cancelAnimFrame = helpers.cancelAnimFrame = (function(){ - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - })(), - animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ - - var currentStep = 0, - easingFunction = easingEffects[easingString] || easingEffects.linear; - - var animationFrame = function(){ - currentStep++; - var stepDecimal = currentStep/totalSteps; - var easeDecimal = easingFunction(stepDecimal); - - callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); - onProgress.call(chartInstance,easeDecimal,stepDecimal); - if (currentStep < totalSteps){ - chartInstance.animationFrame = requestAnimFrame(animationFrame); - } else{ - onComplete.apply(chartInstance); - } - }; - requestAnimFrame(animationFrame); - }, - //-- DOM methods - getRelativePosition = helpers.getRelativePosition = function(evt){ - var mouseX, mouseY; - var e = evt.originalEvent || evt, - canvas = evt.currentTarget || evt.srcElement, - boundingRect = canvas.getBoundingClientRect(); - - if (e.touches){ - mouseX = e.touches[0].clientX - boundingRect.left; - mouseY = e.touches[0].clientY - boundingRect.top; - - } - else{ - mouseX = e.clientX - boundingRect.left; - mouseY = e.clientY - boundingRect.top; - } - - return { - x : mouseX, - y : mouseY - }; - - }, - addEvent = helpers.addEvent = function(node,eventType,method){ - if (node.addEventListener){ - node.addEventListener(eventType,method); - } else if (node.attachEvent){ - node.attachEvent("on"+eventType, method); - } else { - node["on"+eventType] = method; - } - }, - removeEvent = helpers.removeEvent = function(node, eventType, handler){ - if (node.removeEventListener){ - node.removeEventListener(eventType, handler, false); - } else if (node.detachEvent){ - node.detachEvent("on"+eventType,handler); - } else{ - node["on" + eventType] = noop; - } - }, - bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ - // Create the events object if it's not already present - if (!chartInstance.events) chartInstance.events = {}; - - each(arrayOfEvents,function(eventName){ - chartInstance.events[eventName] = function(){ - handler.apply(chartInstance, arguments); - }; - addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); - }); - }, - unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { - each(arrayOfEvents, function(handler,eventName){ - removeEvent(chartInstance.chart.canvas, eventName, handler); - }); - }, - getMaximumWidth = helpers.getMaximumWidth = function(domNode){ - var container = domNode.parentNode, - padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right')); - // TODO = check cross browser stuff with this. - return container.clientWidth - padding; - }, - getMaximumHeight = helpers.getMaximumHeight = function(domNode){ - var container = domNode.parentNode, - padding = parseInt(getStyle(container, 'padding-bottom')) + parseInt(getStyle(container, 'padding-top')); - // TODO = check cross browser stuff with this. - return container.clientHeight - padding; - }, - getStyle = helpers.getStyle = function (el, property) { - return el.currentStyle ? - el.currentStyle[property] : - document.defaultView.getComputedStyle(el, null).getPropertyValue(property); - }, - getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support - retinaScale = helpers.retinaScale = function(chart){ - var ctx = chart.ctx, - width = chart.canvas.width, - height = chart.canvas.height; - - if (window.devicePixelRatio) { - ctx.canvas.style.width = width + "px"; - ctx.canvas.style.height = height + "px"; - ctx.canvas.height = height * window.devicePixelRatio; - ctx.canvas.width = width * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - }, - //-- Canvas methods - clear = helpers.clear = function(chart){ - chart.ctx.clearRect(0,0,chart.width,chart.height); - }, - fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ - return fontStyle + " " + pixelSize+"px " + fontFamily; - }, - longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ - ctx.font = font; - var longest = 0; - each(arrayOfStrings,function(string){ - var textWidth = ctx.measureText(string).width; - longest = (textWidth > longest) ? textWidth : longest; - }); - return longest; - }, - drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - }, - color = helpers.color = function(color){ - if(!window.Color){ - console.log('Color.js not found!'); - return color; - } - return window.Color(color); - }, - isArray = helpers.isArray = function(obj){ - if (!Array.isArray) { - return Object.prototype.toString.call(arg) === '[object Array]'; - } - return Array.isArray(obj); - }; - - //Store a reference to each instance - allowing us to globally resize chart instances on window resize. - //Destroy method on the chart will remove the instance of the chart from this reference. - Chart.instances = {}; - - Chart.Type = function(data,options,chart){ - this.options = options; - this.chart = chart; - this.id = uid(); - //Add the chart instance to the global namespace - Chart.instances[this.id] = this; - - // Initialize is always called when a chart type is created - // By default it is a no op, but it should be extended - if (options.responsive){ - this.resize(); - } - this.initialize.call(this,data); - }; - - //Core methods that'll be a part of every chart type - extend(Chart.Type.prototype,{ - initialize : function(){return this;}, - clear : function(){ - clear(this.chart); - return this; - }, - stop : function(){ - // Stops any current animation loop occuring - Chart.animationService.cancelAnimation(this); - return this; - }, - resize : function(callback){ - this.stop(); - var canvas = this.chart.canvas, - newWidth = getMaximumWidth(this.chart.canvas), - newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); - - canvas.width = this.chart.width = newWidth; - canvas.height = this.chart.height = newHeight; - - retinaScale(this.chart); - - if (typeof callback === "function"){ - callback.apply(this, Array.prototype.slice.call(arguments, 1)); - } - return this; - }, - redraw : noop, - render : function(duration){ - - if (this.options.animation){ - var animation = new Chart.Animation(); - animation.numSteps = (duration || this.options.animationDuration) / 16.66; //60 fps - animation.easing = this.options.animationEasing; - - // render function - animation.render = function(chartInstance, animationObject) { - var easingFunction = helpers.easingEffects[animationObject.easing]; - var stepDecimal = animationObject.currentStep / animationObject.numSteps; - var easeDecimal = easingFunction(stepDecimal); - - chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); - }; - - // user events - animation.onAnimationProgress = this.options.onAnimationProgress; - animation.onAnimationComplete = this.options.onAnimationComplete; - - Chart.animationService.addAnimation(this, animation, duration); - } - else{ - this.draw(); - this.options.onAnimationComplete.call(this); - } - return this; - }, - eachElement : function(callback){ - helpers.each(this.data.datasets,function(dataset, datasetIndex){ - helpers.each(dataset.metaData, callback, this, dataset.metaData, datasetIndex); - },this); - }, - eachValue : function(callback){ - helpers.each(this.data.datasets,function(dataset, datasetIndex){ - helpers.each(dataset.data, callback, this, datasetIndex); - },this); - }, - eachDataset : function(callback){ - helpers.each(this.data.datasets, callback, this); - }, - getElementsAtEvent : function(e){ - var elementsArray = [], - eventPosition = helpers.getRelativePosition(e), - datasetIterator = function(dataset){ - elementsArray.push(dataset.metaData[elementIndex]); - }, - elementIndex; - - for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { - for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { - if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x,eventPosition.y)){ - helpers.each(this.data.datasets, datasetIterator); - } - } - } - - return elementsArray.length ? elementsArray : []; - }, - // Get the single element that was clicked on - // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was drawn - getElementAtEvent : function(e) { - var element = []; - var eventPosition = helpers.getRelativePosition(e); - - for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; ++datasetIndex) { - for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; ++elementIndex) { - if (this.data.datasets[datasetIndex].metaData[elementIndex].inRange(eventPosition.x, eventPosition.y)) { - element.push(this.data.datasets[datasetIndex].metaData[elementIndex]); - return element; - } - } - } - - return []; - }, - generateLegend : function(){ - return template(this.options.legendTemplate,this); - }, - destroy : function(){ - this.clear(); - unbindEvents(this, this.events); - var canvas = this.chart.canvas; - - // Reset canvas height/width attributes starts a fresh with the canvas context - canvas.width = this.chart.width; - canvas.height = this.chart.height; - - // < IE9 doesn't support removeProperty - if (canvas.style.removeProperty) { - canvas.style.removeProperty('width'); - canvas.style.removeProperty('height'); - } else { - canvas.style.removeAttribute('width'); - canvas.style.removeAttribute('height'); - } - - delete Chart.instances[this.id]; - }, - toBase64Image : function(){ - return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); - } - }); - - Chart.Type.extend = function(extensions){ - - var parent = this; - - var ChartType = function(){ - return parent.apply(this,arguments); - }; - - //Copy the prototype object of the this class - ChartType.prototype = clone(parent.prototype); - //Now overwrite some of the properties in the base class with the new extensions - extend(ChartType.prototype, extensions); - - ChartType.extend = Chart.Type.extend; - - if (extensions.name || parent.prototype.name){ - - var chartName = extensions.name || parent.prototype.name; - //Assign any potential default values of the new chart type - - //If none are defined, we'll use a clone of the chart type this is being extended from. - //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart - //doesn't define some defaults of their own. - - var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; - - Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); - - Chart.types[chartName] = ChartType; - - //Register this new chart type in the Chart prototype - Chart.prototype[chartName] = function(data,options){ - var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); - return new ChartType(data,config,this); - }; - } else{ - warn("Name not provided for this chart, so it hasn't been registered"); - } - return parent; - }; - - Chart.Element = function(configuration){ - extend(this,{ - _vm: {}, - }); - extend(this,configuration); - this.initialize.apply(this,arguments); - }; - extend(Chart.Element.prototype,{ - initialize : function(){}, - save: function(){ - this._vm = clone(this); - delete this._vm._vm; - delete this._vm._start; - return this; - }, - pivot: function(){ - if(this._start){ - this._start = clone(this); - helpers.extend(this._start, this._vm); - } - return this; - }, - transition : function(ease){ - if(!this._start){ - if(!this._vm){ - this.save(); - } - this._start = clone(this._vm); - } - - each(this,function(value, key){ - - if(key[0] === '_' || !this.hasOwnProperty(key)){ - // Only non-underscored properties - } - - // Init if doesn't exist - else if(!this._vm[key]){ - this._vm[key] = value || null; - } - - // No unnecessary computations - else if(this[key] === this._vm[key]){ - // It's the same! Woohoo! - } - - // Color transitions if possible - else if(typeof value === 'string'){ - try{ - var color = helpers.color(this._start[key]).mix(helpers.color(this[key]), ease); - this._vm[key] = color.rgbString(); - } catch(err){ - this._vm[key] = value; - } - } - // Number transitions - else if(typeof value === 'number'){ - - this._vm[key] = ((this[key] - this._start[key]) * ease) + this._start[key]; - } - else{ - // Everything else - this._vm[key] = value; - } - - },this); - - if(ease === 1){ - delete this._start; - } - return this; - }, - tooltipPosition : function(){ - return { - x : this.x, - y : this.y - }; - }, - hasValue: function(){ - return isNumber(this.value); - } - }); - - Chart.Element.extend = inherits; - - - Chart.Point = Chart.Element.extend({ - inRange: function(mouseX,mouseY){ - var vm = this._vm; - var hoverRange = vm.hoverRadius + vm.radius; - return ((Math.pow(mouseX - vm.x, 2)+Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange,2)); - }, - inGroupRange: function(mouseX){ - var vm = this._vm; - return (Math.pow(mouseX-vm.x, 2) < Math.pow(vm.radius + this.hoverRadius,2)); - }, - tooltipPosition : function(){ - var vm = this._vm; - return { - x : vm.x, - y : vm.y - }; - }, - draw : function(){ - - var vm = this._vm; - var ctx = this._chart.ctx; - - if (vm.radius > 0 || vm.borderWidth > 0){ - - ctx.beginPath(); - - ctx.arc(vm.x, vm.y, vm.radius, 0, Math.PI*2); - ctx.closePath(); - - ctx.strokeStyle = vm.borderColor || Chart.defaults.global.colorFallback; - ctx.lineWidth = vm.borderWidth || Chart.defaults.global.colorFallback; - - ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.colorFallback; - - ctx.fill(); - ctx.stroke(); - } - } - }); - - - Chart.Line = Chart.Element.extend({ - draw : function(){ - - var vm = this._vm; - var ctx = this._chart.ctx; - - //Draw the line between all the points - ctx.lineWidth = vm.borderWidth || Chart.defaults.global.colorFallback; - ctx.strokeStyle = vm.borderColor || Chart.defaults.global.colorFallback; - ctx.beginPath(); - - helpers.each(vm._points, function(point, index){ - if (index === 0){ - ctx.moveTo(point._vm.x, point._vm.y); - } - else{ - if(vm._tension > 0 || 1){ - var previous = this.previousPoint(point, vm._points, index); - - ctx.bezierCurveTo( - previous._vm.controlPointNextX, - previous._vm.controlPointNextY, - point._vm.controlPointPreviousX, - point._vm.controlPointPreviousY, - point._vm.x, - point._vm.y - ); - } - else{ - ctx.lineTo(point._vm.x,point._vm.y); - } - } - }, this); - - ctx.stroke(); - - if (vm._points.length > 0){ - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(vm._points[vm._points.length - 1].x, vm.scaleBottom); - ctx.lineTo(vm._points[0].x, vm.scaleBottom); - ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.colorFallback; - ctx.closePath(); - ctx.fill(); - } - }, - previousPoint: function(point, collection, index){ - return helpers.findPreviousWhere(collection, function(){return true;}, index) || point; - }, - }); - - Chart.Arc = Chart.Element.extend({ - inRange : function(chartX,chartY){ - - var pointRelativePosition = helpers.getAngleFromPoint(this, { - x: chartX, - y: chartY - }); - - //Check if within the range of the open/close angle - var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), - withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); - - return (betweenAngles && withinRadius); - //Ensure within the outside of the arc centre, but inside arc outer - }, - tooltipPosition : function(){ - var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), - rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; - return { - x : this.x + (Math.cos(centreAngle) * rangeFromCentre), - y : this.y + (Math.sin(centreAngle) * rangeFromCentre) - }; - }, - draw : function(animationPercent){ - - var easingDecimal = animationPercent || 1; - - var ctx = this.ctx; - - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); - - ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); - - ctx.closePath(); - ctx.strokeStyle = this.borderColor; - ctx.lineWidth = this.borderWidth; - - ctx.fillStyle = this.backgroundColor; - - ctx.fill(); - ctx.lineJoin = 'bevel'; - - if (this.showBorder){ - ctx.stroke(); - } - } - }); - - Chart.Rectangle = Chart.Element.extend({ - draw : function(){ - - var vm = this._vm; - - var ctx = this.ctx, - halfWidth = vm.width/2, - leftX = vm.x - halfWidth, - rightX = vm.x + halfWidth, - top = vm.base - (vm.base - vm.y), - 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){ - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = vm.backgroundColor; - ctx.strokeStyle = vm.borderColor; - ctx.lineWidth = vm.borderWidth; - - // It'd be nice to keep this class totally generic to any rectangle - // and simply specify which border to miss out. - ctx.moveTo(leftX, vm.base); - ctx.lineTo(leftX, top); - ctx.lineTo(rightX, top); - ctx.lineTo(rightX, vm.base); - ctx.fill(); - if (vm.borderWidth){ - ctx.stroke(); - } - }, - height : function(){ - var vm = this._vm; - return vm.base - vm.y; - }, - inRange : function(mouseX,mouseY){ - var vm = this._vm; - if (vm.y < vm.base){ - return (mouseX >= vm.x - vm.width/2 && mouseX <= vm.x + vm.width/2) && (mouseY >= vm.y && mouseY <= vm.base); - } else{ - return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y); - } - }, - inGroupRange: function(mouseX){ - var vm = this._vm; - return (mouseX >= vm.x - vm.width/2 && mouseX <= vm.x + vm.width/2); - }, - tooltipPosition : function(){ - var vm = this._vm; - if (vm.y < vm.base){ - return { - x : vm.x, - y : vm.y - }; - } - else{ - return { - x : vm.x, - y : vm.base - }; - } - }, - }); - - Chart.Animation = Chart.Element.extend({ - currentStep: null, // the current animation step - numSteps: 60, // default number of steps - easing: "", // the easing to use for this animation - render: null, // render function used by the animation service - - onAnimationProgress: null, // user specified callback to fire on each step of the animation - onAnimationComplete: null, // user specified callback to fire when the animation finishes - }); - - Chart.Tooltip = Chart.Element.extend({ - initialize : function(){ - var options = this._options; - extend(this, { - opacity:0, - xPadding: options.tooltipXPadding, - yPadding: options.tooltipYPadding, - xOffset: options.tooltipXOffset, - backgroundColor: options.tooltipBackgroundColor, - textColor: options.tooltipFontColor, - _fontFamily: options.tooltipFontFamily, - _fontStyle: options.tooltipFontStyle, - fontSize: options.tooltipFontSize, - titleTextColor: options.tooltipTitleFontColor, - _titleFontFamily: options.tooltipTitleFontFamily, - _titleFontStyle: options.tooltipTitleFontStyle, - titleFontSize: options.tooltipTitleFontSize, - caretHeight: options.tooltipCaretSize, - cornerRadius: options.tooltipCornerRadius, - legendColorBackground : options.multiTooltipKeyBackground, - labels: [], - colors: [], - }); - }, - update: function(){ - - var ctx = this._chart.ctx; - - switch(this._options.hoverMode){ - case 'single': - helpers.extend(this, { - text: template(this._options.tooltipTemplate, this._active[0]), - }); - var tooltipPosition = this._active[0].tooltipPosition(); - helpers.extend(this, { - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - }); - break; - - case 'label': - - // Tooltip Content - - var dataArray, - dataIndex; - - var labels = [], - colors = []; - - for (var i = this._data.datasets.length - 1; i >= 0; i--) { - dataArray = this._data.datasets[i].metaData; - dataIndex = indexOf(dataArray, this._active[0]); - if (dataIndex !== -1){ - break; - } - } - - var medianPosition = (function(index) { - // Get all the points at that particular index - var elements = [], - dataCollection, - xPositions = [], - yPositions = [], - xMax, - yMax, - xMin, - yMin; - helpers.each(this._data.datasets, function(dataset){ - dataCollection = dataset.metaData; - if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ - elements.push(dataCollection[dataIndex]); - } - }); - - helpers.each(elements, function(element) { - xPositions.push(element._vm.x); - yPositions.push(element._vm.y); - - //Include any colour information about the element - labels.push(helpers.template(this._options.multiTooltipTemplate, element)); - colors.push({ - fill: element._vm.backgroundColor, - stroke: element._vm.borderColor - }); - - }, this); - - yMin = min(yPositions); - yMax = max(yPositions); - - xMin = min(xPositions); - xMax = max(xPositions); - - return { - x: (xMin > this._chart.width/2) ? xMin : xMax, - y: (yMin + yMax)/2, - }; - }).call(this, dataIndex); - - // Apply for now - helpers.extend(this, { - x: medianPosition.x, - y: medianPosition.y, - labels: labels, - title: this._active.length ? this._active[0].label : '', - legendColors: colors, - legendBackgroundColor : this._options.multiTooltipKeyBackground, - }); - - - // Calculate Appearance Tweaks - - this.height = (labels.length * this.fontSize) + ((labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; - - var titleWidth = ctx.measureText(this.title).width, - //Label has a legend square as well so account for this. - labelWidth = longestText(ctx,this.font,labels) + this.fontSize + 3, - longestTextWidth = max([labelWidth,titleWidth]); - - this.width = longestTextWidth + (this.xPadding*2); - - - var halfHeight = this.height/2; - - //Check to ensure the height will fit on the canvas - if (this.y - halfHeight < 0 ){ - this.y = halfHeight; - } else if (this.y + halfHeight > this._chart.height){ - this.y = this._chart.height - halfHeight; - } - - //Decide whether to align left or right based on position on canvas - if (this.x > this._chart.width/2){ - this.x -= this.xOffset + this.width; - } else { - this.x += this.xOffset; - } - break; - } - - return this; - }, - draw : function(){ - - var ctx = this._chart.ctx; - var vm = this._vm; - - switch(this._options.hoverMode){ - case 'single': - - ctx.font = fontString(vm.fontSize,vm._fontStyle,vm._fontFamily); - - vm.xAlign = "center"; - vm.yAlign = "above"; - - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = vm.caretPadding = 2; - - var tooltipWidth = ctx.measureText(vm.text).width + 2*vm.xPadding, - tooltipRectHeight = vm.fontSize + 2*vm.yPadding, - tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding; - - if (vm.x + tooltipWidth/2 >this._chart.width){ - vm.xAlign = "left"; - } else if (vm.x - tooltipWidth/2 < 0){ - vm.xAlign = "right"; - } - - if (vm.y - tooltipHeight < 0){ - vm.yAlign = "below"; - } - - var tooltipX = vm.x - tooltipWidth/2, - tooltipY = vm.y - tooltipHeight; - - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); - - // Custom Tooltips - if(this._custom){ - this._custom(this._vm); - } - else{ - switch(vm.yAlign){ - case "above": - //Draw a caret above the x/y - ctx.beginPath(); - ctx.moveTo(vm.x,vm.y - caretPadding); - ctx.lineTo(vm.x + vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); - ctx.lineTo(vm.x - vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); - ctx.closePath(); - ctx.fill(); - break; - case "below": - tooltipY = vm.y + caretPadding + vm.caretHeight; - //Draw a caret below the x/y - ctx.beginPath(); - ctx.moveTo(vm.x, vm.y + caretPadding); - ctx.lineTo(vm.x + vm.caretHeight, vm.y + caretPadding + vm.caretHeight); - ctx.lineTo(vm.x - vm.caretHeight, vm.y + caretPadding + vm.caretHeight); - ctx.closePath(); - ctx.fill(); - break; - } - - switch(vm.xAlign){ - case "left": - tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight); - break; - case "right": - tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight); - break; - } - - drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,vm.cornerRadius); - - ctx.fill(); - - ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(vm.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); - - } - break; - case 'label': - - drawRoundedRectangle(ctx, vm.x, vm.y - vm.height/2, vm.width, vm.height, vm.cornerRadius); - ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); - ctx.fill(); - ctx.closePath(); - - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillStyle = helpers.color(vm.titleTextColor).alpha(vm.opacity).rgbString(); - ctx.font = fontString(vm.fontSize, vm._titleFontStyle, vm._titleFontFamily); - ctx.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0)); - - ctx.font = fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); - helpers.each(vm.labels,function(label,index){ - ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); - ctx.fillText(label,vm.x + vm.xPadding + vm.fontSize + 3, this.getLineHeight(index + 1)); - - //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) - //ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize/2, vm.fontSize, vm.fontSize); - //Instead we'll make a white filled block to put the legendColour palette over. - - ctx.fillStyle = helpers.color(vm.legendBackgroundColor).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); - - ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString(); - ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); - - - },this); - break; - } - }, - getLineHeight : function(index){ - var baseLineHeight = this._vm.y - (this._vm.height/2) + this._vm.yPadding, - afterTitleIndex = index-1; - - //If the index is zero, we're getting the title - if (index === 0){ - return baseLineHeight + this._vm.titleFontSize/2; - } else{ - return baseLineHeight + ((this._vm.fontSize*1.5*afterTitleIndex) + this._vm.fontSize/2) + this._vm.titleFontSize * 1.5; - } - - }, - }); - - Chart.Scale = Chart.Element.extend({ - initialize : function(){ - this.fit(); - }, - buildYLabels : function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) + 10 : 0; - }, - addXLabel : function(label){ - this.xLabels.push(label); - this.valuesCount++; - this.fit(); - }, - removeXLabel : function(){ - this.xLabels.shift(); - this.valuesCount--; - this.fit(); - }, - // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use - fit: function(){ - // First we need the width of the yLabels, assuming the xLabels aren't rotated - - // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation - this.startPoint = (this.display) ? this.fontSize : 0; - this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels - - // Apply padding settings to the start and end point. - this.startPoint += this.padding; - this.endPoint -= this.padding; - - // Cache the starting endpoint, excluding the space for x labels - var cachedEndPoint = this.endPoint; - - // Cache the starting height, so can determine if we need to recalculate the scale yAxis - var cachedHeight = this.endPoint - this.startPoint, - cachedYLabelWidth; - - // Build the current yLabels so we have an idea of what size they'll be to start - /* + // Number - Tooltip label font size in pixels + tooltipFontSize: 14, + + // String - Tooltip font weight style + tooltipFontStyle: "normal", + + // String - Tooltip label font colour + tooltipFontColor: "#fff", + + // String - Tooltip title font declaration for the scale label + tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + + // Number - Tooltip title font size in pixels + tooltipTitleFontSize: 14, + + // String - Tooltip title font weight style + tooltipTitleFontStyle: "bold", + + // String - Tooltip title font colour + tooltipTitleFontColor: "#fff", + + // Number - pixel width of padding around tooltip text + tooltipYPadding: 6, + + // Number - pixel width of padding around tooltip text + tooltipXPadding: 6, + + // Number - Size of the caret on the tooltip + tooltipCaretSize: 8, + + // Number - Pixel radius of the tooltip border + tooltipCornerRadius: 6, + + // Number - Pixel offset from point x to tooltip edge + tooltipXOffset: 10, + + // String - Template string for single tooltips + tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", + + // String - Template string for single tooltips + multiTooltipTemplate: "<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= value %>", + + // String - Colour behind the legend colour block + multiTooltipKeyBackground: '#fff', + + // Function - Will fire on animation progression. + onAnimationProgress: function() {}, + + // Function - Will fire on animation completion. + onAnimationComplete: function() {}, + + // Color String - Used for undefined Colros + colorFallback: 'rgba(0,0,0,0.1)', + + } + }; + + //Create a dictionary of chart types, to allow for extension of existing types + Chart.types = {}; + + //Global Chart helpers object for utility methods and classes + var helpers = Chart.helpers = {}; + + //-- Basic js utility methods + var each = helpers.each = function(loopable, callback, self) { + var additionalArgs = Array.prototype.slice.call(arguments, 3); + // Check to see if null or undefined firstly. + if (loopable) { + if (loopable.length === +loopable.length) { + var i; + for (i = 0; i < loopable.length; i++) { + callback.apply(self, [loopable[i], i].concat(additionalArgs)); + } + } else { + for (var item in loopable) { + callback.apply(self, [loopable[item], item].concat(additionalArgs)); + } + } + } + }, + clone = helpers.clone = function(obj) { + var objClone = {}; + each(obj, function(value, key) { + if (obj.hasOwnProperty(key)) { + objClone[key] = value; + } + }); + return objClone; + }, + extend = helpers.extend = function(base) { + each(Array.prototype.slice.call(arguments, 1), function(extensionObject) { + each(extensionObject, function(value, key) { + if (extensionObject.hasOwnProperty(key)) { + base[key] = value; + } + }); + }); + return base; + }, + merge = helpers.merge = function(base, master) { + //Merge properties in left object over to a shallow clone of object right. + var args = Array.prototype.slice.call(arguments, 0); + args.unshift({}); + return extend.apply(null, args); + }, + indexOf = helpers.indexOf = function(arrayToSearch, item) { + if (Array.prototype.indexOf) { + return arrayToSearch.indexOf(item); + } else { + for (var i = 0; i < arrayToSearch.length; i++) { + if (arrayToSearch[i] === item) return i; + } + return -1; + } + }, + where = helpers.where = function(collection, filterCallback) { + var filtered = []; + + helpers.each(collection, function(item) { + if (filterCallback(item)) { + filtered.push(item); + } + }); + + return filtered; + }, + findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex) { + // Default to start of the array + if (!startIndex) { + startIndex = -1; + } + for (var i = startIndex + 1; i < arrayToSearch.length; i++) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)) { + return currentItem; + } + } + }, + findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex) { + // Default to end of the array + if (!startIndex) { + startIndex = arrayToSearch.length; + } + for (var i = startIndex - 1; i >= 0; i--) { + var currentItem = arrayToSearch[i]; + if (filterCallback(currentItem)) { + return currentItem; + } + } + }, + inherits = helpers.inherits = function(extensions) { + //Basic javascript inheritance based on the model created in Backbone.js + var parent = this; + var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function() { + return parent.apply(this, arguments); + }; + + var Surrogate = function() { + this.constructor = ChartElement; + }; + Surrogate.prototype = parent.prototype; + ChartElement.prototype = new Surrogate(); + + ChartElement.extend = inherits; + + if (extensions) extend(ChartElement.prototype, extensions); + + ChartElement.__super__ = parent.prototype; + + return ChartElement; + }, + noop = helpers.noop = function() {}, + uid = helpers.uid = (function() { + var id = 0; + return function() { + return "chart-" + id++; + }; + })(), + warn = helpers.warn = function(str) { + //Method for warning of errors + if (window.console && typeof window.console.warn === "function") console.warn(str); + }, + amd = helpers.amd = (typeof define === 'function' && define.amd), + //-- Math methods + isNumber = helpers.isNumber = function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + }, + max = helpers.max = function(array) { + return Math.max.apply(Math, array); + }, + min = helpers.min = function(array) { + return Math.min.apply(Math, array); + }, + cap = helpers.cap = function(valueToCap, maxValue, minValue) { + if (isNumber(maxValue)) { + if (valueToCap > maxValue) { + return maxValue; + } + } else if (isNumber(minValue)) { + if (valueToCap < minValue) { + return minValue; + } + } + return valueToCap; + }, + getDecimalPlaces = helpers.getDecimalPlaces = function(num) { + if (num % 1 !== 0 && isNumber(num)) { + var s = num.toString(); + if (s.indexOf("e-") < 0) { + // no exponent, e.g. 0.01 + return s.split(".")[1].length; + } else if (s.indexOf(".") < 0) { + // no decimal point, e.g. 1e-9 + return parseInt(s.split("e-")[1]); + } else { + // exponent and decimal point, e.g. 1.23e-9 + var parts = s.split(".")[1].split("e-"); + return parts[0].length + parseInt(parts[1]); + } + } else { + return 0; + } + }, + toRadians = helpers.radians = function(degrees) { + return degrees * (Math.PI / 180); + }, + // Gets the angle from vertical upright to the point about a centre. + getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint) { + var distanceFromXCenter = anglePoint.x - centrePoint.x, + distanceFromYCenter = anglePoint.y - centrePoint.y, + radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + + var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); + + //If the segment is in the top left quadrant, we need to add another rotation to the angle + if (distanceFromXCenter < 0 && distanceFromYCenter < 0) { + angle += Math.PI * 2; + } + + return { + angle: angle, + distance: radialDistanceFromCenter + }; + }, + aliasPixel = helpers.aliasPixel = function(pixelWidth) { + return (pixelWidth % 2 === 0) ? 0 : 0.5; + }, + splineCurve = helpers.splineCurve = function(FirstPoint, MiddlePoint, AfterPoint, t) { + //Props to Rob Spencer at scaled innovation for his post on splining between points + //http://scaledinnovation.com/analytics/splines/aboutSplines.html + var d01 = Math.sqrt(Math.pow(MiddlePoint.x - FirstPoint.x, 2) + Math.pow(MiddlePoint.y - FirstPoint.y, 2)), + d12 = Math.sqrt(Math.pow(AfterPoint.x - MiddlePoint.x, 2) + Math.pow(AfterPoint.y - MiddlePoint.y, 2)), + fa = t * d01 / (d01 + d12), // scaling factor for triangle Ta + fb = t * d12 / (d01 + d12); + return { + next: { + x: MiddlePoint.x - fa * (AfterPoint.x - FirstPoint.x), + y: MiddlePoint.y - fa * (AfterPoint.y - FirstPoint.y) + }, + previous: { + x: MiddlePoint.x + fb * (AfterPoint.x - FirstPoint.x), + y: MiddlePoint.y + fb * (AfterPoint.y - FirstPoint.y) + } + }; + }, + calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val) { + return Math.floor(Math.log(val) / Math.LN10); + }, + calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly) { + + //Set a minimum step of two - a point at the top of the graph, and a point at the base + var minSteps = 2, + maxSteps = Math.floor(drawingSize / (textSize * 1.5)), + skipFitting = (minSteps >= maxSteps); + + var maxValue = max(valuesArray), + minValue = min(valuesArray); + + // We need some degree of seperation here to calculate the scales if all the values are the same + // Adding/minusing 0.5 will give us a range of 1. + if (maxValue === minValue) { + maxValue += 0.5; + // So we don't end up with a graph with a negative start value if we've said always start from zero + if (minValue >= 0.5 && !startFromZero) { + minValue -= 0.5; + } else { + // Make up a whole number above the values + maxValue += 0.5; + } + } + + var valueRange = Math.abs(maxValue - minValue), + rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), + graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), + graphRange = graphMax - graphMin, + stepValue = Math.pow(10, rangeOrderOfMagnitude), + numberOfSteps = Math.round(graphRange / stepValue); + + //If we have more space on the graph we'll use it to give more definition to the data + while ((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { + if (numberOfSteps > maxSteps) { + stepValue *= 2; + numberOfSteps = Math.round(graphRange / stepValue); + // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. + if (numberOfSteps % 1 !== 0) { + skipFitting = true; + } + } + //We can fit in double the amount of scale points on the scale + else { + //If user has declared ints only, and the step value isn't a decimal + if (integersOnly && rangeOrderOfMagnitude >= 0) { + //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float + if (stepValue / 2 % 1 === 0) { + stepValue /= 2; + numberOfSteps = Math.round(graphRange / stepValue); + } + //If it would make it a float break out of the loop + else { + break; + } + } + //If the scale doesn't have to be an int, make the scale more granular anyway. + else { + stepValue /= 2; + numberOfSteps = Math.round(graphRange / stepValue); + } + + } + } + + if (skipFitting) { + numberOfSteps = minSteps; + stepValue = graphRange / numberOfSteps; + } + return { + steps: numberOfSteps, + stepValue: stepValue, + min: graphMin, + max: graphMin + (numberOfSteps * stepValue) + }; + + }, + /* jshint ignore:start */ + // Blows up jshint errors based on the new Function constructor + //Templating methods + //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ + template = helpers.template = function(templateString, valuesObject) { + + // If templateString is function rather than string-template - call the function for valuesObject + + if (templateString instanceof Function) { + return templateString(valuesObject); + } + + var cache = {}; + + function tmpl(str, data) { + // Figure out if we're getting a template, or if we need to + // load the template - and be sure to cache the result. + var fn = !/\W/.test(str) ? + cache[str] = cache[str] : + + // Generate a reusable function that will serve as a template + // generator (and which will be cached). + new Function("obj", + "var p=[],print=function(){p.push.apply(p,arguments);};" + + + // Introduce the data as local variables using with(){} + "with(obj){p.push('" + + + // Convert the template into pure JavaScript + str + .replace(/[\r\t\n]/g, " ") + .split("<%").join("\t") + .replace(/((^|%>)[^\t]*)'/g, "$1\r") + .replace(/\t=(.*?)%>/g, "',$1,'") + .split("\t").join("');") + .split("%>").join("p.push('") + .split("\r").join("\\'") + + "');}return p.join('');" + ); + + // Provide some basic currying to the user + return data ? fn(data) : fn; + } + return tmpl(templateString, valuesObject); + }, + /* jshint ignore:end */ + generateLabels = helpers.generateLabels = function(templateString, numberOfSteps, graphMin, stepValue) { + var labelsArray = new Array(numberOfSteps); + if (templateString) { + each(labelsArray, function(val, index) { + labelsArray[index] = template(templateString, { + value: (graphMin + (stepValue * (index + 1))) + }); + }); + } + return labelsArray; + }, + //--Animation methods + //Easing functions adapted from Robert Penner's easing equations + //http://www.robertpenner.com/easing/ + easingEffects = helpers.easingEffects = { + linear: function(t) { + return t; + }, + easeInQuad: function(t) { + return t * t; + }, + easeOutQuad: function(t) { + return -1 * t * (t - 2); + }, + easeInOutQuad: function(t) { + if ((t /= 1 / 2) < 1) { + return 1 / 2 * t * t; + } + return -1 / 2 * ((--t) * (t - 2) - 1); + }, + easeInCubic: function(t) { + return t * t * t; + }, + easeOutCubic: function(t) { + return 1 * ((t = t / 1 - 1) * t * t + 1); + }, + easeInOutCubic: function(t) { + if ((t /= 1 / 2) < 1) { + return 1 / 2 * t * t * t; + } + return 1 / 2 * ((t -= 2) * t * t + 2); + }, + easeInQuart: function(t) { + return t * t * t * t; + }, + easeOutQuart: function(t) { + return -1 * ((t = t / 1 - 1) * t * t * t - 1); + }, + easeInOutQuart: function(t) { + if ((t /= 1 / 2) < 1) { + return 1 / 2 * t * t * t * t; + } + return -1 / 2 * ((t -= 2) * t * t * t - 2); + }, + easeInQuint: function(t) { + return 1 * (t /= 1) * t * t * t * t; + }, + easeOutQuint: function(t) { + return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); + }, + easeInOutQuint: function(t) { + if ((t /= 1 / 2) < 1) { + return 1 / 2 * t * t * t * t * t; + } + return 1 / 2 * ((t -= 2) * t * t * t * t + 2); + }, + easeInSine: function(t) { + return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; + }, + easeOutSine: function(t) { + return 1 * Math.sin(t / 1 * (Math.PI / 2)); + }, + easeInOutSine: function(t) { + return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); + }, + easeInExpo: function(t) { + return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); + }, + easeOutExpo: function(t) { + return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); + }, + easeInOutExpo: function(t) { + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + if ((t /= 1 / 2) < 1) { + return 1 / 2 * Math.pow(2, 10 * (t - 1)); + } + return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); + }, + easeInCirc: function(t) { + if (t >= 1) { + return t; + } + return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); + }, + easeOutCirc: function(t) { + return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); + }, + easeInOutCirc: function(t) { + if ((t /= 1 / 2) < 1) { + return -1 / 2 * (Math.sqrt(1 - t * t) - 1); + } + return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); + }, + easeInElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if ((t /= 1) == 1) { + return 1; + } + if (!p) { + p = 1 * 0.3; + } + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if ((t /= 1) == 1) { + return 1; + } + if (!p) { + p = 1 * 0.3; + } + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function(t) { + var s = 1.70158; + var p = 0; + var a = 1; + if (t === 0) { + return 0; + } + if ((t /= 1 / 2) == 2) { + return 1; + } + if (!p) { + p = 1 * (0.3 * 1.5); + } + if (a < Math.abs(1)) { + a = 1; + s = p / 4; + } else { + s = p / (2 * Math.PI) * Math.asin(1 / a); + } + if (t < 1) { + return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + } + return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; + }, + easeInBack: function(t) { + var s = 1.70158; + return 1 * (t /= 1) * t * ((s + 1) * t - s); + }, + easeOutBack: function(t) { + var s = 1.70158; + return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function(t) { + var s = 1.70158; + if ((t /= 1 / 2) < 1) { + return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); + } + return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + easeInBounce: function(t) { + return 1 - easingEffects.easeOutBounce(1 - t); + }, + easeOutBounce: function(t) { + if ((t /= 1) < (1 / 2.75)) { + return 1 * (7.5625 * t * t); + } else if (t < (2 / 2.75)) { + return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); + } else if (t < (2.5 / 2.75)) { + return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); + } else { + return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); + } + }, + easeInOutBounce: function(t) { + if (t < 1 / 2) { + return easingEffects.easeInBounce(t * 2) * 0.5; + } + return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; + } + }, + //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ + requestAnimFrame = helpers.requestAnimFrame = (function() { + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function(callback) { + return window.setTimeout(callback, 1000 / 60); + }; + })(), + cancelAnimFrame = helpers.cancelAnimFrame = (function() { + return window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.oCancelAnimationFrame || + window.msCancelAnimationFrame || + function(callback) { + return window.clearTimeout(callback, 1000 / 60); + }; + })(), + animationLoop = helpers.animationLoop = function(callback, totalSteps, easingString, onProgress, onComplete, chartInstance) { + + var currentStep = 0, + easingFunction = easingEffects[easingString] || easingEffects.linear; + + var animationFrame = function() { + currentStep++; + var stepDecimal = currentStep / totalSteps; + var easeDecimal = easingFunction(stepDecimal); + + callback.call(chartInstance, easeDecimal, stepDecimal, currentStep); + onProgress.call(chartInstance, easeDecimal, stepDecimal); + if (currentStep < totalSteps) { + chartInstance.animationFrame = requestAnimFrame(animationFrame); + } else { + onComplete.apply(chartInstance); + } + }; + requestAnimFrame(animationFrame); + }, + //-- DOM methods + getRelativePosition = helpers.getRelativePosition = function(evt) { + var mouseX, mouseY; + var e = evt.originalEvent || evt, + canvas = evt.currentTarget || evt.srcElement, + boundingRect = canvas.getBoundingClientRect(); + + if (e.touches) { + mouseX = e.touches[0].clientX - boundingRect.left; + mouseY = e.touches[0].clientY - boundingRect.top; + + } else { + mouseX = e.clientX - boundingRect.left; + mouseY = e.clientY - boundingRect.top; + } + + return { + x: mouseX, + y: mouseY + }; + + }, + addEvent = helpers.addEvent = function(node, eventType, method) { + if (node.addEventListener) { + node.addEventListener(eventType, method); + } else if (node.attachEvent) { + node.attachEvent("on" + eventType, method); + } else { + node["on" + eventType] = method; + } + }, + removeEvent = helpers.removeEvent = function(node, eventType, handler) { + if (node.removeEventListener) { + node.removeEventListener(eventType, handler, false); + } else if (node.detachEvent) { + node.detachEvent("on" + eventType, handler); + } else { + node["on" + eventType] = noop; + } + }, + bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler) { + // Create the events object if it's not already present + if (!chartInstance.events) chartInstance.events = {}; + + each(arrayOfEvents, function(eventName) { + chartInstance.events[eventName] = function() { + handler.apply(chartInstance, arguments); + }; + addEvent(chartInstance.chart.canvas, eventName, chartInstance.events[eventName]); + }); + }, + unbindEvents = helpers.unbindEvents = function(chartInstance, arrayOfEvents) { + each(arrayOfEvents, function(handler, eventName) { + removeEvent(chartInstance.chart.canvas, eventName, handler); + }); + }, + getMaximumWidth = helpers.getMaximumWidth = function(domNode) { + var container = domNode.parentNode, + padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right')); + // TODO = check cross browser stuff with this. + return container.clientWidth - padding; + }, + getMaximumHeight = helpers.getMaximumHeight = function(domNode) { + var container = domNode.parentNode, + padding = parseInt(getStyle(container, 'padding-bottom')) + parseInt(getStyle(container, 'padding-top')); + // TODO = check cross browser stuff with this. + return container.clientHeight - padding; + }, + getStyle = helpers.getStyle = function(el, property) { + return el.currentStyle ? + el.currentStyle[property] : + document.defaultView.getComputedStyle(el, null).getPropertyValue(property); + }, + getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support + retinaScale = helpers.retinaScale = function(chart) { + var ctx = chart.ctx, + width = chart.canvas.width, + height = chart.canvas.height; + + if (window.devicePixelRatio) { + ctx.canvas.style.width = width + "px"; + ctx.canvas.style.height = height + "px"; + ctx.canvas.height = height * window.devicePixelRatio; + ctx.canvas.width = width * window.devicePixelRatio; + ctx.scale(window.devicePixelRatio, window.devicePixelRatio); + } + }, + //-- Canvas methods + clear = helpers.clear = function(chart) { + chart.ctx.clearRect(0, 0, chart.width, chart.height); + }, + fontString = helpers.fontString = function(pixelSize, fontStyle, fontFamily) { + return fontStyle + " " + pixelSize + "px " + fontFamily; + }, + longestText = helpers.longestText = function(ctx, font, arrayOfStrings) { + ctx.font = font; + var longest = 0; + each(arrayOfStrings, function(string) { + var textWidth = ctx.measureText(string).width; + longest = (textWidth > longest) ? textWidth : longest; + }); + return longest; + }, + drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + }, + color = helpers.color = function(color) { + if (!window.Color) { + console.log('Color.js not found!'); + return color; + } + return window.Color(color); + }, + isArray = helpers.isArray = function(obj) { + if (!Array.isArray) { + return Object.prototype.toString.call(arg) === '[object Array]'; + } + return Array.isArray(obj); + }; + + //Store a reference to each instance - allowing us to globally resize chart instances on window resize. + //Destroy method on the chart will remove the instance of the chart from this reference. + Chart.instances = {}; + + Chart.Type = function(data, options, chart) { + this.options = options; + this.chart = chart; + this.id = uid(); + //Add the chart instance to the global namespace + Chart.instances[this.id] = this; + + // Initialize is always called when a chart type is created + // By default it is a no op, but it should be extended + if (options.responsive) { + this.resize(); + } + this.initialize.call(this, data); + }; + + //Core methods that'll be a part of every chart type + extend(Chart.Type.prototype, { + initialize: function() { + return this; + }, + clear: function() { + clear(this.chart); + return this; + }, + stop: function() { + // Stops any current animation loop occuring + Chart.animationService.cancelAnimation(this); + return this; + }, + resize: function(callback) { + this.stop(); + var canvas = this.chart.canvas, + newWidth = getMaximumWidth(this.chart.canvas), + newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); + + canvas.width = this.chart.width = newWidth; + canvas.height = this.chart.height = newHeight; + + retinaScale(this.chart); + + if (typeof callback === "function") { + callback.apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this; + }, + redraw: noop, + render: function(duration) { + + if (this.options.animation) { + var animation = new Chart.Animation(); + animation.numSteps = (duration || this.options.animationDuration) / 16.66; //60 fps + animation.easing = this.options.animationEasing; + + // render function + animation.render = function(chartInstance, animationObject) { + var easingFunction = helpers.easingEffects[animationObject.easing]; + var stepDecimal = animationObject.currentStep / animationObject.numSteps; + var easeDecimal = easingFunction(stepDecimal); + + chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); + }; + + // user events + animation.onAnimationProgress = this.options.onAnimationProgress; + animation.onAnimationComplete = this.options.onAnimationComplete; + + Chart.animationService.addAnimation(this, animation, duration); + } else { + this.draw(); + this.options.onAnimationComplete.call(this); + } + return this; + }, + eachElement: function(callback) { + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + helpers.each(dataset.metaData, callback, this, dataset.metaData, datasetIndex); + }, this); + }, + eachValue: function(callback) { + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + helpers.each(dataset.data, callback, this, datasetIndex); + }, this); + }, + eachDataset: function(callback) { + helpers.each(this.data.datasets, callback, this); + }, + getElementsAtEvent: function(e) { + var elementsArray = [], + eventPosition = helpers.getRelativePosition(e), + datasetIterator = function(dataset) { + elementsArray.push(dataset.metaData[elementIndex]); + }, + elementIndex; + + for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; datasetIndex++) { + for (elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; elementIndex++) { + if (this.data.datasets[datasetIndex].metaData[elementIndex].inGroupRange(eventPosition.x, eventPosition.y)) { + helpers.each(this.data.datasets, datasetIterator); + } + } + } + + return elementsArray.length ? elementsArray : []; + }, + // Get the single element that was clicked on + // @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was drawn + getElementAtEvent: function(e) { + var element = []; + var eventPosition = helpers.getRelativePosition(e); + + for (var datasetIndex = 0; datasetIndex < this.data.datasets.length; ++datasetIndex) { + for (var elementIndex = 0; elementIndex < this.data.datasets[datasetIndex].metaData.length; ++elementIndex) { + if (this.data.datasets[datasetIndex].metaData[elementIndex].inRange(eventPosition.x, eventPosition.y)) { + element.push(this.data.datasets[datasetIndex].metaData[elementIndex]); + return element; + } + } + } + + return []; + }, + generateLegend: function() { + return template(this.options.legendTemplate, this); + }, + destroy: function() { + this.clear(); + unbindEvents(this, this.events); + var canvas = this.chart.canvas; + + // Reset canvas height/width attributes starts a fresh with the canvas context + canvas.width = this.chart.width; + canvas.height = this.chart.height; + + // < IE9 doesn't support removeProperty + if (canvas.style.removeProperty) { + canvas.style.removeProperty('width'); + canvas.style.removeProperty('height'); + } else { + canvas.style.removeAttribute('width'); + canvas.style.removeAttribute('height'); + } + + delete Chart.instances[this.id]; + }, + toBase64Image: function() { + return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); + } + }); + + Chart.Type.extend = function(extensions) { + + var parent = this; + + var ChartType = function() { + return parent.apply(this, arguments); + }; + + //Copy the prototype object of the this class + ChartType.prototype = clone(parent.prototype); + //Now overwrite some of the properties in the base class with the new extensions + extend(ChartType.prototype, extensions); + + ChartType.extend = Chart.Type.extend; + + if (extensions.name || parent.prototype.name) { + + var chartName = extensions.name || parent.prototype.name; + //Assign any potential default values of the new chart type + + //If none are defined, we'll use a clone of the chart type this is being extended from. + //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart + //doesn't define some defaults of their own. + + var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; + + Chart.defaults[chartName] = extend(baseDefaults, extensions.defaults); + + Chart.types[chartName] = ChartType; + + //Register this new chart type in the Chart prototype + Chart.prototype[chartName] = function(data, options) { + var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); + return new ChartType(data, config, this); + }; + } else { + warn("Name not provided for this chart, so it hasn't been registered"); + } + return parent; + }; + + Chart.Element = function(configuration) { + extend(this, { + _vm: {}, + }); + extend(this, configuration); + this.initialize.apply(this, arguments); + }; + extend(Chart.Element.prototype, { + initialize: function() {}, + save: function() { + this._vm = clone(this); + delete this._vm._vm; + delete this._vm._start; + return this; + }, + pivot: function() { + if (this._start) { + this._start = clone(this); + helpers.extend(this._start, this._vm); + } + return this; + }, + transition: function(ease) { + if (!this._start) { + if (!this._vm) { + this.save(); + } + this._start = clone(this._vm); + } + + each(this, function(value, key) { + + if (key[0] === '_' || !this.hasOwnProperty(key)) { + // Only non-underscored properties + } + + // Init if doesn't exist + else if (!this._vm[key]) { + this._vm[key] = value || null; + } + + // No unnecessary computations + else if (this[key] === this._vm[key]) { + // It's the same! Woohoo! + } + + // Color transitions if possible + else if (typeof value === 'string') { + try { + var color = helpers.color(this._start[key]).mix(helpers.color(this[key]), ease); + this._vm[key] = color.rgbString(); + } catch (err) { + this._vm[key] = value; + } + } + // Number transitions + else if (typeof value === 'number') { + + this._vm[key] = ((this[key] - this._start[key]) * ease) + this._start[key]; + } else { + // Everything else + this._vm[key] = value; + } + + }, this); + + if (ease === 1) { + delete this._start; + } + return this; + }, + tooltipPosition: function() { + return { + x: this.x, + y: this.y + }; + }, + hasValue: function() { + return isNumber(this.value); + } + }); + + Chart.Element.extend = inherits; + + + Chart.Point = Chart.Element.extend({ + inRange: function(mouseX, mouseY) { + var vm = this._vm; + var hoverRange = vm.hoverRadius + vm.radius; + return ((Math.pow(mouseX - vm.x, 2) + Math.pow(mouseY - vm.y, 2)) < Math.pow(hoverRange, 2)); + }, + inGroupRange: function(mouseX) { + var vm = this._vm; + return (Math.pow(mouseX - vm.x, 2) < Math.pow(vm.radius + this.hoverRadius, 2)); + }, + tooltipPosition: function() { + var vm = this._vm; + return { + x: vm.x, + y: vm.y + }; + }, + draw: function() { + + var vm = this._vm; + var ctx = this._chart.ctx; + + if (vm.radius > 0 || vm.borderWidth > 0) { + + ctx.beginPath(); + + ctx.arc(vm.x, vm.y, vm.radius, 0, Math.PI * 2); + ctx.closePath(); + + ctx.strokeStyle = vm.borderColor || Chart.defaults.global.colorFallback; + ctx.lineWidth = vm.borderWidth || Chart.defaults.global.colorFallback; + + ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.colorFallback; + + ctx.fill(); + ctx.stroke(); + } + } + }); + + + Chart.Line = Chart.Element.extend({ + draw: function() { + + var vm = this._vm; + var ctx = this._chart.ctx; + + //Draw the line between all the points + ctx.lineWidth = vm.borderWidth || Chart.defaults.global.colorFallback; + ctx.strokeStyle = vm.borderColor || Chart.defaults.global.colorFallback; + ctx.beginPath(); + + helpers.each(vm._points, function(point, index) { + if (index === 0) { + ctx.moveTo(point._vm.x, point._vm.y); + } else { + if (vm._tension > 0 || 1) { + var previous = this.previousPoint(point, vm._points, index); + + ctx.bezierCurveTo( + previous._vm.controlPointNextX, + previous._vm.controlPointNextY, + point._vm.controlPointPreviousX, + point._vm.controlPointPreviousY, + point._vm.x, + point._vm.y + ); + } else { + ctx.lineTo(point._vm.x, point._vm.y); + } + } + }, this); + + ctx.stroke(); + + if (vm._points.length > 0) { + //Round off the line by going to the base of the chart, back to the start, then fill. + ctx.lineTo(vm._points[vm._points.length - 1].x, vm.scaleBottom); + ctx.lineTo(vm._points[0].x, vm.scaleBottom); + ctx.fillStyle = vm.backgroundColor || Chart.defaults.global.colorFallback; + ctx.closePath(); + ctx.fill(); + } + }, + previousPoint: function(point, collection, index) { + return helpers.findPreviousWhere(collection, function() { + return true; + }, index) || point; + }, + }); + + Chart.Arc = Chart.Element.extend({ + inRange: function(chartX, chartY) { + + var pointRelativePosition = helpers.getAngleFromPoint(this, { + x: chartX, + y: chartY + }); + + //Check if within the range of the open/close angle + var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), + withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); + + return (betweenAngles && withinRadius); + //Ensure within the outside of the arc centre, but inside arc outer + }, + tooltipPosition: function() { + var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), + rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; + return { + x: this.x + (Math.cos(centreAngle) * rangeFromCentre), + y: this.y + (Math.sin(centreAngle) * rangeFromCentre) + }; + }, + draw: function(animationPercent) { + + var easingDecimal = animationPercent || 1; + + var ctx = this.ctx; + + ctx.beginPath(); + + ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); + + ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); + + ctx.closePath(); + ctx.strokeStyle = this.borderColor; + ctx.lineWidth = this.borderWidth; + + ctx.fillStyle = this.backgroundColor; + + ctx.fill(); + ctx.lineJoin = 'bevel'; + + if (this.showBorder) { + ctx.stroke(); + } + } + }); + + Chart.Rectangle = Chart.Element.extend({ + draw: function() { + + var vm = this._vm; + + var ctx = this.ctx, + halfWidth = vm.width / 2, + leftX = vm.x - halfWidth, + rightX = vm.x + halfWidth, + top = vm.base - (vm.base - vm.y), + 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) { + leftX += halfStroke; + rightX -= halfStroke; + top += halfStroke; + } + + ctx.beginPath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.strokeStyle = vm.borderColor; + ctx.lineWidth = vm.borderWidth; + + // It'd be nice to keep this class totally generic to any rectangle + // and simply specify which border to miss out. + ctx.moveTo(leftX, vm.base); + ctx.lineTo(leftX, top); + ctx.lineTo(rightX, top); + ctx.lineTo(rightX, vm.base); + ctx.fill(); + if (vm.borderWidth) { + ctx.stroke(); + } + }, + height: function() { + var vm = this._vm; + return vm.base - vm.y; + }, + inRange: function(mouseX, mouseY) { + var vm = this._vm; + if (vm.y < vm.base) { + return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.y && mouseY <= vm.base); + } else { + return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2) && (mouseY >= vm.base && mouseY <= vm.y); + } + }, + inGroupRange: function(mouseX) { + var vm = this._vm; + return (mouseX >= vm.x - vm.width / 2 && mouseX <= vm.x + vm.width / 2); + }, + tooltipPosition: function() { + var vm = this._vm; + if (vm.y < vm.base) { + return { + x: vm.x, + y: vm.y + }; + } else { + return { + x: vm.x, + y: vm.base + }; + } + }, + }); + + Chart.Animation = Chart.Element.extend({ + currentStep: null, // the current animation step + numSteps: 60, // default number of steps + easing: "", // the easing to use for this animation + render: null, // render function used by the animation service + + onAnimationProgress: null, // user specified callback to fire on each step of the animation + onAnimationComplete: null, // user specified callback to fire when the animation finishes + }); + + Chart.Tooltip = Chart.Element.extend({ + initialize: function() { + var options = this._options; + extend(this, { + opacity: 0, + xPadding: options.tooltipXPadding, + yPadding: options.tooltipYPadding, + xOffset: options.tooltipXOffset, + backgroundColor: options.tooltipBackgroundColor, + textColor: options.tooltipFontColor, + _fontFamily: options.tooltipFontFamily, + _fontStyle: options.tooltipFontStyle, + fontSize: options.tooltipFontSize, + titleTextColor: options.tooltipTitleFontColor, + _titleFontFamily: options.tooltipTitleFontFamily, + _titleFontStyle: options.tooltipTitleFontStyle, + titleFontSize: options.tooltipTitleFontSize, + caretHeight: options.tooltipCaretSize, + cornerRadius: options.tooltipCornerRadius, + legendColorBackground: options.multiTooltipKeyBackground, + labels: [], + colors: [], + }); + }, + update: function() { + + var ctx = this._chart.ctx; + + switch (this._options.hoverMode) { + case 'single': + helpers.extend(this, { + text: template(this._options.tooltipTemplate, this._active[0]), + }); + var tooltipPosition = this._active[0].tooltipPosition(); + helpers.extend(this, { + x: Math.round(tooltipPosition.x), + y: Math.round(tooltipPosition.y), + }); + break; + + case 'label': + + // Tooltip Content + + var dataArray, + dataIndex; + + var labels = [], + colors = []; + + for (var i = this._data.datasets.length - 1; i >= 0; i--) { + dataArray = this._data.datasets[i].metaData; + dataIndex = indexOf(dataArray, this._active[0]); + if (dataIndex !== -1) { + break; + } + } + + var medianPosition = (function(index) { + // Get all the points at that particular index + var elements = [], + dataCollection, + xPositions = [], + yPositions = [], + xMax, + yMax, + xMin, + yMin; + helpers.each(this._data.datasets, function(dataset) { + dataCollection = dataset.metaData; + if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()) { + elements.push(dataCollection[dataIndex]); + } + }); + + helpers.each(elements, function(element) { + xPositions.push(element._vm.x); + yPositions.push(element._vm.y); + + //Include any colour information about the element + labels.push(helpers.template(this._options.multiTooltipTemplate, element)); + colors.push({ + fill: element._vm.backgroundColor, + stroke: element._vm.borderColor + }); + + }, this); + + yMin = min(yPositions); + yMax = max(yPositions); + + xMin = min(xPositions); + xMax = max(xPositions); + + return { + x: (xMin > this._chart.width / 2) ? xMin : xMax, + y: (yMin + yMax) / 2, + }; + }).call(this, dataIndex); + + // Apply for now + helpers.extend(this, { + x: medianPosition.x, + y: medianPosition.y, + labels: labels, + title: this._active.length ? this._active[0].label : '', + legendColors: colors, + legendBackgroundColor: this._options.multiTooltipKeyBackground, + }); + + + // Calculate Appearance Tweaks + + this.height = (labels.length * this.fontSize) + ((labels.length - 1) * (this.fontSize / 2)) + (this.yPadding * 2) + this.titleFontSize * 1.5; + + var titleWidth = ctx.measureText(this.title).width, + //Label has a legend square as well so account for this. + labelWidth = longestText(ctx, this.font, labels) + this.fontSize + 3, + longestTextWidth = max([labelWidth, titleWidth]); + + this.width = longestTextWidth + (this.xPadding * 2); + + + var halfHeight = this.height / 2; + + //Check to ensure the height will fit on the canvas + if (this.y - halfHeight < 0) { + this.y = halfHeight; + } else if (this.y + halfHeight > this._chart.height) { + this.y = this._chart.height - halfHeight; + } + + //Decide whether to align left or right based on position on canvas + if (this.x > this._chart.width / 2) { + this.x -= this.xOffset + this.width; + } else { + this.x += this.xOffset; + } + break; + } + + return this; + }, + draw: function() { + + var ctx = this._chart.ctx; + var vm = this._vm; + + switch (this._options.hoverMode) { + case 'single': + + ctx.font = fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + + vm.xAlign = "center"; + vm.yAlign = "above"; + + //Distance between the actual element.y position and the start of the tooltip caret + var caretPadding = vm.caretPadding = 2; + + var tooltipWidth = ctx.measureText(vm.text).width + 2 * vm.xPadding, + tooltipRectHeight = vm.fontSize + 2 * vm.yPadding, + tooltipHeight = tooltipRectHeight + vm.caretHeight + caretPadding; + + if (vm.x + tooltipWidth / 2 > this._chart.width) { + vm.xAlign = "left"; + } else if (vm.x - tooltipWidth / 2 < 0) { + vm.xAlign = "right"; + } + + if (vm.y - tooltipHeight < 0) { + vm.yAlign = "below"; + } + + var tooltipX = vm.x - tooltipWidth / 2, + tooltipY = vm.y - tooltipHeight; + + ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + + // Custom Tooltips + if (this._custom) { + this._custom(this._vm); + } else { + switch (vm.yAlign) { + case "above": + //Draw a caret above the x/y + ctx.beginPath(); + ctx.moveTo(vm.x, vm.y - caretPadding); + ctx.lineTo(vm.x + vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); + ctx.lineTo(vm.x - vm.caretHeight, vm.y - (caretPadding + vm.caretHeight)); + ctx.closePath(); + ctx.fill(); + break; + case "below": + tooltipY = vm.y + caretPadding + vm.caretHeight; + //Draw a caret below the x/y + ctx.beginPath(); + ctx.moveTo(vm.x, vm.y + caretPadding); + ctx.lineTo(vm.x + vm.caretHeight, vm.y + caretPadding + vm.caretHeight); + ctx.lineTo(vm.x - vm.caretHeight, vm.y + caretPadding + vm.caretHeight); + ctx.closePath(); + ctx.fill(); + break; + } + + switch (vm.xAlign) { + case "left": + tooltipX = vm.x - tooltipWidth + (vm.cornerRadius + vm.caretHeight); + break; + case "right": + tooltipX = vm.x - (vm.cornerRadius + vm.caretHeight); + break; + } + + drawRoundedRectangle(ctx, tooltipX, tooltipY, tooltipWidth, tooltipRectHeight, vm.cornerRadius); + + ctx.fill(); + + ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillText(vm.text, tooltipX + tooltipWidth / 2, tooltipY + tooltipRectHeight / 2); + + } + break; + case 'label': + + drawRoundedRectangle(ctx, vm.x, vm.y - vm.height / 2, vm.width, vm.height, vm.cornerRadius); + ctx.fillStyle = helpers.color(vm.backgroundColor).alpha(vm.opacity).rgbString(); + ctx.fill(); + ctx.closePath(); + + ctx.textAlign = "left"; + ctx.textBaseline = "middle"; + ctx.fillStyle = helpers.color(vm.titleTextColor).alpha(vm.opacity).rgbString(); + ctx.font = fontString(vm.fontSize, vm._titleFontStyle, vm._titleFontFamily); + ctx.fillText(vm.title, vm.x + vm.xPadding, this.getLineHeight(0)); + + ctx.font = fontString(vm.fontSize, vm._fontStyle, vm._fontFamily); + helpers.each(vm.labels, function(label, index) { + ctx.fillStyle = helpers.color(vm.textColor).alpha(vm.opacity).rgbString(); + ctx.fillText(label, vm.x + vm.xPadding + vm.fontSize + 3, this.getLineHeight(index + 1)); + + //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) + //ctx.clearRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize/2, vm.fontSize, vm.fontSize); + //Instead we'll make a white filled block to put the legendColour palette over. + + ctx.fillStyle = helpers.color(vm.legendBackgroundColor).alpha(vm.opacity).rgbString(); + ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); + + ctx.fillStyle = helpers.color(vm.legendColors[index].fill).alpha(vm.opacity).rgbString(); + ctx.fillRect(vm.x + vm.xPadding, this.getLineHeight(index + 1) - vm.fontSize / 2, vm.fontSize, vm.fontSize); + + + }, this); + break; + } + }, + getLineHeight: function(index) { + var baseLineHeight = this._vm.y - (this._vm.height / 2) + this._vm.yPadding, + afterTitleIndex = index - 1; + + //If the index is zero, we're getting the title + if (index === 0) { + return baseLineHeight + this._vm.titleFontSize / 2; + } else { + return baseLineHeight + ((this._vm.fontSize * 1.5 * afterTitleIndex) + this._vm.fontSize / 2) + this._vm.titleFontSize * 1.5; + } + + }, + }); + + Chart.Scale = Chart.Element.extend({ + initialize: function() { + this.fit(); + }, + buildYLabels: function() { + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i = 0; i <= this.steps; i++) { + this.yLabels.push(template(this.templateString, { + value: (this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces) + })); + } + this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx, this.font, this.yLabels) + 10 : 0; + }, + addXLabel: function(label) { + this.xLabels.push(label); + this.valuesCount++; + this.fit(); + }, + removeXLabel: function() { + this.xLabels.shift(); + this.valuesCount--; + this.fit(); + }, + // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use + fit: function() { + // First we need the width of the yLabels, assuming the xLabels aren't rotated + + // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation + this.startPoint = (this.display) ? this.fontSize : 0; + this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels + + // Apply padding settings to the start and end point. + this.startPoint += this.padding; + this.endPoint -= this.padding; + + // Cache the starting endpoint, excluding the space for x labels + var cachedEndPoint = this.endPoint; + + // Cache the starting height, so can determine if we need to recalculate the scale yAxis + var cachedHeight = this.endPoint - this.startPoint, + cachedYLabelWidth; + + // Build the current yLabels so we have an idea of what size they'll be to start + /* * This sets what is returned from calculateScaleRange as static properties of this class: * this.steps; @@ -1735,609 +1731,609 @@ this.max; * */ - this.calculateYRange(cachedHeight); - - // With these properties set we can now build the array of yLabels - // and also the width of the largest yLabel - this.buildYLabels(); - - this.calculateXLabelRotation(); - - while((cachedHeight > this.endPoint - this.startPoint)){ - cachedHeight = this.endPoint - this.startPoint; - cachedYLabelWidth = this.yLabelWidth; - - this.calculateYRange(cachedHeight); - this.buildYLabels(); - - // Only go through the xLabel loop again if the yLabel width has changed - if (cachedYLabelWidth < this.yLabelWidth){ - this.endPoint = cachedEndPoint; - this.calculateXLabelRotation(); - } - } - - }, - calculateXLabelRotation : function(){ - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - - this.ctx.font = this.font; - - var firstWidth = this.ctx.measureText(this.xLabels[0]).width, - lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, - firstRotated, - lastRotated; - - - this.xScalePaddingRight = lastWidth/2 + 3; - this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth; - - this.xLabelRotation = 0; - if (this.display){ - var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), - cosRotation, - firstRotatedWidth; - this.xLabelWidth = originalLabelWidth; - //Allow 3 pixels x2 padding either side for label readability - var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; - - //Max label rotate should be 90 - also act as a loop counter - while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ - cosRotation = Math.cos(toRadians(this.xLabelRotation)); - - firstRotated = cosRotation * firstWidth; - lastRotated = cosRotation * lastWidth; - - // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth){ - this.xScalePaddingLeft = firstRotated + this.fontSize / 2; - } - this.xScalePaddingRight = this.fontSize/2; - - - this.xLabelRotation++; - this.xLabelWidth = cosRotation * originalLabelWidth; - - } - if (this.xLabelRotation > 0){ - this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; - } - } - else{ - this.xLabelWidth = 0; - this.xScalePaddingRight = this.padding; - this.xScalePaddingLeft = this.padding; - } - - }, - // Needs to be overidden in each Chart type - // Otherwise we need to pass all the data into the scale class - calculateYRange: noop, - drawingArea: function(){ - return this.startPoint - this.endPoint; - }, - calculateY : function(value){ - var scalingFactor = this.drawingArea() / (this.min - this.max); - return this.endPoint - (scalingFactor * (value - this.min)); - }, - calculateX : function(index){ - var isRotated = (this.xLabelRotation > 0), - // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, - innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), - valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), - valueOffset = (valueWidth * index) + this.xScalePaddingLeft; - - if (this.offsetGridLines){ - valueOffset += (valueWidth/2); - } - - return Math.round(valueOffset); - }, - update : function(newProps){ - helpers.extend(this, newProps); - this.fit(); - }, - draw : function(){ - var ctx = this.ctx, - yLabelGap = (this.endPoint - this.startPoint) / this.steps, - xStart = Math.round(this.xScalePaddingLeft); - if (this.display){ - ctx.fillStyle = this.textColor; - ctx.font = this.font; - each(this.yLabels,function(labelString,index){ - var yLabelCenter = this.endPoint - (yLabelGap * index), - linePositionY = Math.round(yLabelCenter), - drawHorizontalLine = this.showHorizontalLines; - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - if (this.showLabels){ - ctx.fillText(labelString,xStart - 10,yLabelCenter); - } - - // This is X axis, so draw it - if (index === 0 && !drawHorizontalLine){ - drawHorizontalLine = true; - } - - if (drawHorizontalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - linePositionY += helpers.aliasPixel(ctx.lineWidth); - - if(drawHorizontalLine){ - ctx.moveTo(xStart, linePositionY); - ctx.lineTo(this.width, linePositionY); - ctx.stroke(); - ctx.closePath(); - } - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - ctx.beginPath(); - ctx.moveTo(xStart - 5, linePositionY); - ctx.lineTo(xStart, linePositionY); - ctx.stroke(); - ctx.closePath(); - - },this); - - each(this.xLabels,function(label,index){ - var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), - // Check to see if line/bar here and decide where to place the line - linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), - isRotated = (this.xLabelRotation > 0), - drawVerticalLine = this.showVerticalLines; - - // This is Y axis, so draw it - if (index === 0 && !drawVerticalLine){ - drawVerticalLine = true; - } - - if (drawVerticalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - if (drawVerticalLine){ - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.startPoint - 3); - ctx.stroke(); - ctx.closePath(); - } - - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - - - // Small lines at the bottom of the base grid line - ctx.beginPath(); - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.endPoint + 5); - ctx.stroke(); - ctx.closePath(); - - ctx.save(); - ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); - ctx.rotate(toRadians(this.xLabelRotation)*-1); - ctx.font = this.font; - ctx.textAlign = (isRotated) ? "right" : "center"; - ctx.textBaseline = (isRotated) ? "middle" : "top"; - ctx.fillText(label, 0, 0); - ctx.restore(); - },this); - - } - } - - }); - - Chart.RadialScale = Chart.Element.extend({ - initialize: function(){ - this.size = min([this.height, this.width]); - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - }, - calculateCenterOffset: function(value){ - // Take into account half font size + the yPadding of the top value - var scalingFactor = this.drawingArea / (this.max - this.min); - - return (value - this.min) * scalingFactor; - }, - update : function(){ - if (!this.lineArc){ - this.setScaleSize(); - } else { - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - } - this.buildYLabels(); - }, - buildYLabels: function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - }, - getCircumference : function(){ - return ((Math.PI*2) / this.valuesCount); - }, - setScaleSize: 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 - */ - - - // 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 = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft, - maxWidthRadius; - this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - for (i=0;i furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } - else if (i < this.valuesCount/2) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - } - else if (i > this.valuesCount/2){ - // More than half the values means we'll right align the text - if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } - } - } - - 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 = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; - - //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); - - }, - setCenterPoint: function(leftMovement, rightMovement){ - - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; - - this.xCenter = (maxLeft + maxRight)/2; - // Always vertically in the centre as the text height doesn't change - this.yCenter = (this.height/2); - }, - - getIndexAngle : function(index){ - var angleMultiplier = (Math.PI * 2) / this.valuesCount; - // Start from the top instead of right, so remove a quarter of the circle - - return index * angleMultiplier - (Math.PI/2); - }, - getPointPosition : function(index, distanceFromCenter){ - var thisAngle = this.getIndexAngle(index); - return { - x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, - y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter - }; - }, - draw: function(){ - if (this.display){ - var ctx = this.ctx; - each(this.yLabels, function(label, index){ - // Don't draw a centre value - if (index > 0){ - var yCenterOffset = index * (this.drawingArea/this.steps), - yHeight = this.yCenter - yCenterOffset, - pointPosition; - - // Draw circular lines around the scale - if (this.lineWidth > 0){ - ctx.strokeStyle = this.lineColor; - ctx.lineWidth = this.lineWidth; - - if(this.lineArc){ - ctx.beginPath(); - ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); - ctx.closePath(); - ctx.stroke(); - } else{ - ctx.beginPath(); - for (var i=0;i= 0; i--) { - if (this.angleLineWidth > 0){ - var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); - ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - ctx.fillStyle = this.pointLabelFontColor; - - var labelsCount = this.labels.length, - halfLabelsCount = this.labels.length/2, - quarterLabelsCount = halfLabelsCount/2, - upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), - exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); - if (i === 0){ - ctx.textAlign = 'center'; - } else if(i === halfLabelsCount){ - ctx.textAlign = 'center'; - } else if (i < halfLabelsCount){ - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (exactQuarter){ - ctx.textBaseline = 'middle'; - } else if (upperHalf){ - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - - Chart.animationService = { - frameDuration: 17, - animations: [], - dropFrames: 0, - addAnimation: function(chartInstance, animationObject, duration) { - - if(!duration){ - chartInstance.animating = true; - } - - for (var index = 0; index < this.animations.length; ++ index){ - if (this.animations[index].chartInstance === chartInstance){ - // replacing an in progress animation - this.animations[index].animationObject = animationObject; - return; - } - } - - this.animations.push({ - chartInstance: chartInstance, - animationObject: animationObject - }); - - // If there are no animations queued, manually kickstart a digest, for lack of a better word - if (this.animations.length == 1) { - helpers.requestAnimFrame.call(window, this.digestWrapper); - } - }, - // Cancel the animation for a given chart instance - cancelAnimation: function(chartInstance) { - var index = helpers.findNextWhere(this.animations, function(animationWrapper) { - return animationWrapper.chartInstance === chartInstance; - }); - - if (index){ - this.animations.splice(index, 1); - chartInstance.animating = false; - } - }, - // calls startDigest with the proper context - digestWrapper: function() { - Chart.animationService.startDigest.call(Chart.animationService); - }, - startDigest: function() { - - var startTime = Date.now(); - var framesToDrop = 0; - - if(this.dropFrames > 1){ - framesToDrop = Math.floor(this.dropFrames); - this.dropFrames -= framesToDrop; - } - - for (var i = 0; i < this.animations.length; i++) { - - if (this.animations[i].animationObject.currentStep === null){ - this.animations[i].animationObject.currentStep = 0; - } - - this.animations[i].animationObject.currentStep += 1 + framesToDrop; - if(this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps){ - this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; - } - - this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); - - if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps){ - // executed the last frame. Remove the animation. - this.animations[i].chartInstance.animating = false; - this.animations.splice(i, 1); - // Keep the index in place to offset the splice - i--; - } - } - - var endTime = Date.now(); - var delay = endTime - startTime - this.frameDuration; - var frameDelay = delay / this.frameDuration; - - if(frameDelay > 1){ - this.dropFrames += frameDelay; - } - - // Do we have more stuff to animate? - if (this.animations.length > 0){ - helpers.requestAnimFrame.call(window, this.digestWrapper); - } - } - }; - - // Attach global event to resize each chart instance when the browser resizes - helpers.addEvent(window, "resize", (function(){ - // Basic debounce of resize function so it doesn't hurt performance when resizing browser. - var timeout; - return function(){ - clearTimeout(timeout); - timeout = setTimeout(function(){ - each(Chart.instances,function(instance){ - // If the responsive flag is set in the chart instance config - // Cascade the resize event down to the chart. - if (instance.options.responsive){ - instance.resize(instance.render, true); - } - }); - }, 50); - }; - })()); - - - if (amd) { - define(function(){ - return Chart; - }); - } else if (typeof module === 'object' && module.exports) { - module.exports = Chart; - } - - root.Chart = Chart; - - Chart.noConflict = function(){ - root.Chart = previous; - return Chart; - }; + this.calculateYRange(cachedHeight); + + // With these properties set we can now build the array of yLabels + // and also the width of the largest yLabel + this.buildYLabels(); + + this.calculateXLabelRotation(); + + while ((cachedHeight > this.endPoint - this.startPoint)) { + cachedHeight = this.endPoint - this.startPoint; + cachedYLabelWidth = this.yLabelWidth; + + this.calculateYRange(cachedHeight); + this.buildYLabels(); + + // Only go through the xLabel loop again if the yLabel width has changed + if (cachedYLabelWidth < this.yLabelWidth) { + this.endPoint = cachedEndPoint; + this.calculateXLabelRotation(); + } + } + + }, + calculateXLabelRotation: function() { + //Get the width of each grid by calculating the difference + //between x offsets between 0 and 1. + + this.ctx.font = this.font; + + var firstWidth = this.ctx.measureText(this.xLabels[0]).width, + lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, + firstRotated, + lastRotated; + + + this.xScalePaddingRight = lastWidth / 2 + 3; + this.xScalePaddingLeft = (firstWidth / 2 > this.yLabelWidth) ? firstWidth / 2 : this.yLabelWidth; + + this.xLabelRotation = 0; + if (this.display) { + var originalLabelWidth = longestText(this.ctx, this.font, this.xLabels), + cosRotation, + firstRotatedWidth; + this.xLabelWidth = originalLabelWidth; + //Allow 3 pixels x2 padding either side for label readability + var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; + + //Max label rotate should be 90 - also act as a loop counter + while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)) { + cosRotation = Math.cos(toRadians(this.xLabelRotation)); + + firstRotated = cosRotation * firstWidth; + lastRotated = cosRotation * lastWidth; + + // We're right aligning the text now. + if (firstRotated + this.fontSize / 2 > this.yLabelWidth) { + this.xScalePaddingLeft = firstRotated + this.fontSize / 2; + } + this.xScalePaddingRight = this.fontSize / 2; + + + this.xLabelRotation++; + this.xLabelWidth = cosRotation * originalLabelWidth; + + } + if (this.xLabelRotation > 0) { + this.endPoint -= Math.sin(toRadians(this.xLabelRotation)) * originalLabelWidth + 3; + } + } else { + this.xLabelWidth = 0; + this.xScalePaddingRight = this.padding; + this.xScalePaddingLeft = this.padding; + } + + }, + // Needs to be overidden in each Chart type + // Otherwise we need to pass all the data into the scale class + calculateYRange: noop, + drawingArea: function() { + return this.startPoint - this.endPoint; + }, + calculateY: function(value) { + var scalingFactor = this.drawingArea() / (this.min - this.max); + return this.endPoint - (scalingFactor * (value - this.min)); + }, + calculateX: function(index) { + var isRotated = (this.xLabelRotation > 0), + // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, + innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), + valueWidth = innerWidth / Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), + valueOffset = (valueWidth * index) + this.xScalePaddingLeft; + + if (this.offsetGridLines) { + valueOffset += (valueWidth / 2); + } + + return Math.round(valueOffset); + }, + update: function(newProps) { + helpers.extend(this, newProps); + this.fit(); + }, + draw: function() { + var ctx = this.ctx, + yLabelGap = (this.endPoint - this.startPoint) / this.steps, + xStart = Math.round(this.xScalePaddingLeft); + if (this.display) { + ctx.fillStyle = this.textColor; + ctx.font = this.font; + each(this.yLabels, function(labelString, index) { + var yLabelCenter = this.endPoint - (yLabelGap * index), + linePositionY = Math.round(yLabelCenter), + drawHorizontalLine = this.showHorizontalLines; + + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + if (this.showLabels) { + ctx.fillText(labelString, xStart - 10, yLabelCenter); + } + + // This is X axis, so draw it + if (index === 0 && !drawHorizontalLine) { + drawHorizontalLine = true; + } + + if (drawHorizontalLine) { + ctx.beginPath(); + } + + if (index > 0) { + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + linePositionY += helpers.aliasPixel(ctx.lineWidth); + + if (drawHorizontalLine) { + ctx.moveTo(xStart, linePositionY); + ctx.lineTo(this.width, linePositionY); + ctx.stroke(); + ctx.closePath(); + } + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + ctx.beginPath(); + ctx.moveTo(xStart - 5, linePositionY); + ctx.lineTo(xStart, linePositionY); + ctx.stroke(); + ctx.closePath(); + + }, this); + + each(this.xLabels, function(label, index) { + var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), + // Check to see if line/bar here and decide where to place the line + linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), + isRotated = (this.xLabelRotation > 0), + drawVerticalLine = this.showVerticalLines; + + // This is Y axis, so draw it + if (index === 0 && !drawVerticalLine) { + drawVerticalLine = true; + } + + if (drawVerticalLine) { + ctx.beginPath(); + } + + if (index > 0) { + // This is a grid line in the centre, so drop that + ctx.lineWidth = this.gridLineWidth; + ctx.strokeStyle = this.gridLineColor; + } else { + // This is the first line on the scale + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + } + + if (drawVerticalLine) { + ctx.moveTo(linePos, this.endPoint); + ctx.lineTo(linePos, this.startPoint - 3); + ctx.stroke(); + ctx.closePath(); + } + + + ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = this.lineColor; + + + // Small lines at the bottom of the base grid line + ctx.beginPath(); + ctx.moveTo(linePos, this.endPoint); + ctx.lineTo(linePos, this.endPoint + 5); + ctx.stroke(); + ctx.closePath(); + + ctx.save(); + ctx.translate(xPos, (isRotated) ? this.endPoint + 12 : this.endPoint + 8); + ctx.rotate(toRadians(this.xLabelRotation) * -1); + ctx.font = this.font; + ctx.textAlign = (isRotated) ? "right" : "center"; + ctx.textBaseline = (isRotated) ? "middle" : "top"; + ctx.fillText(label, 0, 0); + ctx.restore(); + }, this); + + } + } + + }); + + Chart.RadialScale = Chart.Element.extend({ + initialize: function() { + this.size = min([this.height, this.width]); + this.drawingArea = (this.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); + }, + calculateCenterOffset: function(value) { + // Take into account half font size + the yPadding of the top value + var scalingFactor = this.drawingArea / (this.max - this.min); + + return (value - this.min) * scalingFactor; + }, + update: function() { + if (!this.lineArc) { + this.setScaleSize(); + } else { + this.drawingArea = (this.display) ? (this.size / 2) - (this.fontSize / 2 + this.backdropPaddingY) : (this.size / 2); + } + this.buildYLabels(); + }, + buildYLabels: function() { + this.yLabels = []; + + var stepDecimalPlaces = getDecimalPlaces(this.stepValue); + + for (var i = 0; i <= this.steps; i++) { + this.yLabels.push(template(this.templateString, { + value: (this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces) + })); + } + }, + getCircumference: function() { + return ((Math.PI * 2) / this.valuesCount); + }, + setScaleSize: 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 + */ + + + // 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 = min([(this.height / 2 - this.pointLabelFontSize - 5), this.width / 2]), + pointPosition, + i, + textWidth, + halfTextWidth, + furthestRight = this.width, + furthestRightIndex, + furthestRightAngle, + furthestLeft = 0, + furthestLeftIndex, + furthestLeftAngle, + xProtrusionLeft, + xProtrusionRight, + radiusReductionRight, + radiusReductionLeft, + maxWidthRadius; + this.ctx.font = fontString(this.pointLabelFontSize, this.pointLabelFontStyle, this.pointLabelFontFamily); + for (i = 0; i < this.valuesCount; 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(template(this.templateString, { + value: this.labels[i] + })).width + 5; + if (i === 0 || i === this.valuesCount / 2) { + // If we're at index zero, or exactly the middle, 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 (i < this.valuesCount / 2) { + // Less than half the values means we'll left align the text + if (pointPosition.x + textWidth > furthestRight) { + furthestRight = pointPosition.x + textWidth; + furthestRightIndex = i; + } + } else if (i > this.valuesCount / 2) { + // More than half the values means we'll right align the text + if (pointPosition.x - textWidth < furthestLeft) { + furthestLeft = pointPosition.x - textWidth; + furthestLeftIndex = i; + } + } + } + + 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 = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; + radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; + + this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2; + + //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) + this.setCenterPoint(radiusReductionLeft, radiusReductionRight); + + }, + setCenterPoint: function(leftMovement, rightMovement) { + + var maxRight = this.width - rightMovement - this.drawingArea, + maxLeft = leftMovement + this.drawingArea; + + this.xCenter = (maxLeft + maxRight) / 2; + // Always vertically in the centre as the text height doesn't change + this.yCenter = (this.height / 2); + }, + + getIndexAngle: function(index) { + var angleMultiplier = (Math.PI * 2) / this.valuesCount; + // Start from the top instead of right, so remove a quarter of the circle + + return index * angleMultiplier - (Math.PI / 2); + }, + getPointPosition: function(index, distanceFromCenter) { + var thisAngle = this.getIndexAngle(index); + return { + x: (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, + y: (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter + }; + }, + draw: function() { + if (this.display) { + var ctx = this.ctx; + each(this.yLabels, function(label, index) { + // Don't draw a centre value + if (index > 0) { + var yCenterOffset = index * (this.drawingArea / this.steps), + yHeight = this.yCenter - yCenterOffset, + pointPosition; + + // Draw circular lines around the scale + if (this.lineWidth > 0) { + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + + if (this.lineArc) { + ctx.beginPath(); + ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI * 2); + ctx.closePath(); + ctx.stroke(); + } else { + ctx.beginPath(); + for (var i = 0; i < this.valuesCount; i++) { + pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue))); + if (i === 0) { + ctx.moveTo(pointPosition.x, pointPosition.y); + } else { + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } + ctx.closePath(); + ctx.stroke(); + } + } + if (this.showLabels) { + ctx.font = fontString(this.fontSize, this._fontStyle, this._fontFamily); + if (this.showLabelBackdrop) { + var labelWidth = ctx.measureText(label).width; + ctx.fillStyle = this.backdropColor; + ctx.fillRect( + this.xCenter - labelWidth / 2 - this.backdropPaddingX, + yHeight - this.fontSize / 2 - this.backdropPaddingY, + labelWidth + this.backdropPaddingX * 2, + this.fontSize + this.backdropPaddingY * 2 + ); + } + ctx.textAlign = 'center'; + ctx.textBaseline = "middle"; + ctx.fillStyle = this.fontColor; + ctx.fillText(label, this.xCenter, yHeight); + } + } + }, this); + + if (!this.lineArc) { + ctx.lineWidth = this.angleLineWidth; + ctx.strokeStyle = this.angleLineColor; + for (var i = this.valuesCount - 1; i >= 0; i--) { + if (this.angleLineWidth > 0) { + var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(outerPosition.x, outerPosition.y); + ctx.stroke(); + ctx.closePath(); + } + // Extra 3px out for some label spacing + var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); + ctx.font = fontString(this.pointLabelFontSize, this.pointLabelFontStyle, this.pointLabelFontFamily); + ctx.fillStyle = this.pointLabelFontColor; + + var labelsCount = this.labels.length, + halfLabelsCount = this.labels.length / 2, + quarterLabelsCount = halfLabelsCount / 2, + upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), + exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); + if (i === 0) { + ctx.textAlign = 'center'; + } else if (i === halfLabelsCount) { + ctx.textAlign = 'center'; + } else if (i < halfLabelsCount) { + ctx.textAlign = 'left'; + } else { + ctx.textAlign = 'right'; + } + + // Set the correct text baseline based on outer positioning + if (exactQuarter) { + ctx.textBaseline = 'middle'; + } else if (upperHalf) { + ctx.textBaseline = 'bottom'; + } else { + ctx.textBaseline = 'top'; + } + + ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); + } + } + } + } + }); + + Chart.animationService = { + frameDuration: 17, + animations: [], + dropFrames: 0, + addAnimation: function(chartInstance, animationObject, duration) { + + if (!duration) { + chartInstance.animating = true; + } + + for (var index = 0; index < this.animations.length; ++index) { + if (this.animations[index].chartInstance === chartInstance) { + // replacing an in progress animation + this.animations[index].animationObject = animationObject; + return; + } + } + + this.animations.push({ + chartInstance: chartInstance, + animationObject: animationObject + }); + + // If there are no animations queued, manually kickstart a digest, for lack of a better word + if (this.animations.length == 1) { + helpers.requestAnimFrame.call(window, this.digestWrapper); + } + }, + // Cancel the animation for a given chart instance + cancelAnimation: function(chartInstance) { + var index = helpers.findNextWhere(this.animations, function(animationWrapper) { + return animationWrapper.chartInstance === chartInstance; + }); + + if (index) { + this.animations.splice(index, 1); + chartInstance.animating = false; + } + }, + // calls startDigest with the proper context + digestWrapper: function() { + Chart.animationService.startDigest.call(Chart.animationService); + }, + startDigest: function() { + + var startTime = Date.now(); + var framesToDrop = 0; + + if (this.dropFrames > 1) { + framesToDrop = Math.floor(this.dropFrames); + this.dropFrames -= framesToDrop; + } + + for (var i = 0; i < this.animations.length; i++) { + + if (this.animations[i].animationObject.currentStep === null) { + this.animations[i].animationObject.currentStep = 0; + } + + this.animations[i].animationObject.currentStep += 1 + framesToDrop; + if (this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps) { + this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; + } + + this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); + + if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps) { + // executed the last frame. Remove the animation. + this.animations[i].chartInstance.animating = false; + this.animations.splice(i, 1); + // Keep the index in place to offset the splice + i--; + } + } + + var endTime = Date.now(); + var delay = endTime - startTime - this.frameDuration; + var frameDelay = delay / this.frameDuration; + + if (frameDelay > 1) { + this.dropFrames += frameDelay; + } + + // Do we have more stuff to animate? + if (this.animations.length > 0) { + helpers.requestAnimFrame.call(window, this.digestWrapper); + } + } + }; + + // Attach global event to resize each chart instance when the browser resizes + helpers.addEvent(window, "resize", (function() { + // Basic debounce of resize function so it doesn't hurt performance when resizing browser. + var timeout; + return function() { + clearTimeout(timeout); + timeout = setTimeout(function() { + each(Chart.instances, function(instance) { + // If the responsive flag is set in the chart instance config + // Cascade the resize event down to the chart. + if (instance.options.responsive) { + instance.resize(instance.render, true); + } + }); + }, 50); + }; + })()); + + + if (amd) { + define(function() { + return Chart; + }); + } else if (typeof module === 'object' && module.exports) { + module.exports = Chart; + } + + root.Chart = Chart; + + Chart.noConflict = function() { + root.Chart = previous; + return Chart; + }; }).call(this); diff --git a/src/Chart.Doughnut.js b/src/Chart.Doughnut.js index 90f3d0265..b9633aac2 100644 --- a/src/Chart.Doughnut.js +++ b/src/Chart.Doughnut.js @@ -1,212 +1,319 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Whether we should show a stroke on each segment - segmentShowStroke : true, - - //String - The colour of each segment stroke - segmentStrokeColor : "#fff", - - //Number - The width of each segment stroke - segmentStrokeWidth : 2, - - //The percentage of the chart that we cut out of the middle. - percentageInnerCutout : 50, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect - animationEasing : "easeOutBounce", - - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate : true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - - }; - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "Doughnut", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - - // Save data as a source for updating of values & methods - this.data = data; - - //Declare segments as a static property to prevent inheriting across the Chart type prototype - this.segments = []; - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - - this.SegmentArc = Chart.Arc.extend({ - ctx : this.chart.ctx, - x : this.chart.width/2, - y : this.chart.height/2 - }); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - this.calculateTotal(data); - - helpers.each(data,function(datapoint, index){ - if (!datapoint.color) { - datapoint.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)'; - } - this.addData(datapoint, index, true); - },this); - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - this.segments.splice(index, 0, new this.SegmentArc({ - value : segment.value, - outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, - innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - startAngle : Math.PI * 1.5, - circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), - label : segment.label - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - calculateCircumference : function(value) { - if ( this.total > 0 ) { - return (Math.PI*2)*(value / this.total); - } else { - return 0; - } - }, - calculateTotal : function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += Math.abs(segment.value); - },this); - }, - update : function(){ - - // Map new data to data points - if(this.data.length == this.segments.length){ - helpers.each(this.data, function(segment, i){ - helpers.extend(this.segments[i], { - value : segment.value, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - label : segment.label - }); - }, this); - } else{ - // Data size changed without properly inserting, just redraw the chart - this.initialize(this.data); - } - - this.calculateTotal(this.segments); - - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor']); - }); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - this.render(); - }, - - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - }); - }, this); - }, - draw : function(easeDecimal){ - var animDecimal = (easeDecimal) ? easeDecimal : 1; - this.clear(); - helpers.each(this.segments,function(segment,index){ - segment.transition({ - circumference : this.calculateCircumference(segment.value), - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - },animDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - segment.draw(); - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length-1){ - this.segments[index+1].startAngle = segment.endAngle; - } - },this); - - } - }); - - Chart.types.Doughnut.extend({ - name : "Pie", - defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) - }); +(function() { + "use strict"; + + var root = this, + Chart = root.Chart, + //Cache a local reference to Chart.helpers + helpers = Chart.helpers; + + var defaultConfig = { + //Boolean - Whether we should show a stroke on each segment + segmentShowStroke: true, + + //String - The colour of each segment stroke + segmentStrokeColor: "#fff", + + //Number - The width of each segment stroke + segmentStrokeWidth: 2, + + //The percentage of the chart that we cut out of the middle. + percentageInnerCutout: 50, + + //Number - Amount of animation steps + animationSteps: 100, + + //String - Animation easing effect + animationEasing: "easeOutBounce", + + //Boolean - Whether we animate the rotation of the Doughnut + animateRotate: true, + + //Boolean - Whether we animate scaling the Doughnut from the centre + animateScale: false, + + //String - A legend template + legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" + + }; + + Chart.Type.extend({ + //Passing in a name registers this chart in the Chart namespace + name: "Doughnut", + //Providing a defaults will also register the deafults in the chart namespace + defaults: defaultConfig, + //Initialize is fired when the chart is initialized - Data is passed in as a parameter + //Config is automatically merged by the core of Chart.js, and is available at this.options + initialize: function(data) { + + // Save data as a source for updating of values & methods + this.data = data; + + // Slice Type and defaults + this.Slice = Chart.Arc.extend({ + _chart: this.chart, + x: this.chart.width / 2, + y: this.chart.height / 2 + }); + + //Set up tooltip events on the chart + if (this.options.showTooltips) { + helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + } + this.calculateTotal(data); + + // Create new slice for each piece of data + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + dataset.metaData = []; + helpers.each(dataset.data, function(slice, index) { + var metaSlice = new this.Slice(); + if (typeof slice == 'number') { + helpers.extend(metaSlice, { + value: slice + }); + } else { + helpers.extend(metaSlice, slice); + } + if (!metaSlice.color) { + slice.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)'; + } + metaSlice.save(); + dataset.metaData.push(metaSlice); + }, this); + }, this); + + // Create tooltip instance exclusively for this chart with some defaults. + this.tooltip = new Chart.Tooltip({ + _chart: this.chart, + _data: this.data, + _options: this.options, + }, this); + + this.update(); + }, + onHover: function(evt) { + + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.hoverMode) { + case 'single': + return this.getElementAtEvent(e); + case 'label': + return this.getElementsAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + + // On Hover hook + if (this.options.onHover) { + this.options.onHover.call(this, this.active); + } + + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hoverMode) { + case 'single': + this.lastActive[0].backgroundColor = this.data.datasets[0].backgroundColor; + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].backgroundColor; + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hoverMode) { + switch (this.options.hoverMode) { + case 'single': + this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + this.active[i].backgroundColor = this.data.datasets[this.active[i]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[i].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + + // Built in Tooltips + if (this.options.showTooltips) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + helpers.extend(this.tooltip, { + opacity: 1, + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + helpers.extend(this.tooltip, { + opacity: 0, + }); + } + } + + + // Hover animations + this.tooltip.pivot(); + + if (!this.animating) { + var changed; + + helpers.each(this.active, function(element, index) { + if (element !== this.lastActive[index]) { + changed = true; + } + }, this); + + // If entering, leaving, or changing elements, animate the change via pivot + if ((!this.lastActive.length && this.active.length) || + (this.lastActive.length && !this.active.length) || + (this.lastActive.length && this.active.length && changed)) { + + this.stop(); + this.render(this.options.hoverAnimationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + + }, + getSegmentsAtEvent: function(e) { + var segmentsArray = []; + + var location = helpers.getRelativePosition(e); + + helpers.each(this.segments, function(segment) { + if (segment.inRange(location.x, location.y)) segmentsArray.push(segment); + }, this); + return segmentsArray; + }, + calculateCircumference: function(value) { + if (this.total > 0) { + return (Math.PI * 2) * (value / this.total); + } else { + return 0; + } + }, + calculateTotal: function(data) { + this.total = 0; + helpers.each(data, function(segment) { + this.total += Math.abs(segment.value); + }, this); + }, + update: function() { + + this.outerRadius = (helpers.min([this.chart.width, this.chart.height]) - this.options.segmentStrokeWidth / 2) / 2; + + + // Map new data to data points + helpers.each(this.data, function(segment, i) { + helpers.extend(this.segments[i], { + x: this.chart.width / 2, + y: this.chart.height / 2, + value: segment.value, + backgroundColor: segment.color, + hoverBackgroundColor: segment.highlight || segment.color, + borderWidth: this.options.segmentStrokeWidth, + borderColor: this.options.segmentStrokeColor, + label: segment.label, + startAngle: Math.PI * 1.5, + circumference: (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), + innerRadius: (this.options.animateScale) ? 0 : (this.outerRadius / 100) * this.options.percentageInnerCutout, + outerRadius: (this.options.animateScale) ? 0 : this.outerRadius, + }); + }, this); + + this.calculateTotal(this.segments); + + // Reset any highlight colours before updating. + helpers.each(this.activeElements, function(activeElement) { + activeElement.restore(['backgroundColor']); + }); + + helpers.each(this.segments, function(segment) { + segment.save(); + }); + this.render(); + }, + + removeData: function(atIndex) { + var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length - 1; + this.segments.splice(indexToDelete, 1); + this.reflow(); + this.update(); + }, + + reflow: function() { + helpers.extend(this.Slice.prototype, { + x: this.chart.width / 2, + y: this.chart.height / 2 + }); + this.outerRadius = (helpers.min([this.chart.width, this.chart.height]) - this.options.segmentStrokeWidth / 2) / 2; + helpers.each(this.segments, function(segment) { + segment.update({ + outerRadius: this.outerRadius, + innerRadius: (this.outerRadius / 100) * this.options.percentageInnerCutout + }); + }, this); + }, + draw: function(easeDecimal) { + var animDecimal = (easeDecimal) ? easeDecimal : 1; + this.clear(); + helpers.each(this.segments, function(segment, index) { + segment.transition({ + circumference: this.calculateCircumference(segment.value), + outerRadius: this.outerRadius, + innerRadius: (this.outerRadius / 100) * this.options.percentageInnerCutout + }, animDecimal); + + segment.endAngle = segment.startAngle + segment.circumference; + + segment.draw(); + if (index === 0) { + segment.startAngle = Math.PI * 1.5; + } + //Check to see if it's the last segment, if not get the next and update the start angle + if (index < this.segments.length - 1) { + this.segments[index + 1].startAngle = segment.endAngle; + } + }, this); + + } + }); + + Chart.types.Doughnut.extend({ + name: "Pie", + defaults: helpers.merge(defaultConfig, { + percentageInnerCutout: 0 + }) + }); }).call(this); diff --git a/src/Chart.Line.js b/src/Chart.Line.js index 568f1097d..e328b0a68 100644 --- a/src/Chart.Line.js +++ b/src/Chart.Line.js @@ -1,427 +1,423 @@ -(function(){ - "use strict"; +(function() { + "use strict"; - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Number - Tension of the bezier curve between points - tension : 0.4, - - //Number - Radius of each point dot in pixels - pointRadius : 4, - - //Number - Pixel width of point dot border - pointBorderWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHoverRadius : 20, - - //Number - Pixel width of dataset border - borderWidth : 2, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", - - //Boolean - Whether to horizontally center the label and point dot inside the grid - offsetGridLines : false - - }; - - - Chart.Type.extend({ - name: "Line", - defaults : defaultConfig, - initialize: function(data){ - // Save data as a source for updating of values & methods - this.data = data; - - //Custom Point Defaults - this.PointClass = Chart.Point.extend({ - _chart: this.chart, - offsetGridLines : this.options.offsetGridLines, - borderWidth : this.options.pointBorderWidth, - radius : this.options.pointRadius, - hoverRadius : this.options.pointHoverRadius, - }); - - // Events - helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); - - // Build Scale - this.buildScale(this.data.labels); - - - //Create a new line and its points for each dataset and piece of data - helpers.each(this.data.datasets,function(dataset,datasetIndex){ - dataset.metaDataset = new Chart.Line(); - dataset.metaData = []; - helpers.each(dataset.data,function(dataPoint,index){ - dataset.metaData.push(new this.PointClass()); - },this); - },this); - - // Set defaults for lines - this.eachDataset(function(dataset, datasetIndex){ - dataset = helpers.merge(this.options, dataset); - helpers.extend(dataset.metaDataset, { - _points: dataset.metaData, - _datasetIndex: datasetIndex, - _chart: this.chart, - }); - // Copy to view model - dataset.metaDataset.save(); - }, this); - - // Set defaults for points - this.eachElement(function(point, index, dataset, datasetIndex){ - helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.endPoint, - _datasetIndex: datasetIndex, - _index: index, - _chart: this.chart - }); - - // Default bezier control points - helpers.extend(point, { - controlPointPreviousX: this.previousPoint(dataset, index).x, - controlPointPreviousY: this.nextPoint(dataset, index).y, - controlPointNextX: this.previousPoint(dataset, index).x, - controlPointNextY: this.nextPoint(dataset, index).y, - }); - // Copy to view model - point.save(); - }, this); - - // Create tooltip instance exclusively for this chart with some defaults. - this.tooltip = new Chart.Tooltip({ - _chart: this.chart, - _data: this.data, - _options: this.options, - }, this); - - this.update(); - }, - nextPoint: function(collection, index){ - return collection[index - 1] || collection[index]; - }, - previousPoint: function(collection, index){ - return collection[index + 1] || collection[index]; - }, - onHover: function(e){ - - - // If exiting chart - if(e.type == 'mouseout'){ - return this; - } - - this.lastActive = this.lastActive || []; - - // Find Active Elements - this.active = function(){ - switch(this.options.hoverMode){ - case 'single': - return this.getElementAtEvent(e); - case 'label': - return this.getElementsAtEvent(e); - case 'dataset': - return this.getDatasetAtEvent(e); - default: - return e; - } - }.call(this); - - // On Hover hook - if(this.options.onHover){ - this.options.onHover.call(this, this.active); - } - - // Remove styling for last active (even if it may still be active) - if(this.lastActive.length){ - switch(this.options.hoverMode){ - case 'single': - this.lastActive[0].backgroundColor = this.data.datasets[this.lastActive[0]._datasetIndex].pointBackgroundColor; - this.lastActive[0].borderColor = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderColor; - this.lastActive[0].borderWidth = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderWidth; - break; - case 'label': - for (var i = 0; i < this.lastActive.length; i++) { - this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].pointBackgroundColor; - this.lastActive[i].borderColor = this.data.datasets[this.lastActive[i]._datasetIndex].pointBorderColor; - this.lastActive[i].borderWidth = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderWidth; - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - // Built in hover styling - if(this.active.length && this.options.hoverMode){ - switch(this.options.hoverMode){ - case 'single': - this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.5).darken(0.35).rgbString(); - this.active[0].borderColor = this.data.datasets[this.active[0]._datasetIndex].hoverBorderColor || helpers.color(this.active[0].borderColor).saturate(0.5).darken(0.35).rgbString(); - this.active[0].borderWidth = this.data.datasets[this.active[0]._datasetIndex].borderWidth + 10; - break; - case 'label': - for (var i = 0; i < this.active.length; i++) { - this.active[i].backgroundColor = this.data.datasets[this.active[i]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[i].backgroundColor).saturate(0.5).darken(0.35).rgbString(); - this.active[i].borderColor = this.data.datasets[this.active[i]._datasetIndex].hoverBorderColor || helpers.color(this.active[i].borderColor).saturate(0.5).darken(0.35).rgbString(); - this.active[i].borderWidth = this.data.datasets[this.active[i]._datasetIndex].borderWidth + 2; - } - break; - case 'dataset': - break; - default: - // Don't change anything - } - } - - - // Built in Tooltips - if(this.options.showTooltips){ - - // The usual updates - this.tooltip.initialize(); - - // Active - if(this.active.length){ - helpers.extend(this.tooltip, { - opacity: 1, - _active: this.active, - }); - - this.tooltip.update(); - } - else{ - // Inactive - helpers.extend(this.tooltip, { - opacity: 0, - }); - } - } - - - // Hover animations - this.tooltip.pivot(); - - if(!this.animating){ - var changed; - - helpers.each(this.active, function(element, index){ - if (element !== this.lastActive[index]){ - changed = true; - } - }, this); - - // If entering, leaving, or changing elements, animate the change via pivot - if ((!this.lastActive.length && this.active.length) || - (this.lastActive.length && !this.active.length)|| - (this.lastActive.length && this.active.length && changed)){ - - this.stop(); - this.render(this.options.hoverAnimationDuration); - } - } - - // Remember Last Active - this.lastActive = this.active; - return this; - }, - update : function(){ - - this.scale.update(); - - // Update the lines - this.eachDataset(function(dataset, datasetIndex){ - helpers.extend(dataset.metaDataset, { - backgroundColor: dataset.backgroundColor || this.options.backgroundColor, - borderWidth: dataset.borderWidth || this.options.borderWidth, - borderColor: dataset.borderColor || this.options.borderColor, - tension: dataset.tension || this.options.tension, - scaleTop: this.scale.startPoint, - scaleBottom: this.scale.endPoint, - _points: dataset.metaData, - _datasetIndex: datasetIndex, - }); - dataset.metaDataset.pivot(); - }); - - // Update the points - this.eachElement(function(point, index, dataset, datasetIndex){ - helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.calculateY(this.data.datasets[datasetIndex].data[index]), - value : this.data.datasets[datasetIndex].data[index], - label : this.data.labels[index], - datasetLabel: this.data.datasets[datasetIndex].label, - // Appearance - hoverBackgroundColor: this.data.datasets[datasetIndex].pointHoverBackgroundColor || this.options.pointHoverBackgroundColor, - hoverBorderColor : this.data.datasets[datasetIndex].pointHoverBorderColor || this.options.pointHoverBorderColor, - hoverRadius : this.data.datasets[datasetIndex].pointHoverRadius || this.options.pointHoverRadius, - radius: this.data.datasets[datasetIndex].pointRadius || this.options.pointRadius, - borderWidth: this.data.datasets[datasetIndex].pointBorderWidth || this.options.pointBorderWidth, - borderColor: this.data.datasets[datasetIndex].pointBorderColor || this.options.pointBorderColor, - backgroundColor: this.data.datasets[datasetIndex].pointBackgroundColor || this.options.pointBackgroundColor, - tension: this.data.datasets[datasetIndex].metaDataset.tension, - _datasetIndex: datasetIndex, - _index: index, - }); - }, this); - - // Update control points for the bezier curve - this.eachElement(function(point, index, dataset, datasetIndex){ - var controlPoints = helpers.splineCurve( - this.previousPoint(dataset, index), - point, - this.nextPoint(dataset, index), - point.tension - ); - - point.controlPointPreviousX = controlPoints.previous.x; - point.controlPointNextX = controlPoints.next.x; - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (controlPoints.next.y > this.scale.endPoint){ - point.controlPointNextY = this.scale.endPoint; - } - else if (controlPoints.next.y < this.scale.startPoint){ - point.controlPointNextY = this.scale.startPoint; - } - else{ - point.controlPointNextY = controlPoints.next.y; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (controlPoints.previous.y > this.scale.endPoint){ - point.controlPointPreviousY = this.scale.endPoint; - } - else if (controlPoints.previous.y < this.scale.startPoint){ - point.controlPointPreviousY = this.scale.startPoint; - } - else{ - point.controlPointPreviousY = controlPoints.previous.y; - } - // Now pivot the point for animation - point.pivot(); - }, this); - - this.render(); - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachValue(function(value){ - values.push(value); - }); - - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - offsetGridLines : this.options.offsetGridLines, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange : function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : this.data.labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.pointRadius + this.options.pointBorderWidth, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - this.scale = new Chart.Scale(scaleOptions); - }, - redraw : function(){ - - }, - draw : function(ease){ - - var easingDecimal = ease || 1; - this.clear(); - - this.scale.draw(easingDecimal); - - this.eachDataset(function(dataset, datasetIndex){ - - // Transition Point Locations - helpers.each(dataset.metaData, function(point, index){ - point.transition(easingDecimal); - },this); - - // Transition and Draw the line - dataset.metaDataset.transition(easingDecimal).draw(); - - // Draw the points - helpers.each(dataset.metaData,function(point){ - point.draw(); - }); - },this); - - // Finally draw the tooltip - this.tooltip.transition(easingDecimal).draw(); - } - }); + var root = this, + Chart = root.Chart, + helpers = Chart.helpers; + + var defaultConfig = { + + ///Boolean - Whether grid lines are shown across the chart + scaleShowGridLines: true, + + //String - Colour of the grid lines + scaleGridLineColor: "rgba(0,0,0,.05)", + + //Number - Width of the grid lines + scaleGridLineWidth: 1, + + //Boolean - Whether to show horizontal lines (except X axis) + scaleShowHorizontalLines: true, + + //Boolean - Whether to show vertical lines (except Y axis) + scaleShowVerticalLines: true, + + //Number - Tension of the bezier curve between points + tension: 0.4, + + //Number - Radius of each point dot in pixels + pointRadius: 4, + + //Number - Pixel width of point dot border + pointBorderWidth: 1, + + //Number - amount extra to add to the radius to cater for hit detection outside the drawn point + pointHoverRadius: 20, + + //Number - Pixel width of dataset border + borderWidth: 2, + + //String - A legend template + legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", + + //Boolean - Whether to horizontally center the label and point dot inside the grid + offsetGridLines: false + + }; + + + Chart.Type.extend({ + name: "Line", + defaults: defaultConfig, + initialize: function(data) { + // Save data as a source for updating of values & methods + this.data = data; + + //Custom Point Defaults + this.PointClass = Chart.Point.extend({ + _chart: this.chart, + offsetGridLines: this.options.offsetGridLines, + borderWidth: this.options.pointBorderWidth, + radius: this.options.pointRadius, + hoverRadius: this.options.pointHoverRadius, + }); + + // Events + helpers.bindEvents(this, this.options.tooltipEvents, this.onHover); + + // Build Scale + this.buildScale(this.data.labels); + + + //Create a new line and its points for each dataset and piece of data + helpers.each(this.data.datasets, function(dataset, datasetIndex) { + dataset.metaDataset = new Chart.Line(); + dataset.metaData = []; + helpers.each(dataset.data, function(dataPoint, index) { + dataset.metaData.push(new this.PointClass()); + }, this); + }, this); + + // Set defaults for lines + this.eachDataset(function(dataset, datasetIndex) { + dataset = helpers.merge(this.options, dataset); + helpers.extend(dataset.metaDataset, { + _points: dataset.metaData, + _datasetIndex: datasetIndex, + _chart: this.chart, + }); + // Copy to view model + dataset.metaDataset.save(); + }, this); + + // Set defaults for points + this.eachElement(function(point, index, dataset, datasetIndex) { + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.endPoint, + _datasetIndex: datasetIndex, + _index: index, + _chart: this.chart + }); + + // Default bezier control points + helpers.extend(point, { + controlPointPreviousX: this.previousPoint(dataset, index).x, + controlPointPreviousY: this.nextPoint(dataset, index).y, + controlPointNextX: this.previousPoint(dataset, index).x, + controlPointNextY: this.nextPoint(dataset, index).y, + }); + // Copy to view model + point.save(); + }, this); + + // Create tooltip instance exclusively for this chart with some defaults. + this.tooltip = new Chart.Tooltip({ + _chart: this.chart, + _data: this.data, + _options: this.options, + }, this); + + this.update(); + }, + nextPoint: function(collection, index) { + return collection[index - 1] || collection[index]; + }, + previousPoint: function(collection, index) { + return collection[index + 1] || collection[index]; + }, + onHover: function(e) { + + + // If exiting chart + if (e.type == 'mouseout') { + return this; + } + + this.lastActive = this.lastActive || []; + + // Find Active Elements + this.active = function() { + switch (this.options.hoverMode) { + case 'single': + return this.getElementAtEvent(e); + case 'label': + return this.getElementsAtEvent(e); + case 'dataset': + return this.getDatasetAtEvent(e); + default: + return e; + } + }.call(this); + + // On Hover hook + if (this.options.onHover) { + this.options.onHover.call(this, this.active); + } + + // Remove styling for last active (even if it may still be active) + if (this.lastActive.length) { + switch (this.options.hoverMode) { + case 'single': + this.lastActive[0].backgroundColor = this.data.datasets[this.lastActive[0]._datasetIndex].pointBackgroundColor; + this.lastActive[0].borderColor = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderColor; + this.lastActive[0].borderWidth = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderWidth; + break; + case 'label': + for (var i = 0; i < this.lastActive.length; i++) { + this.lastActive[i].backgroundColor = this.data.datasets[this.lastActive[i]._datasetIndex].pointBackgroundColor; + this.lastActive[i].borderColor = this.data.datasets[this.lastActive[i]._datasetIndex].pointBorderColor; + this.lastActive[i].borderWidth = this.data.datasets[this.lastActive[0]._datasetIndex].pointBorderWidth; + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + // Built in hover styling + if (this.active.length && this.options.hoverMode) { + switch (this.options.hoverMode) { + case 'single': + this.active[0].backgroundColor = this.data.datasets[this.active[0]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[0].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + this.active[0].borderColor = this.data.datasets[this.active[0]._datasetIndex].hoverBorderColor || helpers.color(this.active[0].borderColor).saturate(0.5).darken(0.35).rgbString(); + this.active[0].borderWidth = this.data.datasets[this.active[0]._datasetIndex].borderWidth + 10; + break; + case 'label': + for (var i = 0; i < this.active.length; i++) { + this.active[i].backgroundColor = this.data.datasets[this.active[i]._datasetIndex].hoverBackgroundColor || helpers.color(this.active[i].backgroundColor).saturate(0.5).darken(0.35).rgbString(); + this.active[i].borderColor = this.data.datasets[this.active[i]._datasetIndex].hoverBorderColor || helpers.color(this.active[i].borderColor).saturate(0.5).darken(0.35).rgbString(); + this.active[i].borderWidth = this.data.datasets[this.active[i]._datasetIndex].borderWidth + 2; + } + break; + case 'dataset': + break; + default: + // Don't change anything + } + } + + + // Built in Tooltips + if (this.options.showTooltips) { + + // The usual updates + this.tooltip.initialize(); + + // Active + if (this.active.length) { + helpers.extend(this.tooltip, { + opacity: 1, + _active: this.active, + }); + + this.tooltip.update(); + } else { + // Inactive + helpers.extend(this.tooltip, { + opacity: 0, + }); + } + } + + + // Hover animations + this.tooltip.pivot(); + + if (!this.animating) { + var changed; + + helpers.each(this.active, function(element, index) { + if (element !== this.lastActive[index]) { + changed = true; + } + }, this); + + // If entering, leaving, or changing elements, animate the change via pivot + if ((!this.lastActive.length && this.active.length) || + (this.lastActive.length && !this.active.length) || + (this.lastActive.length && this.active.length && changed)) { + + this.stop(); + this.render(this.options.hoverAnimationDuration); + } + } + + // Remember Last Active + this.lastActive = this.active; + return this; + + }, + update: function() { + + this.scale.update(); + + // Update the lines + this.eachDataset(function(dataset, datasetIndex) { + helpers.extend(dataset.metaDataset, { + backgroundColor: dataset.backgroundColor || this.options.backgroundColor, + borderWidth: dataset.borderWidth || this.options.borderWidth, + borderColor: dataset.borderColor || this.options.borderColor, + tension: dataset.tension || this.options.tension, + scaleTop: this.scale.startPoint, + scaleBottom: this.scale.endPoint, + _points: dataset.metaData, + _datasetIndex: datasetIndex, + }); + dataset.metaDataset.pivot(); + }); + + // Update the points + this.eachElement(function(point, index, dataset, datasetIndex) { + helpers.extend(point, { + x: this.scale.calculateX(index), + y: this.scale.calculateY(this.data.datasets[datasetIndex].data[index]), + value: this.data.datasets[datasetIndex].data[index], + label: this.data.labels[index], + datasetLabel: this.data.datasets[datasetIndex].label, + // Appearance + hoverBackgroundColor: this.data.datasets[datasetIndex].pointHoverBackgroundColor || this.options.pointHoverBackgroundColor, + hoverBorderColor: this.data.datasets[datasetIndex].pointHoverBorderColor || this.options.pointHoverBorderColor, + hoverRadius: this.data.datasets[datasetIndex].pointHoverRadius || this.options.pointHoverRadius, + radius: this.data.datasets[datasetIndex].pointRadius || this.options.pointRadius, + borderWidth: this.data.datasets[datasetIndex].pointBorderWidth || this.options.pointBorderWidth, + borderColor: this.data.datasets[datasetIndex].pointBorderColor || this.options.pointBorderColor, + backgroundColor: this.data.datasets[datasetIndex].pointBackgroundColor || this.options.pointBackgroundColor, + tension: this.data.datasets[datasetIndex].metaDataset.tension, + _datasetIndex: datasetIndex, + _index: index, + }); + }, this); + + // Update control points for the bezier curve + this.eachElement(function(point, index, dataset, datasetIndex) { + var controlPoints = helpers.splineCurve( + this.previousPoint(dataset, index), + point, + this.nextPoint(dataset, index), + point.tension + ); + + point.controlPointPreviousX = controlPoints.previous.x; + point.controlPointNextX = controlPoints.next.x; + + // Prevent the bezier going outside of the bounds of the graph + + // Cap puter bezier handles to the upper/lower scale bounds + if (controlPoints.next.y > this.scale.endPoint) { + point.controlPointNextY = this.scale.endPoint; + } else if (controlPoints.next.y < this.scale.startPoint) { + point.controlPointNextY = this.scale.startPoint; + } else { + point.controlPointNextY = controlPoints.next.y; + } + + // Cap inner bezier handles to the upper/lower scale bounds + if (controlPoints.previous.y > this.scale.endPoint) { + point.controlPointPreviousY = this.scale.endPoint; + } else if (controlPoints.previous.y < this.scale.startPoint) { + point.controlPointPreviousY = this.scale.startPoint; + } else { + point.controlPointPreviousY = controlPoints.previous.y; + } + // Now pivot the point for animation + point.pivot(); + }, this); + + this.render(); + }, + buildScale: function(labels) { + var self = this; + + var dataTotal = function() { + var values = []; + self.eachValue(function(value) { + values.push(value); + }); + + return values; + }; + + var scaleOptions = { + templateString: this.options.scaleLabel, + height: this.chart.height, + width: this.chart.width, + ctx: this.chart.ctx, + textColor: this.options.scaleFontColor, + offsetGridLines: this.options.offsetGridLines, + fontSize: this.options.scaleFontSize, + fontStyle: this.options.scaleFontStyle, + fontFamily: this.options.scaleFontFamily, + valuesCount: labels.length, + beginAtZero: this.options.scaleBeginAtZero, + integersOnly: this.options.scaleIntegersOnly, + calculateYRange: function(currentHeight) { + var updatedRanges = helpers.calculateScaleRange( + dataTotal(), + currentHeight, + this.fontSize, + this.beginAtZero, + this.integersOnly + ); + helpers.extend(this, updatedRanges); + }, + xLabels: this.data.labels, + font: helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), + lineWidth: this.options.scaleLineWidth, + lineColor: this.options.scaleLineColor, + showHorizontalLines: this.options.scaleShowHorizontalLines, + showVerticalLines: this.options.scaleShowVerticalLines, + gridLineWidth: (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, + gridLineColor: (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", + padding: (this.options.showScale) ? 0 : this.options.pointRadius + this.options.pointBorderWidth, + showLabels: this.options.scaleShowLabels, + display: this.options.showScale + }; + + if (this.options.scaleOverride) { + helpers.extend(scaleOptions, { + calculateYRange: helpers.noop, + steps: this.options.scaleSteps, + stepValue: this.options.scaleStepWidth, + min: this.options.scaleStartValue, + max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) + }); + } + + this.scale = new Chart.Scale(scaleOptions); + }, + redraw: function() { + + }, + draw: function(ease) { + + var easingDecimal = ease || 1; + this.clear(); + + this.scale.draw(easingDecimal); + + this.eachDataset(function(dataset, datasetIndex) { + + // Transition Point Locations + helpers.each(dataset.metaData, function(point, index) { + point.transition(easingDecimal); + }, this); + + // Transition and Draw the line + dataset.metaDataset.transition(easingDecimal).draw(); + + // Draw the points + helpers.each(dataset.metaData, function(point) { + point.draw(); + }); + }, this); + + // Finally draw the tooltip + this.tooltip.transition(easingDecimal).draw(); + } + }); }).call(this);