Blame view

web/static/js/swapp.ls 36.1 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.
221bfc4d   Goutte   Try even more fix...
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
      #@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...
350
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
351
    else
a2a08db2   Goutte   Make sure targets...
352
#      @is_invalid = false
243cd8a4   Goutte   Timestamp party c...
353
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
a2a08db2   Goutte   Make sure targets...
354
      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...
355
356
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
357

243cd8a4   Goutte   Timestamp party c...
358
    this
8cb213b9   Goutte   Clean up, and pre...
359

6bb225d6   Goutte   Link the time ser...
360
  resetZoom: ->
80352490   Goutte   Multi model suppo...
361
    @time_series.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
362
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
363
364
365
366
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
367
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
368
369
370
371
372
    @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...
373
374


ae0aa7d2   Goutte   Add an x axis lab...
375

ae0aa7d2   Goutte   Add an x axis lab...
376
377
378
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
379

f1f1e797   Goutte   Rewrite ugly java...
380
381
382
383
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

7b5642ae   Goutte   Normalize time in...
384
  (@parameter, @title, @target, @data, @visible, @container, @options = {}) ->
bde97e4d   Goutte   Add more changes ...
385
    # parameter : slug of the parameter to observe, like btan or pdyn
c3008fb2   Goutte   Clean up and refa...
386
387
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
388
    # data : list of {x: <datetime>, y: <float>}
cdf79b23   Goutte   Give the future d...
389
390
391
392
393
    # 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...
394
395
    @init()

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

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

f1f1e797   Goutte   Rewrite ugly java...
401
402
403
404
    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
405
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
406
407
    }

0332f168   Goutte   Initial support f...
408
409
410
411
412
413
414
415
416
    [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, ...
417
418
419
420
421
    # 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...
422
423
    # Domain on a log scale MUST NOT cross zero
#    @yScale = d3.scaleLog().domain(@yDataExtent)
fb5dc2a0   Goutte   Fix a nasty bug, ...
424
    @yScale = d3.scaleLinear().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
425

a8ce269b   Goutte   Force time displa...
426
427
428
429
430
431
432
433
    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...
434
435
436
437
438
439
440
441
442
443
444
445

    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...
446
    @xAxis = d3.axisBottom()
6491a1f1   Goutte   Fix up the bugs l...
447
               .tickFormat(multiFormat)
d49a163c   Goutte   Fix the resize an...
448
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
449
450
451
    @yAxis = d3.axisLeft()
               .ticks(10)

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

f1f1e797   Goutte   Rewrite ugly java...
455
456
457
458
    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

f1f1e797   Goutte   Rewrite ugly java...
459
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
460
461
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
462

123313cb   Goutte   Clip the paths of...
463
464
465
466
467
468
469
    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...
470

123313cb   Goutte   Clip the paths of...
471
472
473
    @pathWrapper = @plotWrapper.append('g')
    @pathWrapper.attr("clip-path", "url(\##{clipId})")
    @path = @pathWrapper.append('path')
f1f1e797   Goutte   Rewrite ugly java...
474
475
                        .datum(@data)
                        .classed('line', true)
cdf79b23   Goutte   Give the future d...
476
477
478
    @predictiveDataPath = @pathWrapper.append('path')
                                      .datum(@predictiveData)
                                      .classed('predictive-line', true)
f1f1e797   Goutte   Rewrite ugly java...
479

dabb9d5f   Goutte   Fix the layers' c...
480
481
    @createCatalogLayers()

780828a8   Goutte   Prepare horizonta...
482
483
484
485
486
487
488
489
490
    @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...
491
    @brush = @plotWrapper.append("g")
2038c9fb   Goutte   Add a zoom reset ...
492
                         .attr("class", "brush")
08569a6b   Goutte   Add a zooming bru...
493

f1f1e797   Goutte   Rewrite ugly java...
494
495
496
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
497
498
499
500
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
501
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
502
503
504
505
506
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
507

0332f168   Goutte   Initial support f...
508

81c9b2e8   Goutte   Add the values to...
509
510
511
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
515
    dx = 8
81c9b2e8   Goutte   Add the values to...
516
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
517
518
519
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
520

81c9b2e8   Goutte   Add the values to...
521
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
522
523
524
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
525

81c9b2e8   Goutte   Add the values to...
526
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
527
528
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
529
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
530

81c9b2e8   Goutte   Add the values to...
531
532
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
533
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
534
535
                        .attr("dy", "1em")

0332f168   Goutte   Initial support f...
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
    #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...
556
557
    @resize()

0332f168   Goutte   Initial support f...
558
  recomputeDimensions: ->
123313cb   Goutte   Clip the paths of...
559
560
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
561
562
    @plotWidth = width
    @plotHeight = height
0332f168   Goutte   Initial support f...
563
564
565
566
567
    [width, height]

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

123313cb   Goutte   Clip the paths of...
569
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
570
571
572
573
574
575
576

    @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...
577
578
579
    @clip.attr("width", width)
         .attr("height", height)

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

780828a8   Goutte   Prepare horizonta...
583
584
585
586
587
588
589
    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...
590
591
592
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
593
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
034132c6   Goutte   More changes from...
594
    @xAxis.ticks(Math.floor(width / 80.0))  # not working as expected
d49a163c   Goutte   Fix the resize an...
595
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
596
597
598
599
600
601
602
603

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

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

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

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

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

6bb225d6   Goutte   Link the time ser...
612
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
613
614
    this

2c0e1515   Goutte   Refactor loading ...
615
616
617
618
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
619
620
621
622
623
624
625
  show: ->
    $(@svg.node()).show()
    @visible = true

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

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

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

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

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

08569a6b   Goutte   Add a zooming bru...
640
641
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
642
643
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
644
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
645
646
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
647
648
649

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
650
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
651
652
653
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
2d8a5753   Goutte   Make the Y-Axis o...
654
    @yScale.domain(d3.extent(@data, (d) -> if startDate <= d.x <= stopDate then d.y else 0))
2038c9fb   Goutte   Add a zoom reset ...
655
656
657
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
658
659
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
660
661
662
    @applyZoom()

  applyZoom: ->
221bfc4d   Goutte   Try even more fix...
663
    duration = 0
6bb225d6   Goutte   Link the time ser...
664
    if @visible
221bfc4d   Goutte   Try even more fix...
665
      duration = 750
c3008fb2   Goutte   Clean up and refa...
666
      console.debug("Applying zoom to visible #{@}…")
221bfc4d   Goutte   Try even more fix...
667
      t = @svg.transition().duration(duration)
c4983585   Goutte   Hide the layers b...
668
669
      @svg.select('.x.axis').transition(t).call(@xAxis)
      @svg.select('.y.axis').transition(t).call(@yAxis)
6bb225d6   Goutte   Link the time ser...
670
      @path.transition(t).attr('d', @line)
cdf79b23   Goutte   Give the future d...
671
      @predictiveDataPath.transition(t).attr('d', @line)
6bb225d6   Goutte   Link the time ser...
672
    else
c3008fb2   Goutte   Clean up and refa...
673
      console.debug("Applying zoom to hidden #{@}…")
c4983585   Goutte   Hide the layers b...
674
675
      @svg.select('.x.axis').call(@xAxis)
      @svg.select('.y.axis').call(@yAxis)
6bb225d6   Goutte   Link the time ser...
676
      @path.attr('d', @line)
cdf79b23   Goutte   Give the future d...
677
      @predictiveDataPath.attr('d', @line)
0332f168   Goutte   Initial support f...
678
    @resizeCatalogLayers()
375dcd95   Goutte   Fix a small graph...
679
    @hideCursor()
221bfc4d   Goutte   Try even more fix...
680
681
682
683
684
685
686
    new Promise((resolve, reject) ->
        if 0 == duration
            resolve()
        else
            setTimeout((-> resolve()), duration+50)
    )

08569a6b   Goutte   Add a zooming bru...
687

0332f168   Goutte   Initial support f...
688
689
690
691
692
693
694
695
696
697
698
699
700
  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...
701
      @hideCatalogLayer(catalog_slug)
0332f168   Goutte   Initial support f...
702
703
704
    this

  createCatalogLayer: (started_at, stopped_at) ->
dabb9d5f   Goutte   Fix the layers' c...
705
    layer_rect = @pathWrapper.append("rect")
0332f168   Goutte   Initial support f...
706
707
                             .attr('y', 0)
                             .attr('height', @plotHeight)
596da00d   Goutte   Add more exceptio...
708
                             .attr('fill', '#FFFD64C2')
221bfc4d   Goutte   Try even more fix...
709
    # ↓ Not triggered, mouse events are captured before they reach this rect
0332f168   Goutte   Initial support f...
710
711
712
713
714
715
716
717
718
    #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...
719
        width = Math.max(2, @xScale(stopped_at) - @xScale(started_at))
0332f168   Goutte   Initial support f...
720
        @layers_rects[catalog_slug][i].attr('x', @xScale(started_at))
284f4688   Goutte   Continue layers i...
721
                                      .attr('width', width)
0332f168   Goutte   Initial support f...
722
723
724
725
726
727
728
729
730
731
732
733
    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...
734
735
736
737
738
739
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
743
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
744
745
746
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
b4abddb7   Goutte   Fix a small issue...
747
748
749
750
    if (not d1) or (not d0)
      @hideCursor()
      return

2463bd16   Goutte   Add a circle foll...
751
752
753
754
    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...
755
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
756

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

8cb213b9   Goutte   Clean up, and pre...
760
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
761
762
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
763
764
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
765
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
766
767
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
768
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
769
770
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
771
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
772
773
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
b4abddb7   Goutte   Fix a small issue...
774
    @showCursor()
2463bd16   Goutte   Add a circle foll...
775
776

    this
f1f1e797   Goutte   Rewrite ugly java...
777

ae0aa7d2   Goutte   Add an x axis lab...
778
779
780
781

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

f1f1e797   Goutte   Rewrite ugly java...
782
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
783
784
785
786
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
787

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

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

b500e561   Goutte   Invert the orbits...
792
793
794
    # 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...
795
796
797
    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
798
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
799
800
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
801

6491a1f1   Goutte   Fix up the bugs l...
802
    @data = {}  # slug => HEE array
a21f81d9   Goutte   Enable Venus and ...
803
    @orbiters = {}  # slug => config
17c52617   Goutte   Make the orbits p...
804
805
806
807
808
    @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...
809
810
811
812

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

f1f1e797   Goutte   Rewrite ugly java...
813
814
815
816
817
    @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...
818
819
820
821
822
    @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...
823
    @xAxisTitle.append('tspan').text('Y')
ae0aa7d2   Goutte   Add an x axis lab...
824
825
826
    # 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 ...
827
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
828
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
829

11662eed   Goutte   Add Y axis label ...
830
831
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
b500e561   Goutte   Invert the orbits...
832
    @yAxisTitle.append('tspan').text('X')
11662eed   Goutte   Add Y axis label ...
833
834
835
836
    @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...
837
838
839
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
6491a1f1   Goutte   Fix up the bugs l...
840
    @sun.append('svg:title').text("Sun")
f1f1e797   Goutte   Rewrite ugly java...
841

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

a21f81d9   Goutte   Enable Venus and ...
845
  initOrbiter: (slug, config, data) ->
dc0be992   Goutte   Support having no...
846
847
848
849
850
851
852
853
854
    @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...
855
856
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

438929a4   Goutte   Rewrite the orbit...
857
858
859
860
861
862
    # 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...
863
    orbiter.append('svg:title').text(config.name)
438929a4   Goutte   Rewrite the orbit...
864
865

    orbit_line = d3.line()
b500e561   Goutte   Invert the orbits...
866
867
                   .x((d) ~> @xScale(d.y))
                   .y((d) ~> @yScale(d.x))
438929a4   Goutte   Rewrite the orbit...
868
869

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

438929a4   Goutte   Rewrite the orbit...
873
874
875
876
877
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line
17c52617   Goutte   Make the orbits p...
878
879
880
    @orbitersExtrema[slug] = d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    )
a21f81d9   Goutte   Enable Venus and ...
881

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

a2a08db2   Goutte   Make sure targets...
884
885
886
887
    if config.active
      @enableTarget slug
    else
      @disableTarget slug
17c52617   Goutte   Make the orbits p...
888

438929a4   Goutte   Rewrite the orbit...
889
890
    this

a2a08db2   Goutte   Make sure targets...
891
892
893
894
895
896
897
898
  enableTarget: (slug) ->
    @orbiters[slug].enabled = true
    @showOrbiter slug

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

17c52617   Goutte   Make the orbits p...
899
  showOrbiter: (slug) ->
dc0be992   Goutte   Support having no...
900
    if not @data[slug].length then return
a2a08db2   Goutte   Make sure targets...
901
    if not @orbiters[slug].enabled then return
17c52617   Goutte   Make the orbits p...
902
903
904
905
906
907
908
    @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...
909
    if not @data[slug].length then return
17c52617   Goutte   Make the orbits p...
910
911
912
913
914
915
    @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 ...
916
917
918
  clear: ->
    $(@svg.node()).remove()

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

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

3c2b15fc   Goutte   Make the Y-Axis o...
925
926
927
928
929
930
931
    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...
932
933
934
    @xScale = d3.scaleLinear().domain([-1 * extremum, extremum])
    @yScale = d3.scaleLinear().domain([extremum, -1 * extremum])

b500e561   Goutte   Invert the orbits...
935
    @xScale.range([0, width])
17c52617   Goutte   Make the orbits p...
936
    @yScale.range([height, 0])
f1f1e797   Goutte   Rewrite ugly java...
937
938
939
940

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
946
947
948
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

17c52617   Goutte   Make the orbits p...
949
950
951
952
953
954
955
956
957
    @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...
958

ae0aa7d2   Goutte   Add an x axis lab...
959
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
960
961
962
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
963

f1f1e797   Goutte   Rewrite ugly java...
964
965
    this

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

17c52617   Goutte   Make the orbits p...
971
    tt = @svg.transition().duration(750)
438929a4   Goutte   Rewrite the orbit...
972
    el = @orbitersElements[slug]
17c52617   Goutte   Make the orbits p...
973
974
975
976
977
    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...
978
979
980
981
982
983

    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...
984
985
986
987
988
989
990
991

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

17c52617   Goutte   Make the orbits p...
996
    @repositionOrbiter(slug, null, true)
438929a4   Goutte   Rewrite the orbit...
997
998
999

    this

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

17c52617   Goutte   Make the orbits p...
1003
  repositionOrbiter: (slug, datum, animate = false) ->
a21f81d9   Goutte   Enable Venus and ...
1004
    data = @data[slug]
dc0be992   Goutte   Support having no...
1005
    if not data.length then return
17c52617   Goutte   Make the orbits p...
1006
    datum ?= @lastOrbiterData[slug]
ae0aa7d2   Goutte   Add an x axis lab...
1007
    datum ?= data[data.length - 1]
17c52617   Goutte   Make the orbits p...
1008
1009
1010
1011
1012
1013
1014
    @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...
1015
1016
1017
1018
1019
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
1020
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
1021
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
1022
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
1023
1024
1025
1026
1027
      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.
1028
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
1029
    this
ae0aa7d2   Goutte   Add an x axis lab...
1030

8cb213b9   Goutte   Clean up, and pre...
1031
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
1032
1033
1034
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
a2a08db2   Goutte   Make sure targets...
1035
1036
1037
      if not data.length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
1038
1039
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
1040
      @showOrbiter(slug)
ae0aa7d2   Goutte   Add an x axis lab...
1041

667eeb24   Goutte   Resize the domain...
1042
1043
1044
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
a2a08db2   Goutte   Make sure targets...
1045
1046
1047
      if not @data[slug].length
        @hideOrbiter(slug)
        continue
667eeb24   Goutte   Resize the domain...
1048
1049
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
a2a08db2   Goutte   Make sure targets...
1050
      @showOrbiter(slug)