flashgrid-ext 2.1.0 → 2.1.1

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