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