PluginProbe
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF / 1.4.6
Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF v1.4.6
2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.9 2.2.8 trunk 1.10 1.3.3 1.3.4 1.3.5 1.3.5.1 1.3.5.2 1.3.6 1.3.6.1 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 All 103 releases
imagify / assets / js / chart.js

chart.js in Imagify Image Optimization: Optimize Images | Compress & Convert to WebP/AVIF 1.4.6, at assets/js/chart.js

3,733 lines 115.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*!
2 * Chart.js
3 * http://chartjs.org/
4 * Version: 1.0.2
5 *
6 * Copyright 2015 Nick Downie
7 * Released under the MIT license
8 * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
9 */
10
11
12 (function(){
13
14 "use strict";
15
16 //Declare root variable - window in the browser, global on the server
17 var root = this,
18 previous = root.Chart;
19
20 //Occupy the global variable of Chart, and create a simple base class
21 var Chart = function(context){
22 var chart = this;
23 this.canvas = context.canvas;
24
25 this.ctx = context;
26
27 //Variables global to the chart
28 var computeDimension = function(element,dimension)
29 {
30 if (element['offset'+dimension])
31 {
32 return element['offset'+dimension];
33 }
34 else
35 {
36 return document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
37 }
38 };
39
40 var width = this.width = computeDimension(context.canvas,'Width') || context.canvas.width;
41 var height = this.height = computeDimension(context.canvas,'Height') || context.canvas.height;
42
43 width = this.width = context.canvas.width;
44 height = this.height = context.canvas.height;
45 this.aspectRatio = this.width / this.height;
46 //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
47 helpers.retinaScale(this);
48
49 return this;
50 };
51 //Globally expose the defaults to allow for user updating/changing
52 Chart.defaults = {
53 global: {
54 // Boolean - Whether to animate the chart
55 animation: true,
56
57 // Number - Number of animation steps
58 animationSteps: 60,
59
60 // String - Animation easing effect
61 animationEasing: "easeOutQuart",
62
63 // Boolean - If we should show the scale at all
64 showScale: true,
65
66 // Boolean - If we want to override with a hard coded scale
67 scaleOverride: false,
68
69 // ** Required if scaleOverride is true **
70 // Number - The number of steps in a hard coded scale
71 scaleSteps: null,
72 // Number - The value jump in the hard coded scale
73 scaleStepWidth: null,
74 // Number - The scale starting value
75 scaleStartValue: null,
76
77 // String - Colour of the scale line
78 scaleLineColor: "rgba(0,0,0,.1)",
79
80 // Number - Pixel width of the scale line
81 scaleLineWidth: 1,
82
83 // Boolean - Whether to show labels on the scale
84 scaleShowLabels: true,
85
86 // Interpolated JS string - can access value
87 scaleLabel: "<%=value%>",
88
89 // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
90 scaleIntegersOnly: true,
91
92 // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
93 scaleBeginAtZero: false,
94
95 // String - Scale label font declaration for the scale label
96 scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
97
98 // Number - Scale label font size in pixels
99 scaleFontSize: 12,
100
101 // String - Scale label font weight style
102 scaleFontStyle: "normal",
103
104 // String - Scale label font colour
105 scaleFontColor: "#666",
106
107 // Boolean - whether or not the chart should be responsive and resize when the browser does.
108 responsive: false,
109
110 // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
111 maintainAspectRatio: true,
112
113 // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
114 showTooltips: true,
115
116 // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
117 customTooltips: false,
118
119 // Array - Array of string names to attach tooltip events
120 tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
121
122 // String - Tooltip background colour
123 tooltipFillColor: "rgba(0,0,0,0.8)",
124
125 // String - Tooltip label font declaration for the scale label
126 tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
127
128 // Number - Tooltip label font size in pixels
129 tooltipFontSize: 14,
130
131 // String - Tooltip font weight style
132 tooltipFontStyle: "normal",
133
134 // String - Tooltip label font colour
135 tooltipFontColor: "#fff",
136
137 // String - Tooltip title font declaration for the scale label
138 tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
139
140 // Number - Tooltip title font size in pixels
141 tooltipTitleFontSize: 14,
142
143 // String - Tooltip title font weight style
144 tooltipTitleFontStyle: "bold",
145
146 // String - Tooltip title font colour
147 tooltipTitleFontColor: "#fff",
148
149 // String - Tooltip title template
150 tooltipTitleTemplate: "<%= label%>",
151
152 // Number - pixel width of padding around tooltip text
153 tooltipYPadding: 6,
154
155 // Number - pixel width of padding around tooltip text
156 tooltipXPadding: 6,
157
158 // Number - Size of the caret on the tooltip
159 tooltipCaretSize: 8,
160
161 // Number - Pixel radius of the tooltip border
162 tooltipCornerRadius: 6,
163
164 // Number - Pixel offset from point x to tooltip edge
165 tooltipXOffset: 10,
166
167 // String - Template string for single tooltips
168 tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
169
170 // String - Template string for single tooltips
171 multiTooltipTemplate: "<%= datasetLabel %>: <%= value %>",
172
173 // String - Colour behind the legend colour block
174 multiTooltipKeyBackground: '#fff',
175
176 // Array - A list of colors to use as the defaults
177 segmentColorDefault: ["#A6CEE3", "#1F78B4", "#B2DF8A", "#33A02C", "#FB9A99", "#E31A1C", "#FDBF6F", "#FF7F00", "#CAB2D6", "#6A3D9A", "#B4B482", "#B15928" ],
178
179 // Array - A list of highlight colors to use as the defaults
180 segmentHighlightColorDefaults: [ "#CEF6FF", "#47A0DC", "#DAFFB2", "#5BC854", "#FFC2C1", "#FF4244", "#FFE797", "#FFA728", "#F2DAFE", "#9265C2", "#DCDCAA", "#D98150" ],
181
182 // Function - Will fire on animation progression.
183 onAnimationProgress: function(){},
184
185 // Function - Will fire on animation completion.
186 onAnimationComplete: function(){}
187
188 }
189 };
190
191 //Create a dictionary of chart types, to allow for extension of existing types
192 Chart.types = {};
193
194 //Global Chart helpers object for utility methods and classes
195 var helpers = Chart.helpers = {};
196
197 //-- Basic js utility methods
198 var each = helpers.each = function(loopable,callback,self){
199 var additionalArgs = Array.prototype.slice.call(arguments, 3);
200 // Check to see if null or undefined firstly.
201 if (loopable){
202 if (loopable.length === +loopable.length){
203 var i;
204 for (i=0; i<loopable.length; i++){
205 callback.apply(self,[loopable[i], i].concat(additionalArgs));
206 }
207 }
208 else{
209 for (var item in loopable){
210 callback.apply(self,[loopable[item],item].concat(additionalArgs));
211 }
212 }
213 }
214 },
215 clone = helpers.clone = function(obj){
216 var objClone = {};
217 each(obj,function(value,key){
218 if (obj.hasOwnProperty(key)){
219 objClone[key] = value;
220 }
221 });
222 return objClone;
223 },
224 extend = helpers.extend = function(base){
225 each(Array.prototype.slice.call(arguments,1), function(extensionObject) {
226 each(extensionObject,function(value,key){
227 if (extensionObject.hasOwnProperty(key)){
228 base[key] = value;
229 }
230 });
231 });
232 return base;
233 },
234 merge = helpers.merge = function(base,master){
235 //Merge properties in left object over to a shallow clone of object right.
236 var args = Array.prototype.slice.call(arguments,0);
237 args.unshift({});
238 return extend.apply(null, args);
239 },
240 indexOf = helpers.indexOf = function(arrayToSearch, item){
241 if (Array.prototype.indexOf) {
242 return arrayToSearch.indexOf(item);
243 }
244 else{
245 for (var i = 0; i < arrayToSearch.length; i++) {
246 if (arrayToSearch[i] === item) return i;
247 }
248 return -1;
249 }
250 },
251 where = helpers.where = function(collection, filterCallback){
252 var filtered = [];
253
254 helpers.each(collection, function(item){
255 if (filterCallback(item)){
256 filtered.push(item);
257 }
258 });
259
260 return filtered;
261 },
262 findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){
263 // Default to start of the array
264 if (!startIndex){
265 startIndex = -1;
266 }
267 for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
268 var currentItem = arrayToSearch[i];
269 if (filterCallback(currentItem)){
270 return currentItem;
271 }
272 }
273 },
274 findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){
275 // Default to end of the array
276 if (!startIndex){
277 startIndex = arrayToSearch.length;
278 }
279 for (var i = startIndex - 1; i >= 0; i--) {
280 var currentItem = arrayToSearch[i];
281 if (filterCallback(currentItem)){
282 return currentItem;
283 }
284 }
285 },
286 inherits = helpers.inherits = function(extensions){
287 //Basic javascript inheritance based on the model created in Backbone.js
288 var parent = this;
289 var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
290
291 var Surrogate = function(){ this.constructor = ChartElement;};
292 Surrogate.prototype = parent.prototype;
293 ChartElement.prototype = new Surrogate();
294
295 ChartElement.extend = inherits;
296
297 if (extensions) extend(ChartElement.prototype, extensions);
298
299 ChartElement.__super__ = parent.prototype;
300
301 return ChartElement;
302 },
303 noop = helpers.noop = function(){},
304 uid = helpers.uid = (function(){
305 var id=0;
306 return function(){
307 return "chart-" + id++;
308 };
309 })(),
310 warn = helpers.warn = function(str){
311 //Method for warning of errors
312 if (window.console && typeof window.console.warn === "function") console.warn(str);
313 },
314 amd = helpers.amd = (typeof define === 'function' && define.amd),
315 //-- Math methods
316 isNumber = helpers.isNumber = function(n){
317 return !isNaN(parseFloat(n)) && isFinite(n);
318 },
319 max = helpers.max = function(array){
320 return Math.max.apply( Math, array );
321 },
322 min = helpers.min = function(array){
323 return Math.min.apply( Math, array );
324 },
325 cap = helpers.cap = function(valueToCap,maxValue,minValue){
326 if(isNumber(maxValue)) {
327 if( valueToCap > maxValue ) {
328 return maxValue;
329 }
330 }
331 else if(isNumber(minValue)){
332 if ( valueToCap < minValue ){
333 return minValue;
334 }
335 }
336 return valueToCap;
337 },
338 getDecimalPlaces = helpers.getDecimalPlaces = function(num){
339 if (num%1!==0 && isNumber(num)){
340 var s = num.toString();
341 if(s.indexOf("e-") < 0){
342 // no exponent, e.g. 0.01
343 return s.split(".")[1].length;
344 }
345 else if(s.indexOf(".") < 0) {
346 // no decimal point, e.g. 1e-9
347 return parseInt(s.split("e-")[1]);
348 }
349 else {
350 // exponent and decimal point, e.g. 1.23e-9
351 var parts = s.split(".")[1].split("e-");
352 return parts[0].length + parseInt(parts[1]);
353 }
354 }
355 else {
356 return 0;
357 }
358 },
359 toRadians = helpers.radians = function(degrees){
360 return degrees * (Math.PI/180);
361 },
362 // Gets the angle from vertical upright to the point about a centre.
363 getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
364 var distanceFromXCenter = anglePoint.x - centrePoint.x,
365 distanceFromYCenter = anglePoint.y - centrePoint.y,
366 radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
367
368
369 var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
370
371 //If the segment is in the top left quadrant, we need to add another rotation to the angle
372 if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
373 angle += Math.PI*2;
374 }
375
376 return {
377 angle: angle,
378 distance: radialDistanceFromCenter
379 };
380 },
381 aliasPixel = helpers.aliasPixel = function(pixelWidth){
382 return (pixelWidth % 2 === 0) ? 0 : 0.5;
383 },
384 splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
385 //Props to Rob Spencer at scaled innovation for his post on splining between points
386 //http://scaledinnovation.com/analytics/splines/aboutSplines.html
387 var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
388 d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
389 fa=t*d01/(d01+d12),// scaling factor for triangle Ta
390 fb=t*d12/(d01+d12);
391 return {
392 inner : {
393 x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
394 y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
395 },
396 outer : {
397 x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
398 y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
399 }
400 };
401 },
402 calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
403 return Math.floor(Math.log(val) / Math.LN10);
404 },
405 calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
406
407 //Set a minimum step of two - a point at the top of the graph, and a point at the base
408 var minSteps = 2,
409 maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
410 skipFitting = (minSteps >= maxSteps);
411
412 // Filter out null values since these would min() to zero
413 var values = [];
414 each(valuesArray, function( v ){
415 v == null || values.push( v );
416 });
417 var minValue = min(values),
418 maxValue = max(values);
419
420 // We need some degree of separation here to calculate the scales if all the values are the same
421 // Adding/minusing 0.5 will give us a range of 1.
422 if (maxValue === minValue){
423 maxValue += 0.5;
424 // So we don't end up with a graph with a negative start value if we've said always start from zero
425 if (minValue >= 0.5 && !startFromZero){
426 minValue -= 0.5;
427 }
428 else{
429 // Make up a whole number above the values
430 maxValue += 0.5;
431 }
432 }
433
434 var valueRange = Math.abs(maxValue - minValue),
435 rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
436 graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
437 graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
438 graphRange = graphMax - graphMin,
439 stepValue = Math.pow(10, rangeOrderOfMagnitude),
440 numberOfSteps = Math.round(graphRange / stepValue);
441
442 //If we have more space on the graph we'll use it to give more definition to the data
443 while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
444 if(numberOfSteps > maxSteps){
445 stepValue *=2;
446 numberOfSteps = Math.round(graphRange/stepValue);
447 // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
448 if (numberOfSteps % 1 !== 0){
449 skipFitting = true;
450 }
451 }
452 //We can fit in double the amount of scale points on the scale
453 else{
454 //If user has declared ints only, and the step value isn't a decimal
455 if (integersOnly && rangeOrderOfMagnitude >= 0){
456 //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
457 if(stepValue/2 % 1 === 0){
458 stepValue /=2;
459 numberOfSteps = Math.round(graphRange/stepValue);
460 }
461 //If it would make it a float break out of the loop
462 else{
463 break;
464 }
465 }
466 //If the scale doesn't have to be an int, make the scale more granular anyway.
467 else{
468 stepValue /=2;
469 numberOfSteps = Math.round(graphRange/stepValue);
470 }
471
472 }
473 }
474
475 if (skipFitting){
476 numberOfSteps = minSteps;
477 stepValue = graphRange / numberOfSteps;
478 }
479
480 return {
481 steps : numberOfSteps,
482 stepValue : stepValue,
483 min : graphMin,
484 max : graphMin + (numberOfSteps * stepValue)
485 };
486
487 },
488 /* jshint ignore:start */
489 // Blows up jshint errors based on the new Function constructor
490 //Templating methods
491 //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
492 template = helpers.template = function(templateString, valuesObject){
493
494 // If templateString is function rather than string-template - call the function for valuesObject
495
496 if(templateString instanceof Function){
497 return templateString(valuesObject);
498 }
499
500 var cache = {};
501 function tmpl(str, data){
502 // Figure out if we're getting a template, or if we need to
503 // load the template - and be sure to cache the result.
504 var fn = !/\W/.test(str) ?
505 cache[str] = cache[str] :
506
507 // Generate a reusable function that will serve as a template
508 // generator (and which will be cached).
509 new Function("obj",
510 "var p=[],print=function(){p.push.apply(p,arguments);};" +
511
512 // Introduce the data as local variables using with(){}
513 "with(obj){p.push('" +
514
515 // Convert the template into pure JavaScript
516 str
517 .replace(/[\r\t\n]/g, " ")
518 .split("<%").join("\t")
519 .replace(/((^|%>)[^\t]*)'/g, "$1\r")
520 .replace(/\t=(.*?)%>/g, "',$1,'")
521 .split("\t").join("');")
522 .split("%>").join("p.push('")
523 .split("\r").join("\\'") +
524 "');}return p.join('');"
525 );
526
527 // Provide some basic currying to the user
528 return data ? fn( data ) : fn;
529 }
530 return tmpl(templateString,valuesObject);
531 },
532 /* jshint ignore:end */
533 generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
534 var labelsArray = new Array(numberOfSteps);
535 if (templateString){
536 each(labelsArray,function(val,index){
537 labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
538 });
539 }
540 return labelsArray;
541 },
542 //--Animation methods
543 //Easing functions adapted from Robert Penner's easing equations
544 //http://www.robertpenner.com/easing/
545 easingEffects = helpers.easingEffects = {
546 linear: function (t) {
547 return t;
548 },
549 easeInQuad: function (t) {
550 return t * t;
551 },
552 easeOutQuad: function (t) {
553 return -1 * t * (t - 2);
554 },
555 easeInOutQuad: function (t) {
556 if ((t /= 1 / 2) < 1){
557 return 1 / 2 * t * t;
558 }
559 return -1 / 2 * ((--t) * (t - 2) - 1);
560 },
561 easeInCubic: function (t) {
562 return t * t * t;
563 },
564 easeOutCubic: function (t) {
565 return 1 * ((t = t / 1 - 1) * t * t + 1);
566 },
567 easeInOutCubic: function (t) {
568 if ((t /= 1 / 2) < 1){
569 return 1 / 2 * t * t * t;
570 }
571 return 1 / 2 * ((t -= 2) * t * t + 2);
572 },
573 easeInQuart: function (t) {
574 return t * t * t * t;
575 },
576 easeOutQuart: function (t) {
577 return -1 * ((t = t / 1 - 1) * t * t * t - 1);
578 },
579 easeInOutQuart: function (t) {
580 if ((t /= 1 / 2) < 1){
581 return 1 / 2 * t * t * t * t;
582 }
583 return -1 / 2 * ((t -= 2) * t * t * t - 2);
584 },
585 easeInQuint: function (t) {
586 return 1 * (t /= 1) * t * t * t * t;
587 },
588 easeOutQuint: function (t) {
589 return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
590 },
591 easeInOutQuint: function (t) {
592 if ((t /= 1 / 2) < 1){
593 return 1 / 2 * t * t * t * t * t;
594 }
595 return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
596 },
597 easeInSine: function (t) {
598 return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
599 },
600 easeOutSine: function (t) {
601 return 1 * Math.sin(t / 1 * (Math.PI / 2));
602 },
603 easeInOutSine: function (t) {
604 return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
605 },
606 easeInExpo: function (t) {
607 return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
608 },
609 easeOutExpo: function (t) {
610 return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
611 },
612 easeInOutExpo: function (t) {
613 if (t === 0){
614 return 0;
615 }
616 if (t === 1){
617 return 1;
618 }
619 if ((t /= 1 / 2) < 1){
620 return 1 / 2 * Math.pow(2, 10 * (t - 1));
621 }
622 return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
623 },
624 easeInCirc: function (t) {
625 if (t >= 1){
626 return t;
627 }
628 return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
629 },
630 easeOutCirc: function (t) {
631 return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
632 },
633 easeInOutCirc: function (t) {
634 if ((t /= 1 / 2) < 1){
635 return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
636 }
637 return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
638 },
639 easeInElastic: function (t) {
640 var s = 1.70158;
641 var p = 0;
642 var a = 1;
643 if (t === 0){
644 return 0;
645 }
646 if ((t /= 1) == 1){
647 return 1;
648 }
649 if (!p){
650 p = 1 * 0.3;
651 }
652 if (a < Math.abs(1)) {
653 a = 1;
654 s = p / 4;
655 } else{
656 s = p / (2 * Math.PI) * Math.asin(1 / a);
657 }
658 return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
659 },
660 easeOutElastic: function (t) {
661 var s = 1.70158;
662 var p = 0;
663 var a = 1;
664 if (t === 0){
665 return 0;
666 }
667 if ((t /= 1) == 1){
668 return 1;
669 }
670 if (!p){
671 p = 1 * 0.3;
672 }
673 if (a < Math.abs(1)) {
674 a = 1;
675 s = p / 4;
676 } else{
677 s = p / (2 * Math.PI) * Math.asin(1 / a);
678 }
679 return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
680 },
681 easeInOutElastic: function (t) {
682 var s = 1.70158;
683 var p = 0;
684 var a = 1;
685 if (t === 0){
686 return 0;
687 }
688 if ((t /= 1 / 2) == 2){
689 return 1;
690 }
691 if (!p){
692 p = 1 * (0.3 * 1.5);
693 }
694 if (a < Math.abs(1)) {
695 a = 1;
696 s = p / 4;
697 } else {
698 s = p / (2 * Math.PI) * Math.asin(1 / a);
699 }
700 if (t < 1){
701 return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));}
702 return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
703 },
704 easeInBack: function (t) {
705 var s = 1.70158;
706 return 1 * (t /= 1) * t * ((s + 1) * t - s);
707 },
708 easeOutBack: function (t) {
709 var s = 1.70158;
710 return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
711 },
712 easeInOutBack: function (t) {
713 var s = 1.70158;
714 if ((t /= 1 / 2) < 1){
715 return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
716 }
717 return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
718 },
719 easeInBounce: function (t) {
720 return 1 - easingEffects.easeOutBounce(1 - t);
721 },
722 easeOutBounce: function (t) {
723 if ((t /= 1) < (1 / 2.75)) {
724 return 1 * (7.5625 * t * t);
725 } else if (t < (2 / 2.75)) {
726 return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
727 } else if (t < (2.5 / 2.75)) {
728 return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
729 } else {
730 return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
731 }
732 },
733 easeInOutBounce: function (t) {
734 if (t < 1 / 2){
735 return easingEffects.easeInBounce(t * 2) * 0.5;
736 }
737 return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
738 }
739 },
740 //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
741 requestAnimFrame = helpers.requestAnimFrame = (function(){
742 return window.requestAnimationFrame ||
743 window.webkitRequestAnimationFrame ||
744 window.mozRequestAnimationFrame ||
745 window.oRequestAnimationFrame ||
746 window.msRequestAnimationFrame ||
747 function(callback) {
748 return window.setTimeout(callback, 1000 / 60);
749 };
750 })(),
751 cancelAnimFrame = helpers.cancelAnimFrame = (function(){
752 return window.cancelAnimationFrame ||
753 window.webkitCancelAnimationFrame ||
754 window.mozCancelAnimationFrame ||
755 window.oCancelAnimationFrame ||
756 window.msCancelAnimationFrame ||
757 function(callback) {
758 return window.clearTimeout(callback, 1000 / 60);
759 };
760 })(),
761 animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
762
763 var currentStep = 0,
764 easingFunction = easingEffects[easingString] || easingEffects.linear;
765
766 var animationFrame = function(){
767 currentStep++;
768 var stepDecimal = currentStep/totalSteps;
769 var easeDecimal = easingFunction(stepDecimal);
770
771 callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
772 onProgress.call(chartInstance,easeDecimal,stepDecimal);
773 if (currentStep < totalSteps){
774 chartInstance.animationFrame = requestAnimFrame(animationFrame);
775 } else{
776 onComplete.apply(chartInstance);
777 }
778 };
779 requestAnimFrame(animationFrame);
780 },
781 //-- DOM methods
782 getRelativePosition = helpers.getRelativePosition = function(evt){
783 var mouseX, mouseY;
784 var e = evt.originalEvent || evt,
785 canvas = evt.currentTarget || evt.srcElement,
786 boundingRect = canvas.getBoundingClientRect();
787
788 if (e.touches){
789 mouseX = e.touches[0].clientX - boundingRect.left;
790 mouseY = e.touches[0].clientY - boundingRect.top;
791
792 }
793 else{
794 mouseX = e.clientX - boundingRect.left;
795 mouseY = e.clientY - boundingRect.top;
796 }
797
798 return {
799 x : mouseX,
800 y : mouseY
801 };
802
803 },
804 addEvent = helpers.addEvent = function(node,eventType,method){
805 if (node.addEventListener){
806 node.addEventListener(eventType,method);
807 } else if (node.attachEvent){
808 node.attachEvent("on"+eventType, method);
809 } else {
810 node["on"+eventType] = method;
811 }
812 },
813 removeEvent = helpers.removeEvent = function(node, eventType, handler){
814 if (node.removeEventListener){
815 node.removeEventListener(eventType, handler, false);
816 } else if (node.detachEvent){
817 node.detachEvent("on"+eventType,handler);
818 } else{
819 node["on" + eventType] = noop;
820 }
821 },
822 bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
823 // Create the events object if it's not already present
824 if (!chartInstance.events) chartInstance.events = {};
825
826 each(arrayOfEvents,function(eventName){
827 chartInstance.events[eventName] = function(){
828 handler.apply(chartInstance, arguments);
829 };
830 addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
831 });
832 },
833 unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
834 each(arrayOfEvents, function(handler,eventName){
835 removeEvent(chartInstance.chart.canvas, eventName, handler);
836 });
837 },
838 getMaximumWidth = helpers.getMaximumWidth = function(domNode){
839 var container = domNode.parentNode,
840 padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right'));
841 // TODO = check cross browser stuff with this.
842 return container ? container.clientWidth - padding : 0;
843 },
844 getMaximumHeight = helpers.getMaximumHeight = function(domNode){
845 var container = domNode.parentNode,
846 padding = parseInt(getStyle(container, 'padding-bottom')) + parseInt(getStyle(container, 'padding-top'));
847 // TODO = check cross browser stuff with this.
848 return container ? container.clientHeight - padding : 0;
849 },
850 getStyle = helpers.getStyle = function (el, property) {
851 return el.currentStyle ?
852 el.currentStyle[property] :
853 document.defaultView.getComputedStyle(el, null).getPropertyValue(property);
854 },
855 getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
856 retinaScale = helpers.retinaScale = function(chart){
857 var ctx = chart.ctx,
858 width = chart.canvas.width,
859 height = chart.canvas.height;
860
861 if (window.devicePixelRatio) {
862 ctx.canvas.style.width = width + "px";
863 ctx.canvas.style.height = height + "px";
864 ctx.canvas.height = height * window.devicePixelRatio;
865 ctx.canvas.width = width * window.devicePixelRatio;
866 ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
867 }
868 },
869 //-- Canvas methods
870 clear = helpers.clear = function(chart){
871 chart.ctx.clearRect(0,0,chart.width,chart.height);
872 },
873 fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
874 return fontStyle + " " + pixelSize+"px " + fontFamily;
875 },
876 longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
877 ctx.font = font;
878 var longest = 0;
879 each(arrayOfStrings,function(string){
880 var textWidth = ctx.measureText(string).width;
881 longest = (textWidth > longest) ? textWidth : longest;
882 });
883 return longest;
884 },
885 drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
886 ctx.beginPath();
887 ctx.moveTo(x + radius, y);
888 ctx.lineTo(x + width - radius, y);
889 ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
890 ctx.lineTo(x + width, y + height - radius);
891 ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
892 ctx.lineTo(x + radius, y + height);
893 ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
894 ctx.lineTo(x, y + radius);
895 ctx.quadraticCurveTo(x, y, x + radius, y);
896 ctx.closePath();
897 };
898
899
900 //Store a reference to each instance - allowing us to globally resize chart instances on window resize.
901 //Destroy method on the chart will remove the instance of the chart from this reference.
902 Chart.instances = {};
903
904 Chart.Type = function(data,options,chart){
905 this.options = options;
906 this.chart = chart;
907 this.id = uid();
908 //Add the chart instance to the global namespace
909 Chart.instances[this.id] = this;
910
911 // Initialize is always called when a chart type is created
912 // By default it is a no op, but it should be extended
913 if (options.responsive){
914 this.resize();
915 }
916 this.initialize.call(this,data);
917 };
918
919 //Core methods that'll be a part of every chart type
920 extend(Chart.Type.prototype,{
921 initialize : function(){return this;},
922 clear : function(){
923 clear(this.chart);
924 return this;
925 },
926 stop : function(){
927 // Stops any current animation loop occuring
928 Chart.animationService.cancelAnimation(this);
929 return this;
930 },
931 resize : function(callback){
932 this.stop();
933 var canvas = this.chart.canvas,
934 newWidth = getMaximumWidth(this.chart.canvas),
935 newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
936
937 canvas.width = this.chart.width = newWidth;
938 canvas.height = this.chart.height = newHeight;
939
940 retinaScale(this.chart);
941
942 if (typeof callback === "function"){
943 callback.apply(this, Array.prototype.slice.call(arguments, 1));
944 }
945 return this;
946 },
947 reflow : noop,
948 render : function(reflow){
949 if (reflow){
950 this.reflow();
951 }
952
953 if (this.options.animation && !reflow){
954 var animation = new Chart.Animation();
955 animation.numSteps = this.options.animationSteps;
956 animation.easing = this.options.animationEasing;
957
958 // render function
959 animation.render = function(chartInstance, animationObject) {
960 var easingFunction = helpers.easingEffects[animationObject.easing];
961 var stepDecimal = animationObject.currentStep / animationObject.numSteps;
962 var easeDecimal = easingFunction(stepDecimal);
963
964 chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep);
965 };
966
967 // user events
968 animation.onAnimationProgress = this.options.onAnimationProgress;
969 animation.onAnimationComplete = this.options.onAnimationComplete;
970
971 Chart.animationService.addAnimation(this, animation);
972 }
973 else{
974 this.draw();
975 this.options.onAnimationComplete.call(this);
976 }
977 return this;
978 },
979 generateLegend : function(){
980 return helpers.template(this.options.legendTemplate, {datasets: this.datasets});
981 },
982 destroy : function(){
983 this.stop();
984 this.clear();
985 unbindEvents(this, this.events);
986 var canvas = this.chart.canvas;
987
988 // Reset canvas height/width attributes starts a fresh with the canvas context
989 canvas.width = this.chart.width;
990 canvas.height = this.chart.height;
991
992 // < IE9 doesn't support removeProperty
993 if (canvas.style.removeProperty) {
994 canvas.style.removeProperty('width');
995 canvas.style.removeProperty('height');
996 } else {
997 canvas.style.removeAttribute('width');
998 canvas.style.removeAttribute('height');
999 }
1000
1001 delete Chart.instances[this.id];
1002 },
1003 showTooltip : function(ChartElements, forceRedraw){
1004 // Only redraw the chart if we've actually changed what we're hovering on.
1005 if (typeof this.activeElements === 'undefined') this.activeElements = [];
1006
1007 var isChanged = (function(Elements){
1008 var changed = false;
1009
1010 if (Elements.length !== this.activeElements.length){
1011 changed = true;
1012 return changed;
1013 }
1014
1015 each(Elements, function(element, index){
1016 if (element !== this.activeElements[index]){
1017 changed = true;
1018 }
1019 }, this);
1020 return changed;
1021 }).call(this, ChartElements);
1022
1023 if (!isChanged && !forceRedraw){
1024 return;
1025 }
1026 else{
1027 this.activeElements = ChartElements;
1028 }
1029 this.draw();
1030 if(this.options.customTooltips){
1031 this.options.customTooltips(false);
1032 }
1033 if (ChartElements.length > 0){
1034 // If we have multiple datasets, show a MultiTooltip for all of the data points at that index
1035 if (this.datasets && this.datasets.length > 1) {
1036 var dataArray,
1037 dataIndex;
1038
1039 for (var i = this.datasets.length - 1; i >= 0; i--) {
1040 dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
1041 dataIndex = indexOf(dataArray, ChartElements[0]);
1042 if (dataIndex !== -1){
1043 break;
1044 }
1045 }
1046 var tooltipLabels = [],
1047 tooltipColors = [],
1048 medianPosition = (function(index) {
1049
1050 // Get all the points at that particular index
1051 var Elements = [],
1052 dataCollection,
1053 xPositions = [],
1054 yPositions = [],
1055 xMax,
1056 yMax,
1057 xMin,
1058 yMin;
1059 helpers.each(this.datasets, function(dataset){
1060 dataCollection = dataset.points || dataset.bars || dataset.segments;
1061 if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
1062 Elements.push(dataCollection[dataIndex]);
1063 }
1064 });
1065
1066 helpers.each(Elements, function(element) {
1067 xPositions.push(element.x);
1068 yPositions.push(element.y);
1069
1070
1071 //Include any colour information about the element
1072 tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
1073 tooltipColors.push({
1074 fill: element._saved.fillColor || element.fillColor,
1075 stroke: element._saved.strokeColor || element.strokeColor
1076 });
1077
1078 }, this);
1079
1080 yMin = min(yPositions);
1081 yMax = max(yPositions);
1082
1083 xMin = min(xPositions);
1084 xMax = max(xPositions);
1085
1086 return {
1087 x: (xMin > this.chart.width/2) ? xMin : xMax,
1088 y: (yMin + yMax)/2
1089 };
1090 }).call(this, dataIndex);
1091
1092 new Chart.MultiTooltip({
1093 x: medianPosition.x,
1094 y: medianPosition.y,
1095 xPadding: this.options.tooltipXPadding,
1096 yPadding: this.options.tooltipYPadding,
1097 xOffset: this.options.tooltipXOffset,
1098 fillColor: this.options.tooltipFillColor,
1099 textColor: this.options.tooltipFontColor,
1100 fontFamily: this.options.tooltipFontFamily,
1101 fontStyle: this.options.tooltipFontStyle,
1102 fontSize: this.options.tooltipFontSize,
1103 titleTextColor: this.options.tooltipTitleFontColor,
1104 titleFontFamily: this.options.tooltipTitleFontFamily,
1105 titleFontStyle: this.options.tooltipTitleFontStyle,
1106 titleFontSize: this.options.tooltipTitleFontSize,
1107 cornerRadius: this.options.tooltipCornerRadius,
1108 labels: tooltipLabels,
1109 legendColors: tooltipColors,
1110 legendColorBackground : this.options.multiTooltipKeyBackground,
1111 title: template(this.options.tooltipTitleTemplate,ChartElements[0]),
1112 chart: this.chart,
1113 ctx: this.chart.ctx,
1114 custom: this.options.customTooltips
1115 }).draw();
1116
1117 } else {
1118 each(ChartElements, function(Element) {
1119 var tooltipPosition = Element.tooltipPosition();
1120 new Chart.Tooltip({
1121 x: Math.round(tooltipPosition.x),
1122 y: Math.round(tooltipPosition.y),
1123 xPadding: this.options.tooltipXPadding,
1124 yPadding: this.options.tooltipYPadding,
1125 fillColor: this.options.tooltipFillColor,
1126 textColor: this.options.tooltipFontColor,
1127 fontFamily: this.options.tooltipFontFamily,
1128 fontStyle: this.options.tooltipFontStyle,
1129 fontSize: this.options.tooltipFontSize,
1130 caretHeight: this.options.tooltipCaretSize,
1131 cornerRadius: this.options.tooltipCornerRadius,
1132 text: template(this.options.tooltipTemplate, Element),
1133 chart: this.chart,
1134 custom: this.options.customTooltips
1135 }).draw();
1136 }, this);
1137 }
1138 }
1139 return this;
1140 },
1141 toBase64Image : function(){
1142 return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
1143 }
1144 });
1145
1146 Chart.Type.extend = function(extensions){
1147
1148 var parent = this;
1149
1150 var ChartType = function(){
1151 return parent.apply(this,arguments);
1152 };
1153
1154 //Copy the prototype object of the this class
1155 ChartType.prototype = clone(parent.prototype);
1156 //Now overwrite some of the properties in the base class with the new extensions
1157 extend(ChartType.prototype, extensions);
1158
1159 ChartType.extend = Chart.Type.extend;
1160
1161 if (extensions.name || parent.prototype.name){
1162
1163 var chartName = extensions.name || parent.prototype.name;
1164 //Assign any potential default values of the new chart type
1165
1166 //If none are defined, we'll use a clone of the chart type this is being extended from.
1167 //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
1168 //doesn't define some defaults of their own.
1169
1170 var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
1171
1172 Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
1173
1174 Chart.types[chartName] = ChartType;
1175
1176 //Register this new chart type in the Chart prototype
1177 Chart.prototype[chartName] = function(data,options){
1178 var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
1179 return new ChartType(data,config,this);
1180 };
1181 } else{
1182 warn("Name not provided for this chart, so it hasn't been registered");
1183 }
1184 return parent;
1185 };
1186
1187 Chart.Element = function(configuration){
1188 extend(this,configuration);
1189 this.initialize.apply(this,arguments);
1190 this.save();
1191 };
1192 extend(Chart.Element.prototype,{
1193 initialize : function(){},
1194 restore : function(props){
1195 if (!props){
1196 extend(this,this._saved);
1197 } else {
1198 each(props,function(key){
1199 this[key] = this._saved[key];
1200 },this);
1201 }
1202 return this;
1203 },
1204 save : function(){
1205 this._saved = clone(this);
1206 delete this._saved._saved;
1207 return this;
1208 },
1209 update : function(newProps){
1210 each(newProps,function(value,key){
1211 this._saved[key] = this[key];
1212 this[key] = value;
1213 },this);
1214 return this;
1215 },
1216 transition : function(props,ease){
1217 each(props,function(value,key){
1218 this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
1219 },this);
1220 return this;
1221 },
1222 tooltipPosition : function(){
1223 return {
1224 x : this.x,
1225 y : this.y
1226 };
1227 },
1228 hasValue: function(){
1229 return isNumber(this.value);
1230 }
1231 });
1232
1233 Chart.Element.extend = inherits;
1234
1235
1236 Chart.Point = Chart.Element.extend({
1237 display: true,
1238 inRange: function(chartX,chartY){
1239 var hitDetectionRange = this.hitDetectionRadius + this.radius;
1240 return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
1241 },
1242 draw : function(){
1243 if (this.display){
1244 var ctx = this.ctx;
1245 ctx.beginPath();
1246
1247 ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
1248 ctx.closePath();
1249
1250 ctx.strokeStyle = this.strokeColor;
1251 ctx.lineWidth = this.strokeWidth;
1252
1253 ctx.fillStyle = this.fillColor;
1254
1255 ctx.fill();
1256 ctx.stroke();
1257 }
1258
1259
1260 //Quick debug for bezier curve splining
1261 //Highlights control points and the line between them.
1262 //Handy for dev - stripped in the min version.
1263
1264 // ctx.save();
1265 // ctx.fillStyle = "black";
1266 // ctx.strokeStyle = "black"
1267 // ctx.beginPath();
1268 // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
1269 // ctx.fill();
1270
1271 // ctx.beginPath();
1272 // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
1273 // ctx.fill();
1274
1275 // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
1276 // ctx.lineTo(this.x, this.y);
1277 // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
1278 // ctx.stroke();
1279
1280 // ctx.restore();
1281
1282
1283
1284 }
1285 });
1286
1287 Chart.Arc = Chart.Element.extend({
1288 inRange : function(chartX,chartY){
1289
1290 var pointRelativePosition = helpers.getAngleFromPoint(this, {
1291 x: chartX,
1292 y: chartY
1293 });
1294
1295 // Normalize all angles to 0 - 2*PI (0 - 360°)
1296 var pointRelativeAngle = pointRelativePosition.angle % (Math.PI * 2),
1297 startAngle = (Math.PI * 2 + this.startAngle) % (Math.PI * 2),
1298 endAngle = (Math.PI * 2 + this.endAngle) % (Math.PI * 2) || 360;
1299
1300 // Calculate wether the pointRelativeAngle is between the start and the end angle
1301 var betweenAngles = (endAngle < startAngle) ?
1302 pointRelativeAngle <= endAngle || pointRelativeAngle >= startAngle:
1303 pointRelativeAngle >= startAngle && pointRelativeAngle <= endAngle;
1304
1305 //Check if within the range of the open/close angle
1306 var withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
1307
1308 return (betweenAngles && withinRadius);
1309 //Ensure within the outside of the arc centre, but inside arc outer
1310 },
1311 tooltipPosition : function(){
1312 var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
1313 rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
1314 return {
1315 x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
1316 y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
1317 };
1318 },
1319 draw : function(animationPercent){
1320
1321 var easingDecimal = animationPercent || 1;
1322
1323 var ctx = this.ctx;
1324
1325 ctx.beginPath();
1326
1327 ctx.arc(this.x, this.y, this.outerRadius < 0 ? 0 : this.outerRadius, this.startAngle, this.endAngle);
1328
1329 ctx.arc(this.x, this.y, this.innerRadius < 0 ? 0 : this.innerRadius, this.endAngle, this.startAngle, true);
1330
1331 ctx.closePath();
1332 ctx.strokeStyle = this.strokeColor;
1333 ctx.lineWidth = this.strokeWidth;
1334
1335 ctx.fillStyle = this.fillColor;
1336
1337 ctx.fill();
1338 ctx.lineJoin = 'bevel';
1339
1340 if (this.showStroke){
1341 ctx.stroke();
1342 }
1343 }
1344 });
1345
1346 Chart.Rectangle = Chart.Element.extend({
1347 draw : function(){
1348 var ctx = this.ctx,
1349 halfWidth = this.width/2,
1350 leftX = this.x - halfWidth,
1351 rightX = this.x + halfWidth,
1352 top = this.base - (this.base - this.y),
1353 halfStroke = this.strokeWidth / 2;
1354
1355 // Canvas doesn't allow us to stroke inside the width so we can
1356 // adjust the sizes to fit if we're setting a stroke on the line
1357 if (this.showStroke){
1358 leftX += halfStroke;
1359 rightX -= halfStroke;
1360 top += halfStroke;
1361 }
1362
1363 ctx.beginPath();
1364
1365 ctx.fillStyle = this.fillColor;
1366 ctx.strokeStyle = this.strokeColor;
1367 ctx.lineWidth = this.strokeWidth;
1368
1369 // It'd be nice to keep this class totally generic to any rectangle
1370 // and simply specify which border to miss out.
1371 ctx.moveTo(leftX, this.base);
1372 ctx.lineTo(leftX, top);
1373 ctx.lineTo(rightX, top);
1374 ctx.lineTo(rightX, this.base);
1375 ctx.fill();
1376 if (this.showStroke){
1377 ctx.stroke();
1378 }
1379 },
1380 height : function(){
1381 return this.base - this.y;
1382 },
1383 inRange : function(chartX,chartY){
1384 return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
1385 }
1386 });
1387
1388 Chart.Animation = Chart.Element.extend({
1389 currentStep: null, // the current animation step
1390 numSteps: 60, // default number of steps
1391 easing: "", // the easing to use for this animation
1392 render: null, // render function used by the animation service
1393
1394 onAnimationProgress: null, // user specified callback to fire on each step of the animation
1395 onAnimationComplete: null, // user specified callback to fire when the animation finishes
1396 });
1397
1398 Chart.Tooltip = Chart.Element.extend({
1399 draw : function(){
1400
1401 var ctx = this.chart.ctx;
1402
1403 ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
1404
1405 this.xAlign = "center";
1406 this.yAlign = "above";
1407
1408 //Distance between the actual element.y position and the start of the tooltip caret
1409 var caretPadding = this.caretPadding = 2;
1410
1411 var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
1412 tooltipRectHeight = this.fontSize + 2*this.yPadding,
1413 tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
1414
1415 if (this.x + tooltipWidth/2 >this.chart.width){
1416 this.xAlign = "left";
1417 } else if (this.x - tooltipWidth/2 < 0){
1418 this.xAlign = "right";
1419 }
1420
1421 if (this.y - tooltipHeight < 0){
1422 this.yAlign = "below";
1423 }
1424
1425
1426 var tooltipX = this.x - tooltipWidth/2,
1427 tooltipY = this.y - tooltipHeight;
1428
1429 ctx.fillStyle = this.fillColor;
1430
1431 // Custom Tooltips
1432 if(this.custom){
1433 this.custom(this);
1434 }
1435 else{
1436 switch(this.yAlign)
1437 {
1438 case "above":
1439 //Draw a caret above the x/y
1440 ctx.beginPath();
1441 ctx.moveTo(this.x,this.y - caretPadding);
1442 ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
1443 ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
1444 ctx.closePath();
1445 ctx.fill();
1446 break;
1447 case "below":
1448 tooltipY = this.y + caretPadding + this.caretHeight;
1449 //Draw a caret below the x/y
1450 ctx.beginPath();
1451 ctx.moveTo(this.x, this.y + caretPadding);
1452 ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
1453 ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
1454 ctx.closePath();
1455 ctx.fill();
1456 break;
1457 }
1458
1459 switch(this.xAlign)
1460 {
1461 case "left":
1462 tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
1463 break;
1464 case "right":
1465 tooltipX = this.x - (this.cornerRadius + this.caretHeight);
1466 break;
1467 }
1468
1469 drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
1470
1471 ctx.fill();
1472
1473 ctx.fillStyle = this.textColor;
1474 ctx.textAlign = "center";
1475 ctx.textBaseline = "middle";
1476 ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
1477 }
1478 }
1479 });
1480
1481 Chart.MultiTooltip = Chart.Element.extend({
1482 initialize : function(){
1483 this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
1484
1485 this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
1486
1487 this.titleHeight = this.title ? this.titleFontSize * 1.5 : 0;
1488 this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleHeight;
1489
1490 this.ctx.font = this.titleFont;
1491
1492 var titleWidth = this.ctx.measureText(this.title).width,
1493 //Label has a legend square as well so account for this.
1494 labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
1495 longestTextWidth = max([labelWidth,titleWidth]);
1496
1497 this.width = longestTextWidth + (this.xPadding*2);
1498
1499
1500 var halfHeight = this.height/2;
1501
1502 //Check to ensure the height will fit on the canvas
1503 if (this.y - halfHeight < 0 ){
1504 this.y = halfHeight;
1505 } else if (this.y + halfHeight > this.chart.height){
1506 this.y = this.chart.height - halfHeight;
1507 }
1508
1509 //Decide whether to align left or right based on position on canvas
1510 if (this.x > this.chart.width/2){
1511 this.x -= this.xOffset + this.width;
1512 } else {
1513 this.x += this.xOffset;
1514 }
1515
1516
1517 },
1518 getLineHeight : function(index){
1519 var baseLineHeight = this.y - (this.height/2) + this.yPadding,
1520 afterTitleIndex = index-1;
1521
1522 //If the index is zero, we're getting the title
1523 if (index === 0){
1524 return baseLineHeight + this.titleHeight / 3;
1525 } else{
1526 return baseLineHeight + ((this.fontSize * 1.5 * afterTitleIndex) + this.fontSize / 2) + this.titleHeight;
1527 }
1528
1529 },
1530 draw : function(){
1531 // Custom Tooltips
1532 if(this.custom){
1533 this.custom(this);
1534 }
1535 else{
1536 drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
1537 var ctx = this.ctx;
1538 ctx.fillStyle = this.fillColor;
1539 ctx.fill();
1540 ctx.closePath();
1541
1542 ctx.textAlign = "left";
1543 ctx.textBaseline = "middle";
1544 ctx.fillStyle = this.titleTextColor;
1545 ctx.font = this.titleFont;
1546
1547 ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
1548
1549 ctx.font = this.font;
1550 helpers.each(this.labels,function(label,index){
1551 ctx.fillStyle = this.textColor;
1552 ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
1553
1554 //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
1555 //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1556 //Instead we'll make a white filled block to put the legendColour palette over.
1557
1558 ctx.fillStyle = this.legendColorBackground;
1559 ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1560
1561 ctx.fillStyle = this.legendColors[index].fill;
1562 ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1563
1564
1565 },this);
1566 }
1567 }
1568 });
1569
1570 Chart.Scale = Chart.Element.extend({
1571 initialize : function(){
1572 this.fit();
1573 },
1574 buildYLabels : function(){
1575 this.yLabels = [];
1576
1577 var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
1578
1579 for (var i=0; i<=this.steps; i++){
1580 this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
1581 }
1582 this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) + 10 : 0;
1583 },
1584 addXLabel : function(label){
1585 this.xLabels.push(label);
1586 this.valuesCount++;
1587 this.fit();
1588 },
1589 removeXLabel : function(){
1590 this.xLabels.shift();
1591 this.valuesCount--;
1592 this.fit();
1593 },
1594 // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
1595 fit: function(){
1596 // First we need the width of the yLabels, assuming the xLabels aren't rotated
1597
1598 // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
1599 this.startPoint = (this.display) ? this.fontSize : 0;
1600 this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
1601
1602 // Apply padding settings to the start and end point.
1603 this.startPoint += this.padding;
1604 this.endPoint -= this.padding;
1605
1606 // Cache the starting endpoint, excluding the space for x labels
1607 var cachedEndPoint = this.endPoint;
1608
1609 // Cache the starting height, so can determine if we need to recalculate the scale yAxis
1610 var cachedHeight = this.endPoint - this.startPoint,
1611 cachedYLabelWidth;
1612
1613 // Build the current yLabels so we have an idea of what size they'll be to start
1614 /*
1615 * This sets what is returned from calculateScaleRange as static properties of this class:
1616 *
1617 this.steps;
1618 this.stepValue;
1619 this.min;
1620 this.max;
1621 *
1622 */
1623 this.calculateYRange(cachedHeight);
1624
1625 // With these properties set we can now build the array of yLabels
1626 // and also the width of the largest yLabel
1627 this.buildYLabels();
1628
1629 this.calculateXLabelRotation();
1630
1631 while((cachedHeight > this.endPoint - this.startPoint)){
1632 cachedHeight = this.endPoint - this.startPoint;
1633 cachedYLabelWidth = this.yLabelWidth;
1634
1635 this.calculateYRange(cachedHeight);
1636 this.buildYLabels();
1637
1638 // Only go through the xLabel loop again if the yLabel width has changed
1639 if (cachedYLabelWidth < this.yLabelWidth){
1640 this.endPoint = cachedEndPoint;
1641 this.calculateXLabelRotation();
1642 }
1643 }
1644
1645 },
1646 calculateXLabelRotation : function(){
1647 //Get the width of each grid by calculating the difference
1648 //between x offsets between 0 and 1.
1649
1650 this.ctx.font = this.font;
1651
1652 var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
1653 lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
1654 firstRotated,
1655 lastRotated;
1656
1657
1658 this.xScalePaddingRight = lastWidth/2 + 3;
1659 this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth;
1660
1661 this.xLabelRotation = 0;
1662 if (this.display){
1663 var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
1664 cosRotation,
1665 firstRotatedWidth;
1666 this.xLabelWidth = originalLabelWidth;
1667 //Allow 3 pixels x2 padding either side for label readability
1668 var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
1669
1670 //Max label rotate should be 90 - also act as a loop counter
1671 while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
1672 cosRotation = Math.cos(toRadians(this.xLabelRotation));
1673
1674 firstRotated = cosRotation * firstWidth;
1675 lastRotated = cosRotation * lastWidth;
1676
1677 // We're right aligning the text now.
1678 if (firstRotated + this.fontSize / 2 > this.yLabelWidth){
1679 this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
1680 }
1681 this.xScalePaddingRight = this.fontSize/2;
1682
1683
1684 this.xLabelRotation++;
1685 this.xLabelWidth = cosRotation * originalLabelWidth;
1686
1687 }
1688 if (this.xLabelRotation > 0){
1689 this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
1690 }
1691 }
1692 else{
1693 this.xLabelWidth = 0;
1694 this.xScalePaddingRight = this.padding;
1695 this.xScalePaddingLeft = this.padding;
1696 }
1697
1698 },
1699 // Needs to be overidden in each Chart type
1700 // Otherwise we need to pass all the data into the scale class
1701 calculateYRange: noop,
1702 drawingArea: function(){
1703 return this.startPoint - this.endPoint;
1704 },
1705 calculateY : function(value){
1706 var scalingFactor = this.drawingArea() / (this.min - this.max);
1707 return this.endPoint - (scalingFactor * (value - this.min));
1708 },
1709 calculateX : function(index){
1710 var isRotated = (this.xLabelRotation > 0),
1711 // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
1712 innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
1713 valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1),
1714 valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
1715
1716 if (this.offsetGridLines){
1717 valueOffset += (valueWidth/2);
1718 }
1719
1720 return Math.round(valueOffset);
1721 },
1722 update : function(newProps){
1723 helpers.extend(this, newProps);
1724 this.fit();
1725 },
1726 draw : function(){
1727 var ctx = this.ctx,
1728 yLabelGap = (this.endPoint - this.startPoint) / this.steps,
1729 xStart = Math.round(this.xScalePaddingLeft);
1730 if (this.display){
1731 ctx.fillStyle = this.textColor;
1732 ctx.font = this.font;
1733 each(this.yLabels,function(labelString,index){
1734 var yLabelCenter = this.endPoint - (yLabelGap * index),
1735 linePositionY = Math.round(yLabelCenter),
1736 drawHorizontalLine = this.showHorizontalLines;
1737
1738 ctx.textAlign = "right";
1739 ctx.textBaseline = "middle";
1740 if (this.showLabels){
1741 ctx.fillText(labelString,xStart - 10,yLabelCenter);
1742 }
1743
1744 // This is X axis, so draw it
1745 if (index === 0 && !drawHorizontalLine){
1746 drawHorizontalLine = true;
1747 }
1748
1749 if (drawHorizontalLine){
1750 ctx.beginPath();
1751 }
1752
1753 if (index > 0){
1754 // This is a grid line in the centre, so drop that
1755 ctx.lineWidth = this.gridLineWidth;
1756 ctx.strokeStyle = this.gridLineColor;
1757 } else {
1758 // This is the first line on the scale
1759 ctx.lineWidth = this.lineWidth;
1760 ctx.strokeStyle = this.lineColor;
1761 }
1762
1763 linePositionY += helpers.aliasPixel(ctx.lineWidth);
1764
1765 if(drawHorizontalLine){
1766 ctx.moveTo(xStart, linePositionY);
1767 ctx.lineTo(this.width, linePositionY);
1768 ctx.stroke();
1769 ctx.closePath();
1770 }
1771
1772 ctx.lineWidth = this.lineWidth;
1773 ctx.strokeStyle = this.lineColor;
1774 ctx.beginPath();
1775 ctx.moveTo(xStart - 5, linePositionY);
1776 ctx.lineTo(xStart, linePositionY);
1777 ctx.stroke();
1778 ctx.closePath();
1779
1780 },this);
1781
1782 each(this.xLabels,function(label,index){
1783 var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
1784 // Check to see if line/bar here and decide where to place the line
1785 linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
1786 isRotated = (this.xLabelRotation > 0),
1787 drawVerticalLine = this.showVerticalLines;
1788
1789 // This is Y axis, so draw it
1790 if (index === 0 && !drawVerticalLine){
1791 drawVerticalLine = true;
1792 }
1793
1794 if (drawVerticalLine){
1795 ctx.beginPath();
1796 }
1797
1798 if (index > 0){
1799 // This is a grid line in the centre, so drop that
1800 ctx.lineWidth = this.gridLineWidth;
1801 ctx.strokeStyle = this.gridLineColor;
1802 } else {
1803 // This is the first line on the scale
1804 ctx.lineWidth = this.lineWidth;
1805 ctx.strokeStyle = this.lineColor;
1806 }
1807
1808 if (drawVerticalLine){
1809 ctx.moveTo(linePos,this.endPoint);
1810 ctx.lineTo(linePos,this.startPoint - 3);
1811 ctx.stroke();
1812 ctx.closePath();
1813 }
1814
1815
1816 ctx.lineWidth = this.lineWidth;
1817 ctx.strokeStyle = this.lineColor;
1818
1819
1820 // Small lines at the bottom of the base grid line
1821 ctx.beginPath();
1822 ctx.moveTo(linePos,this.endPoint);
1823 ctx.lineTo(linePos,this.endPoint + 5);
1824 ctx.stroke();
1825 ctx.closePath();
1826
1827 ctx.save();
1828 ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
1829 ctx.rotate(toRadians(this.xLabelRotation)*-1);
1830 ctx.font = this.font;
1831 ctx.textAlign = (isRotated) ? "right" : "center";
1832 ctx.textBaseline = (isRotated) ? "middle" : "top";
1833 ctx.fillText(label, 0, 0);
1834 ctx.restore();
1835 },this);
1836
1837 }
1838 }
1839
1840 });
1841
1842 Chart.RadialScale = Chart.Element.extend({
1843 initialize: function(){
1844 this.size = min([this.height, this.width]);
1845 this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
1846 },
1847 calculateCenterOffset: function(value){
1848 // Take into account half font size + the yPadding of the top value
1849 var scalingFactor = this.drawingArea / (this.max - this.min);
1850
1851 return (value - this.min) * scalingFactor;
1852 },
1853 update : function(){
1854 if (!this.lineArc){
1855 this.setScaleSize();
1856 } else {
1857 this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
1858 }
1859 this.buildYLabels();
1860 },
1861 buildYLabels: function(){
1862 this.yLabels = [];
1863
1864 var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
1865
1866 for (var i=0; i<=this.steps; i++){
1867 this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
1868 }
1869 },
1870 getCircumference : function(){
1871 return ((Math.PI*2) / this.valuesCount);
1872 },
1873 setScaleSize: function(){
1874 /*
1875 * Right, this is really confusing and there is a lot of maths going on here
1876 * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
1877 *
1878 * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
1879 *
1880 * Solution:
1881 *
1882 * We assume the radius of the polygon is half the size of the canvas at first
1883 * at each index we check if the text overlaps.
1884 *
1885 * Where it does, we store that angle and that index.
1886 *
1887 * After finding the largest index and angle we calculate how much we need to remove
1888 * from the shape radius to move the point inwards by that x.
1889 *
1890 * We average the left and right distances to get the maximum shape radius that can fit in the box
1891 * along with labels.
1892 *
1893 * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
1894 * on each side, removing that from the size, halving it and adding the left x protrusion width.
1895 *
1896 * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
1897 * and position it in the most space efficient manner
1898 *
1899 * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
1900 */
1901
1902
1903 // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
1904 // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
1905 var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
1906 pointPosition,
1907 i,
1908 textWidth,
1909 halfTextWidth,
1910 furthestRight = this.width,
1911 furthestRightIndex,
1912 furthestRightAngle,
1913 furthestLeft = 0,
1914 furthestLeftIndex,
1915 furthestLeftAngle,
1916 xProtrusionLeft,
1917 xProtrusionRight,
1918 radiusReductionRight,
1919 radiusReductionLeft,
1920 maxWidthRadius;
1921 this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
1922 for (i=0;i<this.valuesCount;i++){
1923 // 5px to space the text slightly out - similar to what we do in the draw function.
1924 pointPosition = this.getPointPosition(i, largestPossibleRadius);
1925 textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5;
1926 if (i === 0 || i === this.valuesCount/2){
1927 // If we're at index zero, or exactly the middle, we're at exactly the top/bottom
1928 // of the radar chart, so text will be aligned centrally, so we'll half it and compare
1929 // w/left and right text sizes
1930 halfTextWidth = textWidth/2;
1931 if (pointPosition.x + halfTextWidth > furthestRight) {
1932 furthestRight = pointPosition.x + halfTextWidth;
1933 furthestRightIndex = i;
1934 }
1935 if (pointPosition.x - halfTextWidth < furthestLeft) {
1936 furthestLeft = pointPosition.x - halfTextWidth;
1937 furthestLeftIndex = i;
1938 }
1939 }
1940 else if (i < this.valuesCount/2) {
1941 // Less than half the values means we'll left align the text
1942 if (pointPosition.x + textWidth > furthestRight) {
1943 furthestRight = pointPosition.x + textWidth;
1944 furthestRightIndex = i;
1945 }
1946 }
1947 else if (i > this.valuesCount/2){
1948 // More than half the values means we'll right align the text
1949 if (pointPosition.x - textWidth < furthestLeft) {
1950 furthestLeft = pointPosition.x - textWidth;
1951 furthestLeftIndex = i;
1952 }
1953 }
1954 }
1955
1956 xProtrusionLeft = furthestLeft;
1957
1958 xProtrusionRight = Math.ceil(furthestRight - this.width);
1959
1960 furthestRightAngle = this.getIndexAngle(furthestRightIndex);
1961
1962 furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
1963
1964 radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
1965
1966 radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
1967
1968 // Ensure we actually need to reduce the size of the chart
1969 radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
1970 radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
1971
1972 this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
1973
1974 //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
1975 this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
1976
1977 },
1978 setCenterPoint: function(leftMovement, rightMovement){
1979
1980 var maxRight = this.width - rightMovement - this.drawingArea,
1981 maxLeft = leftMovement + this.drawingArea;
1982
1983 this.xCenter = (maxLeft + maxRight)/2;
1984 // Always vertically in the centre as the text height doesn't change
1985 this.yCenter = (this.height/2);
1986 },
1987
1988 getIndexAngle : function(index){
1989 var angleMultiplier = (Math.PI * 2) / this.valuesCount;
1990 // Start from the top instead of right, so remove a quarter of the circle
1991
1992 return index * angleMultiplier - (Math.PI/2);
1993 },
1994 getPointPosition : function(index, distanceFromCenter){
1995 var thisAngle = this.getIndexAngle(index);
1996 return {
1997 x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
1998 y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
1999 };
2000 },
2001 draw: function(){
2002 if (this.display){
2003 var ctx = this.ctx;
2004 each(this.yLabels, function(label, index){
2005 // Don't draw a centre value
2006 if (index > 0){
2007 var yCenterOffset = index * (this.drawingArea/this.steps),
2008 yHeight = this.yCenter - yCenterOffset,
2009 pointPosition;
2010
2011 // Draw circular lines around the scale
2012 if (this.lineWidth > 0){
2013 ctx.strokeStyle = this.lineColor;
2014 ctx.lineWidth = this.lineWidth;
2015
2016 if(this.lineArc){
2017 ctx.beginPath();
2018 ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
2019 ctx.closePath();
2020 ctx.stroke();
2021 } else{
2022 ctx.beginPath();
2023 for (var i=0;i<this.valuesCount;i++)
2024 {
2025 pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue)));
2026 if (i === 0){
2027 ctx.moveTo(pointPosition.x, pointPosition.y);
2028 } else {
2029 ctx.lineTo(pointPosition.x, pointPosition.y);
2030 }
2031 }
2032 ctx.closePath();
2033 ctx.stroke();
2034 }
2035 }
2036 if(this.showLabels){
2037 ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
2038 if (this.showLabelBackdrop){
2039 var labelWidth = ctx.measureText(label).width;
2040 ctx.fillStyle = this.backdropColor;
2041 ctx.fillRect(
2042 this.xCenter - labelWidth/2 - this.backdropPaddingX,
2043 yHeight - this.fontSize/2 - this.backdropPaddingY,
2044 labelWidth + this.backdropPaddingX*2,
2045 this.fontSize + this.backdropPaddingY*2
2046 );
2047 }
2048 ctx.textAlign = 'center';
2049 ctx.textBaseline = "middle";
2050 ctx.fillStyle = this.fontColor;
2051 ctx.fillText(label, this.xCenter, yHeight);
2052 }
2053 }
2054 }, this);
2055
2056 if (!this.lineArc){
2057 ctx.lineWidth = this.angleLineWidth;
2058 ctx.strokeStyle = this.angleLineColor;
2059 for (var i = this.valuesCount - 1; i >= 0; i--) {
2060 var centerOffset = null, outerPosition = null;
2061
2062 if (this.angleLineWidth > 0){
2063 centerOffset = this.calculateCenterOffset(this.max);
2064 outerPosition = this.getPointPosition(i, centerOffset);
2065 ctx.beginPath();
2066 ctx.moveTo(this.xCenter, this.yCenter);
2067 ctx.lineTo(outerPosition.x, outerPosition.y);
2068 ctx.stroke();
2069 ctx.closePath();
2070 }
2071
2072 if (this.backgroundColors && this.backgroundColors.length == this.valuesCount) {
2073 if (centerOffset == null)
2074 centerOffset = this.calculateCenterOffset(this.max);
2075
2076 if (outerPosition == null)
2077 outerPosition = this.getPointPosition(i, centerOffset);
2078
2079 var previousOuterPosition = this.getPointPosition(i === 0 ? this.valuesCount - 1 : i - 1, centerOffset);
2080 var nextOuterPosition = this.getPointPosition(i === this.valuesCount - 1 ? 0 : i + 1, centerOffset);
2081
2082 var previousOuterHalfway = { x: (previousOuterPosition.x + outerPosition.x) / 2, y: (previousOuterPosition.y + outerPosition.y) / 2 };
2083 var nextOuterHalfway = { x: (outerPosition.x + nextOuterPosition.x) / 2, y: (outerPosition.y + nextOuterPosition.y) / 2 };
2084
2085 ctx.beginPath();
2086 ctx.moveTo(this.xCenter, this.yCenter);
2087 ctx.lineTo(previousOuterHalfway.x, previousOuterHalfway.y);
2088 ctx.lineTo(outerPosition.x, outerPosition.y);
2089 ctx.lineTo(nextOuterHalfway.x, nextOuterHalfway.y);
2090 ctx.fillStyle = this.backgroundColors[i];
2091 ctx.fill();
2092 ctx.closePath();
2093 }
2094 // Extra 3px out for some label spacing
2095 var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
2096 ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
2097 ctx.fillStyle = this.pointLabelFontColor;
2098
2099 var labelsCount = this.labels.length,
2100 halfLabelsCount = this.labels.length/2,
2101 quarterLabelsCount = halfLabelsCount/2,
2102 upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
2103 exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
2104 if (i === 0){
2105 ctx.textAlign = 'center';
2106 } else if(i === halfLabelsCount){
2107 ctx.textAlign = 'center';
2108 } else if (i < halfLabelsCount){
2109 ctx.textAlign = 'left';
2110 } else {
2111 ctx.textAlign = 'right';
2112 }
2113
2114 // Set the correct text baseline based on outer positioning
2115 if (exactQuarter){
2116 ctx.textBaseline = 'middle';
2117 } else if (upperHalf){
2118 ctx.textBaseline = 'bottom';
2119 } else {
2120 ctx.textBaseline = 'top';
2121 }
2122
2123 ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
2124 }
2125 }
2126 }
2127 }
2128 });
2129
2130 Chart.animationService = {
2131 frameDuration: 17,
2132 animations: [],
2133 dropFrames: 0,
2134 addAnimation: function(chartInstance, animationObject) {
2135 for (var index = 0; index < this.animations.length; ++ index){
2136 if (this.animations[index].chartInstance === chartInstance){
2137 // replacing an in progress animation
2138 this.animations[index].animationObject = animationObject;
2139 return;
2140 }
2141 }
2142
2143 this.animations.push({
2144 chartInstance: chartInstance,
2145 animationObject: animationObject
2146 });
2147
2148 // If there are no animations queued, manually kickstart a digest, for lack of a better word
2149 if (this.animations.length == 1) {
2150 helpers.requestAnimFrame.call(window, this.digestWrapper);
2151 }
2152 },
2153 // Cancel the animation for a given chart instance
2154 cancelAnimation: function(chartInstance) {
2155 var index = helpers.findNextWhere(this.animations, function(animationWrapper) {
2156 return animationWrapper.chartInstance === chartInstance;
2157 });
2158
2159 if (index)
2160 {
2161 this.animations.splice(index, 1);
2162 }
2163 },
2164 // calls startDigest with the proper context
2165 digestWrapper: function() {
2166 Chart.animationService.startDigest.call(Chart.animationService);
2167 },
2168 startDigest: function() {
2169
2170 var startTime = Date.now();
2171 var framesToDrop = 0;
2172
2173 if(this.dropFrames > 1){
2174 framesToDrop = Math.floor(this.dropFrames);
2175 this.dropFrames -= framesToDrop;
2176 }
2177
2178 for (var i = 0; i < this.animations.length; i++) {
2179
2180 if (this.animations[i].animationObject.currentStep === null){
2181 this.animations[i].animationObject.currentStep = 0;
2182 }
2183
2184 this.animations[i].animationObject.currentStep += 1 + framesToDrop;
2185 if(this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps){
2186 this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps;
2187 }
2188
2189 this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject);
2190
2191 // Check if executed the last frame.
2192 if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps){
2193 // Call onAnimationComplete
2194 this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance);
2195 // Remove the animation.
2196 this.animations.splice(i, 1);
2197 // Keep the index in place to offset the splice
2198 i--;
2199 }
2200 }
2201
2202 var endTime = Date.now();
2203 var delay = endTime - startTime - this.frameDuration;
2204 var frameDelay = delay / this.frameDuration;
2205
2206 if(frameDelay > 1){
2207 this.dropFrames += frameDelay;
2208 }
2209
2210 // Do we have more stuff to animate?
2211 if (this.animations.length > 0){
2212 helpers.requestAnimFrame.call(window, this.digestWrapper);
2213 }
2214 }
2215 };
2216
2217 // Attach global event to resize each chart instance when the browser resizes
2218 helpers.addEvent(window, "resize", (function(){
2219 // Basic debounce of resize function so it doesn't hurt performance when resizing browser.
2220 var timeout;
2221 return function(){
2222 clearTimeout(timeout);
2223 timeout = setTimeout(function(){
2224 each(Chart.instances,function(instance){
2225 // If the responsive flag is set in the chart instance config
2226 // Cascade the resize event down to the chart.
2227 if (instance.options.responsive){
2228 instance.resize(instance.render, true);
2229 }
2230 });
2231 }, 50);
2232 };
2233 })());
2234
2235
2236 if (amd) {
2237 define('Chart', [], function(){
2238 return Chart;
2239 });
2240 } else if (typeof module === 'object' && module.exports) {
2241 module.exports = Chart;
2242 }
2243
2244 root.Chart = Chart;
2245
2246 Chart.noConflict = function(){
2247 root.Chart = previous;
2248 return Chart;
2249 };
2250
2251 }).call(this);
2252
2253 (function(){
2254 "use strict";
2255
2256 var root = this,
2257 Chart = root.Chart,
2258 helpers = Chart.helpers;
2259
2260
2261 var defaultConfig = {
2262 //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
2263 scaleBeginAtZero : true,
2264
2265 //Boolean - Whether grid lines are shown across the chart
2266 scaleShowGridLines : true,
2267
2268 //String - Colour of the grid lines
2269 scaleGridLineColor : "rgba(0,0,0,.05)",
2270
2271 //Number - Width of the grid lines
2272 scaleGridLineWidth : 1,
2273
2274 //Boolean - Whether to show horizontal lines (except X axis)
2275 scaleShowHorizontalLines: true,
2276
2277 //Boolean - Whether to show vertical lines (except Y axis)
2278 scaleShowVerticalLines: true,
2279
2280 //Boolean - If there is a stroke on each bar
2281 barShowStroke : true,
2282
2283 //Number - Pixel width of the bar stroke
2284 barStrokeWidth : 2,
2285
2286 //Number - Spacing between each of the X value sets
2287 barValueSpacing : 5,
2288
2289 //Number - Spacing between data sets within X values
2290 barDatasetSpacing : 1,
2291
2292 //String - A legend template
2293 legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
2294
2295 };
2296
2297
2298 Chart.Type.extend({
2299 name: "Bar",
2300 defaults : defaultConfig,
2301 initialize: function(data){
2302
2303 //Expose options as a scope variable here so we can access it in the ScaleClass
2304 var options = this.options;
2305
2306 this.ScaleClass = Chart.Scale.extend({
2307 offsetGridLines : true,
2308 calculateBarX : function(datasetCount, datasetIndex, barIndex){
2309 //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
2310 var xWidth = this.calculateBaseWidth(),
2311 xAbsolute = this.calculateX(barIndex) - (xWidth/2),
2312 barWidth = this.calculateBarWidth(datasetCount);
2313
2314 return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
2315 },
2316 calculateBaseWidth : function(){
2317 return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing);
2318 },
2319 calculateBarWidth : function(datasetCount){
2320 //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
2321 var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
2322
2323 return (baseWidth / datasetCount);
2324 }
2325 });
2326
2327 this.datasets = [];
2328
2329 //Set up tooltip events on the chart
2330 if (this.options.showTooltips){
2331 helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2332 var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
2333
2334 this.eachBars(function(bar){
2335 bar.restore(['fillColor', 'strokeColor']);
2336 });
2337 helpers.each(activeBars, function(activeBar){
2338 activeBar.fillColor = activeBar.highlightFill;
2339 activeBar.strokeColor = activeBar.highlightStroke;
2340 });
2341 this.showTooltip(activeBars);
2342 });
2343 }
2344
2345 //Declare the extension of the default point, to cater for the options passed in to the constructor
2346 this.BarClass = Chart.Rectangle.extend({
2347 strokeWidth : this.options.barStrokeWidth,
2348 showStroke : this.options.barShowStroke,
2349 ctx : this.chart.ctx
2350 });
2351
2352 //Iterate through each of the datasets, and build this into a property of the chart
2353 helpers.each(data.datasets,function(dataset,datasetIndex){
2354
2355 var datasetObject = {
2356 label : dataset.label || null,
2357 fillColor : dataset.fillColor,
2358 strokeColor : dataset.strokeColor,
2359 bars : []
2360 };
2361
2362 this.datasets.push(datasetObject);
2363
2364 helpers.each(dataset.data,function(dataPoint,index){
2365 //Add a new point for each piece of data, passing any required data to draw.
2366 datasetObject.bars.push(new this.BarClass({
2367 value : dataPoint,
2368 label : data.labels[index],
2369 datasetLabel: dataset.label,
2370 strokeColor : (typeof dataset.strokeColor != 'string') ? dataset.strokeColor[index] : dataset.strokeColor,
2371 fillColor : (typeof dataset.fillColor != 'string') ? dataset.fillColor[index] : dataset.fillColor,
2372 highlightFill : (dataset.highlightFill && typeof dataset.highlightFill != 'string') ? dataset.highlightFill[index] || dataset.highlightFill : (typeof dataset.fillColor != 'string') ? dataset.fillColor[index] : dataset.fillColor,
2373 highlightStroke : (dataset.highlightStroke && typeof dataset.highlightStroke != 'string') ? dataset.highlightStroke[index] || dataset.highlightStroke : (typeof dataset.strokeColor != 'string') ? dataset.strokeColor[index] : dataset.strokeColor
2374 }));
2375 },this);
2376
2377 },this);
2378
2379 this.buildScale(data.labels);
2380
2381 this.BarClass.prototype.base = this.scale.endPoint;
2382
2383 this.eachBars(function(bar, index, datasetIndex){
2384 helpers.extend(bar, {
2385 width : this.scale.calculateBarWidth(this.datasets.length),
2386 x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
2387 y: this.scale.endPoint
2388 });
2389 bar.save();
2390 }, this);
2391
2392 this.render();
2393 },
2394 update : function(){
2395 this.scale.update();
2396 // Reset any highlight colours before updating.
2397 helpers.each(this.activeElements, function(activeElement){
2398 activeElement.restore(['fillColor', 'strokeColor']);
2399 });
2400
2401 this.eachBars(function(bar){
2402 bar.save();
2403 });
2404 this.render();
2405 },
2406 eachBars : function(callback){
2407 helpers.each(this.datasets,function(dataset, datasetIndex){
2408 helpers.each(dataset.bars, callback, this, datasetIndex);
2409 },this);
2410 },
2411 getBarsAtEvent : function(e){
2412 var barsArray = [],
2413 eventPosition = helpers.getRelativePosition(e),
2414 datasetIterator = function(dataset){
2415 barsArray.push(dataset.bars[barIndex]);
2416 },
2417 barIndex;
2418
2419 for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) {
2420 for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) {
2421 if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
2422 helpers.each(this.datasets, datasetIterator);
2423 return barsArray;
2424 }
2425 }
2426 }
2427
2428 return barsArray;
2429 },
2430 buildScale : function(labels){
2431 var self = this;
2432
2433 var dataTotal = function(){
2434 var values = [];
2435 self.eachBars(function(bar){
2436 values.push(bar.value);
2437 });
2438 return values;
2439 };
2440
2441 var scaleOptions = {
2442 templateString : this.options.scaleLabel,
2443 height : this.chart.height,
2444 width : this.chart.width,
2445 ctx : this.chart.ctx,
2446 textColor : this.options.scaleFontColor,
2447 fontSize : this.options.scaleFontSize,
2448 fontStyle : this.options.scaleFontStyle,
2449 fontFamily : this.options.scaleFontFamily,
2450 valuesCount : labels.length,
2451 beginAtZero : this.options.scaleBeginAtZero,
2452 integersOnly : this.options.scaleIntegersOnly,
2453 calculateYRange: function(currentHeight){
2454 var updatedRanges = helpers.calculateScaleRange(
2455 dataTotal(),
2456 currentHeight,
2457 this.fontSize,
2458 this.beginAtZero,
2459 this.integersOnly
2460 );
2461 helpers.extend(this, updatedRanges);
2462 },
2463 xLabels : labels,
2464 font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
2465 lineWidth : this.options.scaleLineWidth,
2466 lineColor : this.options.scaleLineColor,
2467 showHorizontalLines : this.options.scaleShowHorizontalLines,
2468 showVerticalLines : this.options.scaleShowVerticalLines,
2469 gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
2470 gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
2471 padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
2472 showLabels : this.options.scaleShowLabels,
2473 display : this.options.showScale
2474 };
2475
2476 if (this.options.scaleOverride){
2477 helpers.extend(scaleOptions, {
2478 calculateYRange: helpers.noop,
2479 steps: this.options.scaleSteps,
2480 stepValue: this.options.scaleStepWidth,
2481 min: this.options.scaleStartValue,
2482 max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
2483 });
2484 }
2485
2486 this.scale = new this.ScaleClass(scaleOptions);
2487 },
2488 addData : function(valuesArray,label){
2489 //Map the values array for each of the datasets
2490 helpers.each(valuesArray,function(value,datasetIndex){
2491 //Add a new point for each piece of data, passing any required data to draw.
2492 this.datasets[datasetIndex].bars.push(new this.BarClass({
2493 value : value,
2494 label : label,
2495 datasetLabel: this.datasets[datasetIndex].label,
2496 x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1),
2497 y: this.scale.endPoint,
2498 width : this.scale.calculateBarWidth(this.datasets.length),
2499 base : this.scale.endPoint,
2500 strokeColor : this.datasets[datasetIndex].strokeColor,
2501 fillColor : this.datasets[datasetIndex].fillColor
2502 }));
2503 },this);
2504
2505 this.scale.addXLabel(label);
2506 //Then re-render the chart.
2507 this.update();
2508 },
2509 removeData : function(){
2510 this.scale.removeXLabel();
2511 //Then re-render the chart.
2512 helpers.each(this.datasets,function(dataset){
2513 dataset.bars.shift();
2514 },this);
2515 this.update();
2516 },
2517 reflow : function(){
2518 helpers.extend(this.BarClass.prototype,{
2519 y: this.scale.endPoint,
2520 base : this.scale.endPoint
2521 });
2522 var newScaleProps = helpers.extend({
2523 height : this.chart.height,
2524 width : this.chart.width
2525 });
2526 this.scale.update(newScaleProps);
2527 },
2528 draw : function(ease){
2529 var easingDecimal = ease || 1;
2530 this.clear();
2531
2532 var ctx = this.chart.ctx;
2533
2534 this.scale.draw(easingDecimal);
2535
2536 //Draw all the bars for each dataset
2537 helpers.each(this.datasets,function(dataset,datasetIndex){
2538 helpers.each(dataset.bars,function(bar,index){
2539 if (bar.hasValue()){
2540 bar.base = this.scale.endPoint;
2541 //Transition then draw
2542 bar.transition({
2543 x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
2544 y : this.scale.calculateY(bar.value),
2545 width : this.scale.calculateBarWidth(this.datasets.length)
2546 }, easingDecimal).draw();
2547 }
2548 },this);
2549
2550 },this);
2551 }
2552 });
2553
2554
2555 }).call(this);
2556
2557 (function(){
2558 "use strict";
2559
2560 var root = this,
2561 Chart = root.Chart,
2562 //Cache a local reference to Chart.helpers
2563 helpers = Chart.helpers;
2564
2565 var defaultConfig = {
2566 //Boolean - Whether we should show a stroke on each segment
2567 segmentShowStroke : true,
2568
2569 //String - The colour of each segment stroke
2570 segmentStrokeColor : "#fff",
2571
2572 //Number - The width of each segment stroke
2573 segmentStrokeWidth : 2,
2574
2575 //The percentage of the chart that we cut out of the middle.
2576 percentageInnerCutout : 50,
2577
2578 //Number - Amount of animation steps
2579 animationSteps : 100,
2580
2581 //String - Animation easing effect
2582 animationEasing : "easeOutBounce",
2583
2584 //Boolean - Whether we animate the rotation of the Doughnut
2585 animateRotate : true,
2586
2587 //Boolean - Whether we animate scaling the Doughnut from the centre
2588 animateScale : false,
2589
2590 //String - A legend template
2591 legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>"
2592
2593 };
2594
2595 Chart.Type.extend({
2596 //Passing in a name registers this chart in the Chart namespace
2597 name: "Doughnut",
2598 //Providing a defaults will also register the deafults in the chart namespace
2599 defaults : defaultConfig,
2600 //Initialize is fired when the chart is initialized - Data is passed in as a parameter
2601 //Config is automatically merged by the core of Chart.js, and is available at this.options
2602 initialize: function(data){
2603
2604 //Declare segments as a static property to prevent inheriting across the Chart type prototype
2605 this.segments = [];
2606 this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2;
2607
2608 this.SegmentArc = Chart.Arc.extend({
2609 ctx : this.chart.ctx,
2610 x : this.chart.width/2,
2611 y : this.chart.height/2
2612 });
2613
2614 //Set up tooltip events on the chart
2615 if (this.options.showTooltips){
2616 helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2617 var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
2618
2619 helpers.each(this.segments,function(segment){
2620 segment.restore(["fillColor"]);
2621 });
2622 helpers.each(activeSegments,function(activeSegment){
2623 activeSegment.fillColor = activeSegment.highlightColor;
2624 });
2625 this.showTooltip(activeSegments);
2626 });
2627 }
2628 this.calculateTotal(data);
2629
2630 helpers.each(data,function(datapoint, index){
2631 if (!datapoint.color) {
2632 datapoint.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)';
2633 }
2634 this.addData(datapoint, index, true);
2635 },this);
2636
2637 this.render();
2638 },
2639 getSegmentsAtEvent : function(e){
2640 var segmentsArray = [];
2641
2642 var location = helpers.getRelativePosition(e);
2643
2644 helpers.each(this.segments,function(segment){
2645 if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
2646 },this);
2647 return segmentsArray;
2648 },
2649 addData : function(segment, atIndex, silent){
2650 var index = atIndex !== undefined ? atIndex : this.segments.length;
2651 if ( typeof(segment.color) === "undefined" ) {
2652 segment.color = Chart.defaults.global.segmentColorDefault[index % Chart.defaults.global.segmentColorDefault.length];
2653 segment.highlight = Chart.defaults.global.segmentHighlightColorDefaults[index % Chart.defaults.global.segmentHighlightColorDefaults.length];
2654 }
2655 this.segments.splice(index, 0, new this.SegmentArc({
2656 value : segment.value,
2657 outerRadius : (this.options.animateScale) ? 0 : this.outerRadius,
2658 innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout,
2659 fillColor : segment.color,
2660 highlightColor : segment.highlight || segment.color,
2661 showStroke : this.options.segmentShowStroke,
2662 strokeWidth : this.options.segmentStrokeWidth,
2663 strokeColor : this.options.segmentStrokeColor,
2664 startAngle : Math.PI * 1.5,
2665 circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value),
2666 label : segment.label
2667 }));
2668 if (!silent){
2669 this.reflow();
2670 this.update();
2671 }
2672 },
2673 calculateCircumference : function(value) {
2674 if ( this.total > 0 ) {
2675 return (Math.PI*2)*(value / this.total);
2676 } else {
2677 return 0;
2678 }
2679 },
2680 calculateTotal : function(data){
2681 this.total = 0;
2682 helpers.each(data,function(segment){
2683 this.total += Math.abs(segment.value);
2684 },this);
2685 },
2686 update : function(){
2687 this.calculateTotal(this.segments);
2688
2689 // Reset any highlight colours before updating.
2690 helpers.each(this.activeElements, function(activeElement){
2691 activeElement.restore(['fillColor']);
2692 });
2693
2694 helpers.each(this.segments,function(segment){
2695 segment.save();
2696 });
2697 this.render();
2698 },
2699
2700 removeData: function(atIndex){
2701 var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
2702 this.segments.splice(indexToDelete, 1);
2703 this.reflow();
2704 this.update();
2705 },
2706
2707 reflow : function(){
2708 helpers.extend(this.SegmentArc.prototype,{
2709 x : this.chart.width/2,
2710 y : this.chart.height/2
2711 });
2712 this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2;
2713 helpers.each(this.segments, function(segment){
2714 segment.update({
2715 outerRadius : this.outerRadius,
2716 innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
2717 });
2718 }, this);
2719 },
2720 draw : function(easeDecimal){
2721 var animDecimal = (easeDecimal) ? easeDecimal : 1;
2722 this.clear();
2723 helpers.each(this.segments,function(segment,index){
2724 segment.transition({
2725 circumference : this.calculateCircumference(segment.value),
2726 outerRadius : this.outerRadius,
2727 innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
2728 },animDecimal);
2729
2730 segment.endAngle = segment.startAngle + segment.circumference;
2731
2732 segment.draw();
2733 if (index === 0){
2734 segment.startAngle = Math.PI * 1.5;
2735 }
2736 //Check to see if it's the last segment, if not get the next and update the start angle
2737 if (index < this.segments.length-1){
2738 this.segments[index+1].startAngle = segment.endAngle;
2739 }
2740 },this);
2741
2742 }
2743 });
2744
2745 Chart.types.Doughnut.extend({
2746 name : "Pie",
2747 defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0})
2748 });
2749
2750 }).call(this);
2751
2752 (function(){
2753 "use strict";
2754
2755 var root = this,
2756 Chart = root.Chart,
2757 helpers = Chart.helpers;
2758
2759 var defaultConfig = {
2760
2761 ///Boolean - Whether grid lines are shown across the chart
2762 scaleShowGridLines : true,
2763
2764 //String - Colour of the grid lines
2765 scaleGridLineColor : "rgba(0,0,0,.05)",
2766
2767 //Number - Width of the grid lines
2768 scaleGridLineWidth : 1,
2769
2770 //Boolean - Whether to show horizontal lines (except X axis)
2771 scaleShowHorizontalLines: true,
2772
2773 //Boolean - Whether to show vertical lines (except Y axis)
2774 scaleShowVerticalLines: true,
2775
2776 //Boolean - Whether the line is curved between points
2777 bezierCurve : true,
2778
2779 //Number - Tension of the bezier curve between points
2780 bezierCurveTension : 0.4,
2781
2782 //Boolean - Whether to show a dot for each point
2783 pointDot : true,
2784
2785 //Number - Radius of each point dot in pixels
2786 pointDotRadius : 4,
2787
2788 //Number - Pixel width of point dot stroke
2789 pointDotStrokeWidth : 1,
2790
2791 //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
2792 pointHitDetectionRadius : 20,
2793
2794 //Boolean - Whether to show a stroke for datasets
2795 datasetStroke : true,
2796
2797 //Number - Pixel width of dataset stroke
2798 datasetStrokeWidth : 2,
2799
2800 //Boolean - Whether to fill the dataset with a colour
2801 datasetFill : true,
2802
2803 //String - A legend template
2804 legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>",
2805
2806 //Boolean - Whether to horizontally center the label and point dot inside the grid
2807 offsetGridLines : false
2808
2809 };
2810
2811
2812 Chart.Type.extend({
2813 name: "Line",
2814 defaults : defaultConfig,
2815 initialize: function(data){
2816 //Declare the extension of the default point, to cater for the options passed in to the constructor
2817 this.PointClass = Chart.Point.extend({
2818 offsetGridLines : this.options.offsetGridLines,
2819 strokeWidth : this.options.pointDotStrokeWidth,
2820 radius : this.options.pointDotRadius,
2821 display: this.options.pointDot,
2822 hitDetectionRadius : this.options.pointHitDetectionRadius,
2823 ctx : this.chart.ctx,
2824 inRange : function(mouseX){
2825 return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
2826 }
2827 });
2828
2829 this.datasets = [];
2830
2831 //Set up tooltip events on the chart
2832 if (this.options.showTooltips){
2833 helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
2834 var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
2835 this.eachPoints(function(point){
2836 point.restore(['fillColor', 'strokeColor']);
2837 });
2838 helpers.each(activePoints, function(activePoint){
2839 activePoint.fillColor = activePoint.highlightFill;
2840 activePoint.strokeColor = activePoint.highlightStroke;
2841 });
2842 this.showTooltip(activePoints);
2843 });
2844 }
2845
2846 //Iterate through each of the datasets, and build this into a property of the chart
2847 helpers.each(data.datasets,function(dataset){
2848
2849 var datasetObject = {
2850 label : dataset.label || null,
2851 fillColor : dataset.fillColor,
2852 strokeColor : dataset.strokeColor,
2853 pointColor : dataset.pointColor,
2854 pointStrokeColor : dataset.pointStrokeColor,
2855 points : []
2856 };
2857
2858 this.datasets.push(datasetObject);
2859
2860
2861 helpers.each(dataset.data,function(dataPoint,index){
2862 //Add a new point for each piece of data, passing any required data to draw.
2863 datasetObject.points.push(new this.PointClass({
2864 value : dataPoint,
2865 label : data.labels[index],
2866 datasetLabel: dataset.label,
2867 strokeColor : dataset.pointStrokeColor,
2868 fillColor : dataset.pointColor,
2869 highlightFill : dataset.pointHighlightFill || dataset.pointColor,
2870 highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
2871 }));
2872 },this);
2873
2874 this.buildScale(data.labels);
2875
2876
2877 this.eachPoints(function(point, index){
2878 helpers.extend(point, {
2879 x: this.scale.calculateX(index),
2880 y: this.scale.endPoint
2881 });
2882 point.save();
2883 }, this);
2884
2885 },this);
2886
2887
2888 this.render();
2889 },
2890 update : function(){
2891 this.scale.update();
2892 // Reset any highlight colours before updating.
2893 helpers.each(this.activeElements, function(activeElement){
2894 activeElement.restore(['fillColor', 'strokeColor']);
2895 });
2896 this.eachPoints(function(point){
2897 point.save();
2898 });
2899 this.render();
2900 },
2901 eachPoints : function(callback){
2902 helpers.each(this.datasets,function(dataset){
2903 helpers.each(dataset.points,callback,this);
2904 },this);
2905 },
2906 getPointsAtEvent : function(e){
2907 var pointsArray = [],
2908 eventPosition = helpers.getRelativePosition(e);
2909 helpers.each(this.datasets,function(dataset){
2910 helpers.each(dataset.points,function(point){
2911 if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
2912 });
2913 },this);
2914 return pointsArray;
2915 },
2916 buildScale : function(labels){
2917 var self = this;
2918
2919 var dataTotal = function(){
2920 var values = [];
2921 self.eachPoints(function(point){
2922 values.push(point.value);
2923 });
2924
2925 return values;
2926 };
2927
2928 var scaleOptions = {
2929 templateString : this.options.scaleLabel,
2930 height : this.chart.height,
2931 width : this.chart.width,
2932 ctx : this.chart.ctx,
2933 textColor : this.options.scaleFontColor,
2934 offsetGridLines : this.options.offsetGridLines,
2935 fontSize : this.options.scaleFontSize,
2936 fontStyle : this.options.scaleFontStyle,
2937 fontFamily : this.options.scaleFontFamily,
2938 valuesCount : labels.length,
2939 beginAtZero : this.options.scaleBeginAtZero,
2940 integersOnly : this.options.scaleIntegersOnly,
2941 calculateYRange : function(currentHeight){
2942 var updatedRanges = helpers.calculateScaleRange(
2943 dataTotal(),
2944 currentHeight,
2945 this.fontSize,
2946 this.beginAtZero,
2947 this.integersOnly
2948 );
2949 helpers.extend(this, updatedRanges);
2950 },
2951 xLabels : labels,
2952 font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
2953 lineWidth : this.options.scaleLineWidth,
2954 lineColor : this.options.scaleLineColor,
2955 showHorizontalLines : this.options.scaleShowHorizontalLines,
2956 showVerticalLines : this.options.scaleShowVerticalLines,
2957 gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
2958 gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
2959 padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
2960 showLabels : this.options.scaleShowLabels,
2961 display : this.options.showScale
2962 };
2963
2964 if (this.options.scaleOverride){
2965 helpers.extend(scaleOptions, {
2966 calculateYRange: helpers.noop,
2967 steps: this.options.scaleSteps,
2968 stepValue: this.options.scaleStepWidth,
2969 min: this.options.scaleStartValue,
2970 max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
2971 });
2972 }
2973
2974
2975 this.scale = new Chart.Scale(scaleOptions);
2976 },
2977 addData : function(valuesArray,label){
2978 //Map the values array for each of the datasets
2979
2980 helpers.each(valuesArray,function(value,datasetIndex){
2981 //Add a new point for each piece of data, passing any required data to draw.
2982 this.datasets[datasetIndex].points.push(new this.PointClass({
2983 value : value,
2984 label : label,
2985 datasetLabel: this.datasets[datasetIndex].label,
2986 x: this.scale.calculateX(this.scale.valuesCount+1),
2987 y: this.scale.endPoint,
2988 strokeColor : this.datasets[datasetIndex].pointStrokeColor,
2989 fillColor : this.datasets[datasetIndex].pointColor
2990 }));
2991 },this);
2992
2993 this.scale.addXLabel(label);
2994 //Then re-render the chart.
2995 this.update();
2996 },
2997 removeData : function(){
2998 this.scale.removeXLabel();
2999 //Then re-render the chart.
3000 helpers.each(this.datasets,function(dataset){
3001 dataset.points.shift();
3002 },this);
3003 this.update();
3004 },
3005 reflow : function(){
3006 var newScaleProps = helpers.extend({
3007 height : this.chart.height,
3008 width : this.chart.width
3009 });
3010 this.scale.update(newScaleProps);
3011 },
3012 draw : function(ease){
3013 var easingDecimal = ease || 1;
3014 this.clear();
3015
3016 var ctx = this.chart.ctx;
3017
3018 // Some helper methods for getting the next/prev points
3019 var hasValue = function(item){
3020 return item.value !== null;
3021 },
3022 nextPoint = function(point, collection, index){
3023 return helpers.findNextWhere(collection, hasValue, index) || point;
3024 },
3025 previousPoint = function(point, collection, index){
3026 return helpers.findPreviousWhere(collection, hasValue, index) || point;
3027 };
3028
3029 if (!this.scale) return;
3030 this.scale.draw(easingDecimal);
3031
3032
3033 helpers.each(this.datasets,function(dataset){
3034 var pointsWithValues = helpers.where(dataset.points, hasValue);
3035
3036 //Transition each point first so that the line and point drawing isn't out of sync
3037 //We can use this extra loop to calculate the control points of this dataset also in this loop
3038
3039 helpers.each(dataset.points, function(point, index){
3040 if (point.hasValue()){
3041 point.transition({
3042 y : this.scale.calculateY(point.value),
3043 x : this.scale.calculateX(index)
3044 }, easingDecimal);
3045 }
3046 },this);
3047
3048
3049 // Control points need to be calculated in a separate loop, because we need to know the current x/y of the point
3050 // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
3051 if (this.options.bezierCurve){
3052 helpers.each(pointsWithValues, function(point, index){
3053 var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
3054 point.controlPoints = helpers.splineCurve(
3055 previousPoint(point, pointsWithValues, index),
3056 point,
3057 nextPoint(point, pointsWithValues, index),
3058 tension
3059 );
3060
3061 // Prevent the bezier going outside of the bounds of the graph
3062
3063 // Cap puter bezier handles to the upper/lower scale bounds
3064 if (point.controlPoints.outer.y > this.scale.endPoint){
3065 point.controlPoints.outer.y = this.scale.endPoint;
3066 }
3067 else if (point.controlPoints.outer.y < this.scale.startPoint){
3068 point.controlPoints.outer.y = this.scale.startPoint;
3069 }
3070
3071 // Cap inner bezier handles to the upper/lower scale bounds
3072 if (point.controlPoints.inner.y > this.scale.endPoint){
3073 point.controlPoints.inner.y = this.scale.endPoint;
3074 }
3075 else if (point.controlPoints.inner.y < this.scale.startPoint){
3076 point.controlPoints.inner.y = this.scale.startPoint;
3077 }
3078 },this);
3079 }
3080
3081
3082 //Draw the line between all the points
3083 ctx.lineWidth = this.options.datasetStrokeWidth;
3084 ctx.strokeStyle = dataset.strokeColor;
3085 ctx.beginPath();
3086
3087 helpers.each(pointsWithValues, function(point, index){
3088 if (index === 0){
3089 ctx.moveTo(point.x, point.y);
3090 }
3091 else{
3092 if(this.options.bezierCurve){
3093 var previous = previousPoint(point, pointsWithValues, index);
3094
3095 ctx.bezierCurveTo(
3096 previous.controlPoints.outer.x,
3097 previous.controlPoints.outer.y,
3098 point.controlPoints.inner.x,
3099 point.controlPoints.inner.y,
3100 point.x,
3101 point.y
3102 );
3103 }
3104 else{
3105 ctx.lineTo(point.x,point.y);
3106 }
3107 }
3108 }, this);
3109
3110 if (this.options.datasetStroke) {
3111 ctx.stroke();
3112 }
3113
3114 if (this.options.datasetFill && pointsWithValues.length > 0){
3115 //Round off the line by going to the base of the chart, back to the start, then fill.
3116 ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
3117 ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
3118 ctx.fillStyle = dataset.fillColor;
3119 ctx.closePath();
3120 ctx.fill();
3121 }
3122
3123 //Now draw the points over the line
3124 //A little inefficient double looping, but better than the line
3125 //lagging behind the point positions
3126 helpers.each(pointsWithValues,function(point){
3127 point.draw();
3128 });
3129 },this);
3130 }
3131 });
3132
3133
3134 }).call(this);
3135
3136 (function(){
3137 "use strict";
3138
3139 var root = this,
3140 Chart = root.Chart,
3141 //Cache a local reference to Chart.helpers
3142 helpers = Chart.helpers;
3143
3144 var defaultConfig = {
3145 //Boolean - Show a backdrop to the scale label
3146 scaleShowLabelBackdrop : true,
3147
3148 //String - The colour of the label backdrop
3149 scaleBackdropColor : "rgba(255,255,255,0.75)",
3150
3151 // Boolean - Whether the scale should begin at zero
3152 scaleBeginAtZero : true,
3153
3154 //Number - The backdrop padding above & below the label in pixels
3155 scaleBackdropPaddingY : 2,
3156
3157 //Number - The backdrop padding to the side of the label in pixels
3158 scaleBackdropPaddingX : 2,
3159
3160 //Boolean - Show line for each value in the scale
3161 scaleShowLine : true,
3162
3163 //Boolean - Stroke a line around each segment in the chart
3164 segmentShowStroke : true,
3165
3166 //String - The colour of the stroke on each segment.
3167 segmentStrokeColor : "#fff",
3168
3169 //Number - The width of the stroke value in pixels
3170 segmentStrokeWidth : 2,
3171
3172 //Number - Amount of animation steps
3173 animationSteps : 100,
3174
3175 //String - Animation easing effect.
3176 animationEasing : "easeOutBounce",
3177
3178 //Boolean - Whether to animate the rotation of the chart
3179 animateRotate : true,
3180
3181 //Boolean - Whether to animate scaling the chart from the centre
3182 animateScale : false,
3183
3184 //String - A legend template
3185 legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>"
3186 };
3187
3188
3189 Chart.Type.extend({
3190 //Passing in a name registers this chart in the Chart namespace
3191 name: "PolarArea",
3192 //Providing a defaults will also register the deafults in the chart namespace
3193 defaults : defaultConfig,
3194 //Initialize is fired when the chart is initialized - Data is passed in as a parameter
3195 //Config is automatically merged by the core of Chart.js, and is available at this.options
3196 initialize: function(data){
3197 this.segments = [];
3198 //Declare segment class as a chart instance specific class, so it can share props for this instance
3199 this.SegmentArc = Chart.Arc.extend({
3200 showStroke : this.options.segmentShowStroke,
3201 strokeWidth : this.options.segmentStrokeWidth,
3202 strokeColor : this.options.segmentStrokeColor,
3203 ctx : this.chart.ctx,
3204 innerRadius : 0,
3205 x : this.chart.width/2,
3206 y : this.chart.height/2
3207 });
3208 this.scale = new Chart.RadialScale({
3209 display: this.options.showScale,
3210 fontStyle: this.options.scaleFontStyle,
3211 fontSize: this.options.scaleFontSize,
3212 fontFamily: this.options.scaleFontFamily,
3213 fontColor: this.options.scaleFontColor,
3214 showLabels: this.options.scaleShowLabels,
3215 showLabelBackdrop: this.options.scaleShowLabelBackdrop,
3216 backdropColor: this.options.scaleBackdropColor,
3217 backdropPaddingY : this.options.scaleBackdropPaddingY,
3218 backdropPaddingX: this.options.scaleBackdropPaddingX,
3219 lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
3220 lineColor: this.options.scaleLineColor,
3221 lineArc: true,
3222 width: this.chart.width,
3223 height: this.chart.height,
3224 xCenter: this.chart.width/2,
3225 yCenter: this.chart.height/2,
3226 ctx : this.chart.ctx,
3227 templateString: this.options.scaleLabel,
3228 valuesCount: data.length
3229 });
3230
3231 this.updateScaleRange(data);
3232
3233 this.scale.update();
3234
3235 helpers.each(data,function(segment,index){
3236 this.addData(segment,index,true);
3237 },this);
3238
3239 //Set up tooltip events on the chart
3240 if (this.options.showTooltips){
3241 helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
3242 var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
3243 helpers.each(this.segments,function(segment){
3244 segment.restore(["fillColor"]);
3245 });
3246 helpers.each(activeSegments,function(activeSegment){
3247 activeSegment.fillColor = activeSegment.highlightColor;
3248 });
3249 this.showTooltip(activeSegments);
3250 });
3251 }
3252
3253 this.render();
3254 },
3255 getSegmentsAtEvent : function(e){
3256 var segmentsArray = [];
3257
3258 var location = helpers.getRelativePosition(e);
3259
3260 helpers.each(this.segments,function(segment){
3261 if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
3262 },this);
3263 return segmentsArray;
3264 },
3265 addData : function(segment, atIndex, silent){
3266 var index = atIndex || this.segments.length;
3267
3268 this.segments.splice(index, 0, new this.SegmentArc({
3269 fillColor: segment.color,
3270 highlightColor: segment.highlight || segment.color,
3271 label: segment.label,
3272 value: segment.value,
3273 outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value),
3274 circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(),
3275 startAngle: Math.PI * 1.5
3276 }));
3277 if (!silent){
3278 this.reflow();
3279 this.update();
3280 }
3281 },
3282 removeData: function(atIndex){
3283 var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
3284 this.segments.splice(indexToDelete, 1);
3285 this.reflow();
3286 this.update();
3287 },
3288 calculateTotal: function(data){
3289 this.total = 0;
3290 helpers.each(data,function(segment){
3291 this.total += segment.value;
3292 },this);
3293 this.scale.valuesCount = this.segments.length;
3294 },
3295 updateScaleRange: function(datapoints){
3296 var valuesArray = [];
3297 helpers.each(datapoints,function(segment){
3298 valuesArray.push(segment.value);
3299 });
3300
3301 var scaleSizes = (this.options.scaleOverride) ?
3302 {
3303 steps: this.options.scaleSteps,
3304 stepValue: this.options.scaleStepWidth,
3305 min: this.options.scaleStartValue,
3306 max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
3307 } :
3308 helpers.calculateScaleRange(
3309 valuesArray,
3310 helpers.min([this.chart.width, this.chart.height])/2,
3311 this.options.scaleFontSize,
3312 this.options.scaleBeginAtZero,
3313 this.options.scaleIntegersOnly
3314 );
3315
3316 helpers.extend(
3317 this.scale,
3318 scaleSizes,
3319 {
3320 size: helpers.min([this.chart.width, this.chart.height]),
3321 xCenter: this.chart.width/2,
3322 yCenter: this.chart.height/2
3323 }
3324 );
3325
3326 },
3327 update : function(){
3328 this.calculateTotal(this.segments);
3329
3330 helpers.each(this.segments,function(segment){
3331 segment.save();
3332 });
3333
3334 this.reflow();
3335 this.render();
3336 },
3337 reflow : function(){
3338 helpers.extend(this.SegmentArc.prototype,{
3339 x : this.chart.width/2,
3340 y : this.chart.height/2
3341 });
3342 this.updateScaleRange(this.segments);
3343 this.scale.update();
3344
3345 helpers.extend(this.scale,{
3346 xCenter: this.chart.width/2,
3347 yCenter: this.chart.height/2
3348 });
3349
3350 helpers.each(this.segments, function(segment){
3351 segment.update({
3352 outerRadius : this.scale.calculateCenterOffset(segment.value)
3353 });
3354 }, this);
3355
3356 },
3357 draw : function(ease){
3358 var easingDecimal = ease || 1;
3359 //Clear & draw the canvas
3360 this.clear();
3361 helpers.each(this.segments,function(segment, index){
3362 segment.transition({
3363 circumference : this.scale.getCircumference(),
3364 outerRadius : this.scale.calculateCenterOffset(segment.value)
3365 },easingDecimal);
3366
3367 segment.endAngle = segment.startAngle + segment.circumference;
3368
3369 // If we've removed the first segment we need to set the first one to
3370 // start at the top.
3371 if (index === 0){
3372 segment.startAngle = Math.PI * 1.5;
3373 }
3374
3375 //Check to see if it's the last segment, if not get the next and update the start angle
3376 if (index < this.segments.length - 1){
3377 this.segments[index+1].startAngle = segment.endAngle;
3378 }
3379 segment.draw();
3380 }, this);
3381 this.scale.draw();
3382 }
3383 });
3384
3385 }).call(this);
3386
3387 (function(){
3388 "use strict";
3389
3390 var root = this,
3391 Chart = root.Chart,
3392 helpers = Chart.helpers;
3393
3394
3395
3396 Chart.Type.extend({
3397 name: "Radar",
3398 defaults:{
3399 //Boolean - Whether to show lines for each scale point
3400 scaleShowLine : true,
3401
3402 //Boolean - Whether we show the angle lines out of the radar
3403 angleShowLineOut : true,
3404
3405 //Boolean - Whether to show labels on the scale
3406 scaleShowLabels : false,
3407
3408 // Boolean - Whether the scale should begin at zero
3409 scaleBeginAtZero : true,
3410
3411 //String - Colour of the angle line
3412 angleLineColor : "rgba(0,0,0,.1)",
3413
3414 //Number - Pixel width of the angle line
3415 angleLineWidth : 1,
3416
3417 //String - Point label font declaration
3418 pointLabelFontFamily : "'Arial'",
3419
3420 //String - Point label font weight
3421 pointLabelFontStyle : "normal",
3422
3423 //Number - Point label font size in pixels
3424 pointLabelFontSize : 10,
3425
3426 //String - Point label font colour
3427 pointLabelFontColor : "#666",
3428
3429 //Boolean - Whether to show a dot for each point
3430 pointDot : true,
3431
3432 //Number - Radius of each point dot in pixels
3433 pointDotRadius : 3,
3434
3435 //Number - Pixel width of point dot stroke
3436 pointDotStrokeWidth : 1,
3437
3438 //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
3439 pointHitDetectionRadius : 20,
3440
3441 //Boolean - Whether to show a stroke for datasets
3442 datasetStroke : true,
3443
3444 //Number - Pixel width of dataset stroke
3445 datasetStrokeWidth : 2,
3446
3447 //Boolean - Whether to fill the dataset with a colour
3448 datasetFill : true,
3449
3450 //String - A legend template
3451 legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
3452
3453 },
3454
3455 initialize: function(data){
3456 this.PointClass = Chart.Point.extend({
3457 strokeWidth : this.options.pointDotStrokeWidth,
3458 radius : this.options.pointDotRadius,
3459 display: this.options.pointDot,
3460 hitDetectionRadius : this.options.pointHitDetectionRadius,
3461 ctx : this.chart.ctx
3462 });
3463
3464 this.datasets = [];
3465
3466 this.buildScale(data);
3467
3468 //Set up tooltip events on the chart
3469 if (this.options.showTooltips){
3470 helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
3471 var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
3472
3473 this.eachPoints(function(point){
3474 point.restore(['fillColor', 'strokeColor']);
3475 });
3476 helpers.each(activePointsCollection, function(activePoint){
3477 activePoint.fillColor = activePoint.highlightFill;
3478 activePoint.strokeColor = activePoint.highlightStroke;
3479 });
3480
3481 this.showTooltip(activePointsCollection);
3482 });
3483 }
3484
3485 //Iterate through each of the datasets, and build this into a property of the chart
3486 helpers.each(data.datasets,function(dataset){
3487
3488 var datasetObject = {
3489 label: dataset.label || null,
3490 fillColor : dataset.fillColor,
3491 strokeColor : dataset.strokeColor,
3492 pointColor : dataset.pointColor,
3493 pointStrokeColor : dataset.pointStrokeColor,
3494 points : []
3495 };
3496
3497 this.datasets.push(datasetObject);
3498
3499 helpers.each(dataset.data,function(dataPoint,index){
3500 //Add a new point for each piece of data, passing any required data to draw.
3501 var pointPosition;
3502 if (!this.scale.animation){
3503 pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint));
3504 }
3505 datasetObject.points.push(new this.PointClass({
3506 value : dataPoint,
3507 label : data.labels[index],
3508 datasetLabel: dataset.label,
3509 x: (this.options.animation) ? this.scale.xCenter : pointPosition.x,
3510 y: (this.options.animation) ? this.scale.yCenter : pointPosition.y,
3511 strokeColor : dataset.pointStrokeColor,
3512 fillColor : dataset.pointColor,
3513 highlightFill : dataset.pointHighlightFill || dataset.pointColor,
3514 highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
3515 }));
3516 },this);
3517
3518 },this);
3519
3520 this.render();
3521 },
3522 eachPoints : function(callback){
3523 helpers.each(this.datasets,function(dataset){
3524 helpers.each(dataset.points,callback,this);
3525 },this);
3526 },
3527
3528 getPointsAtEvent : function(evt){
3529 var mousePosition = helpers.getRelativePosition(evt),
3530 fromCenter = helpers.getAngleFromPoint({
3531 x: this.scale.xCenter,
3532 y: this.scale.yCenter
3533 }, mousePosition);
3534
3535 var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount,
3536 pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex),
3537 activePointsCollection = [];
3538
3539 // If we're at the top, make the pointIndex 0 to get the first of the array.
3540 if (pointIndex >= this.scale.valuesCount || pointIndex < 0){
3541 pointIndex = 0;
3542 }
3543
3544 if (fromCenter.distance <= this.scale.drawingArea){
3545 helpers.each(this.datasets, function(dataset){
3546 activePointsCollection.push(dataset.points[pointIndex]);
3547 });
3548 }
3549
3550 return activePointsCollection;
3551 },
3552
3553 buildScale : function(data){
3554 this.scale = new Chart.RadialScale({
3555 display: this.options.showScale,
3556 fontStyle: this.options.scaleFontStyle,
3557 fontSize: this.options.scaleFontSize,
3558 fontFamily: this.options.scaleFontFamily,
3559 fontColor: this.options.scaleFontColor,
3560 showLabels: this.options.scaleShowLabels,
3561 showLabelBackdrop: this.options.scaleShowLabelBackdrop,
3562 backdropColor: this.options.scaleBackdropColor,
3563 backgroundColors: this.options.scaleBackgroundColors,
3564 backdropPaddingY : this.options.scaleBackdropPaddingY,
3565 backdropPaddingX: this.options.scaleBackdropPaddingX,
3566 lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
3567 lineColor: this.options.scaleLineColor,
3568 angleLineColor : this.options.angleLineColor,
3569 angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0,
3570 // Point labels at the edge of each line
3571 pointLabelFontColor : this.options.pointLabelFontColor,
3572 pointLabelFontSize : this.options.pointLabelFontSize,
3573 pointLabelFontFamily : this.options.pointLabelFontFamily,
3574 pointLabelFontStyle : this.options.pointLabelFontStyle,
3575 height : this.chart.height,
3576 width: this.chart.width,
3577 xCenter: this.chart.width/2,
3578 yCenter: this.chart.height/2,
3579 ctx : this.chart.ctx,
3580 templateString: this.options.scaleLabel,
3581 labels: data.labels,
3582 valuesCount: data.datasets[0].data.length
3583 });
3584
3585 this.scale.setScaleSize();
3586 this.updateScaleRange(data.datasets);
3587 this.scale.buildYLabels();
3588 },
3589 updateScaleRange: function(datasets){
3590 var valuesArray = (function(){
3591 var totalDataArray = [];
3592 helpers.each(datasets,function(dataset){
3593 if (dataset.data){
3594 totalDataArray = totalDataArray.concat(dataset.data);
3595 }
3596 else {
3597 helpers.each(dataset.points, function(point){
3598 totalDataArray.push(point.value);
3599 });
3600 }
3601 });
3602 return totalDataArray;
3603 })();
3604
3605
3606 var scaleSizes = (this.options.scaleOverride) ?
3607 {
3608 steps: this.options.scaleSteps,
3609 stepValue: this.options.scaleStepWidth,
3610 min: this.options.scaleStartValue,
3611 max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
3612 } :
3613 helpers.calculateScaleRange(
3614 valuesArray,
3615 helpers.min([this.chart.width, this.chart.height])/2,
3616 this.options.scaleFontSize,
3617 this.options.scaleBeginAtZero,
3618 this.options.scaleIntegersOnly
3619 );
3620
3621 helpers.extend(
3622 this.scale,
3623 scaleSizes
3624 );
3625
3626 },
3627 addData : function(valuesArray,label){
3628 //Map the values array for each of the datasets
3629 this.scale.valuesCount++;
3630 helpers.each(valuesArray,function(value,datasetIndex){
3631 var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value));
3632 this.datasets[datasetIndex].points.push(new this.PointClass({
3633 value : value,
3634 label : label,
3635 datasetLabel: this.datasets[datasetIndex].label,
3636 x: pointPosition.x,
3637 y: pointPosition.y,
3638 strokeColor : this.datasets[datasetIndex].pointStrokeColor,
3639 fillColor : this.datasets[datasetIndex].pointColor
3640 }));
3641 },this);
3642
3643 this.scale.labels.push(label);
3644
3645 this.reflow();
3646
3647 this.update();
3648 },
3649 removeData : function(){
3650 this.scale.valuesCount--;
3651 this.scale.labels.shift();
3652 helpers.each(this.datasets,function(dataset){
3653 dataset.points.shift();
3654 },this);
3655 this.reflow();
3656 this.update();
3657 },
3658 update : function(){
3659 this.eachPoints(function(point){
3660 point.save();
3661 });
3662 this.reflow();
3663 this.render();
3664 },
3665 reflow: function(){
3666 helpers.extend(this.scale, {
3667 width : this.chart.width,
3668 height: this.chart.height,
3669 size : helpers.min([this.chart.width, this.chart.height]),
3670 xCenter: this.chart.width/2,
3671 yCenter: this.chart.height/2
3672 });
3673 this.updateScaleRange(this.datasets);
3674 this.scale.setScaleSize();
3675 this.scale.buildYLabels();
3676 },
3677 draw : function(ease){
3678 var easeDecimal = ease || 1,
3679 ctx = this.chart.ctx;
3680 this.clear();
3681 this.scale.draw();
3682
3683 helpers.each(this.datasets,function(dataset){
3684
3685 //Transition each point first so that the line and point drawing isn't out of sync
3686 helpers.each(dataset.points,function(point,index){
3687 if (point.hasValue()){
3688 point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal);
3689 }
3690 },this);
3691
3692
3693
3694 //Draw the line between all the points
3695 ctx.lineWidth = this.options.datasetStrokeWidth;
3696 ctx.strokeStyle = dataset.strokeColor;
3697 ctx.beginPath();
3698 helpers.each(dataset.points,function(point,index){
3699 if (index === 0){
3700 ctx.moveTo(point.x,point.y);
3701 }
3702 else{
3703 ctx.lineTo(point.x,point.y);
3704 }
3705 },this);
3706 ctx.closePath();
3707 ctx.stroke();
3708
3709 ctx.fillStyle = dataset.fillColor;
3710 if(this.options.datasetFill){
3711 ctx.fill();
3712 }
3713 //Now draw the points over the line
3714 //A little inefficient double looping, but better than the line
3715 //lagging behind the point positions
3716 helpers.each(dataset.points,function(point){
3717 if (point.hasValue()){
3718 point.draw();
3719 }
3720 });
3721
3722 },this);
3723
3724 }
3725
3726 });
3727
3728
3729
3730
3731
3732 }).call(this);
3733