Details | Last modification | View Log | RSS feed
| Rev | Author | Line No. | Line |
|---|---|---|---|
| 1 | espaco | 1 | /**! |
| 2 | * easyPieChart |
||
| 3 | * Lightweight plugin to render simple, animated and retina optimized pie charts |
||
| 4 | * |
||
| 5 | * @license |
||
| 6 | * @author Robert Fleischmann <rendro87@gmail.com> (http://robert-fleischmann.de) |
||
| 7 | * @version 2.1.5 |
||
| 8 | **/ |
||
| 9 | |||
| 10 | (function(root, factory) { |
||
| 11 | if(typeof exports === 'object') { |
||
| 12 | module.exports = factory(require('angular')); |
||
| 13 | } |
||
| 14 | else if(typeof define === 'function' && define.amd) { |
||
| 15 | define(['angular'], factory); |
||
| 16 | } |
||
| 17 | else { |
||
| 18 | factory(root.angular); |
||
| 19 | } |
||
| 20 | }(this, function(angular) { |
||
| 21 | |||
| 22 | (function (angular) { |
||
| 23 | |||
| 24 | 'use strict'; |
||
| 25 | |||
| 26 | return angular.module('easypiechart', []) |
||
| 27 | |||
| 28 | .directive('easypiechart', [function() { |
||
| 29 | return { |
||
| 30 | restrict: 'A', |
||
| 31 | require: '?ngModel', |
||
| 32 | scope: { |
||
| 33 | percent: '=', |
||
| 34 | options: '=' |
||
| 35 | }, |
||
| 36 | link: function (scope, element, attrs) { |
||
| 37 | |||
| 38 | scope.percent = scope.percent || 0; |
||
| 39 | |||
| 40 | /** |
||
| 41 | * default easy pie chart options |
||
| 42 | * @type {Object} |
||
| 43 | */ |
||
| 44 | var options = { |
||
| 45 | barColor: '#ef1e25', |
||
| 46 | trackColor: '#f9f9f9', |
||
| 47 | scaleColor: '#dfe0e0', |
||
| 48 | scaleLength: 5, |
||
| 49 | lineCap: 'round', |
||
| 50 | lineWidth: 3, |
||
| 51 | size: 110, |
||
| 52 | rotate: 0, |
||
| 53 | animate: { |
||
| 54 | duration: 1000, |
||
| 55 | enabled: true |
||
| 56 | } |
||
| 57 | }; |
||
| 58 | scope.options = angular.extend(options, scope.options); |
||
| 59 | |||
| 60 | var pieChart = new EasyPieChart(element[0], options); |
||
| 61 | |||
| 62 | scope.$watch('percent', function(newVal, oldVal) { |
||
| 63 | pieChart.update(newVal); |
||
| 64 | }); |
||
| 65 | } |
||
| 66 | }; |
||
| 67 | }]); |
||
| 68 | |||
| 69 | })(angular); |
||
| 70 | /** |
||
| 71 | * Renderer to render the chart on a canvas object |
||
| 72 | * @param {DOMElement} el DOM element to host the canvas (root of the plugin) |
||
| 73 | * @param {object} options options object of the plugin |
||
| 74 | */ |
||
| 75 | var CanvasRenderer = function(el, options) { |
||
| 76 | var cachedBackground; |
||
| 77 | var canvas = document.createElement('canvas'); |
||
| 78 | |||
| 79 | el.appendChild(canvas); |
||
| 80 | |||
| 81 | if (typeof(G_vmlCanvasManager) !== 'undefined') { |
||
| 82 | G_vmlCanvasManager.initElement(canvas); |
||
| 83 | } |
||
| 84 | |||
| 85 | var ctx = canvas.getContext('2d'); |
||
| 86 | |||
| 87 | canvas.width = canvas.height = options.size; |
||
| 88 | |||
| 89 | // canvas on retina devices |
||
| 90 | var scaleBy = 1; |
||
| 91 | if (window.devicePixelRatio > 1) { |
||
| 92 | scaleBy = window.devicePixelRatio; |
||
| 93 | canvas.style.width = canvas.style.height = [options.size, 'px'].join(''); |
||
| 94 | canvas.width = canvas.height = options.size * scaleBy; |
||
| 95 | ctx.scale(scaleBy, scaleBy); |
||
| 96 | } |
||
| 97 | |||
| 98 | // move 0,0 coordinates to the center |
||
| 99 | ctx.translate(options.size / 2, options.size / 2); |
||
| 100 | |||
| 101 | // rotate canvas -90deg |
||
| 102 | ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); |
||
| 103 | |||
| 104 | var radius = (options.size - options.lineWidth) / 2; |
||
| 105 | if (options.scaleColor && options.scaleLength) { |
||
| 106 | radius -= options.scaleLength + 2; // 2 is the distance between scale and bar |
||
| 107 | } |
||
| 108 | |||
| 109 | // IE polyfill for Date |
||
| 110 | Date.now = Date.now || function() { |
||
| 111 | return +(new Date()); |
||
| 112 | }; |
||
| 113 | |||
| 114 | /** |
||
| 115 | * Draw a circle around the center of the canvas |
||
| 116 | * @param {strong} color Valid CSS color string |
||
| 117 | * @param {number} lineWidth Width of the line in px |
||
| 118 | * @param {number} percent Percentage to draw (float between -1 and 1) |
||
| 119 | */ |
||
| 120 | var drawCircle = function(color, lineWidth, percent) { |
||
| 121 | percent = Math.min(Math.max(-1, percent || 0), 1); |
||
| 122 | var isNegative = percent <= 0 ? true : false; |
||
| 123 | |||
| 124 | ctx.beginPath(); |
||
| 125 | ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, isNegative); |
||
| 126 | |||
| 127 | ctx.strokeStyle = color; |
||
| 128 | ctx.lineWidth = lineWidth; |
||
| 129 | |||
| 130 | ctx.stroke(); |
||
| 131 | }; |
||
| 132 | |||
| 133 | /** |
||
| 134 | * Draw the scale of the chart |
||
| 135 | */ |
||
| 136 | var drawScale = function() { |
||
| 137 | var offset; |
||
| 138 | var length; |
||
| 139 | |||
| 140 | ctx.lineWidth = 1; |
||
| 141 | ctx.fillStyle = options.scaleColor; |
||
| 142 | |||
| 143 | ctx.save(); |
||
| 144 | for (var i = 24; i > 0; --i) { |
||
| 145 | if (i % 6 === 0) { |
||
| 146 | length = options.scaleLength; |
||
| 147 | offset = 0; |
||
| 148 | } else { |
||
| 149 | length = options.scaleLength * 0.6; |
||
| 150 | offset = options.scaleLength - length; |
||
| 151 | } |
||
| 152 | ctx.fillRect(-options.size/2 + offset, 0, length, 1); |
||
| 153 | ctx.rotate(Math.PI / 12); |
||
| 154 | } |
||
| 155 | ctx.restore(); |
||
| 156 | }; |
||
| 157 | |||
| 158 | /** |
||
| 159 | * Request animation frame wrapper with polyfill |
||
| 160 | * @return {function} Request animation frame method or timeout fallback |
||
| 161 | */ |
||
| 162 | var reqAnimationFrame = (function() { |
||
| 163 | return window.requestAnimationFrame || |
||
| 164 | window.webkitRequestAnimationFrame || |
||
| 165 | window.mozRequestAnimationFrame || |
||
| 166 | function(callback) { |
||
| 167 | window.setTimeout(callback, 1000 / 60); |
||
| 168 | }; |
||
| 169 | }()); |
||
| 170 | |||
| 171 | /** |
||
| 172 | * Draw the background of the plugin including the scale and the track |
||
| 173 | */ |
||
| 174 | var drawBackground = function() { |
||
| 175 | if(options.scaleColor) drawScale(); |
||
| 176 | if(options.trackColor) drawCircle(options.trackColor, options.lineWidth, 1); |
||
| 177 | }; |
||
| 178 | |||
| 179 | /** |
||
| 180 | * Canvas accessor |
||
| 181 | */ |
||
| 182 | this.getCanvas = function() { |
||
| 183 | return canvas; |
||
| 184 | }; |
||
| 185 | |||
| 186 | /** |
||
| 187 | * Canvas 2D context 'ctx' accessor |
||
| 188 | */ |
||
| 189 | this.getCtx = function() { |
||
| 190 | return ctx; |
||
| 191 | }; |
||
| 192 | |||
| 193 | /** |
||
| 194 | * Clear the complete canvas |
||
| 195 | */ |
||
| 196 | this.clear = function() { |
||
| 197 | ctx.clearRect(options.size / -2, options.size / -2, options.size, options.size); |
||
| 198 | }; |
||
| 199 | |||
| 200 | /** |
||
| 201 | * Draw the complete chart |
||
| 202 | * @param {number} percent Percent shown by the chart between -100 and 100 |
||
| 203 | */ |
||
| 204 | this.draw = function(percent) { |
||
| 205 | // do we need to render a background |
||
| 206 | if (!!options.scaleColor || !!options.trackColor) { |
||
| 207 | // getImageData and putImageData are supported |
||
| 208 | if (ctx.getImageData && ctx.putImageData) { |
||
| 209 | if (!cachedBackground) { |
||
| 210 | drawBackground(); |
||
| 211 | cachedBackground = ctx.getImageData(0, 0, options.size * scaleBy, options.size * scaleBy); |
||
| 212 | } else { |
||
| 213 | ctx.putImageData(cachedBackground, 0, 0); |
||
| 214 | } |
||
| 215 | } else { |
||
| 216 | this.clear(); |
||
| 217 | drawBackground(); |
||
| 218 | } |
||
| 219 | } else { |
||
| 220 | this.clear(); |
||
| 221 | } |
||
| 222 | |||
| 223 | ctx.lineCap = options.lineCap; |
||
| 224 | |||
| 225 | // if barcolor is a function execute it and pass the percent as a value |
||
| 226 | var color; |
||
| 227 | if (typeof(options.barColor) === 'function') { |
||
| 228 | color = options.barColor(percent); |
||
| 229 | } else { |
||
| 230 | color = options.barColor; |
||
| 231 | } |
||
| 232 | |||
| 233 | // draw bar |
||
| 234 | drawCircle(color, options.lineWidth, percent / 100); |
||
| 235 | }.bind(this); |
||
| 236 | |||
| 237 | /** |
||
| 238 | * Animate from some percent to some other percentage |
||
| 239 | * @param {number} from Starting percentage |
||
| 240 | * @param {number} to Final percentage |
||
| 241 | */ |
||
| 242 | this.animate = function(from, to) { |
||
| 243 | var startTime = Date.now(); |
||
| 244 | options.onStart(from, to); |
||
| 245 | var animation = function() { |
||
| 246 | var process = Math.min(Date.now() - startTime, options.animate.duration); |
||
| 247 | var currentValue = options.easing(this, process, from, to - from, options.animate.duration); |
||
| 248 | this.draw(currentValue); |
||
| 249 | options.onStep(from, to, currentValue); |
||
| 250 | if (process >= options.animate.duration) { |
||
| 251 | options.onStop(from, to); |
||
| 252 | } else { |
||
| 253 | reqAnimationFrame(animation); |
||
| 254 | } |
||
| 255 | }.bind(this); |
||
| 256 | |||
| 257 | reqAnimationFrame(animation); |
||
| 258 | }.bind(this); |
||
| 259 | }; |
||
| 260 | |||
| 261 | var EasyPieChart = function(el, opts) { |
||
| 262 | var defaultOptions = { |
||
| 263 | barColor: '#ef1e25', |
||
| 264 | trackColor: '#f9f9f9', |
||
| 265 | scaleColor: '#dfe0e0', |
||
| 266 | scaleLength: 5, |
||
| 267 | lineCap: 'round', |
||
| 268 | lineWidth: 3, |
||
| 269 | size: 110, |
||
| 270 | rotate: 0, |
||
| 271 | animate: { |
||
| 272 | duration: 1000, |
||
| 273 | enabled: true |
||
| 274 | }, |
||
| 275 | easing: function (x, t, b, c, d) { // more can be found here: http://gsgd.co.uk/sandbox/jquery/easing/ |
||
| 276 | t = t / (d/2); |
||
| 277 | if (t < 1) { |
||
| 278 | return c / 2 * t * t + b; |
||
| 279 | } |
||
| 280 | return -c/2 * ((--t)*(t-2) - 1) + b; |
||
| 281 | }, |
||
| 282 | onStart: function(from, to) { |
||
| 283 | return; |
||
| 284 | }, |
||
| 285 | onStep: function(from, to, currentValue) { |
||
| 286 | return; |
||
| 287 | }, |
||
| 288 | onStop: function(from, to) { |
||
| 289 | return; |
||
| 290 | } |
||
| 291 | }; |
||
| 292 | |||
| 293 | // detect present renderer |
||
| 294 | if (typeof(CanvasRenderer) !== 'undefined') { |
||
| 295 | defaultOptions.renderer = CanvasRenderer; |
||
| 296 | } else if (typeof(SVGRenderer) !== 'undefined') { |
||
| 297 | defaultOptions.renderer = SVGRenderer; |
||
| 298 | } else { |
||
| 299 | throw new Error('Please load either the SVG- or the CanvasRenderer'); |
||
| 300 | } |
||
| 301 | |||
| 302 | var options = {}; |
||
| 303 | var currentValue = 0; |
||
| 304 | |||
| 305 | /** |
||
| 306 | * Initialize the plugin by creating the options object and initialize rendering |
||
| 307 | */ |
||
| 308 | var init = function() { |
||
| 309 | this.el = el; |
||
| 310 | this.options = options; |
||
| 311 | |||
| 312 | // merge user options into default options |
||
| 313 | for (var i in defaultOptions) { |
||
| 314 | if (defaultOptions.hasOwnProperty(i)) { |
||
| 315 | options[i] = opts && typeof(opts[i]) !== 'undefined' ? opts[i] : defaultOptions[i]; |
||
| 316 | if (typeof(options[i]) === 'function') { |
||
| 317 | options[i] = options[i].bind(this); |
||
| 318 | } |
||
| 319 | } |
||
| 320 | } |
||
| 321 | |||
| 322 | // check for jQuery easing |
||
| 323 | if (typeof(options.easing) === 'string' && typeof(jQuery) !== 'undefined' && jQuery.isFunction(jQuery.easing[options.easing])) { |
||
| 324 | options.easing = jQuery.easing[options.easing]; |
||
| 325 | } else { |
||
| 326 | options.easing = defaultOptions.easing; |
||
| 327 | } |
||
| 328 | |||
| 329 | // process earlier animate option to avoid bc breaks |
||
| 330 | if (typeof(options.animate) === 'number') { |
||
| 331 | options.animate = { |
||
| 332 | duration: options.animate, |
||
| 333 | enabled: true |
||
| 334 | }; |
||
| 335 | } |
||
| 336 | |||
| 337 | if (typeof(options.animate) === 'boolean' && !options.animate) { |
||
| 338 | options.animate = { |
||
| 339 | duration: 1000, |
||
| 340 | enabled: options.animate |
||
| 341 | }; |
||
| 342 | } |
||
| 343 | |||
| 344 | // create renderer |
||
| 345 | this.renderer = new options.renderer(el, options); |
||
| 346 | |||
| 347 | // initial draw |
||
| 348 | this.renderer.draw(currentValue); |
||
| 349 | |||
| 350 | // initial update |
||
| 351 | if (el.dataset && el.dataset.percent) { |
||
| 352 | this.update(parseFloat(el.dataset.percent)); |
||
| 353 | } else if (el.getAttribute && el.getAttribute('data-percent')) { |
||
| 354 | this.update(parseFloat(el.getAttribute('data-percent'))); |
||
| 355 | } |
||
| 356 | }.bind(this); |
||
| 357 | |||
| 358 | /** |
||
| 359 | * Update the value of the chart |
||
| 360 | * @param {number} newValue Number between 0 and 100 |
||
| 361 | * @return {object} Instance of the plugin for method chaining |
||
| 362 | */ |
||
| 363 | this.update = function(newValue) { |
||
| 364 | newValue = parseFloat(newValue); |
||
| 365 | if (options.animate.enabled) { |
||
| 366 | this.renderer.animate(currentValue, newValue); |
||
| 367 | } else { |
||
| 368 | this.renderer.draw(newValue); |
||
| 369 | } |
||
| 370 | currentValue = newValue; |
||
| 371 | return this; |
||
| 372 | }.bind(this); |
||
| 373 | |||
| 374 | /** |
||
| 375 | * Disable animation |
||
| 376 | * @return {object} Instance of the plugin for method chaining |
||
| 377 | */ |
||
| 378 | this.disableAnimation = function() { |
||
| 379 | options.animate.enabled = false; |
||
| 380 | return this; |
||
| 381 | }; |
||
| 382 | |||
| 383 | /** |
||
| 384 | * Enable animation |
||
| 385 | * @return {object} Instance of the plugin for method chaining |
||
| 386 | */ |
||
| 387 | this.enableAnimation = function() { |
||
| 388 | options.animate.enabled = true; |
||
| 389 | return this; |
||
| 390 | }; |
||
| 391 | |||
| 392 | init(); |
||
| 393 | }; |
||
| 394 | |||
| 395 | |||
| 396 | })); |