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