Blame view

web/static/js/swapp.ls 31 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.
cad44b6d   Goutte   Finish adding the...
14
# You also WILL NEED d3js v4 documentation : https://d3js.org/
9c0c4509   Goutte   Add a loader to t...
15
16
17
# 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
    @targets = {}
    configs = [@configuration.targets[k] for k of @configuration.targets]
cad44b6d   Goutte   Finish adding the...
59
60
61
    configs.forEach (target_config) ~>
      @addTarget(new Target(target_config.slug, target_config.name, target_config))

b7fe650c   Goutte   Misc bundle of ol...
62
    @parameters = {}
cad44b6d   Goutte   Finish adding the...
63
64
65
    @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.
cad44b6d   Goutte   Finish adding the...
77
    started_at = moment().subtract(1, 'year').hours(0).minutes(0).seconds(0)
1324cc91   Goutte   Make the footer i...
78
    stopped_at = moment().add(3, '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

cad44b6d   Goutte   Finish adding the...
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
  buildSampUrl: ->
    [started_at, stopped_at] = @getDomain()
    targets = [t for t of @targets when @targets[t].active].sort().join('-')
    parameters = [p for p of @parameters when @parameters[p].active].sort().join('-')
    url = @configuration['api']['samp']
    url = url.replace('<targets>', targets)
    url = url.replace('<params>', parameters)
    url = url.replace('<started_at>', started_at.format(API_TIME_FORMAT))
    url = url.replace('<stopped_at>', stopped_at.format(API_TIME_FORMAT))
    url

  buildSampName: ->
    [started_at, stopped_at] = @getDomain()
    targets = [t.name for t in @getEnabledTargets()].sort().join(', ')
    "Heliopropa for #{targets} from #{started_at.format(API_TIME_FORMAT)} to #{stopped_at.format(API_TIME_FORMAT)}."

b60e7acd   Goutte   Rename "source" i...
116
117
  addTarget: (target) ->
    @targets[target.slug] = target
f75faf5f   Goutte   WIP
118
119
    this

2c0e1515   Goutte   Refactor loading ...
120
121
122
123
#  enableAllTargets: ->
#    for slug, target of @targets
#      enableTarget(slug)
#    this
f75faf5f   Goutte   WIP
124

cad44b6d   Goutte   Finish adding the...
125
126
127
  getEnabledTargets: ->
    [target for slug, target of @targets when target.active]

6bb225d6   Goutte   Link the time ser...
128
  enableTarget: (target_slug) ->
80352490   Goutte   Multi model suppo...
129
    @time_series.forEach((ts) ~> ts.show() if ts.target.slug == target_slug && @parameters[ts.parameter].active)
b60e7acd   Goutte   Rename "source" i...
130
    @targets[target_slug].active = true
17c52617   Goutte   Make the orbits p...
131
    @orbits?.showOrbiter target_slug
f75faf5f   Goutte   WIP
132
133
    this

6bb225d6   Goutte   Link the time ser...
134
  disableTarget: (target_slug) ->
80352490   Goutte   Multi model suppo...
135
    @time_series.forEach((ts) -> ts.hide() if ts.target.slug == target_slug)
b60e7acd   Goutte   Rename "source" i...
136
    @targets[target_slug].active = false
17c52617   Goutte   Make the orbits p...
137
    @orbits?.hideOrbiter target_slug
f75faf5f   Goutte   WIP
138
139
    this

fe3132dd   Goutte   Refactor even more.
140
  resize: ->
a21f81d9   Goutte   Enable Venus and ...
141
    @orbits?.resize();
80352490   Goutte   Multi model suppo...
142
    @time_series.forEach((ts) -> ts.resize())
fe3132dd   Goutte   Refactor even more.
143

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

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

b60e7acd   Goutte   Rename "source" i...
150
151
152
153
154
  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...
155

f75faf5f   Goutte   WIP
156
    sw = this
2c0e1515   Goutte   Refactor loading ...
157
    new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
158
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
159
      d3.csv(url, (csv) ->
60b73eb1   Goutte   Change temperatur...
160
        console.debug("Requested CSV for #{target_slug}…", csv)
f75faf5f   Goutte   WIP
161
        timeFormat = d3.timeParse('%Y-%m-%dT%H:%M:%S%Z')
6491a1f1   Goutte   Fix up the bugs l...
162
        data = { 'hee': [] }
f75faf5f   Goutte   WIP
163
        configuration['parameters'].forEach((parameter) ->
06a3a1f1   Goutte   Continue bug hunt...
164
          data[parameter['id']] = []
f75faf5f   Goutte   WIP
165
        )
80352490   Goutte   Multi model suppo...
166
167
        unless csv then reject 'invalid'
        unless csv.length then reject 'empty'
f75faf5f   Goutte   WIP
168
169
170
171
172
173
        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...
174
175
176
          if d['xhee'] and d['yhee']
            data['hee'].push({
              t: dtime, x: parseFloat(d['xhee']), y: parseFloat(d['yhee'])
06a3a1f1   Goutte   Continue bug hunt...
177
            })
f75faf5f   Goutte   WIP
178
        )
9bfa6c42   Goutte   More bug hunting.
179
        resolve data
f75faf5f   Goutte   WIP
180
181
      )
    )
2c0e1515   Goutte   Refactor loading ...
182
183

  loadAndCreatePlots: (started_at, stopped_at) ->
243cd8a4   Goutte   Timestamp party c...
184
    """
80352490   Goutte   Multi model suppo...
185
186
    started_at: moment(.js) datetime object
    stopped_at: moment(.js) datetime object
243cd8a4   Goutte   Timestamp party c...
187
    """
2c0e1515   Goutte   Refactor loading ...
188
    @showLoader()
7994cf1a   Goutte   Hunt bugs.
189
190
191
    @started_at = started_at
    @stopped_at = stopped_at
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
243cd8a4   Goutte   Timestamp party c...
192
193
    started_at = started_at.format(API_TIME_FORMAT)
    stopped_at = stopped_at.format(API_TIME_FORMAT)
60b73eb1   Goutte   Change temperatur...
194
195
196

    targets = [@targets[k] for k of @targets]
    targets.forEach((target) ~>
4900d232   Goutte   Add the time inte...
197
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
198
      targetButton.addClass('loading')
60b73eb1   Goutte   Change temperatur...
199
200
201
202
203
204
205
      targetButton.removeClass('failed error empty')
    )
    handleTarget = (i) ~>
      if i >= targets.length then return
      target = targets[i]
      console.info "Loading CSV data of #{target.name}…"
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
206
207
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
9bfa6c42   Goutte   More bug hunting.
208
          console.info "Loaded CSV data of #{target.name}.", data
2c0e1515   Goutte   Refactor loading ...
209
          @createTimeSeries(target, data)
6491a1f1   Goutte   Fix up the bugs l...
210
          @orbits.initOrbiter(target.slug, target.config, data['hee'])
2c0e1515   Goutte   Refactor loading ...
211
          targetButton.removeClass('loading')
5e099488   Goutte   Fix that loading ...
212
          if target.active then @hideLoader() else @disableTarget(target.slug)
60b73eb1   Goutte   Change temperatur...
213
          handleTarget(i+1)
2c0e1515   Goutte   Refactor loading ...
214
        ,
9bfa6c42   Goutte   More bug hunting.
215
        (error) ~>
80352490   Goutte   Multi model suppo...
216
217
218
219
220
          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.
60b73eb1   Goutte   Change temperatur...
221
222
              # alert("There was an error with #{target.name}.\nPlease retry in a few moments.")
              targetButton.addClass('error')
80352490   Goutte   Multi model suppo...
223
224
225
226
227
228
229
230
              @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 ...
231
          targetButton.addClass('failed')
9bfa6c42   Goutte   More bug hunting.
232
233
          targetButton.removeClass('loading')
          @hideLoader()
60b73eb1   Goutte   Change temperatur...
234
          handleTarget(i+1)
2c0e1515   Goutte   Refactor loading ...
235
      )
60b73eb1   Goutte   Change temperatur...
236
237
238
239
    handleTarget(0)


    this
2c0e1515   Goutte   Refactor loading ...
240
241
242

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

b60e7acd   Goutte   Rename "source" i...
248
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
249
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
250
251
252
      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...
253
      @time_series.push(new TimeSeries(
7b5642ae   Goutte   Normalize time in...
254
255
256
257
        id, title, target, data[id], @parameters[id].active, container, {
          'started_at': @started_at,
          'stopped_at': @stopped_at,
        }
6bb225d6   Goutte   Link the time ser...
258
      ))
4816cef4   Goutte   Refactor some more.
259
    )
80352490   Goutte   Multi model suppo...
260
    @time_series.forEach((ts) ~>  # returning true may be faster
243cd8a4   Goutte   Timestamp party c...
261
      ts.options['onMouseOver'] = ~>
80352490   Goutte   Multi model suppo...
262
        @time_series.forEach((ts2) -> ts2.showCursor()) ; true
243cd8a4   Goutte   Timestamp party c...
263
      ts.options['onMouseOut'] = ~>
80352490   Goutte   Multi model suppo...
264
        @time_series.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
265
      ts.options['onMouseMove'] = (t) ~>
80352490   Goutte   Multi model suppo...
266
        @time_series.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
267
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
268
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
269
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
270
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
271
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
272
    )
80352490   Goutte   Multi model suppo...
273
    @time_series
4816cef4   Goutte   Refactor some more.
274

cad44b6d   Goutte   Finish adding the...
275
276
277
  getEnabledParameters: ->
    [p for slug, p in @parameters when p.active]

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

b7fe650c   Goutte   Misc bundle of ol...
284
285
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
286
    @parameters[parameter_slug].active = false
80352490   Goutte   Multi model suppo...
287
    @time_series.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
288
    this
ae0aa7d2   Goutte   Add an x axis lab...
289

a06a0a67   Goutte   Prepare the time ...
290
291
292
293
294
  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...
295
296
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
297
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
298
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
299
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
300
      return
1754789b   Goutte   Decorate and clea...
301
302
303
304
    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...
305

243cd8a4   Goutte   Timestamp party c...
306
307
308
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
309

80352490   Goutte   Multi model suppo...
310
311
    if (not @is_invalid) and
       (@started_at <= started_at <= @stopped_at) and
6bb225d6   Goutte   Link the time ser...
312
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
313
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
314
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
315
      # the visible ones, for a smoother transition.
80352490   Goutte   Multi model suppo...
316
317
      @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...
318
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
319
    else
80352490   Goutte   Multi model suppo...
320
      @is_invalid = false
243cd8a4   Goutte   Timestamp party c...
321
322
323
324
325
      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 ...
326

243cd8a4   Goutte   Timestamp party c...
327
    this
8cb213b9   Goutte   Clean up, and pre...
328

6bb225d6   Goutte   Link the time ser...
329
  resetZoom: ->
80352490   Goutte   Multi model suppo...
330
    @time_series.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
331
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
332
333
334
335
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
336
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
337
338
339
340
341
    @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...
342
343


ae0aa7d2   Goutte   Add an x axis lab...
344

ae0aa7d2   Goutte   Add an x axis lab...
345
346
347
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
348

f1f1e797   Goutte   Rewrite ugly java...
349
350
351
352
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

7b5642ae   Goutte   Normalize time in...
353
  (@parameter, @title, @target, @data, @visible, @container, @options = {}) ->
bde97e4d   Goutte   Add more changes ...
354
    # parameter : slug of the parameter to observe, like btan or pdyn
c3008fb2   Goutte   Clean up and refa...
355
356
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
357
    # data : list of {x: <datetime>, y: <float>}
f1f1e797   Goutte   Rewrite ugly java...
358
359
    @init()

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

f1f1e797   Goutte   Rewrite ugly java...
362
  init: ->
c3008fb2   Goutte   Clean up and refa...
363
    console.info "Initializing plot of #{@}…"
f1f1e797   Goutte   Rewrite ugly java...
364

7b5642ae   Goutte   Normalize time in...
365
366
367
368
369
370
    # pre-compute extents for performance when zooming
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)
    if @options['started_at'] then @xDataExtent[0] = @options['started_at']
    if @options['stopped_at'] then @xDataExtent[1] = @options['stopped_at']

f1f1e797   Goutte   Rewrite ugly java...
371
372
373
374
    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
375
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
376
377
    }

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

6491a1f1   Goutte   Fix up the bugs l...
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
    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...
401
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
402
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
403
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
404
405
406
407
408
409
410
411
    @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...
412
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
413

f1f1e797   Goutte   Rewrite ugly java...
414
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
415
416
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
417

123313cb   Goutte   Clip the paths of...
418
419
420
421
422
423
424
425
426
427
    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...
428
429
430
                        .datum(@data)
                        .classed('line', true)

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

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

f1f1e797   Goutte   Rewrite ugly java...
438
439
440
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
441
442
443
444
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
445
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
446
447
448
449
450
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
451

81c9b2e8   Goutte   Add the values to...
452
453
454
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
458
    dx = 8
81c9b2e8   Goutte   Add the values to...
459
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
460
461
462
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
463

81c9b2e8   Goutte   Add the values to...
464
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
465
466
467
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
468

81c9b2e8   Goutte   Add the values to...
469
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
470
471
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
472
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
473

81c9b2e8   Goutte   Add the values to...
474
475
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
476
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
477
478
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
479
480
    @resize()

123313cb   Goutte   Clip the paths of...
481
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
482
  resize: ->
123313cb   Goutte   Clip the paths of...
483
484
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
485
486
487

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

123313cb   Goutte   Clip the paths of...
489
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
490
491
492
493
494
495
496

    @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...
497
498
499
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
500
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
501
502
503
504

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

2463bd16   Goutte   Add a circle foll...
505
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
506
507
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
508
509
510
511
512
513
514
515

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

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

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

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

2463bd16   Goutte   Add a circle foll...
522
523
524
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
525
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
526
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
527
528
529
530
531
532
533
534
535
536
      # 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 ...
537
538
      # 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...
539
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
540
541
542
543
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
544

6bb225d6   Goutte   Link the time ser...
545
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
546
547
    this

2c0e1515   Goutte   Refactor loading ...
548
549
550
551
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
552
553
554
555
556
557
558
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
560
561
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
562
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
563
564

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

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

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

08569a6b   Goutte   Add a zooming bru...
573
574
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
575
576
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
577
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
578
579
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
580
581
582

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
583
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
584
585
586
587
588
589
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
590
591
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
592
593
594
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
595
    if @visible
c3008fb2   Goutte   Clean up and refa...
596
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
597
598
599
600
601
      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...
602
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
603
604
605
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
606

541e2936   Goutte   Synchronize the t...
607
608
609
610
611
612
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
616
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
617
618
619
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
620
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
621
622
623
624
    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...
625
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
626

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

8cb213b9   Goutte   Clean up, and pre...
630
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
631
632
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
633
634
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
635
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
636
637
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
638
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
639
640
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
641
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
642
643
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
644
645

    this
f1f1e797   Goutte   Rewrite ugly java...
646

ae0aa7d2   Goutte   Add an x axis lab...
647
648
649
650

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

f1f1e797   Goutte   Rewrite ugly java...
651
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
652
653
654
655
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
656

a21f81d9   Goutte   Enable Venus and ...
657
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
658
659
660
    @init()

  init: ->
c3008fb2   Goutte   Clean up and refa...
661
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
662

b500e561   Goutte   Invert the orbits...
663
664
665
    # In the variable names below, x and y are the usual ones.
    # (on the plots we show Y on the x axis, and X reversed on the y axis)

f1f1e797   Goutte   Rewrite ugly java...
666
667
668
    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
669
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
670
671
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
672

6491a1f1   Goutte   Fix up the bugs l...
673
    @data = {}  # slug => HEE array
a21f81d9   Goutte   Enable Venus and ...
674
    @orbiters = {}  # slug => config
17c52617   Goutte   Make the orbits p...
675
676
677
678
679
    @orbitersElements = {}  # see initOrbiter
    @orbitersExtrema = {}  # slug => local extrema
    @lastOrbiterData = {}  # slug => most recently used datum for position
    @xScale = d3.scaleLinear().domain([-1, 1])
    @yScale = d3.scaleLinear().domain([1, -1])
f1f1e797   Goutte   Rewrite ugly java...
680
681
682
683

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

f1f1e797   Goutte   Rewrite ugly java...
684
685
686
687
688
    @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...
689
690
691
692
693
    @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")
b500e561   Goutte   Invert the orbits...
694
    @xAxisTitle.append('tspan').text('Y')
ae0aa7d2   Goutte   Add an x axis lab...
695
696
697
    # 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 ...
698
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
699
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
700

11662eed   Goutte   Add Y axis label ...
701
702
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
b500e561   Goutte   Invert the orbits...
703
    @yAxisTitle.append('tspan').text('X')
11662eed   Goutte   Add Y axis label ...
704
705
706
707
    @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...
708
709
710
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
711
    @sun.append('svg:title').text("Sun")
f1f1e797   Goutte   Rewrite ugly java...
712

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

a21f81d9   Goutte   Enable Venus and ...
716
  initOrbiter: (slug, config, data) ->
dc0be992   Goutte   Support having no...
717
718
719
720
721
722
723
724
725
    @data[slug] = data
    @orbiters[slug] = config

    if data.length
      console.info "Initializing orbit of #{config.name}…"
    else
      console.warn "No orbit data for #{config.name}…"
      return

438929a4   Goutte   Rewrite the orbit...
726
727
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

438929a4   Goutte   Rewrite the orbit...
728
729
730
731
732
733
    # 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...
734
    orbiter.append('svg:title').text(config.name)
438929a4   Goutte   Rewrite the orbit...
735
736

    orbit_line = d3.line()
b500e561   Goutte   Invert the orbits...
737
738
                   .x((d) ~> @xScale(d.y))
                   .y((d) ~> @yScale(d.x))
438929a4   Goutte   Rewrite the orbit...
739
740

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

438929a4   Goutte   Rewrite the orbit...
744
745
746
747
748
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line
17c52617   Goutte   Make the orbits p...
749
750
751
    @orbitersExtrema[slug] = d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    )
a21f81d9   Goutte   Enable Venus and ...
752

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

17c52617   Goutte   Make the orbits p...
755
756
    @resize(true)

438929a4   Goutte   Rewrite the orbit...
757
758
    this

17c52617   Goutte   Make the orbits p...
759
  showOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
760
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
761
762
763
764
765
766
767
    @orbiters[slug].hidden = false
    @orbitersElements[slug].orbiter.style("display", null)
    @orbitersElements[slug].orbit_ellipse.style("display", null)
    @orbitersElements[slug].orbit_section.style("display", null)
    @resize(true)

  hideOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
768
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
769
770
771
772
773
774
    @orbiters[slug].hidden = true
    @orbitersElements[slug].orbiter.style("display", "none")
    @orbitersElements[slug].orbit_ellipse.style("display", "none")
    @orbitersElements[slug].orbit_section.style("display", "none")
    @resize(true)

2c0e1515   Goutte   Refactor loading ...
775
776
777
  clear: ->
    $(@svg.node()).remove()

17c52617   Goutte   Make the orbits p...
778
  resize: (animate = false) ->
abcf4c94   Goutte   Clean up.
779
780
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
781

abcf4c94   Goutte   Clean up.
782
    console.debug("Resizing orbits : #{width} × #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
783

17c52617   Goutte   Make the orbits p...
784
785
786
787
788
789
    extremum = 1.1 * d3.max([s for s, o of @orbiters when not o.hidden], (d) ~>
      @orbitersExtrema[d]
    )
    @xScale = d3.scaleLinear().domain([-1 * extremum, extremum])
    @yScale = d3.scaleLinear().domain([extremum, -1 * extremum])

b500e561   Goutte   Invert the orbits...
790
    @xScale.range([0, width])
17c52617   Goutte   Make the orbits p...
791
    @yScale.range([height, 0])
f1f1e797   Goutte   Rewrite ugly java...
792
793
794
795

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

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

438929a4   Goutte   Rewrite the orbit...
798
    for slug, config of @orbiters
17c52617   Goutte   Make the orbits p...
799
      @resizeOrbiter(slug, config, width, height, animate)
438929a4   Goutte   Rewrite the orbit...
800

f1f1e797   Goutte   Rewrite ugly java...
801
802
803
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

17c52617   Goutte   Make the orbits p...
804
805
806
807
808
809
810
811
812
    @svg.select('.x.axis').attr('transform', 'translate(0,' + height + ')')
    if animate
      t = @svg.transition().duration(750)
      t1 = @svg.transition().duration(4750)
      @svg.select('.x.axis').transition(t).call(@xAxis);
      @svg.select('.y.axis').transition(t).call(@yAxis);
    else
      @svg.select('.x.axis').call(@xAxis)
      @svg.select('.y.axis').call(@yAxis)
f1f1e797   Goutte   Rewrite ugly java...
813

ae0aa7d2   Goutte   Add an x axis lab...
814
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
815
816
817
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
818

f1f1e797   Goutte   Rewrite ugly java...
819
820
    this

17c52617   Goutte   Make the orbits p...
821
  resizeOrbiter: (slug, config, width, height, animate = false) ->
dc0be992   Goutte   Support having no...
822
823
    data = @data[slug]
    if not data.length then return
abcf4c94   Goutte   Clean up.
824
    console.debug("Resizing orbit of #{slug}…")
438929a4   Goutte   Rewrite the orbit...
825

17c52617   Goutte   Make the orbits p...
826
    tt = @svg.transition().duration(750)
438929a4   Goutte   Rewrite the orbit...
827
    el = @orbitersElements[slug]
17c52617   Goutte   Make the orbits p...
828
829
830
831
832
    orbit_section = el['orbit_section']
    if animate
      t = @svg.transition().duration(750)
      orbit_section = orbit_section.transition(tt)
    orbit_section.attr('d', el['orbit_line'])
438929a4   Goutte   Rewrite the orbit...
833
834
835
836
837
838

    a = config['orbit']['a']
    b = config['orbit']['b']
    c = Math.sqrt(a*a - b*b)
    cx = (width / 2) - c
    cy = (height / 2)
17c52617   Goutte   Make the orbits p...
839
840
841
842
843
844
845
846

    orbit_ellipse = el['orbit_ellipse']
    if animate
      t = @svg.transition().duration(750)
      orbit_ellipse = orbit_ellipse.transition(t)
    # These ellipses ain't worth much
    # Maybe a simple circle whose radius is the mean radius of the orbit ?
    orbit_ellipse.attr('cx', cx).attr('cy', cy)
438929a4   Goutte   Rewrite the orbit...
847
848
        .attr('rx', @xScale(a) - @xScale(0))
        .attr('ry', @yScale(b) - @yScale(0))
a21f81d9   Goutte   Enable Venus and ...
849
#        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
438929a4   Goutte   Rewrite the orbit...
850

17c52617   Goutte   Make the orbits p...
851
    @repositionOrbiter(slug, null, true)
438929a4   Goutte   Rewrite the orbit...
852
853
854

    this

17c52617   Goutte   Make the orbits p...
855
  repositionOrbiter: (slug, datum, animate = false) ->
a21f81d9   Goutte   Enable Venus and ...
856
    data = @data[slug]
dc0be992   Goutte   Support having no...
857
    if not data.length then return
17c52617   Goutte   Make the orbits p...
858
    datum ?= @lastOrbiterData[slug]
ae0aa7d2   Goutte   Add an x axis lab...
859
    datum ?= data[data.length - 1]
17c52617   Goutte   Make the orbits p...
860
861
862
863
864
865
866
    @lastOrbiterData[slug] = datum
    el = @orbitersElements[slug]['orbiter']
    if animate
      t = @svg.transition().duration(750)
      el = el.transition(t)
    el.attr('x', @xScale(datum.y) - 16)
    el.attr('y', @yScale(datum.x) - 16)
ae0aa7d2   Goutte   Add an x axis lab...
867
868
869
870
871
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
872
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
873
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
874
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
875
876
877
878
879
      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.
880
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
881
    this
ae0aa7d2   Goutte   Add an x axis lab...
882

8cb213b9   Goutte   Clean up, and pre...
883
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
884
885
886
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
dc0be992   Goutte   Support having no...
887
      if not data.length then return
667eeb24   Goutte   Resize the domain...
888
889
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
ae0aa7d2   Goutte   Add an x axis lab...
890

667eeb24   Goutte   Resize the domain...
891
892
893
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
dc0be992   Goutte   Support having no...
894
      if not @data[slug].length then return
667eeb24   Goutte   Resize the domain...
895
896
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
abcf4c94   Goutte   Clean up.