Blame view

web/static/js/swapp.ls 27.8 KB
f1f1e797   Goutte   Rewrite ugly java...
1
# Livescript transpiles to javascript, and is easier on the eyes and brain.
4cf497e0   Goutte   Make the targets ...
2
# Get the `lsc` binary from here : http://livescript.net
8cb213b9   Goutte   Clean up, and pre...
3
# It is quite close to Python, syntax-wise, and full of sugar.
f1f1e797   Goutte   Rewrite ugly java...
4

4cf497e0   Goutte   Make the targets ...
5
# To transpile this file to javascript, and generate `swapp.js` :
a21f81d9   Goutte   Enable Venus and ...
6
# $ lsc --compile swapp.ls
4cf497e0   Goutte   Make the targets ...
7
8
# You can have it watch for changes and transpile automatically :
# $ lsc --watch --compile swapp.ls
a21f81d9   Goutte   Enable Venus and ...
9

4cf497e0   Goutte   Make the targets ...
10
11
12
# All the "javascript" code is in this file, except for inline scripts in
# templates, such as `home.html.jinja2`.

8cb213b9   Goutte   Clean up, and pre...
13
# Note: We use Promises and ES6 whenever relevant.
9c0c4509   Goutte   Add a loader to t...
14
15
16
17
# You also will need d3js v4 documentation : https://d3js.org/
# We're using a custom build of 4.9.1, one line changed, see d3-custom.js
# Event bubbling cannot trigger two rects unless we make an event dispatcher,
# and d3's brush is stopping propagation, as it should by default.
b60e7acd   Goutte   Rename "source" i...
18

4cf497e0   Goutte   Make the targets ...
19
20
21
###############################################################################

const GOLDEN_RATIO = 2 / (1 + Math.sqrt(5))  # Between 0 and 1 (0.618...)
f1f1e797   Goutte   Rewrite ugly java...
22

ae0aa7d2   Goutte   Add an x axis lab...
23
24
###############################################################################

b60e7acd   Goutte   Rename "source" i...
25
26
class Target
  (@slug, @name, @config) ->
2c0e1515   Goutte   Refactor loading ...
27
    @active = @config.active
b60e7acd   Goutte   Rename "source" i...
28
29
30

###############################################################################

ae0aa7d2   Goutte   Add an x axis lab...
31
export class SpaceWeather
4cf497e0   Goutte   Make the targets ...
32
33
  """
  The main app, instanciated from an inline script.
b60e7acd   Goutte   Rename "source" i...
34
  It defaults to an interval starting a year ago, and ending in seven days.
6bb225d6   Goutte   Link the time ser...
35
  (both at midnight)
4cf497e0   Goutte   Make the targets ...
36
  """
ae0aa7d2   Goutte   Add an x axis lab...
37

2c0e1515   Goutte   Refactor loading ...
38
  API_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss"
243cd8a4   Goutte   Timestamp party c...
39
  INPUT_TIME_FORMAT = "YYYY-MM-DD"
2c0e1515   Goutte   Refactor loading ...
40

f75faf5f   Goutte   WIP
41
  (@configuration) ->
05e269d1   Goutte   Show an error mes...
42
    console.info """©2017
2038c9fb   Goutte   Add a zoom reset ...
43
44
45
46
47
48
49
50
51
52
53
54
55
  _   _      _ _       ____
 | | | | ___| (_) ___ |  _ \\ _ __ ___  _ __   __ _
 | |_| |/ _ \\ | |/ _ \\| |_) | '__/ _ \\| '_ \\ / _` |
 |  _  |  __/ | | (_) |  __/| | | (_) | |_) | (_| |
 |_| |_|\\___|_|_|\\___/|_|_  |_|_ \\___/| .__/ \\__,_|
 | |__  _   _   / ___|  _ \\|  _ \\|  _ \\_|
 | '_ \\| | | | | |   | | | | |_) | |_) |
 | |_) | |_| | | |___| |_| |  __/|  __/
 |_.__/ \\__, |  \\____|____/|_|   |_|
        |___/

The full source of this website is available at :
https://gitlab.irap.omp.eu/CDPP/SPACEWEATHERONLINE
6bb225d6   Goutte   Link the time ser...
56
"""  # HelioPropa by CDPP (mushed 'cause we need to escape backslashes)
b60e7acd   Goutte   Rename "source" i...
57
58
59
60
    @targets = {}
    configs = [@configuration.targets[k] for k of @configuration.targets]
    configs.forEach((target_config) ~>
      @targets[target_config.slug] = new Target(target_config.slug, target_config.name, target_config)
fe3132dd   Goutte   Refactor even more.
61
    )
b7fe650c   Goutte   Misc bundle of ol...
62
63
64
65
    @parameters = {}
    @configuration['parameters'].forEach((p) ~>
        @parameters[p['id']] = p
    )
80352490   Goutte   Multi model suppo...
66
67
    @orbiter = null    # our Orbiter defined below
    @time_series = []  # a List of TimeSeries objects
ae0aa7d2   Goutte   Add an x axis lab...
68

b60e7acd   Goutte   Rename "source" i...
69
70
71
72
73
74
  init: ->
    """
    This is called by the inline bootstrap javascript code.
    This ain't in the constructor because it might return a Promise later on.
    (for the loader, for example)
    """
97d6cb96   Goutte   Change default ti...
75
    # Default time interval is from two weeks ago to one week ahead.
2c0e1515   Goutte   Refactor loading ...
76
    # We set the h/m/s to zero to benefit from a daily cache.
6491a1f1   Goutte   Fix up the bugs l...
77
    started_at = moment().subtract(6, 'month').hours(0).minutes(0).seconds(0)
97d6cb96   Goutte   Change default ti...
78
    stopped_at = moment().add(1, 'week').hours(0).minutes(0).seconds(0)
7994cf1a   Goutte   Hunt bugs.
79
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
80
    @loadAndCreatePlots(started_at, stopped_at)
243cd8a4   Goutte   Timestamp party c...
81

b60e7acd   Goutte   Rename "source" i...
82
83
84
    window.addEventListener 'resize', ~> @resize()

  buildDataUrlForTarget: (target_slug, started_at, stopped_at) ->
f75faf5f   Goutte   WIP
85
    url = @configuration['api']['data_for_interval']
b60e7acd   Goutte   Rename "source" i...
86
    url = url.replace('<target>', target_slug)
a4a9ef03   Goutte   Cache generated C...
87
88
    url = url.replace('<started_at>', started_at)
    url = url.replace('<stopped_at>', stopped_at)
f75faf5f   Goutte   WIP
89
    url
ae0aa7d2   Goutte   Add an x axis lab...
90

6b149919   Goutte   Add a Download bu...
91
92
93
94
95
96
97
98
99
  buildDownloadUrl: ->
    [started_at, stopped_at] = @getDomain()
    targets = [t for t of @targets when @targets[t].active].sort().join('-')
    url = @configuration['api']['download']
    url = url.replace('<targets>', targets)
    url = url.replace('<started_at>', started_at.format(API_TIME_FORMAT))
    url = url.replace('<stopped_at>', stopped_at.format(API_TIME_FORMAT))
    url

b60e7acd   Goutte   Rename "source" i...
100
101
  addTarget: (target) ->
    @targets[target.slug] = target
f75faf5f   Goutte   WIP
102
103
    this

2c0e1515   Goutte   Refactor loading ...
104
105
106
107
#  enableAllTargets: ->
#    for slug, target of @targets
#      enableTarget(slug)
#    this
f75faf5f   Goutte   WIP
108

6bb225d6   Goutte   Link the time ser...
109
  enableTarget: (target_slug) ->
80352490   Goutte   Multi model suppo...
110
    @time_series.forEach((ts) ~> ts.show() if ts.target.slug == target_slug && @parameters[ts.parameter].active)
b60e7acd   Goutte   Rename "source" i...
111
    @targets[target_slug].active = true
f75faf5f   Goutte   WIP
112
113
    this

6bb225d6   Goutte   Link the time ser...
114
  disableTarget: (target_slug) ->
80352490   Goutte   Multi model suppo...
115
    @time_series.forEach((ts) -> ts.hide() if ts.target.slug == target_slug)
b60e7acd   Goutte   Rename "source" i...
116
    @targets[target_slug].active = false
f75faf5f   Goutte   WIP
117
118
    this

fe3132dd   Goutte   Refactor even more.
119
  resize: ->
a21f81d9   Goutte   Enable Venus and ...
120
    @orbits?.resize();
80352490   Goutte   Multi model suppo...
121
    @time_series.forEach((ts) -> ts.resize())
fe3132dd   Goutte   Refactor even more.
122

2c0e1515   Goutte   Refactor loading ...
123
  showLoader: ->
05e269d1   Goutte   Show an error mes...
124
    $('#plots_loader').show();
2c0e1515   Goutte   Refactor loading ...
125

9c0c4509   Goutte   Add a loader to t...
126
  hideLoader: ->
05e269d1   Goutte   Show an error mes...
127
    $('#plots_loader').hide();
9c0c4509   Goutte   Add a loader to t...
128

b60e7acd   Goutte   Rename "source" i...
129
130
131
132
133
  loadData: (target_slug, started_at, stopped_at) ->
    """
    Load the data as CSV for the specified target and interval,
    and return it in a Promise.
    """
243cd8a4   Goutte   Timestamp party c...
134

f75faf5f   Goutte   WIP
135
    sw = this
2c0e1515   Goutte   Refactor loading ...
136
    new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
137
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
138
      d3.csv(url, (csv) ->
9bfa6c42   Goutte   More bug hunting.
139
        console.debug("Requested CSV for #{target_slug}...", csv)
f75faf5f   Goutte   WIP
140
        timeFormat = d3.timeParse('%Y-%m-%dT%H:%M:%S%Z')
6491a1f1   Goutte   Fix up the bugs l...
141
        data = { 'hee': [] }
f75faf5f   Goutte   WIP
142
        configuration['parameters'].forEach((parameter) ->
06a3a1f1   Goutte   Continue bug hunt...
143
          data[parameter['id']] = []
f75faf5f   Goutte   WIP
144
        )
80352490   Goutte   Multi model suppo...
145
146
        unless csv then reject 'invalid'
        unless csv.length then reject 'empty'
f75faf5f   Goutte   WIP
147
148
149
150
151
152
        csv.forEach((d) ->
          dtime = timeFormat(d['time'])
          configuration['parameters'].forEach((parameter) ->
            id = parameter['id']
            data[id].push({x: dtime, y: parseFloat(d[id])})
          )
6491a1f1   Goutte   Fix up the bugs l...
153
154
155
          if d['xhee'] and d['yhee']
            data['hee'].push({
              t: dtime, x: parseFloat(d['xhee']), y: parseFloat(d['yhee'])
06a3a1f1   Goutte   Continue bug hunt...
156
            })
f75faf5f   Goutte   WIP
157
        )
9bfa6c42   Goutte   More bug hunting.
158
        resolve data
f75faf5f   Goutte   WIP
159
160
      )
    )
2c0e1515   Goutte   Refactor loading ...
161
162

  loadAndCreatePlots: (started_at, stopped_at) ->
243cd8a4   Goutte   Timestamp party c...
163
    """
80352490   Goutte   Multi model suppo...
164
165
    started_at: moment(.js) datetime object
    stopped_at: moment(.js) datetime object
243cd8a4   Goutte   Timestamp party c...
166
    """
2c0e1515   Goutte   Refactor loading ...
167
    @showLoader()
7994cf1a   Goutte   Hunt bugs.
168
169
170
    @started_at = started_at
    @stopped_at = stopped_at
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
243cd8a4   Goutte   Timestamp party c...
171
172
    started_at = started_at.format(API_TIME_FORMAT)
    stopped_at = stopped_at.format(API_TIME_FORMAT)
5e099488   Goutte   Fix that loading ...
173
174
    # active_targets = [@targets[k] for k of @targets when @targets[k].active]
    [@targets[k] for k of @targets].forEach((target) ~>
2c0e1515   Goutte   Refactor loading ...
175
      console.info "Loading CSV data of #{target.name}…"
4900d232   Goutte   Add the time inte...
176
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
177
      targetButton.addClass('loading')
6491a1f1   Goutte   Fix up the bugs l...
178
      targetButton.removeClass('failed empty')
2c0e1515   Goutte   Refactor loading ...
179
180
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
9bfa6c42   Goutte   More bug hunting.
181
          console.info "Loaded CSV data of #{target.name}.", data
2c0e1515   Goutte   Refactor loading ...
182
          @createTimeSeries(target, data)
6491a1f1   Goutte   Fix up the bugs l...
183
          @orbits.initOrbiter(target.slug, target.config, data['hee'])
2c0e1515   Goutte   Refactor loading ...
184
          targetButton.removeClass('loading')
5e099488   Goutte   Fix that loading ...
185
          if target.active then @hideLoader() else @disableTarget(target.slug)
2c0e1515   Goutte   Refactor loading ...
186
        ,
9bfa6c42   Goutte   More bug hunting.
187
        (error) ~>
80352490   Goutte   Multi model suppo...
188
189
190
191
192
193
194
195
196
197
198
199
200
201
          switch error
            case 'invalid'
              console.error("Failed loading CSV data of #{target.name}.")
              # Sometimes, AMDA's API returns garbage, so the CSV sometime fails
              # But when we re-generate it a second time, usually it's okay.
              alert("There was an error with #{target.name}.\nPlease retry in a few moments.")
              @is_invalid = true
              break
            case 'empty'
              msg = "No data for #{target.name}\n during interval from \n#{started_at} to #{stopped_at}."
              console.warn(msg)
              targetButton.addClass('empty')
#              alert(msg)
              break
27097d87   Goutte   Change the error ...
202
          targetButton.addClass('failed')
9bfa6c42   Goutte   More bug hunting.
203
204
          targetButton.removeClass('loading')
          @hideLoader()
80352490   Goutte   Multi model suppo...
205
              
2c0e1515   Goutte   Refactor loading ...
206
207
208
209
210
      )
    )

  clearPlots: ->
    @orbits.clear()
80352490   Goutte   Multi model suppo...
211
    @time_series.forEach((ts) -> ts.clear())
2c0e1515   Goutte   Refactor loading ...
212
    @orbits = null
80352490   Goutte   Multi model suppo...
213
    @time_series = []  # do we de-reference everything ? listeners ? #memleak?
2c0e1515   Goutte   Refactor loading ...
214
    this
ae0aa7d2   Goutte   Add an x axis lab...
215

b60e7acd   Goutte   Rename "source" i...
216
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
217
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
218
219
220
      container = @configuration['time_series_container']
      id = parameter['id'] ; title = parameter['title']
      if id not of data then console.error("No data for id '#{id}'.", data)
80352490   Goutte   Multi model suppo...
221
      @time_series.push(new TimeSeries(
c3008fb2   Goutte   Clean up and refa...
222
        id, title, target, data[id], @parameters[id].active, container
6bb225d6   Goutte   Link the time ser...
223
      ))
4816cef4   Goutte   Refactor some more.
224
    )
80352490   Goutte   Multi model suppo...
225
    @time_series.forEach((ts) ~>  # returning true may be faster
243cd8a4   Goutte   Timestamp party c...
226
      ts.options['onMouseOver'] = ~>
80352490   Goutte   Multi model suppo...
227
        @time_series.forEach((ts2) -> ts2.showCursor()) ; true
243cd8a4   Goutte   Timestamp party c...
228
      ts.options['onMouseOut'] = ~>
80352490   Goutte   Multi model suppo...
229
        @time_series.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
230
      ts.options['onMouseMove'] = (t) ~>
80352490   Goutte   Multi model suppo...
231
        @time_series.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
232
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
233
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
234
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
235
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
236
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
237
    )
80352490   Goutte   Multi model suppo...
238
    @time_series
4816cef4   Goutte   Refactor some more.
239

b7fe650c   Goutte   Misc bundle of ol...
240
241
  enableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
242
    @parameters[parameter_slug].active = true
80352490   Goutte   Multi model suppo...
243
    @time_series.forEach((ts) ~> ts.show() if ts.parameter == parameter_slug && @targets[ts.target.slug].active)
b7fe650c   Goutte   Misc bundle of ol...
244
    this
4816cef4   Goutte   Refactor some more.
245

b7fe650c   Goutte   Misc bundle of ol...
246
247
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
248
    @parameters[parameter_slug].active = false
80352490   Goutte   Multi model suppo...
249
    @time_series.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
250
    this
ae0aa7d2   Goutte   Add an x axis lab...
251

a06a0a67   Goutte   Prepare the time ...
252
253
254
255
256
  getDomain: ->
    if @current_started_at? and @current_stopped_at?
      return [@current_started_at, @current_stopped_at]
    return [@started_at, @stopped_at]

8cb213b9   Goutte   Clean up, and pre...
257
258
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
259
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
260
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
261
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
262
      return
1754789b   Goutte   Decorate and clea...
263
264
265
266
    max_stopped_at = started_at.clone().add(2, 'years')
    if stopped_at > max_stopped_at
      console.warn "The time interval was truncated beacuse it was bigger than two years."
      stopped_at = max_stopped_at
8cb213b9   Goutte   Clean up, and pre...
267

243cd8a4   Goutte   Timestamp party c...
268
269
270
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
271

80352490   Goutte   Multi model suppo...
272
273
    if (not @is_invalid) and
       (@started_at <= started_at <= @stopped_at) and
6bb225d6   Goutte   Link the time ser...
274
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
275
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
276
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
277
      # the visible ones, for a smoother transition.
80352490   Goutte   Multi model suppo...
278
279
      @time_series.forEach((ts) -> if not ts.visible then ts.zoomIn(started_at, stopped_at))
      @time_series.forEach((ts) -> if     ts.visible then ts.zoomIn(started_at, stopped_at))
8cb213b9   Goutte   Clean up, and pre...
280
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
281
    else
80352490   Goutte   Multi model suppo...
282
      @is_invalid = false
243cd8a4   Goutte   Timestamp party c...
283
284
285
286
287
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
      console.warn "This might take a while… Why not see what else we're up to on http://cdpp.eu while you're waiting?"
      # fetch new data and remake the plots
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
288

243cd8a4   Goutte   Timestamp party c...
289
    this
8cb213b9   Goutte   Clean up, and pre...
290

6bb225d6   Goutte   Link the time ser...
291
  resetZoom: ->
80352490   Goutte   Multi model suppo...
292
    @time_series.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
293
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
294
295
296
297
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
298
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
299
300
301
302
303
    @current_started_at = started_at
    @current_stopped_at = stopped_at
    $("\#started_at").val(started_at.format(INPUT_TIME_FORMAT))
    $("\#stopped_at").val(stopped_at.format(INPUT_TIME_FORMAT))
    this
8cb213b9   Goutte   Clean up, and pre...
304
305


ae0aa7d2   Goutte   Add an x axis lab...
306

ae0aa7d2   Goutte   Add an x axis lab...
307
308
309
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
310

f1f1e797   Goutte   Rewrite ugly java...
311
312
313
314
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

c3008fb2   Goutte   Clean up and refa...
315
  (@parameter, @title, @target, data, @visible, @container, @options = {}) ->
4cf497e0   Goutte   Make the targets ...
316
    # parameter : slug of the parameter to observe, like magn or pdyn
c3008fb2   Goutte   Clean up and refa...
317
318
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
319
    # data : list of {x: <datetime>, y: <float>}
6bb225d6   Goutte   Link the time ser...
320
    @setData(data)
f1f1e797   Goutte   Rewrite ugly java...
321
322
    @init()

2038c9fb   Goutte   Add a zoom reset ...
323
324
  toString: -> "#{@title} of #{@target.name}"

6bb225d6   Goutte   Link the time ser...
325
  setData: (data) ->
c3008fb2   Goutte   Clean up and refa...
326
    @data = data  # and pre-compute extents for performance when zooming
6bb225d6   Goutte   Link the time ser...
327
328
329
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)

f1f1e797   Goutte   Rewrite ugly java...
330
  init: ->
c3008fb2   Goutte   Clean up and refa...
331
    console.info "Initializing plot of #{@}…"
f1f1e797   Goutte   Rewrite ugly java...
332
333
334
335
336

    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
337
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
338
339
    }

6bb225d6   Goutte   Link the time ser...
340
341
    @xScale = d3.scaleTime().domain(@xDataExtent)
    @yScale = d3.scaleLinear().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
342

6491a1f1   Goutte   Fix up the bugs l...
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
    formatMillisecond = d3.timeFormat(".%L")
    formatSecond = d3.timeFormat(":%S")
    formatMinute = d3.timeFormat("%I:%M")
    formatHour = d3.timeFormat("%I:%M")
    formatDay = d3.timeFormat("%a %d")
    formatWeek = d3.timeFormat("%b %d")
    formatMonth = d3.timeFormat("%B")
    formatYear = d3.timeFormat("%Y")

    multiFormat = (date) ->
      if date > d3.timeSecond(date) then return formatMillisecond(date)
      if date > d3.timeMinute(date) then return formatSecond(date)
      if date > d3.timeHour(date)   then return formatMinute(date)
      if date > d3.timeDay(date)    then return formatHour(date)
      if date > d3.timeMonth(date)
        if date > d3.timeWeek(date) then return formatDay(date)
        else return formatWeek(date)
      if date > d3.timeYear(date)   then return formatMonth(date)
      return formatYear(date)

f1f1e797   Goutte   Rewrite ugly java...
363
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
364
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
365
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
366
367
368
369
370
371
372
373
    @yAxis = d3.axisLeft()
               .ticks(10)

    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

    @svg = d3.select(@container).append('svg')
b60e7acd   Goutte   Rename "source" i...
374
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
375

f1f1e797   Goutte   Rewrite ugly java...
376
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
377
378
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
379

123313cb   Goutte   Clip the paths of...
380
381
382
383
384
385
386
387
388
389
    clipId = "ts-clip-#{@parameter}-#{@target.slug}"
    @clip = @svg.append("defs").append("svg:clipPath")
                .attr("id", clipId)
                .append("svg:rect")
                .attr("x", 0)
                .attr("y", 0)

    @pathWrapper = @plotWrapper.append('g')
    @pathWrapper.attr("clip-path", "url(\##{clipId})")
    @path = @pathWrapper.append('path')
f1f1e797   Goutte   Rewrite ugly java...
390
391
392
                        .datum(@data)
                        .classed('line', true)

08569a6b   Goutte   Add a zooming bru...
393
    @brush = @plotWrapper.append("g")
2038c9fb   Goutte   Add a zoom reset ...
394
                         .attr("class", "brush")
08569a6b   Goutte   Add a zooming bru...
395

2038c9fb   Goutte   Add a zoom reset ...
396
    # deprecated, use brush's 'overlay' child
2463bd16   Goutte   Add a circle foll...
397
398
    @mouseCanvas = @plotWrapper.append("rect")
                               .style("fill", "none")
3ee0b596   Goutte   Fix an annoying b...
399

f1f1e797   Goutte   Rewrite ugly java...
400
401
402
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
403
404
405
406
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
407
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
408
409
410
411
412
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
413

81c9b2e8   Goutte   Add the values to...
414
415
416
    @focus = @plotWrapper.append('g').style("display", "none")

    @cursorCircle = @focus.append("circle")
123313cb   Goutte   Clip the paths of...
417
418
                          .attr("class", "cursor-circle")
                          .attr("r", 3)
81c9b2e8   Goutte   Add the values to...
419

541e2936   Goutte   Synchronize the t...
420
    dx = 8
81c9b2e8   Goutte   Add the values to...
421
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
422
423
424
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
425

81c9b2e8   Goutte   Add the values to...
426
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
427
428
429
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
430

81c9b2e8   Goutte   Add the values to...
431
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
432
433
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
434
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
435

81c9b2e8   Goutte   Add the values to...
436
437
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
438
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
439
440
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
441
442
    @resize()

123313cb   Goutte   Clip the paths of...
443
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
444
  resize: ->
123313cb   Goutte   Clip the paths of...
445
446
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
447
448
449

    @plotWidth = width
    @plotHeight = height
f1f1e797   Goutte   Rewrite ugly java...
450

123313cb   Goutte   Clip the paths of...
451
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
452
453
454
455
456
457
458

    @xScale.range([0, width]);
    @yScale.range([height, 0]);

    @svg.attr('width',  width + @margin.right + @margin.left)
        .attr('height', height + @margin.top + @margin.bottom)

123313cb   Goutte   Clip the paths of...
459
460
461
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
462
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
463
464
465
466

    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
467
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
468
469
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
470
471
472
473
474
475
476
477

    @svg.select('.x.axis')
        .attr('transform', 'translate(0,' + height + ')')
        .call(@xAxis)

    @svg.select('.y.axis')
        .call(@yAxis)

fe3132dd   Goutte   Refactor even more.
478
    @yAxisText.attr("y", 20 - @margin.left)
f1f1e797   Goutte   Rewrite ugly java...
479
480
              .attr("x", 0 - (height / 2))

b60e7acd   Goutte   Rename "source" i...
481
    @yAxisTextTarget.attr("y", 0 - @margin.left)
fe3132dd   Goutte   Refactor even more.
482
483
                    .attr("x", 0 - (height / 2))

2463bd16   Goutte   Add a circle foll...
484
485
486
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
487
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
488
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
489
490
491
492
493
494
495
496
497
498
      # looks like d3.brush handles its own resizing on window.resize
      @brushFunction =
          d3.brushX()
            .extent([[0, 0], [width, height]])
            .handleSize(0)
            .on("end", @onBrushEnd)
#            .on("start", @onBrushStart)
#            .on("move", @onBrushMove)
      @brush.call(@brushFunction)

2038c9fb   Goutte   Add a zoom reset ...
499
500
      # We're also adding our own cursor events to the brush's overlay,
      # because it captures events and a rect cannot contain another.
08569a6b   Goutte   Add a zooming bru...
501
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
502
503
504
505
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
506

6bb225d6   Goutte   Link the time ser...
507
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
508
509
    this

2c0e1515   Goutte   Refactor loading ...
510
511
512
513
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
514
515
516
517
518
519
520
  show: ->
    $(@svg.node()).show()
    @visible = true

  hide: ->
    $(@svg.node()).hide()
    @visible = false
8cb213b9   Goutte   Clean up, and pre...
521

541e2936   Goutte   Synchronize the t...
522
523
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
524
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
525
526

  onMouseOver: ~>
c3008fb2   Goutte   Clean up and refa...
527
    if @options.onMouseOver? then @options.onMouseOver() else @showCursor()
541e2936   Goutte   Synchronize the t...
528
529

  onMouseOut: ~>
c3008fb2   Goutte   Clean up and refa...
530
    if @options.onMouseOut? then @options.onMouseOut() else @hideCursor()
541e2936   Goutte   Synchronize the t...
531

2038c9fb   Goutte   Add a zoom reset ...
532
  onDoubleClick: ~>
c3008fb2   Goutte   Clean up and refa...
533
    if @options.onDblClick? then @options.onDblClick() else @resetZoom()
2038c9fb   Goutte   Add a zoom reset ...
534

08569a6b   Goutte   Add a zooming bru...
535
536
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
537
538
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
539
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
540
541
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
542
543
544

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
545
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
546
547
548
549
550
551
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
552
553
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
554
555
556
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
557
    if @visible
c3008fb2   Goutte   Clean up and refa...
558
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
559
560
561
562
563
      t = @svg.transition().duration(750)
      @svg.select('.x.axis').transition(t).call(@xAxis);
      @svg.select('.y.axis').transition(t).call(@yAxis);
      @path.transition(t).attr('d', @line)
    else
c3008fb2   Goutte   Clean up and refa...
564
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
565
566
567
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
568

541e2936   Goutte   Synchronize the t...
569
570
571
572
573
574
  showCursor: ->
    @focus.style("display", null)

  hideCursor: ->
    @focus.style("display", "none")

8cb213b9   Goutte   Clean up, and pre...
575
  bisectDate: d3.bisector((d) -> d.x).left  # /!\ complex
6491a1f1   Goutte   Fix up the bugs l...
576
  timeFormat: d3.timeFormat("%Y-%m-%d %H:%M")
2463bd16   Goutte   Add a circle foll...
577

541e2936   Goutte   Synchronize the t...
578
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
579
580
581
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
582
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
583
584
585
586
    d = if x0 - d0.x > d1.x - x0 then d1 else d0
    xx = @xScale(d.x)
    yy = @yScale(d.y)

541e2936   Goutte   Synchronize the t...
587
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
588

8cb213b9   Goutte   Clean up, and pre...
589
    dx = 8  # horizontal delta between the dot and the text
541e2936   Goutte   Synchronize the t...
590
591
    dx = -1 * dx if mirrored

8cb213b9   Goutte   Clean up, and pre...
592
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
593
594
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
595
596
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
597
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
598
599
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
600
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
601
602
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
603
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
604
605
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
606
607

    this
f1f1e797   Goutte   Rewrite ugly java...
608

ae0aa7d2   Goutte   Add an x axis lab...
609
610
611
612

###############################################################################
###############################################################################

f1f1e797   Goutte   Rewrite ugly java...
613
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
614
615
616
617
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
618

a21f81d9   Goutte   Enable Venus and ...
619
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
620
621
622
    @init()

  init: ->
c3008fb2   Goutte   Clean up and refa...
623
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
624
625
626
627

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
628
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
629
630
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
631

6491a1f1   Goutte   Fix up the bugs l...
632
    @data = {}  # slug => HEE array
a21f81d9   Goutte   Enable Venus and ...
633
    @orbiters = {}  # slug => config
7994cf1a   Goutte   Hunt bugs.
634
    @orbitersElements = {}
a21f81d9   Goutte   Enable Venus and ...
635
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
636
637
638
639
640
641
    @xScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])
    @yScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])

    @xAxis = d3.axisBottom().ticks(10)
    @yAxis = d3.axisLeft().ticks(10)

f1f1e797   Goutte   Rewrite ugly java...
642
643
644
645
646
    @svg = d3.select(@container).append('svg')

    @plotWrapper = @svg.append('g')
    @plotWrapper.attr('transform', 'translate(' + @margin.left + ',' + @margin.top + ')')

ae0aa7d2   Goutte   Add an x axis lab...
647
648
649
650
651
652
653
654
655
    @xAxisLine = @plotWrapper.append('g').classed('x axis', true)
    @yAxisLine = @plotWrapper.append('g').classed('y axis', true)

    @xAxisTitle = @xAxisLine.append('text').attr('fill', '#000')
    @xAxisTitle.style("text-anchor", "middle")
    @xAxisTitle.append('tspan').text('X')
    # No : https://bugzilla.mozilla.org/show_bug.cgi?id=308338
    # @xAxisTitle.append('tspan').attr('baseline-shift', 'sub').text('HEE')
    # Also, don't use em as dy units
11662eed   Goutte   Add Y axis label ...
656
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
657
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
658

11662eed   Goutte   Add Y axis label ...
659
660
661
662
663
664
665
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
    @yAxisTitle.append('tspan').text('Y')
    @yAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
    @yAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
    @yAxisTitle.attr('transform', 'rotate(-90)')

8bd715ad   Goutte   Use a pixel art i...
666
667
668
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
669
    @sun.append('svg:title').text("Sun")
f1f1e797   Goutte   Rewrite ugly java...
670

3ee0b596   Goutte   Fix an annoying b...
671
    $(@svg.node()).hide();  # we'll show it later when there'll be data
f1f1e797   Goutte   Rewrite ugly java...
672
673
    @resize()

a21f81d9   Goutte   Enable Venus and ...
674
  initOrbiter: (slug, config, data) ->
ebe77ce4   Goutte   Clean up.
675
    console.info "Initializing orbit of #{config.name}…"
438929a4   Goutte   Rewrite the orbit...
676
677
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

a21f81d9   Goutte   Enable Venus and ...
678
679
680
681
682
683
    @extremum = Math.max(@extremum, 1.11 * d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    ))
    @xScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])
    @yScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])

438929a4   Goutte   Rewrite the orbit...
684
685
686
687
688
689
    # The order is important, as it will define the default z-order
    orbit_ellipse = @plotWrapper.append("svg:ellipse")
                                .classed('orbit orbit_ellipse', true)
    orbiter = @plotWrapper.append("svg:image")
                          .attr('xlink:href', config['img'])
                          .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
690
    orbiter.append('svg:title').text(config.name)
438929a4   Goutte   Rewrite the orbit...
691
692
693
694
695
696

    orbit_line = d3.line()
                   .x((d) ~> @xScale(d.x))
                   .y((d) ~> @yScale(d.y))

    orbit_section = @plotWrapper.append('path')
a21f81d9   Goutte   Enable Venus and ...
697
                                .datum(data)
438929a4   Goutte   Rewrite the orbit...
698
699
                                .classed('orbit orbit_section', true)

a21f81d9   Goutte   Enable Venus and ...
700
701
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
702
703
704
705
706
707
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
708
709
    @resize()

3ee0b596   Goutte   Fix an annoying b...
710
    $(@svg.node()).show();
8cb213b9   Goutte   Clean up, and pre...
711

438929a4   Goutte   Rewrite the orbit...
712
713
    this

2c0e1515   Goutte   Refactor loading ...
714
715
716
  clear: ->
    $(@svg.node()).remove()

f1f1e797   Goutte   Rewrite ugly java...
717
  resize: ->
abcf4c94   Goutte   Clean up.
718
719
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
720

abcf4c94   Goutte   Clean up.
721
    console.debug("Resizing orbits : #{width} × #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
722
723
724
725
726
727
728

    @xScale.range([0, width]);
    @yScale.range([height, 0]);

    @svg.attr('width',  width + @margin.right + @margin.left)
        .attr('height', height + @margin.top + @margin.bottom)

8bd715ad   Goutte   Use a pixel art i...
729
    @sun.attr("x", width / 2 - 16).attr("y", height / 2 - 16)
f1f1e797   Goutte   Rewrite ugly java...
730

438929a4   Goutte   Rewrite the orbit...
731
    for slug, config of @orbiters
abcf4c94   Goutte   Clean up.
732
      @resizeOrbiter(slug, config, width, height)
438929a4   Goutte   Rewrite the orbit...
733

f1f1e797   Goutte   Rewrite ugly java...
734
735
736
737
738
739
740
741
742
743
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

    @svg.select('.x.axis')
        .attr('transform', 'translate(0,' + height + ')')
        .call(@xAxis)

    @svg.select('.y.axis')
        .call(@yAxis)

ae0aa7d2   Goutte   Add an x axis lab...
744
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
745
746
747
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
748

f1f1e797   Goutte   Rewrite ugly java...
749
750
    this

abcf4c94   Goutte   Clean up.
751
752
  resizeOrbiter: (slug, config, width, height) ->
    console.debug("Resizing orbit of #{slug}…")
438929a4   Goutte   Rewrite the orbit...
753
754
755
756
757
758
759
760
761
762
763
764
765

    el = @orbitersElements[slug]
    el['orbit_section'].attr('d', el['orbit_line'])

    a = config['orbit']['a']
    b = config['orbit']['b']
    c = Math.sqrt(a*a - b*b)
    cx = (width / 2) - c
    cy = (height / 2)
    @yScale.range([0, height])
    el['orbit_ellipse'].attr('cx', cx).attr('cy', cy)
        .attr('rx', @xScale(a) - @xScale(0))
        .attr('ry', @yScale(b) - @yScale(0))
a21f81d9   Goutte   Enable Venus and ...
766
#        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
438929a4   Goutte   Rewrite the orbit...
767
768
    @yScale.range([height, 0])

a21f81d9   Goutte   Enable Venus and ...
769
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
770
771
772
773
774
775

    el['orbiter'].attr('x', @xScale(data[data.length - 1].x) - 16)
    el['orbiter'].attr('y', @yScale(data[data.length - 1].y) - 16)

    this

ae0aa7d2   Goutte   Add an x axis lab...
776
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
777
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
778
779
780
781
    datum ?= data[data.length - 1]
    el = @orbitersElements[slug]
    el['orbiter'].attr('x', @xScale(datum.x) - 16)
    el['orbiter'].attr('y', @yScale(datum.y) - 16)
ae0aa7d2   Goutte   Add an x axis lab...
782
783
784
785
786
    this

  bisectDate: d3.bisector((d) -> d.t).left

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
787
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
788
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
789
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
790
791
792
793
794
      i = @bisectDate(data, t, 1)
      d0 = data[i - 1]
      d1 = data[i]
      continue unless d1 and d0
      d = if t - d0.t > d1.t - t then d1 else d0
abcf4c94   Goutte   Clean up.
795
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
796
    this
ae0aa7d2   Goutte   Add an x axis lab...
797

8cb213b9   Goutte   Clean up, and pre...
798
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
799
800
801
802
803
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
ae0aa7d2   Goutte   Add an x axis lab...
804

667eeb24   Goutte   Resize the domain...
805
806
807
808
809
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
abcf4c94   Goutte   Clean up.