Blame view

web/static/js/swapp.ls 33 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
# All the "javascript" code is in this file, except for inline scripts in
2d8a5753   Goutte   Make the Y-Axis o...
11
# templates, such as in `home.html.jinja2`.
4cf497e0   Goutte   Make the targets ...
12

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.
780828a8   Goutte   Prepare horizonta...
34
  It defaults to an interval starting two months ago, and ending in a month.
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

3c2b15fc   Goutte   Make the Y-Axis o...
66
67
    @orbits = null     # an Orbiter instance (defined below)
    @time_series = []  # a List of TimeSeries instances
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)
    """
fb383448   Goutte   Implement the cac...
75
    # Default time interval is from one year ago to three weeks ahead.
2c0e1515   Goutte   Refactor loading ...
76
    # We set the h/m/s to zero to benefit from a daily cache.
fb383448   Goutte   Implement the cac...
77
    # If you edit these values you need to change the cache warmup too (run.py)
780828a8   Goutte   Prepare horizonta...
78
79
    started_at = moment().subtract(2, 'month').hours(0).minutes(0).seconds(0)
    stopped_at = moment().add(1, 'month').hours(0).minutes(0).seconds(0)
7994cf1a   Goutte   Hunt bugs.
80
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
81
    @loadAndCreatePlots(started_at, stopped_at)
b60e7acd   Goutte   Rename "source" i...
82
    window.addEventListener 'resize', ~> @resize()
fb383448   Goutte   Implement the cac...
83
    this
b60e7acd   Goutte   Rename "source" i...
84
85

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

6b149919   Goutte   Add a Download bu...
92
93
94
95
96
97
98
99
100
  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...
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
  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...
117
118
  addTarget: (target) ->
    @targets[target.slug] = target
f75faf5f   Goutte   WIP
119
120
    this

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

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

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

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

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

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

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

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

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

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

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

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

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

cad44b6d   Goutte   Finish adding the...
278
279
280
  getEnabledParameters: ->
    [p for slug, p in @parameters when p.active]

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

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

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

243cd8a4   Goutte   Timestamp party c...
309
310
311
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
312

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

243cd8a4   Goutte   Timestamp party c...
330
    this
8cb213b9   Goutte   Clean up, and pre...
331

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

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


ae0aa7d2   Goutte   Add an x axis lab...
347

ae0aa7d2   Goutte   Add an x axis lab...
348
349
350
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
351

f1f1e797   Goutte   Rewrite ugly java...
352
353
354
355
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

7b5642ae   Goutte   Normalize time in...
356
  (@parameter, @title, @target, @data, @visible, @container, @options = {}) ->
bde97e4d   Goutte   Add more changes ...
357
    # parameter : slug of the parameter to observe, like btan or pdyn
c3008fb2   Goutte   Clean up and refa...
358
359
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
360
    # data : list of {x: <datetime>, y: <float>}
cdf79b23   Goutte   Give the future d...
361
362
363
364
365
    # options: object with the following properties
    #          started_at (Moment obj)
    #          stopped_at (Moment obj)
    now = moment()
    @predictiveData = [d for d in @data when moment(d.x) >= now]
f1f1e797   Goutte   Rewrite ugly java...
366
367
    @init()

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

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

7b5642ae   Goutte   Normalize time in...
373
    # pre-compute extents for performance when zooming
2d8a5753   Goutte   Make the Y-Axis o...
374
    # those are final and always hold the biggest extent
7b5642ae   Goutte   Normalize time in...
375
376
377
378
379
    @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...
380
381
382
383
    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
384
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
385
386
    }

6bb225d6   Goutte   Link the time ser...
387
388
    @xScale = d3.scaleTime().domain(@xDataExtent)
    @yScale = d3.scaleLinear().domain(@yDataExtent)
ff4f2af5   Goutte   Log problems with...
389
390
    # Domain on a log scale MUST NOT cross zero
#    @yScale = d3.scaleLog().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
391

6491a1f1   Goutte   Fix up the bugs l...
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
    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...
412
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
413
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
414
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
415
416
417
418
419
420
421
422
    @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...
423
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
424

f1f1e797   Goutte   Rewrite ugly java...
425
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
426
427
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
428

123313cb   Goutte   Clip the paths of...
429
430
431
432
433
434
435
436
437
438
    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...
439
440
                        .datum(@data)
                        .classed('line', true)
cdf79b23   Goutte   Give the future d...
441
442
443
    @predictiveDataPath = @pathWrapper.append('path')
                                      .datum(@predictiveData)
                                      .classed('predictive-line', true)
f1f1e797   Goutte   Rewrite ugly java...
444

780828a8   Goutte   Prepare horizonta...
445
446
447
448
449
450
451
452
453
    @horizontalLines = []
    if @options['horizontalLines']
      for line in @options['horizontalLines']
        lineElement = @svg.append("line")
                          .attr("class", "line horitonal-line")
                          .style("stroke", "orange")  # move to CSS
                          .style("stroke-dasharray", ("3, 2"))  # idem
        @horizontalLines.push({'element': lineElement, 'config': line})

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

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

f1f1e797   Goutte   Rewrite ugly java...
461
462
463
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
464
465
466
467
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
468
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
469
470
471
472
473
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
474

81c9b2e8   Goutte   Add the values to...
475
476
477
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
481
    dx = 8
81c9b2e8   Goutte   Add the values to...
482
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
483
484
485
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
486

81c9b2e8   Goutte   Add the values to...
487
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
488
489
490
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
491

81c9b2e8   Goutte   Add the values to...
492
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
493
494
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
495
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
496

81c9b2e8   Goutte   Add the values to...
497
498
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
499
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
500
501
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
502
503
    @resize()

123313cb   Goutte   Clip the paths of...
504
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
505
  resize: ->
123313cb   Goutte   Clip the paths of...
506
507
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
508
509
510

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

123313cb   Goutte   Clip the paths of...
512
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
513
514
515
516
517
518
519

    @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...
520
521
522
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
523
    @path.attr('d', @line)
cdf79b23   Goutte   Give the future d...
524
    @predictiveDataPath.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
525

780828a8   Goutte   Prepare horizonta...
526
527
528
529
530
531
532
    for line in @horizontalLines
      lineValue = @yScale(line['config']['value']) + @margin.top
      line['element'].attr("x1", @margin.left)
                     .attr("y1", lineValue)
                     .attr("x2", @margin.left + width)
                     .attr("y2", lineValue)

f1f1e797   Goutte   Rewrite ugly java...
533
534
535
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
536
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
034132c6   Goutte   More changes from...
537
    @xAxis.ticks(Math.floor(width / 80.0))  # not working as expected
d49a163c   Goutte   Fix the resize an...
538
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
539
540
541
542
543
544
545
546

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

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

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

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

2463bd16   Goutte   Add a circle foll...
553
554
555
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
556
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
557
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
558
559
560
561
562
563
564
565
566
567
      # 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 ...
568
569
      # 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...
570
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
571
572
573
574
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
575

6bb225d6   Goutte   Link the time ser...
576
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
577
578
    this

2c0e1515   Goutte   Refactor loading ...
579
580
581
582
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
583
584
585
586
587
588
589
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
591
592
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
593
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
594
595

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

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

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

08569a6b   Goutte   Add a zooming bru...
604
605
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
606
607
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
608
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
609
610
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
611
612
613

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
614
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
615
616
617
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
2d8a5753   Goutte   Make the Y-Axis o...
618
    @yScale.domain(d3.extent(@data, (d) -> if startDate <= d.x <= stopDate then d.y else 0))
2038c9fb   Goutte   Add a zoom reset ...
619
620
621
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
622
623
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
624
625
626
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
627
    if @visible
c3008fb2   Goutte   Clean up and refa...
628
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
629
630
631
632
      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)
cdf79b23   Goutte   Give the future d...
633
      @predictiveDataPath.transition(t).attr('d', @line)
6bb225d6   Goutte   Link the time ser...
634
    else
c3008fb2   Goutte   Clean up and refa...
635
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
636
637
638
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
cdf79b23   Goutte   Give the future d...
639
      @predictiveDataPath.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
640

541e2936   Goutte   Synchronize the t...
641
642
643
644
645
646
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
650
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
651
652
653
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
654
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
655
656
657
658
    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...
659
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
660

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

8cb213b9   Goutte   Clean up, and pre...
664
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
665
666
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
667
668
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
669
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
670
671
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
672
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
673
674
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
675
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
676
677
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
678
679

    this
f1f1e797   Goutte   Rewrite ugly java...
680

ae0aa7d2   Goutte   Add an x axis lab...
681
682
683
684

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

f1f1e797   Goutte   Rewrite ugly java...
685
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
686
687
688
689
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
690

a21f81d9   Goutte   Enable Venus and ...
691
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
692
693
694
    @init()

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

b500e561   Goutte   Invert the orbits...
697
698
699
    # 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...
700
701
702
    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
703
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
704
705
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
706

6491a1f1   Goutte   Fix up the bugs l...
707
    @data = {}  # slug => HEE array
a21f81d9   Goutte   Enable Venus and ...
708
    @orbiters = {}  # slug => config
17c52617   Goutte   Make the orbits p...
709
710
711
712
713
    @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...
714
715
716
717

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

f1f1e797   Goutte   Rewrite ugly java...
718
719
720
721
722
    @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...
723
724
725
726
727
    @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...
728
    @xAxisTitle.append('tspan').text('Y')
ae0aa7d2   Goutte   Add an x axis lab...
729
730
731
    # 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 ...
732
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
733
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
734

11662eed   Goutte   Add Y axis label ...
735
736
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
b500e561   Goutte   Invert the orbits...
737
    @yAxisTitle.append('tspan').text('X')
11662eed   Goutte   Add Y axis label ...
738
739
740
741
    @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...
742
743
744
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
745
    @sun.append('svg:title').text("Sun")
f1f1e797   Goutte   Rewrite ugly java...
746

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

a21f81d9   Goutte   Enable Venus and ...
750
  initOrbiter: (slug, config, data) ->
dc0be992   Goutte   Support having no...
751
752
753
754
755
756
757
758
759
    @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...
760
761
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

438929a4   Goutte   Rewrite the orbit...
762
763
764
765
766
767
    # 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...
768
    orbiter.append('svg:title').text(config.name)
438929a4   Goutte   Rewrite the orbit...
769
770

    orbit_line = d3.line()
b500e561   Goutte   Invert the orbits...
771
772
                   .x((d) ~> @xScale(d.y))
                   .y((d) ~> @yScale(d.x))
438929a4   Goutte   Rewrite the orbit...
773
774

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

438929a4   Goutte   Rewrite the orbit...
778
779
780
781
782
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line
17c52617   Goutte   Make the orbits p...
783
784
785
    @orbitersExtrema[slug] = d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    )
a21f81d9   Goutte   Enable Venus and ...
786

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

17c52617   Goutte   Make the orbits p...
789
790
    @resize(true)

438929a4   Goutte   Rewrite the orbit...
791
792
    this

17c52617   Goutte   Make the orbits p...
793
  showOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
794
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
795
796
797
798
799
800
801
    @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...
802
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
803
804
805
806
807
808
    @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 ...
809
810
811
  clear: ->
    $(@svg.node()).remove()

3c2b15fc   Goutte   Make the Y-Axis o...
812
  resize: (animate = false, extremum = null) ->
abcf4c94   Goutte   Clean up.
813
814
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
815

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

3c2b15fc   Goutte   Make the Y-Axis o...
818
819
820
821
822
823
824
    if extremum == null
      extremum = 1.1 * d3.max(
        [@orbitersExtrema[s] for s, o of @orbiters when not o.hidden]
      )
#    extremum = 1.1 * d3.max([s for s, o of @orbiters when not o.hidden], (d) ~>
#      @orbitersExtrema[d]
#    )
17c52617   Goutte   Make the orbits p...
825
826
827
    @xScale = d3.scaleLinear().domain([-1 * extremum, extremum])
    @yScale = d3.scaleLinear().domain([extremum, -1 * extremum])

b500e561   Goutte   Invert the orbits...
828
    @xScale.range([0, width])
17c52617   Goutte   Make the orbits p...
829
    @yScale.range([height, 0])
f1f1e797   Goutte   Rewrite ugly java...
830
831
832
833

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
839
840
841
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

17c52617   Goutte   Make the orbits p...
842
843
844
845
846
847
848
849
850
    @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...
851

ae0aa7d2   Goutte   Add an x axis lab...
852
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
853
854
855
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
856

f1f1e797   Goutte   Rewrite ugly java...
857
858
    this

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

17c52617   Goutte   Make the orbits p...
864
    tt = @svg.transition().duration(750)
438929a4   Goutte   Rewrite the orbit...
865
    el = @orbitersElements[slug]
17c52617   Goutte   Make the orbits p...
866
867
868
869
870
    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...
871
872
873
874
875
876

    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...
877
878
879
880
881
882
883
884

    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...
885
886
        .attr('rx', @xScale(a) - @xScale(0))
        .attr('ry', @yScale(b) - @yScale(0))
a21f81d9   Goutte   Enable Venus and ...
887
#        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
438929a4   Goutte   Rewrite the orbit...
888

17c52617   Goutte   Make the orbits p...
889
    @repositionOrbiter(slug, null, true)
438929a4   Goutte   Rewrite the orbit...
890
891
892

    this

3c2b15fc   Goutte   Make the Y-Axis o...
893
894
895
  zoomToTarget: (slug) ->
    @resize(true, 1.1 * @orbitersExtrema[slug])

17c52617   Goutte   Make the orbits p...
896
  repositionOrbiter: (slug, datum, animate = false) ->
a21f81d9   Goutte   Enable Venus and ...
897
    data = @data[slug]
dc0be992   Goutte   Support having no...
898
    if not data.length then return
17c52617   Goutte   Make the orbits p...
899
    datum ?= @lastOrbiterData[slug]
ae0aa7d2   Goutte   Add an x axis lab...
900
    datum ?= data[data.length - 1]
17c52617   Goutte   Make the orbits p...
901
902
903
904
905
906
907
    @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...
908
909
910
911
912
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
913
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
914
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
915
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
916
917
918
919
920
      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.
921
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
922
    this
ae0aa7d2   Goutte   Add an x axis lab...
923

8cb213b9   Goutte   Clean up, and pre...
924
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
925
926
927
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
dc0be992   Goutte   Support having no...
928
      if not data.length then return
667eeb24   Goutte   Resize the domain...
929
930
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
ae0aa7d2   Goutte   Add an x axis lab...
931

667eeb24   Goutte   Resize the domain...
932
933
934
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
dc0be992   Goutte   Support having no...
935
      if not @data[slug].length then return
667eeb24   Goutte   Resize the domain...
936
937
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
abcf4c94   Goutte   Clean up.