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