gauge.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /* global window, define, module */
  2. (function(global, factory) {
  3. var Gauge = factory(global);
  4. if(typeof define === "function" && define.amd) {
  5. // AMD support
  6. define(function() {return Gauge;});
  7. }else if(typeof module === "object" && module.exports) {
  8. // CommonJS support
  9. module.exports = Gauge;
  10. }else {
  11. // We are probably running in the browser
  12. global.Gauge = Gauge;
  13. }
  14. })(typeof window === "undefined" ? this : window, function(global, undefined) {
  15. var document = global.document,
  16. requestAnimationFrame = (global.requestAnimationFrame ||
  17. global.mozRequestAnimationFrame ||
  18. global.webkitRequestAnimationFrame ||
  19. global.msRequestAnimationFrame ||
  20. function(cb) {
  21. return setTimeout(cb, 1000 / 60);
  22. });
  23. // EXPERIMENTAL!!
  24. /**
  25. * Simplistic animation function for animating the gauge. That's all!
  26. * Options are:
  27. * {
  28. * duration: 1, // In seconds
  29. * start: 0, // The start value
  30. * end: 100, // The end value
  31. * step: function, // REQUIRED! The step function that will be passed the value and does something
  32. * easing: function // The easing function. Default is easeInOutCubic
  33. * }
  34. */
  35. function Animation(options) {
  36. var duration = options.duration,
  37. currentIteration = 1,
  38. iterations = 60 * duration,
  39. start = options.start || 0,
  40. end = options.end,
  41. change = end - start,
  42. step = options.step,
  43. easing = options.easing || function easeInOutCubic(pos) {
  44. // https://github.com/danro/easing-js/blob/master/easing.js
  45. if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,3);
  46. return 0.5 * (Math.pow((pos-2),3) + 2);
  47. };
  48. function animate() {
  49. var progress = (currentIteration++) / iterations;
  50. var value = change * easing(progress) + start;
  51. // console.log(progress + ", " + value);
  52. step(value);
  53. if(progress < 1) {
  54. requestAnimationFrame(animate);
  55. }
  56. }
  57. // start!
  58. requestAnimationFrame(animate);
  59. }
  60. var Gauge = (function() {
  61. var SVG_NS = "http://www.w3.org/2000/svg";
  62. var GaugeDefaults = {
  63. dialStartAngle: 135,
  64. dialEndAngle: 45,
  65. centerX: 500,
  66. centerY: 500,
  67. radius: 400
  68. };
  69. /**
  70. * A utility function to create SVG dom tree
  71. * @param {String} name The SVG element name
  72. * @param {Object} attrs The attributes as they appear in DOM e.g. stroke-width and not strokeWidth
  73. * @param {Array} children An array of children (can be created by this same function)
  74. * @return The SVG element
  75. */
  76. function svg(name, attrs, children) {
  77. var elem = document.createElementNS(SVG_NS, name);
  78. for(var attrName in attrs) {
  79. elem.setAttribute(attrName, attrs[attrName]);
  80. }
  81. if(children) {
  82. children.forEach(function(c) {
  83. elem.appendChild(c);
  84. });
  85. }
  86. return elem;
  87. }
  88. /**
  89. * Translates percentage value to angle. e.g. If gauge span angle is 180deg, then 50%
  90. * will be 90deg
  91. */
  92. function getAngle(percentage, gaugeSpanAngle) {
  93. return percentage * gaugeSpanAngle / 100;
  94. }
  95. function normalize(value, limit) {
  96. var val = Number(value);
  97. if(val > limit) return limit;
  98. if(val < 0) return 0;
  99. return val;
  100. }
  101. function getValueInPercentage(value, limit) {
  102. return 100 * value / limit;
  103. }
  104. /**
  105. * Gets cartesian co-ordinates for a specified radius and angle (in degrees)
  106. * @param cx {Number} The center x co-oriinate
  107. * @param cy {Number} The center y co-ordinate
  108. * @param radius {Number} The radius of the circle
  109. * @param angle {Number} The angle in degrees
  110. * @return An object with x,y co-ordinates
  111. */
  112. function getCartesian(cx, cy, radius, angle) {
  113. var rad = angle * Math.PI / 180;
  114. return {
  115. x: Math.round((cx + radius * Math.cos(rad)) * 1000) / 1000,
  116. y: Math.round((cy + radius * Math.sin(rad)) * 1000) / 1000
  117. };
  118. }
  119. // Returns start and end points for dial
  120. // i.e. starts at 135deg ends at 45deg with large arc flag
  121. // REMEMBER!! angle=0 starts on X axis and then increases clockwise
  122. function getDialCoords(radius, startAngle, endAngle) {
  123. var cx = GaugeDefaults.centerX,
  124. cy = GaugeDefaults.centerY;
  125. return {
  126. end: getCartesian(cx, cy, radius, endAngle),
  127. start: getCartesian(cx, cy, radius, startAngle)
  128. };
  129. }
  130. function defaultLabelRenderer(theValue) {
  131. return Math.round(theValue);
  132. }
  133. /**
  134. * Creates a Gauge object. This should be called without the 'new' operator. Various options
  135. * can be passed for the gauge:
  136. * {
  137. * dialStartAngle: The angle to start the dial. MUST be greater than dialEndAngle. Default 135deg
  138. * dialEndAngle: The angle to end the dial. Default 45deg
  139. * radius: The gauge's radius. Default 400
  140. * max: The maximum value of the gauge. Default 100
  141. * value: The starting value of the gauge. Default 0
  142. * label: The function on how to render the center label (Should return a value)
  143. * }
  144. * @param {Element} elem The DOM into which to render the gauge
  145. * @param {Object} opts The gauge options
  146. * @return a Gauge object
  147. */
  148. return function Gauge(elem, opts) {
  149. opts = opts || {};
  150. var gaugeContainer = elem,
  151. limit = opts.max || 100,
  152. value = normalize(opts.value || 0, limit),
  153. radius = opts.radius || 400,
  154. displayValue = opts.showValue === false ? false : true,
  155. valueLabelRender = typeof (opts.label) === "function" ? opts.label : defaultLabelRenderer,
  156. startAngle = typeof (opts.dialStartAngle) === "undefined" ? 135 : opts.dialStartAngle,
  157. endAngle = typeof (opts.dialEndAngle) === "undefined" ? 45 : opts.dialEndAngle,
  158. valueDialClass = typeof (opts.valueDialClass) === "undefined" ? 'value' : opts.valueDialClass,
  159. valueTextClass = typeof (opts.valueTextClass) === "undefined" ? 'value-text' : opts.valueTextClass,
  160. dialClass = typeof (opts.dialClass) === "undefined" ? 'dial' : opts.dialClass,
  161. gaugeClass = typeof (opts.gaugeClass) === "undefined" ? 'gauge' : opts.gaugeClass,
  162. gaugeTextElem,
  163. gaugeValuePath,
  164. instance;
  165. if(startAngle < endAngle) {
  166. console.log("WARNING! Start angle should be greater than end angle. Swapping");
  167. var tmp = startAngle;
  168. startAngle = endAngle;
  169. endAngle = tmp;
  170. }
  171. function pathString(radius, startAngle, endAngle, largeArc) {
  172. var coords = getDialCoords(radius, startAngle, endAngle),
  173. start = coords.start,
  174. end = coords.end,
  175. largeArcFlag = typeof(largeArc) === "undefined" ? 1 : largeArc;
  176. return ["M", start.x, start.y, "A", radius, radius, "0", largeArcFlag, "1", end.x, end.y].join(" ");
  177. }
  178. function initializeGauge(elem) {
  179. gaugeTextElem = svg("text", {
  180. "class": valueTextClass,
  181. "x": 500,
  182. "y": 550,
  183. "font-size": "700%",
  184. "font-family": "sans-serif",
  185. "font-weight": "bold",
  186. "text-anchor": "middle"
  187. });
  188. gaugeValuePath = svg("path", {
  189. "class": valueDialClass,
  190. "fill": "transparent",
  191. "stroke": "#666",
  192. "stroke-width": 25,
  193. "d": pathString(radius, startAngle, startAngle) // value of 0
  194. });
  195. var angle = getAngle(100, 360 - Math.abs(startAngle - endAngle));
  196. var flag = angle <= 180 ? 0 : 1;
  197. var gaugeElement = svg("svg", {"viewBox": "0 0 1000 1000", "class": gaugeClass},
  198. [
  199. svg("path", {
  200. "class": dialClass,
  201. "fill": "transparent",
  202. "stroke": "#eee",
  203. "stroke-width": 20,
  204. "d": pathString(radius, startAngle, endAngle, flag)
  205. }),
  206. gaugeTextElem,
  207. gaugeValuePath
  208. ]
  209. );
  210. elem.appendChild(gaugeElement);
  211. }
  212. function updateGauge(theValue) {
  213. var val = getValueInPercentage(theValue, limit),
  214. // angle = getAngle(val, 360 - Math.abs(endAngle - startAngle)),
  215. angle = getAngle(val, 360 - Math.abs(startAngle - endAngle)),
  216. // this is because we are using arc greater than 180deg
  217. flag = angle <= 180 ? 0 : 1;
  218. (displayValue && (gaugeTextElem.textContent = valueLabelRender.call(opts, theValue)));
  219. gaugeValuePath.setAttribute("d", pathString(radius, startAngle, angle + startAngle, flag));
  220. }
  221. instance = {
  222. setMaxValue: function(max) {
  223. limit = max;
  224. },
  225. setValue: function(val) {
  226. value = normalize(val, limit);
  227. updateGauge(value);
  228. },
  229. setValueAnimated: function(val, duration) {
  230. var oldVal = value;
  231. value = normalize(val, limit);
  232. if(oldVal === value) {
  233. return;
  234. }
  235. Animation({
  236. start: oldVal || 0,
  237. end: value,
  238. duration: duration || 1,
  239. step: function(val) {
  240. updateGauge(Math.round(val * 100) / 100);
  241. }
  242. });
  243. },
  244. getValue: function() {
  245. return value;
  246. }
  247. };
  248. initializeGauge(gaugeContainer);
  249. updateGauge(value);
  250. return instance;
  251. };
  252. })();
  253. return Gauge;
  254. });