# Request Animation Frame Polyfill # CoffeeScript version of http://paulirish.com/2011/requestanimationframe-for-smart-animating/ do () -> vendors = ['ms', 'moz', 'webkit', 'o'] for vendor in vendors if window.requestAnimationFrame break window.requestAnimationFrame = window[vendor + 'RequestAnimationFrame'] window.cancelAnimationFrame = window[vendor + 'CancelAnimationFrame'] or window[vendor + 'CancelRequestAnimationFrame'] browserRequestAnimationFrame = null lastId = 0 isCancelled = {} if not requestAnimationFrame window.requestAnimationFrame = (callback, element) -> currTime = new Date().getTime() timeToCall = Math.max(0, 16 - (currTime - lastTime)) id = window.setTimeout(() -> callback(currTime + timeToCall) , timeToCall) lastTime = currTime + timeToCall return id # This implementation should only be used with the setTimeout() # version of window.requestAnimationFrame(). window.cancelAnimationFrame = (id) -> clearTimeout(id) else if not window.cancelAnimationFrame browserRequestAnimationFrame = window.requestAnimationFrame window.requestAnimationFrame = (callback, element) -> myId = ++lastId browserRequestAnimationFrame(() -> if not isCancelled[myId] callback() , element) return myId window.cancelAnimationFrame = (id) -> isCancelled[id] = true String.prototype.hashCode = () -> hash = 0 if this.length == 0 return hash for i in [0...this.length] char = this.charCodeAt(i) hash = ((hash << 5) - hash) + char hash = hash & hash # Convert to 32bit integer return hash secondsToString = (sec) -> hr = Math.floor(sec / 3600) min = Math.floor((sec - (hr * 3600))/60) sec -= ((hr * 3600) + (min * 60)) sec += '' min += '' while min.length < 2 min = '0' + min while sec.length < 2 sec = '0' + sec hr = if hr then hr + ':' else '' return hr + min + ':' + sec formatNumber = (num...) -> value = num[0] digits = 0 || num[1] return addCommas(value.toFixed(digits)) mergeObjects = (obj1, obj2) -> out = {} for own key, val of obj1 out[key] = val for own key, val of obj2 out[key] = val return out addCommas = (nStr) -> nStr += '' x = nStr.split('.') x1 = x[0] x2 = '' if x.length > 1 x2 = '.' + x[1] rgx = /(\d+)(\d{3})/ while rgx.test(x1) x1 = x1.replace(rgx, '$1' + ',' + '$2') return x1 + x2 cutHex = (nStr) -> if nStr.charAt(0) == "#" return nStr.substring(1,7) return nStr class ValueUpdater animationSpeed: 32 constructor: (addToAnimationQueue=true, @clear=true) -> if addToAnimationQueue AnimationUpdater.add(@) update: (force=false) -> if force or @displayedValue != @value if @ctx and @clear @ctx.clearRect(0, 0, @canvas.width, @canvas.height) diff = @value - @displayedValue if Math.abs(diff / @animationSpeed) <= 0.001 @displayedValue = @value else @displayedValue = @displayedValue + diff / @animationSpeed @render() return true return false class BaseGauge extends ValueUpdater displayScale: 1 setTextField: (textField, fractionDigits) -> @textField = if textField instanceof TextRenderer then textField else new TextRenderer(textField, fractionDigits) setMinValue: (@minValue, updateStartValue=true) -> if updateStartValue @displayedValue = @minValue for gauge in @gp or [] gauge.displayedValue = @minValue setOptions: (options=null) -> @options = mergeObjects(@options, options) if @textField @textField.el.style.fontSize = options.fontSize + 'px' if @options.angle > .5 @options.angle = .5 @configDisplayScale() return @ configDisplayScale: () -> prevDisplayScale = @displayScale if @options.highDpiSupport == false delete @displayScale else devicePixelRatio = window.devicePixelRatio or 1 backingStorePixelRatio = @ctx.webkitBackingStorePixelRatio or @ctx.mozBackingStorePixelRatio or @ctx.msBackingStorePixelRatio or @ctx.oBackingStorePixelRatio or @ctx.backingStorePixelRatio or 1 @displayScale = devicePixelRatio / backingStorePixelRatio if @displayScale != prevDisplayScale width = @canvas.G__width or @canvas.width height = @canvas.G__height or @canvas.height @canvas.width = width * @displayScale @canvas.height = height * @displayScale @canvas.style.width = "#{width}px" @canvas.style.height = "#{height}px" @canvas.G__width = width @canvas.G__height = height return @ class TextRenderer constructor: (@el, @fractionDigits) -> # Default behaviour, override to customize rendering render: (gauge) -> @el.innerHTML = formatNumber(gauge.displayedValue, @fractionDigits) class AnimatedText extends ValueUpdater displayedValue: 0 value: 0 setVal: (value) -> @value = 1 * value constructor: (@elem, @text=false) -> @value = 1 * @elem.innerHTML if @text @value = 0 render: () -> if @text textVal = secondsToString(@displayedValue.toFixed(0)) else textVal = addCommas(formatNumber(@displayedValue)) @elem.innerHTML = textVal AnimatedTextFactory = create: (objList) -> out = [] for elem in objList out.push(new AnimatedText(elem)) return out class GaugePointer extends ValueUpdater displayedValue: 0 value: 0 options: strokeWidth: 0.035 length: 0.1 color: "#000000" constructor: (@gauge) -> @ctx = @gauge.ctx @canvas = @gauge.canvas super(false, false) @setOptions() setOptions: (options=null) -> @options = mergeObjects(@options, options) @length = 2*@gauge.radius * @gauge.options.radiusScale * @options.length @strokeWidth = @canvas.height * @options.strokeWidth @maxValue = @gauge.maxValue @minValue = @gauge.minValue @animationSpeed = @gauge.animationSpeed @options.angle = @gauge.options.angle render: () -> angle = @gauge.getAngle.call(@, @displayedValue) x = Math.round(@length * Math.cos(angle)) y = Math.round(@length * Math.sin(angle)) startX = Math.round(@strokeWidth * Math.cos(angle - Math.PI/2)) startY = Math.round(@strokeWidth * Math.sin(angle - Math.PI/2)) endX = Math.round(@strokeWidth * Math.cos(angle + Math.PI/2)) endY = Math.round(@strokeWidth * Math.sin(angle + Math.PI/2)) @ctx.fillStyle = @options.color @ctx.beginPath() @ctx.arc(0, 0, @strokeWidth, 0, Math.PI*2, true) @ctx.fill() @ctx.beginPath() @ctx.moveTo(startX, startY) @ctx.lineTo(x, y) @ctx.lineTo(endX, endY) @ctx.fill() class Bar constructor: (@elem) -> updateValues: (arrValues) -> @value = arrValues[0] @maxValue = arrValues[1] @avgValue = arrValues[2] @render() render: () -> if @textField @textField.text(formatNumber(@value)) if @maxValue == 0 @maxValue = @avgValue * 2 valPercent = (@value / @maxValue) * 100 avgPercent = (@avgValue / @maxValue) * 100 $(".bar-value", @elem).css({"width": valPercent + "%"}) $(".typical-value", @elem).css({"width": avgPercent + "%"}) class Gauge extends BaseGauge elem: null value: [20] # we support multiple pointers maxValue: 80 minValue: 0 displayedAngle: 0 displayedValue: 0 lineWidth: 40 paddingTop: 0.1 paddingBottom: 0.1 percentColors: null, options: colorStart: "#6fadcf" colorStop: undefined gradientType: 0 # 0 : radial, 1 : linear strokeColor: "#e0e0e0" pointer: length: 0.8 strokeWidth: 0.035 angle: 0.15 lineWidth: 0.44 radiusScale: 1.0 fontSize: 40 limitMax: false limitMin: false constructor: (@canvas) -> super() @percentColors = null @forceUpdate = true if typeof G_vmlCanvasManager != 'undefined' @canvas = window.G_vmlCanvasManager.initElement(@canvas) @ctx = @canvas.getContext('2d') # Set canvas size to parent size h = @canvas.clientHeight; w = @canvas.clientWidth; @canvas.height = h; @canvas.width = w; @gp = [new GaugePointer(@)] @setOptions() @render() setOptions: (options=null) -> super(options) @configPercentColors() @extraPadding = 0 if @options.angle < 0 phi = Math.PI*(1 + @options.angle) @extraPadding = Math.sin(phi) @availableHeight = @canvas.height * (1 - @paddingTop - @paddingBottom) @lineWidth = @availableHeight * @options.lineWidth # .2 - .7 @radius = (@availableHeight - @lineWidth/2) / (1.0 + @extraPadding) @ctx.clearRect(0, 0, @canvas.width, @canvas.height) # @render() for gauge in @gp gauge.setOptions(@options.pointer) gauge.render() return @ configPercentColors: () -> @percentColors = null; if (@options.percentColors != undefined) @percentColors = new Array() for i in [0..(@options.percentColors.length-1)] rval = parseInt((cutHex(@options.percentColors[i][1])).substring(0,2),16) gval = parseInt((cutHex(@options.percentColors[i][1])).substring(2,4),16) bval = parseInt((cutHex(@options.percentColors[i][1])).substring(4,6),16) @percentColors[i] = { pct: @options.percentColors[i][0], color: { r: rval, g: gval, b: bval } } set: (value) -> if not (value instanceof Array) value = [value] # check if we have enough GaugePointers initialized # lazy initialization if value.length > @gp.length for i in [0...(value.length - @gp.length)] gp = new GaugePointer(@) gp.setOptions(@options.pointer) @gp.push(gp) else if value.length < @gp.length # Delete redundant GaugePointers @gp = @gp.slice(@gp.length-value.length) # get max value and update pointer(s) i = 0 for val in value # Limit pointer within min and max? if val > @maxValue if @options.limitMax val = @maxValue else @maxValue = val + 1 else if val < @minValue if @options.limitMin val = @minValue else @minValue = val - 1 @gp[i].value = val @gp[i++].setOptions({minValue: @minValue, maxValue: @maxValue, angle: @options.angle}) @value = Math.max(Math.min(value[value.length - 1], @maxValue), @minValue) # TODO: Span maybe?? # Force first .set() AnimationUpdater.run(@forceUpdate) @forceUpdate = false getAngle: (value) -> return (1 + @options.angle) * Math.PI + ((value - @minValue) / (@maxValue - @minValue)) * (1 - @options.angle * 2) * Math.PI getColorForPercentage: (pct, grad) -> if pct == 0 color = @percentColors[0].color; else color = @percentColors[@percentColors.length - 1].color; for i in [0..(@percentColors.length - 1)] if (pct <= @percentColors[i].pct) if grad == true # Gradually change between colors startColor = @percentColors[i - 1] || @percentColors[0] endColor = @percentColors[i] rangePct = (pct - startColor.pct) / (endColor.pct - startColor.pct) # How far between both colors color = { r: Math.floor(startColor.color.r * (1 - rangePct) + endColor.color.r * rangePct), g: Math.floor(startColor.color.g * (1 - rangePct) + endColor.color.g * rangePct), b: Math.floor(startColor.color.b * (1 - rangePct) + endColor.color.b * rangePct) } else color = @percentColors[i].color break return 'rgb(' + [color.r, color.g, color.b].join(',') + ')' getColorForValue: (val, grad) -> pct = (val - @minValue) / (@maxValue - @minValue) return @getColorForPercentage(pct, grad); renderStaticLabels: (staticLabels, w, h, radius) -> @ctx.save() @ctx.translate(w, h) # Scale font size the hard way - assuming size comes first. font = staticLabels.font or "10px Times" re = /\d+\.?\d?/ match = font.match(re)[0] rest = font.slice(match.length); fontsize = parseFloat(match) * this.displayScale; @ctx.font = fontsize + rest; @ctx.fillStyle = staticLabels.color || "#000000"; @ctx.textBaseline = "bottom" @ctx.textAlign = "center" for value in staticLabels.labels # Draw labels depending on limitMin/Max if (not @options.limitMin or value >= @minValue) and (not @options.limitMax or value <= @maxValue) rotationAngle = @getAngle(value) - 3*Math.PI/2 @ctx.rotate(rotationAngle) @ctx.fillText(formatNumber(value, staticLabels.fractionDigits), 0, -radius - @lineWidth/2) @ctx.rotate(-rotationAngle) @ctx.restore() render: () -> # Draw using canvas w = @canvas.width / 2 h = @canvas.height*@paddingTop + @availableHeight - (@radius + @lineWidth/2)*@extraPadding displayedAngle = @getAngle(@displayedValue) if @textField @textField.render(@) @ctx.lineCap = "butt" radius = @radius * @options.radiusScale if (@options.staticLabels) @renderStaticLabels(@options.staticLabels, w, h, radius) if (@options.staticZones) @ctx.save() @ctx.translate(w, h) @ctx.lineWidth = @lineWidth for zone in @options.staticZones # Draw zones depending on limitMin/Max min = zone.min if @options.limitMin and min < @minValue min = @minValue max = zone.max if @options.limitMax and max > @maxValue max = @maxValue @ctx.strokeStyle = zone.strokeStyle @ctx.beginPath() @ctx.arc(0, 0, radius, @getAngle(min), @getAngle(max), false) @ctx.stroke() @ctx.restore() else if @options.customFillStyle != undefined fillStyle = @options.customFillStyle(@) else if @percentColors != null fillStyle = @getColorForValue(@displayedValue, true) else if @options.colorStop != undefined if @options.gradientType == 0 fillStyle = this.ctx.createRadialGradient(w, h, 9, w, h, 70); else fillStyle = this.ctx.createLinearGradient(0, 0, w, 0); fillStyle.addColorStop(0, @options.colorStart) fillStyle.addColorStop(1, @options.colorStop) else fillStyle = @options.colorStart @ctx.strokeStyle = fillStyle @ctx.beginPath() @ctx.arc(w, h, radius, (1 + @options.angle) * Math.PI, displayedAngle, false) @ctx.lineWidth = @lineWidth @ctx.stroke() @ctx.strokeStyle = @options.strokeColor @ctx.beginPath() @ctx.arc(w, h, radius, displayedAngle, (2 - @options.angle) * Math.PI, false) @ctx.stroke() # Draw pointers from (w, h) @ctx.translate(w, h) for gauge in @gp gauge.update(true) @ctx.translate(-w, -h) class BaseDonut extends BaseGauge lineWidth: 15 displayedValue: 0 value: 33 maxValue: 80 minValue: 0 options: lineWidth: 0.10 colorStart: "#6f6ea0" colorStop: "#c0c0db" strokeColor: "#eeeeee" shadowColor: "#d5d5d5" angle: 0.35 radiusScale: 1.0 constructor: (@canvas) -> super() if typeof G_vmlCanvasManager != 'undefined' @canvas = window.G_vmlCanvasManager.initElement(@canvas) @ctx = @canvas.getContext('2d') @setOptions() @render() getAngle: (value) -> return (1 - @options.angle) * Math.PI + ((value - @minValue) / (@maxValue - @minValue)) * ((2 + @options.angle) - (1 - @options.angle)) * Math.PI setOptions: (options=null) -> super(options) @lineWidth = @canvas.height * @options.lineWidth @radius = @options.radiusScale * (@canvas.height / 2 - @lineWidth/2) return @ set: (value) -> @value = value if @value > @maxValue @maxValue = @value * 1.1 AnimationUpdater.run() render: () -> displayedAngle = @getAngle(@displayedValue) w = @canvas.width / 2 h = @canvas.height / 2 if @textField @textField.render(@) grdFill = @ctx.createRadialGradient(w, h, 39, w, h, 70) grdFill.addColorStop(0, @options.colorStart) grdFill.addColorStop(1, @options.colorStop) start = @radius - @lineWidth / 2 stop = @radius + @lineWidth / 2 @ctx.strokeStyle = @options.strokeColor @ctx.beginPath() @ctx.arc(w, h, @radius, (1 - @options.angle) * Math.PI, (2 + @options.angle) * Math.PI, false) @ctx.lineWidth = @lineWidth @ctx.lineCap = "round" @ctx.stroke() @ctx.strokeStyle = grdFill @ctx.beginPath() @ctx.arc(w, h, @radius, (1 - @options.angle) * Math.PI, displayedAngle, false) @ctx.stroke() class Donut extends BaseDonut strokeGradient: (w, h, start, stop) -> grd = @ctx.createRadialGradient(w, h, start, w, h, stop) grd.addColorStop(0, @options.shadowColor) grd.addColorStop(0.12, @options._orgStrokeColor) grd.addColorStop(0.88, @options._orgStrokeColor) grd.addColorStop(1, @options.shadowColor) return grd setOptions: (options=null) -> super(options) w = @canvas.width / 2 h = @canvas.height / 2 start = @radius - @lineWidth / 2 stop = @radius + @lineWidth / 2 @options._orgStrokeColor = @options.strokeColor @options.strokeColor = @strokeGradient(w, h, start, stop) return @ window.AnimationUpdater = elements: [] animId: null addAll: (list) -> for elem in list AnimationUpdater.elements.push(elem) add: (object) -> AnimationUpdater.elements.push(object) run: (force=false) -> animationFinished = true for elem in AnimationUpdater.elements if elem.update(force is true) animationFinished = false if not animationFinished AnimationUpdater.animId = requestAnimationFrame(AnimationUpdater.run) else cancelAnimationFrame(AnimationUpdater.animId) if typeof window.define == 'function' && window.define.amd? define(() -> { Gauge: Gauge, Donut: Donut, BaseDonut: BaseDonut, TextRenderer: TextRenderer } ) else if typeof module != 'undefined' && module.exports? module.exports = { Gauge: Gauge, Donut: Donut, BaseDonut: BaseDonut, TextRenderer: TextRenderer } else window.Gauge = Gauge window.Donut = Donut window.BaseDonut = BaseDonut window.TextRenderer = TextRenderer