active_frontend 1.0.0

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