chart-js-rails 0.0.6 → 0.0.7

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