gauge.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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. slice = Array.prototype.slice,
  17. requestAnimationFrame = (global.requestAnimationFrame ||
  18. global.mozRequestAnimationFrame ||
  19. global.webkitRequestAnimationFrame ||
  20. global.msRequestAnimationFrame ||
  21. function(cb) {
  22. return setTimeout(cb, 1000 / 60);
  23. });
  24. // EXPERIMENTAL!!
  25. /**
  26. * Simplistic animation function for animating the gauge. That's all!
  27. * Options are:
  28. * {
  29. * duration: 1, // In seconds
  30. * start: 0, // The start value
  31. * end: 100, // The end value
  32. * step: function, // REQUIRED! The step function that will be passed the value and does something
  33. * easing: function // The easing function. Default is easeInOutCubic
  34. * }
  35. */
  36. function Animation(options) {
  37. var duration = options.duration,
  38. currentIteration = 1,
  39. iterations = 60 * duration,
  40. start = options.start || 0,
  41. end = options.end,
  42. change = end - start,
  43. step = options.step,
  44. easing = options.easing || function easeInOutCubic(pos) {
  45. // https://github.com/danro/easing-js/blob/master/easing.js
  46. if ((pos/=0.5) < 1) return 0.5*Math.pow(pos,3);
  47. return 0.5 * (Math.pow((pos-2),3) + 2);
  48. };
  49. function animate() {
  50. var progress = currentIteration / iterations,
  51. value = change * easing(progress) + start;
  52. // console.log(progress + ", " + value);
  53. step(value, currentIteration);
  54. currentIteration += 1;
  55. if(progress < 1) {
  56. requestAnimationFrame(animate);
  57. }
  58. }
  59. // start!
  60. requestAnimationFrame(animate);
  61. }
  62. var Gauge = (function() {
  63. var SVG_NS = "http://www.w3.org/2000/svg";
  64. var GaugeDefaults = {
  65. centerX: 50,
  66. centerY: 50
  67. };
  68. var defaultOptions = {
  69. dialRadius: 40,
  70. dialStartAngle: 135,
  71. dialEndAngle: 45,
  72. value: 0,
  73. max: 100,
  74. min: 0,
  75. valueDialClass: "value",
  76. valueClass: "value-text",
  77. dialClass: "dial",
  78. gaugeClass: "gauge",
  79. showValue: true,
  80. gaugeColor: null,
  81. label: function(val) {return Math.round(val);}
  82. };
  83. function shallowCopy(/* source, ...targets*/) {
  84. var target = arguments[0], sources = slice.call(arguments, 1);
  85. sources.forEach(function(s) {
  86. for(var k in s) {
  87. if(s.hasOwnProperty(k)) {
  88. target[k] = s[k];
  89. }
  90. }
  91. });
  92. return target;
  93. }
  94. /**
  95. * A utility function to create SVG dom tree
  96. * @param {String} name The SVG element name
  97. * @param {Object} attrs The attributes as they appear in DOM e.g. stroke-width and not strokeWidth
  98. * @param {Array} children An array of children (can be created by this same function)
  99. * @return The SVG element
  100. */
  101. function svg(name, attrs, children) {
  102. var elem = document.createElementNS(SVG_NS, name);
  103. for(var attrName in attrs) {
  104. elem.setAttribute(attrName, attrs[attrName]);
  105. }
  106. if(children) {
  107. children.forEach(function(c) {
  108. elem.appendChild(c);
  109. });
  110. }
  111. return elem;
  112. }
  113. /**
  114. * Translates percentage value to angle. e.g. If gauge span angle is 180deg, then 50%
  115. * will be 90deg
  116. */
  117. function getAngle(percentage, gaugeSpanAngle) {
  118. return percentage * gaugeSpanAngle / 100;
  119. }
  120. function normalize(value, min, limit) {
  121. var val = Number(value);
  122. if(val > limit) return limit;
  123. if(val < min) return min;
  124. return val;
  125. }
  126. function getValueInPercentage(value, min, max) {
  127. var newMax = max - min, newVal = value - min;
  128. return 100 * newVal / newMax;
  129. // var absMin = Math.abs(min);
  130. // return 100 * (absMin + value) / (max + absMin);
  131. }
  132. /**
  133. * Gets cartesian co-ordinates for a specified radius and angle (in degrees)
  134. * @param cx {Number} The center x co-oriinate
  135. * @param cy {Number} The center y co-ordinate
  136. * @param radius {Number} The radius of the circle
  137. * @param angle {Number} The angle in degrees
  138. * @return An object with x,y co-ordinates
  139. */
  140. function getCartesian(cx, cy, radius, angle) {
  141. var rad = angle * Math.PI / 180;
  142. return {
  143. x: Math.round((cx + radius * Math.cos(rad)) * 1000) / 1000,
  144. y: Math.round((cy + radius * Math.sin(rad)) * 1000) / 1000
  145. };
  146. }
  147. // Returns start and end points for dial
  148. // i.e. starts at 135deg ends at 45deg with large arc flag
  149. // REMEMBER!! angle=0 starts on X axis and then increases clockwise
  150. function getDialCoords(radius, startAngle, endAngle) {
  151. var cx = GaugeDefaults.centerX,
  152. cy = GaugeDefaults.centerY;
  153. return {
  154. end: getCartesian(cx, cy, radius, endAngle),
  155. start: getCartesian(cx, cy, radius, startAngle)
  156. };
  157. }
  158. /**
  159. * Creates a Gauge object. This should be called without the 'new' operator. Various options
  160. * can be passed for the gauge:
  161. * {
  162. * dialStartAngle: The angle to start the dial. MUST be greater than dialEndAngle. Default 135deg
  163. * dialEndAngle: The angle to end the dial. Default 45deg
  164. * radius: The gauge's radius. Default 400
  165. * max: The maximum value of the gauge. Default 100
  166. * value: The starting value of the gauge. Default 0
  167. * label: The function on how to render the center label (Should return a value)
  168. * }
  169. * @param {Element} elem The DOM into which to render the gauge
  170. * @param {Object} opts The gauge options
  171. * @return a Gauge object
  172. */
  173. return function Gauge(elem, opts) {
  174. opts = shallowCopy({}, defaultOptions, opts);
  175. var gaugeContainer = elem,
  176. limit = opts.max,
  177. min = opts.min,
  178. value = normalize(opts.value, min, limit),
  179. radius = opts.dialRadius,
  180. displayValue = opts.showValue,
  181. startAngle = opts.dialStartAngle,
  182. endAngle = opts.dialEndAngle,
  183. valueDialClass = opts.valueDialClass,
  184. valueTextClass = opts.valueClass,
  185. valueLabelClass = opts.valueLabelClass,
  186. dialClass = opts.dialClass,
  187. gaugeClass = opts.gaugeClass,
  188. gaugeColor = opts.color,
  189. gaugeValueElem,
  190. gaugeValuePath,
  191. label = opts.label,
  192. viewBox = opts.viewBox,
  193. instance;
  194. if(startAngle < endAngle) {
  195. console.log("WARN! startAngle < endAngle, Swapping");
  196. var tmp = startAngle;
  197. startAngle = endAngle;
  198. endAngle = tmp;
  199. }
  200. function pathString(radius, startAngle, endAngle, largeArc) {
  201. var coords = getDialCoords(radius, startAngle, endAngle),
  202. start = coords.start,
  203. end = coords.end,
  204. largeArcFlag = typeof(largeArc) === "undefined" ? 1 : largeArc;
  205. return [
  206. "M", start.x, start.y,
  207. "A", radius, radius, 0, largeArcFlag, 1, end.x, end.y
  208. ].join(" ");
  209. }
  210. function initializeGauge(elem) {
  211. gaugeValueElem = svg("text", {
  212. x: 50,
  213. y: 50,
  214. fill: "#999",
  215. "class": valueTextClass,
  216. "font-size": "100%",
  217. "font-family": "sans-serif",
  218. "font-weight": "normal",
  219. "text-anchor": "middle",
  220. "alignment-baseline": "middle",
  221. "dominant-baseline": "central"
  222. });
  223. gaugeValuePath = svg("path", {
  224. "class": valueDialClass,
  225. fill: "none",
  226. stroke: "#666",
  227. "stroke-width": 2.5,
  228. d: pathString(radius, startAngle, startAngle) // value of 0
  229. });
  230. var angle = getAngle(100, 360 - Math.abs(startAngle - endAngle));
  231. var flag = angle <= 180 ? 0 : 1;
  232. var gaugeElement = svg("svg", {"viewBox": viewBox || "0 0 100 100", "class": gaugeClass},
  233. [
  234. svg("path", {
  235. "class": dialClass,
  236. fill: "none",
  237. stroke: "#eee",
  238. "stroke-width": 2,
  239. d: pathString(radius, startAngle, endAngle, flag)
  240. }),
  241. gaugeValueElem,
  242. gaugeValuePath
  243. ]
  244. );
  245. elem.appendChild(gaugeElement);
  246. }
  247. function updateGauge(theValue, frame) {
  248. var val = getValueInPercentage(theValue, min, limit),
  249. // angle = getAngle(val, 360 - Math.abs(endAngle - startAngle)),
  250. angle = getAngle(val, 360 - Math.abs(startAngle - endAngle)),
  251. // this is because we are using arc greater than 180deg
  252. flag = angle <= 180 ? 0 : 1;
  253. if(displayValue) {
  254. gaugeValueElem.textContent = label.call(opts, theValue);
  255. }
  256. gaugeValuePath.setAttribute("d", pathString(radius, startAngle, angle + startAngle, flag));
  257. }
  258. function setGaugeColor(value, duration) {
  259. var c = gaugeColor(value),
  260. dur = duration * 1000,
  261. pathTransition = "stroke " + dur + "ms ease";
  262. // textTransition = "fill " + dur + "ms ease";
  263. gaugeValuePath.style = [
  264. "stroke: " + c,
  265. "-webkit-transition: " + pathTransition,
  266. "-moz-transition: " + pathTransition,
  267. "transition: " + pathTransition,
  268. ].join(";");
  269. /*
  270. gaugeValueElem.style = [
  271. "fill: " + c,
  272. "-webkit-transition: " + textTransition,
  273. "-moz-transition: " + textTransition,
  274. "transition: " + textTransition,
  275. ].join(";");
  276. */
  277. }
  278. instance = {
  279. setMaxValue: function(max) {
  280. limit = max;
  281. },
  282. setValue: function(val) {
  283. value = normalize(val, min, limit);
  284. if(gaugeColor) {
  285. setGaugeColor(value, 0)
  286. }
  287. updateGauge(value);
  288. },
  289. setValueAnimated: function(val, duration) {
  290. var oldVal = value;
  291. value = normalize(val, min, limit);
  292. if(oldVal === value) {
  293. return;
  294. }
  295. if(gaugeColor) {
  296. setGaugeColor(value, duration);
  297. }
  298. Animation({
  299. start: oldVal || 0,
  300. end: value,
  301. duration: duration || 1,
  302. step: function(val, frame) {
  303. updateGauge(val, frame);
  304. }
  305. });
  306. },
  307. getValue: function() {
  308. return value;
  309. }
  310. };
  311. initializeGauge(gaugeContainer);
  312. instance.setValue(value);
  313. return instance;
  314. };
  315. })();
  316. return Gauge;
  317. });