Blame view

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

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

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

8cb213b9   Goutte   Clean up, and pre...
13
# Note: We use Promises and ES6 whenever relevant.
9c0c4509   Goutte   Add a loader to t...
14
15
16
17
# You also will need d3js v4 documentation : https://d3js.org/
# 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
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.
b60e7acd   Goutte   Rename "source" i...
34
  It defaults to an interval starting a year ago, and ending in seven days.
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) ->
2038c9fb   Goutte   Add a zoom reset ...
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    console.info """
  _   _      _ _       ____
 | | | | ___| (_) ___ |  _ \\ _ __ ___  _ __   __ _
 | |_| |/ _ \\ | |/ _ \\| |_) | '__/ _ \\| '_ \\ / _` |
 |  _  |  __/ | | (_) |  __/| | | (_) | |_) | (_| |
 |_| |_|\\___|_|_|\\___/|_|_  |_|_ \\___/| .__/ \\__,_|
 | |__  _   _   / ___|  _ \\|  _ \\|  _ \\_|
 | '_ \\| | | | | |   | | | | |_) | |_) |
 | |_) | |_| | | |___| |_| |  __/|  __/
 |_.__/ \\__, |  \\____|____/|_|   |_|
        |___/

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
59
60
    @targets = {}
    configs = [@configuration.targets[k] for k of @configuration.targets]
    configs.forEach((target_config) ~>
      @targets[target_config.slug] = new Target(target_config.slug, target_config.name, target_config)
fe3132dd   Goutte   Refactor even more.
61
    )
b7fe650c   Goutte   Misc bundle of ol...
62
63
64
65
    @parameters = {}
    @configuration['parameters'].forEach((p) ~>
        @parameters[p['id']] = p
    )
7994cf1a   Goutte   Hunt bugs.
66
67
    @orbiter = null   # our Orbiter
    @timeSeries = []  # a List of TimeSeries objects
ae0aa7d2   Goutte   Add an x axis lab...
68

b60e7acd   Goutte   Rename "source" i...
69
70
71
72
73
74
  init: ->
    """
    This is called by the inline bootstrap javascript code.
    This ain't in the constructor because it might return a Promise later on.
    (for the loader, for example)
    """
2c0e1515   Goutte   Refactor loading ...
75
76
    # Default time interval is from one year ago to one week ahead.
    # We set the h/m/s to zero to benefit from a daily cache.
7994cf1a   Goutte   Hunt bugs.
77
78
79
    started_at = moment().subtract(1, 'years').hours(0).minutes(0).seconds(0)
    stopped_at = moment().add(7, 'days').hours(0).minutes(0).seconds(0)
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
80
    @loadAndCreatePlots(started_at, stopped_at)
243cd8a4   Goutte   Timestamp party c...
81

b60e7acd   Goutte   Rename "source" i...
82
83
84
    window.addEventListener 'resize', ~> @resize()

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

b60e7acd   Goutte   Rename "source" i...
91
92
  addTarget: (target) ->
    @targets[target.slug] = target
f75faf5f   Goutte   WIP
93
94
    this

2c0e1515   Goutte   Refactor loading ...
95
96
97
98
#  enableAllTargets: ->
#    for slug, target of @targets
#      enableTarget(slug)
#    this
f75faf5f   Goutte   WIP
99

6bb225d6   Goutte   Link the time ser...
100
  enableTarget: (target_slug) ->
243cd8a4   Goutte   Timestamp party c...
101
    @timeSeries.forEach((ts) ~> ts.show() if ts.target.slug == target_slug && @parameters[ts.parameter].active)
b60e7acd   Goutte   Rename "source" i...
102
    @targets[target_slug].active = true
f75faf5f   Goutte   WIP
103
104
    this

6bb225d6   Goutte   Link the time ser...
105
  disableTarget: (target_slug) ->
243cd8a4   Goutte   Timestamp party c...
106
    @timeSeries.forEach((ts) -> ts.hide() if ts.target.slug == target_slug)
b60e7acd   Goutte   Rename "source" i...
107
    @targets[target_slug].active = false
f75faf5f   Goutte   WIP
108
109
    this

fe3132dd   Goutte   Refactor even more.
110
  resize: ->
a21f81d9   Goutte   Enable Venus and ...
111
    @orbits?.resize();
243cd8a4   Goutte   Timestamp party c...
112
    @timeSeries.forEach((ts) -> ts.resize())
fe3132dd   Goutte   Refactor even more.
113

2c0e1515   Goutte   Refactor loading ...
114
115
116
  showLoader: ->
    $("\#plots_loader").show();

9c0c4509   Goutte   Add a loader to t...
117
118
119
  hideLoader: ->
    $("\#plots_loader").hide();

b60e7acd   Goutte   Rename "source" i...
120
121
122
123
124
  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...
125

f75faf5f   Goutte   WIP
126
    sw = this
2c0e1515   Goutte   Refactor loading ...
127
    new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
128
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
129
      d3.csv(url, (csv) ->
f75faf5f   Goutte   WIP
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
        timeFormat = d3.timeParse('%Y-%m-%dT%H:%M:%S%Z')
        data = {'hci': []};
        configuration['parameters'].forEach((parameter) ->
            data[parameter['id']] = []
        )
        csv.forEach((d) ->
          dtime = timeFormat(d['time'])
          configuration['parameters'].forEach((parameter) ->
            id = parameter['id']
            data[id].push({x: dtime, y: parseFloat(d[id])})
          )
          if (d['xhci'] && d['yhci'])
            data['hci'].push({t: dtime, x: parseFloat(d['xhci']), y: parseFloat(d['yhci'])});
        )
        resolve(data)
      )
    )
2c0e1515   Goutte   Refactor loading ...
147
148

  loadAndCreatePlots: (started_at, stopped_at) ->
243cd8a4   Goutte   Timestamp party c...
149
150
151
152
    """
    started_at: moment(.js) object
    stopped_at: moment(.js) object
    """
2c0e1515   Goutte   Refactor loading ...
153
    @showLoader()
7994cf1a   Goutte   Hunt bugs.
154
155
156
    @started_at = started_at
    @stopped_at = stopped_at
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
243cd8a4   Goutte   Timestamp party c...
157
158
    started_at = started_at.format(API_TIME_FORMAT)
    stopped_at = stopped_at.format(API_TIME_FORMAT)
5e099488   Goutte   Fix that loading ...
159
160
    # active_targets = [@targets[k] for k of @targets when @targets[k].active]
    [@targets[k] for k of @targets].forEach((target) ~>
2c0e1515   Goutte   Refactor loading ...
161
      console.info "Loading CSV data of #{target.name}…"
4900d232   Goutte   Add the time inte...
162
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
163
164
165
166
167
168
169
      targetButton.addClass('loading')
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
          console.info "Loaded CSV data of #{target.name}."
          @createTimeSeries(target, data)
          @orbits.initOrbiter(target.slug, target.config, data['hci'])
          targetButton.removeClass('loading')
5e099488   Goutte   Fix that loading ...
170
          if target.active then @hideLoader() else @disableTarget(target.slug)
2c0e1515   Goutte   Refactor loading ...
171
172
173
174
175
176
177
        ,
        (error) -> console.error("Failed loading CSV data of #{target.name}.", error)
      )
    )

  clearPlots: ->
    @orbits.clear()
243cd8a4   Goutte   Timestamp party c...
178
    @timeSeries.forEach((ts) -> ts.clear())
2c0e1515   Goutte   Refactor loading ...
179
    @orbits = null
243cd8a4   Goutte   Timestamp party c...
180
    @timeSeries = []  # do we de-reference everything ? listeners ? #memleak?
2c0e1515   Goutte   Refactor loading ...
181
    this
ae0aa7d2   Goutte   Add an x axis lab...
182

b60e7acd   Goutte   Rename "source" i...
183
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
184
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
185
186
187
      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)
243cd8a4   Goutte   Timestamp party c...
188
      @timeSeries.push(new TimeSeries(
c3008fb2   Goutte   Clean up and refa...
189
        id, title, target, data[id], @parameters[id].active, container
6bb225d6   Goutte   Link the time ser...
190
      ))
4816cef4   Goutte   Refactor some more.
191
    )
243cd8a4   Goutte   Timestamp party c...
192
193
194
195
196
    @timeSeries.forEach((ts) ~>  # returning true may be faster
      ts.options['onMouseOver'] = ~>
        @timeSeries.forEach((ts2) -> ts2.showCursor()) ; true
      ts.options['onMouseOut'] = ~>
        @timeSeries.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
197
      ts.options['onMouseMove'] = (t) ~>
243cd8a4   Goutte   Timestamp party c...
198
        @timeSeries.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
199
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
200
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
201
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
202
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
203
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
204
    )
243cd8a4   Goutte   Timestamp party c...
205
    @timeSeries
4816cef4   Goutte   Refactor some more.
206

b7fe650c   Goutte   Misc bundle of ol...
207
208
  enableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
209
    @parameters[parameter_slug].active = true
243cd8a4   Goutte   Timestamp party c...
210
    @timeSeries.forEach((ts) ~> ts.show() if ts.parameter == parameter_slug && @targets[ts.target.slug].active)
b7fe650c   Goutte   Misc bundle of ol...
211
    this
4816cef4   Goutte   Refactor some more.
212

b7fe650c   Goutte   Misc bundle of ol...
213
214
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
215
    @parameters[parameter_slug].active = false
243cd8a4   Goutte   Timestamp party c...
216
    @timeSeries.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
217
    this
ae0aa7d2   Goutte   Add an x axis lab...
218

a06a0a67   Goutte   Prepare the time ...
219
220
221
222
223
  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...
224
225
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
226
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
227
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
228
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
229
      return
1754789b   Goutte   Decorate and clea...
230
231
232
233
    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...
234

243cd8a4   Goutte   Timestamp party c...
235
236
237
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
238

6bb225d6   Goutte   Link the time ser...
239
240
    if (@started_at <= started_at <= @stopped_at) and
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
241
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
242
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
243
      # the visible ones, for a smoother transition.
243cd8a4   Goutte   Timestamp party c...
244
245
      @timeSeries.forEach((ts) -> if not ts.visible then ts.zoomIn(started_at, stopped_at))
      @timeSeries.forEach((ts) -> if     ts.visible then ts.zoomIn(started_at, stopped_at))
8cb213b9   Goutte   Clean up, and pre...
246
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
247
248
249
250
251
252
    else
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
      console.warn "This might take a while… Why not see what else we're up to on http://cdpp.eu while you're waiting?"
      # fetch new data and remake the plots
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
253

243cd8a4   Goutte   Timestamp party c...
254
    this
8cb213b9   Goutte   Clean up, and pre...
255

6bb225d6   Goutte   Link the time ser...
256
  resetZoom: ->
243cd8a4   Goutte   Timestamp party c...
257
    @timeSeries.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
258
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
259
260
261
262
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
263
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
264
265
266
267
268
    @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...
269
270


ae0aa7d2   Goutte   Add an x axis lab...
271

ae0aa7d2   Goutte   Add an x axis lab...
272
273
274
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
275

f1f1e797   Goutte   Rewrite ugly java...
276
277
278
279
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

c3008fb2   Goutte   Clean up and refa...
280
  (@parameter, @title, @target, data, @visible, @container, @options = {}) ->
4cf497e0   Goutte   Make the targets ...
281
    # parameter : slug of the parameter to observe, like magn or pdyn
c3008fb2   Goutte   Clean up and refa...
282
283
    # title : string, more descriptive, shown on the left of the Y axis
    # target : target object, like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
284
    # data : list of {x: <datetime>, y: <float>}
6bb225d6   Goutte   Link the time ser...
285
    @setData(data)
f1f1e797   Goutte   Rewrite ugly java...
286
287
    @init()

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

6bb225d6   Goutte   Link the time ser...
290
  setData: (data) ->
c3008fb2   Goutte   Clean up and refa...
291
    @data = data  # and pre-compute extents for performance when zooming
6bb225d6   Goutte   Link the time ser...
292
293
294
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)

f1f1e797   Goutte   Rewrite ugly java...
295
  init: ->
c3008fb2   Goutte   Clean up and refa...
296
    console.info "Initializing plot of #{@}…"
f1f1e797   Goutte   Rewrite ugly java...
297
298
299
300
301

    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
302
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
303
304
    }

6bb225d6   Goutte   Link the time ser...
305
306
    @xScale = d3.scaleTime().domain(@xDataExtent)
    @yScale = d3.scaleLinear().domain(@yDataExtent)
f1f1e797   Goutte   Rewrite ugly java...
307

f1f1e797   Goutte   Rewrite ugly java...
308
    @xAxis = d3.axisBottom()
08569a6b   Goutte   Add a zooming bru...
309
#               .tickFormat(d3.timeFormat("%Y-%m-%d"))
d49a163c   Goutte   Fix the resize an...
310
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
311
312
313
314
315
316
317
318
    @yAxis = d3.axisLeft()
               .ticks(10)

    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

    @svg = d3.select(@container).append('svg')
b60e7acd   Goutte   Rename "source" i...
319
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
320

f1f1e797   Goutte   Rewrite ugly java...
321
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
322
323
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
324

123313cb   Goutte   Clip the paths of...
325
326
327
328
329
330
331
332
333
334
    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)

    @pathWrapper = @plotWrapper.append('g')
    @pathWrapper.attr("clip-path", "url(\##{clipId})")
    @path = @pathWrapper.append('path')
f1f1e797   Goutte   Rewrite ugly java...
335
336
337
                        .datum(@data)
                        .classed('line', true)

08569a6b   Goutte   Add a zooming bru...
338
    @brush = @plotWrapper.append("g")
2038c9fb   Goutte   Add a zoom reset ...
339
                         .attr("class", "brush")
08569a6b   Goutte   Add a zooming bru...
340

2038c9fb   Goutte   Add a zoom reset ...
341
    # deprecated, use brush's 'overlay' child
2463bd16   Goutte   Add a circle foll...
342
343
    @mouseCanvas = @plotWrapper.append("rect")
                               .style("fill", "none")
3ee0b596   Goutte   Fix an annoying b...
344

f1f1e797   Goutte   Rewrite ugly java...
345
346
347
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
348
349
350
351
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
352
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
353
354
355
356
357
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
358

81c9b2e8   Goutte   Add the values to...
359
360
361
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
365
    dx = 8
81c9b2e8   Goutte   Add the values to...
366
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
367
368
369
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
370

81c9b2e8   Goutte   Add the values to...
371
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
372
373
374
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
375

81c9b2e8   Goutte   Add the values to...
376
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
377
378
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
379
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
380

81c9b2e8   Goutte   Add the values to...
381
382
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
383
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
384
385
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
386
387
    @resize()

123313cb   Goutte   Clip the paths of...
388
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
389
  resize: ->
123313cb   Goutte   Clip the paths of...
390
391
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
392
393
394

    @plotWidth = width
    @plotHeight = height
f1f1e797   Goutte   Rewrite ugly java...
395

123313cb   Goutte   Clip the paths of...
396
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
397
398
399
400
401
402
403

    @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...
404
405
406
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
407
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
408
409
410
411

    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
412
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
413
414
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
415
416
417
418
419
420
421
422

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

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

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

b60e7acd   Goutte   Rename "source" i...
426
    @yAxisTextTarget.attr("y", 0 - @margin.left)
fe3132dd   Goutte   Refactor even more.
427
428
                    .attr("x", 0 - (height / 2))

2463bd16   Goutte   Add a circle foll...
429
430
431
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
432
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
433
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
434
435
436
437
438
439
440
441
442
443
      # looks like 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)

2038c9fb   Goutte   Add a zoom reset ...
444
445
      # We're also adding our own cursor events to the brush's overlay,
      # because it captures events and a rect cannot contain another.
08569a6b   Goutte   Add a zooming bru...
446
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
447
448
449
450
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
451

6bb225d6   Goutte   Link the time ser...
452
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
453
454
    this

2c0e1515   Goutte   Refactor loading ...
455
456
457
458
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
459
460
461
462
463
464
465
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
467
468
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
469
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
470
471

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

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

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

08569a6b   Goutte   Add a zooming bru...
480
481
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
482
483
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
484
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
485
486
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
487
488
489

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
490
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
491
492
493
494
495
496
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
497
498
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
499
500
501
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
502
    if @visible
c3008fb2   Goutte   Clean up and refa...
503
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
504
505
506
507
508
      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)
    else
c3008fb2   Goutte   Clean up and refa...
509
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
510
511
512
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
513

541e2936   Goutte   Synchronize the t...
514
515
516
517
518
519
  showCursor: ->
    @focus.style("display", null)

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

8cb213b9   Goutte   Clean up, and pre...
520
  bisectDate: d3.bisector((d) -> d.x).left  # /!\ complex
81c9b2e8   Goutte   Add the values to...
521
  timeFormat: d3.timeFormat("%Y-%m-%d %Hh")
2463bd16   Goutte   Add a circle foll...
522

541e2936   Goutte   Synchronize the t...
523
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
524
525
526
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
527
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
528
529
530
531
    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...
532
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
533

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

8cb213b9   Goutte   Clean up, and pre...
537
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
538
539
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
540
541
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
542
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
543
544
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
545
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
546
547
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
548
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
549
550
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
551
552

    this
f1f1e797   Goutte   Rewrite ugly java...
553

ae0aa7d2   Goutte   Add an x axis lab...
554
555
556
557

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

f1f1e797   Goutte   Rewrite ugly java...
558
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
559
560
561
562
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
563

a21f81d9   Goutte   Enable Venus and ...
564
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
565
566
567
    @init()

  init: ->
c3008fb2   Goutte   Clean up and refa...
568
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
569
570
571
572

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
573
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
574
575
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
576

a21f81d9   Goutte   Enable Venus and ...
577
578
    @data = {}  # slug => HCI array
    @orbiters = {}  # slug => config
7994cf1a   Goutte   Hunt bugs.
579
    @orbitersElements = {}
a21f81d9   Goutte   Enable Venus and ...
580
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
581
582
583
584
585
586
    @xScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])
    @yScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])

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

f1f1e797   Goutte   Rewrite ugly java...
587
588
589
590
591
    @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...
592
593
594
595
596
597
598
599
600
    @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")
    @xAxisTitle.append('tspan').text('X')
    # 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 ...
601
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
602
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
603

11662eed   Goutte   Add Y axis label ...
604
605
606
607
608
609
610
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
    @yAxisTitle.append('tspan').text('Y')
    @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...
611
612
613
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
614
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
615

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

a21f81d9   Goutte   Enable Venus and ...
619
  initOrbiter: (slug, config, data) ->
ebe77ce4   Goutte   Clean up.
620
    console.info "Initializing orbit of #{config.name}…"
438929a4   Goutte   Rewrite the orbit...
621
622
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

a21f81d9   Goutte   Enable Venus and ...
623
624
625
626
627
628
    @extremum = Math.max(@extremum, 1.11 * d3.max(data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    ))
    @xScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])
    @yScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])

438929a4   Goutte   Rewrite the orbit...
629
630
631
632
633
634
635
636
637
638
639
640
    # 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')

    orbit_line = d3.line()
                   .x((d) ~> @xScale(d.x))
                   .y((d) ~> @yScale(d.y))

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

a21f81d9   Goutte   Enable Venus and ...
644
645
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
646
647
648
649
650
651
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
652
653
    @resize()

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

438929a4   Goutte   Rewrite the orbit...
656
657
    this

2c0e1515   Goutte   Refactor loading ...
658
659
660
  clear: ->
    $(@svg.node()).remove()

f1f1e797   Goutte   Rewrite ugly java...
661
  resize: ->
abcf4c94   Goutte   Clean up.
662
663
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
664

abcf4c94   Goutte   Clean up.
665
    console.debug("Resizing orbits : #{width} × #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
666
667
668
669
670
671
672

    @xScale.range([0, width]);
    @yScale.range([height, 0]);

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

8bd715ad   Goutte   Use a pixel art i...
673
    @sun.attr("x", width / 2 - 16).attr("y", height / 2 - 16)
f1f1e797   Goutte   Rewrite ugly java...
674

438929a4   Goutte   Rewrite the orbit...
675
    for slug, config of @orbiters
abcf4c94   Goutte   Clean up.
676
      @resizeOrbiter(slug, config, width, height)
438929a4   Goutte   Rewrite the orbit...
677

f1f1e797   Goutte   Rewrite ugly java...
678
679
680
681
682
683
684
685
686
687
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

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

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

ae0aa7d2   Goutte   Add an x axis lab...
688
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
689
690
691
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
692

f1f1e797   Goutte   Rewrite ugly java...
693
694
    this

abcf4c94   Goutte   Clean up.
695
696
  resizeOrbiter: (slug, config, width, height) ->
    console.debug("Resizing orbit of #{slug}…")
438929a4   Goutte   Rewrite the orbit...
697
698
699
700
701
702
703
704
705
706
707
708
709

    el = @orbitersElements[slug]
    el['orbit_section'].attr('d', el['orbit_line'])

    a = config['orbit']['a']
    b = config['orbit']['b']
    c = Math.sqrt(a*a - b*b)
    cx = (width / 2) - c
    cy = (height / 2)
    @yScale.range([0, height])
    el['orbit_ellipse'].attr('cx', cx).attr('cy', cy)
        .attr('rx', @xScale(a) - @xScale(0))
        .attr('ry', @yScale(b) - @yScale(0))
a21f81d9   Goutte   Enable Venus and ...
710
#        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
438929a4   Goutte   Rewrite the orbit...
711
712
    @yScale.range([height, 0])

a21f81d9   Goutte   Enable Venus and ...
713
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
714
715
716
717
718
719

    el['orbiter'].attr('x', @xScale(data[data.length - 1].x) - 16)
    el['orbiter'].attr('y', @yScale(data[data.length - 1].y) - 16)

    this

ae0aa7d2   Goutte   Add an x axis lab...
720
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
721
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
722
723
724
725
    datum ?= data[data.length - 1]
    el = @orbitersElements[slug]
    el['orbiter'].attr('x', @xScale(datum.x) - 16)
    el['orbiter'].attr('y', @yScale(datum.y) - 16)
ae0aa7d2   Goutte   Add an x axis lab...
726
727
728
729
730
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
731
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
732
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
733
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
734
735
736
737
738
      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.
739
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
740
    this
ae0aa7d2   Goutte   Add an x axis lab...
741

8cb213b9   Goutte   Clean up, and pre...
742
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
743
744
745
746
747
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      data = @data[slug].filter (d) -> started_at <= d.t <= stopped_at
      el['orbit_section'].datum(data)
      el['orbit_section'].attr('d', el['orbit_line'])
ae0aa7d2   Goutte   Add an x axis lab...
748

667eeb24   Goutte   Resize the domain...
749
750
751
752
753
  resetZoom: ->
    for slug, config of @orbiters
      el = @orbitersElements[slug]
      el['orbit_section'].datum(@data[slug])
      el['orbit_section'].attr('d', el['orbit_line'])
abcf4c94   Goutte   Clean up.