Blame view

web/static/js/swapp.ls 35.5 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: ->
fb5dc2a0   Goutte   Fix a nasty bug, ...
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

0332f168   Goutte   Initial support f...
295
296
297
298
299
300
301
302
  showCatalogLayer: (catalog_slug) ->
    @time_series.forEach((ts) -> ts.showCatalogLayer(catalog_slug))
    this

  hideCatalogLayer: (catalog_slug) ->
    @time_series.forEach((ts) -> ts.hideCatalogLayer(catalog_slug))
    this

a06a0a67   Goutte   Prepare the time ...
303
304
305
306
307
  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...
308
309
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
310
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
311
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
312
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
313
      return
1754789b   Goutte   Decorate and clea...
314
315
316
317
    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...
318

243cd8a4   Goutte   Timestamp party c...
319
320
321
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
322

5ef50583   Goutte   Clean up.
323
324
#    if (not @is_invalid) and
    if (@started_at <= started_at <= @stopped_at) and
6bb225d6   Goutte   Link the time ser...
325
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
326
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
327
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
328
      # the visible ones, for a smoother transition.
80352490   Goutte   Multi model suppo...
329
330
      @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...
331
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
332
    else
a2a08db2   Goutte   Make sure targets...
333
#      @is_invalid = false
243cd8a4   Goutte   Timestamp party c...
334
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
a2a08db2   Goutte   Make sure targets...
335
      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...
336
337
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
338

243cd8a4   Goutte   Timestamp party c...
339
    this
8cb213b9   Goutte   Clean up, and pre...
340

6bb225d6   Goutte   Link the time ser...
341
  resetZoom: ->
80352490   Goutte   Multi model suppo...
342
    @time_series.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
343
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
344
345
346
347
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
348
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
349
350
351
352
353
    @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...
354
355


ae0aa7d2   Goutte   Add an x axis lab...
356

ae0aa7d2   Goutte   Add an x axis lab...
357
358
359
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
360

f1f1e797   Goutte   Rewrite ugly java...
361
362
363
364
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
382
383
384
385
    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
386
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
387
388
    }

0332f168   Goutte   Initial support f...
389
390
391
392
393
394
395
396
397
    [width, height] = @recomputeDimensions()

    # Pre-compute extents for performance when zooming.
    # These are final and always hold the biggest extent.
    @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']

fb5dc2a0   Goutte   Fix a nasty bug, ...
398
399
400
401
402
    # https://github.com/d3/d3-scale/blob/master/src/utcTime.js
    # scaleUtc collides with our custom multiFormat ticks
#    @xScale = d3.scaleUtc().domain(@xDataExtent)
    @xScale = d3.scaleTime().domain(@xDataExtent)

ff4f2af5   Goutte   Log problems with...
403
404
    # Domain on a log scale MUST NOT cross zero
#    @yScale = d3.scaleLog().domain(@yDataExtent)
fb5dc2a0   Goutte   Fix a nasty bug, ...
405
    @yScale = d3.scaleLinear().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
406

a8ce269b   Goutte   Force time displa...
407
408
409
410
411
412
413
414
    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...
415
416
417
418
419
420
421
422
423
424
425
426

    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...
427
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
428
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
429
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
430
431
432
    @yAxis = d3.axisLeft()
               .ticks(10)

0332f168   Goutte   Initial support f...
433
434
435
    @svg = d3.select(@container).append('svg')
    @svg.attr("class", "#{@parameter} #{@target.slug}")

f1f1e797   Goutte   Rewrite ugly java...
436
437
438
439
    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

f1f1e797   Goutte   Rewrite ugly java...
440
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
441
442
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
443

123313cb   Goutte   Clip the paths of...
444
445
446
447
448
449
450
    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)

0332f168   Goutte   Initial support f...
451
452
    @createCatalogLayers()

123313cb   Goutte   Clip the paths of...
453
454
455
    @pathWrapper = @plotWrapper.append('g')
    @pathWrapper.attr("clip-path", "url(\##{clipId})")
    @path = @pathWrapper.append('path')
f1f1e797   Goutte   Rewrite ugly java...
456
457
                        .datum(@data)
                        .classed('line', true)
cdf79b23   Goutte   Give the future d...
458
459
460
    @predictiveDataPath = @pathWrapper.append('path')
                                      .datum(@predictiveData)
                                      .classed('predictive-line', true)
f1f1e797   Goutte   Rewrite ugly java...
461

780828a8   Goutte   Prepare horizonta...
462
463
464
465
466
467
468
469
470
    @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...
471
    @brush = @plotWrapper.append("g")
2038c9fb   Goutte   Add a zoom reset ...
472
                         .attr("class", "brush")
08569a6b   Goutte   Add a zooming bru...
473

f1f1e797   Goutte   Rewrite ugly java...
474
475
476
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
477
478
479
480
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
481
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
482
483
484
485
486
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
487

0332f168   Goutte   Initial support f...
488

81c9b2e8   Goutte   Add the values to...
489
490
491
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
495
    dx = 8
81c9b2e8   Goutte   Add the values to...
496
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
497
498
499
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
500

81c9b2e8   Goutte   Add the values to...
501
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
502
503
504
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
505

81c9b2e8   Goutte   Add the values to...
506
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
507
508
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
509
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
510

81c9b2e8   Goutte   Add the values to...
511
512
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
513
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
514
515
                        .attr("dy", "1em")

0332f168   Goutte   Initial support f...
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
    #console.debug "Creating the zooming brush for #{@}…"
    # Note that 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)

    # We're also adding our own cursor events to the brush's overlay,
    # because it captures events and a rect cannot contain another.
    @brushOverlay = @svg.select(".brush .overlay")
    @brushOverlay
        .on("mouseover.swapp", @onMouseOver)
        .on("mouseout.swapp",  @onMouseOut)
        .on("mousemove.swapp", @onMouseMove)
        .on("dblclick.swapp",  @onDoubleClick)

f1f1e797   Goutte   Rewrite ugly java...
536
537
    @resize()

0332f168   Goutte   Initial support f...
538
  recomputeDimensions: ->
123313cb   Goutte   Clip the paths of...
539
540
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
541
542
    @plotWidth = width
    @plotHeight = height
0332f168   Goutte   Initial support f...
543
544
545
546
547
    [width, height]

  RATIO = Math.pow(GOLDEN_RATIO, 4)
  resize: ->
    [width, height] = @recomputeDimensions()
f1f1e797   Goutte   Rewrite ugly java...
548

123313cb   Goutte   Clip the paths of...
549
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
550
551
552
553
554
555
556

    @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...
557
558
559
    @clip.attr("width", width)
         .attr("height", height)

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

780828a8   Goutte   Prepare horizonta...
563
564
565
566
567
568
569
    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...
570
571
572
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
573
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
034132c6   Goutte   More changes from...
574
    @xAxis.ticks(Math.floor(width / 80.0))  # not working as expected
d49a163c   Goutte   Fix the resize an...
575
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
576
577
578
579
580
581
582
583

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

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

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

b60e7acd   Goutte   Rename "source" i...
587
    @yAxisTextTarget.attr("y", 0 - @margin.left)
0332f168   Goutte   Initial support f...
588
                    .attr("x", 0 - (height / 2.0))
08569a6b   Goutte   Add a zooming bru...
589

0332f168   Goutte   Initial support f...
590
    @resizeCatalogLayers()
b7fe650c   Goutte   Misc bundle of ol...
591

6bb225d6   Goutte   Link the time ser...
592
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
593
594
    this

2c0e1515   Goutte   Refactor loading ...
595
596
597
598
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
599
600
601
602
603
604
605
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
607
  onMouseMove: ~>
0332f168   Goutte   Initial support f...
608
    x = @xScale.invert(d3.mouse(@brushOverlay.node())[0])
c3008fb2   Goutte   Clean up and refa...
609
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
610
611

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

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

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

08569a6b   Goutte   Add a zooming bru...
620
621
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
622
623
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
624
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
625
626
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
627
628
629

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
630
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
631
632
633
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
2d8a5753   Goutte   Make the Y-Axis o...
634
    @yScale.domain(d3.extent(@data, (d) -> if startDate <= d.x <= stopDate then d.y else 0))
2038c9fb   Goutte   Add a zoom reset ...
635
636
637
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
638
639
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
640
641
642
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
643
    if @visible
c3008fb2   Goutte   Clean up and refa...
644
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
645
646
647
648
      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...
649
      @predictiveDataPath.transition(t).attr('d', @line)
6bb225d6   Goutte   Link the time ser...
650
    else
c3008fb2   Goutte   Clean up and refa...
651
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
652
653
654
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
cdf79b23   Goutte   Give the future d...
655
      @predictiveDataPath.attr('d', @line)
0332f168   Goutte   Initial support f...
656
    @resizeCatalogLayers()
375dcd95   Goutte   Fix a small graph...
657
    @hideCursor()
08569a6b   Goutte   Add a zooming bru...
658

0332f168   Goutte   Initial support f...
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
  createCatalogLayers: ->
    @layers_rects = {}
    for catalog_slug, layers of @target.config.layers
      #console.debug("Creating layers of #{catalog_slug}…", layers)
      @layers_rects[catalog_slug] = []
      for layer in layers
        started_at = moment(layer.start)
        stopped_at = moment(layer.stop)
        #console.debug(started_at, stopped_at)
        #console.debug(layer.start, layer.stop)
        @layers_rects[catalog_slug].push(
          @createCatalogLayer(started_at, stopped_at)
        )
    this

  createCatalogLayer: (started_at, stopped_at) ->
    layer_rect = @plotWrapper.append("rect")
                             .attr('y', 0)
                             .attr('height', @plotHeight)
                             .attr('fill', '#FFFD6492')
    # Not triggered, mouse events are captured before they reach this rect
    #layer_rect.append('svg:title').text("I AM TEXT")
    layer_rect

  resizeCatalogLayers: ->
    for catalog_slug, layers of @target.config.layers
      #console.debug("Resizing layers of #{catalog_slug}…", layers)
      for layer, i in layers
        started_at = moment(layer.start)
        stopped_at = moment(layer.stop)
        @layers_rects[catalog_slug][i].attr('x', @xScale(started_at))
                                      .attr('width', @xScale(stopped_at) - @xScale(started_at))
    this

  showCatalogLayer: (catalog_slug) ->
    for layer, i in @target.config.layers[catalog_slug]
      @layers_rects[catalog_slug][i].style("display", null)
    this

  hideCatalogLayer: (catalog_slug) ->
    for layer, i in @target.config.layers[catalog_slug]
      @layers_rects[catalog_slug][i].style("display", "none")
    this

541e2936   Goutte   Synchronize the t...
703
704
705
706
707
708
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
712
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
713
714
715
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
b4abddb7   Goutte   Fix a small issue...
716
717
718
719
    if (not d1) or (not d0)
      @hideCursor()
      return

2463bd16   Goutte   Add a circle foll...
720
721
722
723
    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...
724
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
725

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

8cb213b9   Goutte   Clean up, and pre...
729
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
730
731
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
732
733
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
734
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
735
736
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
737
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
738
739
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
740
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
741
742
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
b4abddb7   Goutte   Fix a small issue...
743
    @showCursor()
2463bd16   Goutte   Add a circle foll...
744
745

    this
f1f1e797   Goutte   Rewrite ugly java...
746

ae0aa7d2   Goutte   Add an x axis lab...
747
748
749
750

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

f1f1e797   Goutte   Rewrite ugly java...
751
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
752
753
754
755
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
756

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

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

b500e561   Goutte   Invert the orbits...
761
762
763
    # 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...
764
765
766
    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
767
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
768
769
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
770

6491a1f1   Goutte   Fix up the bugs l...
771
    @data = {}  # slug => HEE array
a21f81d9   Goutte   Enable Venus and ...
772
    @orbiters = {}  # slug => config
17c52617   Goutte   Make the orbits p...
773
774
775
776
777
    @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...
778
779
780
781

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

f1f1e797   Goutte   Rewrite ugly java...
782
783
784
785
786
    @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...
787
788
789
790
791
    @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...
792
    @xAxisTitle.append('tspan').text('Y')
ae0aa7d2   Goutte   Add an x axis lab...
793
794
795
    # 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 ...
796
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
797
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
798

11662eed   Goutte   Add Y axis label ...
799
800
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
b500e561   Goutte   Invert the orbits...
801
    @yAxisTitle.append('tspan').text('X')
11662eed   Goutte   Add Y axis label ...
802
803
804
805
    @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...
806
807
808
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
809
    @sun.append('svg:title').text("Sun")
f1f1e797   Goutte   Rewrite ugly java...
810

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

a21f81d9   Goutte   Enable Venus and ...
814
  initOrbiter: (slug, config, data) ->
dc0be992   Goutte   Support having no...
815
816
817
818
819
820
821
822
823
    @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...
824
825
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

438929a4   Goutte   Rewrite the orbit...
826
827
828
829
830
831
    # 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...
832
    orbiter.append('svg:title').text(config.name)
438929a4   Goutte   Rewrite the orbit...
833
834

    orbit_line = d3.line()
b500e561   Goutte   Invert the orbits...
835
836
                   .x((d) ~> @xScale(d.y))
                   .y((d) ~> @yScale(d.x))
438929a4   Goutte   Rewrite the orbit...
837
838

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

438929a4   Goutte   Rewrite the orbit...
842
843
844
845
846
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line
17c52617   Goutte   Make the orbits p...
847
848
849
    @orbitersExtrema[slug] = d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    )
a21f81d9   Goutte   Enable Venus and ...
850

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

a2a08db2   Goutte   Make sure targets...
853
854
855
856
    if config.active
      @enableTarget slug
    else
      @disableTarget slug
17c52617   Goutte   Make the orbits p...
857

438929a4   Goutte   Rewrite the orbit...
858
859
    this

a2a08db2   Goutte   Make sure targets...
860
861
862
863
864
865
866
867
  enableTarget: (slug) ->
    @orbiters[slug].enabled = true
    @showOrbiter slug

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

17c52617   Goutte   Make the orbits p...
868
  showOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
869
    if not @data[slug].length then return
a2a08db2   Goutte   Make sure targets...
870
    if not @orbiters[slug].enabled then return
17c52617   Goutte   Make the orbits p...
871
872
873
874
875
876
877
    @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...
878
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
879
880
881
882
883
884
    @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 ...
885
886
887
  clear: ->
    $(@svg.node()).remove()

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

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

3c2b15fc   Goutte   Make the Y-Axis o...
894
895
896
897
898
899
900
    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...
901
902
903
    @xScale = d3.scaleLinear().domain([-1 * extremum, extremum])
    @yScale = d3.scaleLinear().domain([extremum, -1 * extremum])

b500e561   Goutte   Invert the orbits...
904
    @xScale.range([0, width])
17c52617   Goutte   Make the orbits p...
905
    @yScale.range([height, 0])
f1f1e797   Goutte   Rewrite ugly java...
906
907
908
909

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

0332f168   Goutte   Initial support f...
910
    @sun.attr("x", width / 2.0 - 16).attr("y", height / 2.0 - 16)
f1f1e797   Goutte   Rewrite ugly java...
911

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

f1f1e797   Goutte   Rewrite ugly java...
915
916
917
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

17c52617   Goutte   Make the orbits p...
918
919
920
921
922
923
924
925
926
    @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...
927

ae0aa7d2   Goutte   Add an x axis lab...
928
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
929
930
931
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
932

f1f1e797   Goutte   Rewrite ugly java...
933
934
    this

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

17c52617   Goutte   Make the orbits p...
940
    tt = @svg.transition().duration(750)
438929a4   Goutte   Rewrite the orbit...
941
    el = @orbitersElements[slug]
17c52617   Goutte   Make the orbits p...
942
943
944
945
946
    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...
947
948
949
950
951
952

    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...
953
954
955
956
957
958
959
960

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

17c52617   Goutte   Make the orbits p...
965
    @repositionOrbiter(slug, null, true)
438929a4   Goutte   Rewrite the orbit...
966
967
968

    this

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

17c52617   Goutte   Make the orbits p...
972
  repositionOrbiter: (slug, datum, animate = false) ->
a21f81d9   Goutte   Enable Venus and ...
973
    data = @data[slug]
dc0be992   Goutte   Support having no...
974
    if not data.length then return
17c52617   Goutte   Make the orbits p...
975
    datum ?= @lastOrbiterData[slug]
ae0aa7d2   Goutte   Add an x axis lab...
976
    datum ?= data[data.length - 1]
17c52617   Goutte   Make the orbits p...
977
978
979
980
981
982
983
    @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...
984
985
986
987
988
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
989
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
990
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
991
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
992
993
994
995
996
      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.
997
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
998
    this
ae0aa7d2   Goutte   Add an x axis lab...
999

8cb213b9   Goutte   Clean up, and pre...
1000
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
1001
1002
1003
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
a2a08db2   Goutte   Make sure targets...
1004
1005
1006
      if not data.length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
1007
1008
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
1009
      @showOrbiter(slug)
ae0aa7d2   Goutte   Add an x axis lab...
1010

667eeb24   Goutte   Resize the domain...
1011
1012
1013
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
a2a08db2   Goutte   Make sure targets...
1014
1015
1016
      if not @data[slug].length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
1017
1018
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
1019
      @showOrbiter(slug)