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