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