flashgrid-ext 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);