gauge.coffee 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. # Request Animation Frame Polyfill
  2. # CoffeeScript version of http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  3. do () ->
  4. vendors = ['ms', 'moz', 'webkit', 'o']
  5. for vendor in vendors
  6. if window.requestAnimationFrame
  7. break
  8. window.requestAnimationFrame = window[vendor + 'RequestAnimationFrame']
  9. window.cancelAnimationFrame = window[vendor + 'CancelAnimationFrame'] or window[vendor + 'CancelRequestAnimationFrame']
  10. browserRequestAnimationFrame = null
  11. lastId = 0
  12. isCancelled = {}
  13. if not requestAnimationFrame
  14. window.requestAnimationFrame = (callback, element) ->
  15. currTime = new Date().getTime()
  16. timeToCall = Math.max(0, 16 - (currTime - lastTime))
  17. id = window.setTimeout(() ->
  18. callback(currTime + timeToCall)
  19. , timeToCall)
  20. lastTime = currTime + timeToCall
  21. return id
  22. # This implementation should only be used with the setTimeout()
  23. # version of window.requestAnimationFrame().
  24. window.cancelAnimationFrame = (id) ->
  25. clearTimeout(id)
  26. else if not window.cancelAnimationFrame
  27. browserRequestAnimationFrame = window.requestAnimationFrame
  28. window.requestAnimationFrame = (callback, element) ->
  29. myId = ++lastId
  30. browserRequestAnimationFrame(() ->
  31. if not isCancelled[myId]
  32. callback()
  33. , element)
  34. return myId
  35. window.cancelAnimationFrame = (id) ->
  36. isCancelled[id] = true
  37. String.prototype.hashCode = () ->
  38. hash = 0
  39. if this.length == 0
  40. return hash
  41. for i in [0...this.length]
  42. char = this.charCodeAt(i)
  43. hash = ((hash << 5) - hash) + char
  44. hash = hash & hash # Convert to 32bit integer
  45. return hash
  46. secondsToString = (sec) ->
  47. hr = Math.floor(sec / 3600)
  48. min = Math.floor((sec - (hr * 3600))/60)
  49. sec -= ((hr * 3600) + (min * 60))
  50. sec += ''
  51. min += ''
  52. while min.length < 2
  53. min = '0' + min
  54. while sec.length < 2
  55. sec = '0' + sec
  56. hr = if hr then hr + ':' else ''
  57. return hr + min + ':' + sec
  58. formatNumber = (num) ->
  59. return addCommas(num.toFixed(0))
  60. updateObjectValues = (obj1, obj2) ->
  61. for own key, val of obj2
  62. obj1[key] = val
  63. return obj1
  64. mergeObjects = (obj1, obj2) ->
  65. out = {}
  66. for own key, val of obj1
  67. out[key] = val
  68. for own key, val of obj2
  69. out[key] = val
  70. return out
  71. addCommas = (nStr) ->
  72. nStr += ''
  73. x = nStr.split('.')
  74. x1 = x[0]
  75. x2 = ''
  76. if x.length > 1
  77. x2 = '.' + x[1]
  78. rgx = /(\d+)(\d{3})/
  79. while rgx.test(x1)
  80. x1 = x1.replace(rgx, '$1' + ',' + '$2')
  81. return x1 + x2
  82. cutHex = (nStr) ->
  83. if nStr.charAt(0) == "#"
  84. return nStr.substring(1,7)
  85. return nStr
  86. class ValueUpdater
  87. animationSpeed: 32
  88. constructor: (addToAnimationQueue=true, @clear=true) ->
  89. if addToAnimationQueue
  90. AnimationUpdater.add(@)
  91. update: (force=false) ->
  92. if force or @displayedValue != @value
  93. if @ctx and @clear
  94. @ctx.clearRect(0, 0, @canvas.width, @canvas.height)
  95. diff = @value - @displayedValue
  96. if Math.abs(diff / @animationSpeed) <= 0.001
  97. @displayedValue = @value
  98. else
  99. @displayedValue = @displayedValue + diff / @animationSpeed
  100. @render()
  101. return true
  102. return false
  103. class BaseGauge extends ValueUpdater
  104. displayScale: 1
  105. setTextField: (textField) ->
  106. @textField = if textField instanceof TextRenderer then textField else new TextRenderer(textField)
  107. setMinValue: (@minValue, updateStartValue=true) ->
  108. if updateStartValue
  109. @displayedValue = @minValue
  110. for gauge in @gp or []
  111. gauge.displayedValue = @minValue
  112. setOptions: (options=null) ->
  113. @options = mergeObjects(@options, options)
  114. if @textField
  115. @textField.el.style.fontSize = options.fontSize + 'px'
  116. if @options.angle > .5
  117. @gauge.options.angle = .5
  118. @configDisplayScale()
  119. return @
  120. configDisplayScale: () ->
  121. prevDisplayScale = @displayScale
  122. if @options.highDpiSupport == false
  123. delete @displayScale
  124. else
  125. devicePixelRatio = window.devicePixelRatio or 1
  126. backingStorePixelRatio =
  127. @ctx.webkitBackingStorePixelRatio or
  128. @ctx.mozBackingStorePixelRatio or
  129. @ctx.msBackingStorePixelRatio or
  130. @ctx.oBackingStorePixelRatio or
  131. @ctx.backingStorePixelRatio or 1
  132. @displayScale = devicePixelRatio / backingStorePixelRatio
  133. if @displayScale != prevDisplayScale
  134. width = @canvas.G__width or @canvas.width
  135. height = @canvas.G__height or @canvas.height
  136. @canvas.width = width * @displayScale
  137. @canvas.height = height * @displayScale
  138. @canvas.style.width = "#{width}px"
  139. @canvas.style.height = "#{height}px"
  140. @canvas.G__width = width
  141. @canvas.G__height = height
  142. return @
  143. class TextRenderer
  144. constructor: (@el) ->
  145. # Default behaviour, override to customize rendering
  146. render: (gauge) ->
  147. @el.innerHTML = formatNumber(gauge.displayedValue)
  148. class AnimatedText extends ValueUpdater
  149. displayedValue: 0
  150. value: 0
  151. setVal: (value) ->
  152. @value = 1 * value
  153. constructor: (@elem, @text=false) ->
  154. @value = 1 * @elem.innerHTML
  155. if @text
  156. @value = 0
  157. render: () ->
  158. if @text
  159. textVal = secondsToString(@displayedValue.toFixed(0))
  160. else
  161. textVal = addCommas(formatNumber(@displayedValue))
  162. @elem.innerHTML = textVal
  163. AnimatedTextFactory =
  164. create: (objList) ->
  165. out = []
  166. for elem in objList
  167. out.push(new AnimatedText(elem))
  168. return out
  169. class GaugePointer extends ValueUpdater
  170. displayedValue: 0
  171. value: 0
  172. options:
  173. strokeWidth: 0.035
  174. length: 0.1
  175. color: "#000000"
  176. constructor: (@gauge) ->
  177. @ctx = @gauge.ctx
  178. @canvas = @gauge.canvas
  179. super(false, false)
  180. @setOptions()
  181. setOptions: (options=null) ->
  182. updateObjectValues(@options, options)
  183. @length = @canvas.height * @options.length
  184. @strokeWidth = @canvas.height * @options.strokeWidth
  185. @maxValue = @gauge.maxValue
  186. @minValue = @gauge.minValue
  187. @animationSpeed = @gauge.animationSpeed
  188. @options.angle = @gauge.options.angle
  189. render: () ->
  190. angle = @gauge.getAngle.call(@, @displayedValue)
  191. centerX = @canvas.width / 2
  192. centerY = @canvas.height * 0.9
  193. x = Math.round(centerX + @length * Math.cos(angle))
  194. y = Math.round(centerY + @length * Math.sin(angle))
  195. startX = Math.round(centerX + @strokeWidth * Math.cos(angle - Math.PI/2))
  196. startY = Math.round(centerY + @strokeWidth * Math.sin(angle - Math.PI/2))
  197. endX = Math.round(centerX + @strokeWidth * Math.cos(angle + Math.PI/2))
  198. endY = Math.round(centerY + @strokeWidth * Math.sin(angle + Math.PI/2))
  199. @ctx.fillStyle = @options.color
  200. @ctx.beginPath()
  201. @ctx.arc(centerX, centerY, @strokeWidth, 0, Math.PI*2, true)
  202. @ctx.fill()
  203. @ctx.beginPath()
  204. @ctx.moveTo(startX, startY)
  205. @ctx.lineTo(x, y)
  206. @ctx.lineTo(endX, endY)
  207. @ctx.fill()
  208. class Bar
  209. constructor: (@elem) ->
  210. updateValues: (arrValues) ->
  211. @value = arrValues[0]
  212. @maxValue = arrValues[1]
  213. @avgValue = arrValues[2]
  214. @render()
  215. render: () ->
  216. if @textField
  217. @textField.text(formatNumber(@value))
  218. if @maxValue == 0
  219. @maxValue = @avgValue * 2
  220. valPercent = (@value / @maxValue) * 100
  221. avgPercent = (@avgValue / @maxValue) * 100
  222. $(".bar-value", @elem).css({"width": valPercent + "%"})
  223. $(".typical-value", @elem).css({"width": avgPercent + "%"})
  224. class Gauge extends BaseGauge
  225. elem: null
  226. value: [20] # we support multiple pointers
  227. maxValue: 80
  228. minValue: 0
  229. displayedAngle: 0
  230. displayedValue: 0
  231. lineWidth: 40
  232. paddingBottom: 0.1
  233. percentColors: null,
  234. options:
  235. colorStart: "#6fadcf"
  236. colorStop: undefined
  237. gradientType: 0 # 0 : radial, 1 : linear
  238. strokeColor: "#e0e0e0"
  239. pointer:
  240. length: 0.8
  241. strokeWidth: 0.035
  242. angle: 0.15
  243. lineWidth: 0.44
  244. fontSize: 40
  245. limitMax: false
  246. constructor: (@canvas) ->
  247. super()
  248. @percentColors = null
  249. if typeof G_vmlCanvasManager != 'undefined'
  250. @canvas = window.G_vmlCanvasManager.initElement(@canvas)
  251. @ctx = @canvas.getContext('2d')
  252. @gp = [new GaugePointer(@)]
  253. @setOptions()
  254. @render()
  255. setOptions: (options=null) ->
  256. super(options)
  257. @configPercentColors()
  258. @lineWidth = @canvas.height * (1 - @paddingBottom) * @options.lineWidth # .2 - .7
  259. @radius = @canvas.height * (1 - @paddingBottom) - @lineWidth
  260. @ctx.clearRect(0, 0, @canvas.width, @canvas.height)
  261. @render()
  262. for gauge in @gp
  263. gauge.setOptions(@options.pointer)
  264. gauge.render()
  265. return @
  266. configPercentColors: () ->
  267. @percentColors = null;
  268. if (@options.percentColors != undefined)
  269. @percentColors = new Array()
  270. for i in [0..(@options.percentColors.length-1)]
  271. rval = parseInt((cutHex(@options.percentColors[i][1])).substring(0,2),16)
  272. gval = parseInt((cutHex(@options.percentColors[i][1])).substring(2,4),16)
  273. bval = parseInt((cutHex(@options.percentColors[i][1])).substring(4,6),16)
  274. @percentColors[i] = { pct: @options.percentColors[i][0], color: { r: rval, g: gval, b: bval } }
  275. set: (value) ->
  276. if not (value instanceof Array)
  277. value = [value]
  278. # check if we have enough GaugePointers initialized
  279. # lazy initialization
  280. if value.length > @gp.length
  281. for i in [0...(value.length - @gp.length)]
  282. @gp.push(new GaugePointer(@))
  283. # get max value and update pointer(s)
  284. i = 0
  285. max_hit = false
  286. for val in value
  287. if val > @maxValue
  288. @maxValue = @value * 1.1
  289. max_hit = true
  290. @gp[i].value = val
  291. @gp[i++].setOptions({maxValue: @maxValue, angle: @options.angle})
  292. @value = value[value.length - 1] # TODO: Span maybe??
  293. if max_hit
  294. unless @options.limitMax
  295. AnimationUpdater.run()
  296. else
  297. AnimationUpdater.run()
  298. getAngle: (value) ->
  299. return (1 + @options.angle) * Math.PI + ((value - @minValue) / (@maxValue - @minValue)) * (1 - @options.angle * 2) * Math.PI
  300. getColorForPercentage: (pct, grad) ->
  301. if pct == 0
  302. color = @percentColors[0].color;
  303. else
  304. color = @percentColors[@percentColors.length - 1].color;
  305. for i in [0..(@percentColors.length - 1)]
  306. if (pct <= @percentColors[i].pct)
  307. if grad == true
  308. # Gradually change between colors
  309. startColor = @percentColors[i - 1]
  310. endColor = @percentColors[i]
  311. rangePct = (pct - startColor.pct) / (endColor.pct - startColor.pct) # How far between both colors
  312. color = {
  313. r: Math.floor(startColor.color.r * (1 - rangePct) + endColor.color.r * rangePct),
  314. g: Math.floor(startColor.color.g * (1 - rangePct) + endColor.color.g * rangePct),
  315. b: Math.floor(startColor.color.b * (1 - rangePct) + endColor.color.b * rangePct)
  316. }
  317. else
  318. color = @percentColors[i].color
  319. break
  320. return 'rgb(' + [color.r, color.g, color.b].join(',') + ')'
  321. getColorForValue: (val, grad) ->
  322. pct = (val - @minValue) / (@maxValue - @minValue)
  323. return @getColorForPercentage(pct, grad);
  324. render: () ->
  325. # Draw using canvas
  326. w = @canvas.width / 2
  327. h = @canvas.height * (1 - @paddingBottom)
  328. displayedAngle = @getAngle(@displayedValue)
  329. if @textField
  330. @textField.render(@)
  331. @ctx.lineCap = "butt"
  332. if @options.customFillStyle != undefined
  333. fillStyle = @options.customFillStyle(@)
  334. else if @percentColors != null
  335. fillStyle = @getColorForValue(@displayedValue, true)
  336. else if @options.colorStop != undefined
  337. if @options.gradientType == 0
  338. fillStyle = this.ctx.createRadialGradient(w, h, 9, w, h, 70);
  339. else
  340. fillStyle = this.ctx.createLinearGradient(0, 0, w, 0);
  341. fillStyle.addColorStop(0, @options.colorStart)
  342. fillStyle.addColorStop(1, @options.colorStop)
  343. else
  344. fillStyle = @options.colorStart
  345. @ctx.strokeStyle = fillStyle
  346. @ctx.beginPath()
  347. @ctx.arc(w, h, @radius, (1 + @options.angle) * Math.PI, displayedAngle, false)
  348. @ctx.lineWidth = @lineWidth
  349. @ctx.stroke()
  350. @ctx.strokeStyle = @options.strokeColor
  351. @ctx.beginPath()
  352. @ctx.arc(w, h, @radius, displayedAngle, (2 - @options.angle) * Math.PI, false)
  353. @ctx.stroke()
  354. for gauge in @gp
  355. gauge.update(true)
  356. class BaseDonut extends BaseGauge
  357. lineWidth: 15
  358. displayedValue: 0
  359. value: 33
  360. maxValue: 80
  361. minValue: 0
  362. options:
  363. lineWidth: 0.10
  364. colorStart: "#6f6ea0"
  365. colorStop: "#c0c0db"
  366. strokeColor: "#eeeeee"
  367. shadowColor: "#d5d5d5"
  368. angle: 0.35
  369. constructor: (@canvas) ->
  370. super()
  371. if typeof G_vmlCanvasManager != 'undefined'
  372. @canvas = window.G_vmlCanvasManager.initElement(@canvas)
  373. @ctx = @canvas.getContext('2d')
  374. @setOptions()
  375. @render()
  376. getAngle: (value) ->
  377. return (1 - @options.angle) * Math.PI + ((value - @minValue) / (@maxValue - @minValue)) * ((2 + @options.angle) - (1 - @options.angle)) * Math.PI
  378. setOptions: (options=null) ->
  379. super(options)
  380. @lineWidth = @canvas.height * @options.lineWidth
  381. @radius = @canvas.height / 2 - @lineWidth/2
  382. return @
  383. set: (value) ->
  384. @value = value
  385. if @value > @maxValue
  386. @maxValue = @value * 1.1
  387. AnimationUpdater.run()
  388. render: () ->
  389. displayedAngle = @getAngle(@displayedValue)
  390. w = @canvas.width / 2
  391. h = @canvas.height / 2
  392. if @textField
  393. @textField.render(@)
  394. grdFill = @ctx.createRadialGradient(w, h, 39, w, h, 70)
  395. grdFill.addColorStop(0, @options.colorStart)
  396. grdFill.addColorStop(1, @options.colorStop)
  397. start = @radius - @lineWidth / 2
  398. stop = @radius + @lineWidth / 2
  399. @ctx.strokeStyle = @options.strokeColor
  400. @ctx.beginPath()
  401. @ctx.arc(w, h, @radius, (1 - @options.angle) * Math.PI, (2 + @options.angle) * Math.PI, false)
  402. @ctx.lineWidth = @lineWidth
  403. @ctx.lineCap = "round"
  404. @ctx.stroke()
  405. @ctx.strokeStyle = grdFill
  406. @ctx.beginPath()
  407. @ctx.arc(w, h, @radius, (1 - @options.angle) * Math.PI, displayedAngle, false)
  408. @ctx.stroke()
  409. class Donut extends BaseDonut
  410. strokeGradient: (w, h, start, stop) ->
  411. grd = @ctx.createRadialGradient(w, h, start, w, h, stop)
  412. grd.addColorStop(0, @options.shadowColor)
  413. grd.addColorStop(0.12, @options._orgStrokeColor)
  414. grd.addColorStop(0.88, @options._orgStrokeColor)
  415. grd.addColorStop(1, @options.shadowColor)
  416. return grd
  417. setOptions: (options=null) ->
  418. super(options)
  419. w = @canvas.width / 2
  420. h = @canvas.height / 2
  421. start = @radius - @lineWidth / 2
  422. stop = @radius + @lineWidth / 2
  423. @options._orgStrokeColor = @options.strokeColor
  424. @options.strokeColor = @strokeGradient(w, h, start, stop)
  425. return @
  426. window.AnimationUpdater =
  427. elements: []
  428. animId: null
  429. addAll: (list) ->
  430. for elem in list
  431. AnimationUpdater.elements.push(elem)
  432. add: (object) ->
  433. AnimationUpdater.elements.push(object)
  434. run: () ->
  435. animationFinished = true
  436. for elem in AnimationUpdater.elements
  437. if elem.update()
  438. animationFinished = false
  439. if not animationFinished
  440. AnimationUpdater.animId = requestAnimationFrame(AnimationUpdater.run)
  441. else
  442. cancelAnimationFrame(AnimationUpdater.animId)
  443. window.Gauge = Gauge
  444. window.Donut = Donut
  445. window.BaseDonut = BaseDonut
  446. window.TextRenderer = TextRenderer