6 * Copyright 2015 Nick Downie
7 * Released under the MIT license
8 * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
16 //Declare root variable - window in the browser, global on the server
18 previous
= root
.Chart
;
20 //Occupy the global variable of Chart, and create a simple base class
21 var Chart = function(context
){
23 this.canvas
= context
.canvas
;
27 //Variables global to the chart
28 var computeDimension = function(element
,dimension
)
30 if (element
['offset'+dimension
])
32 return element
['offset'+dimension
];
36 return document
.defaultView
.getComputedStyle(element
).getPropertyValue(dimension
);
40 var width
= this.width
= computeDimension(context
.canvas
,'Width') || context
.canvas
.width
;
41 var height
= this.height
= computeDimension(context
.canvas
,'Height') || context
.canvas
.height
;
43 this.aspectRatio
= this.width
/ this.height
;
44 //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
45 helpers
.retinaScale(this);
49 //Globally expose the defaults to allow for user updating/changing
52 // Boolean - Whether to animate the chart
55 // Number - Number of animation steps
58 // String - Animation easing effect
59 animationEasing
: "easeOutQuart",
61 // Boolean - If we should show the scale at all
64 // Boolean - If we want to override with a hard coded scale
67 // ** Required if scaleOverride is true **
68 // Number - The number of steps in a hard coded scale
70 // Number - The value jump in the hard coded scale
72 // Number - The scale starting value
73 scaleStartValue
: null,
75 // String - Colour of the scale line
76 scaleLineColor
: "rgba(0,0,0,.1)",
78 // Number - Pixel width of the scale line
81 // Boolean - Whether to show labels on the scale
82 scaleShowLabels
: true,
84 // Interpolated JS string - can access value
85 scaleLabel
: "<%=value%>",
87 // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
88 scaleIntegersOnly
: true,
90 // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
91 scaleBeginAtZero
: false,
93 // String - Scale label font declaration for the scale label
94 scaleFontFamily
: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
96 // Number - Scale label font size in pixels
99 // String - Scale label font weight style
100 scaleFontStyle
: "normal",
102 // String - Scale label font colour
103 scaleFontColor
: "#666",
105 // Boolean - whether or not the chart should be responsive and resize when the browser does.
108 // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
109 maintainAspectRatio
: true,
111 // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
114 // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
115 customTooltips
: false,
117 // Array - Array of string names to attach tooltip events
118 tooltipEvents
: ["mousemove", "touchstart", "touchmove", "mouseout"],
120 // String - Tooltip background colour
121 tooltipFillColor
: "rgba(0,0,0,0.8)",
123 // String - Tooltip label font declaration for the scale label
124 tooltipFontFamily
: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
126 // Number - Tooltip label font size in pixels
129 // String - Tooltip font weight style
130 tooltipFontStyle
: "normal",
132 // String - Tooltip label font colour
133 tooltipFontColor
: "#fff",
135 // String - Tooltip title font declaration for the scale label
136 tooltipTitleFontFamily
: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
138 // Number - Tooltip title font size in pixels
139 tooltipTitleFontSize
: 14,
141 // String - Tooltip title font weight style
142 tooltipTitleFontStyle
: "bold",
144 // String - Tooltip title font colour
145 tooltipTitleFontColor
: "#fff",
147 // String - Tooltip title template
148 tooltipTitleTemplate
: "<%= label%>",
150 // Number - pixel width of padding around tooltip text
153 // Number - pixel width of padding around tooltip text
156 // Number - Size of the caret on the tooltip
159 // Number - Pixel radius of the tooltip border
160 tooltipCornerRadius
: 6,
162 // Number - Pixel offset from point x to tooltip edge
165 // String - Template string for single tooltips
166 tooltipTemplate
: "<%if (label){%><%=label%>: <%}%><%= value %>",
168 // String - Template string for single tooltips
169 multiTooltipTemplate
: "<%= datasetLabel %>: <%= value %>",
171 // String - Colour behind the legend colour block
172 multiTooltipKeyBackground
: '#fff',
174 // Array - A list of colors to use as the defaults
175 segmentColorDefault
: ["#A6CEE3", "#1F78B4", "#B2DF8A", "#33A02C", "#FB9A99", "#E31A1C", "#FDBF6F", "#FF7F00", "#CAB2D6", "#6A3D9A", "#B4B482", "#B15928" ],
177 // Array - A list of highlight colors to use as the defaults
178 segmentHighlightColorDefaults
: [ "#CEF6FF", "#47A0DC", "#DAFFB2", "#5BC854", "#FFC2C1", "#FF4244", "#FFE797", "#FFA728", "#F2DAFE", "#9265C2", "#DCDCAA", "#D98150" ],
180 // Function - Will fire on animation progression.
181 onAnimationProgress: function(){},
183 // Function - Will fire on animation completion.
184 onAnimationComplete: function(){}
189 //Create a dictionary of chart types, to allow for extension of existing types
192 //Global Chart helpers object for utility methods and classes
193 var helpers
= Chart
.helpers
= {};
195 //-- Basic js utility methods
196 var each
= helpers
.each = function(loopable
,callback
,self
){
197 var additionalArgs
= Array
.prototype.slice
.call(arguments
, 3);
198 // Check to see if null or undefined firstly.
200 if (loopable
.length
=== +loopable
.length
){
202 for (i
=0; i
<loopable
.length
; i
++){
203 callback
.apply(self
,[loopable
[i
], i
].concat(additionalArgs
));
207 for (var item
in loopable
){
208 callback
.apply(self
,[loopable
[item
],item
].concat(additionalArgs
));
213 clone
= helpers
.clone = function(obj
){
215 each(obj
,function(value
,key
){
216 if (obj
.hasOwnProperty(key
)){
217 objClone
[key
] = value
;
222 extend
= helpers
.extend = function(base
){
223 each(Array
.prototype.slice
.call(arguments
,1), function(extensionObject
) {
224 each(extensionObject
,function(value
,key
){
225 if (extensionObject
.hasOwnProperty(key
)){
232 merge
= helpers
.merge = function(base
,master
){
233 //Merge properties in left object over to a shallow clone of object right.
234 var args
= Array
.prototype.slice
.call(arguments
,0);
236 return extend
.apply(null, args
);
238 indexOf
= helpers
.indexOf = function(arrayToSearch
, item
){
239 if (Array
.prototype.indexOf
) {
240 return arrayToSearch
.indexOf(item
);
243 for (var i
= 0; i
< arrayToSearch
.length
; i
++) {
244 if (arrayToSearch
[i
] === item
) return i
;
249 where
= helpers
.where = function(collection
, filterCallback
){
252 helpers
.each(collection
, function(item
){
253 if (filterCallback(item
)){
260 findNextWhere
= helpers
.findNextWhere = function(arrayToSearch
, filterCallback
, startIndex
){
261 // Default to start of the array
265 for (var i
= startIndex
+ 1; i
< arrayToSearch
.length
; i
++) {
266 var currentItem
= arrayToSearch
[i
];
267 if (filterCallback(currentItem
)){
272 findPreviousWhere
= helpers
.findPreviousWhere = function(arrayToSearch
, filterCallback
, startIndex
){
273 // Default to end of the array
275 startIndex
= arrayToSearch
.length
;
277 for (var i
= startIndex
- 1; i
>= 0; i
--) {
278 var currentItem
= arrayToSearch
[i
];
279 if (filterCallback(currentItem
)){
284 inherits
= helpers
.inherits = function(extensions
){
285 //Basic javascript inheritance based on the model created in Backbone.js
287 var ChartElement
= (extensions
&& extensions
.hasOwnProperty("constructor")) ? extensions
.constructor : function(){ return parent
.apply(this, arguments
); };
289 var Surrogate = function(){ this.constructor = ChartElement
;};
290 Surrogate
.prototype = parent
.prototype;
291 ChartElement
.prototype = new Surrogate();
293 ChartElement
.extend
= inherits
;
295 if (extensions
) extend(ChartElement
.prototype, extensions
);
297 ChartElement
.__super__
= parent
.prototype;
301 noop
= helpers
.noop = function(){},
302 uid
= helpers
.uid
= (function(){
305 return "chart-" + id
++;
308 warn
= helpers
.warn = function(str
){
309 //Method for warning of errors
310 if (window
.console
&& typeof window
.console
.warn
=== "function") console
.warn(str
);
312 amd
= helpers
.amd
= (typeof define
=== 'function' && define
.amd
),
314 isNumber
= helpers
.isNumber = function(n
){
315 return !isNaN(parseFloat(n
)) && isFinite(n
);
317 max
= helpers
.max = function(array
){
318 return Math
.max
.apply( Math
, array
);
320 min
= helpers
.min = function(array
){
321 return Math
.min
.apply( Math
, array
);
323 cap
= helpers
.cap = function(valueToCap
,maxValue
,minValue
){
324 if(isNumber(maxValue
)) {
325 if( valueToCap
> maxValue
) {
329 else if(isNumber(minValue
)){
330 if ( valueToCap
< minValue
){
336 getDecimalPlaces
= helpers
.getDecimalPlaces = function(num
){
337 if (num
%1!==0 && isNumber(num
)){
338 var s
= num
.toString();
339 if(s
.indexOf("e-") < 0){
340 // no exponent, e.g. 0.01
341 return s
.split(".")[1].length
;
343 else if(s
.indexOf(".") < 0) {
344 // no decimal point, e.g. 1e-9
345 return parseInt(s
.split("e-")[1]);
348 // exponent and decimal point, e.g. 1.23e-9
349 var parts
= s
.split(".")[1].split("e-");
350 return parts
[0].length
+ parseInt(parts
[1]);
357 toRadians
= helpers
.radians = function(degrees
){
358 return degrees
* (Math
.PI
/180);
360 // Gets the angle from vertical upright to the point about a centre.
361 getAngleFromPoint
= helpers
.getAngleFromPoint = function(centrePoint
, anglePoint
){
362 var distanceFromXCenter
= anglePoint
.x
- centrePoint
.x
,
363 distanceFromYCenter
= anglePoint
.y
- centrePoint
.y
,
364 radialDistanceFromCenter
= Math
.sqrt( distanceFromXCenter
* distanceFromXCenter
+ distanceFromYCenter
* distanceFromYCenter
);
367 var angle
= Math
.PI
* 2 + Math
.atan2(distanceFromYCenter
, distanceFromXCenter
);
369 //If the segment is in the top left quadrant, we need to add another rotation to the angle
370 if (distanceFromXCenter
< 0 && distanceFromYCenter
< 0){
376 distance
: radialDistanceFromCenter
379 aliasPixel
= helpers
.aliasPixel = function(pixelWidth
){
380 return (pixelWidth
% 2 === 0) ? 0 : 0.5;
382 splineCurve
= helpers
.splineCurve = function(FirstPoint
,MiddlePoint
,AfterPoint
,t
){
383 //Props to Rob Spencer at scaled innovation for his post on splining between points
384 //http://scaledinnovation.com/analytics/splines/aboutSplines.html
385 var d01
=Math
.sqrt(Math
.pow(MiddlePoint
.x
-FirstPoint
.x
,2)+Math
.pow(MiddlePoint
.y
-FirstPoint
.y
,2)),
386 d12
=Math
.sqrt(Math
.pow(AfterPoint
.x
-MiddlePoint
.x
,2)+Math
.pow(AfterPoint
.y
-MiddlePoint
.y
,2)),
387 fa
=t
*d01
/(d01+d12),// scaling factor
for triangle Ta
391 x
: MiddlePoint
.x
-fa
*(AfterPoint
.x
-FirstPoint
.x
),
392 y
: MiddlePoint
.y
-fa
*(AfterPoint
.y
-FirstPoint
.y
)
395 x
: MiddlePoint
.x
+fb
*(AfterPoint
.x
-FirstPoint
.x
),
396 y
: MiddlePoint
.y
+fb
*(AfterPoint
.y
-FirstPoint
.y
)
400 calculateOrderOfMagnitude
= helpers
.calculateOrderOfMagnitude = function(val
){
401 return Math
.floor(Math
.log(val
) / Math
.LN10
);
403 calculateScaleRange
= helpers
.calculateScaleRange = function(valuesArray
, drawingSize
, textSize
, startFromZero
, integersOnly
){
405 //Set a minimum step of two - a point at the top of the graph, and a point at the base
407 maxSteps
= Math
.floor(drawingSize
/(textSize
* 1.5)),
408 skipFitting
= (minSteps
>= maxSteps
);
410 // Filter out null values since these would min() to zero
412 each(valuesArray
, function( v
){
413 v
== null || values
.push( v
);
415 var minValue
= min(values
),
416 maxValue
= max(values
);
418 // We need some degree of separation here to calculate the scales if all the values are the same
419 // Adding/minusing 0.5 will give us a range of 1.
420 if (maxValue
=== minValue
){
422 // So we don't end up with a graph with a negative start value if we've said always start from zero
423 if (minValue
>= 0.5 && !startFromZero
){
427 // Make up a whole number above the values
432 var valueRange
= Math
.abs(maxValue
- minValue
),
433 rangeOrderOfMagnitude
= calculateOrderOfMagnitude(valueRange
),
434 graphMax
= Math
.ceil(maxValue
/ (1 * Math
.pow(10, rangeOrderOfMagnitude
))) * Math
.pow(10, rangeOrderOfMagnitude
),
435 graphMin
= (startFromZero
) ? 0 : Math
.floor(minValue
/ (1 * Math
.pow(10, rangeOrderOfMagnitude
))) * Math
.pow(10, rangeOrderOfMagnitude
),
436 graphRange
= graphMax
- graphMin
,
437 stepValue
= Math
.pow(10, rangeOrderOfMagnitude
),
438 numberOfSteps
= Math
.round(graphRange
/ stepValue
);
440 //If we have more space on the graph we'll use it to give more definition to the data
441 while((numberOfSteps
> maxSteps
|| (numberOfSteps
* 2) < maxSteps
) && !skipFitting
) {
442 if(numberOfSteps
> maxSteps
){
444 numberOfSteps
= Math
.round(graphRange
/stepValue
);
445 // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
446 if (numberOfSteps
% 1 !== 0){
450 //We can fit in double the amount of scale points on the scale
452 //If user has declared ints only, and the step value isn't a decimal
453 if (integersOnly
&& rangeOrderOfMagnitude
>= 0){
454 //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
455 if(stepValue
/2 % 1 === 0){
457 numberOfSteps
= Math
.round(graphRange
/stepValue
);
459 //If it would make it a float break out of the loop
464 //If the scale doesn't have to be an int, make the scale more granular anyway.
467 numberOfSteps
= Math
.round(graphRange
/stepValue
);
474 numberOfSteps
= minSteps
;
475 stepValue
= graphRange
/ numberOfSteps
;
479 steps
: numberOfSteps
,
480 stepValue
: stepValue
,
482 max
: graphMin
+ (numberOfSteps
* stepValue
)
486 /* jshint ignore:start */
487 // Blows up jshint errors based on the new Function constructor
489 //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
490 template
= helpers
.template = function(templateString
, valuesObject
){
492 // If templateString is function rather than string-template - call the function for valuesObject
494 if(templateString
instanceof Function
){
495 return templateString(valuesObject
);
499 function tmpl(str
, data
){
500 // Figure out if we're getting a template, or if we need to
501 // load the template - and be sure to cache the result.
502 var fn
= !/\W/.test(str
) ?
503 cache
[str
] = cache
[str
] :
505 // Generate a reusable function that will serve as a template
506 // generator (and which will be cached).
508 "var p=[],print=function(){p.push.apply(p,arguments);};" +
510 // Introduce the data as local variables using with(){}
511 "with(obj){p.push('" +
513 // Convert the template into pure JavaScript
515 .replace(/[\r\t\n]/g, " ")
516 .split("<%").join("\t")
517 .replace(/((^|%>)[^\t]*)'/g, "$1\r")
518 .replace(/\t=(.*?)%>/g, "',$1,'")
519 .split("\t").join("');")
520 .split("%>").join("p.push('")
521 .split("\r").join("\\'") +
522 "');}return p.join('');"
525 // Provide some basic currying to the user
526 return data
? fn( data
) : fn
;
528 return tmpl(templateString
,valuesObject
);
530 /* jshint ignore:end */
531 generateLabels
= helpers
.generateLabels = function(templateString
,numberOfSteps
,graphMin
,stepValue
){
532 var labelsArray
= new Array(numberOfSteps
);
534 each(labelsArray
,function(val
,index
){
535 labelsArray
[index
] = template(templateString
,{value
: (graphMin
+ (stepValue
*(index
+1)))});
540 //--Animation methods
541 //Easing functions adapted from Robert Penner's easing equations
542 //http://www.robertpenner.com/easing/
543 easingEffects
= helpers
.easingEffects
= {
544 linear: function (t
) {
547 easeInQuad: function (t
) {
550 easeOutQuad: function (t
) {
551 return -1 * t
* (t
- 2);
553 easeInOutQuad: function (t
) {
554 if ((t
/= 1 / 2) < 1){
555 return 1 / 2 * t
* t
;
557 return -1 / 2 * ((--t
) * (t
- 2) - 1);
559 easeInCubic: function (t
) {
562 easeOutCubic: function (t
) {
563 return 1 * ((t
= t
/ 1 - 1) * t
* t
+ 1);
565 easeInOutCubic: function (t
) {
566 if ((t
/= 1 / 2) < 1){
567 return 1 / 2 * t
* t
* t
;
569 return 1 / 2 * ((t
-= 2) * t
* t
+ 2);
571 easeInQuart: function (t
) {
572 return t
* t
* t
* t
;
574 easeOutQuart: function (t
) {
575 return -1 * ((t
= t
/ 1 - 1) * t
* t
* t
- 1);
577 easeInOutQuart: function (t
) {
578 if ((t
/= 1 / 2) < 1){
579 return 1 / 2 * t
* t
* t
* t
;
581 return -1 / 2 * ((t
-= 2) * t
* t
* t
- 2);
583 easeInQuint: function (t
) {
584 return 1 * (t
/= 1) * t
* t
* t
* t
;
586 easeOutQuint: function (t
) {
587 return 1 * ((t
= t
/ 1 - 1) * t
* t
* t
* t
+ 1);
589 easeInOutQuint: function (t
) {
590 if ((t
/= 1 / 2) < 1){
591 return 1 / 2 * t
* t
* t
* t
* t
;
593 return 1 / 2 * ((t
-= 2) * t
* t
* t
* t
+ 2);
595 easeInSine: function (t
) {
596 return -1 * Math
.cos(t
/ 1 * (Math
.PI
/ 2)) + 1;
598 easeOutSine: function (t
) {
599 return 1 * Math
.sin(t
/ 1 * (Math
.PI
/ 2));
601 easeInOutSine: function (t
) {
602 return -1 / 2 * (Math
.cos(Math
.PI
* t
/ 1) - 1);
604 easeInExpo: function (t
) {
605 return (t
=== 0) ? 1 : 1 * Math
.pow(2, 10 * (t
/ 1 - 1));
607 easeOutExpo: function (t
) {
608 return (t
=== 1) ? 1 : 1 * (-Math
.pow(2, -10 * t
/ 1) + 1);
610 easeInOutExpo: function (t
) {
617 if ((t
/= 1 / 2) < 1){
618 return 1 / 2 * Math
.pow(2, 10 * (t
- 1));
620 return 1 / 2 * (-Math
.pow(2, -10 * --t
) + 2);
622 easeInCirc: function (t
) {
626 return -1 * (Math
.sqrt(1 - (t
/= 1) * t
) - 1);
628 easeOutCirc: function (t
) {
629 return 1 * Math
.sqrt(1 - (t
= t
/ 1 - 1) * t
);
631 easeInOutCirc: function (t
) {
632 if ((t
/= 1 / 2) < 1){
633 return -1 / 2 * (Math
.sqrt(1 - t
* t
) - 1);
635 return 1 / 2 * (Math
.sqrt(1 - (t
-= 2) * t
) + 1);
637 easeInElastic: function (t
) {
650 if (a
< Math
.abs(1)) {
654 s
= p
/ (2 * Math
.PI
) * Math
.asin(1 / a
);
656 return -(a
* Math
.pow(2, 10 * (t
-= 1)) * Math
.sin((t
* 1 - s
) * (2 * Math
.PI
) / p
));
658 easeOutElastic: function (t
) {
671 if (a
< Math
.abs(1)) {
675 s
= p
/ (2 * Math
.PI
) * Math
.asin(1 / a
);
677 return a
* Math
.pow(2, -10 * t
) * Math
.sin((t
* 1 - s
) * (2 * Math
.PI
) / p
) + 1;
679 easeInOutElastic: function (t
) {
686 if ((t
/= 1 / 2) == 2){
692 if (a
< Math
.abs(1)) {
696 s
= p
/ (2 * Math
.PI
) * Math
.asin(1 / a
);
699 return -0.5 * (a
* Math
.pow(2, 10 * (t
-= 1)) * Math
.sin((t
* 1 - s
) * (2 * Math
.PI
) / p
));}
700 return a
* Math
.pow(2, -10 * (t
-= 1)) * Math
.sin((t
* 1 - s
) * (2 * Math
.PI
) / p
) * 0.5 + 1;
702 easeInBack: function (t
) {
704 return 1 * (t
/= 1) * t
* ((s
+ 1) * t
- s
);
706 easeOutBack: function (t
) {
708 return 1 * ((t
= t
/ 1 - 1) * t
* ((s
+ 1) * t
+ s
) + 1);
710 easeInOutBack: function (t
) {
712 if ((t
/= 1 / 2) < 1){
713 return 1 / 2 * (t
* t
* (((s
*= (1.525)) + 1) * t
- s
));
715 return 1 / 2 * ((t
-= 2) * t
* (((s
*= (1.525)) + 1) * t
+ s
) + 2);
717 easeInBounce: function (t
) {
718 return 1 - easingEffects
.easeOutBounce(1 - t
);
720 easeOutBounce: function (t
) {
721 if ((t
/= 1) < (1 / 2.75)) {
722 return 1 * (7.5625 * t
* t
);
723 } else if (t
< (2 / 2.75)) {
724 return 1 * (7.5625 * (t
-= (1.5 / 2.75)) * t
+ 0.75);
725 } else if (t
< (2.5 / 2.75)) {
726 return 1 * (7.5625 * (t
-= (2.25 / 2.75)) * t
+ 0.9375);
728 return 1 * (7.5625 * (t
-= (2.625 / 2.75)) * t
+ 0.984375);
731 easeInOutBounce: function (t
) {
733 return easingEffects
.easeInBounce(t
* 2) * 0.5;
735 return easingEffects
.easeOutBounce(t
* 2 - 1) * 0.5 + 1 * 0.5;
738 //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
739 requestAnimFrame
= helpers
.requestAnimFrame
= (function(){
740 return window
.requestAnimationFrame
||
741 window
.webkitRequestAnimationFrame
||
742 window
.mozRequestAnimationFrame
||
743 window
.oRequestAnimationFrame
||
744 window
.msRequestAnimationFrame
||
746 return window
.setTimeout(callback
, 1000 / 60);
749 cancelAnimFrame
= helpers
.cancelAnimFrame
= (function(){
750 return window
.cancelAnimationFrame
||
751 window
.webkitCancelAnimationFrame
||
752 window
.mozCancelAnimationFrame
||
753 window
.oCancelAnimationFrame
||
754 window
.msCancelAnimationFrame
||
756 return window
.clearTimeout(callback
, 1000 / 60);
759 animationLoop
= helpers
.animationLoop = function(callback
,totalSteps
,easingString
,onProgress
,onComplete
,chartInstance
){
762 easingFunction
= easingEffects
[easingString
] || easingEffects
.linear
;
764 var animationFrame = function(){
766 var stepDecimal
= currentStep
/totalSteps
;
767 var easeDecimal
= easingFunction(stepDecimal
);
769 callback
.call(chartInstance
,easeDecimal
,stepDecimal
, currentStep
);
770 onProgress
.call(chartInstance
,easeDecimal
,stepDecimal
);
771 if (currentStep
< totalSteps
){
772 chartInstance
.animationFrame
= requestAnimFrame(animationFrame
);
774 onComplete
.apply(chartInstance
);
777 requestAnimFrame(animationFrame
);
780 getRelativePosition
= helpers
.getRelativePosition = function(evt
){
782 var e
= evt
.originalEvent
|| evt
,
783 canvas
= evt
.currentTarget
|| evt
.srcElement
,
784 boundingRect
= canvas
.getBoundingClientRect();
787 mouseX
= e
.touches
[0].clientX
- boundingRect
.left
;
788 mouseY
= e
.touches
[0].clientY
- boundingRect
.top
;
792 mouseX
= e
.clientX
- boundingRect
.left
;
793 mouseY
= e
.clientY
- boundingRect
.top
;
802 addEvent
= helpers
.addEvent = function(node
,eventType
,method
){
803 if (node
.addEventListener
){
804 node
.addEventListener(eventType
,method
);
805 } else if (node
.attachEvent
){
806 node
.attachEvent("on"+eventType
, method
);
808 node
["on"+eventType
] = method
;
811 removeEvent
= helpers
.removeEvent = function(node
, eventType
, handler
){
812 if (node
.removeEventListener
){
813 node
.removeEventListener(eventType
, handler
, false);
814 } else if (node
.detachEvent
){
815 node
.detachEvent("on"+eventType
,handler
);
817 node
["on" + eventType
] = noop
;
820 bindEvents
= helpers
.bindEvents = function(chartInstance
, arrayOfEvents
, handler
){
821 // Create the events object if it's not already present
822 if (!chartInstance
.events
) chartInstance
.events
= {};
824 each(arrayOfEvents
,function(eventName
){
825 chartInstance
.events
[eventName
] = function(){
826 handler
.apply(chartInstance
, arguments
);
828 addEvent(chartInstance
.chart
.canvas
,eventName
,chartInstance
.events
[eventName
]);
831 unbindEvents
= helpers
.unbindEvents = function (chartInstance
, arrayOfEvents
) {
832 each(arrayOfEvents
, function(handler
,eventName
){
833 removeEvent(chartInstance
.chart
.canvas
, eventName
, handler
);
836 getMaximumWidth
= helpers
.getMaximumWidth = function(domNode
){
837 var container
= domNode
.parentNode
,
838 padding
= parseInt(getStyle(container
, 'padding-left')) + parseInt(getStyle(container
, 'padding-right'));
839 // TODO = check cross browser stuff with this.
840 return container
? container
.clientWidth
- padding
: 0;
842 getMaximumHeight
= helpers
.getMaximumHeight = function(domNode
){
843 var container
= domNode
.parentNode
,
844 padding
= parseInt(getStyle(container
, 'padding-bottom')) + parseInt(getStyle(container
, 'padding-top'));
845 // TODO = check cross browser stuff with this.
846 return container
? container
.clientHeight
- padding
: 0;
848 getStyle
= helpers
.getStyle = function (el
, property
) {
849 return el
.currentStyle
?
850 el
.currentStyle
[property
] :
851 document
.defaultView
.getComputedStyle(el
, null).getPropertyValue(property
);
853 getMaximumSize
= helpers
.getMaximumSize
= helpers
.getMaximumWidth
, // legacy support
854 retinaScale
= helpers
.retinaScale = function(chart
){
856 width
= chart
.canvas
.width
,
857 height
= chart
.canvas
.height
;
859 if (window
.devicePixelRatio
) {
860 ctx
.canvas
.style
.width
= width
+ "px";
861 ctx
.canvas
.style
.height
= height
+ "px";
862 ctx
.canvas
.height
= height
* window
.devicePixelRatio
;
863 ctx
.canvas
.width
= width
* window
.devicePixelRatio
;
864 ctx
.scale(window
.devicePixelRatio
, window
.devicePixelRatio
);
868 clear
= helpers
.clear = function(chart
){
869 chart
.ctx
.clearRect(0,0,chart
.width
,chart
.height
);
871 fontString
= helpers
.fontString = function(pixelSize
,fontStyle
,fontFamily
){
872 return fontStyle
+ " " + pixelSize
+"px " + fontFamily
;
874 longestText
= helpers
.longestText = function(ctx
,font
,arrayOfStrings
){
877 each(arrayOfStrings
,function(string
){
878 var textWidth
= ctx
.measureText(string
).width
;
879 longest
= (textWidth
> longest
) ? textWidth
: longest
;
883 drawRoundedRectangle
= helpers
.drawRoundedRectangle = function(ctx
,x
,y
,width
,height
,radius
){
885 ctx
.moveTo(x
+ radius
, y
);
886 ctx
.lineTo(x
+ width
- radius
, y
);
887 ctx
.quadraticCurveTo(x
+ width
, y
, x
+ width
, y
+ radius
);
888 ctx
.lineTo(x
+ width
, y
+ height
- radius
);
889 ctx
.quadraticCurveTo(x
+ width
, y
+ height
, x
+ width
- radius
, y
+ height
);
890 ctx
.lineTo(x
+ radius
, y
+ height
);
891 ctx
.quadraticCurveTo(x
, y
+ height
, x
, y
+ height
- radius
);
892 ctx
.lineTo(x
, y
+ radius
);
893 ctx
.quadraticCurveTo(x
, y
, x
+ radius
, y
);
898 //Store a reference to each instance - allowing us to globally resize chart instances on window resize.
899 //Destroy method on the chart will remove the instance of the chart from this reference.
900 Chart
.instances
= {};
902 Chart
.Type = function(data
,options
,chart
){
903 this.options
= options
;
906 //Add the chart instance to the global namespace
907 Chart
.instances
[this.id
] = this;
909 // Initialize is always called when a chart type is created
910 // By default it is a no op, but it should be extended
911 if (options
.responsive
){
914 this.initialize
.call(this,data
);
917 //Core methods that'll be a part of every chart type
918 extend(Chart
.Type
.prototype,{
919 initialize : function(){return this;},
925 // Stops any current animation loop occuring
926 Chart
.animationService
.cancelAnimation(this);
929 resize : function(callback
){
931 var canvas
= this.chart
.canvas
,
932 newWidth
= getMaximumWidth(this.chart
.canvas
),
933 newHeight
= this.options
.maintainAspectRatio
? newWidth
/ this.chart
.aspectRatio
: getMaximumHeight(this.chart
.canvas
);
935 canvas
.width
= this.chart
.width
= newWidth
;
936 canvas
.height
= this.chart
.height
= newHeight
;
938 retinaScale(this.chart
);
940 if (typeof callback
=== "function"){
941 callback
.apply(this, Array
.prototype.slice
.call(arguments
, 1));
946 render : function(reflow
){
951 if (this.options
.animation
&& !reflow
){
952 var animation
= new Chart
.Animation();
953 animation
.numSteps
= this.options
.animationSteps
;
954 animation
.easing
= this.options
.animationEasing
;
957 animation
.render = function(chartInstance
, animationObject
) {
958 var easingFunction
= helpers
.easingEffects
[animationObject
.easing
];
959 var stepDecimal
= animationObject
.currentStep
/ animationObject
.numSteps
;
960 var easeDecimal
= easingFunction(stepDecimal
);
962 chartInstance
.draw(easeDecimal
, stepDecimal
, animationObject
.currentStep
);
966 animation
.onAnimationProgress
= this.options
.onAnimationProgress
;
967 animation
.onAnimationComplete
= this.options
.onAnimationComplete
;
969 Chart
.animationService
.addAnimation(this, animation
);
973 this.options
.onAnimationComplete
.call(this);
977 generateLegend : function(){
978 return helpers
.template(this.options
.legendTemplate
, this);
980 destroy : function(){
983 unbindEvents(this, this.events
);
984 var canvas
= this.chart
.canvas
;
986 // Reset canvas height/width attributes starts a fresh with the canvas context
987 canvas
.width
= this.chart
.width
;
988 canvas
.height
= this.chart
.height
;
990 // < IE9 doesn't support removeProperty
991 if (canvas
.style
.removeProperty
) {
992 canvas
.style
.removeProperty('width');
993 canvas
.style
.removeProperty('height');
995 canvas
.style
.removeAttribute('width');
996 canvas
.style
.removeAttribute('height');
999 delete Chart
.instances
[this.id
];
1001 showTooltip : function(ChartElements
, forceRedraw
){
1002 // Only redraw the chart if we've actually changed what we're hovering on.
1003 if (typeof this.activeElements
=== 'undefined') this.activeElements
= [];
1005 var isChanged
= (function(Elements
){
1006 var changed
= false;
1008 if (Elements
.length
!== this.activeElements
.length
){
1013 each(Elements
, function(element
, index
){
1014 if (element
!== this.activeElements
[index
]){
1019 }).call(this, ChartElements
);
1021 if (!isChanged
&& !forceRedraw
){
1025 this.activeElements
= ChartElements
;
1028 if(this.options
.customTooltips
){
1029 this.options
.customTooltips(false);
1031 if (ChartElements
.length
> 0){
1032 // If we have multiple datasets, show a MultiTooltip for all of the data points at that index
1033 if (this.datasets
&& this.datasets
.length
> 1) {
1037 for (var i
= this.datasets
.length
- 1; i
>= 0; i
--) {
1038 dataArray
= this.datasets
[i
].points
|| this.datasets
[i
].bars
|| this.datasets
[i
].segments
;
1039 dataIndex
= indexOf(dataArray
, ChartElements
[0]);
1040 if (dataIndex
!== -1){
1044 var tooltipLabels
= [],
1046 medianPosition
= (function(index
) {
1048 // Get all the points at that particular index
1057 helpers
.each(this.datasets
, function(dataset
){
1058 dataCollection
= dataset
.points
|| dataset
.bars
|| dataset
.segments
;
1059 if (dataCollection
[dataIndex
] && dataCollection
[dataIndex
].hasValue()){
1060 Elements
.push(dataCollection
[dataIndex
]);
1064 helpers
.each(Elements
, function(element
) {
1065 xPositions
.push(element
.x
);
1066 yPositions
.push(element
.y
);
1069 //Include any colour information about the element
1070 tooltipLabels
.push(helpers
.template(this.options
.multiTooltipTemplate
, element
));
1071 tooltipColors
.push({
1072 fill
: element
._saved
.fillColor
|| element
.fillColor
,
1073 stroke
: element
._saved
.strokeColor
|| element
.strokeColor
1078 yMin
= min(yPositions
);
1079 yMax
= max(yPositions
);
1081 xMin
= min(xPositions
);
1082 xMax
= max(xPositions
);
1085 x
: (xMin
> this.chart
.width
/2) ? xMin
: xMax
,
1088 }).call(this, dataIndex
);
1090 new Chart
.MultiTooltip({
1091 x
: medianPosition
.x
,
1092 y
: medianPosition
.y
,
1093 xPadding
: this.options
.tooltipXPadding
,
1094 yPadding
: this.options
.tooltipYPadding
,
1095 xOffset
: this.options
.tooltipXOffset
,
1096 fillColor
: this.options
.tooltipFillColor
,
1097 textColor
: this.options
.tooltipFontColor
,
1098 fontFamily
: this.options
.tooltipFontFamily
,
1099 fontStyle
: this.options
.tooltipFontStyle
,
1100 fontSize
: this.options
.tooltipFontSize
,
1101 titleTextColor
: this.options
.tooltipTitleFontColor
,
1102 titleFontFamily
: this.options
.tooltipTitleFontFamily
,
1103 titleFontStyle
: this.options
.tooltipTitleFontStyle
,
1104 titleFontSize
: this.options
.tooltipTitleFontSize
,
1105 cornerRadius
: this.options
.tooltipCornerRadius
,
1106 labels
: tooltipLabels
,
1107 legendColors
: tooltipColors
,
1108 legendColorBackground
: this.options
.multiTooltipKeyBackground
,
1109 title
: template(this.options
.tooltipTitleTemplate
,ChartElements
[0]),
1111 ctx
: this.chart
.ctx
,
1112 custom
: this.options
.customTooltips
1116 each(ChartElements
, function(Element
) {
1117 var tooltipPosition
= Element
.tooltipPosition();
1119 x
: Math
.round(tooltipPosition
.x
),
1120 y
: Math
.round(tooltipPosition
.y
),
1121 xPadding
: this.options
.tooltipXPadding
,
1122 yPadding
: this.options
.tooltipYPadding
,
1123 fillColor
: this.options
.tooltipFillColor
,
1124 textColor
: this.options
.tooltipFontColor
,
1125 fontFamily
: this.options
.tooltipFontFamily
,
1126 fontStyle
: this.options
.tooltipFontStyle
,
1127 fontSize
: this.options
.tooltipFontSize
,
1128 caretHeight
: this.options
.tooltipCaretSize
,
1129 cornerRadius
: this.options
.tooltipCornerRadius
,
1130 text
: template(this.options
.tooltipTemplate
, Element
),
1132 custom
: this.options
.customTooltips
1139 toBase64Image : function(){
1140 return this.chart
.canvas
.toDataURL
.apply(this.chart
.canvas
, arguments
);
1144 Chart
.Type
.extend = function(extensions
){
1148 var ChartType = function(){
1149 return parent
.apply(this,arguments
);
1152 //Copy the prototype object of the this class
1153 ChartType
.prototype = clone(parent
.prototype);
1154 //Now overwrite some of the properties in the base class with the new extensions
1155 extend(ChartType
.prototype, extensions
);
1157 ChartType
.extend
= Chart
.Type
.extend
;
1159 if (extensions
.name
|| parent
.prototype.name
){
1161 var chartName
= extensions
.name
|| parent
.prototype.name
;
1162 //Assign any potential default values of the new chart type
1164 //If none are defined, we'll use a clone of the chart type this is being extended from.
1165 //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
1166 //doesn't define some defaults of their own.
1168 var baseDefaults
= (Chart
.defaults
[parent
.prototype.name
]) ? clone(Chart
.defaults
[parent
.prototype.name
]) : {};
1170 Chart
.defaults
[chartName
] = extend(baseDefaults
,extensions
.defaults
);
1172 Chart
.types
[chartName
] = ChartType
;
1174 //Register this new chart type in the Chart prototype
1175 Chart
.prototype[chartName
] = function(data
,options
){
1176 var config
= merge(Chart
.defaults
.global
, Chart
.defaults
[chartName
], options
|| {});
1177 return new ChartType(data
,config
,this);
1180 warn("Name not provided for this chart, so it hasn't been registered");
1185 Chart
.Element = function(configuration
){
1186 extend(this,configuration
);
1187 this.initialize
.apply(this,arguments
);
1190 extend(Chart
.Element
.prototype,{
1191 initialize : function(){},
1192 restore : function(props
){
1194 extend(this,this._saved
);
1196 each(props
,function(key
){
1197 this[key
] = this._saved
[key
];
1203 this._saved
= clone(this);
1204 delete this._saved
._saved
;
1207 update : function(newProps
){
1208 each(newProps
,function(value
,key
){
1209 this._saved
[key
] = this[key
];
1214 transition : function(props
,ease
){
1215 each(props
,function(value
,key
){
1216 this[key
] = ((value
- this._saved
[key
]) * ease
) + this._saved
[key
];
1220 tooltipPosition : function(){
1226 hasValue: function(){
1227 return isNumber(this.value
);
1231 Chart
.Element
.extend
= inherits
;
1234 Chart
.Point
= Chart
.Element
.extend({
1236 inRange: function(chartX
,chartY
){
1237 var hitDetectionRange
= this.hitDetectionRadius
+ this.radius
;
1238 return ((Math
.pow(chartX
-this.x
, 2)+Math
.pow(chartY
-this.y
, 2)) < Math
.pow(hitDetectionRange
,2));
1245 ctx
.arc(this.x
, this.y
, this.radius
, 0, Math
.PI
*2);
1248 ctx
.strokeStyle
= this.strokeColor
;
1249 ctx
.lineWidth
= this.strokeWidth
;
1251 ctx
.fillStyle
= this.fillColor
;
1258 //Quick debug for bezier curve splining
1259 //Highlights control points and the line between them.
1260 //Handy for dev - stripped in the min version.
1263 // ctx.fillStyle = "black";
1264 // ctx.strokeStyle = "black"
1266 // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
1270 // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
1273 // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
1274 // ctx.lineTo(this.x, this.y);
1275 // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
1285 Chart
.Arc
= Chart
.Element
.extend({
1286 inRange : function(chartX
,chartY
){
1288 var pointRelativePosition
= helpers
.getAngleFromPoint(this, {
1293 // Normalize all angles to 0 - 2*PI (0 - 360°)
1294 var pointRelativeAngle
= pointRelativePosition
.angle
% (Math
.PI
* 2),
1295 startAngle
= (Math
.PI
* 2 + this.startAngle
) % (Math
.PI
* 2),
1296 endAngle
= (Math
.PI
* 2 + this.endAngle
) % (Math
.PI
* 2) || 360;
1298 // Calculate wether the pointRelativeAngle is between the start and the end angle
1299 var betweenAngles
= (endAngle
< startAngle
) ?
1300 pointRelativeAngle
<= endAngle
|| pointRelativeAngle
>= startAngle
:
1301 pointRelativeAngle
>= startAngle
&& pointRelativeAngle
<= endAngle
;
1303 //Check if within the range of the open/close angle
1304 var withinRadius
= (pointRelativePosition
.distance
>= this.innerRadius
&& pointRelativePosition
.distance
<= this.outerRadius
);
1306 return (betweenAngles
&& withinRadius
);
1307 //Ensure within the outside of the arc centre, but inside arc outer
1309 tooltipPosition : function(){
1310 var centreAngle
= this.startAngle
+ ((this.endAngle
- this.startAngle
) / 2),
1311 rangeFromCentre
= (this.outerRadius
- this.innerRadius
) / 2 + this.innerRadius
;
1313 x
: this.x
+ (Math
.cos(centreAngle
) * rangeFromCentre
),
1314 y
: this.y
+ (Math
.sin(centreAngle
) * rangeFromCentre
)
1317 draw : function(animationPercent
){
1319 var easingDecimal
= animationPercent
|| 1;
1325 ctx
.arc(this.x
, this.y
, this.outerRadius
< 0 ? 0 : this.outerRadius
, this.startAngle
, this.endAngle
);
1327 ctx
.arc(this.x
, this.y
, this.innerRadius
< 0 ? 0 : this.innerRadius
, this.endAngle
, this.startAngle
, true);
1330 ctx
.strokeStyle
= this.strokeColor
;
1331 ctx
.lineWidth
= this.strokeWidth
;
1333 ctx
.fillStyle
= this.fillColor
;
1336 ctx
.lineJoin
= 'bevel';
1338 if (this.showStroke
){
1344 Chart
.Rectangle
= Chart
.Element
.extend({
1347 halfWidth
= this.width
/2,
1348 leftX
= this.x
- halfWidth
,
1349 rightX
= this.x
+ halfWidth
,
1350 top
= this.base
- (this.base
- this.y
),
1351 halfStroke
= this.strokeWidth
/ 2;
1353 // Canvas doesn't allow us to stroke inside the width so we can
1354 // adjust the sizes to fit if we're setting a stroke on the line
1355 if (this.showStroke
){
1356 leftX
+= halfStroke
;
1357 rightX
-= halfStroke
;
1363 ctx
.fillStyle
= this.fillColor
;
1364 ctx
.strokeStyle
= this.strokeColor
;
1365 ctx
.lineWidth
= this.strokeWidth
;
1367 // It'd be nice to keep this class totally generic to any rectangle
1368 // and simply specify which border to miss out.
1369 ctx
.moveTo(leftX
, this.base
);
1370 ctx
.lineTo(leftX
, top
);
1371 ctx
.lineTo(rightX
, top
);
1372 ctx
.lineTo(rightX
, this.base
);
1374 if (this.showStroke
){
1378 height : function(){
1379 return this.base
- this.y
;
1381 inRange : function(chartX
,chartY
){
1382 return (chartX
>= this.x
- this.width
/2 && chartX <= this.x + this.width/2) && (chartY
>= this.y
&& chartY
<= this.base
);
1386 Chart
.Animation
= Chart
.Element
.extend({
1387 currentStep
: null, // the current animation step
1388 numSteps
: 60, // default number of steps
1389 easing
: "", // the easing to use for this animation
1390 render
: null, // render function used by the animation service
1392 onAnimationProgress
: null, // user specified callback to fire on each step of the animation
1393 onAnimationComplete
: null, // user specified callback to fire when the animation finishes
1396 Chart
.Tooltip
= Chart
.Element
.extend({
1399 var ctx
= this.chart
.ctx
;
1401 ctx
.font
= fontString(this.fontSize
,this.fontStyle
,this.fontFamily
);
1403 this.xAlign
= "center";
1404 this.yAlign
= "above";
1406 //Distance between the actual element.y position and the start of the tooltip caret
1407 var caretPadding
= this.caretPadding
= 2;
1409 var tooltipWidth
= ctx
.measureText(this.text
).width
+ 2*this.xPadding
,
1410 tooltipRectHeight
= this.fontSize
+ 2*this.yPadding
,
1411 tooltipHeight
= tooltipRectHeight
+ this.caretHeight
+ caretPadding
;
1413 if (this.x
+ tooltipWidth
/2 >this.chart
.width
){
1414 this.xAlign
= "left";
1415 } else if (this.x
- tooltipWidth
/2 < 0){
1416 this.xAlign
= "right";
1419 if (this.y
- tooltipHeight
< 0){
1420 this.yAlign
= "below";
1424 var tooltipX
= this.x
- tooltipWidth
/2,
1425 tooltipY
= this.y
- tooltipHeight
;
1427 ctx
.fillStyle
= this.fillColor
;
1437 //Draw a caret above the x/y
1439 ctx
.moveTo(this.x
,this.y
- caretPadding
);
1440 ctx
.lineTo(this.x
+ this.caretHeight
, this.y
- (caretPadding
+ this.caretHeight
));
1441 ctx
.lineTo(this.x
- this.caretHeight
, this.y
- (caretPadding
+ this.caretHeight
));
1446 tooltipY
= this.y
+ caretPadding
+ this.caretHeight
;
1447 //Draw a caret below the x/y
1449 ctx
.moveTo(this.x
, this.y
+ caretPadding
);
1450 ctx
.lineTo(this.x
+ this.caretHeight
, this.y
+ caretPadding
+ this.caretHeight
);
1451 ctx
.lineTo(this.x
- this.caretHeight
, this.y
+ caretPadding
+ this.caretHeight
);
1460 tooltipX
= this.x
- tooltipWidth
+ (this.cornerRadius
+ this.caretHeight
);
1463 tooltipX
= this.x
- (this.cornerRadius
+ this.caretHeight
);
1467 drawRoundedRectangle(ctx
,tooltipX
,tooltipY
,tooltipWidth
,tooltipRectHeight
,this.cornerRadius
);
1471 ctx
.fillStyle
= this.textColor
;
1472 ctx
.textAlign
= "center";
1473 ctx
.textBaseline
= "middle";
1474 ctx
.fillText(this.text
, tooltipX
+ tooltipWidth
/2, tooltipY + tooltipRectHeight/2);
1479 Chart
.MultiTooltip
= Chart
.Element
.extend({
1480 initialize : function(){
1481 this.font
= fontString(this.fontSize
,this.fontStyle
,this.fontFamily
);
1483 this.titleFont
= fontString(this.titleFontSize
,this.titleFontStyle
,this.titleFontFamily
);
1485 this.titleHeight
= this.title
? this.titleFontSize
* 1.5 : 0;
1486 this.height
= (this.labels
.length
* this.fontSize
) + ((this.labels
.length
-1) * (this.fontSize
/2)) + (this.yPadding
*2) + this.titleHeight
;
1488 this.ctx
.font
= this.titleFont
;
1490 var titleWidth
= this.ctx
.measureText(this.title
).width
,
1491 //Label has a legend square as well so account for this.
1492 labelWidth
= longestText(this.ctx
,this.font
,this.labels
) + this.fontSize
+ 3,
1493 longestTextWidth
= max([labelWidth
,titleWidth
]);
1495 this.width
= longestTextWidth
+ (this.xPadding
*2);
1498 var halfHeight
= this.height
/2;
1500 //Check to ensure the height will fit on the canvas
1501 if (this.y
- halfHeight
< 0 ){
1502 this.y
= halfHeight
;
1503 } else if (this.y
+ halfHeight
> this.chart
.height
){
1504 this.y
= this.chart
.height
- halfHeight
;
1507 //Decide whether to align left or right based on position on canvas
1508 if (this.x
> this.chart
.width
/2){
1509 this.x
-= this.xOffset
+ this.width
;
1511 this.x
+= this.xOffset
;
1516 getLineHeight : function(index
){
1517 var baseLineHeight
= this.y
- (this.height
/2) + this.yPadding
,
1518 afterTitleIndex
= index
-1;
1520 //If the index is zero, we're getting the title
1522 return baseLineHeight
+ this.titleHeight
/ 3;
1524 return baseLineHeight
+ ((this.fontSize
* 1.5 * afterTitleIndex
) + this.fontSize
/ 2) + this.titleHeight
;
1534 drawRoundedRectangle(this.ctx
,this.x
,this.y
- this.height
/2,this.width
,this.height
,this.cornerRadius
);
1536 ctx
.fillStyle
= this.fillColor
;
1540 ctx
.textAlign
= "left";
1541 ctx
.textBaseline
= "middle";
1542 ctx
.fillStyle
= this.titleTextColor
;
1543 ctx
.font
= this.titleFont
;
1545 ctx
.fillText(this.title
,this.x
+ this.xPadding
, this.getLineHeight(0));
1547 ctx
.font
= this.font
;
1548 helpers
.each(this.labels
,function(label
,index
){
1549 ctx
.fillStyle
= this.textColor
;
1550 ctx
.fillText(label
,this.x
+ this.xPadding
+ this.fontSize
+ 3, this.getLineHeight(index
+ 1));
1552 //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
1553 //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
1554 //Instead we'll make a white filled block to put the legendColour palette over.
1556 ctx
.fillStyle
= this.legendColorBackground
;
1557 ctx
.fillRect(this.x
+ this.xPadding
, this.getLineHeight(index
+ 1) - this.fontSize
/2, this.fontSize
, this.fontSize
);
1559 ctx
.fillStyle
= this.legendColors
[index
].fill
;
1560 ctx
.fillRect(this.x
+ this.xPadding
, this.getLineHeight(index
+ 1) - this.fontSize
/2, this.fontSize
, this.fontSize
);
1568 Chart
.Scale
= Chart
.Element
.extend({
1569 initialize : function(){
1572 buildYLabels : function(){
1575 var stepDecimalPlaces
= getDecimalPlaces(this.stepValue
);
1577 for (var i
=0; i
<=this.steps
; i
++){
1578 this.yLabels
.push(template(this.templateString
,{value
:(this.min
+ (i
* this.stepValue
)).toFixed(stepDecimalPlaces
)}));
1580 this.yLabelWidth
= (this.display
&& this.showLabels
) ? longestText(this.ctx
,this.font
,this.yLabels
) + 10 : 0;
1582 addXLabel : function(label
){
1583 this.xLabels
.push(label
);
1587 removeXLabel : function(){
1588 this.xLabels
.shift();
1592 // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
1594 // First we need the width of the yLabels, assuming the xLabels aren't rotated
1596 // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
1597 this.startPoint
= (this.display
) ? this.fontSize
: 0;
1598 this.endPoint
= (this.display
) ? this.height
- (this.fontSize
* 1.5) - 5 : this.height
; // -5 to pad labels
1600 // Apply padding settings to the start and end point.
1601 this.startPoint
+= this.padding
;
1602 this.endPoint
-= this.padding
;
1604 // Cache the starting endpoint, excluding the space for x labels
1605 var cachedEndPoint
= this.endPoint
;
1607 // Cache the starting height, so can determine if we need to recalculate the scale yAxis
1608 var cachedHeight
= this.endPoint
- this.startPoint
,
1611 // Build the current yLabels so we have an idea of what size they'll be to start
1613 * This sets what is returned from calculateScaleRange as static properties of this class:
1621 this.calculateYRange(cachedHeight
);
1623 // With these properties set we can now build the array of yLabels
1624 // and also the width of the largest yLabel
1625 this.buildYLabels();
1627 this.calculateXLabelRotation();
1629 while((cachedHeight
> this.endPoint
- this.startPoint
)){
1630 cachedHeight
= this.endPoint
- this.startPoint
;
1631 cachedYLabelWidth
= this.yLabelWidth
;
1633 this.calculateYRange(cachedHeight
);
1634 this.buildYLabels();
1636 // Only go through the xLabel loop again if the yLabel width has changed
1637 if (cachedYLabelWidth
< this.yLabelWidth
){
1638 this.endPoint
= cachedEndPoint
;
1639 this.calculateXLabelRotation();
1644 calculateXLabelRotation : function(){
1645 //Get the width of each grid by calculating the difference
1646 //between x offsets between 0 and 1.
1648 this.ctx
.font
= this.font
;
1650 var firstWidth
= this.ctx
.measureText(this.xLabels
[0]).width
,
1651 lastWidth
= this.ctx
.measureText(this.xLabels
[this.xLabels
.length
- 1]).width
,
1656 this.xScalePaddingRight
= lastWidth
/2 + 3;
1657 this.xScalePaddingLeft
= (firstWidth
/2 > this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth
;
1659 this.xLabelRotation
= 0;
1661 var originalLabelWidth
= longestText(this.ctx
,this.font
,this.xLabels
),
1664 this.xLabelWidth
= originalLabelWidth
;
1665 //Allow 3 pixels x2 padding either side for label readability
1666 var xGridWidth
= Math
.floor(this.calculateX(1) - this.calculateX(0)) - 6;
1668 //Max label rotate should be 90 - also act as a loop counter
1669 while ((this.xLabelWidth
> xGridWidth
&& this.xLabelRotation
=== 0) || (this.xLabelWidth
> xGridWidth
&& this.xLabelRotation
<= 90 && this.xLabelRotation
> 0)){
1670 cosRotation
= Math
.cos(toRadians(this.xLabelRotation
));
1672 firstRotated
= cosRotation
* firstWidth
;
1673 lastRotated
= cosRotation
* lastWidth
;
1675 // We're right aligning the text now.
1676 if (firstRotated
+ this.fontSize
/ 2 > this.yLabelWidth
){
1677 this.xScalePaddingLeft
= firstRotated
+ this.fontSize
/ 2;
1679 this.xScalePaddingRight
= this.fontSize
/2;
1682 this.xLabelRotation
++;
1683 this.xLabelWidth
= cosRotation
* originalLabelWidth
;
1686 if (this.xLabelRotation
> 0){
1687 this.endPoint
-= Math
.sin(toRadians(this.xLabelRotation
))*originalLabelWidth
+ 3;
1691 this.xLabelWidth
= 0;
1692 this.xScalePaddingRight
= this.padding
;
1693 this.xScalePaddingLeft
= this.padding
;
1697 // Needs to be overidden in each Chart type
1698 // Otherwise we need to pass all the data into the scale class
1699 calculateYRange
: noop
,
1700 drawingArea: function(){
1701 return this.startPoint
- this.endPoint
;
1703 calculateY : function(value
){
1704 var scalingFactor
= this.drawingArea() / (this.min
- this.max
);
1705 return this.endPoint
- (scalingFactor
* (value
- this.min
));
1707 calculateX : function(index
){
1708 var isRotated
= (this.xLabelRotation
> 0),
1709 // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
1710 innerWidth
= this.width
- (this.xScalePaddingLeft
+ this.xScalePaddingRight
),
1711 valueWidth
= innerWidth
/Math
.max((this.valuesCount
- ((this.offsetGridLines
) ? 0 : 1)), 1),
1712 valueOffset
= (valueWidth
* index
) + this.xScalePaddingLeft
;
1714 if (this.offsetGridLines
){
1715 valueOffset
+= (valueWidth
/2);
1718 return Math
.round(valueOffset
);
1720 update : function(newProps
){
1721 helpers
.extend(this, newProps
);
1726 yLabelGap
= (this.endPoint
- this.startPoint
) / this.steps
,
1727 xStart
= Math
.round(this.xScalePaddingLeft
);
1729 ctx
.fillStyle
= this.textColor
;
1730 ctx
.font
= this.font
;
1731 each(this.yLabels
,function(labelString
,index
){
1732 var yLabelCenter
= this.endPoint
- (yLabelGap
* index
),
1733 linePositionY
= Math
.round(yLabelCenter
),
1734 drawHorizontalLine
= this.showHorizontalLines
;
1736 ctx
.textAlign
= "right";
1737 ctx
.textBaseline
= "middle";
1738 if (this.showLabels
){
1739 ctx
.fillText(labelString
,xStart
- 10,yLabelCenter
);
1742 // This is X axis, so draw it
1743 if (index
=== 0 && !drawHorizontalLine
){
1744 drawHorizontalLine
= true;
1747 if (drawHorizontalLine
){
1752 // This is a grid line in the centre, so drop that
1753 ctx
.lineWidth
= this.gridLineWidth
;
1754 ctx
.strokeStyle
= this.gridLineColor
;
1756 // This is the first line on the scale
1757 ctx
.lineWidth
= this.lineWidth
;
1758 ctx
.strokeStyle
= this.lineColor
;
1761 linePositionY
+= helpers
.aliasPixel(ctx
.lineWidth
);
1763 if(drawHorizontalLine
){
1764 ctx
.moveTo(xStart
, linePositionY
);
1765 ctx
.lineTo(this.width
, linePositionY
);
1770 ctx
.lineWidth
= this.lineWidth
;
1771 ctx
.strokeStyle
= this.lineColor
;
1773 ctx
.moveTo(xStart
- 5, linePositionY
);
1774 ctx
.lineTo(xStart
, linePositionY
);
1780 each(this.xLabels
,function(label
,index
){
1781 var xPos
= this.calculateX(index
) + aliasPixel(this.lineWidth
),
1782 // Check to see if line/bar here and decide where to place the line
1783 linePos
= this.calculateX(index
- (this.offsetGridLines
? 0.5 : 0)) + aliasPixel(this.lineWidth
),
1784 isRotated
= (this.xLabelRotation
> 0),
1785 drawVerticalLine
= this.showVerticalLines
;
1787 // This is Y axis, so draw it
1788 if (index
=== 0 && !drawVerticalLine
){
1789 drawVerticalLine
= true;
1792 if (drawVerticalLine
){
1797 // This is a grid line in the centre, so drop that
1798 ctx
.lineWidth
= this.gridLineWidth
;
1799 ctx
.strokeStyle
= this.gridLineColor
;
1801 // This is the first line on the scale
1802 ctx
.lineWidth
= this.lineWidth
;
1803 ctx
.strokeStyle
= this.lineColor
;
1806 if (drawVerticalLine
){
1807 ctx
.moveTo(linePos
,this.endPoint
);
1808 ctx
.lineTo(linePos
,this.startPoint
- 3);
1814 ctx
.lineWidth
= this.lineWidth
;
1815 ctx
.strokeStyle
= this.lineColor
;
1818 // Small lines at the bottom of the base grid line
1820 ctx
.moveTo(linePos
,this.endPoint
);
1821 ctx
.lineTo(linePos
,this.endPoint
+ 5);
1826 ctx
.translate(xPos
,(isRotated
) ? this.endPoint
+ 12 : this.endPoint
+ 8);
1827 ctx
.rotate(toRadians(this.xLabelRotation
)*-1);
1828 ctx
.font
= this.font
;
1829 ctx
.textAlign
= (isRotated
) ? "right" : "center";
1830 ctx
.textBaseline
= (isRotated
) ? "middle" : "top";
1831 ctx
.fillText(label
, 0, 0);
1840 Chart
.RadialScale
= Chart
.Element
.extend({
1841 initialize: function(){
1842 this.size
= min([this.height
, this.width
]);
1843 this.drawingArea
= (this.display
) ? (this.size
/2) - (this.fontSize/2 + this.backdropPaddingY
) : (this.size
/2);
1845 calculateCenterOffset: function(value
){
1846 // Take into account half font size + the yPadding of the top value
1847 var scalingFactor
= this.drawingArea
/ (this.max
- this.min
);
1849 return (value
- this.min
) * scalingFactor
;
1851 update : function(){
1853 this.setScaleSize();
1855 this.drawingArea
= (this.display
) ? (this.size
/2) - (this.fontSize/2 + this.backdropPaddingY
) : (this.size
/2);
1857 this.buildYLabels();
1859 buildYLabels: function(){
1862 var stepDecimalPlaces
= getDecimalPlaces(this.stepValue
);
1864 for (var i
=0; i
<=this.steps
; i
++){
1865 this.yLabels
.push(template(this.templateString
,{value
:(this.min
+ (i
* this.stepValue
)).toFixed(stepDecimalPlaces
)}));
1868 getCircumference : function(){
1869 return ((Math
.PI
*2) / this.valuesCount
);
1871 setScaleSize: function(){
1873 * Right, this is really confusing and there is a lot of maths going on here
1874 * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
1876 * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
1880 * We assume the radius of the polygon is half the size of the canvas at first
1881 * at each index we check if the text overlaps.
1883 * Where it does, we store that angle and that index.
1885 * After finding the largest index and angle we calculate how much we need to remove
1886 * from the shape radius to move the point inwards by that x.
1888 * We average the left and right distances to get the maximum shape radius that can fit in the box
1889 * along with labels.
1891 * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
1892 * on each side, removing that from the size, halving it and adding the left x protrusion width.
1894 * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
1895 * and position it in the most space efficient manner
1897 * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
1901 // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
1902 // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
1903 var largestPossibleRadius
= min([(this.height
/2 - this.pointLabelFontSize - 5), this.width/2]),
1908 furthestRight
= this.width
,
1916 radiusReductionRight
,
1917 radiusReductionLeft
,
1919 this.ctx
.font
= fontString(this.pointLabelFontSize
,this.pointLabelFontStyle
,this.pointLabelFontFamily
);
1920 for (i
=0;i
<this.valuesCount
;i
++){
1921 // 5px to space the text slightly out - similar to what we do in the draw function.
1922 pointPosition
= this.getPointPosition(i
, largestPossibleRadius
);
1923 textWidth
= this.ctx
.measureText(template(this.templateString
, { value
: this.labels
[i
] })).width
+ 5;
1924 if (i
=== 0 || i
=== this.valuesCount
/2){
1925 // If we're at index zero, or exactly the middle, we're at exactly the top/bottom
1926 // of the radar chart, so text will be aligned centrally, so we'll half it and compare
1927 // w/left and right text sizes
1928 halfTextWidth
= textWidth
/2;
1929 if (pointPosition
.x
+ halfTextWidth
> furthestRight
) {
1930 furthestRight
= pointPosition
.x
+ halfTextWidth
;
1931 furthestRightIndex
= i
;
1933 if (pointPosition
.x
- halfTextWidth
< furthestLeft
) {
1934 furthestLeft
= pointPosition
.x
- halfTextWidth
;
1935 furthestLeftIndex
= i
;
1938 else if (i
< this.valuesCount
/2) {
1939 // Less than half the values means we'll left align the text
1940 if (pointPosition
.x
+ textWidth
> furthestRight
) {
1941 furthestRight
= pointPosition
.x
+ textWidth
;
1942 furthestRightIndex
= i
;
1945 else if (i
> this.valuesCount
/2){
1946 // More than half the values means we'll right align the text
1947 if (pointPosition
.x
- textWidth
< furthestLeft
) {
1948 furthestLeft
= pointPosition
.x
- textWidth
;
1949 furthestLeftIndex
= i
;
1954 xProtrusionLeft
= furthestLeft
;
1956 xProtrusionRight
= Math
.ceil(furthestRight
- this.width
);
1958 furthestRightAngle
= this.getIndexAngle(furthestRightIndex
);
1960 furthestLeftAngle
= this.getIndexAngle(furthestLeftIndex
);
1962 radiusReductionRight
= xProtrusionRight
/ Math
.sin(furthestRightAngle
+ Math
.PI
/2);
1964 radiusReductionLeft
= xProtrusionLeft
/ Math
.sin(furthestLeftAngle
+ Math
.PI
/2);
1966 // Ensure we actually need to reduce the size of the chart
1967 radiusReductionRight
= (isNumber(radiusReductionRight
)) ? radiusReductionRight
: 0;
1968 radiusReductionLeft
= (isNumber(radiusReductionLeft
)) ? radiusReductionLeft
: 0;
1970 this.drawingArea
= largestPossibleRadius
- (radiusReductionLeft
+ radiusReductionRight
)/2;
1972 //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
1973 this.setCenterPoint(radiusReductionLeft
, radiusReductionRight
);
1976 setCenterPoint: function(leftMovement
, rightMovement
){
1978 var maxRight
= this.width
- rightMovement
- this.drawingArea
,
1979 maxLeft
= leftMovement
+ this.drawingArea
;
1981 this.xCenter
= (maxLeft
+ maxRight
)/2;
1982 // Always vertically in the centre as the text height doesn't change
1983 this.yCenter
= (this.height
/2);
1986 getIndexAngle : function(index
){
1987 var angleMultiplier
= (Math
.PI
* 2) / this.valuesCount
;
1988 // Start from the top instead of right, so remove a quarter of the circle
1990 return index
* angleMultiplier
- (Math
.PI
/2);
1992 getPointPosition : function(index
, distanceFromCenter
){
1993 var thisAngle
= this.getIndexAngle(index
);
1995 x
: (Math
.cos(thisAngle
) * distanceFromCenter
) + this.xCenter
,
1996 y
: (Math
.sin(thisAngle
) * distanceFromCenter
) + this.yCenter
2002 each(this.yLabels
, function(label
, index
){
2003 // Don't draw a centre value
2005 var yCenterOffset
= index
* (this.drawingArea
/this.steps
),
2006 yHeight
= this.yCenter
- yCenterOffset
,
2009 // Draw circular lines around the scale
2010 if (this.lineWidth
> 0){
2011 ctx
.strokeStyle
= this.lineColor
;
2012 ctx
.lineWidth
= this.lineWidth
;
2016 ctx
.arc(this.xCenter
, this.yCenter
, yCenterOffset
, 0, Math
.PI
*2);
2021 for (var i
=0;i
<this.valuesCount
;i
++)
2023 pointPosition
= this.getPointPosition(i
, this.calculateCenterOffset(this.min
+ (index
* this.stepValue
)));
2025 ctx
.moveTo(pointPosition
.x
, pointPosition
.y
);
2027 ctx
.lineTo(pointPosition
.x
, pointPosition
.y
);
2034 if(this.showLabels
){
2035 ctx
.font
= fontString(this.fontSize
,this.fontStyle
,this.fontFamily
);
2036 if (this.showLabelBackdrop
){
2037 var labelWidth
= ctx
.measureText(label
).width
;
2038 ctx
.fillStyle
= this.backdropColor
;
2040 this.xCenter
- labelWidth
/2 - this.backdropPaddingX
,
2041 yHeight
- this.fontSize
/2 - this.backdropPaddingY
,
2042 labelWidth
+ this.backdropPaddingX
*2,
2043 this.fontSize
+ this.backdropPaddingY
*2
2046 ctx
.textAlign
= 'center';
2047 ctx
.textBaseline
= "middle";
2048 ctx
.fillStyle
= this.fontColor
;
2049 ctx
.fillText(label
, this.xCenter
, yHeight
);
2055 ctx
.lineWidth
= this.angleLineWidth
;
2056 ctx
.strokeStyle
= this.angleLineColor
;
2057 for (var i
= this.valuesCount
- 1; i
>= 0; i
--) {
2058 var centerOffset
= null, outerPosition
= null;
2060 if (this.angleLineWidth
> 0 && (i
% this.angleLineInterval
=== 0)){
2061 centerOffset
= this.calculateCenterOffset(this.max
);
2062 outerPosition
= this.getPointPosition(i
, centerOffset
);
2064 ctx
.moveTo(this.xCenter
, this.yCenter
);
2065 ctx
.lineTo(outerPosition
.x
, outerPosition
.y
);
2070 if (this.backgroundColors
&& this.backgroundColors
.length
== this.valuesCount
) {
2071 if (centerOffset
== null)
2072 centerOffset
= this.calculateCenterOffset(this.max
);
2074 if (outerPosition
== null)
2075 outerPosition
= this.getPointPosition(i
, centerOffset
);
2077 var previousOuterPosition
= this.getPointPosition(i
=== 0 ? this.valuesCount
- 1 : i
- 1, centerOffset
);
2078 var nextOuterPosition
= this.getPointPosition(i
=== this.valuesCount
- 1 ? 0 : i
+ 1, centerOffset
);
2080 var previousOuterHalfway
= { x
: (previousOuterPosition
.x
+ outerPosition
.x
) / 2, y
: (previousOuterPosition
.y
+ outerPosition
.y
) / 2 };
2081 var nextOuterHalfway
= { x
: (outerPosition
.x
+ nextOuterPosition
.x
) / 2, y
: (outerPosition
.y
+ nextOuterPosition
.y
) / 2 };
2084 ctx
.moveTo(this.xCenter
, this.yCenter
);
2085 ctx
.lineTo(previousOuterHalfway
.x
, previousOuterHalfway
.y
);
2086 ctx
.lineTo(outerPosition
.x
, outerPosition
.y
);
2087 ctx
.lineTo(nextOuterHalfway
.x
, nextOuterHalfway
.y
);
2088 ctx
.fillStyle
= this.backgroundColors
[i
];
2092 // Extra 3px out for some label spacing
2093 var pointLabelPosition
= this.getPointPosition(i
, this.calculateCenterOffset(this.max
) + 5);
2094 ctx
.font
= fontString(this.pointLabelFontSize
,this.pointLabelFontStyle
,this.pointLabelFontFamily
);
2095 ctx
.fillStyle
= this.pointLabelFontColor
;
2097 var labelsCount
= this.labels
.length
,
2098 halfLabelsCount
= this.labels
.length
/2,
2099 quarterLabelsCount
= halfLabelsCount
/2,
2100 upperHalf
= (i
< quarterLabelsCount
|| i
> labelsCount
- quarterLabelsCount
),
2101 exactQuarter
= (i
=== quarterLabelsCount
|| i
=== labelsCount
- quarterLabelsCount
);
2103 ctx
.textAlign
= 'center';
2104 } else if(i
=== halfLabelsCount
){
2105 ctx
.textAlign
= 'center';
2106 } else if (i
< halfLabelsCount
){
2107 ctx
.textAlign
= 'left';
2109 ctx
.textAlign
= 'right';
2112 // Set the correct text baseline based on outer positioning
2114 ctx
.textBaseline
= 'middle';
2115 } else if (upperHalf
){
2116 ctx
.textBaseline
= 'bottom';
2118 ctx
.textBaseline
= 'top';
2121 ctx
.fillText(this.labels
[i
], pointLabelPosition
.x
, pointLabelPosition
.y
);
2128 Chart
.animationService
= {
2132 addAnimation: function(chartInstance
, animationObject
) {
2133 for (var index
= 0; index
< this.animations
.length
; ++ index
){
2134 if (this.animations
[index
].chartInstance
=== chartInstance
){
2135 // replacing an in progress animation
2136 this.animations
[index
].animationObject
= animationObject
;
2141 this.animations
.push({
2142 chartInstance
: chartInstance
,
2143 animationObject
: animationObject
2146 // If there are no animations queued, manually kickstart a digest, for lack of a better word
2147 if (this.animations
.length
== 1) {
2148 helpers
.requestAnimFrame
.call(window
, this.digestWrapper
);
2151 // Cancel the animation for a given chart instance
2152 cancelAnimation: function(chartInstance
) {
2153 var index
= helpers
.findNextWhere(this.animations
, function(animationWrapper
) {
2154 return animationWrapper
.chartInstance
=== chartInstance
;
2159 this.animations
.splice(index
, 1);
2162 // calls startDigest with the proper context
2163 digestWrapper: function() {
2164 Chart
.animationService
.startDigest
.call(Chart
.animationService
);
2166 startDigest: function() {
2168 var startTime
= Date
.now();
2169 var framesToDrop
= 0;
2171 if(this.dropFrames
> 1){
2172 framesToDrop
= Math
.floor(this.dropFrames
);
2173 this.dropFrames
-= framesToDrop
;
2176 for (var i
= 0; i
< this.animations
.length
; i
++) {
2178 if (this.animations
[i
].animationObject
.currentStep
=== null){
2179 this.animations
[i
].animationObject
.currentStep
= 0;
2182 this.animations
[i
].animationObject
.currentStep
+= 1 + framesToDrop
;
2183 if(this.animations
[i
].animationObject
.currentStep
> this.animations
[i
].animationObject
.numSteps
){
2184 this.animations
[i
].animationObject
.currentStep
= this.animations
[i
].animationObject
.numSteps
;
2187 this.animations
[i
].animationObject
.render(this.animations
[i
].chartInstance
, this.animations
[i
].animationObject
);
2189 // Check if executed the last frame.
2190 if (this.animations
[i
].animationObject
.currentStep
== this.animations
[i
].animationObject
.numSteps
){
2191 // Call onAnimationComplete
2192 this.animations
[i
].animationObject
.onAnimationComplete
.call(this.animations
[i
].chartInstance
);
2193 // Remove the animation.
2194 this.animations
.splice(i
, 1);
2195 // Keep the index in place to offset the splice
2200 var endTime
= Date
.now();
2201 var delay
= endTime
- startTime
- this.frameDuration
;
2202 var frameDelay
= delay
/ this.frameDuration
;
2205 this.dropFrames
+= frameDelay
;
2208 // Do we have more stuff to animate?
2209 if (this.animations
.length
> 0){
2210 helpers
.requestAnimFrame
.call(window
, this.digestWrapper
);
2215 // Attach global event to resize each chart instance when the browser resizes
2216 helpers
.addEvent(window
, "resize", (function(){
2217 // Basic debounce of resize function so it doesn't hurt performance when resizing browser.
2220 clearTimeout(timeout
);
2221 timeout
= setTimeout(function(){
2222 each(Chart
.instances
,function(instance
){
2223 // If the responsive flag is set in the chart instance config
2224 // Cascade the resize event down to the chart.
2225 if (instance
.options
.responsive
){
2226 instance
.resize(instance
.render
, true);
2235 define('Chart', [], function(){
2238 } else if (typeof module
=== 'object' && module
.exports
) {
2239 module
.exports
= Chart
;
2244 Chart
.noConflict = function(){
2245 root
.Chart
= previous
;
2256 helpers
= Chart
.helpers
;
2259 var defaultConfig
= {
2260 //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
2261 scaleBeginAtZero
: true,
2263 //Boolean - Whether grid lines are shown across the chart
2264 scaleShowGridLines
: true,
2266 //String - Colour of the grid lines
2267 scaleGridLineColor
: "rgba(0,0,0,.05)",
2269 //Number - Width of the grid lines
2270 scaleGridLineWidth
: 1,
2272 //Boolean - Whether to show horizontal lines (except X axis)
2273 scaleShowHorizontalLines
: true,
2275 //Boolean - Whether to show vertical lines (except Y axis)
2276 scaleShowVerticalLines
: true,
2278 //Boolean - If there is a stroke on each bar
2279 barShowStroke
: true,
2281 //Number - Pixel width of the bar stroke
2284 //Number - Spacing between each of the X value sets
2285 barValueSpacing
: 5,
2287 //Number - Spacing between data sets within X values
2288 barDatasetSpacing
: 1,
2290 //String - A legend template
2291 legendTemplate
: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span class=\"<%=name.toLowerCase()%>-legend-icon\" style=\"background-color:<%=datasets[i].fillColor%>\"></span><span class=\"<%=name.toLowerCase()%>-legend-text\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
2298 defaults
: defaultConfig
,
2299 initialize: function(data
){
2301 //Expose options as a scope variable here so we can access it in the ScaleClass
2302 var options
= this.options
;
2304 this.ScaleClass
= Chart
.Scale
.extend({
2305 offsetGridLines
: true,
2306 calculateBarX : function(datasetCount
, datasetIndex
, barIndex
){
2307 //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
2308 var xWidth
= this.calculateBaseWidth(),
2309 xAbsolute
= this.calculateX(barIndex
) - (xWidth
/2),
2310 barWidth
= this.calculateBarWidth(datasetCount
);
2312 return xAbsolute
+ (barWidth
* datasetIndex
) + (datasetIndex
* options
.barDatasetSpacing
) + barWidth
/2;
2314 calculateBaseWidth : function(){
2315 return (this.calculateX(1) - this.calculateX(0)) - (2*options
.barValueSpacing
);
2317 calculateBarWidth : function(datasetCount
){
2318 //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
2319 var baseWidth
= this.calculateBaseWidth() - ((datasetCount
- 1) * options
.barDatasetSpacing
);
2321 return (baseWidth
/ datasetCount
);
2327 //Set up tooltip events on the chart
2328 if (this.options
.showTooltips
){
2329 helpers
.bindEvents(this, this.options
.tooltipEvents
, function(evt
){
2330 var activeBars
= (evt
.type
!== 'mouseout') ? this.getBarsAtEvent(evt
) : [];
2332 this.eachBars(function(bar
){
2333 bar
.restore(['fillColor', 'strokeColor']);
2335 helpers
.each(activeBars
, function(activeBar
){
2337 activeBar
.fillColor
= activeBar
.highlightFill
;
2338 activeBar
.strokeColor
= activeBar
.highlightStroke
;
2341 this.showTooltip(activeBars
);
2345 //Declare the extension of the default point, to cater for the options passed in to the constructor
2346 this.BarClass
= Chart
.Rectangle
.extend({
2347 strokeWidth
: this.options
.barStrokeWidth
,
2348 showStroke
: this.options
.barShowStroke
,
2349 ctx
: this.chart
.ctx
2352 //Iterate through each of the datasets, and build this into a property of the chart
2353 helpers
.each(data
.datasets
,function(dataset
,datasetIndex
){
2355 var datasetObject
= {
2356 label
: dataset
.label
|| null,
2357 fillColor
: dataset
.fillColor
,
2358 strokeColor
: dataset
.strokeColor
,
2362 this.datasets
.push(datasetObject
);
2364 helpers
.each(dataset
.data
,function(dataPoint
,index
){
2365 //Add a new point for each piece of data, passing any required data to draw.
2366 datasetObject
.bars
.push(new this.BarClass({
2368 label
: data
.labels
[index
],
2369 datasetLabel
: dataset
.label
,
2370 strokeColor
: (typeof dataset
.strokeColor
== 'object') ? dataset
.strokeColor
[index
] : dataset
.strokeColor
,
2371 fillColor
: (typeof dataset
.fillColor
== 'object') ? dataset
.fillColor
[index
] : dataset
.fillColor
,
2372 highlightFill
: (dataset
.highlightFill
) ? (typeof dataset
.highlightFill
== 'object') ? dataset
.highlightFill
[index
] : dataset
.highlightFill
: (typeof dataset
.fillColor
== 'object') ? dataset
.fillColor
[index
] : dataset
.fillColor
,
2373 highlightStroke
: (dataset
.highlightStroke
) ? (typeof dataset
.highlightStroke
== 'object') ? dataset
.highlightStroke
[index
] : dataset
.highlightStroke
: (typeof dataset
.strokeColor
== 'object') ? dataset
.strokeColor
[index
] : dataset
.strokeColor
2379 this.buildScale(data
.labels
);
2381 this.BarClass
.prototype.base
= this.scale
.endPoint
;
2383 this.eachBars(function(bar
, index
, datasetIndex
){
2384 helpers
.extend(bar
, {
2385 width
: this.scale
.calculateBarWidth(this.datasets
.length
),
2386 x
: this.scale
.calculateBarX(this.datasets
.length
, datasetIndex
, index
),
2387 y
: this.scale
.endPoint
2394 update : function(){
2395 this.scale
.update();
2396 // Reset any highlight colours before updating.
2397 helpers
.each(this.activeElements
, function(activeElement
){
2398 activeElement
.restore(['fillColor', 'strokeColor']);
2401 this.eachBars(function(bar
){
2406 eachBars : function(callback
){
2407 helpers
.each(this.datasets
,function(dataset
, datasetIndex
){
2408 helpers
.each(dataset
.bars
, callback
, this, datasetIndex
);
2411 getBarsAtEvent : function(e
){
2413 eventPosition
= helpers
.getRelativePosition(e
),
2414 datasetIterator = function(dataset
){
2415 barsArray
.push(dataset
.bars
[barIndex
]);
2419 for (var datasetIndex
= 0; datasetIndex
< this.datasets
.length
; datasetIndex
++) {
2420 for (barIndex
= 0; barIndex
< this.datasets
[datasetIndex
].bars
.length
; barIndex
++) {
2421 if (this.datasets
[datasetIndex
].bars
[barIndex
].inRange(eventPosition
.x
,eventPosition
.y
)){
2422 helpers
.each(this.datasets
, datasetIterator
);
2430 buildScale : function(labels
){
2433 var dataTotal = function(){
2435 self
.eachBars(function(bar
){
2436 values
.push(bar
.value
);
2441 var scaleOptions
= {
2442 templateString
: this.options
.scaleLabel
,
2443 height
: this.chart
.height
,
2444 width
: this.chart
.width
,
2445 ctx
: this.chart
.ctx
,
2446 textColor
: this.options
.scaleFontColor
,
2447 fontSize
: this.options
.scaleFontSize
,
2448 fontStyle
: this.options
.scaleFontStyle
,
2449 fontFamily
: this.options
.scaleFontFamily
,
2450 valuesCount
: labels
.length
,
2451 beginAtZero
: this.options
.scaleBeginAtZero
,
2452 integersOnly
: this.options
.scaleIntegersOnly
,
2453 calculateYRange: function(currentHeight
){
2454 var updatedRanges
= helpers
.calculateScaleRange(
2461 helpers
.extend(this, updatedRanges
);
2464 font
: helpers
.fontString(this.options
.scaleFontSize
, this.options
.scaleFontStyle
, this.options
.scaleFontFamily
),
2465 lineWidth
: this.options
.scaleLineWidth
,
2466 lineColor
: this.options
.scaleLineColor
,
2467 showHorizontalLines
: this.options
.scaleShowHorizontalLines
,
2468 showVerticalLines
: this.options
.scaleShowVerticalLines
,
2469 gridLineWidth
: (this.options
.scaleShowGridLines
) ? this.options
.scaleGridLineWidth
: 0,
2470 gridLineColor
: (this.options
.scaleShowGridLines
) ? this.options
.scaleGridLineColor
: "rgba(0,0,0,0)",
2471 padding
: (this.options
.showScale
) ? 0 : (this.options
.barShowStroke
) ? this.options
.barStrokeWidth
: 0,
2472 showLabels
: this.options
.scaleShowLabels
,
2473 display
: this.options
.showScale
2476 if (this.options
.scaleOverride
){
2477 helpers
.extend(scaleOptions
, {
2478 calculateYRange
: helpers
.noop
,
2479 steps
: this.options
.scaleSteps
,
2480 stepValue
: this.options
.scaleStepWidth
,
2481 min
: this.options
.scaleStartValue
,
2482 max
: this.options
.scaleStartValue
+ (this.options
.scaleSteps
* this.options
.scaleStepWidth
)
2486 this.scale
= new this.ScaleClass(scaleOptions
);
2488 addData : function(valuesArray
,label
){
2489 //Map the values array for each of the datasets
2490 helpers
.each(valuesArray
,function(value
,datasetIndex
){
2491 //Add a new point for each piece of data, passing any required data to draw.
2492 this.datasets
[datasetIndex
].bars
.push(new this.BarClass({
2495 datasetLabel
: this.datasets
[datasetIndex
].label
,
2496 x
: this.scale
.calculateBarX(this.datasets
.length
, datasetIndex
, this.scale
.valuesCount
+1),
2497 y
: this.scale
.endPoint
,
2498 width
: this.scale
.calculateBarWidth(this.datasets
.length
),
2499 base
: this.scale
.endPoint
,
2500 strokeColor
: this.datasets
[datasetIndex
].strokeColor
,
2501 fillColor
: this.datasets
[datasetIndex
].fillColor
2505 this.scale
.addXLabel(label
);
2506 //Then re-render the chart.
2509 removeData : function(){
2510 this.scale
.removeXLabel();
2511 //Then re-render the chart.
2512 helpers
.each(this.datasets
,function(dataset
){
2513 dataset
.bars
.shift();
2517 reflow : function(){
2518 helpers
.extend(this.BarClass
.prototype,{
2519 y
: this.scale
.endPoint
,
2520 base
: this.scale
.endPoint
2522 var newScaleProps
= helpers
.extend({
2523 height
: this.chart
.height
,
2524 width
: this.chart
.width
2526 this.scale
.update(newScaleProps
);
2528 draw : function(ease
){
2529 var easingDecimal
= ease
|| 1;
2532 var ctx
= this.chart
.ctx
;
2534 this.scale
.draw(easingDecimal
);
2536 //Draw all the bars for each dataset
2537 helpers
.each(this.datasets
,function(dataset
,datasetIndex
){
2538 helpers
.each(dataset
.bars
,function(bar
,index
){
2539 if (bar
.hasValue()){
2540 bar
.base
= this.scale
.endPoint
;
2541 //Transition then draw
2543 x
: this.scale
.calculateBarX(this.datasets
.length
, datasetIndex
, index
),
2544 y
: this.scale
.calculateY(bar
.value
),
2545 width
: this.scale
.calculateBarWidth(this.datasets
.length
)
2546 }, easingDecimal
).draw();
2562 //Cache a local reference to Chart.helpers
2563 helpers
= Chart
.helpers
;
2565 var defaultConfig
= {
2566 //Boolean - Whether we should show a stroke on each segment
2567 segmentShowStroke
: true,
2569 //String - The colour of each segment stroke
2570 segmentStrokeColor
: "#fff",
2572 //Number - The width of each segment stroke
2573 segmentStrokeWidth
: 2,
2575 //The percentage of the chart that we cut out of the middle.
2576 percentageInnerCutout
: 50,
2578 //Number - Amount of animation steps
2579 animationSteps
: 100,
2581 //String - Animation easing effect
2582 animationEasing
: "easeOutBounce",
2584 //Boolean - Whether we animate the rotation of the Doughnut
2585 animateRotate
: true,
2587 //Boolean - Whether we animate scaling the Doughnut from the centre
2588 animateScale
: false,
2590 //String - A legend template
2591 legendTemplate
: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span class=\"<%=name.toLowerCase()%>-legend-icon\" style=\"background-color:<%=segments[i].fillColor%>\"></span><span class=\"<%=name.toLowerCase()%>-legend-text\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>"
2596 //Passing in a name registers this chart in the Chart namespace
2598 //Providing a defaults will also register the defaults in the chart namespace
2599 defaults
: defaultConfig
,
2600 //Initialize is fired when the chart is initialized - Data is passed in as a parameter
2601 //Config is automatically merged by the core of Chart.js, and is available at this.options
2602 initialize: function(data
){
2604 //Declare segments as a static property to prevent inheriting across the Chart type prototype
2606 this.outerRadius
= (helpers
.min([this.chart
.width
,this.chart
.height
]) - this.options
.segmentStrokeWidth
/2)/2;
2608 this.SegmentArc
= Chart
.Arc
.extend({
2609 ctx
: this.chart
.ctx
,
2610 x
: this.chart
.width
/2,
2611 y
: this.chart
.height
/2
2614 //Set up tooltip events on the chart
2615 if (this.options
.showTooltips
){
2616 helpers
.bindEvents(this, this.options
.tooltipEvents
, function(evt
){
2617 var activeSegments
= (evt
.type
!== 'mouseout') ? this.getSegmentsAtEvent(evt
) : [];
2619 helpers
.each(this.segments
,function(segment
){
2620 segment
.restore(["fillColor"]);
2622 helpers
.each(activeSegments
,function(activeSegment
){
2623 activeSegment
.fillColor
= activeSegment
.highlightColor
;
2625 this.showTooltip(activeSegments
);
2628 this.calculateTotal(data
);
2630 helpers
.each(data
,function(datapoint
, index
){
2631 if (!datapoint
.color
) {
2632 datapoint
.color
= 'hsl(' + (360 * index
/ data
.length
) + ', 100%, 50%)';
2634 this.addData(datapoint
, index
, true);
2639 getSegmentsAtEvent : function(e
){
2640 var segmentsArray
= [];
2642 var location
= helpers
.getRelativePosition(e
);
2644 helpers
.each(this.segments
,function(segment
){
2645 if (segment
.inRange(location
.x
,location
.y
)) segmentsArray
.push(segment
);
2647 return segmentsArray
;
2649 addData : function(segment
, atIndex
, silent
){
2650 var index
= atIndex
!== undefined ? atIndex
: this.segments
.length
;
2651 if ( typeof(segment
.color
) === "undefined" ) {
2652 segment
.color
= Chart
.defaults
.global
.segmentColorDefault
[index
% Chart
.defaults
.global
.segmentColorDefault
.length
];
2653 segment
.highlight
= Chart
.defaults
.global
.segmentHighlightColorDefaults
[index
% Chart
.defaults
.global
.segmentHighlightColorDefaults
.length
];
2655 this.segments
.splice(index
, 0, new this.SegmentArc({
2656 value
: segment
.value
,
2657 outerRadius
: (this.options
.animateScale
) ? 0 : this.outerRadius
,
2658 innerRadius
: (this.options
.animateScale
) ? 0 : (this.outerRadius
/100) * this.options
.percentageInnerCutout
,
2659 fillColor
: segment
.color
,
2660 highlightColor
: segment
.highlight
|| segment
.color
,
2661 showStroke
: this.options
.segmentShowStroke
,
2662 strokeWidth
: this.options
.segmentStrokeWidth
,
2663 strokeColor
: this.options
.segmentStrokeColor
,
2664 startAngle
: Math
.PI
* 1.5,
2665 circumference
: (this.options
.animateRotate
) ? 0 : this.calculateCircumference(segment
.value
),
2666 label
: segment
.label
2673 calculateCircumference : function(value
) {
2674 if ( this.total
> 0 ) {
2675 return (Math
.PI
*2)*(value
/ this.total
);
2680 calculateTotal : function(data
){
2682 helpers
.each(data
,function(segment
){
2683 this.total
+= Math
.abs(segment
.value
);
2686 update : function(){
2687 this.calculateTotal(this.segments
);
2689 // Reset any highlight colours before updating.
2690 helpers
.each(this.activeElements
, function(activeElement
){
2691 activeElement
.restore(['fillColor']);
2694 helpers
.each(this.segments
,function(segment
){
2700 removeData: function(atIndex
){
2701 var indexToDelete
= (helpers
.isNumber(atIndex
)) ? atIndex
: this.segments
.length
-1;
2702 this.segments
.splice(indexToDelete
, 1);
2707 reflow : function(){
2708 helpers
.extend(this.SegmentArc
.prototype,{
2709 x
: this.chart
.width
/2,
2710 y
: this.chart
.height
/2
2712 this.outerRadius
= (helpers
.min([this.chart
.width
,this.chart
.height
]) - this.options
.segmentStrokeWidth
/2)/2;
2713 helpers
.each(this.segments
, function(segment
){
2715 outerRadius
: this.outerRadius
,
2716 innerRadius
: (this.outerRadius
/100) * this.options
.percentageInnerCutout
2720 draw : function(easeDecimal
){
2721 var animDecimal
= (easeDecimal
) ? easeDecimal
: 1;
2723 helpers
.each(this.segments
,function(segment
,index
){
2724 segment
.transition({
2725 circumference
: this.calculateCircumference(segment
.value
),
2726 outerRadius
: this.outerRadius
,
2727 innerRadius
: (this.outerRadius
/100) * this.options
.percentageInnerCutout
2730 segment
.endAngle
= segment
.startAngle
+ segment
.circumference
;
2734 segment
.startAngle
= Math
.PI
* 1.5;
2736 //Check to see if it's the last segment, if not get the next and update the start angle
2737 if (index
< this.segments
.length
-1){
2738 this.segments
[index
+1].startAngle
= segment
.endAngle
;
2745 Chart
.types
.Doughnut
.extend({
2747 defaults
: helpers
.merge(defaultConfig
,{percentageInnerCutout
: 0})
2757 helpers
= Chart
.helpers
;
2759 var defaultConfig
= {
2761 ///Boolean - Whether grid lines are shown across the chart
2762 scaleShowGridLines
: true,
2764 //String - Colour of the grid lines
2765 scaleGridLineColor
: "rgba(0,0,0,.05)",
2767 //Number - Width of the grid lines
2768 scaleGridLineWidth
: 1,
2770 //Boolean - Whether to show horizontal lines (except X axis)
2771 scaleShowHorizontalLines
: true,
2773 //Boolean - Whether to show vertical lines (except Y axis)
2774 scaleShowVerticalLines
: true,
2776 //Boolean - Whether the line is curved between points
2779 //Number - Tension of the bezier curve between points
2780 bezierCurveTension
: 0.4,
2782 //Boolean - Whether to show a dot for each point
2785 //Number - Radius of each point dot in pixels
2788 //Number - Pixel width of point dot stroke
2789 pointDotStrokeWidth
: 1,
2791 //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
2792 pointHitDetectionRadius
: 20,
2794 //Boolean - Whether to show a stroke for datasets
2795 datasetStroke
: true,
2797 //Number - Pixel width of dataset stroke
2798 datasetStrokeWidth
: 2,
2800 //Boolean - Whether to fill the dataset with a colour
2803 //String - A legend template
2804 legendTemplate
: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span class=\"<%=name.toLowerCase()%>-legend-icon\" style=\"background-color:<%=datasets[i].strokeColor%>\"></span><span class=\"<%=name.toLowerCase()%>-legend-text\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>",
2806 //Boolean - Whether to horizontally center the label and point dot inside the grid
2807 offsetGridLines
: false
2814 defaults
: defaultConfig
,
2815 initialize: function(data
){
2816 //Declare the extension of the default point, to cater for the options passed in to the constructor
2817 this.PointClass
= Chart
.Point
.extend({
2818 offsetGridLines
: this.options
.offsetGridLines
,
2819 strokeWidth
: this.options
.pointDotStrokeWidth
,
2820 radius
: this.options
.pointDotRadius
,
2821 display
: this.options
.pointDot
,
2822 hitDetectionRadius
: this.options
.pointHitDetectionRadius
,
2823 ctx
: this.chart
.ctx
,
2824 inRange : function(mouseX
){
2825 return (Math
.pow(mouseX
-this.x
, 2) < Math
.pow(this.radius
+ this.hitDetectionRadius
,2));
2831 //Set up tooltip events on the chart
2832 if (this.options
.showTooltips
){
2833 helpers
.bindEvents(this, this.options
.tooltipEvents
, function(evt
){
2834 var activePoints
= (evt
.type
!== 'mouseout') ? this.getPointsAtEvent(evt
) : [];
2835 this.eachPoints(function(point
){
2836 point
.restore(['fillColor', 'strokeColor']);
2838 helpers
.each(activePoints
, function(activePoint
){
2839 activePoint
.fillColor
= activePoint
.highlightFill
;
2840 activePoint
.strokeColor
= activePoint
.highlightStroke
;
2842 this.showTooltip(activePoints
);
2846 //Iterate through each of the datasets, and build this into a property of the chart
2847 helpers
.each(data
.datasets
,function(dataset
){
2849 var datasetObject
= {
2850 label
: dataset
.label
|| null,
2851 fillColor
: dataset
.fillColor
,
2852 strokeColor
: dataset
.strokeColor
,
2853 pointColor
: dataset
.pointColor
,
2854 pointStrokeColor
: dataset
.pointStrokeColor
,
2858 this.datasets
.push(datasetObject
);
2861 helpers
.each(dataset
.data
,function(dataPoint
,index
){
2862 //Add a new point for each piece of data, passing any required data to draw.
2863 datasetObject
.points
.push(new this.PointClass({
2865 label
: data
.labels
[index
],
2866 datasetLabel
: dataset
.label
,
2867 strokeColor
: dataset
.pointStrokeColor
,
2868 fillColor
: dataset
.pointColor
,
2869 highlightFill
: dataset
.pointHighlightFill
|| dataset
.pointColor
,
2870 highlightStroke
: dataset
.pointHighlightStroke
|| dataset
.pointStrokeColor
2874 this.buildScale(data
.labels
);
2877 this.eachPoints(function(point
, index
){
2878 helpers
.extend(point
, {
2879 x
: this.scale
.calculateX(index
),
2880 y
: this.scale
.endPoint
2890 update : function(){
2891 this.scale
.update();
2892 // Reset any highlight colours before updating.
2893 helpers
.each(this.activeElements
, function(activeElement
){
2894 activeElement
.restore(['fillColor', 'strokeColor']);
2896 this.eachPoints(function(point
){
2901 eachPoints : function(callback
){
2902 helpers
.each(this.datasets
,function(dataset
){
2903 helpers
.each(dataset
.points
,callback
,this);
2906 getPointsAtEvent : function(e
){
2907 var pointsArray
= [],
2908 eventPosition
= helpers
.getRelativePosition(e
);
2909 helpers
.each(this.datasets
,function(dataset
){
2910 helpers
.each(dataset
.points
,function(point
){
2911 if (point
.inRange(eventPosition
.x
,eventPosition
.y
)) pointsArray
.push(point
);
2916 buildScale : function(labels
){
2919 var dataTotal = function(){
2921 self
.eachPoints(function(point
){
2922 values
.push(point
.value
);
2928 var scaleOptions
= {
2929 templateString
: this.options
.scaleLabel
,
2930 height
: this.chart
.height
,
2931 width
: this.chart
.width
,
2932 ctx
: this.chart
.ctx
,
2933 textColor
: this.options
.scaleFontColor
,
2934 offsetGridLines
: this.options
.offsetGridLines
,
2935 fontSize
: this.options
.scaleFontSize
,
2936 fontStyle
: this.options
.scaleFontStyle
,
2937 fontFamily
: this.options
.scaleFontFamily
,
2938 valuesCount
: labels
.length
,
2939 beginAtZero
: this.options
.scaleBeginAtZero
,
2940 integersOnly
: this.options
.scaleIntegersOnly
,
2941 calculateYRange : function(currentHeight
){
2942 var updatedRanges
= helpers
.calculateScaleRange(
2949 helpers
.extend(this, updatedRanges
);
2952 font
: helpers
.fontString(this.options
.scaleFontSize
, this.options
.scaleFontStyle
, this.options
.scaleFontFamily
),
2953 lineWidth
: this.options
.scaleLineWidth
,
2954 lineColor
: this.options
.scaleLineColor
,
2955 showHorizontalLines
: this.options
.scaleShowHorizontalLines
,
2956 showVerticalLines
: this.options
.scaleShowVerticalLines
,
2957 gridLineWidth
: (this.options
.scaleShowGridLines
) ? this.options
.scaleGridLineWidth
: 0,
2958 gridLineColor
: (this.options
.scaleShowGridLines
) ? this.options
.scaleGridLineColor
: "rgba(0,0,0,0)",
2959 padding
: (this.options
.showScale
) ? 0 : this.options
.pointDotRadius
+ this.options
.pointDotStrokeWidth
,
2960 showLabels
: this.options
.scaleShowLabels
,
2961 display
: this.options
.showScale
2964 if (this.options
.scaleOverride
){
2965 helpers
.extend(scaleOptions
, {
2966 calculateYRange
: helpers
.noop
,
2967 steps
: this.options
.scaleSteps
,
2968 stepValue
: this.options
.scaleStepWidth
,
2969 min
: this.options
.scaleStartValue
,
2970 max
: this.options
.scaleStartValue
+ (this.options
.scaleSteps
* this.options
.scaleStepWidth
)
2975 this.scale
= new Chart
.Scale(scaleOptions
);
2977 addData : function(valuesArray
,label
){
2978 //Map the values array for each of the datasets
2980 helpers
.each(valuesArray
,function(value
,datasetIndex
){
2981 //Add a new point for each piece of data, passing any required data to draw.
2982 this.datasets
[datasetIndex
].points
.push(new this.PointClass({
2985 datasetLabel
: this.datasets
[datasetIndex
].label
,
2986 x
: this.scale
.calculateX(this.scale
.valuesCount
+1),
2987 y
: this.scale
.endPoint
,
2988 strokeColor
: this.datasets
[datasetIndex
].pointStrokeColor
,
2989 fillColor
: this.datasets
[datasetIndex
].pointColor
2993 this.scale
.addXLabel(label
);
2994 //Then re-render the chart.
2997 removeData : function(){
2998 this.scale
.removeXLabel();
2999 //Then re-render the chart.
3000 helpers
.each(this.datasets
,function(dataset
){
3001 dataset
.points
.shift();
3005 reflow : function(){
3006 var newScaleProps
= helpers
.extend({
3007 height
: this.chart
.height
,
3008 width
: this.chart
.width
3010 this.scale
.update(newScaleProps
);
3012 draw : function(ease
){
3013 var easingDecimal
= ease
|| 1;
3016 var ctx
= this.chart
.ctx
;
3018 // Some helper methods for getting the next/prev points
3019 var hasValue = function(item
){
3020 return item
.value
!== null;
3022 nextPoint = function(point
, collection
, index
){
3023 return helpers
.findNextWhere(collection
, hasValue
, index
) || point
;
3025 previousPoint = function(point
, collection
, index
){
3026 return helpers
.findPreviousWhere(collection
, hasValue
, index
) || point
;
3029 if (!this.scale
) return;
3030 this.scale
.draw(easingDecimal
);
3033 helpers
.each(this.datasets
,function(dataset
){
3034 var pointsWithValues
= helpers
.where(dataset
.points
, hasValue
);
3036 //Transition each point first so that the line and point drawing isn't out of sync
3037 //We can use this extra loop to calculate the control points of this dataset also in this loop
3039 helpers
.each(dataset
.points
, function(point
, index
){
3040 if (point
.hasValue()){
3042 y
: this.scale
.calculateY(point
.value
),
3043 x
: this.scale
.calculateX(index
)
3049 // Control points need to be calculated in a separate loop, because we need to know the current x/y of the point
3050 // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
3051 if (this.options
.bezierCurve
){
3052 helpers
.each(pointsWithValues
, function(point
, index
){
3053 var tension
= (index
> 0 && index
< pointsWithValues
.length
- 1) ? this.options
.bezierCurveTension
: 0;
3054 point
.controlPoints
= helpers
.splineCurve(
3055 previousPoint(point
, pointsWithValues
, index
),
3057 nextPoint(point
, pointsWithValues
, index
),
3061 // Prevent the bezier going outside of the bounds of the graph
3063 // Cap puter bezier handles to the upper/lower scale bounds
3064 if (point
.controlPoints
.outer
.y
> this.scale
.endPoint
){
3065 point
.controlPoints
.outer
.y
= this.scale
.endPoint
;
3067 else if (point
.controlPoints
.outer
.y
< this.scale
.startPoint
){
3068 point
.controlPoints
.outer
.y
= this.scale
.startPoint
;
3071 // Cap inner bezier handles to the upper/lower scale bounds
3072 if (point
.controlPoints
.inner
.y
> this.scale
.endPoint
){
3073 point
.controlPoints
.inner
.y
= this.scale
.endPoint
;
3075 else if (point
.controlPoints
.inner
.y
< this.scale
.startPoint
){
3076 point
.controlPoints
.inner
.y
= this.scale
.startPoint
;
3082 //Draw the line between all the points
3083 ctx
.lineWidth
= this.options
.datasetStrokeWidth
;
3084 ctx
.strokeStyle
= dataset
.strokeColor
;
3087 helpers
.each(pointsWithValues
, function(point
, index
){
3089 ctx
.moveTo(point
.x
, point
.y
);
3092 if(this.options
.bezierCurve
){
3093 var previous
= previousPoint(point
, pointsWithValues
, index
);
3096 previous
.controlPoints
.outer
.x
,
3097 previous
.controlPoints
.outer
.y
,
3098 point
.controlPoints
.inner
.x
,
3099 point
.controlPoints
.inner
.y
,
3105 ctx
.lineTo(point
.x
,point
.y
);
3110 if (this.options
.datasetStroke
) {
3114 if (this.options
.datasetFill
&& pointsWithValues
.length
> 0){
3115 //Round off the line by going to the base of the chart, back to the start, then fill.
3116 ctx
.lineTo(pointsWithValues
[pointsWithValues
.length
- 1].x
, this.scale
.endPoint
);
3117 ctx
.lineTo(pointsWithValues
[0].x
, this.scale
.endPoint
);
3118 ctx
.fillStyle
= dataset
.fillColor
;
3123 //Now draw the points over the line
3124 //A little inefficient double looping, but better than the line
3125 //lagging behind the point positions
3126 helpers
.each(pointsWithValues
,function(point
){
3141 //Cache a local reference to Chart.helpers
3142 helpers
= Chart
.helpers
;
3144 var defaultConfig
= {
3145 //Boolean - Show a backdrop to the scale label
3146 scaleShowLabelBackdrop
: true,
3148 //String - The colour of the label backdrop
3149 scaleBackdropColor
: "rgba(255,255,255,0.75)",
3151 // Boolean - Whether the scale should begin at zero
3152 scaleBeginAtZero
: true,
3154 //Number - The backdrop padding above & below the label in pixels
3155 scaleBackdropPaddingY
: 2,
3157 //Number - The backdrop padding to the side of the label in pixels
3158 scaleBackdropPaddingX
: 2,
3160 //Boolean - Show line for each value in the scale
3161 scaleShowLine
: true,
3163 //Boolean - Stroke a line around each segment in the chart
3164 segmentShowStroke
: true,
3166 //String - The colour of the stroke on each segment.
3167 segmentStrokeColor
: "#fff",
3169 //Number - The width of the stroke value in pixels
3170 segmentStrokeWidth
: 2,
3172 //Number - Amount of animation steps
3173 animationSteps
: 100,
3175 //String - Animation easing effect.
3176 animationEasing
: "easeOutBounce",
3178 //Boolean - Whether to animate the rotation of the chart
3179 animateRotate
: true,
3181 //Boolean - Whether to animate scaling the chart from the centre
3182 animateScale
: false,
3184 //String - A legend template
3185 legendTemplate
: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span class=\"<%=name.toLowerCase()%>-legend-icon\" style=\"background-color:<%=segments[i].fillColor%>\"></span><span class=\"<%=name.toLowerCase()%>-legend-text\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>"
3190 //Passing in a name registers this chart in the Chart namespace
3192 //Providing a defaults will also register the defaults in the chart namespace
3193 defaults
: defaultConfig
,
3194 //Initialize is fired when the chart is initialized - Data is passed in as a parameter
3195 //Config is automatically merged by the core of Chart.js, and is available at this.options
3196 initialize: function(data
){
3198 //Declare segment class as a chart instance specific class, so it can share props for this instance
3199 this.SegmentArc
= Chart
.Arc
.extend({
3200 showStroke
: this.options
.segmentShowStroke
,
3201 strokeWidth
: this.options
.segmentStrokeWidth
,
3202 strokeColor
: this.options
.segmentStrokeColor
,
3203 ctx
: this.chart
.ctx
,
3205 x
: this.chart
.width
/2,
3206 y
: this.chart
.height
/2
3208 this.scale
= new Chart
.RadialScale({
3209 display
: this.options
.showScale
,
3210 fontStyle
: this.options
.scaleFontStyle
,
3211 fontSize
: this.options
.scaleFontSize
,
3212 fontFamily
: this.options
.scaleFontFamily
,
3213 fontColor
: this.options
.scaleFontColor
,
3214 showLabels
: this.options
.scaleShowLabels
,
3215 showLabelBackdrop
: this.options
.scaleShowLabelBackdrop
,
3216 backdropColor
: this.options
.scaleBackdropColor
,
3217 backdropPaddingY
: this.options
.scaleBackdropPaddingY
,
3218 backdropPaddingX
: this.options
.scaleBackdropPaddingX
,
3219 lineWidth
: (this.options
.scaleShowLine
) ? this.options
.scaleLineWidth
: 0,
3220 lineColor
: this.options
.scaleLineColor
,
3222 width
: this.chart
.width
,
3223 height
: this.chart
.height
,
3224 xCenter
: this.chart
.width
/2,
3225 yCenter
: this.chart
.height
/2,
3226 ctx
: this.chart
.ctx
,
3227 templateString
: this.options
.scaleLabel
,
3228 valuesCount
: data
.length
3231 this.updateScaleRange(data
);
3233 this.scale
.update();
3235 helpers
.each(data
,function(segment
,index
){
3236 this.addData(segment
,index
,true);
3239 //Set up tooltip events on the chart
3240 if (this.options
.showTooltips
){
3241 helpers
.bindEvents(this, this.options
.tooltipEvents
, function(evt
){
3242 var activeSegments
= (evt
.type
!== 'mouseout') ? this.getSegmentsAtEvent(evt
) : [];
3243 helpers
.each(this.segments
,function(segment
){
3244 segment
.restore(["fillColor"]);
3246 helpers
.each(activeSegments
,function(activeSegment
){
3247 activeSegment
.fillColor
= activeSegment
.highlightColor
;
3249 this.showTooltip(activeSegments
);
3255 getSegmentsAtEvent : function(e
){
3256 var segmentsArray
= [];
3258 var location
= helpers
.getRelativePosition(e
);
3260 helpers
.each(this.segments
,function(segment
){
3261 if (segment
.inRange(location
.x
,location
.y
)) segmentsArray
.push(segment
);
3263 return segmentsArray
;
3265 addData : function(segment
, atIndex
, silent
){
3266 var index
= atIndex
|| this.segments
.length
;
3268 this.segments
.splice(index
, 0, new this.SegmentArc({
3269 fillColor
: segment
.color
,
3270 highlightColor
: segment
.highlight
|| segment
.color
,
3271 label
: segment
.label
,
3272 value
: segment
.value
,
3273 outerRadius
: (this.options
.animateScale
) ? 0 : this.scale
.calculateCenterOffset(segment
.value
),
3274 circumference
: (this.options
.animateRotate
) ? 0 : this.scale
.getCircumference(),
3275 startAngle
: Math
.PI
* 1.5
3282 removeData: function(atIndex
){
3283 var indexToDelete
= (helpers
.isNumber(atIndex
)) ? atIndex
: this.segments
.length
-1;
3284 this.segments
.splice(indexToDelete
, 1);
3288 calculateTotal: function(data
){
3290 helpers
.each(data
,function(segment
){
3291 this.total
+= segment
.value
;
3293 this.scale
.valuesCount
= this.segments
.length
;
3295 updateScaleRange: function(datapoints
){
3296 var valuesArray
= [];
3297 helpers
.each(datapoints
,function(segment
){
3298 valuesArray
.push(segment
.value
);
3301 var scaleSizes
= (this.options
.scaleOverride
) ?
3303 steps
: this.options
.scaleSteps
,
3304 stepValue
: this.options
.scaleStepWidth
,
3305 min
: this.options
.scaleStartValue
,
3306 max
: this.options
.scaleStartValue
+ (this.options
.scaleSteps
* this.options
.scaleStepWidth
)
3308 helpers
.calculateScaleRange(
3310 helpers
.min([this.chart
.width
, this.chart
.height
])/2,
3311 this.options
.scaleFontSize
,
3312 this.options
.scaleBeginAtZero
,
3313 this.options
.scaleIntegersOnly
3320 size
: helpers
.min([this.chart
.width
, this.chart
.height
]),
3321 xCenter
: this.chart
.width
/2,
3322 yCenter
: this.chart
.height
/2
3327 update : function(){
3328 this.calculateTotal(this.segments
);
3330 helpers
.each(this.segments
,function(segment
){
3337 reflow : function(){
3338 helpers
.extend(this.SegmentArc
.prototype,{
3339 x
: this.chart
.width
/2,
3340 y
: this.chart
.height
/2
3342 this.updateScaleRange(this.segments
);
3343 this.scale
.update();
3345 helpers
.extend(this.scale
,{
3346 xCenter
: this.chart
.width
/2,
3347 yCenter
: this.chart
.height
/2
3350 helpers
.each(this.segments
, function(segment
){
3352 outerRadius
: this.scale
.calculateCenterOffset(segment
.value
)
3357 draw : function(ease
){
3358 var easingDecimal
= ease
|| 1;
3359 //Clear & draw the canvas
3361 helpers
.each(this.segments
,function(segment
, index
){
3362 segment
.transition({
3363 circumference
: this.scale
.getCircumference(),
3364 outerRadius
: this.scale
.calculateCenterOffset(segment
.value
)
3367 segment
.endAngle
= segment
.startAngle
+ segment
.circumference
;
3369 // If we've removed the first segment we need to set the first one to
3370 // start at the top.
3372 segment
.startAngle
= Math
.PI
* 1.5;
3375 //Check to see if it's the last segment, if not get the next and update the start angle
3376 if (index
< this.segments
.length
- 1){
3377 this.segments
[index
+1].startAngle
= segment
.endAngle
;
3392 helpers
= Chart
.helpers
;
3399 //Boolean - Whether to show lines for each scale point
3400 scaleShowLine
: true,
3402 //Boolean - Whether we show the angle lines out of the radar
3403 angleShowLineOut
: true,
3405 //Boolean - Whether to show labels on the scale
3406 scaleShowLabels
: false,
3408 // Boolean - Whether the scale should begin at zero
3409 scaleBeginAtZero
: true,
3411 //String - Colour of the angle line
3412 angleLineColor
: "rgba(0,0,0,.1)",
3414 //Number - Pixel width of the angle line
3417 //Number - Interval at which to draw angle lines ("every Nth point")
3418 angleLineInterval
: 1,
3420 //String - Point label font declaration
3421 pointLabelFontFamily
: "'Arial'",
3423 //String - Point label font weight
3424 pointLabelFontStyle
: "normal",
3426 //Number - Point label font size in pixels
3427 pointLabelFontSize
: 10,
3429 //String - Point label font colour
3430 pointLabelFontColor
: "#666",
3432 //Boolean - Whether to show a dot for each point
3435 //Number - Radius of each point dot in pixels
3438 //Number - Pixel width of point dot stroke
3439 pointDotStrokeWidth
: 1,
3441 //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
3442 pointHitDetectionRadius
: 20,
3444 //Boolean - Whether to show a stroke for datasets
3445 datasetStroke
: true,
3447 //Number - Pixel width of dataset stroke
3448 datasetStrokeWidth
: 2,
3450 //Boolean - Whether to fill the dataset with a colour
3453 //String - A legend template
3454 legendTemplate
: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span class=\"<%=name.toLowerCase()%>-legend-icon\" style=\"background-color:<%=datasets[i].strokeColor%>\"></span><span class=\"<%=name.toLowerCase()%>-legend-text\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
3458 initialize: function(data
){
3459 this.PointClass
= Chart
.Point
.extend({
3460 strokeWidth
: this.options
.pointDotStrokeWidth
,
3461 radius
: this.options
.pointDotRadius
,
3462 display
: this.options
.pointDot
,
3463 hitDetectionRadius
: this.options
.pointHitDetectionRadius
,
3464 ctx
: this.chart
.ctx
3469 this.buildScale(data
);
3471 //Set up tooltip events on the chart
3472 if (this.options
.showTooltips
){
3473 helpers
.bindEvents(this, this.options
.tooltipEvents
, function(evt
){
3474 var activePointsCollection
= (evt
.type
!== 'mouseout') ? this.getPointsAtEvent(evt
) : [];
3476 this.eachPoints(function(point
){
3477 point
.restore(['fillColor', 'strokeColor']);
3479 helpers
.each(activePointsCollection
, function(activePoint
){
3480 activePoint
.fillColor
= activePoint
.highlightFill
;
3481 activePoint
.strokeColor
= activePoint
.highlightStroke
;
3484 this.showTooltip(activePointsCollection
);
3488 //Iterate through each of the datasets, and build this into a property of the chart
3489 helpers
.each(data
.datasets
,function(dataset
){
3491 var datasetObject
= {
3492 label
: dataset
.label
|| null,
3493 fillColor
: dataset
.fillColor
,
3494 strokeColor
: dataset
.strokeColor
,
3495 pointColor
: dataset
.pointColor
,
3496 pointStrokeColor
: dataset
.pointStrokeColor
,
3500 this.datasets
.push(datasetObject
);
3502 helpers
.each(dataset
.data
,function(dataPoint
,index
){
3503 //Add a new point for each piece of data, passing any required data to draw.
3505 if (!this.scale
.animation
){
3506 pointPosition
= this.scale
.getPointPosition(index
, this.scale
.calculateCenterOffset(dataPoint
));
3508 datasetObject
.points
.push(new this.PointClass({
3510 label
: data
.labels
[index
],
3511 datasetLabel
: dataset
.label
,
3512 x
: (this.options
.animation
) ? this.scale
.xCenter
: pointPosition
.x
,
3513 y
: (this.options
.animation
) ? this.scale
.yCenter
: pointPosition
.y
,
3514 strokeColor
: dataset
.pointStrokeColor
,
3515 fillColor
: dataset
.pointColor
,
3516 highlightFill
: dataset
.pointHighlightFill
|| dataset
.pointColor
,
3517 highlightStroke
: dataset
.pointHighlightStroke
|| dataset
.pointStrokeColor
3525 eachPoints : function(callback
){
3526 helpers
.each(this.datasets
,function(dataset
){
3527 helpers
.each(dataset
.points
,callback
,this);
3531 getPointsAtEvent : function(evt
){
3532 var mousePosition
= helpers
.getRelativePosition(evt
),
3533 fromCenter
= helpers
.getAngleFromPoint({
3534 x
: this.scale
.xCenter
,
3535 y
: this.scale
.yCenter
3538 var anglePerIndex
= (Math
.PI
* 2) /this.scale
.valuesCount
,
3539 pointIndex
= Math
.round((fromCenter
.angle
- Math
.PI
* 1.5) / anglePerIndex
),
3540 activePointsCollection
= [];
3542 // If we're at the top, make the pointIndex 0 to get the first of the array.
3543 if (pointIndex
>= this.scale
.valuesCount
|| pointIndex
< 0){
3547 if (fromCenter
.distance
<= this.scale
.drawingArea
){
3548 helpers
.each(this.datasets
, function(dataset
){
3549 activePointsCollection
.push(dataset
.points
[pointIndex
]);
3553 return activePointsCollection
;
3556 buildScale : function(data
){
3557 this.scale
= new Chart
.RadialScale({
3558 display
: this.options
.showScale
,
3559 fontStyle
: this.options
.scaleFontStyle
,
3560 fontSize
: this.options
.scaleFontSize
,
3561 fontFamily
: this.options
.scaleFontFamily
,
3562 fontColor
: this.options
.scaleFontColor
,
3563 showLabels
: this.options
.scaleShowLabels
,
3564 showLabelBackdrop
: this.options
.scaleShowLabelBackdrop
,
3565 backdropColor
: this.options
.scaleBackdropColor
,
3566 backgroundColors
: this.options
.scaleBackgroundColors
,
3567 backdropPaddingY
: this.options
.scaleBackdropPaddingY
,
3568 backdropPaddingX
: this.options
.scaleBackdropPaddingX
,
3569 lineWidth
: (this.options
.scaleShowLine
) ? this.options
.scaleLineWidth
: 0,
3570 lineColor
: this.options
.scaleLineColor
,
3571 angleLineColor
: this.options
.angleLineColor
,
3572 angleLineWidth
: (this.options
.angleShowLineOut
) ? this.options
.angleLineWidth
: 0,
3573 angleLineInterval
: (this.options
.angleLineInterval
) ? this.options
.angleLineInterval
: 1,
3574 // Point labels at the edge of each line
3575 pointLabelFontColor
: this.options
.pointLabelFontColor
,
3576 pointLabelFontSize
: this.options
.pointLabelFontSize
,
3577 pointLabelFontFamily
: this.options
.pointLabelFontFamily
,
3578 pointLabelFontStyle
: this.options
.pointLabelFontStyle
,
3579 height
: this.chart
.height
,
3580 width
: this.chart
.width
,
3581 xCenter
: this.chart
.width
/2,
3582 yCenter
: this.chart
.height
/2,
3583 ctx
: this.chart
.ctx
,
3584 templateString
: this.options
.scaleLabel
,
3585 labels
: data
.labels
,
3586 valuesCount
: data
.datasets
[0].data
.length
3589 this.scale
.setScaleSize();
3590 this.updateScaleRange(data
.datasets
);
3591 this.scale
.buildYLabels();
3593 updateScaleRange: function(datasets
){
3594 var valuesArray
= (function(){
3595 var totalDataArray
= [];
3596 helpers
.each(datasets
,function(dataset
){
3598 totalDataArray
= totalDataArray
.concat(dataset
.data
);
3601 helpers
.each(dataset
.points
, function(point
){
3602 totalDataArray
.push(point
.value
);
3606 return totalDataArray
;
3610 var scaleSizes
= (this.options
.scaleOverride
) ?
3612 steps
: this.options
.scaleSteps
,
3613 stepValue
: this.options
.scaleStepWidth
,
3614 min
: this.options
.scaleStartValue
,
3615 max
: this.options
.scaleStartValue
+ (this.options
.scaleSteps
* this.options
.scaleStepWidth
)
3617 helpers
.calculateScaleRange(
3619 helpers
.min([this.chart
.width
, this.chart
.height
])/2,
3620 this.options
.scaleFontSize
,
3621 this.options
.scaleBeginAtZero
,
3622 this.options
.scaleIntegersOnly
3631 addData : function(valuesArray
,label
){
3632 //Map the values array for each of the datasets
3633 this.scale
.valuesCount
++;
3634 helpers
.each(valuesArray
,function(value
,datasetIndex
){
3635 var pointPosition
= this.scale
.getPointPosition(this.scale
.valuesCount
, this.scale
.calculateCenterOffset(value
));
3636 this.datasets
[datasetIndex
].points
.push(new this.PointClass({
3639 datasetLabel
: this.datasets
[datasetIndex
].label
,
3642 strokeColor
: this.datasets
[datasetIndex
].pointStrokeColor
,
3643 fillColor
: this.datasets
[datasetIndex
].pointColor
3647 this.scale
.labels
.push(label
);
3653 removeData : function(){
3654 this.scale
.valuesCount
--;
3655 this.scale
.labels
.shift();
3656 helpers
.each(this.datasets
,function(dataset
){
3657 dataset
.points
.shift();
3662 update : function(){
3663 this.eachPoints(function(point
){
3670 helpers
.extend(this.scale
, {
3671 width
: this.chart
.width
,
3672 height
: this.chart
.height
,
3673 size
: helpers
.min([this.chart
.width
, this.chart
.height
]),
3674 xCenter
: this.chart
.width
/2,
3675 yCenter
: this.chart
.height
/2
3677 this.updateScaleRange(this.datasets
);
3678 this.scale
.setScaleSize();
3679 this.scale
.buildYLabels();
3681 draw : function(ease
){
3682 var easeDecimal
= ease
|| 1,
3683 ctx
= this.chart
.ctx
;
3687 helpers
.each(this.datasets
,function(dataset
){
3689 //Transition each point first so that the line and point drawing isn't out of sync
3690 helpers
.each(dataset
.points
,function(point
,index
){
3691 if (point
.hasValue()){
3692 point
.transition(this.scale
.getPointPosition(index
, this.scale
.calculateCenterOffset(point
.value
)), easeDecimal
);
3698 //Draw the line between all the points
3699 ctx
.lineWidth
= this.options
.datasetStrokeWidth
;
3700 ctx
.strokeStyle
= dataset
.strokeColor
;
3702 helpers
.each(dataset
.points
,function(point
,index
){
3704 ctx
.moveTo(point
.x
,point
.y
);
3707 ctx
.lineTo(point
.x
,point
.y
);
3713 ctx
.fillStyle
= dataset
.fillColor
;
3714 if(this.options
.datasetFill
){
3717 //Now draw the points over the line
3718 //A little inefficient double looping, but better than the line
3719 //lagging behind the point positions
3720 helpers
.each(dataset
.points
,function(point
){
3721 if (point
.hasValue()){