# Real-time Gauge Visualization. Note: Looks best with a 4:3 aspect ratio (w:h)
class Epoch.Time.Gauge extends Epoch.Chart.Canvas
defaults =
type: 'time.gauge'
domain: [0, 1]
ticks: 10
tickSize: 5
tickOffset: 5
fps: 34
format: Epoch.Formats.percent
optionListeners =
'option:domain': 'domainChanged'
'option:ticks': 'ticksChanged'
'option:tickSize': 'tickSizeChanged'
'option:tickOffset': 'tickOffsetChanged'
'option:format': 'formatChanged'
# Creates the new gauge chart.
# @param [Object] options Options for the gauge chart.
# @option options [Array] domain The domain to use when rendering values (default: [0, 1]).
# @option options [Integer] ticks Number of ticks to render (default: 10).
# @option options [Integer] tickSize The length (in pixels) for each tick (default: 5).
# @option options [Integer] tickOffset The number of pixels by which to offset ticks from the outer arc (default: 5).
# @option options [Integer] fps The number of animation frames to render per second (default: 34).
# @option options [Function] format The formatting function to use when rendering the gauge label
# (default: Epoch.Formats.percent).
constructor: (@options={}) ->
super(@options = Epoch.Util.defaults(@options, defaults))
@value = @options.value or 0
if @options.model
@options.model.on 'data:push', => @pushFromModel()
# SVG Labels Overlay
if @el.style('position') != 'absolute' and @el.style('position') != 'relative'
@el.style('position', 'relative')
@svg = @el.insert('svg', ':first-child')
.attr('width', @width)
.attr('height', @height)
.attr('class', 'gauge-labels')
@svg.style
'position': 'absolute'
'z-index': '1'
@svg.append('g')
.attr('transform', "translate(#{@textX()}, #{@textY()})")
.append('text')
.attr('class', 'value')
.text(@options.format(@value))
# Animations
@animation =
interval: null
active: false
delta: 0
target: 0
@_animate = =>
if Math.abs(@animation.target - @value) < Math.abs(@animation.delta)
@value = @animation.target
clearInterval @animation.interval
@animation.active = false
else
@value += @animation.delta
@svg.select('text.value').text(@options.format(@value))
@draw()
@onAll optionListeners
@draw()
# Sets the value for the gauge to display and begins animating the guage.
# @param [Number] value Value to set for the gauge.
update: (value) ->
@animation.target = value
@animation.delta = (value - @value) / @options.fps
unless @animation.active
@animation.interval = setInterval @_animate, (1000/@options.fps)
@animation.active = true
# Alias for the update()
method.
# @param [Number] value Value to set for the gauge.
push: (value) ->
@update value
# Responds to a model's 'data:push' event.
pushFromModel: ->
next = @options.model.getNext(@options.type, @options.dataFormat)
@update next
# @return [Number] The radius for the gauge.
radius: -> @getHeight() / 1.58
# @return [Number] The center position x-coordinate for the gauge.
centerX: -> @getWidth() / 2
# @return [Number] The center position y-coordinate for the gauge.
centerY: -> 0.68 * @getHeight()
# @return [Number] The x-coordinate for the gauge text display.
textX: -> @width / 2
# @return [Number] The y-coordinate for the gauge text display.
textY: -> 0.48 * @height
# @return [Number] The angle to set for the needle given a value within the domain.
# @param [Number] value Value to translate into a needle angle.
getAngle: (value) ->
[a, b] = @options.domain
((value - a) / (b - a)) * (Math.PI + 2*Math.PI/8) - Math.PI/2 - Math.PI/8
# Sets context styles given a particular selector.
# @param [String] selector The selector to use when setting the styles.
setStyles: (selector) ->
styles = @getStyles selector
@ctx.fillStyle = styles.fill
@ctx.strokeStyle = styles.stroke
@ctx.lineWidth = styles['stroke-width'].replace('px', '') if styles['stroke-width']?
# Draws the gauge.
draw: ->
[cx, cy, r] = [@centerX(), @centerY(), @radius()]
[tickOffset, tickSize] = [@options.tickOffset, @options.tickSize]
@clear()
# Draw Ticks
t = d3.scale.linear()
.domain([0, @options.ticks])
.range([ -(9/8)*Math.PI, Math.PI/8 ])
@setStyles '.epoch .gauge .tick'
@ctx.beginPath()
for i in [0..@options.ticks]
a = t(i)
[c, s] = [Math.cos(a), Math.sin(a)]
x1 = c * (r-tickOffset) + cx
y1 = s * (r-tickOffset) + cy
x2 = c * (r-tickOffset-tickSize) + cx
y2 = s * (r-tickOffset-tickSize) + cy
@ctx.moveTo x1, y1
@ctx.lineTo x2, y2
@ctx.stroke()
# Outer arc
@setStyles '.epoch .gauge .arc.outer'
@ctx.beginPath()
@ctx.arc cx, cy, r, -(9/8)*Math.PI, (1/8)*Math.PI, false
@ctx.stroke()
# Inner arc
@setStyles '.epoch .gauge .arc.inner'
@ctx.beginPath()
@ctx.arc cx, cy, r-10, -(9/8)*Math.PI, (1/8)*Math.PI, false
@ctx.stroke()
@drawNeedle()
super()
# Draws the needle.
drawNeedle: ->
[cx, cy, r] = [@centerX(), @centerY(), @radius()]
ratio = @value / @options.domain[1]
@setStyles '.epoch .gauge .needle'
@ctx.beginPath()
@ctx.save()
@ctx.translate cx, cy
@ctx.rotate @getAngle(@value)
@ctx.moveTo 4 * @pixelRatio, 0
@ctx.lineTo -4 * @pixelRatio, 0
@ctx.lineTo -1 * @pixelRatio, 19-r
@ctx.lineTo 1, 19-r
@ctx.fill()
@setStyles '.epoch .gauge .needle-base'
@ctx.beginPath()
@ctx.arc 0, 0, (@getWidth() / 25), 0, 2*Math.PI
@ctx.fill()
@ctx.restore()
# Correctly responds to an option:
domainChanged: -> @draw()
# Correctly responds to an option:
ticksChanged: -> @draw()
# Correctly responds to an option:
tickSizeChanged: -> @draw()
# Correctly responds to an option:
tickOffsetChanged: -> @draw()
# Correctly responds to an option:
formatChanged: -> @svg.select('text.value').text(@options.format(@value))
# "The mother of a million sons... CIVILIZATION!" -- Justice