d3-gauge.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. 'use strict';
  2. // heavily inspired by: http://bl.ocks.org/tomerd/1499279
  3. // var xtend = require('xtend');
  4. function xtend() {
  5. var target = {}
  6. for (var i = 0; i < arguments.length; i++) {
  7. var source = arguments[i]
  8. for (var key in source) {
  9. if (source.hasOwnProperty(key)) {
  10. target[key] = source[key]
  11. }
  12. }
  13. }
  14. return target
  15. }
  16. // var defaultOpts = require('./defaults/simple');
  17. var defaultOpts = {
  18. size : 125
  19. , min : 0
  20. , max : 100
  21. , transitionDuration : 500
  22. , clazz : 'simple'
  23. , label : 'label.text'
  24. , minorTicks : 4
  25. , majorTicks : 5
  26. , needleWidthRatio : 0.6
  27. , needleContainerRadiusRatio : 0.7
  28. , zones: [
  29. { clazz: 'yellow-zone', from: 0.73, to: 0.9 }
  30. , { clazz: 'red-zone', from: 0.9, to: 1.0 }
  31. ]
  32. };
  33. // var d3 = require('d3');
  34. // var go = module.exports = Gauge;
  35. //var go = Gauge;
  36. var proto = Gauge.prototype;
  37. /**
  38. * Creates a gauge appended to the given DOM element.
  39. *
  40. * Example:
  41. *
  42. * ```js
  43. * var simpleOpts = {
  44. * size : 100
  45. * , min : 0
  46. * , max : 50
  47. * , transitionDuration : 500
  48. *
  49. * , label : 'label.text'
  50. * , minorTicks : 4
  51. * , majorTicks : 5
  52. * , needleWidthRatio : 0.6
  53. * , needleContainerRadiusRatio : 0.7
  54. *
  55. * , zones: [
  56. * { clazz: 'yellow-zone', from: 0.73, to: 0.9 }
  57. * , { clazz: 'red-zone', from: 0.9, to: 1.0 }
  58. * ]
  59. * }
  60. * var gauge = Gauge(document.getElementById('simple-gauge'), simpleOpts);
  61. * gauge.write(39);
  62. * ```
  63. *
  64. * @name Gauge
  65. * @function
  66. * @param el {DOMElement} to which the gauge is appended
  67. * @param opts {Object} gauge configuration with the following properties all of which have sensible defaults:
  68. * - label {String} that appears in the top portion of the gauge
  69. * - clazz {String} class to apply to the gauge element in order to support custom styling
  70. * - size {Number} the over all size (radius) of the gauge
  71. * - preserveAspectRatio {String} default 'xMinYMin meet', see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/preserveAspectRatio
  72. * - min {Number} the minimum value that the gauge measures
  73. * - max {Number} the maximum value that the gauge measures
  74. * - majorTicks {Number} the number of major ticks to draw
  75. * - minorTicks {Number} the number of minor ticks to draw in between two major ticks
  76. * - needleWidthRatio {Number} tweaks the gauge's needle width
  77. * - needleConatinerRadiusRatio {Number} tweaks the gauge's needle container circumference
  78. * - transitionDuration {Number} the time in ms it takes for the needle to move to a new position
  79. * - zones {Array[Object]} each with the following properties
  80. * - clazz {String} class to apply to the zone element in order to style its fill
  81. * - from {Number} between 0 and 1 to determine zone's start
  82. * - to {Number} between 0 and 1 to determine zone's end
  83. * @return {Object} the gauge with a `write` method
  84. */
  85. function Gauge() {
  86. }
  87. Gauge.prototype.draw = function (el, opts) {
  88. if (!(this instanceof Gauge)) return new Gauge(el, opts);
  89. this._el = el;
  90. this._opts = xtend(defaultOpts, opts);
  91. this._size = this._opts.size;
  92. this._radius = this._size * 0.9 / 2;
  93. this._cx = this._size / 2;
  94. this._cy = this._cx;
  95. this._preserveAspectRatio = this._opts.preserveAspectRatio;
  96. this._min = this._opts.min;
  97. this._max = this._opts.max;
  98. this._range = this._max - this._min;
  99. this._majorTicks = this._opts.majorTicks;
  100. this._minorTicks = this._opts.minorTicks;
  101. this._needleWidthRatio = this._opts.needleWidthRatio;
  102. this._needleContainerRadiusRatio = this._opts.needleContainerRadiusRatio;
  103. this._transitionDuration = this._opts.transitionDuration;
  104. this._label = this._opts.label;
  105. this._zones = this._opts.zones || [];
  106. this._clazz = this._opts.clazz;
  107. this._initZones();
  108. this._render();
  109. }
  110. /**
  111. * Writes a value to the gauge and updates its state, i.e. needle position, accordingly.
  112. * @name write
  113. * @function
  114. * @param value {Number} the new gauge value, should be in between min and max
  115. * @param transitionDuration {Number} (optional) transition duration, if not supplied the configured duration is used
  116. */
  117. proto.write = function(value, transitionDuration) {
  118. var self = this;
  119. function transition () {
  120. var needleValue = value
  121. , overflow = value > self._max
  122. , underflow = value < self._min;
  123. if (overflow) needleValue = self._max + 0.02 * self._range;
  124. else if (underflow) needleValue = self._min - 0.02 * self._range;
  125. var targetRotation = self._toDegrees(needleValue) - 90
  126. , currentRotation = self._currentRotation || targetRotation;
  127. self._currentRotation = targetRotation;
  128. return function (step) {
  129. var rotation = currentRotation + (targetRotation - currentRotation) * step;
  130. return 'translate(' + self._cx + ', ' + self._cy + ') rotate(' + rotation + ')';
  131. }
  132. }
  133. var needleContainer = this._gauge.select('.needle-container');
  134. needleContainer
  135. .selectAll('text')
  136. .attr('class', 'current-value')
  137. .text(Math.round(value));
  138. var needle = needleContainer.selectAll('path');
  139. needle
  140. .transition()
  141. .duration(transitionDuration ? transitionDuration : this._transitionDuration)
  142. .attrTween('transform', transition);
  143. }
  144. proto._initZones = function () {
  145. var self = this;
  146. function percentToVal (percent) {
  147. return self._min + self._range * percent;
  148. }
  149. function initZone (zone) {
  150. return {
  151. clazz: zone.clazz
  152. , from: percentToVal(zone.from)
  153. , to: percentToVal(zone.to)
  154. }
  155. }
  156. // create new zones to not mess with the passed in args
  157. this._zones = this._zones.map(initZone);
  158. }
  159. proto._render = function () {
  160. this._initGauge();
  161. this._drawOuterCircle();
  162. this._drawInnerCircle();
  163. this._drawLabel();
  164. this._drawZones();
  165. this._drawTicks();
  166. this._drawNeedle();
  167. this.write(this._min, 0);
  168. }
  169. proto._initGauge = function () {
  170. this._gauge = d3.select(this._el)
  171. .append('svg:svg')
  172. .attr('class' , 'd3-gauge' + (this._clazz ? ' ' + this._clazz : ''))
  173. .attr('width' , this._size)
  174. .attr('height' , this._size)
  175. .attr('viewBox', '0 0 ' + this._size + ' ' + this._size)
  176. .attr('preserveAspectRatio', this._preserveAspectRatio || 'xMinYMin meet')
  177. }
  178. proto._drawOuterCircle = function () {
  179. this._gauge
  180. .append('svg:circle')
  181. .attr('class' , 'outer-circle')
  182. .attr('cx' , this._cx)
  183. .attr('cy' , this._cy)
  184. .attr('r' , this._radius)
  185. }
  186. proto._drawInnerCircle = function () {
  187. this._gauge
  188. .append('svg:circle')
  189. .attr('class' , 'inner-circle')
  190. .attr('cx' , this._cx)
  191. .attr('cy' , this._cy)
  192. .attr('r' , 0.9 * this._radius)
  193. }
  194. proto._drawLabel = function () {
  195. if (typeof this._label === undefined) return;
  196. var fontSize = Math.round(this._size / 9);
  197. var halfFontSize = fontSize / 2;
  198. this._gauge
  199. .append('svg:text')
  200. .attr('class', 'label')
  201. .attr('x', this._cx)
  202. .attr('y', this._cy / 2 + halfFontSize)
  203. .attr('dy', halfFontSize)
  204. .attr('text-anchor', 'middle')
  205. .text(this._label)
  206. }
  207. proto._drawTicks = function () {
  208. var majorDelta = this._range / (this._majorTicks - 1)
  209. , minorDelta = majorDelta / this._minorTicks
  210. , point
  211. ;
  212. for (var major = this._min; major <= this._max; major += majorDelta) {
  213. var minorMax = Math.min(major + majorDelta, this._max);
  214. for (var minor = major + minorDelta; minor < minorMax; minor += minorDelta) {
  215. this._drawLine(this._toPoint(minor, 0.75), this._toPoint(minor, 0.85), 'minor-tick');
  216. }
  217. this._drawLine(this._toPoint(major, 0.7), this._toPoint(major, 0.85), 'major-tick');
  218. if (major === this._min || major === this._max) {
  219. point = this._toPoint(major, 0.63);
  220. this._gauge
  221. .append('svg:text')
  222. .attr('class', 'major-tick-label')
  223. .attr('x', point.x)
  224. .attr('y', point.y)
  225. .attr('text-anchor', major === this._min ? 'start' : 'end')
  226. .text(major)
  227. }
  228. }
  229. }
  230. proto._drawLine = function (p1, p2, clazz) {
  231. this._gauge
  232. .append('svg:line')
  233. .attr('class' , clazz)
  234. .attr('x1' , p1.x)
  235. .attr('y1' , p1.y)
  236. .attr('x2' , p2.x)
  237. .attr('y2' , p2.y)
  238. }
  239. proto._drawZones = function () {
  240. var self = this;
  241. function drawZone (zone) {
  242. self._drawBand(zone.from, zone.to, zone.clazz);
  243. }
  244. this._zones.forEach(drawZone);
  245. }
  246. proto._drawBand = function (start, end, clazz) {
  247. var self = this;
  248. function transform () {
  249. return 'translate(' + self._cx + ', ' + self._cy +') rotate(270)';
  250. }
  251. var arc = d3.svg.arc()
  252. .startAngle(this._toRadians(start))
  253. .endAngle(this._toRadians(end))
  254. .innerRadius(0.65 * this._radius)
  255. .outerRadius(0.85 * this._radius)
  256. ;
  257. this._gauge
  258. .append('svg:path')
  259. .attr('class', clazz)
  260. .attr('d', arc)
  261. .attr('transform', transform)
  262. }
  263. proto._drawNeedle = function () {
  264. var needleContainer = this._gauge
  265. .append('svg:g')
  266. .attr('class', 'needle-container');
  267. var midValue = (this._min + this._max) / 2;
  268. var needlePath = this._buildNeedlePath(midValue);
  269. var needleLine = d3.svg.line()
  270. .x(function(d) { return d.x })
  271. .y(function(d) { return d.y })
  272. .interpolate('basis');
  273. needleContainer
  274. .selectAll('path')
  275. .data([ needlePath ])
  276. .enter()
  277. .append('svg:path')
  278. .attr('class' , 'needle')
  279. .attr('d' , needleLine)
  280. needleContainer
  281. .append('svg:circle')
  282. .attr('cx' , this._cx)
  283. .attr('cy' , this._cy)
  284. .attr('r' , this._radius * this._needleContainerRadiusRatio / 10)
  285. // TODO: not styling font-size since we need to calculate other values from it
  286. // how do I extract style value?
  287. var fontSize = Math.round(this._size / 10);
  288. needleContainer
  289. .selectAll('text')
  290. .data([ midValue ])
  291. .enter()
  292. .append('svg:text')
  293. .attr('x' , this._cx)
  294. .attr('y' , this._size - this._cy / 4 - fontSize)
  295. .attr('dy' , fontSize / 2)
  296. .attr('text-anchor' , 'middle')
  297. }
  298. proto._buildNeedlePath = function (value) {
  299. var self = this;
  300. function valueToPoint(value, factor) {
  301. var point = self._toPoint(value, factor);
  302. point.x -= self._cx;
  303. point.y -= self._cy;
  304. return point;
  305. }
  306. var delta = this._range * this._needleWidthRatio / 10
  307. , tailValue = value - (this._range * (1/ (270/360)) / 2)
  308. var head = valueToPoint(value, 0.85)
  309. , head1 = valueToPoint(value - delta, 0.12)
  310. , head2 = valueToPoint(value + delta, 0.12)
  311. var tail = valueToPoint(tailValue, 0.28)
  312. , tail1 = valueToPoint(tailValue - delta, 0.12)
  313. , tail2 = valueToPoint(tailValue + delta, 0.12)
  314. return [head, head1, tail2, tail, tail1, head2, head];
  315. }
  316. proto._toDegrees = function (value) {
  317. // Note: tried to factor out 'this._range * 270' but that breaks things, most likely due to rounding behavior
  318. return value / this._range * 270 - (this._min / this._range * 270 + 45);
  319. }
  320. proto._toRadians = function (value) {
  321. return this._toDegrees(value) * Math.PI / 180;
  322. }
  323. proto._toPoint = function (value, factor) {
  324. var len = this._radius * factor;
  325. var inRadians = this._toRadians(value);
  326. return {
  327. x: this._cx - len * Math.cos(inRadians),
  328. y: this._cy - len * Math.sin(inRadians)
  329. };
  330. }