Blame view

web/static/js/swapp.ls 36.2 KB
4bbc0e4a   Goutte   Continue THE GREA...
1
2
3
4
5
6
7
8
9
10
11
12
13
14














11d86851   Goutte   Add support for s...
15
# DEPRECATED and NOT USED ANYMORE
4bbc0e4a   Goutte   Continue THE GREA...
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Use web/static/js/main.js instead.















f1f1e797   Goutte   Rewrite ugly java...
32
# Livescript transpiles to javascript, and is easier on the eyes and brain.
4cf497e0   Goutte   Make the targets ...
33
# Get the `lsc` binary from here : http://livescript.net
8cb213b9   Goutte   Clean up, and pre...
34
# It is quite close to Python, syntax-wise, and full of sugar.
f1f1e797   Goutte   Rewrite ugly java...
35

4cf497e0   Goutte   Make the targets ...
36
# To transpile this file to javascript, and generate `swapp.js` :
a21f81d9   Goutte   Enable Venus and ...
37
# $ lsc --compile swapp.ls
4cf497e0   Goutte   Make the targets ...
38
39
# You can have it watch for changes and transpile automatically :
# $ lsc --watch --compile swapp.ls
a21f81d9   Goutte   Enable Venus and ...
40

4cf497e0   Goutte   Make the targets ...
41
# All the "javascript" code is in this file, except for inline scripts in
2d8a5753   Goutte   Make the Y-Axis o...
42
# templates, such as in `home.html.jinja2`.
4cf497e0   Goutte   Make the targets ...
43

8cb213b9   Goutte   Clean up, and pre...
44
# Note: We use Promises and ES6 whenever relevant.
cad44b6d   Goutte   Finish adding the...
45
# You also WILL NEED d3js v4 documentation : https://d3js.org/
9c0c4509   Goutte   Add a loader to t...
46
47
48
# 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...
49

4cf497e0   Goutte   Make the targets ...
50
51
###############################################################################

d729e9cd   Goutte   Cleanup.
52
const GOLDEN_RATIO = 2 / (1 + Math.sqrt(5))  # Between 0 and 1 (0.618…)
f1f1e797   Goutte   Rewrite ugly java...
53

ae0aa7d2   Goutte   Add an x axis lab...
54
55
###############################################################################

b60e7acd   Goutte   Rename "source" i...
56
57
class Target
  (@slug, @name, @config) ->
2c0e1515   Goutte   Refactor loading ...
58
    @active = @config.active
b60e7acd   Goutte   Rename "source" i...
59
60
61

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

ae0aa7d2   Goutte   Add an x axis lab...
62
export class SpaceWeather
4cf497e0   Goutte   Make the targets ...
63
64
  """
  The main app, instanciated from an inline script.
780828a8   Goutte   Prepare horizonta...
65
  It defaults to an interval starting two months ago, and ending in a month.
6bb225d6   Goutte   Link the time ser...
66
  (both at midnight)
4cf497e0   Goutte   Make the targets ...
67
  """
ae0aa7d2   Goutte   Add an x axis lab...
68

2c0e1515   Goutte   Refactor loading ...
69
  API_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss"
243cd8a4   Goutte   Timestamp party c...
70
  INPUT_TIME_FORMAT = "YYYY-MM-DD"
2c0e1515   Goutte   Refactor loading ...
71

f75faf5f   Goutte   WIP
72
  (@configuration) ->
05e269d1   Goutte   Show an error mes...
73
    console.info """©2017
2038c9fb   Goutte   Add a zoom reset ...
74
75
76
77
78
79
80
81
82
83
84
85
86
  _   _      _ _       ____
 | | | | ___| (_) ___ |  _ \\ _ __ ___  _ __   __ _
 | |_| |/ _ \\ | |/ _ \\| |_) | '__/ _ \\| '_ \\ / _` |
 |  _  |  __/ | | (_) |  __/| | | (_) | |_) | (_| |
 |_| |_|\\___|_|_|\\___/|_|_  |_|_ \\___/| .__/ \\__,_|
 | |__  _   _   / ___|  _ \\|  _ \\|  _ \\_|
 | '_ \\| | | | | |   | | | | |_) | |_) |
 | |_) | |_| | | |___| |_| |  __/|  __/
 |_.__/ \\__, |  \\____|____/|_|   |_|
        |___/

The full source of this website is available at :
https://gitlab.irap.omp.eu/CDPP/SPACEWEATHERONLINE
6bb225d6   Goutte   Link the time ser...
87
"""  # HelioPropa by CDPP (mushed 'cause we need to escape backslashes)
b60e7acd   Goutte   Rename "source" i...
88
89
    @targets = {}
    configs = [@configuration.targets[k] for k of @configuration.targets]
cad44b6d   Goutte   Finish adding the...
90
91
92
    configs.forEach (target_config) ~>
      @addTarget(new Target(target_config.slug, target_config.name, target_config))

b7fe650c   Goutte   Misc bundle of ol...
93
    @parameters = {}
cad44b6d   Goutte   Finish adding the...
94
95
96
    @configuration['parameters'].forEach (p) ~>
      @parameters[p['id']] = p

3c2b15fc   Goutte   Make the Y-Axis o...
97
98
    @orbits = null     # an Orbiter instance (defined below)
    @time_series = []  # a List of TimeSeries instances
ae0aa7d2   Goutte   Add an x axis lab...
99

284f4688   Goutte   Continue layers i...
100
  init: (started_at, stopped_at) ->
b60e7acd   Goutte   Rename "source" i...
101
102
103
104
105
    """
    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...
106
107
108
    # 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.
109
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
110
    @loadAndCreatePlots(started_at, stopped_at)
b60e7acd   Goutte   Rename "source" i...
111
    window.addEventListener 'resize', ~> @resize()
fb383448   Goutte   Implement the cac...
112
    this
b60e7acd   Goutte   Rename "source" i...
113
114

  buildDataUrlForTarget: (target_slug, started_at, stopped_at) ->
f75faf5f   Goutte   WIP
115
    url = @configuration['api']['data_for_interval']
b60e7acd   Goutte   Rename "source" i...
116
    url = url.replace('<target>', target_slug)
a4a9ef03   Goutte   Cache generated C...
117
118
    url = url.replace('<started_at>', started_at)
    url = url.replace('<stopped_at>', stopped_at)
f75faf5f   Goutte   WIP
119
    url
ae0aa7d2   Goutte   Add an x axis lab...
120

6b149919   Goutte   Add a Download bu...
121
122
123
124
125
126
127
128
129
  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...
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
  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...
146
147
  addTarget: (target) ->
    @targets[target.slug] = target
f75faf5f   Goutte   WIP
148
149
    this

2c0e1515   Goutte   Refactor loading ...
150
151
152
153
#  enableAllTargets: ->
#    for slug, target of @targets
#      enableTarget(slug)
#    this
f75faf5f   Goutte   WIP
154

cad44b6d   Goutte   Finish adding the...
155
156
157
  getEnabledTargets: ->
    [target for slug, target of @targets when target.active]

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

6bb225d6   Goutte   Link the time ser...
164
  disableTarget: (target_slug) ->
80352490   Goutte   Multi model suppo...
165
    @time_series.forEach((ts) -> ts.hide() if ts.target.slug == target_slug)
b60e7acd   Goutte   Rename "source" i...
166
    @targets[target_slug].active = false
a2a08db2   Goutte   Make sure targets...
167
    @orbits?.disableTarget target_slug
f75faf5f   Goutte   WIP
168
169
    this

fe3132dd   Goutte   Refactor even more.
170
  resize: ->
fb5dc2a0   Goutte   Fix a nasty bug, ...
171
    @orbits?.resize()
80352490   Goutte   Multi model suppo...
172
    @time_series.forEach((ts) -> ts.resize())
d729e9cd   Goutte   Cleanup.
173
    this
fe3132dd   Goutte   Refactor even more.
174

2c0e1515   Goutte   Refactor loading ...
175
  showLoader: ->
d729e9cd   Goutte   Cleanup.
176
    $('#plots_loader').show()
2c0e1515   Goutte   Refactor loading ...
177

9c0c4509   Goutte   Add a loader to t...
178
  hideLoader: ->
d729e9cd   Goutte   Cleanup.
179
    $('#plots_loader').hide()
9c0c4509   Goutte   Add a loader to t...
180

b60e7acd   Goutte   Rename "source" i...
181
182
183
184
185
  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...
186

f75faf5f   Goutte   WIP
187
    sw = this
2c0e1515   Goutte   Refactor loading ...
188
    new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
189
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
190
      d3.csv(url, (csv) ->
60b73eb1   Goutte   Change temperatur...
191
        console.debug("Requested CSV for #{target_slug}…", csv)
a8ce269b   Goutte   Force time displa...
192
        timeFormat = d3.utcParse('%Y-%m-%dT%H:%M:%S%Z')
6491a1f1   Goutte   Fix up the bugs l...
193
        data = { 'hee': [] }
f75faf5f   Goutte   WIP
194
        configuration['parameters'].forEach((parameter) ->
06a3a1f1   Goutte   Continue bug hunt...
195
          data[parameter['id']] = []
f75faf5f   Goutte   WIP
196
        )
80352490   Goutte   Multi model suppo...
197
198
        unless csv then reject 'invalid'
        unless csv.length then reject 'empty'
f75faf5f   Goutte   WIP
199
200
201
202
        csv.forEach((d) ->
          dtime = timeFormat(d['time'])
          configuration['parameters'].forEach((parameter) ->
            id = parameter['id']
d1c44c51   Goutte   Enable Earth
203
204
205
            val = parseFloat(d[id])
            if not isNaN(val)
              data[id].push({x: dtime, y: val})
f75faf5f   Goutte   WIP
206
          )
6491a1f1   Goutte   Fix up the bugs l...
207
208
209
          if d['xhee'] and d['yhee']
            data['hee'].push({
              t: dtime, x: parseFloat(d['xhee']), y: parseFloat(d['yhee'])
06a3a1f1   Goutte   Continue bug hunt...
210
            })
f75faf5f   Goutte   WIP
211
        )
9bfa6c42   Goutte   More bug hunting.
212
        resolve data
f75faf5f   Goutte   WIP
213
214
      )
    )
2c0e1515   Goutte   Refactor loading ...
215
216

  loadAndCreatePlots: (started_at, stopped_at) ->
243cd8a4   Goutte   Timestamp party c...
217
    """
80352490   Goutte   Multi model suppo...
218
219
    started_at: moment(.js) datetime object
    stopped_at: moment(.js) datetime object
243cd8a4   Goutte   Timestamp party c...
220
    """
2c0e1515   Goutte   Refactor loading ...
221
    @showLoader()
7994cf1a   Goutte   Hunt bugs.
222
223
224
    @started_at = started_at
    @stopped_at = stopped_at
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
243cd8a4   Goutte   Timestamp party c...
225
226
    started_at = started_at.format(API_TIME_FORMAT)
    stopped_at = stopped_at.format(API_TIME_FORMAT)
60b73eb1   Goutte   Change temperatur...
227
228
229

    targets = [@targets[k] for k of @targets]
    targets.forEach((target) ~>
4900d232   Goutte   Add the time inte...
230
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
231
      targetButton.addClass('loading')
60b73eb1   Goutte   Change temperatur...
232
233
234
235
236
237
238
      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 ...
239
240
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
9bfa6c42   Goutte   More bug hunting.
241
          console.info "Loaded CSV data of #{target.name}.", data
2c0e1515   Goutte   Refactor loading ...
242
          @createTimeSeries(target, data)
6491a1f1   Goutte   Fix up the bugs l...
243
          @orbits.initOrbiter(target.slug, target.config, data['hee'])
2c0e1515   Goutte   Refactor loading ...
244
          targetButton.removeClass('loading')
5e099488   Goutte   Fix that loading ...
245
          if target.active then @hideLoader() else @disableTarget(target.slug)
60b73eb1   Goutte   Change temperatur...
246
          handleTarget(i+1)
2c0e1515   Goutte   Refactor loading ...
247
        ,
9bfa6c42   Goutte   More bug hunting.
248
        (error) ~>
80352490   Goutte   Multi model suppo...
249
250
251
252
253
          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...
254
255
              # alert("There was an error with #{target.name}.\nPlease retry in a few moments.")
              targetButton.addClass('error')
a2a08db2   Goutte   Make sure targets...
256
#              @is_invalid = true
80352490   Goutte   Multi model suppo...
257
258
259
260
261
262
263
              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 ...
264
          targetButton.addClass('failed')
9bfa6c42   Goutte   More bug hunting.
265
266
          targetButton.removeClass('loading')
          @hideLoader()
60b73eb1   Goutte   Change temperatur...
267
          handleTarget(i+1)
2c0e1515   Goutte   Refactor loading ...
268
      )
60b73eb1   Goutte   Change temperatur...
269
    handleTarget(0)
60b73eb1   Goutte   Change temperatur...
270
    this
2c0e1515   Goutte   Refactor loading ...
271
272
273

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

b60e7acd   Goutte   Rename "source" i...
279
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
280
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
281
282
283
      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
284
285
286
287
288
289
290
291
      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.
292
    )
b4abddb7   Goutte   Fix a small issue...
293
294
    # 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...
295
      ts.options['onMouseOver'] = ~>
b4abddb7   Goutte   Fix a small issue...
296
        true  # let's do nothing, we'll show the cursor during moveCursor()
243cd8a4   Goutte   Timestamp party c...
297
      ts.options['onMouseOut'] = ~>
80352490   Goutte   Multi model suppo...
298
        @time_series.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
299
      ts.options['onMouseMove'] = (t) ~>
80352490   Goutte   Multi model suppo...
300
        @time_series.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
301
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
302
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
303
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
304
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
305
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
306
    )
80352490   Goutte   Multi model suppo...
307
    @time_series
4816cef4   Goutte   Refactor some more.
308

cad44b6d   Goutte   Finish adding the...
309
310
311
  getEnabledParameters: ->
    [p for slug, p in @parameters when p.active]

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

b7fe650c   Goutte   Misc bundle of ol...
318
319
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
320
    @parameters[parameter_slug].active = false
80352490   Goutte   Multi model suppo...
321
    @time_series.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
322
    this
ae0aa7d2   Goutte   Add an x axis lab...
323

0332f168   Goutte   Initial support f...
324
325
326
327
328
329
330
331
  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 ...
332
333
334
335
336
  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...
337
338
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
339
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
340
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
341
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
342
      return
1754789b   Goutte   Decorate and clea...
343
344
345
346
    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...
347

243cd8a4   Goutte   Timestamp party c...
348
349
350
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
351

5ef50583   Goutte   Clean up.
352
353
#    if (not @is_invalid) and
    if (@started_at <= started_at <= @stopped_at) and
6bb225d6   Goutte   Link the time ser...
354
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
355
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
356
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
357
      # the visible ones, for a smoother transition.
221bfc4d   Goutte   Try even more fix...
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
      #@time_series.forEach((ts) -> if not ts.visible then ts.zoomIn(started_at, stopped_at))

      tsv = @time_series.filter((ts) -> ts.visible)
      tsv_cursor = 0
      tsv_length = tsv.length
      zoomedOnVisible = new Promise((resolve, reject) ->

        tsv_zoom_on_next = (i) ->
          if i >= tsv_length
            resolve()
            return
          ts = tsv[i]
          ts.zoomIn(started_at, stopped_at)
            .then(-> tsv_zoom_on_next(i+1))
        tsv_zoom_on_next(0)

      )

      zoomedOnVisible.then(->
        @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...
381
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
382
    else
a2a08db2   Goutte   Make sure targets...
383
#      @is_invalid = false
243cd8a4   Goutte   Timestamp party c...
384
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
a2a08db2   Goutte   Make sure targets...
385
      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...
386
387
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
388

243cd8a4   Goutte   Timestamp party c...
389
    this
8cb213b9   Goutte   Clean up, and pre...
390

6bb225d6   Goutte   Link the time ser...
391
  resetZoom: ->
80352490   Goutte   Multi model suppo...
392
    @time_series.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
393
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
394
395
396
397
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
398
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
399
400
401
402
403
    @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...
404
405


ae0aa7d2   Goutte   Add an x axis lab...
406

ae0aa7d2   Goutte   Add an x axis lab...
407
408
409
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
410

f1f1e797   Goutte   Rewrite ugly java...
411
412
413
414
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

7b5642ae   Goutte   Normalize time in...
415
  (@parameter, @title, @target, @data, @visible, @container, @options = {}) ->
bde97e4d   Goutte   Add more changes ...
416
    # parameter : slug of the parameter to observe, like btan or pdyn
c3008fb2   Goutte   Clean up and refa...
417
418
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
419
    # data : list of {x: <datetime>, y: <float>}
cdf79b23   Goutte   Give the future d...
420
421
422
423
424
    # 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...
425
426
    @init()

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

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

f1f1e797   Goutte   Rewrite ugly java...
432
433
434
435
    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
436
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
437
438
    }

0332f168   Goutte   Initial support f...
439
440
441
442
443
444
445
446
447
    [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, ...
448
449
450
451
452
    # 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...
453
454
    # Domain on a log scale MUST NOT cross zero
#    @yScale = d3.scaleLog().domain(@yDataExtent)
fb5dc2a0   Goutte   Fix a nasty bug, ...
455
    @yScale = d3.scaleLinear().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
456

a8ce269b   Goutte   Force time displa...
457
458
459
460
461
462
463
464
    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...
465
466
467
468
469
470
471
472
473
474
475
476

    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...
477
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
478
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
479
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
480
481
482
    @yAxis = d3.axisLeft()
               .ticks(10)

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

f1f1e797   Goutte   Rewrite ugly java...
486
487
488
489
    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

f1f1e797   Goutte   Rewrite ugly java...
490
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
491
492
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
493

123313cb   Goutte   Clip the paths of...
494
495
496
497
498
499
500
    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...
501

123313cb   Goutte   Clip the paths of...
502
503
504
    @pathWrapper = @plotWrapper.append('g')
    @pathWrapper.attr("clip-path", "url(\##{clipId})")
    @path = @pathWrapper.append('path')
f1f1e797   Goutte   Rewrite ugly java...
505
506
                        .datum(@data)
                        .classed('line', true)
cdf79b23   Goutte   Give the future d...
507
508
509
    @predictiveDataPath = @pathWrapper.append('path')
                                      .datum(@predictiveData)
                                      .classed('predictive-line', true)
f1f1e797   Goutte   Rewrite ugly java...
510

dabb9d5f   Goutte   Fix the layers' c...
511
512
    @createCatalogLayers()

780828a8   Goutte   Prepare horizonta...
513
514
515
516
517
518
519
520
521
    @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...
522
    @brush = @plotWrapper.append("g")
2038c9fb   Goutte   Add a zoom reset ...
523
                         .attr("class", "brush")
08569a6b   Goutte   Add a zooming bru...
524

f1f1e797   Goutte   Rewrite ugly java...
525
526
527
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
528
529
530
531
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
532
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
533
534
535
536
537
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
538

0332f168   Goutte   Initial support f...
539

81c9b2e8   Goutte   Add the values to...
540
541
542
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
546
    dx = 8
81c9b2e8   Goutte   Add the values to...
547
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
548
549
550
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
551

81c9b2e8   Goutte   Add the values to...
552
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
553
554
555
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
556

81c9b2e8   Goutte   Add the values to...
557
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
558
559
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
560
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
561

81c9b2e8   Goutte   Add the values to...
562
563
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
564
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
565
566
                        .attr("dy", "1em")

0332f168   Goutte   Initial support f...
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
    #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...
587
588
    @resize()

0332f168   Goutte   Initial support f...
589
  recomputeDimensions: ->
123313cb   Goutte   Clip the paths of...
590
591
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
592
593
    @plotWidth = width
    @plotHeight = height
0332f168   Goutte   Initial support f...
594
595
596
597
598
    [width, height]

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

123313cb   Goutte   Clip the paths of...
600
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
601
602
603
604
605
606
607

    @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...
608
609
610
    @clip.attr("width", width)
         .attr("height", height)

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

780828a8   Goutte   Prepare horizonta...
614
615
616
617
618
619
620
    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...
621
622
623
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
624
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
034132c6   Goutte   More changes from...
625
    @xAxis.ticks(Math.floor(width / 80.0))  # not working as expected
d49a163c   Goutte   Fix the resize an...
626
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
627
628
629
630
631
632
633
634

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

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

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

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

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

6bb225d6   Goutte   Link the time ser...
643
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
644
645
    this

2c0e1515   Goutte   Refactor loading ...
646
647
648
649
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
650
651
652
653
654
655
656
  show: ->
    $(@svg.node()).show()
    @visible = true

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

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

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

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

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

08569a6b   Goutte   Add a zooming bru...
671
672
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
673
674
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
675
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
676
677
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
678
679
680

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
681
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
682
683
684
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
2d8a5753   Goutte   Make the Y-Axis o...
685
    @yScale.domain(d3.extent(@data, (d) -> if startDate <= d.x <= stopDate then d.y else 0))
2038c9fb   Goutte   Add a zoom reset ...
686
687
688
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
689
690
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
691
692
693
    @applyZoom()

  applyZoom: ->
221bfc4d   Goutte   Try even more fix...
694
    duration = 0
6bb225d6   Goutte   Link the time ser...
695
    if @visible
221bfc4d   Goutte   Try even more fix...
696
      duration = 750
c3008fb2   Goutte   Clean up and refa...
697
      console.debug("Applying zoom to visible #{@}…")
221bfc4d   Goutte   Try even more fix...
698
      t = @svg.transition().duration(duration)
c4983585   Goutte   Hide the layers b...
699
700
      @svg.select('.x.axis').transition(t).call(@xAxis)
      @svg.select('.y.axis').transition(t).call(@yAxis)
6bb225d6   Goutte   Link the time ser...
701
      @path.transition(t).attr('d', @line)
cdf79b23   Goutte   Give the future d...
702
      @predictiveDataPath.transition(t).attr('d', @line)
6bb225d6   Goutte   Link the time ser...
703
    else
c3008fb2   Goutte   Clean up and refa...
704
      console.debug("Applying zoom to hidden #{@}…")
c4983585   Goutte   Hide the layers b...
705
706
      @svg.select('.x.axis').call(@xAxis)
      @svg.select('.y.axis').call(@yAxis)
6bb225d6   Goutte   Link the time ser...
707
      @path.attr('d', @line)
cdf79b23   Goutte   Give the future d...
708
      @predictiveDataPath.attr('d', @line)
0332f168   Goutte   Initial support f...
709
    @resizeCatalogLayers()
375dcd95   Goutte   Fix a small graph...
710
    @hideCursor()
221bfc4d   Goutte   Try even more fix...
711
712
713
714
715
716
717
    new Promise((resolve, reject) ->
        if 0 == duration
            resolve()
        else
            setTimeout((-> resolve()), duration+50)
    )

08569a6b   Goutte   Add a zooming bru...
718

0332f168   Goutte   Initial support f...
719
720
721
722
723
724
725
726
727
728
729
730
731
  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...
732
      @hideCatalogLayer(catalog_slug)
0332f168   Goutte   Initial support f...
733
734
735
    this

  createCatalogLayer: (started_at, stopped_at) ->
dabb9d5f   Goutte   Fix the layers' c...
736
    layer_rect = @pathWrapper.append("rect")
0332f168   Goutte   Initial support f...
737
738
                             .attr('y', 0)
                             .attr('height', @plotHeight)
596da00d   Goutte   Add more exceptio...
739
                             .attr('fill', '#FFFD64C2')
221bfc4d   Goutte   Try even more fix...
740
    # ↓ Not triggered, mouse events are captured before they reach this rect
0332f168   Goutte   Initial support f...
741
742
743
744
745
746
747
748
749
    #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...
750
        width = Math.max(2, @xScale(stopped_at) - @xScale(started_at))
0332f168   Goutte   Initial support f...
751
        @layers_rects[catalog_slug][i].attr('x', @xScale(started_at))
284f4688   Goutte   Continue layers i...
752
                                      .attr('width', width)
0332f168   Goutte   Initial support f...
753
754
755
756
757
758
759
760
761
762
763
764
    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...
765
766
767
768
769
770
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
774
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
775
776
777
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
b4abddb7   Goutte   Fix a small issue...
778
779
780
781
    if (not d1) or (not d0)
      @hideCursor()
      return

2463bd16   Goutte   Add a circle foll...
782
783
784
785
    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...
786
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
787

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

8cb213b9   Goutte   Clean up, and pre...
791
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
792
793
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
794
795
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
796
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
797
798
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
799
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
800
801
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
802
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
803
804
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
b4abddb7   Goutte   Fix a small issue...
805
    @showCursor()
2463bd16   Goutte   Add a circle foll...
806
807

    this
f1f1e797   Goutte   Rewrite ugly java...
808

ae0aa7d2   Goutte   Add an x axis lab...
809
810
811
812

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

f1f1e797   Goutte   Rewrite ugly java...
813
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
814
815
816
817
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
818

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

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

b500e561   Goutte   Invert the orbits...
823
824
825
    # 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...
826
827
828
    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
829
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
830
831
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
832

6491a1f1   Goutte   Fix up the bugs l...
833
    @data = {}  # slug => HEE array
a21f81d9   Goutte   Enable Venus and ...
834
    @orbiters = {}  # slug => config
17c52617   Goutte   Make the orbits p...
835
836
837
838
839
    @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...
840
841
842
843

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

f1f1e797   Goutte   Rewrite ugly java...
844
845
846
847
848
    @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...
849
850
851
852
853
    @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...
854
    @xAxisTitle.append('tspan').text('Y')
ae0aa7d2   Goutte   Add an x axis lab...
855
856
857
    # 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 ...
858
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
859
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
860

11662eed   Goutte   Add Y axis label ...
861
862
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
b500e561   Goutte   Invert the orbits...
863
    @yAxisTitle.append('tspan').text('X')
11662eed   Goutte   Add Y axis label ...
864
865
866
867
    @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...
868
869
870
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
871
    @sun.append('svg:title').text("Sun")
f1f1e797   Goutte   Rewrite ugly java...
872

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

a21f81d9   Goutte   Enable Venus and ...
876
  initOrbiter: (slug, config, data) ->
dc0be992   Goutte   Support having no...
877
878
879
880
881
882
883
884
885
    @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...
886
887
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

438929a4   Goutte   Rewrite the orbit...
888
889
890
891
892
893
    # 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...
894
    orbiter.append('svg:title').text(config.name)
438929a4   Goutte   Rewrite the orbit...
895
896

    orbit_line = d3.line()
b500e561   Goutte   Invert the orbits...
897
898
                   .x((d) ~> @xScale(d.y))
                   .y((d) ~> @yScale(d.x))
438929a4   Goutte   Rewrite the orbit...
899
900

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

438929a4   Goutte   Rewrite the orbit...
904
905
906
907
908
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line
17c52617   Goutte   Make the orbits p...
909
910
911
    @orbitersExtrema[slug] = d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    )
a21f81d9   Goutte   Enable Venus and ...
912

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

a2a08db2   Goutte   Make sure targets...
915
916
917
918
    if config.active
      @enableTarget slug
    else
      @disableTarget slug
17c52617   Goutte   Make the orbits p...
919

438929a4   Goutte   Rewrite the orbit...
920
921
    this

a2a08db2   Goutte   Make sure targets...
922
923
924
925
926
927
928
929
  enableTarget: (slug) ->
    @orbiters[slug].enabled = true
    @showOrbiter slug

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

17c52617   Goutte   Make the orbits p...
930
  showOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
931
    if not @data[slug].length then return
a2a08db2   Goutte   Make sure targets...
932
    if not @orbiters[slug].enabled then return
17c52617   Goutte   Make the orbits p...
933
934
935
936
937
938
939
    @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...
940
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
941
942
943
944
945
946
    @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 ...
947
948
949
  clear: ->
    $(@svg.node()).remove()

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

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

3c2b15fc   Goutte   Make the Y-Axis o...
956
957
958
959
960
961
962
    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...
963
964
965
    @xScale = d3.scaleLinear().domain([-1 * extremum, extremum])
    @yScale = d3.scaleLinear().domain([extremum, -1 * extremum])

b500e561   Goutte   Invert the orbits...
966
    @xScale.range([0, width])
17c52617   Goutte   Make the orbits p...
967
    @yScale.range([height, 0])
f1f1e797   Goutte   Rewrite ugly java...
968
969
970
971

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
977
978
979
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

17c52617   Goutte   Make the orbits p...
980
981
982
983
984
985
986
987
988
    @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...
989

ae0aa7d2   Goutte   Add an x axis lab...
990
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
991
992
993
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
994

f1f1e797   Goutte   Rewrite ugly java...
995
996
    this

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

17c52617   Goutte   Make the orbits p...
1002
    tt = @svg.transition().duration(750)
438929a4   Goutte   Rewrite the orbit...
1003
    el = @orbitersElements[slug]
17c52617   Goutte   Make the orbits p...
1004
1005
1006
1007
1008
    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...
1009
1010
1011
1012
1013
1014

    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...
1015
1016
1017
1018
1019
1020
1021
1022

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

17c52617   Goutte   Make the orbits p...
1027
    @repositionOrbiter(slug, null, true)
438929a4   Goutte   Rewrite the orbit...
1028
1029
1030

    this

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

17c52617   Goutte   Make the orbits p...
1034
  repositionOrbiter: (slug, datum, animate = false) ->
a21f81d9   Goutte   Enable Venus and ...
1035
    data = @data[slug]
dc0be992   Goutte   Support having no...
1036
    if not data.length then return
17c52617   Goutte   Make the orbits p...
1037
    datum ?= @lastOrbiterData[slug]
ae0aa7d2   Goutte   Add an x axis lab...
1038
    datum ?= data[data.length - 1]
17c52617   Goutte   Make the orbits p...
1039
1040
1041
1042
1043
1044
1045
    @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...
1046
1047
1048
1049
1050
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
1051
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
1052
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
1053
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
1054
1055
1056
1057
1058
      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.
1059
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
1060
    this
ae0aa7d2   Goutte   Add an x axis lab...
1061

8cb213b9   Goutte   Clean up, and pre...
1062
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
1063
1064
1065
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
a2a08db2   Goutte   Make sure targets...
1066
1067
1068
      if not data.length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
1069
1070
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
1071
      @showOrbiter(slug)
ae0aa7d2   Goutte   Add an x axis lab...
1072

667eeb24   Goutte   Resize the domain...
1073
1074
1075
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
a2a08db2   Goutte   Make sure targets...
1076
1077
1078
      if not @data[slug].length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
1079
1080
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
1081
      @showOrbiter(slug)