forked from s-yadav/radialIndicator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
radialIndicator.js
446 lines (358 loc) · 17.3 KB
/
radialIndicator.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
/*
radialIndicator.js v 1.3.1
Author: Sudhanshu Yadav
Copyright (c) 2015,2016 Sudhanshu Yadav - ignitersworld.com , released under the MIT license.
Demo on: ignitersworld.com/lab/radialIndicator.html
*/
;(function (factory) {
/** support UMD ***/
var global = Function('return this')() || (42, eval)('this');
if (typeof define === "function" && define.amd) {
define(["jquery"], function ($) {
return (global.radialIndicator = factory($, global));
});
} else if (typeof module === "object" && module.exports) {
module.exports = global.document ?
factory(require("jquery"), global) :
function (w) {
if (!w.document) {
throw new Error("radialIndiactor requires a window with a document");
}
return factory(require("jquery")(w), w);
};
} else {
global.radialIndicator = factory(global.jQuery, global);
}
}(function ($, window, undefined) {
var document = window.document;
"use strict";
//circumfence and quart value to start bar from top
var circ = Math.PI * 2,
quart = Math.PI / 2;
//function to smooth canvas drawing for ratina devices
//method to manage device pixel ratio in ratina devices
var smoothCanvas = (function() {
var ctx = document.createElement("canvas").getContext("2d"),
dpr = window.devicePixelRatio || 1,
bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1,
ratio = dpr / bsr; //PIXEL RATIO
return function(w, h, canvasElm) {
var can = canvasElm || document.createElement("canvas");
can.width = w * ratio;
can.height = h * ratio;
can.style.width = w + "px";
can.style.height = h + "px";
can.getContext("2d").setTransform(ratio, 0, 0, ratio, 0, 0);
return can;
}
}());
//function to convert hex to rgb
function hexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] : null;
}
function getPropVal(curShift, perShift, bottomRange, topRange) {
return Math.round(bottomRange + ((topRange - bottomRange) * curShift / perShift));
}
//function to get current color in case of
function getCurrentColor(curPer, bottomVal, topVal, bottomColor, topColor) {
var rgbAryTop = topColor.indexOf('#') != -1 ? hexToRgb(topColor) : topColor.match(/\d+/g),
rgbAryBottom = bottomColor.indexOf('#') != -1 ? hexToRgb(bottomColor) : bottomColor.match(/\d+/g),
perShift = topVal - bottomVal,
curShift = curPer - bottomVal;
if (!rgbAryTop || !rgbAryBottom) return null;
return 'rgb(' + getPropVal(curShift, perShift, rgbAryBottom[0], rgbAryTop[0]) + ',' + getPropVal(curShift, perShift, rgbAryBottom[1], rgbAryTop[1]) + ',' + getPropVal(curShift, perShift, rgbAryBottom[2], rgbAryTop[2]) + ')';
}
//to merge object
function merge() {
var arg = arguments,
target = arg[0];
for (var i = 1, ln = arg.length; i < ln; i++) {
var obj = arg[i];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
target[k] = obj[k];
}
}
}
return target;
}
//function to apply formatting on number depending on parameter
function formatter(pattern) {
return function(num) {
if (!pattern) return num.toString();
num = num || 0
var numRev = num.toString().split('').reverse(),
output = pattern.split("").reverse(),
i = 0,
lastHashReplaced = 0;
//changes hash with numbers
for (var ln = output.length; i < ln; i++) {
if (!numRev.length) break;
if (output[i] == "#") {
lastHashReplaced = i;
output[i] = numRev.shift();
}
}
//add overflowing numbers before prefix
output.splice(lastHashReplaced + 1, output.lastIndexOf('#') - lastHashReplaced, numRev.reverse().join(""));
return output.reverse().join('');
}
}
//circle bar class
function Indicator(container, indOption) {
var self = this;
indOption = indOption || {};
indOption = merge({}, radialIndicator.defaults, indOption);
this.indOption = indOption;
//create a queryselector if a selector string is passed in container
if (typeof container == "string")
container = document.querySelector(container);
//get the first element if container is a node list
if (container.length)
container = container[0];
this.container = container;
//create a canvas element
var canElm = document.createElement("canvas");
container.appendChild(canElm);
this.canElm = canElm; // dom object where drawing will happen
this.ctx = canElm.getContext('2d'); //get 2d canvas context
//add intial value
this.current_value = indOption.initValue || indOption.minValue || 0;
//handeling user interaction
var startListener = function(e) {
if (!indOption.interaction) return;
var touchMove = e.type == "touchstart" ? "touchmove" : "mousemove",
touchEnd = e.type == "touchstart" ? "touchend" : "mouseup",
position = canElm.getBoundingClientRect(),
cy = position.top + canElm.offsetHeight / 2,
cx = position.left + canElm.offsetWidth / 2;
var moveListener = function(e) {
e.preventDefault();
//get the cordinates
var mx = e.clientX || e.touches[0].clientX,
my = e.clientY || e.touches[0].clientY,
radian = (circ + quart + Math.atan2((my - cy), (mx - cx))) % (circ + 0.0175),
radius = (indOption.radius - 1 + indOption.barWidth / 2),
circum = circ * radius,
precision = indOption.precision != null ? indOption.precision : 0,
precisionNo = Math.pow(10, precision),
val = Math.round(precisionNo * radian * radius * (indOption.maxValue - indOption.minValue) / circum) / precisionNo;
self.value(val);
};
var endListener = function() {
document.removeEventListener(touchMove, moveListener, false);
document.removeEventListener(touchEnd, endListener, false);
};
document.addEventListener(touchMove, moveListener, false);
document.addEventListener(touchEnd, endListener, false);
};
canElm.addEventListener('touchstart', startListener, false);
canElm.addEventListener('mousedown', startListener, false);
canElm.addEventListener("mousewheel", MouseWheelHandler, false);
canElm.addEventListener("DOMMouseScroll", MouseWheelHandler, false);
function MouseWheelHandler(e) {
if (!indOption.interaction) return;
e.preventDefault();
// cross-browser wheel delta
var delta = -(Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)))),
precision = indOption.precision != null ? indOption.precision : 0,
precisionNo = Math.pow(10, precision),
diff = indOption.maxValue - indOption.minValue,
val = self.current_value + Math.round(precisionNo * delta * diff / Math.min(diff, 100)) / precisionNo;
self.value(val);
return false;
}
}
Indicator.prototype = {
constructor: radialIndicator,
_init: function() {
var indOption = this.indOption,
canElm = this.canElm,
ctx = this.ctx,
dim = (indOption.radius + indOption.barWidth) * 2; //elm width and height
//create a formatter function
this.formatter = typeof indOption.format == "function" ? indOption.format : formatter(indOption.format);
//maximum text length;
this.maxLength = indOption.percentage ? 4 : this.formatter(indOption.maxValue).length;
//smooth the canvas elm for ratina display
smoothCanvas(dim, dim, canElm);
//draw background bar
this._drawBarBg();
//put the initial value if defined
this.value(this.current_value);
return this;
},
//draw background bar
_drawBarBg: function() {
var indOption = this.indOption,
ctx = this.ctx,
dim = (indOption.radius + indOption.barWidth) * 2, //elm width and height
center = dim / 2; //center point in both x and y axis
//draw nackground circle
ctx.strokeStyle = indOption.barBgColor; //background circle color
ctx.lineWidth = indOption.barWidth;
if (indOption.barBgColor != "transparent") {
ctx.beginPath();
ctx.arc(center, center, indOption.radius - 1 + indOption.barWidth / 2, 0, 2 * Math.PI);
ctx.stroke();
}
},
//update the value of indicator without animation
value: function(val) {
//return the val if val is not provided
if (val === undefined || isNaN(val)) {
return this.current_value;
}
val = parseFloat(val);
var ctx = this.ctx,
indOption = this.indOption,
curColor = indOption.barColor,
dim = (indOption.radius + indOption.barWidth) * 2,
minVal = indOption.minValue,
maxVal = indOption.maxValue,
center = dim / 2;
//limit the val in range of minumum and maximum value
val = val < minVal ? minVal : val > maxVal ? maxVal : val;
var precision = indOption.precision != null ? indOption.precision : 0,
precisionNo = Math.pow(10, precision),
perVal = Math.round(((val - minVal) * precisionNo / (maxVal - minVal)) * 100) / precisionNo, //percentage value tp two decimal precision
dispVal = indOption.percentage ? perVal + '%' : this.formatter(val); //formatted value
//save val on object
this.current_value = val;
//draw the bg circle
ctx.clearRect(0, 0, dim, dim);
this._drawBarBg();
//get current color if color range is set
if (typeof curColor == "object") {
var range = Object.keys(curColor);
for (var i = 1, ln = range.length; i < ln; i++) {
var bottomVal = range[i - 1],
topVal = range[i],
bottomColor = curColor[bottomVal],
topColor = curColor[topVal],
newColor = val == bottomVal ? bottomColor : val == topVal ? topColor : val > bottomVal && val < topVal ? indOption.interpolate ? getCurrentColor(val, bottomVal, topVal, bottomColor, topColor) : topColor : false;
if (newColor != false) {
curColor = newColor;
break;
}
}
}
//draw th circle value
ctx.strokeStyle = curColor;
//add linecap if value setted on options
if (indOption.roundCorner) ctx.lineCap = "round";
ctx.beginPath();
ctx.arc(center, center, indOption.radius - 1 + indOption.barWidth / 2, -(quart), ((circ) * perVal / 100) - quart, false);
ctx.stroke();
//add percentage text
if (indOption.displayNumber) {
var cFont = ctx.font.split(' '),
weight = indOption.fontWeight,
fontSize = indOption.fontSize || (dim / (this.maxLength - (Math.floor(this.maxLength * 1.4 / 4) - 1)));
cFont = indOption.fontFamily || cFont[cFont.length - 1];
ctx.fillStyle = indOption.fontColor || curColor;
ctx.font = weight + " " + fontSize + "px " + cFont;
ctx.textAlign = "center";
ctx.textBaseline = indOption.textBaseline;
ctx.fillText(dispVal, center, center);
}
//call onChange callback
indOption.onChange.call(this.container,val);
return this;
},
//animate progressbar to the value
animate: function(val) {
var indOption = this.indOption,
counter = this.current_value || indOption.minValue,
self = this,
minVal = indOption.minValue,
maxVal = indOption.maxValue,
frameNum = indOption.frameNum || (indOption.percentage ? 100 : 500),
precision = indOption.precision != null ? indOption.precision : Math.ceil(Math.log(maxVal - minVal / frameNum)),
precisionNo = Math.pow(10, precision),
incBy = (maxVal - minVal) / frameNum ; //increment by .2% on every tick and 1% if showing as percentage
//limit the val in range of minumum and maximum value
val = val < minVal ? minVal : val > maxVal ? maxVal : val;
var back = val < counter;
//clear interval function if already started
if (this.intvFunc) clearInterval(this.intvFunc);
this.intvFunc = setInterval(function() {
if ((!back && counter >= val) || (back && counter <= val)) {
if (self.current_value == counter) {
clearInterval(self.intvFunc);
if (indOption.onAnimationComplete) indOption.onAnimationComplete(self.current_value);
return;
} else {
counter = val;
}
}
self.value(counter); //dispaly the value
if (counter != val) {
counter = Math.round((counter + (back ? -incBy : incBy)) * precisionNo) / precisionNo;
} //increment or decrement till counter does not reach to value
}, indOption.frameTime);
return this;
},
//method to update options
option: function(key, val) {
if (val === undefined) return this.option[key];
if (['radius', 'barWidth', 'barBgColor', 'format', 'maxValue', 'percentage'].indexOf(key) != -1) {
this.indOption[key] = val;
this._init().value(this.current_value);
}
this.indOption[key] = val;
}
};
/** Initializer function **/
function radialIndicator(container, options) {
var progObj = new Indicator(container, options);
progObj._init();
return progObj;
}
//radial indicator defaults
radialIndicator.defaults = {
radius: 50, //inner radius of indicator
barWidth: 5, //bar width
barBgColor: '#eeeeee', //unfilled bar color
barColor: '#99CC33', //filled bar color , can be a range also having different colors on different value like {0 : "#ccc", 50 : '#333', 100: '#000'}
format: null, //format indicator numbers, can be a # formator ex (##,###.##) or a function
frameTime: 10, //miliseconds to move from one frame to another
frameNum: null, //Defines numbers of frame in indicator, defaults to 100 when showing percentage and 500 for other values
fontColor: null, //font color
fontFamily: null, //defines font family
fontWeight: 'bold', //defines font weight
fontSize: null, //define the font size of indicator number
textBaseline: 'middle', //define the text base line of indicator number
interpolate: true, //interpolate color between ranges
percentage: false, //show percentage of value
precision: null, //default value for precision depend on difference between min and max divided by number of frames
displayNumber: true, //display indicator number
roundCorner: false, //have round corner in filled bar
minValue: 0, //minimum value
maxValue: 100, //maximum value
initValue: 0, //define initial value of indicator,
interaction: false, //if true it allows to change radial indicator value using mouse or touch interaction
onChange: function() {}
};
window.radialIndicator = radialIndicator;
//add as a jquery plugin
if ($) {
$.fn.radialIndicator = function(options) {
return this.each(function() {
var newPCObj = radialIndicator(this, options);
$.data(this, 'radialIndicator', newPCObj);
});
};
}
return radialIndicator;
}));