| 776 |
lars |
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 |
}));
|