summaryrefslogtreecommitdiffstats
path: root/debian/missing-sources/epoch/src/basic.coffee
blob: 8af3465bec60197212f622bc3c634a4b70861e2a (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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# Base class for all two-dimensional basic d3 charts. This class handles axes and
# margins so that subclasses can focus on the construction of particular chart
# types.
class Epoch.Chart.Plot extends Epoch.Chart.SVG
  defaults =
    domain: null,
    range: null,
    axes: ['left', 'bottom']
    ticks:
      top: 14
      bottom: 14
      left: 5
      right: 5
    tickFormats:
      top: Epoch.Formats.regular
      bottom: Epoch.Formats.regular
      left: Epoch.Formats.si
      right: Epoch.Formats.si

  defaultAxisMargins =
    top: 25
    right: 50
    bottom: 25
    left: 50

  optionListeners =
    'option:margins.top': 'marginsChanged'
    'option:margins.right': 'marginsChanged'
    'option:margins.bottom': 'marginsChanged'
    'option:margins.left': 'marginsChanged'
    'option:axes': 'axesChanged'
    'option:ticks.top': 'ticksChanged'
    'option:ticks.right': 'ticksChanged'
    'option:ticks.bottom': 'ticksChanged'
    'option:ticks.left': 'ticksChanged'
    'option:tickFormats.top': 'tickFormatsChanged'
    'option:tickFormats.right': 'tickFormatsChanged'
    'option:tickFormats.bottom': 'tickFormatsChanged'
    'option:tickFormats.left': 'tickFormatsChanged'
    'option:domain': 'domainChanged'
    'option:range': 'rangeChanged'

  # Creates a new plot chart.
  # @param [Object] options Options to use when constructing the plot.
  # @option options [Object] margins For setting explicit values for the top,
  #   right, bottom, and left margins in the visualization. Normally these can
  #   be omitted and the class will set appropriately sized margins given which
  #   axes are specified.
  # @option options [Array] axes A list of axes to display (top, left, bottom, right).
  # @option options [Object] ticks Number of ticks to place on the top, left bottom
  #   and right axes.
  # @option options [Object] tickFormats What tick formatting functions to use for
  #   the top, bottom, left, and right axes.
  constructor: (@options={}) ->
    givenMargins = Epoch.Util.copy(@options.margins) or {}
    super(@options = Epoch.Util.defaults(@options, defaults))

    # Margins are used in a special way and only for making room for axes.
    # However, a user may explicitly set margins in the options, so we need
    # to determine if they did so, and zero out the ones they didn't if no
    # axis is present.
    @margins = {}
    for pos in ['top', 'right', 'bottom', 'left']
      @margins[pos] = if @options.margins? and @options.margins[pos]?
        @options.margins[pos]
      else if @hasAxis(pos)
        defaultAxisMargins[pos]
      else
        6

    # Add a translation for the top and left margins
    @g = @svg.append("g")
      .attr("transform", "translate(#{@margins.left}, #{@margins.top})")

    # Register option change events
    @onAll optionListeners

  # Sets the tick formatting function to use on the given axis.
  # @param [String] axis Name of the axis.
  # @param [Function] fn Formatting function to use.
  setTickFormat: (axis, fn) ->
    @options.tickFormats[axis] = fn

  # @return [Boolean] <code>true</code> if the chart has an axis with a given name, <code>false</code> otherwise.
  # @param [String] axis Name of axis to check.
  hasAxis: (axis) ->
    @options.axes.indexOf(axis) > -1

  # @return [Number] Width of the visualization portion of the chart (width - margins).
  innerWidth: ->
    @width - (@margins.left + @margins.right)

  # @return [Number] Height of the visualization portion of the chart (height - margins).
  innerHeight: ->
    @height - (@margins.top + @margins.bottom)

  # @return [Function] The x scale for the visualization.
  x: ->
    domain = @options.domain ? @extent((d) -> d.x)
    d3.scale.linear()
      .domain(domain)
      .range([0, @innerWidth()])

  # @return [Function] The y scale for the visualization.
  y: (givenDomain) ->
    d3.scale.linear()
      .domain(@_getScaleDomain(givenDomain))
      .range([@innerHeight(), 0])

  # @return [Function] d3 axis to use for the bottom of the visualization.
  bottomAxis: ->
    d3.svg.axis().scale(@x()).orient('bottom')
      .ticks(@options.ticks.bottom)
      .tickFormat(@options.tickFormats.bottom)

  # @return [Function] d3 axis to use for the top of the visualization.
  topAxis: ->
    d3.svg.axis().scale(@x()).orient('top')
      .ticks(@options.ticks.top)
      .tickFormat(@options.tickFormats.top)

  # @return [Function] d3 axis to use on the left of the visualization.
  leftAxis: ->
    range = if @options.range then @options.range.left else null
    d3.svg.axis().scale(@y(range)).orient('left')
      .ticks(@options.ticks.left)
      .tickFormat(@options.tickFormats.left)

  # @return [Function] d3 axis to use on the right of the visualization.
  rightAxis: ->
    range = if @options.range then @options.range.right else null
    d3.svg.axis().scale(@y(range)).orient('right')
      .ticks(@options.ticks.right)
      .tickFormat(@options.tickFormats.right)

  # Renders the axes for the visualization (subclasses must implement specific
  # drawing routines).
  draw: ->
    if @_axesDrawn
      @_redrawAxes()
    else
      @_drawAxes()
    super()

  # Redraws the axes for the visualization.
  _redrawAxes: ->
    if @hasAxis('bottom')
      @g.selectAll('.x.axis.bottom').transition()
        .duration(500)
        .ease('linear')
        .call(@bottomAxis())
    if @hasAxis('top')
      @g.selectAll('.x.axis.top').transition()
        .duration(500)
        .ease('linear')
        .call(@topAxis())
    if @hasAxis('left')
      @g.selectAll('.y.axis.left').transition()
        .duration(500)
        .ease('linear')
        .call(@leftAxis())
    if @hasAxis('right')
      @g.selectAll('.y.axis.right').transition()
        .duration(500)
        .ease('linear')
        .call(@rightAxis())

  # Draws the initial axes for the visualization.
  _drawAxes: ->
    if @hasAxis('bottom')
      @g.append("g")
        .attr("class", "x axis bottom")
        .attr("transform", "translate(0, #{@innerHeight()})")
        .call(@bottomAxis())
    if @hasAxis('top')
      @g.append("g")
        .attr('class', 'x axis top')
        .call(@topAxis())
    if @hasAxis('left')
      @g.append("g")
        .attr("class", "y axis left")
        .call(@leftAxis())
    if @hasAxis('right')
      @g.append('g')
        .attr('class', 'y axis right')
        .attr('transform', "translate(#{@innerWidth()}, 0)")
        .call(@rightAxis())
    @_axesDrawn = true

  dimensionsChanged: ->
    super()
    @g.selectAll('.axis').remove()
    @_axesDrawn = false
    @draw()

  # Updates margins in response to a <code>option:margin.*</code> event.
  marginsChanged: ->
    return unless @options.margins?
    for own pos, size of @options.margins
      unless size?
        @margins[pos] = 6
      else
        @margins[pos] = size

    @g.transition()
      .duration(750)
      .attr("transform", "translate(#{@margins.left}, #{@margins.top})")

    @draw()

  # Updates axes in response to a <code>option:axes</code> event.
  axesChanged: ->
    # Remove default axis margins
    for pos in ['top', 'right', 'bottom', 'left']
      continue if @options.margins? and @options.margins[pos]?
      if @hasAxis(pos)
        @margins[pos] = defaultAxisMargins[pos]
      else
        @margins[pos] = 6

    # Update the margin offset
    @g.transition()
      .duration(750)
      .attr("transform", "translate(#{@margins.left}, #{@margins.top})")

    # Remove the axes and redraw
    @g.selectAll('.axis').remove()
    @_axesDrawn = false
    @draw()

  # Updates ticks in response to a <code>option:ticks.*</code> event.
  ticksChanged: -> @draw()

  # Updates tick formats in response to a <code>option:tickFormats.*</code> event.
  tickFormatsChanged: -> @draw()

  # Updates chart in response to a <code>option:domain</code> event.
  domainChanged: -> @draw()

  # Updates chart in response to a <code>option:range</code> event.
  rangeChanged: -> @draw()

# "They will see us waving from such great heights, come down now..." - The Postal Service