summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/epoch/src/time/gauge.coffee
blob: 8efadb2ba613fd48bc01c55133a19319734dc1fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# 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 <code>update()</code> 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 <code>option:</code>
  domainChanged: -> @draw()

  # Correctly responds to an <code>option:</code>
  ticksChanged: -> @draw()

  # Correctly responds to an <code>option:</code>
  tickSizeChanged: -> @draw()

  # Correctly responds to an <code>option:</code>
  tickOffsetChanged: -> @draw()

  # Correctly responds to an <code>option:</code>
  formatChanged: -> @svg.select('text.value').text(@options.format(@value))



# "The mother of a million sons... CIVILIZATION!" -- Justice