Blame view

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

284f4688   Goutte   Continue layers i...
69
  init: (started_at, stopped_at) ->
b60e7acd   Goutte   Rename "source" i...
70
71
72
73
74
    """
    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)
    """
284f4688   Goutte   Continue layers i...
75
76
77
    # We set the h/m/s to zero to ensure we benefit from the daily cache.
    started_at = moment(started_at).hours(0).minutes(0).seconds(0)
    stopped_at = moment(stopped_at).hours(0).minutes(0).seconds(0)
7994cf1a   Goutte   Hunt bugs.
78
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
79
    @loadAndCreatePlots(started_at, stopped_at)
b60e7acd   Goutte   Rename "source" i...
80
    window.addEventListener 'resize', ~> @resize()
fb383448   Goutte   Implement the cac...
81
    this
b60e7acd   Goutte   Rename "source" i...
82
83

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

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

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

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

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

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

fe3132dd   Goutte   Refactor even more.
139
  resize: ->
fb5dc2a0   Goutte   Fix a nasty bug, ...
140
    @orbits?.resize()
80352490   Goutte   Multi model suppo...
141
    @time_series.forEach((ts) -> ts.resize())
d729e9cd   Goutte   Cleanup.
142
    this
fe3132dd   Goutte   Refactor even more.
143

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

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

b60e7acd   Goutte   Rename "source" i...
150
151
152
153
154
  loadData: (target_slug, started_at, stopped_at) ->
    """
    Load the data as CSV for the specified target and interval,
    and return it in a Promise.
    """
243cd8a4   Goutte   Timestamp party c...
155

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

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

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

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

b60e7acd   Goutte   Rename "source" i...
248
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
249
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
250
251
252
      container = @configuration['time_series_container']
      id = parameter['id'] ; title = parameter['title']
      if id not of data then console.error("No data for id '#{id}'.", data)
d1c44c51   Goutte   Enable Earth
253
254
255
256
257
258
259
260
      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.
261
    )
b4abddb7   Goutte   Fix a small issue...
262
263
    # 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...
264
      ts.options['onMouseOver'] = ~>
b4abddb7   Goutte   Fix a small issue...
265
        true  # let's do nothing, we'll show the cursor during moveCursor()
243cd8a4   Goutte   Timestamp party c...
266
      ts.options['onMouseOut'] = ~>
80352490   Goutte   Multi model suppo...
267
        @time_series.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
268
      ts.options['onMouseMove'] = (t) ~>
80352490   Goutte   Multi model suppo...
269
        @time_series.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
270
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
271
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
272
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
273
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
274
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
275
    )
80352490   Goutte   Multi model suppo...
276
    @time_series
4816cef4   Goutte   Refactor some more.
277

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

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

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

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

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

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

243cd8a4   Goutte   Timestamp party c...
337
    this
8cb213b9   Goutte   Clean up, and pre...
338

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

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


ae0aa7d2   Goutte   Add an x axis lab...
354

ae0aa7d2   Goutte   Add an x axis lab...
355
356
357
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
358

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

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

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

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

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

0332f168   Goutte   Initial support f...
387
388
389
390
391
392
393
394
395
    [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, ...
396
397
398
399
400
    # 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...
401
402
    # Domain on a log scale MUST NOT cross zero
#    @yScale = d3.scaleLog().domain(@yDataExtent)
fb5dc2a0   Goutte   Fix a nasty bug, ...
403
    @yScale = d3.scaleLinear().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
404

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

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

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

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

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

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

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

dabb9d5f   Goutte   Fix the layers' c...
459
460
    @createCatalogLayers()

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

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

0332f168   Goutte   Initial support f...
487

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

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

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

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

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

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

0332f168   Goutte   Initial support f...
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
    #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...
535
536
    @resize()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

0332f168   Goutte   Initial support f...
658
659
660
661
662
663
664
665
666
667
668
669
670
  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)
        )
c4983585   Goutte   Hide the layers b...
671
      @hideCatalogLayer(catalog_slug)
0332f168   Goutte   Initial support f...
672
673
674
    this

  createCatalogLayer: (started_at, stopped_at) ->
dabb9d5f   Goutte   Fix the layers' c...
675
    layer_rect = @pathWrapper.append("rect")
0332f168   Goutte   Initial support f...
676
677
                             .attr('y', 0)
                             .attr('height', @plotHeight)
596da00d   Goutte   Add more exceptio...
678
                             .attr('fill', '#FFFD64C2')
0332f168   Goutte   Initial support f...
679
680
681
682
683
684
685
686
687
688
    # 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)
284f4688   Goutte   Continue layers i...
689
        width = Math.max(2, @xScale(stopped_at) - @xScale(started_at))
0332f168   Goutte   Initial support f...
690
        @layers_rects[catalog_slug][i].attr('x', @xScale(started_at))
284f4688   Goutte   Continue layers i...
691
                                      .attr('width', width)
0332f168   Goutte   Initial support f...
692
693
694
695
696
697
698
699
700
701
702
703
    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...
704
705
706
707
708
709
  showCursor: ->
    @focus.style("display", null)

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

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

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

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

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

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

    this
f1f1e797   Goutte   Rewrite ugly java...
747

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
934
935
    this

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

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

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

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

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

    this

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

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

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

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

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

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