Blame view

web/static/js/swapp.ls 33.4 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
###############################################################################

d729e9cd   Goutte   Cleanup.
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
a2a08db2   Goutte   Make sure targets...
132
    @orbits?.enableTarget 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
a2a08db2   Goutte   Make sure targets...
138
    @orbits?.disableTarget 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())
d729e9cd   Goutte   Cleanup.
144
    this
fe3132dd   Goutte   Refactor even more.
145

2c0e1515   Goutte   Refactor loading ...
146
  showLoader: ->
d729e9cd   Goutte   Cleanup.
147
    $('#plots_loader').show()
2c0e1515   Goutte   Refactor loading ...
148

9c0c4509   Goutte   Add a loader to t...
149
  hideLoader: ->
d729e9cd   Goutte   Cleanup.
150
    $('#plots_loader').hide()
9c0c4509   Goutte   Add a loader to t...
151

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

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

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

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

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

b60e7acd   Goutte   Rename "source" i...
250
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
251
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
252
253
254
      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
255
256
257
258
259
260
261
262
      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.
263
    )
b4abddb7   Goutte   Fix a small issue...
264
265
    # Let's override all time series' input handlers to link them together
    @time_series.forEach((ts) ~>  # returning true may be faster, how to bench?
243cd8a4   Goutte   Timestamp party c...
266
      ts.options['onMouseOver'] = ~>
b4abddb7   Goutte   Fix a small issue...
267
        true  # let's do nothing, we'll show the cursor during moveCursor()
243cd8a4   Goutte   Timestamp party c...
268
      ts.options['onMouseOut'] = ~>
80352490   Goutte   Multi model suppo...
269
        @time_series.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
270
      ts.options['onMouseMove'] = (t) ~>
80352490   Goutte   Multi model suppo...
271
        @time_series.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
272
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
273
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
274
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
275
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
276
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
277
    )
80352490   Goutte   Multi model suppo...
278
    @time_series
4816cef4   Goutte   Refactor some more.
279

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

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

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

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

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

5ef50583   Goutte   Clean up.
315
316
#    if (not @is_invalid) and
    if (@started_at <= started_at <= @stopped_at) and
6bb225d6   Goutte   Link the time ser...
317
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
318
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
319
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
320
      # the visible ones, for a smoother transition.
80352490   Goutte   Multi model suppo...
321
322
      @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...
323
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
324
    else
a2a08db2   Goutte   Make sure targets...
325
#      @is_invalid = false
243cd8a4   Goutte   Timestamp party c...
326
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
a2a08db2   Goutte   Make sure targets...
327
      console.warn "This might take a good while… Why not see what else we're up to on http://cdpp.eu while you're waiting?"
243cd8a4   Goutte   Timestamp party c...
328
329
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
330

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

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

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


ae0aa7d2   Goutte   Add an x axis lab...
348

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

b60e7acd   Goutte   Rename "source" i...
352

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

7b5642ae   Goutte   Normalize time in...
357
  (@parameter, @title, @target, @data, @visible, @container, @options = {}) ->
bde97e4d   Goutte   Add more changes ...
358
    # parameter : slug of the parameter to observe, like btan or pdyn
c3008fb2   Goutte   Clean up and refa...
359
360
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
361
    # data : list of {x: <datetime>, y: <float>}
cdf79b23   Goutte   Give the future d...
362
363
364
365
366
    # 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...
367
368
    @init()

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

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

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

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

a8ce269b   Goutte   Force time displa...
393
394
395
396
397
398
399
400
    formatMillisecond = d3.utcFormat(".%L")
    formatSecond = d3.utcFormat(":%S")
    formatMinute = d3.utcFormat("%H:%M")
    formatHour = d3.utcFormat("%H:%M")
    formatDay = d3.utcFormat("%a %d")
    formatWeek = d3.utcFormat("%b %d")
    formatMonth = d3.utcFormat("%B")
    formatYear = d3.utcFormat("%Y")
6491a1f1   Goutte   Fix up the bugs l...
401
402
403
404
405
406
407
408
409
410
411
412

    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...
413
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
414
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
415
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
416
417
418
419
420
421
422
423
    @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...
424
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
425

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

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

780828a8   Goutte   Prepare horizonta...
446
447
448
449
450
451
452
453
454
    @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...
455
    @brush = @plotWrapper.append("g")
2038c9fb   Goutte   Add a zoom reset ...
456
                         .attr("class", "brush")
08569a6b   Goutte   Add a zooming bru...
457

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

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

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

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

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

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

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

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

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

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

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

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

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

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

780828a8   Goutte   Prepare horizonta...
527
528
529
530
531
532
533
    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...
534
535
536
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8cb213b9   Goutte   Clean up, and pre...
648
  bisectDate: d3.bisector((d) -> d.x).left  # /!\ complex
a8ce269b   Goutte   Force time displa...
649
  timeFormat: d3.utcFormat("%Y-%m-%d %H:%M")
2463bd16   Goutte   Add a circle foll...
650

541e2936   Goutte   Synchronize the t...
651
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
652
653
654
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
b4abddb7   Goutte   Fix a small issue...
655
656
657
658
    if (not d1) or (not d0)
      @hideCursor()
      return

2463bd16   Goutte   Add a circle foll...
659
660
661
662
    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...
663
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
664

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

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

    this
f1f1e797   Goutte   Rewrite ugly java...
685

ae0aa7d2   Goutte   Add an x axis lab...
686
687
688
689

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

f1f1e797   Goutte   Rewrite ugly java...
690
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
691
692
693
694
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
695

a21f81d9   Goutte   Enable Venus and ...
696
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
697

c3008fb2   Goutte   Clean up and refa...
698
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
699

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

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

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

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

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

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

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

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

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

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

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

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

a2a08db2   Goutte   Make sure targets...
792
793
794
795
    if config.active
      @enableTarget slug
    else
      @disableTarget slug
17c52617   Goutte   Make the orbits p...
796

438929a4   Goutte   Rewrite the orbit...
797
798
    this

a2a08db2   Goutte   Make sure targets...
799
800
801
802
803
804
805
806
  enableTarget: (slug) ->
    @orbiters[slug].enabled = true
    @showOrbiter slug

  disableTarget: (slug) ->
    @orbiters[slug].enabled = false
    @hideOrbiter slug

17c52617   Goutte   Make the orbits p...
807
  showOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
808
    if not @data[slug].length then return
a2a08db2   Goutte   Make sure targets...
809
    if not @orbiters[slug].enabled then return
17c52617   Goutte   Make the orbits p...
810
811
812
813
814
815
816
    @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...
817
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
818
819
820
821
822
823
    @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 ...
824
825
826
  clear: ->
    $(@svg.node()).remove()

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

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

3c2b15fc   Goutte   Make the Y-Axis o...
833
834
835
836
837
838
839
    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...
840
841
842
    @xScale = d3.scaleLinear().domain([-1 * extremum, extremum])
    @yScale = d3.scaleLinear().domain([extremum, -1 * extremum])

b500e561   Goutte   Invert the orbits...
843
    @xScale.range([0, width])
17c52617   Goutte   Make the orbits p...
844
    @yScale.range([height, 0])
f1f1e797   Goutte   Rewrite ugly java...
845
846
847
848

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
854
855
856
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

17c52617   Goutte   Make the orbits p...
857
858
859
860
861
862
863
864
865
    @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...
866

ae0aa7d2   Goutte   Add an x axis lab...
867
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
868
869
870
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
871

f1f1e797   Goutte   Rewrite ugly java...
872
873
    this

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

17c52617   Goutte   Make the orbits p...
879
    tt = @svg.transition().duration(750)
438929a4   Goutte   Rewrite the orbit...
880
    el = @orbitersElements[slug]
17c52617   Goutte   Make the orbits p...
881
882
883
884
885
    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...
886
887
888
889
890
891

    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...
892
893
894
895
896
897
898
899

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

17c52617   Goutte   Make the orbits p...
904
    @repositionOrbiter(slug, null, true)
438929a4   Goutte   Rewrite the orbit...
905
906
907

    this

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

17c52617   Goutte   Make the orbits p...
911
  repositionOrbiter: (slug, datum, animate = false) ->
a21f81d9   Goutte   Enable Venus and ...
912
    data = @data[slug]
dc0be992   Goutte   Support having no...
913
    if not data.length then return
17c52617   Goutte   Make the orbits p...
914
    datum ?= @lastOrbiterData[slug]
ae0aa7d2   Goutte   Add an x axis lab...
915
    datum ?= data[data.length - 1]
17c52617   Goutte   Make the orbits p...
916
917
918
919
920
921
922
    @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...
923
924
925
926
927
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
928
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
929
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
930
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
931
932
933
934
935
      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.
936
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
937
    this
ae0aa7d2   Goutte   Add an x axis lab...
938

8cb213b9   Goutte   Clean up, and pre...
939
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
940
941
942
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
a2a08db2   Goutte   Make sure targets...
943
944
945
      if not data.length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
946
947
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
948
      @showOrbiter(slug)
ae0aa7d2   Goutte   Add an x axis lab...
949

667eeb24   Goutte   Resize the domain...
950
951
952
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
a2a08db2   Goutte   Make sure targets...
953
954
955
      if not @data[slug].length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
956
957
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
958
      @showOrbiter(slug)