/*! * chart.js * http://chartjs.org/ * version: 1.0.2 * * copyright 2015 nick downie * released under the mit license * https://github.com/nnnick/chart.js/blob/master/license.md */ (function(){ "use strict"; //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; 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); } }; var width = this.width = computedimension(context.canvas,'width') || context.canvas.width; var height = this.height = computedimension(context.canvas,'height') || context.canvas.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); 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 animationsteps: 60, // string - animation easing effect animationeasing: "easeoutquart", // boolean - if we should show the scale at all showscale: true, // 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, // string - colour of the scale line scalelinecolor: "rgba(0,0,0,.1)", // number - pixel width of the scale line scalelinewidth: 1, // boolean - whether to show labels on the scale scaleshowlabels: true, // 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 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", // number - scale label font size in pixels scalefontsize: 12, // string - scale label font weight style scalefontstyle: "normal", // 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 to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container maintainaspectratio: 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, // array - array of string names to attach tooltip events tooltipevents: ["mousemove", "touchstart", "touchmove", "mouseout"], // string - tooltip background colour tooltipfillcolor: "rgba(0,0,0,0.8)", // 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", // string - tooltip title template tooltiptitletemplate: "<%= label%>", // 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: "<%= datasetlabel %>: <%= value %>", // string - colour behind the legend colour block multitooltipkeybackground: '#fff', // array - a list of colors to use as the defaults segmentcolordefault: ["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99", "#e31a1c", "#fdbf6f", "#ff7f00", "#cab2d6", "#6a3d9a", "#b4b482", "#b15928" ], // array - a list of highlight colors to use as the defaults segmenthighlightcolordefaults: [ "#cef6ff", "#47a0dc", "#daffb2", "#5bc854", "#ffc2c1", "#ff4244", "#ffe797", "#ffa728", "#f2dafe", "#9265c2", "#dcdcaa", "#d98150" ], // function - will fire on animation progression. onanimationprogress: function(){}, // function - will fire on animation completion. onanimationcomplete: function(){} } }; //create a dictionary of chart types, to allow for extension of existing types chart.types = {}; //global chart helpers object for utility methods and classes var helpers = chart.helpers = {}; //-- basic js utility methods var each = helpers.each = function(loopable,callback,self){ var additionalargs = array.prototype.slice.call(arguments, 3); // check to see if null or undefined firstly. if (loopable){ if (loopable.length === +loopable.length){ var i; for (i=0; i= 0; i--) { var currentitem = arraytosearch[i]; if (filtercallback(currentitem)){ return currentitem; } } }, inherits = helpers.inherits = function(extensions){ //basic javascript inheritance based on the model created in backbone.js var parent = this; var chartelement = (extensions && extensions.hasownproperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; var surrogate = function(){ this.constructor = chartelement;}; surrogate.prototype = parent.prototype; chartelement.prototype = new surrogate(); chartelement.extend = inherits; if (extensions) extend(chartelement.prototype, extensions); chartelement.__super__ = parent.prototype; return chartelement; }, noop = helpers.noop = function(){}, uid = helpers.uid = (function(){ var id=0; return function(){ return "chart-" + id++; }; })(), warn = helpers.warn = function(str){ //method for warning of errors if (window.console && typeof window.console.warn === "function") console.warn(str); }, amd = helpers.amd = (typeof define === 'function' && define.amd), //-- math methods isnumber = helpers.isnumber = function(n){ return !isnan(parsefloat(n)) && isfinite(n); }, max = helpers.max = function(array){ return math.max.apply( math, array ); }, min = helpers.min = function(array){ return math.min.apply( math, array ); }, cap = helpers.cap = function(valuetocap,maxvalue,minvalue){ if(isnumber(maxvalue)) { if( valuetocap > maxvalue ) { return maxvalue; } } else if(isnumber(minvalue)){ if ( valuetocap < minvalue ){ return minvalue; } } return valuetocap; }, getdecimalplaces = helpers.getdecimalplaces = function(num){ if (num%1!==0 && isnumber(num)){ var s = num.tostring(); if(s.indexof("e-") < 0){ // no exponent, e.g. 0.01 return s.split(".")[1].length; } else if(s.indexof(".") < 0) { // no decimal point, e.g. 1e-9 return parseint(s.split("e-")[1]); } else { // exponent and decimal point, e.g. 1.23e-9 var parts = s.split(".")[1].split("e-"); return parts[0].length + parseint(parts[1]); } } else { return 0; } }, toradians = helpers.radians = function(degrees){ return degrees * (math.pi/180); }, // gets the angle from vertical upright to the point about a centre. getanglefrompoint = helpers.getanglefrompoint = function(centrepoint, anglepoint){ var distancefromxcenter = anglepoint.x - centrepoint.x, distancefromycenter = anglepoint.y - centrepoint.y, radialdistancefromcenter = math.sqrt( distancefromxcenter * distancefromxcenter + distancefromycenter * distancefromycenter); var angle = math.pi * 2 + math.atan2(distancefromycenter, distancefromxcenter); //if the segment is in the top left quadrant, we need to add another rotation to the angle if (distancefromxcenter < 0 && distancefromycenter < 0){ angle += math.pi*2; } return { angle: angle, distance: radialdistancefromcenter }; }, aliaspixel = helpers.aliaspixel = function(pixelwidth){ return (pixelwidth % 2 === 0) ? 0 : 0.5; }, splinecurve = helpers.splinecurve = function(firstpoint,middlepoint,afterpoint,t){ //props to rob spencer at scaled innovation for his post on splining between points //http://scaledinnovation.com/analytics/splines/aboutsplines.html var d01=math.sqrt(math.pow(middlepoint.x-firstpoint.x,2)+math.pow(middlepoint.y-firstpoint.y,2)), d12=math.sqrt(math.pow(afterpoint.x-middlepoint.x,2)+math.pow(afterpoint.y-middlepoint.y,2)), fa=t*d01/(d01+d12),// scaling factor for triangle ta fb=t*d12/(d01+d12); return { inner : { x : middlepoint.x-fa*(afterpoint.x-firstpoint.x), y : middlepoint.y-fa*(afterpoint.y-firstpoint.y) }, outer : { 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); // filter out null values since these would min() to zero var values = []; each(valuesarray, function( v ){ v == null || values.push( v ); }); var minvalue = min(values), maxvalue = max(values); // we need some degree of separation 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 ? container.clientwidth - padding : 0; }, 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 ? container.clientheight - padding : 0; }, 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(); }; //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; }, reflow : noop, render : function(reflow){ if (reflow){ this.reflow(); } if (this.options.animation && !reflow){ var animation = new chart.animation(); animation.numsteps = this.options.animationsteps; 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); } else{ this.draw(); this.options.onanimationcomplete.call(this); } return this; }, 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]; }, showtooltip : function(chartelements, forceredraw){ // only redraw the chart if we've actually changed what we're hovering on. if (typeof this.activeelements === 'undefined') this.activeelements = []; var ischanged = (function(elements){ var changed = false; if (elements.length !== this.activeelements.length){ changed = true; return changed; } each(elements, function(element, index){ if (element !== this.activeelements[index]){ changed = true; } }, this); return changed; }).call(this, chartelements); if (!ischanged && !forceredraw){ return; } else{ this.activeelements = chartelements; } this.draw(); if(this.options.customtooltips){ this.options.customtooltips(false); } if (chartelements.length > 0){ // if we have multiple datasets, show a multitooltip for all of the data points at that index if (this.datasets && this.datasets.length > 1) { var dataarray, dataindex; for (var i = this.datasets.length - 1; i >= 0; i--) { dataarray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; dataindex = indexof(dataarray, chartelements[0]); if (dataindex !== -1){ break; } } var tooltiplabels = [], tooltipcolors = [], medianposition = (function(index) { // get all the points at that particular index var elements = [], datacollection, xpositions = [], ypositions = [], xmax, ymax, xmin, ymin; helpers.each(this.datasets, function(dataset){ datacollection = dataset.points || dataset.bars || dataset.segments; if (datacollection[dataindex] && datacollection[dataindex].hasvalue()){ elements.push(datacollection[dataindex]); } }); helpers.each(elements, function(element) { xpositions.push(element.x); ypositions.push(element.y); //include any colour information about the element tooltiplabels.push(helpers.template(this.options.multitooltiptemplate, element)); tooltipcolors.push({ fill: element._saved.fillcolor || element.fillcolor, stroke: element._saved.strokecolor || element.strokecolor }); }, 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); new chart.multitooltip({ x: medianposition.x, y: medianposition.y, xpadding: this.options.tooltipxpadding, ypadding: this.options.tooltipypadding, xoffset: this.options.tooltipxoffset, fillcolor: this.options.tooltipfillcolor, textcolor: this.options.tooltipfontcolor, fontfamily: this.options.tooltipfontfamily, fontstyle: this.options.tooltipfontstyle, fontsize: this.options.tooltipfontsize, titletextcolor: this.options.tooltiptitlefontcolor, titlefontfamily: this.options.tooltiptitlefontfamily, titlefontstyle: this.options.tooltiptitlefontstyle, titlefontsize: this.options.tooltiptitlefontsize, cornerradius: this.options.tooltipcornerradius, labels: tooltiplabels, legendcolors: tooltipcolors, legendcolorbackground : this.options.multitooltipkeybackground, title: template(this.options.tooltiptitletemplate,chartelements[0]), chart: this.chart, ctx: this.chart.ctx, custom: this.options.customtooltips }).draw(); } else { each(chartelements, function(element) { var tooltipposition = element.tooltipposition(); new chart.tooltip({ x: math.round(tooltipposition.x), y: math.round(tooltipposition.y), xpadding: this.options.tooltipxpadding, ypadding: this.options.tooltipypadding, fillcolor: this.options.tooltipfillcolor, textcolor: this.options.tooltipfontcolor, fontfamily: this.options.tooltipfontfamily, fontstyle: this.options.tooltipfontstyle, fontsize: this.options.tooltipfontsize, caretheight: this.options.tooltipcaretsize, cornerradius: this.options.tooltipcornerradius, text: template(this.options.tooltiptemplate, element), chart: this.chart, custom: this.options.customtooltips }).draw(); }, this); } } return this; }, 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,configuration); this.initialize.apply(this,arguments); this.save(); }; extend(chart.element.prototype,{ initialize : function(){}, restore : function(props){ if (!props){ extend(this,this._saved); } else { each(props,function(key){ this[key] = this._saved[key]; },this); } return this; }, save : function(){ this._saved = clone(this); delete this._saved._saved; return this; }, update : function(newprops){ each(newprops,function(value,key){ this._saved[key] = this[key]; this[key] = value; },this); return this; }, transition : function(props,ease){ each(props,function(value,key){ this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; },this); 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({ display: true, inrange: function(chartx,charty){ var hitdetectionrange = this.hitdetectionradius + this.radius; return ((math.pow(chartx-this.x, 2)+math.pow(charty-this.y, 2)) < math.pow(hitdetectionrange,2)); }, draw : function(){ if (this.display){ var ctx = this.ctx; ctx.beginpath(); ctx.arc(this.x, this.y, this.radius, 0, math.pi*2); ctx.closepath(); ctx.strokestyle = this.strokecolor; ctx.linewidth = this.strokewidth; ctx.fillstyle = this.fillcolor; ctx.fill(); ctx.stroke(); } //quick debug for bezier curve splining //highlights control points and the line between them. //handy for dev - stripped in the min version. // ctx.save(); // ctx.fillstyle = "black"; // ctx.strokestyle = "black" // ctx.beginpath(); // ctx.arc(this.controlpoints.inner.x,this.controlpoints.inner.y, 2, 0, math.pi*2); // ctx.fill(); // ctx.beginpath(); // ctx.arc(this.controlpoints.outer.x,this.controlpoints.outer.y, 2, 0, math.pi*2); // ctx.fill(); // ctx.moveto(this.controlpoints.inner.x,this.controlpoints.inner.y); // ctx.lineto(this.x, this.y); // ctx.lineto(this.controlpoints.outer.x,this.controlpoints.outer.y); // ctx.stroke(); // ctx.restore(); } }); chart.arc = chart.element.extend({ inrange : function(chartx,charty){ var pointrelativeposition = helpers.getanglefrompoint(this, { x: chartx, y: charty }); // normalize all angles to 0 - 2*pi (0 - 360°) var pointrelativeangle = pointrelativeposition.angle % (math.pi * 2), startangle = (math.pi * 2 + this.startangle) % (math.pi * 2), endangle = (math.pi * 2 + this.endangle) % (math.pi * 2) || 360; // calculate wether the pointrelativeangle is between the start and the end angle var betweenangles = (endangle < startangle) ? pointrelativeangle <= endangle || pointrelativeangle >= startangle: pointrelativeangle >= startangle && pointrelativeangle <= endangle; //check if within the range of the open/close angle var 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 < 0 ? 0 : this.outerradius, this.startangle, this.endangle); ctx.arc(this.x, this.y, this.innerradius < 0 ? 0 : this.innerradius, this.endangle, this.startangle, true); ctx.closepath(); ctx.strokestyle = this.strokecolor; ctx.linewidth = this.strokewidth; ctx.fillstyle = this.fillcolor; ctx.fill(); ctx.linejoin = 'bevel'; if (this.showstroke){ ctx.stroke(); } } }); chart.rectangle = chart.element.extend({ draw : function(){ var ctx = this.ctx, halfwidth = this.width/2, leftx = this.x - halfwidth, rightx = this.x + halfwidth, top = this.base - (this.base - this.y), halfstroke = this.strokewidth / 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 (this.showstroke){ leftx += halfstroke; rightx -= halfstroke; top += halfstroke; } ctx.beginpath(); ctx.fillstyle = this.fillcolor; ctx.strokestyle = this.strokecolor; ctx.linewidth = this.strokewidth; // it'd be nice to keep this class totally generic to any rectangle // and simply specify which border to miss out. ctx.moveto(leftx, this.base); ctx.lineto(leftx, top); ctx.lineto(rightx, top); ctx.lineto(rightx, this.base); ctx.fill(); if (this.showstroke){ ctx.stroke(); } }, height : function(){ return this.base - this.y; }, inrange : function(chartx,charty){ return (chartx >= this.x - this.width/2 && chartx <= this.x + this.width/2) && (charty >= this.y && charty <= this.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({ draw : function(){ var ctx = this.chart.ctx; ctx.font = fontstring(this.fontsize,this.fontstyle,this.fontfamily); this.xalign = "center"; this.yalign = "above"; //distance between the actual element.y position and the start of the tooltip caret var caretpadding = this.caretpadding = 2; var tooltipwidth = ctx.measuretext(this.text).width + 2*this.xpadding, tooltiprectheight = this.fontsize + 2*this.ypadding, tooltipheight = tooltiprectheight + this.caretheight + caretpadding; if (this.x + tooltipwidth/2 >this.chart.width){ this.xalign = "left"; } else if (this.x - tooltipwidth/2 < 0){ this.xalign = "right"; } if (this.y - tooltipheight < 0){ this.yalign = "below"; } var tooltipx = this.x - tooltipwidth/2, tooltipy = this.y - tooltipheight; ctx.fillstyle = this.fillcolor; // custom tooltips if(this.custom){ this.custom(this); } else{ switch(this.yalign) { case "above": //draw a caret above the x/y ctx.beginpath(); ctx.moveto(this.x,this.y - caretpadding); ctx.lineto(this.x + this.caretheight, this.y - (caretpadding + this.caretheight)); ctx.lineto(this.x - this.caretheight, this.y - (caretpadding + this.caretheight)); ctx.closepath(); ctx.fill(); break; case "below": tooltipy = this.y + caretpadding + this.caretheight; //draw a caret below the x/y ctx.beginpath(); ctx.moveto(this.x, this.y + caretpadding); ctx.lineto(this.x + this.caretheight, this.y + caretpadding + this.caretheight); ctx.lineto(this.x - this.caretheight, this.y + caretpadding + this.caretheight); ctx.closepath(); ctx.fill(); break; } switch(this.xalign) { case "left": tooltipx = this.x - tooltipwidth + (this.cornerradius + this.caretheight); break; case "right": tooltipx = this.x - (this.cornerradius + this.caretheight); break; } drawroundedrectangle(ctx,tooltipx,tooltipy,tooltipwidth,tooltiprectheight,this.cornerradius); ctx.fill(); ctx.fillstyle = this.textcolor; ctx.textalign = "center"; ctx.textbaseline = "middle"; ctx.filltext(this.text, tooltipx + tooltipwidth/2, tooltipy + tooltiprectheight/2); } } }); chart.multitooltip = chart.element.extend({ initialize : function(){ this.font = fontstring(this.fontsize,this.fontstyle,this.fontfamily); this.titlefont = fontstring(this.titlefontsize,this.titlefontstyle,this.titlefontfamily); this.titleheight = this.title ? this.titlefontsize * 1.5 : 0; this.height = (this.labels.length * this.fontsize) + ((this.labels.length-1) * (this.fontsize/2)) + (this.ypadding*2) + this.titleheight; this.ctx.font = this.titlefont; var titlewidth = this.ctx.measuretext(this.title).width, //label has a legend square as well so account for this. labelwidth = longesttext(this.ctx,this.font,this.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; } }, getlineheight : function(index){ var baselineheight = this.y - (this.height/2) + this.ypadding, aftertitleindex = index-1; //if the index is zero, we're getting the title if (index === 0){ return baselineheight + this.titleheight / 3; } else{ return baselineheight + ((this.fontsize * 1.5 * aftertitleindex) + this.fontsize / 2) + this.titleheight; } }, draw : function(){ // custom tooltips if(this.custom){ this.custom(this); } else{ drawroundedrectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerradius); var ctx = this.ctx; ctx.fillstyle = this.fillcolor; ctx.fill(); ctx.closepath(); ctx.textalign = "left"; ctx.textbaseline = "middle"; ctx.fillstyle = this.titletextcolor; ctx.font = this.titlefont; ctx.filltext(this.title,this.x + this.xpadding, this.getlineheight(0)); ctx.font = this.font; helpers.each(this.labels,function(label,index){ ctx.fillstyle = this.textcolor; ctx.filltext(label,this.x + this.xpadding + this.fontsize + 3, this.getlineheight(index + 1)); //a bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) //ctx.clearrect(this.x + this.xpadding, this.getlineheight(index + 1) - this.fontsize/2, this.fontsize, this.fontsize); //instead we'll make a white filled block to put the legendcolour palette over. ctx.fillstyle = this.legendcolorbackground; ctx.fillrect(this.x + this.xpadding, this.getlineheight(index + 1) - this.fontsize/2, this.fontsize, this.fontsize); ctx.fillstyle = this.legendcolors[index].fill; ctx.fillrect(this.x + this.xpadding, this.getlineheight(index + 1) - this.fontsize/2, this.fontsize, this.fontsize); },this); } } }); 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.stepvalue; this.min; this.max; * */ this.calculateyrange(cachedheight); // with these properties set we can now build the array of ylabels // and also the width of the largest ylabel this.buildylabels(); this.calculatexlabelrotation(); while((cachedheight > this.endpoint - this.startpoint)){ cachedheight = this.endpoint - this.startpoint; cachedylabelwidth = this.ylabelwidth; this.calculateyrange(cachedheight); this.buildylabels(); // only go through the xlabel loop again if the ylabel width has changed if (cachedylabelwidth < this.ylabelwidth){ this.endpoint = cachedendpoint; this.calculatexlabelrotation(); } } }, calculatexlabelrotation : function(){ //get the width of each grid by calculating the difference //between x offsets between 0 and 1. this.ctx.font = this.font; var firstwidth = this.ctx.measuretext(this.xlabels[0]).width, lastwidth = this.ctx.measuretext(this.xlabels[this.xlabels.length - 1]).width, firstrotated, lastrotated; this.xscalepaddingright = lastwidth/2 + 3; this.xscalepaddingleft = (firstwidth/2 > this.ylabelwidth) ? firstwidth/2 : this.ylabelwidth; this.xlabelrotation = 0; if (this.display){ var originallabelwidth = longesttext(this.ctx,this.font,this.xlabels), cosrotation, firstrotatedwidth; this.xlabelwidth = originallabelwidth; //allow 3 pixels x2 padding either side for label readability var xgridwidth = math.floor(this.calculatex(1) - this.calculatex(0)) - 6; //max label rotate should be 90 - also act as a loop counter while ((this.xlabelwidth > xgridwidth && this.xlabelrotation === 0) || (this.xlabelwidth > xgridwidth && this.xlabelrotation <= 90 && this.xlabelrotation > 0)){ cosrotation = math.cos(toradians(this.xlabelrotation)); firstrotated = cosrotation * firstwidth; lastrotated = cosrotation * lastwidth; // we're right aligning the text now. if (firstrotated + this.fontsize / 2 > this.ylabelwidth){ this.xscalepaddingleft = firstrotated + this.fontsize / 2; } this.xscalepaddingright = this.fontsize/2; this.xlabelrotation++; this.xlabelwidth = cosrotation * originallabelwidth; } if (this.xlabelrotation > 0){ this.endpoint -= math.sin(toradians(this.xlabelrotation))*originallabelwidth + 3; } } else{ this.xlabelwidth = 0; this.xscalepaddingright = this.padding; this.xscalepaddingleft = this.padding; } }, // needs to be overidden in each chart type // otherwise we need to pass all the data into the scale class calculateyrange: noop, drawingarea: function(){ return this.startpoint - this.endpoint; }, calculatey : function(value){ var scalingfactor = this.drawingarea() / (this.min - this.max); return this.endpoint - (scalingfactor * (value - this.min)); }, calculatex : function(index){ var isrotated = (this.xlabelrotation > 0), // innerwidth = (this.offsetgridlines) ? this.width - offsetleft - this.padding : this.width - (offsetleft + halflabelwidth * 2) - this.padding, innerwidth = this.width - (this.xscalepaddingleft + this.xscalepaddingright), valuewidth = innerwidth/math.max((this.valuescount - ((this.offsetgridlines) ? 0 : 1)), 1), valueoffset = (valuewidth * index) + this.xscalepaddingleft; if (this.offsetgridlines){ valueoffset += (valuewidth/2); } return math.round(valueoffset); }, update : function(newprops){ helpers.extend(this, newprops); this.fit(); }, draw : function(){ var ctx = this.ctx, ylabelgap = (this.endpoint - this.startpoint) / this.steps, xstart = math.round(this.xscalepaddingleft); if (this.display){ ctx.fillstyle = this.textcolor; ctx.font = this.font; each(this.ylabels,function(labelstring,index){ var ylabelcenter = this.endpoint - (ylabelgap * index), linepositiony = math.round(ylabelcenter), drawhorizontalline = this.showhorizontallines; ctx.textalign = "right"; ctx.textbaseline = "middle"; if (this.showlabels){ ctx.filltext(labelstring,xstart - 10,ylabelcenter); } // this is x axis, so draw it if (index === 0 && !drawhorizontalline){ drawhorizontalline = true; } if (drawhorizontalline){ ctx.beginpath(); } if (index > 0){ // this is a grid line in the centre, so drop that ctx.linewidth = this.gridlinewidth; ctx.strokestyle = this.gridlinecolor; } else { // this is the first line on the scale ctx.linewidth = this.linewidth; ctx.strokestyle = this.linecolor; } linepositiony += helpers.aliaspixel(ctx.linewidth); if(drawhorizontalline){ ctx.moveto(xstart, linepositiony); ctx.lineto(this.width, linepositiony); ctx.stroke(); ctx.closepath(); } ctx.linewidth = this.linewidth; ctx.strokestyle = this.linecolor; ctx.beginpath(); ctx.moveto(xstart - 5, linepositiony); ctx.lineto(xstart, linepositiony); ctx.stroke(); ctx.closepath(); },this); each(this.xlabels,function(label,index){ var xpos = this.calculatex(index) + aliaspixel(this.linewidth), // check to see if line/bar here and decide where to place the line linepos = this.calculatex(index - (this.offsetgridlines ? 0.5 : 0)) + aliaspixel(this.linewidth), isrotated = (this.xlabelrotation > 0), drawverticalline = this.showverticallines; // this is y axis, so draw it if (index === 0 && !drawverticalline){ drawverticalline = true; } if (drawverticalline){ ctx.beginpath(); } if (index > 0){ // this is a grid line in the centre, so drop that ctx.linewidth = this.gridlinewidth; ctx.strokestyle = this.gridlinecolor; } else { // this is the first line on the scale ctx.linewidth = this.linewidth; ctx.strokestyle = this.linecolor; } if (drawverticalline){ ctx.moveto(linepos,this.endpoint); ctx.lineto(linepos,this.startpoint - 3); ctx.stroke(); ctx.closepath(); } ctx.linewidth = this.linewidth; ctx.strokestyle = this.linecolor; // small lines at the bottom of the base grid line ctx.beginpath(); ctx.moveto(linepos,this.endpoint); ctx.lineto(linepos,this.endpoint + 5); ctx.stroke(); ctx.closepath(); ctx.save(); ctx.translate(xpos,(isrotated) ? this.endpoint + 12 : this.endpoint + 8); ctx.rotate(toradians(this.xlabelrotation)*-1); ctx.font = this.font; ctx.textalign = (isrotated) ? "right" : "center"; ctx.textbaseline = (isrotated) ? "middle" : "top"; ctx.filltext(label, 0, 0); ctx.restore(); },this); } } }); chart.radialscale = chart.element.extend({ initialize: function(){ this.size = min([this.height, this.width]); this.drawingarea = (this.display) ? (this.size/2) - (this.fontsize/2 + this.backdroppaddingy) : (this.size/2); }, calculatecenteroffset: function(value){ // take into account half font size + the ypadding of the top value var scalingfactor = this.drawingarea / (this.max - this.min); return (value - this.min) * scalingfactor; }, update : function(){ if (!this.linearc){ this.setscalesize(); } else { this.drawingarea = (this.display) ? (this.size/2) - (this.fontsize/2 + this.backdroppaddingy) : (this.size/2); } this.buildylabels(); }, buildylabels: function(){ this.ylabels = []; var stepdecimalplaces = getdecimalplaces(this.stepvalue); for (var i=0; i<=this.steps; i++){ this.ylabels.push(template(this.templatestring,{value:(this.min + (i * this.stepvalue)).tofixed(stepdecimalplaces)})); } }, getcircumference : function(){ return ((math.pi*2) / this.valuescount); }, setscalesize: function(){ /* * right, this is really confusing and there is a lot of maths going on here * the gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 * * reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif * * solution: * * we assume the radius of the polygon is half the size of the canvas at first * at each index we check if the text overlaps. * * where it does, we store that angle and that index. * * after finding the largest index and angle we calculate how much we need to remove * from the shape radius to move the point inwards by that x. * * we average the left and right distances to get the maximum shape radius that can fit in the box * along with labels. * * once we have that, we can find the centre point for the chart, by taking the x text protrusion * on each side, removing that from the size, halving it and adding the left x protrusion width. * * this will mean we have a shape fitted to the canvas, as large as it can be with the labels * and position it in the most space efficient manner * * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif */ // get maximum radius of the polygon. either half the height (minus the text width) or half the width. // use this to calculate the offset + change. - make sure l/r protrusion is at least 0 to stop issues with centre points var largestpossibleradius = min([(this.height/2 - this.pointlabelfontsize - 5), this.width/2]), pointposition, i, textwidth, halftextwidth, furthestright = this.width, furthestrightindex, furthestrightangle, furthestleft = 0, furthestleftindex, furthestleftangle, xprotrusionleft, xprotrusionright, radiusreductionright, radiusreductionleft, maxwidthradius; this.ctx.font = fontstring(this.pointlabelfontsize,this.pointlabelfontstyle,this.pointlabelfontfamily); for (i=0;i furthestright) { furthestright = pointposition.x + halftextwidth; furthestrightindex = i; } if (pointposition.x - halftextwidth < furthestleft) { furthestleft = pointposition.x - halftextwidth; furthestleftindex = i; } } else if (i < this.valuescount/2) { // less than half the values means we'll left align the text if (pointposition.x + textwidth > furthestright) { furthestright = pointposition.x + textwidth; furthestrightindex = i; } } else if (i > this.valuescount/2){ // more than half the values means we'll right align the text if (pointposition.x - textwidth < furthestleft) { furthestleft = pointposition.x - textwidth; furthestleftindex = i; } } } xprotrusionleft = furthestleft; xprotrusionright = math.ceil(furthestright - this.width); furthestrightangle = this.getindexangle(furthestrightindex); furthestleftangle = this.getindexangle(furthestleftindex); radiusreductionright = xprotrusionright / math.sin(furthestrightangle + math.pi/2); radiusreductionleft = xprotrusionleft / math.sin(furthestleftangle + math.pi/2); // ensure we actually need to reduce the size of the chart radiusreductionright = (isnumber(radiusreductionright)) ? radiusreductionright : 0; radiusreductionleft = (isnumber(radiusreductionleft)) ? radiusreductionleft : 0; this.drawingarea = largestpossibleradius - (radiusreductionleft + radiusreductionright)/2; //this.drawingarea = min([maxwidthradius, (this.height - (2 * (this.pointlabelfontsize + 5)))/2]) this.setcenterpoint(radiusreductionleft, radiusreductionright); }, setcenterpoint: function(leftmovement, rightmovement){ var maxright = this.width - rightmovement - this.drawingarea, maxleft = leftmovement + this.drawingarea; this.xcenter = (maxleft + maxright)/2; // always vertically in the centre as the text height doesn't change this.ycenter = (this.height/2); }, getindexangle : function(index){ var anglemultiplier = (math.pi * 2) / this.valuescount; // start from the top instead of right, so remove a quarter of the circle return index * anglemultiplier - (math.pi/2); }, getpointposition : function(index, distancefromcenter){ var thisangle = this.getindexangle(index); return { x : (math.cos(thisangle) * distancefromcenter) + this.xcenter, y : (math.sin(thisangle) * distancefromcenter) + this.ycenter }; }, draw: function(){ if (this.display){ var ctx = this.ctx; each(this.ylabels, function(label, index){ // don't draw a centre value if (index > 0){ var ycenteroffset = index * (this.drawingarea/this.steps), yheight = this.ycenter - ycenteroffset, pointposition; // draw circular lines around the scale if (this.linewidth > 0){ ctx.strokestyle = this.linecolor; ctx.linewidth = this.linewidth; if(this.linearc){ ctx.beginpath(); ctx.arc(this.xcenter, this.ycenter, ycenteroffset, 0, math.pi*2); ctx.closepath(); ctx.stroke(); } else{ ctx.beginpath(); for (var i=0;i= 0; i--) { var centeroffset = null, outerposition = null; if (this.anglelinewidth > 0){ centeroffset = this.calculatecenteroffset(this.max); outerposition = this.getpointposition(i, centeroffset); ctx.beginpath(); ctx.moveto(this.xcenter, this.ycenter); ctx.lineto(outerposition.x, outerposition.y); ctx.stroke(); ctx.closepath(); } if (this.backgroundcolors && this.backgroundcolors.length == this.valuescount) { if (centeroffset == null) centeroffset = this.calculatecenteroffset(this.max); if (outerposition == null) outerposition = this.getpointposition(i, centeroffset); var previousouterposition = this.getpointposition(i === 0 ? this.valuescount - 1 : i - 1, centeroffset); var nextouterposition = this.getpointposition(i === this.valuescount - 1 ? 0 : i + 1, centeroffset); var previousouterhalfway = { x: (previousouterposition.x + outerposition.x) / 2, y: (previousouterposition.y + outerposition.y) / 2 }; var nextouterhalfway = { x: (outerposition.x + nextouterposition.x) / 2, y: (outerposition.y + nextouterposition.y) / 2 }; ctx.beginpath(); ctx.moveto(this.xcenter, this.ycenter); ctx.lineto(previousouterhalfway.x, previousouterhalfway.y); ctx.lineto(outerposition.x, outerposition.y); ctx.lineto(nextouterhalfway.x, nextouterhalfway.y); ctx.fillstyle = this.backgroundcolors[i]; ctx.fill(); 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) { 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); } }, // 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); // check if executed the last frame. if (this.animations[i].animationobject.currentstep == this.animations[i].animationobject.numsteps){ // call onanimationcomplete this.animations[i].animationobject.onanimationcomplete.call(this.animations[i].chartinstance); // remove the animation. 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('chart', [], 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); (function(){ "use strict"; var root = this, chart = root.chart, helpers = chart.helpers; var defaultconfig = { //boolean - whether the scale should start at zero, or an order of magnitude down from the lowest value scalebeginatzero : true, //boolean - whether grid lines are shown across the chart scaleshowgridlines : true, //string - colour of the grid lines scalegridlinecolor : "rgba(0,0,0,.05)", //number - width of the grid lines scalegridlinewidth : 1, //boolean - whether to show horizontal lines (except x axis) scaleshowhorizontallines: true, //boolean - whether to show vertical lines (except y axis) scaleshowverticallines: true, //boolean - if there is a stroke on each bar barshowstroke : true, //number - pixel width of the bar stroke barstrokewidth : 2, //number - spacing between each of the x value sets barvaluespacing : 5, //number - spacing between data sets within x values bardatasetspacing : 1, //string - a legend template legendtemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" }; chart.type.extend({ name: "bar", defaults : defaultconfig, initialize: function(data){ //expose options as a scope variable here so we can access it in the scaleclass var options = this.options; this.scaleclass = chart.scale.extend({ offsetgridlines : true, calculatebarx : function(datasetcount, datasetindex, barindex){ //reusable method for calculating the xposition of a given bar based on datasetindex & width of the bar var xwidth = this.calculatebasewidth(), xabsolute = this.calculatex(barindex) - (xwidth/2), barwidth = this.calculatebarwidth(datasetcount); return xabsolute + (barwidth * datasetindex) + (datasetindex * options.bardatasetspacing) + barwidth/2; }, calculatebasewidth : function(){ return (this.calculatex(1) - this.calculatex(0)) - (2*options.barvaluespacing); }, calculatebarwidth : function(datasetcount){ //the padding between datasets is to the right of each bar, providing that there are more than 1 dataset var basewidth = this.calculatebasewidth() - ((datasetcount - 1) * options.bardatasetspacing); return (basewidth / datasetcount); } }); this.datasets = []; //set up tooltip events on the chart if (this.options.showtooltips){ helpers.bindevents(this, this.options.tooltipevents, function(evt){ var activebars = (evt.type !== 'mouseout') ? this.getbarsatevent(evt) : []; this.eachbars(function(bar){ bar.restore(['fillcolor', 'strokecolor']); }); helpers.each(activebars, function(activebar){ activebar.fillcolor = activebar.highlightfill; activebar.strokecolor = activebar.highlightstroke; }); this.showtooltip(activebars); }); } //declare the extension of the default point, to cater for the options passed in to the constructor this.barclass = chart.rectangle.extend({ strokewidth : this.options.barstrokewidth, showstroke : this.options.barshowstroke, ctx : this.chart.ctx }); //iterate through each of the datasets, and build this into a property of the chart helpers.each(data.datasets,function(dataset,datasetindex){ var datasetobject = { label : dataset.label || null, fillcolor : dataset.fillcolor, strokecolor : dataset.strokecolor, bars : [] }; this.datasets.push(datasetobject); helpers.each(dataset.data,function(datapoint,index){ //add a new point for each piece of data, passing any required data to draw. datasetobject.bars.push(new this.barclass({ value : datapoint, label : data.labels[index], datasetlabel: dataset.label, strokecolor : dataset.strokecolor, fillcolor : dataset.fillcolor, highlightfill : dataset.highlightfill || dataset.fillcolor, highlightstroke : dataset.highlightstroke || dataset.strokecolor })); },this); },this); this.buildscale(data.labels); this.barclass.prototype.base = this.scale.endpoint; this.eachbars(function(bar, index, datasetindex){ helpers.extend(bar, { width : this.scale.calculatebarwidth(this.datasets.length), x: this.scale.calculatebarx(this.datasets.length, datasetindex, index), y: this.scale.endpoint }); bar.save(); }, this); this.render(); }, update : function(){ this.scale.update(); // reset any highlight colours before updating. helpers.each(this.activeelements, function(activeelement){ activeelement.restore(['fillcolor', 'strokecolor']); }); this.eachbars(function(bar){ bar.save(); }); this.render(); }, eachbars : function(callback){ helpers.each(this.datasets,function(dataset, datasetindex){ helpers.each(dataset.bars, callback, this, datasetindex); },this); }, getbarsatevent : function(e){ var barsarray = [], eventposition = helpers.getrelativeposition(e), datasetiterator = function(dataset){ barsarray.push(dataset.bars[barindex]); }, barindex; for (var datasetindex = 0; datasetindex < this.datasets.length; datasetindex++) { for (barindex = 0; barindex < this.datasets[datasetindex].bars.length; barindex++) { if (this.datasets[datasetindex].bars[barindex].inrange(eventposition.x,eventposition.y)){ helpers.each(this.datasets, datasetiterator); return barsarray; } } } return barsarray; }, buildscale : function(labels){ var self = this; var datatotal = function(){ var values = []; self.eachbars(function(bar){ values.push(bar.value); }); return values; }; var scaleoptions = { templatestring : this.options.scalelabel, height : this.chart.height, width : this.chart.width, ctx : this.chart.ctx, textcolor : this.options.scalefontcolor, fontsize : this.options.scalefontsize, fontstyle : this.options.scalefontstyle, fontfamily : this.options.scalefontfamily, valuescount : labels.length, beginatzero : this.options.scalebeginatzero, integersonly : this.options.scaleintegersonly, calculateyrange: function(currentheight){ var updatedranges = helpers.calculatescalerange( datatotal(), currentheight, this.fontsize, this.beginatzero, this.integersonly ); helpers.extend(this, updatedranges); }, xlabels : labels, font : helpers.fontstring(this.options.scalefontsize, this.options.scalefontstyle, this.options.scalefontfamily), linewidth : this.options.scalelinewidth, linecolor : this.options.scalelinecolor, showhorizontallines : this.options.scaleshowhorizontallines, showverticallines : this.options.scaleshowverticallines, gridlinewidth : (this.options.scaleshowgridlines) ? this.options.scalegridlinewidth : 0, gridlinecolor : (this.options.scaleshowgridlines) ? this.options.scalegridlinecolor : "rgba(0,0,0,0)", padding : (this.options.showscale) ? 0 : (this.options.barshowstroke) ? this.options.barstrokewidth : 0, showlabels : this.options.scaleshowlabels, display : this.options.showscale }; if (this.options.scaleoverride){ helpers.extend(scaleoptions, { calculateyrange: helpers.noop, steps: this.options.scalesteps, stepvalue: this.options.scalestepwidth, min: this.options.scalestartvalue, max: this.options.scalestartvalue + (this.options.scalesteps * this.options.scalestepwidth) }); } this.scale = new this.scaleclass(scaleoptions); }, adddata : function(valuesarray,label){ //map the values array for each of the datasets helpers.each(valuesarray,function(value,datasetindex){ //add a new point for each piece of data, passing any required data to draw. this.datasets[datasetindex].bars.push(new this.barclass({ value : value, label : label, datasetlabel: this.datasets[datasetindex].label, x: this.scale.calculatebarx(this.datasets.length, datasetindex, this.scale.valuescount+1), y: this.scale.endpoint, width : this.scale.calculatebarwidth(this.datasets.length), base : this.scale.endpoint, strokecolor : this.datasets[datasetindex].strokecolor, fillcolor : this.datasets[datasetindex].fillcolor })); },this); this.scale.addxlabel(label); //then re-render the chart. this.update(); }, removedata : function(){ this.scale.removexlabel(); //then re-render the chart. helpers.each(this.datasets,function(dataset){ dataset.bars.shift(); },this); this.update(); }, reflow : function(){ helpers.extend(this.barclass.prototype,{ y: this.scale.endpoint, base : this.scale.endpoint }); var newscaleprops = helpers.extend({ height : this.chart.height, width : this.chart.width }); this.scale.update(newscaleprops); }, draw : function(ease){ var easingdecimal = ease || 1; this.clear(); var ctx = this.chart.ctx; this.scale.draw(easingdecimal); //draw all the bars for each dataset helpers.each(this.datasets,function(dataset,datasetindex){ helpers.each(dataset.bars,function(bar,index){ if (bar.hasvalue()){ bar.base = this.scale.endpoint; //transition then draw bar.transition({ x : this.scale.calculatebarx(this.datasets.length, datasetindex, index), y : this.scale.calculatey(bar.value), width : this.scale.calculatebarwidth(this.datasets.length) }, easingdecimal).draw(); } },this); },this); } }); }).call(this); (function(){ "use strict"; var root = this, chart = root.chart, //cache a local reference to chart.helpers helpers = chart.helpers; var defaultconfig = { //boolean - whether we should show a stroke on each segment segmentshowstroke : true, //string - the colour of each segment stroke segmentstrokecolor : "#fff", //number - the width of each segment stroke segmentstrokewidth : 2, //the percentage of the chart that we cut out of the middle. percentageinnercutout : 50, //number - amount of animation steps animationsteps : 100, //string - animation easing effect animationeasing : "easeoutbounce", //boolean - whether we animate the rotation of the doughnut animaterotate : true, //boolean - whether we animate scaling the doughnut from the centre animatescale : false, //string - a legend template legendtemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" }; chart.type.extend({ //passing in a name registers this chart in the chart namespace name: "doughnut", //providing a defaults will also register the deafults in the chart namespace defaults : defaultconfig, //initialize is fired when the chart is initialized - data is passed in as a parameter //config is automatically merged by the core of chart.js, and is available at this.options initialize: function(data){ //declare segments as a static property to prevent inheriting across the chart type prototype this.segments = []; this.outerradius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentstrokewidth/2)/2; this.segmentarc = chart.arc.extend({ ctx : this.chart.ctx, x : this.chart.width/2, y : this.chart.height/2 }); //set up tooltip events on the chart if (this.options.showtooltips){ helpers.bindevents(this, this.options.tooltipevents, function(evt){ var activesegments = (evt.type !== 'mouseout') ? this.getsegmentsatevent(evt) : []; helpers.each(this.segments,function(segment){ segment.restore(["fillcolor"]); }); helpers.each(activesegments,function(activesegment){ activesegment.fillcolor = activesegment.highlightcolor; }); this.showtooltip(activesegments); }); } this.calculatetotal(data); helpers.each(data,function(datapoint, index){ if (!datapoint.color) { datapoint.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)'; } this.adddata(datapoint, index, true); },this); this.render(); }, getsegmentsatevent : function(e){ var segmentsarray = []; var location = helpers.getrelativeposition(e); helpers.each(this.segments,function(segment){ if (segment.inrange(location.x,location.y)) segmentsarray.push(segment); },this); return segmentsarray; }, adddata : function(segment, atindex, silent){ var index = atindex !== undefined ? atindex : this.segments.length; if ( typeof(segment.color) === "undefined" ) { segment.color = chart.defaults.global.segmentcolordefault[index % chart.defaults.global.segmentcolordefault.length]; segment.highlight = chart.defaults.global.segmenthighlightcolordefaults[index % chart.defaults.global.segmenthighlightcolordefaults.length]; } this.segments.splice(index, 0, new this.segmentarc({ value : segment.value, outerradius : (this.options.animatescale) ? 0 : this.outerradius, innerradius : (this.options.animatescale) ? 0 : (this.outerradius/100) * this.options.percentageinnercutout, fillcolor : segment.color, highlightcolor : segment.highlight || segment.color, showstroke : this.options.segmentshowstroke, strokewidth : this.options.segmentstrokewidth, strokecolor : this.options.segmentstrokecolor, startangle : math.pi * 1.5, circumference : (this.options.animaterotate) ? 0 : this.calculatecircumference(segment.value), label : segment.label })); if (!silent){ this.reflow(); this.update(); } }, calculatecircumference : function(value) { if ( this.total > 0 ) { return (math.pi*2)*(value / this.total); } else { return 0; } }, calculatetotal : function(data){ this.total = 0; helpers.each(data,function(segment){ this.total += math.abs(segment.value); },this); }, update : function(){ this.calculatetotal(this.segments); // reset any highlight colours before updating. helpers.each(this.activeelements, function(activeelement){ activeelement.restore(['fillcolor']); }); helpers.each(this.segments,function(segment){ segment.save(); }); this.render(); }, removedata: function(atindex){ var indextodelete = (helpers.isnumber(atindex)) ? atindex : this.segments.length-1; this.segments.splice(indextodelete, 1); this.reflow(); this.update(); }, reflow : function(){ helpers.extend(this.segmentarc.prototype,{ x : this.chart.width/2, y : this.chart.height/2 }); this.outerradius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentstrokewidth/2)/2; helpers.each(this.segments, function(segment){ segment.update({ outerradius : this.outerradius, innerradius : (this.outerradius/100) * this.options.percentageinnercutout }); }, this); }, draw : function(easedecimal){ var animdecimal = (easedecimal) ? easedecimal : 1; this.clear(); helpers.each(this.segments,function(segment,index){ segment.transition({ circumference : this.calculatecircumference(segment.value), outerradius : this.outerradius, innerradius : (this.outerradius/100) * this.options.percentageinnercutout },animdecimal); segment.endangle = segment.startangle + segment.circumference; segment.draw(); if (index === 0){ segment.startangle = math.pi * 1.5; } //check to see if it's the last segment, if not get the next and update the start angle if (index < this.segments.length-1){ this.segments[index+1].startangle = segment.endangle; } },this); } }); chart.types.doughnut.extend({ name : "pie", defaults : helpers.merge(defaultconfig,{percentageinnercutout : 0}) }); }).call(this); (function(){ "use strict"; var root = this, chart = root.chart, helpers = chart.helpers; var defaultconfig = { ///boolean - whether grid lines are shown across the chart scaleshowgridlines : true, //string - colour of the grid lines scalegridlinecolor : "rgba(0,0,0,.05)", //number - width of the grid lines scalegridlinewidth : 1, //boolean - whether to show horizontal lines (except x axis) scaleshowhorizontallines: true, //boolean - whether to show vertical lines (except y axis) scaleshowverticallines: true, //boolean - whether the line is curved between points beziercurve : true, //number - tension of the bezier curve between points beziercurvetension : 0.4, //boolean - whether to show a dot for each point pointdot : true, //number - radius of each point dot in pixels pointdotradius : 4, //number - pixel width of point dot stroke pointdotstrokewidth : 1, //number - amount extra to add to the radius to cater for hit detection outside the drawn point pointhitdetectionradius : 20, //boolean - whether to show a stroke for datasets datasetstroke : true, //number - pixel width of dataset stroke datasetstrokewidth : 2, //boolean - whether to fill the dataset with a colour datasetfill : true, //string - a legend template legendtemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
", //boolean - whether to horizontally center the label and point dot inside the grid offsetgridlines : false }; chart.type.extend({ name: "line", defaults : defaultconfig, initialize: function(data){ //declare the extension of the default point, to cater for the options passed in to the constructor this.pointclass = chart.point.extend({ offsetgridlines : this.options.offsetgridlines, strokewidth : this.options.pointdotstrokewidth, radius : this.options.pointdotradius, display: this.options.pointdot, hitdetectionradius : this.options.pointhitdetectionradius, ctx : this.chart.ctx, inrange : function(mousex){ return (math.pow(mousex-this.x, 2) < math.pow(this.radius + this.hitdetectionradius,2)); } }); this.datasets = []; //set up tooltip events on the chart if (this.options.showtooltips){ helpers.bindevents(this, this.options.tooltipevents, function(evt){ var activepoints = (evt.type !== 'mouseout') ? this.getpointsatevent(evt) : []; this.eachpoints(function(point){ point.restore(['fillcolor', 'strokecolor']); }); helpers.each(activepoints, function(activepoint){ activepoint.fillcolor = activepoint.highlightfill; activepoint.strokecolor = activepoint.highlightstroke; }); this.showtooltip(activepoints); }); } //iterate through each of the datasets, and build this into a property of the chart helpers.each(data.datasets,function(dataset){ var datasetobject = { label : dataset.label || null, fillcolor : dataset.fillcolor, strokecolor : dataset.strokecolor, pointcolor : dataset.pointcolor, pointstrokecolor : dataset.pointstrokecolor, points : [] }; this.datasets.push(datasetobject); helpers.each(dataset.data,function(datapoint,index){ //add a new point for each piece of data, passing any required data to draw. datasetobject.points.push(new this.pointclass({ value : datapoint, label : data.labels[index], datasetlabel: dataset.label, strokecolor : dataset.pointstrokecolor, fillcolor : dataset.pointcolor, highlightfill : dataset.pointhighlightfill || dataset.pointcolor, highlightstroke : dataset.pointhighlightstroke || dataset.pointstrokecolor })); },this); this.buildscale(data.labels); this.eachpoints(function(point, index){ helpers.extend(point, { x: this.scale.calculatex(index), y: this.scale.endpoint }); point.save(); }, this); },this); this.render(); }, update : function(){ this.scale.update(); // reset any highlight colours before updating. helpers.each(this.activeelements, function(activeelement){ activeelement.restore(['fillcolor', 'strokecolor']); }); this.eachpoints(function(point){ point.save(); }); this.render(); }, eachpoints : function(callback){ helpers.each(this.datasets,function(dataset){ helpers.each(dataset.points,callback,this); },this); }, getpointsatevent : function(e){ var pointsarray = [], eventposition = helpers.getrelativeposition(e); helpers.each(this.datasets,function(dataset){ helpers.each(dataset.points,function(point){ if (point.inrange(eventposition.x,eventposition.y)) pointsarray.push(point); }); },this); return pointsarray; }, buildscale : function(labels){ var self = this; var datatotal = function(){ var values = []; self.eachpoints(function(point){ values.push(point.value); }); return values; }; var scaleoptions = { templatestring : this.options.scalelabel, height : this.chart.height, width : this.chart.width, ctx : this.chart.ctx, textcolor : this.options.scalefontcolor, offsetgridlines : this.options.offsetgridlines, fontsize : this.options.scalefontsize, fontstyle : this.options.scalefontstyle, fontfamily : this.options.scalefontfamily, valuescount : labels.length, beginatzero : this.options.scalebeginatzero, integersonly : this.options.scaleintegersonly, calculateyrange : function(currentheight){ var updatedranges = helpers.calculatescalerange( datatotal(), currentheight, this.fontsize, this.beginatzero, this.integersonly ); helpers.extend(this, updatedranges); }, xlabels : labels, font : helpers.fontstring(this.options.scalefontsize, this.options.scalefontstyle, this.options.scalefontfamily), linewidth : this.options.scalelinewidth, linecolor : this.options.scalelinecolor, showhorizontallines : this.options.scaleshowhorizontallines, showverticallines : this.options.scaleshowverticallines, gridlinewidth : (this.options.scaleshowgridlines) ? this.options.scalegridlinewidth : 0, gridlinecolor : (this.options.scaleshowgridlines) ? this.options.scalegridlinecolor : "rgba(0,0,0,0)", padding: (this.options.showscale) ? 0 : this.options.pointdotradius + this.options.pointdotstrokewidth, showlabels : this.options.scaleshowlabels, display : this.options.showscale }; if (this.options.scaleoverride){ helpers.extend(scaleoptions, { calculateyrange: helpers.noop, steps: this.options.scalesteps, stepvalue: this.options.scalestepwidth, min: this.options.scalestartvalue, max: this.options.scalestartvalue + (this.options.scalesteps * this.options.scalestepwidth) }); } this.scale = new chart.scale(scaleoptions); }, adddata : function(valuesarray,label){ //map the values array for each of the datasets helpers.each(valuesarray,function(value,datasetindex){ //add a new point for each piece of data, passing any required data to draw. this.datasets[datasetindex].points.push(new this.pointclass({ value : value, label : label, datasetlabel: this.datasets[datasetindex].label, x: this.scale.calculatex(this.scale.valuescount+1), y: this.scale.endpoint, strokecolor : this.datasets[datasetindex].pointstrokecolor, fillcolor : this.datasets[datasetindex].pointcolor })); },this); this.scale.addxlabel(label); //then re-render the chart. this.update(); }, removedata : function(){ this.scale.removexlabel(); //then re-render the chart. helpers.each(this.datasets,function(dataset){ dataset.points.shift(); },this); this.update(); }, reflow : function(){ var newscaleprops = helpers.extend({ height : this.chart.height, width : this.chart.width }); this.scale.update(newscaleprops); }, draw : function(ease){ var easingdecimal = ease || 1; this.clear(); var ctx = this.chart.ctx; // some helper methods for getting the next/prev points var hasvalue = function(item){ return item.value !== null; }, nextpoint = function(point, collection, index){ return helpers.findnextwhere(collection, hasvalue, index) || point; }, previouspoint = function(point, collection, index){ return helpers.findpreviouswhere(collection, hasvalue, index) || point; }; if (!this.scale) return; this.scale.draw(easingdecimal); helpers.each(this.datasets,function(dataset){ var pointswithvalues = helpers.where(dataset.points, hasvalue); //transition each point first so that the line and point drawing isn't out of sync //we can use this extra loop to calculate the control points of this dataset also in this loop helpers.each(dataset.points, function(point, index){ if (point.hasvalue()){ point.transition({ y : this.scale.calculatey(point.value), x : this.scale.calculatex(index) }, easingdecimal); } },this); // control points need to be calculated in a separate loop, because we need to know the current x/y of the point // this would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed if (this.options.beziercurve){ helpers.each(pointswithvalues, function(point, index){ var tension = (index > 0 && index < pointswithvalues.length - 1) ? this.options.beziercurvetension : 0; point.controlpoints = helpers.splinecurve( previouspoint(point, pointswithvalues, index), point, nextpoint(point, pointswithvalues, index), tension ); // prevent the bezier going outside of the bounds of the graph // cap puter bezier handles to the upper/lower scale bounds if (point.controlpoints.outer.y > this.scale.endpoint){ point.controlpoints.outer.y = this.scale.endpoint; } else if (point.controlpoints.outer.y < this.scale.startpoint){ point.controlpoints.outer.y = this.scale.startpoint; } // cap inner bezier handles to the upper/lower scale bounds if (point.controlpoints.inner.y > this.scale.endpoint){ point.controlpoints.inner.y = this.scale.endpoint; } else if (point.controlpoints.inner.y < this.scale.startpoint){ point.controlpoints.inner.y = this.scale.startpoint; } },this); } //draw the line between all the points ctx.linewidth = this.options.datasetstrokewidth; ctx.strokestyle = dataset.strokecolor; ctx.beginpath(); helpers.each(pointswithvalues, function(point, index){ if (index === 0){ ctx.moveto(point.x, point.y); } else{ if(this.options.beziercurve){ var previous = previouspoint(point, pointswithvalues, index); ctx.beziercurveto( previous.controlpoints.outer.x, previous.controlpoints.outer.y, point.controlpoints.inner.x, point.controlpoints.inner.y, point.x, point.y ); } else{ ctx.lineto(point.x,point.y); } } }, this); if (this.options.datasetstroke) { ctx.stroke(); } if (this.options.datasetfill && pointswithvalues.length > 0){ //round off the line by going to the base of the chart, back to the start, then fill. ctx.lineto(pointswithvalues[pointswithvalues.length - 1].x, this.scale.endpoint); ctx.lineto(pointswithvalues[0].x, this.scale.endpoint); ctx.fillstyle = dataset.fillcolor; ctx.closepath(); ctx.fill(); } //now draw the points over the line //a little inefficient double looping, but better than the line //lagging behind the point positions helpers.each(pointswithvalues,function(point){ point.draw(); }); },this); } }); }).call(this); (function(){ "use strict"; var root = this, chart = root.chart, //cache a local reference to chart.helpers helpers = chart.helpers; var defaultconfig = { //boolean - show a backdrop to the scale label scaleshowlabelbackdrop : true, //string - the colour of the label backdrop scalebackdropcolor : "rgba(255,255,255,0.75)", // boolean - whether the scale should begin at zero scalebeginatzero : true, //number - the backdrop padding above & below the label in pixels scalebackdroppaddingy : 2, //number - the backdrop padding to the side of the label in pixels scalebackdroppaddingx : 2, //boolean - show line for each value in the scale scaleshowline : true, //boolean - stroke a line around each segment in the chart segmentshowstroke : true, //string - the colour of the stroke on each segment. segmentstrokecolor : "#fff", //number - the width of the stroke value in pixels segmentstrokewidth : 2, //number - amount of animation steps animationsteps : 100, //string - animation easing effect. animationeasing : "easeoutbounce", //boolean - whether to animate the rotation of the chart animaterotate : true, //boolean - whether to animate scaling the chart from the centre animatescale : false, //string - a legend template legendtemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" }; chart.type.extend({ //passing in a name registers this chart in the chart namespace name: "polararea", //providing a defaults will also register the deafults in the chart namespace defaults : defaultconfig, //initialize is fired when the chart is initialized - data is passed in as a parameter //config is automatically merged by the core of chart.js, and is available at this.options initialize: function(data){ this.segments = []; //declare segment class as a chart instance specific class, so it can share props for this instance this.segmentarc = chart.arc.extend({ showstroke : this.options.segmentshowstroke, strokewidth : this.options.segmentstrokewidth, strokecolor : this.options.segmentstrokecolor, ctx : this.chart.ctx, innerradius : 0, x : this.chart.width/2, y : this.chart.height/2 }); this.scale = new chart.radialscale({ display: this.options.showscale, fontstyle: this.options.scalefontstyle, fontsize: this.options.scalefontsize, fontfamily: this.options.scalefontfamily, fontcolor: this.options.scalefontcolor, showlabels: this.options.scaleshowlabels, showlabelbackdrop: this.options.scaleshowlabelbackdrop, backdropcolor: this.options.scalebackdropcolor, backdroppaddingy : this.options.scalebackdroppaddingy, backdroppaddingx: this.options.scalebackdroppaddingx, linewidth: (this.options.scaleshowline) ? this.options.scalelinewidth : 0, linecolor: this.options.scalelinecolor, linearc: true, width: this.chart.width, height: this.chart.height, xcenter: this.chart.width/2, ycenter: this.chart.height/2, ctx : this.chart.ctx, templatestring: this.options.scalelabel, valuescount: data.length }); this.updatescalerange(data); this.scale.update(); helpers.each(data,function(segment,index){ this.adddata(segment,index,true); },this); //set up tooltip events on the chart if (this.options.showtooltips){ helpers.bindevents(this, this.options.tooltipevents, function(evt){ var activesegments = (evt.type !== 'mouseout') ? this.getsegmentsatevent(evt) : []; helpers.each(this.segments,function(segment){ segment.restore(["fillcolor"]); }); helpers.each(activesegments,function(activesegment){ activesegment.fillcolor = activesegment.highlightcolor; }); this.showtooltip(activesegments); }); } this.render(); }, getsegmentsatevent : function(e){ var segmentsarray = []; var location = helpers.getrelativeposition(e); helpers.each(this.segments,function(segment){ if (segment.inrange(location.x,location.y)) segmentsarray.push(segment); },this); return segmentsarray; }, adddata : function(segment, atindex, silent){ var index = atindex || this.segments.length; this.segments.splice(index, 0, new this.segmentarc({ fillcolor: segment.color, highlightcolor: segment.highlight || segment.color, label: segment.label, value: segment.value, outerradius: (this.options.animatescale) ? 0 : this.scale.calculatecenteroffset(segment.value), circumference: (this.options.animaterotate) ? 0 : this.scale.getcircumference(), startangle: math.pi * 1.5 })); if (!silent){ this.reflow(); this.update(); } }, removedata: function(atindex){ var indextodelete = (helpers.isnumber(atindex)) ? atindex : this.segments.length-1; this.segments.splice(indextodelete, 1); this.reflow(); this.update(); }, calculatetotal: function(data){ this.total = 0; helpers.each(data,function(segment){ this.total += segment.value; },this); this.scale.valuescount = this.segments.length; }, updatescalerange: function(datapoints){ var valuesarray = []; helpers.each(datapoints,function(segment){ valuesarray.push(segment.value); }); var scalesizes = (this.options.scaleoverride) ? { steps: this.options.scalesteps, stepvalue: this.options.scalestepwidth, min: this.options.scalestartvalue, max: this.options.scalestartvalue + (this.options.scalesteps * this.options.scalestepwidth) } : helpers.calculatescalerange( valuesarray, helpers.min([this.chart.width, this.chart.height])/2, this.options.scalefontsize, this.options.scalebeginatzero, this.options.scaleintegersonly ); helpers.extend( this.scale, scalesizes, { size: helpers.min([this.chart.width, this.chart.height]), xcenter: this.chart.width/2, ycenter: this.chart.height/2 } ); }, update : function(){ this.calculatetotal(this.segments); helpers.each(this.segments,function(segment){ segment.save(); }); this.reflow(); this.render(); }, reflow : function(){ helpers.extend(this.segmentarc.prototype,{ x : this.chart.width/2, y : this.chart.height/2 }); this.updatescalerange(this.segments); this.scale.update(); helpers.extend(this.scale,{ xcenter: this.chart.width/2, ycenter: this.chart.height/2 }); helpers.each(this.segments, function(segment){ segment.update({ outerradius : this.scale.calculatecenteroffset(segment.value) }); }, this); }, draw : function(ease){ var easingdecimal = ease || 1; //clear & draw the canvas this.clear(); helpers.each(this.segments,function(segment, index){ segment.transition({ circumference : this.scale.getcircumference(), outerradius : this.scale.calculatecenteroffset(segment.value) },easingdecimal); segment.endangle = segment.startangle + segment.circumference; // if we've removed the first segment we need to set the first one to // start at the top. if (index === 0){ segment.startangle = math.pi * 1.5; } //check to see if it's the last segment, if not get the next and update the start angle if (index < this.segments.length - 1){ this.segments[index+1].startangle = segment.endangle; } segment.draw(); }, this); this.scale.draw(); } }); }).call(this); (function(){ "use strict"; var root = this, chart = root.chart, helpers = chart.helpers; chart.type.extend({ name: "radar", defaults:{ //boolean - whether to show lines for each scale point scaleshowline : true, //boolean - whether we show the angle lines out of the radar angleshowlineout : true, //boolean - whether to show labels on the scale scaleshowlabels : false, // boolean - whether the scale should begin at zero scalebeginatzero : true, //string - colour of the angle line anglelinecolor : "rgba(0,0,0,.1)", //number - pixel width of the angle line anglelinewidth : 1, //string - point label font declaration pointlabelfontfamily : "'arial'", //string - point label font weight pointlabelfontstyle : "normal", //number - point label font size in pixels pointlabelfontsize : 10, //string - point label font colour pointlabelfontcolor : "#666", //boolean - whether to show a dot for each point pointdot : true, //number - radius of each point dot in pixels pointdotradius : 3, //number - pixel width of point dot stroke pointdotstrokewidth : 1, //number - amount extra to add to the radius to cater for hit detection outside the drawn point pointhitdetectionradius : 20, //boolean - whether to show a stroke for datasets datasetstroke : true, //number - pixel width of dataset stroke datasetstrokewidth : 2, //boolean - whether to fill the dataset with a colour datasetfill : true, //string - a legend template legendtemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" }, initialize: function(data){ this.pointclass = chart.point.extend({ strokewidth : this.options.pointdotstrokewidth, radius : this.options.pointdotradius, display: this.options.pointdot, hitdetectionradius : this.options.pointhitdetectionradius, ctx : this.chart.ctx }); this.datasets = []; this.buildscale(data); //set up tooltip events on the chart if (this.options.showtooltips){ helpers.bindevents(this, this.options.tooltipevents, function(evt){ var activepointscollection = (evt.type !== 'mouseout') ? this.getpointsatevent(evt) : []; this.eachpoints(function(point){ point.restore(['fillcolor', 'strokecolor']); }); helpers.each(activepointscollection, function(activepoint){ activepoint.fillcolor = activepoint.highlightfill; activepoint.strokecolor = activepoint.highlightstroke; }); this.showtooltip(activepointscollection); }); } //iterate through each of the datasets, and build this into a property of the chart helpers.each(data.datasets,function(dataset){ var datasetobject = { label: dataset.label || null, fillcolor : dataset.fillcolor, strokecolor : dataset.strokecolor, pointcolor : dataset.pointcolor, pointstrokecolor : dataset.pointstrokecolor, points : [] }; this.datasets.push(datasetobject); helpers.each(dataset.data,function(datapoint,index){ //add a new point for each piece of data, passing any required data to draw. var pointposition; if (!this.scale.animation){ pointposition = this.scale.getpointposition(index, this.scale.calculatecenteroffset(datapoint)); } datasetobject.points.push(new this.pointclass({ value : datapoint, label : data.labels[index], datasetlabel: dataset.label, x: (this.options.animation) ? this.scale.xcenter : pointposition.x, y: (this.options.animation) ? this.scale.ycenter : pointposition.y, strokecolor : dataset.pointstrokecolor, fillcolor : dataset.pointcolor, highlightfill : dataset.pointhighlightfill || dataset.pointcolor, highlightstroke : dataset.pointhighlightstroke || dataset.pointstrokecolor })); },this); },this); this.render(); }, eachpoints : function(callback){ helpers.each(this.datasets,function(dataset){ helpers.each(dataset.points,callback,this); },this); }, getpointsatevent : function(evt){ var mouseposition = helpers.getrelativeposition(evt), fromcenter = helpers.getanglefrompoint({ x: this.scale.xcenter, y: this.scale.ycenter }, mouseposition); var angleperindex = (math.pi * 2) /this.scale.valuescount, pointindex = math.round((fromcenter.angle - math.pi * 1.5) / angleperindex), activepointscollection = []; // if we're at the top, make the pointindex 0 to get the first of the array. if (pointindex >= this.scale.valuescount || pointindex < 0){ pointindex = 0; } if (fromcenter.distance <= this.scale.drawingarea){ helpers.each(this.datasets, function(dataset){ activepointscollection.push(dataset.points[pointindex]); }); } return activepointscollection; }, buildscale : function(data){ this.scale = new chart.radialscale({ display: this.options.showscale, fontstyle: this.options.scalefontstyle, fontsize: this.options.scalefontsize, fontfamily: this.options.scalefontfamily, fontcolor: this.options.scalefontcolor, showlabels: this.options.scaleshowlabels, showlabelbackdrop: this.options.scaleshowlabelbackdrop, backdropcolor: this.options.scalebackdropcolor, backgroundcolors: this.options.scalebackgroundcolors, backdroppaddingy : this.options.scalebackdroppaddingy, backdroppaddingx: this.options.scalebackdroppaddingx, linewidth: (this.options.scaleshowline) ? this.options.scalelinewidth : 0, linecolor: this.options.scalelinecolor, anglelinecolor : this.options.anglelinecolor, anglelinewidth : (this.options.angleshowlineout) ? this.options.anglelinewidth : 0, // point labels at the edge of each line pointlabelfontcolor : this.options.pointlabelfontcolor, pointlabelfontsize : this.options.pointlabelfontsize, pointlabelfontfamily : this.options.pointlabelfontfamily, pointlabelfontstyle : this.options.pointlabelfontstyle, height : this.chart.height, width: this.chart.width, xcenter: this.chart.width/2, ycenter: this.chart.height/2, ctx : this.chart.ctx, templatestring: this.options.scalelabel, labels: data.labels, valuescount: data.datasets[0].data.length }); this.scale.setscalesize(); this.updatescalerange(data.datasets); this.scale.buildylabels(); }, updatescalerange: function(datasets){ var valuesarray = (function(){ var totaldataarray = []; helpers.each(datasets,function(dataset){ if (dataset.data){ totaldataarray = totaldataarray.concat(dataset.data); } else { helpers.each(dataset.points, function(point){ totaldataarray.push(point.value); }); } }); return totaldataarray; })(); var scalesizes = (this.options.scaleoverride) ? { steps: this.options.scalesteps, stepvalue: this.options.scalestepwidth, min: this.options.scalestartvalue, max: this.options.scalestartvalue + (this.options.scalesteps * this.options.scalestepwidth) } : helpers.calculatescalerange( valuesarray, helpers.min([this.chart.width, this.chart.height])/2, this.options.scalefontsize, this.options.scalebeginatzero, this.options.scaleintegersonly ); helpers.extend( this.scale, scalesizes ); }, adddata : function(valuesarray,label){ //map the values array for each of the datasets this.scale.valuescount++; helpers.each(valuesarray,function(value,datasetindex){ var pointposition = this.scale.getpointposition(this.scale.valuescount, this.scale.calculatecenteroffset(value)); this.datasets[datasetindex].points.push(new this.pointclass({ value : value, label : label, datasetlabel: this.datasets[datasetindex].label, x: pointposition.x, y: pointposition.y, strokecolor : this.datasets[datasetindex].pointstrokecolor, fillcolor : this.datasets[datasetindex].pointcolor })); },this); this.scale.labels.push(label); this.reflow(); this.update(); }, removedata : function(){ this.scale.valuescount--; this.scale.labels.shift(); helpers.each(this.datasets,function(dataset){ dataset.points.shift(); },this); this.reflow(); this.update(); }, update : function(){ this.eachpoints(function(point){ point.save(); }); this.reflow(); this.render(); }, reflow: function(){ helpers.extend(this.scale, { width : this.chart.width, height: this.chart.height, size : helpers.min([this.chart.width, this.chart.height]), xcenter: this.chart.width/2, ycenter: this.chart.height/2 }); this.updatescalerange(this.datasets); this.scale.setscalesize(); this.scale.buildylabels(); }, draw : function(ease){ var easedecimal = ease || 1, ctx = this.chart.ctx; this.clear(); this.scale.draw(); helpers.each(this.datasets,function(dataset){ //transition each point first so that the line and point drawing isn't out of sync helpers.each(dataset.points,function(point,index){ if (point.hasvalue()){ point.transition(this.scale.getpointposition(index, this.scale.calculatecenteroffset(point.value)), easedecimal); } },this); //draw the line between all the points ctx.linewidth = this.options.datasetstrokewidth; ctx.strokestyle = dataset.strokecolor; ctx.beginpath(); helpers.each(dataset.points,function(point,index){ if (index === 0){ ctx.moveto(point.x,point.y); } else{ ctx.lineto(point.x,point.y); } },this); ctx.closepath(); ctx.stroke(); ctx.fillstyle = dataset.fillcolor; if(this.options.datasetfill){ ctx.fill(); } //now draw the points over the line //a little inefficient double looping, but better than the line //lagging behind the point positions helpers.each(dataset.points,function(point){ if (point.hasvalue()){ point.draw(); } }); },this); } }); }).call(this);