chart-js-rails 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);