Blame view

web/static/js/swapp.ls 35.4 KB
f1f1e797   Goutte   Rewrite ugly java...
1
# Livescript transpiles to javascript, and is easier on the eyes and brain.
4cf497e0   Goutte   Make the targets ...
2
# Get the `lsc` binary from here : http://livescript.net
8cb213b9   Goutte   Clean up, and pre...
3
# It is quite close to Python, syntax-wise, and full of sugar.
f1f1e797   Goutte   Rewrite ugly java...
4

4cf497e0   Goutte   Make the targets ...
5
# To transpile this file to javascript, and generate `swapp.js` :
a21f81d9   Goutte   Enable Venus and ...
6
# $ lsc --compile swapp.ls
4cf497e0   Goutte   Make the targets ...
7
8
# You can have it watch for changes and transpile automatically :
# $ lsc --watch --compile swapp.ls
a21f81d9   Goutte   Enable Venus and ...
9

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

8cb213b9   Goutte   Clean up, and pre...
13
# Note: We use Promises and ES6 whenever relevant.
cad44b6d   Goutte   Finish adding the...
14
# You also WILL NEED d3js v4 documentation : https://d3js.org/
9c0c4509   Goutte   Add a loader to t...
15
16
17
# We're using a custom build of 4.9.1, one line changed, see d3-custom.js
# Event bubbling cannot trigger two rects unless we make an event dispatcher,
# and d3's brush is stopping propagation, as it should by default.
b60e7acd   Goutte   Rename "source" i...
18

4cf497e0   Goutte   Make the targets ...
19
20
###############################################################################

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

ae0aa7d2   Goutte   Add an x axis lab...
23
24
###############################################################################

b60e7acd   Goutte   Rename "source" i...
25
26
class Target
  (@slug, @name, @config) ->
2c0e1515   Goutte   Refactor loading ...
27
    @active = @config.active
b60e7acd   Goutte   Rename "source" i...
28
29
30

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

ae0aa7d2   Goutte   Add an x axis lab...
31
export class SpaceWeather
4cf497e0   Goutte   Make the targets ...
32
33
  """
  The main app, instanciated from an inline script.
780828a8   Goutte   Prepare horizonta...
34
  It defaults to an interval starting two months ago, and ending in a month.
6bb225d6   Goutte   Link the time ser...
35
  (both at midnight)
4cf497e0   Goutte   Make the targets ...
36
  """
ae0aa7d2   Goutte   Add an x axis lab...
37

2c0e1515   Goutte   Refactor loading ...
38
  API_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss"
243cd8a4   Goutte   Timestamp party c...
39
  INPUT_TIME_FORMAT = "YYYY-MM-DD"
2c0e1515   Goutte   Refactor loading ...
40

f75faf5f   Goutte   WIP
41
  (@configuration) ->
05e269d1   Goutte   Show an error mes...
42
    console.info """©2017
2038c9fb   Goutte   Add a zoom reset ...
43
44
45
46
47
48
49
50
51
52
53
54
55
  _   _      _ _       ____
 | | | | ___| (_) ___ |  _ \\ _ __ ___  _ __   __ _
 | |_| |/ _ \\ | |/ _ \\| |_) | '__/ _ \\| '_ \\ / _` |
 |  _  |  __/ | | (_) |  __/| | | (_) | |_) | (_| |
 |_| |_|\\___|_|_|\\___/|_|_  |_|_ \\___/| .__/ \\__,_|
 | |__  _   _   / ___|  _ \\|  _ \\|  _ \\_|
 | '_ \\| | | | | |   | | | | |_) | |_) |
 | |_) | |_| | | |___| |_| |  __/|  __/
 |_.__/ \\__, |  \\____|____/|_|   |_|
        |___/

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

b7fe650c   Goutte   Misc bundle of ol...
62
    @parameters = {}
cad44b6d   Goutte   Finish adding the...
63
64
65
    @configuration['parameters'].forEach (p) ~>
      @parameters[p['id']] = p

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

284f4688   Goutte   Continue layers i...
69
  init: (started_at, stopped_at) ->
b60e7acd   Goutte   Rename "source" i...
70
71
72
73
74
    """
    This is called by the inline bootstrap javascript code.
    This ain't in the constructor because it might return a Promise later on.
    (for the loader, for example)
    """
284f4688   Goutte   Continue layers i...
75
76
77
    # We set the h/m/s to zero to ensure we benefit from the daily cache.
    started_at = moment(started_at).hours(0).minutes(0).seconds(0)
    stopped_at = moment(stopped_at).hours(0).minutes(0).seconds(0)
7994cf1a   Goutte   Hunt bugs.
78
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
79
    @loadAndCreatePlots(started_at, stopped_at)
b60e7acd   Goutte   Rename "source" i...
80
    window.addEventListener 'resize', ~> @resize()
fb383448   Goutte   Implement the cac...
81
    this
b60e7acd   Goutte   Rename "source" i...
82
83

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

6b149919   Goutte   Add a Download bu...
90
91
92
93
94
95
96
97
98
  buildDownloadUrl: ->
    [started_at, stopped_at] = @getDomain()
    targets = [t for t of @targets when @targets[t].active].sort().join('-')
    url = @configuration['api']['download']
    url = url.replace('<targets>', targets)
    url = url.replace('<started_at>', started_at.format(API_TIME_FORMAT))
    url = url.replace('<stopped_at>', stopped_at.format(API_TIME_FORMAT))
    url

cad44b6d   Goutte   Finish adding the...
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
  buildSampUrl: ->
    [started_at, stopped_at] = @getDomain()
    targets = [t for t of @targets when @targets[t].active].sort().join('-')
    parameters = [p for p of @parameters when @parameters[p].active].sort().join('-')
    url = @configuration['api']['samp']
    url = url.replace('<targets>', targets)
    url = url.replace('<params>', parameters)
    url = url.replace('<started_at>', started_at.format(API_TIME_FORMAT))
    url = url.replace('<stopped_at>', stopped_at.format(API_TIME_FORMAT))
    url

  buildSampName: ->
    [started_at, stopped_at] = @getDomain()
    targets = [t.name for t in @getEnabledTargets()].sort().join(', ')
    "Heliopropa for #{targets} from #{started_at.format(API_TIME_FORMAT)} to #{stopped_at.format(API_TIME_FORMAT)}."

b60e7acd   Goutte   Rename "source" i...
115
116
  addTarget: (target) ->
    @targets[target.slug] = target
f75faf5f   Goutte   WIP
117
118
    this

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

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

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

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

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

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

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

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

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

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

    targets = [@targets[k] for k of @targets]
    targets.forEach((target) ~>
4900d232   Goutte   Add the time inte...
199
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
200
      targetButton.addClass('loading')
60b73eb1   Goutte   Change temperatur...
201
202
203
204
205
206
207
      targetButton.removeClass('failed error empty')
    )
    handleTarget = (i) ~>
      if i >= targets.length then return
      target = targets[i]
      console.info "Loading CSV data of #{target.name}…"
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
208
209
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
9bfa6c42   Goutte   More bug hunting.
210
          console.info "Loaded CSV data of #{target.name}.", data
2c0e1515   Goutte   Refactor loading ...
211
          @createTimeSeries(target, data)
6491a1f1   Goutte   Fix up the bugs l...
212
          @orbits.initOrbiter(target.slug, target.config, data['hee'])
2c0e1515   Goutte   Refactor loading ...
213
          targetButton.removeClass('loading')
5e099488   Goutte   Fix that loading ...
214
          if target.active then @hideLoader() else @disableTarget(target.slug)
60b73eb1   Goutte   Change temperatur...
215
          handleTarget(i+1)
2c0e1515   Goutte   Refactor loading ...
216
        ,
9bfa6c42   Goutte   More bug hunting.
217
        (error) ~>
80352490   Goutte   Multi model suppo...
218
219
220
221
222
          switch error
            case 'invalid'
              console.error("Failed loading CSV data of #{target.name}.")
              # Sometimes, AMDA's API returns garbage, so the CSV sometime fails
              # But when we re-generate it a second time, usually it's okay.
60b73eb1   Goutte   Change temperatur...
223
224
              # alert("There was an error with #{target.name}.\nPlease retry in a few moments.")
              targetButton.addClass('error')
a2a08db2   Goutte   Make sure targets...
225
#              @is_invalid = true
80352490   Goutte   Multi model suppo...
226
227
228
229
230
231
232
              break
            case 'empty'
              msg = "No data for #{target.name}\n during interval from \n#{started_at} to #{stopped_at}."
              console.warn(msg)
              targetButton.addClass('empty')
#              alert(msg)
              break
27097d87   Goutte   Change the error ...
233
          targetButton.addClass('failed')
9bfa6c42   Goutte   More bug hunting.
234
235
          targetButton.removeClass('loading')
          @hideLoader()
60b73eb1   Goutte   Change temperatur...
236
          handleTarget(i+1)
2c0e1515   Goutte   Refactor loading ...
237
      )
60b73eb1   Goutte   Change temperatur...
238
    handleTarget(0)
60b73eb1   Goutte   Change temperatur...
239
    this
2c0e1515   Goutte   Refactor loading ...
240
241
242

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

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

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

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

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

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

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

a06a0a67   Goutte   Prepare the time ...
301
302
303
304
305
  getDomain: ->
    if @current_started_at? and @current_stopped_at?
      return [@current_started_at, @current_stopped_at]
    return [@started_at, @stopped_at]

8cb213b9   Goutte   Clean up, and pre...
306
307
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
308
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
309
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
310
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
311
      return
1754789b   Goutte   Decorate and clea...
312
313
314
315
    max_stopped_at = started_at.clone().add(2, 'years')
    if stopped_at > max_stopped_at
      console.warn "The time interval was truncated beacuse it was bigger than two years."
      stopped_at = max_stopped_at
8cb213b9   Goutte   Clean up, and pre...
316

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

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

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

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

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
346
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
347
348
349
350
351
    @current_started_at = started_at
    @current_stopped_at = stopped_at
    $("\#started_at").val(started_at.format(INPUT_TIME_FORMAT))
    $("\#stopped_at").val(stopped_at.format(INPUT_TIME_FORMAT))
    this
8cb213b9   Goutte   Clean up, and pre...
352
353


ae0aa7d2   Goutte   Add an x axis lab...
354

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

b60e7acd   Goutte   Rename "source" i...
358

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

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

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

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

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

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

    # Pre-compute extents for performance when zooming.
    # These are final and always hold the biggest extent.
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)
    if @options['started_at'] then @xDataExtent[0] = @options['started_at']
    if @options['stopped_at'] then @xDataExtent[1] = @options['stopped_at']

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

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

a8ce269b   Goutte   Force time displa...
405
406
407
408
409
410
411
412
    formatMillisecond = d3.utcFormat(".%L")
    formatSecond = d3.utcFormat(":%S")
    formatMinute = d3.utcFormat("%H:%M")
    formatHour = d3.utcFormat("%H:%M")
    formatDay = d3.utcFormat("%a %d")
    formatWeek = d3.utcFormat("%b %d")
    formatMonth = d3.utcFormat("%B")
    formatYear = d3.utcFormat("%Y")
6491a1f1   Goutte   Fix up the bugs l...
413
414
415
416
417
418
419
420
421
422
423
424

    multiFormat = (date) ->
      if date > d3.timeSecond(date) then return formatMillisecond(date)
      if date > d3.timeMinute(date) then return formatSecond(date)
      if date > d3.timeHour(date)   then return formatMinute(date)
      if date > d3.timeDay(date)    then return formatHour(date)
      if date > d3.timeMonth(date)
        if date > d3.timeWeek(date) then return formatDay(date)
        else return formatWeek(date)
      if date > d3.timeYear(date)   then return formatMonth(date)
      return formatYear(date)

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

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

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

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

123313cb   Goutte   Clip the paths of...
442
443
444
445
446
447
448
    clipId = "ts-clip-#{@parameter}-#{@target.slug}"
    @clip = @svg.append("defs").append("svg:clipPath")
                .attr("id", clipId)
                .append("svg:rect")
                .attr("x", 0)
                .attr("y", 0)

0332f168   Goutte   Initial support f...
449
450
    @createCatalogLayers()

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

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

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

0332f168   Goutte   Initial support f...
486

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

0332f168   Goutte   Initial support f...
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
  createCatalogLayers: ->
    @layers_rects = {}
    for catalog_slug, layers of @target.config.layers
      #console.debug("Creating layers of #{catalog_slug}…", layers)
      @layers_rects[catalog_slug] = []
      for layer in layers
        started_at = moment(layer.start)
        stopped_at = moment(layer.stop)
        #console.debug(started_at, stopped_at)
        #console.debug(layer.start, layer.stop)
        @layers_rects[catalog_slug].push(
          @createCatalogLayer(started_at, stopped_at)
        )
    this

  createCatalogLayer: (started_at, stopped_at) ->
    layer_rect = @plotWrapper.append("rect")
                             .attr('y', 0)
                             .attr('height', @plotHeight)
596da00d   Goutte   Add more exceptio...
676
                             .attr('fill', '#FFFD64C2')
0332f168   Goutte   Initial support f...
677
678
679
680
681
682
683
684
685
686
    # Not triggered, mouse events are captured before they reach this rect
    #layer_rect.append('svg:title').text("I AM TEXT")
    layer_rect

  resizeCatalogLayers: ->
    for catalog_slug, layers of @target.config.layers
      #console.debug("Resizing layers of #{catalog_slug}…", layers)
      for layer, i in layers
        started_at = moment(layer.start)
        stopped_at = moment(layer.stop)
284f4688   Goutte   Continue layers i...
687
        width = Math.max(2, @xScale(stopped_at) - @xScale(started_at))
0332f168   Goutte   Initial support f...
688
        @layers_rects[catalog_slug][i].attr('x', @xScale(started_at))
284f4688   Goutte   Continue layers i...
689
                                      .attr('width', width)
0332f168   Goutte   Initial support f...
690
691
692
693
694
695
696
697
698
699
700
701
    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...
702
703
704
705
706
707
  showCursor: ->
    @focus.style("display", null)

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

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

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

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

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

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

    this
f1f1e797   Goutte   Rewrite ugly java...
745

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
932
933
    this

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

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

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

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

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

    this

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

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

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

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

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

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