Blame view

web/static/js/swapp.ls 24.3 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
39
  API_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss"

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

The full source of this website is available at :
https://gitlab.irap.omp.eu/CDPP/SPACEWEATHERONLINE
6bb225d6   Goutte   Link the time ser...
55
"""  # HelioPropa by CDPP (mushed 'cause we need to escape backslashes)
b60e7acd   Goutte   Rename "source" i...
56
57
58
59
    @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.
60
    )
b7fe650c   Goutte   Misc bundle of ol...
61
62
63
64
    @parameters = {}
    @configuration['parameters'].forEach((p) ~>
        @parameters[p['id']] = p
    )
ae0aa7d2   Goutte   Add an x axis lab...
65

b60e7acd   Goutte   Rename "source" i...
66
67
68
69
70
71
  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)
    """
b60e7acd   Goutte   Rename "source" i...
72
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
2c0e1515   Goutte   Refactor loading ...
73
74
    # 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.
8cb213b9   Goutte   Clean up, and pre...
75
76
    @started_at = moment().subtract(1, 'years').hours(0).minutes(0).seconds(0)
    @stopped_at = moment().add(7, 'days').hours(0).minutes(0).seconds(0)
2c0e1515   Goutte   Refactor loading ...
77
78
    started_at = @started_at.format(API_TIME_FORMAT)
    stopped_at = @stopped_at.format(API_TIME_FORMAT)
ebe77ce4   Goutte   Clean up.
79
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
2c0e1515   Goutte   Refactor loading ...
80
    @loadAndCreatePlots(started_at, stopped_at)
b60e7acd   Goutte   Rename "source" i...
81
82
83
    window.addEventListener 'resize', ~> @resize()

  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

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

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

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

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

fe3132dd   Goutte   Refactor even more.
109
  resize: ->
a21f81d9   Goutte   Enable Venus and ...
110
    @orbits?.resize();
d49a163c   Goutte   Fix the resize an...
111
    timeSeries.forEach((ts) -> ts.resize())
fe3132dd   Goutte   Refactor even more.
112

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

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

b60e7acd   Goutte   Rename "source" i...
119
120
121
122
123
  loadData: (target_slug, started_at, stopped_at) ->
    """
    Load the data as CSV for the specified target and interval,
    and return it in a Promise.
    """
f75faf5f   Goutte   WIP
124
    sw = this
2c0e1515   Goutte   Refactor loading ...
125
    new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
126
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
127
      d3.csv(url, (csv) ->
f75faf5f   Goutte   WIP
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
        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 ...
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170

  loadAndCreatePlots: (started_at, stopped_at) ->
    @showLoader()
    active_targets = [@targets[k] for k of @targets when @targets[k].active]
    active_targets.forEach((target) ~>
      console.info "Loading CSV data of #{target.name}…"
      targetButton = $(".orbiters_filters .target.#{target.slug}")
      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')
          @hideLoader()
        ,
        (error) -> console.error("Failed loading CSV data of #{target.name}.", error)
      )
    )

  clearPlots: ->
    @orbits.clear()
    timeSeries.forEach((ts) -> ts.clear())
    @orbits = null
    timeSeries = []  # do we de-reference all existing TimeSeries ? #memleak?
    this
ae0aa7d2   Goutte   Add an x axis lab...
171

9c0c4509   Goutte   Add a loader to t...
172
  timeSeries = []  # deprecated (was for scoping) ; use @property with ~>
b60e7acd   Goutte   Rename "source" i...
173
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
174
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
175
176
177
      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)
6bb225d6   Goutte   Link the time ser...
178
      timeSeries.push(new TimeSeries(
c3008fb2   Goutte   Clean up and refa...
179
        id, title, target, data[id], @parameters[id].active, container
6bb225d6   Goutte   Link the time ser...
180
      ))
4816cef4   Goutte   Refactor some more.
181
    )
9c0c4509   Goutte   Add a loader to t...
182
    timeSeries.forEach((ts) ~>  # returning true may be faster
4816cef4   Goutte   Refactor some more.
183
      ts.options['onMouseOver'] = ->
9c0c4509   Goutte   Add a loader to t...
184
        timeSeries.forEach((ts2) -> ts2.showCursor()) ; true
4816cef4   Goutte   Refactor some more.
185
      ts.options['onMouseOut'] = ->
9c0c4509   Goutte   Add a loader to t...
186
        timeSeries.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
187
      ts.options['onMouseMove'] = (t) ~>
4816cef4   Goutte   Refactor some more.
188
        timeSeries.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
189
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
190
      ts.options['onBrushEnd'] = (sta, sto) ~>
9c0c4509   Goutte   Add a loader to t...
191
        @resizeDomain(sta, sto) ; true
c3008fb2   Goutte   Clean up and refa...
192
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
193
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
194
195
    )

b7fe650c   Goutte   Misc bundle of ol...
196
197
  enableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
198
    @parameters[parameter_slug].active = true
6bb225d6   Goutte   Link the time ser...
199
    timeSeries.forEach((ts) ~> ts.show() if ts.parameter == parameter_slug && @targets[ts.target.slug].active)
b7fe650c   Goutte   Misc bundle of ol...
200
    this
4816cef4   Goutte   Refactor some more.
201

b7fe650c   Goutte   Misc bundle of ol...
202
203
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
204
    @parameters[parameter_slug].active = false
6bb225d6   Goutte   Link the time ser...
205
    timeSeries.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
206
    this
ae0aa7d2   Goutte   Add an x axis lab...
207

a06a0a67   Goutte   Prepare the time ...
208
209
210
211
212
  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...
213
214
215
216
217
218
219
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
      tmp = started_at
      started_at = stopped_at
      stopped_at = started_at

    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
220
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
221
222
      return

a06a0a67   Goutte   Prepare the time ...
223
224
225
    @current_started_at = started_at
    @current_stopped_at = stopped_at

6bb225d6   Goutte   Link the time ser...
226
227
    if (@started_at <= started_at <= @stopped_at) and
       (@started_at <= stopped_at <= @stopped_at) then
c3008fb2   Goutte   Clean up and refa...
228
      console.info "Resizing the temporal domain from #{started_at} to #{stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
229
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
230
      # the visible ones, for a smoother transition.
6bb225d6   Goutte   Link the time ser...
231
232
      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...
233
234
235
      @orbits.resizeDomain started_at, stopped_at
      return

2c0e1515   Goutte   Refactor loading ...
236
237
238
239
    # fetch new data and remake the plots
    @clearPlots()
    @loadAndCreatePlots(started_at, stopped_at)

8cb213b9   Goutte   Clean up, and pre...
240

6bb225d6   Goutte   Link the time ser...
241
242
  resetZoom: ->
    timeSeries.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
243
    @orbits.resetZoom()
8cb213b9   Goutte   Clean up, and pre...
244
245


ae0aa7d2   Goutte   Add an x axis lab...
246

ae0aa7d2   Goutte   Add an x axis lab...
247
248
249
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
250

f1f1e797   Goutte   Rewrite ugly java...
251
252
253
254
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

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

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

6bb225d6   Goutte   Link the time ser...
265
  setData: (data) ->
c3008fb2   Goutte   Clean up and refa...
266
    @data = data  # and pre-compute extents for performance when zooming
6bb225d6   Goutte   Link the time ser...
267
268
269
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)

f1f1e797   Goutte   Rewrite ugly java...
270
  init: ->
c3008fb2   Goutte   Clean up and refa...
271
    console.info "Initializing plot of #{@}…"
f1f1e797   Goutte   Rewrite ugly java...
272
273
274
275
276

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

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

f1f1e797   Goutte   Rewrite ugly java...
283
    @xAxis = d3.axisBottom()
08569a6b   Goutte   Add a zooming bru...
284
#               .tickFormat(d3.timeFormat("%Y-%m-%d"))
d49a163c   Goutte   Fix the resize an...
285
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
286
287
288
289
290
291
292
293
    @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...
294
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
295

f1f1e797   Goutte   Rewrite ugly java...
296
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
297
298
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
299

123313cb   Goutte   Clip the paths of...
300
301
302
303
304
305
306
307
308
309
    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...
310
311
312
                        .datum(@data)
                        .classed('line', true)

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

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

f1f1e797   Goutte   Rewrite ugly java...
320
321
322
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
323
324
325
326
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
327
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
328
329
330
331
332
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
333

81c9b2e8   Goutte   Add the values to...
334
335
336
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
340
    dx = 8
81c9b2e8   Goutte   Add the values to...
341
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
342
343
344
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
345

81c9b2e8   Goutte   Add the values to...
346
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
347
348
349
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
350

81c9b2e8   Goutte   Add the values to...
351
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
352
353
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
354
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
355

81c9b2e8   Goutte   Add the values to...
356
357
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
358
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
359
360
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
361
362
    @resize()

123313cb   Goutte   Clip the paths of...
363
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
364
  resize: ->
123313cb   Goutte   Clip the paths of...
365
366
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
367
368
369

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

123313cb   Goutte   Clip the paths of...
371
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
372
373
374
375
376
377
378

    @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...
379
380
381
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
382
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
383
384
385
386

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

2463bd16   Goutte   Add a circle foll...
387
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
388
389
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
390
391
392
393
394
395
396
397

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

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

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

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

2463bd16   Goutte   Add a circle foll...
404
405
406
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
407
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
408
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
409
410
411
412
413
414
415
416
417
418
      # 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 ...
419
420
      # 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...
421
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
422
423
424
425
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
426

6bb225d6   Goutte   Link the time ser...
427
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
428
429
    this

2c0e1515   Goutte   Refactor loading ...
430
431
432
433
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
434
435
436
437
438
439
440
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
442
443
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
444
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
445
446

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

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

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

08569a6b   Goutte   Add a zooming bru...
455
456
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
457
458
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
459
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
460
461
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
462
463
464

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
465
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
466
467
468
469
470
471
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
472
473
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
474
475
476
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
477
    if @visible
c3008fb2   Goutte   Clean up and refa...
478
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
479
480
481
482
483
      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...
484
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
485
486
487
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
488

541e2936   Goutte   Synchronize the t...
489
490
491
492
493
494
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
498
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
499
500
501
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
502
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
503
504
505
506
    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...
507
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
508

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

8cb213b9   Goutte   Clean up, and pre...
512
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
513
514
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
515
516
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
517
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
518
519
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
520
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
521
522
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
523
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
524
525
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
526
527

    this
f1f1e797   Goutte   Rewrite ugly java...
528

ae0aa7d2   Goutte   Add an x axis lab...
529
530
531
532

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

f1f1e797   Goutte   Rewrite ugly java...
533
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
534
535
536
537
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
538

a21f81d9   Goutte   Enable Venus and ...
539
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
540
541
542
    @init()

  init: ->
c3008fb2   Goutte   Clean up and refa...
543
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
544
545
546
547

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
548
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
549
550
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
551

a21f81d9   Goutte   Enable Venus and ...
552
553
554
    @data = {}  # slug => HCI array
    @orbiters = {}  # slug => config
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
555
556
557
558
559
560
    @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...
561
562
563
564
565
    @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...
566
567
568
569
570
571
572
573
574
    @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 ...
575
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
576
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
577

11662eed   Goutte   Add Y axis label ...
578
579
580
581
582
583
584
    @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...
585
586
587
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
588
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
589

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

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

a21f81d9   Goutte   Enable Venus and ...
598
599
600
601
602
603
    @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...
604
605
606
607
608
609
610
611
612
613
614
615
    # 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 ...
616
                                .datum(data)
438929a4   Goutte   Rewrite the orbit...
617
618
                                .classed('orbit orbit_section', true)

a21f81d9   Goutte   Enable Venus and ...
619
620
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
621
622
623
624
625
626
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
627
628
    @resize()

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

438929a4   Goutte   Rewrite the orbit...
631
632
    this

2c0e1515   Goutte   Refactor loading ...
633
634
635
  clear: ->
    $(@svg.node()).remove()

f1f1e797   Goutte   Rewrite ugly java...
636
  resize: ->
abcf4c94   Goutte   Clean up.
637
638
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
639

abcf4c94   Goutte   Clean up.
640
    console.debug("Resizing orbits : #{width} × #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
641
642
643
644
645
646
647

    @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...
648
    @sun.attr("x", width / 2 - 16).attr("y", height / 2 - 16)
f1f1e797   Goutte   Rewrite ugly java...
649

438929a4   Goutte   Rewrite the orbit...
650
    for slug, config of @orbiters
abcf4c94   Goutte   Clean up.
651
      @resizeOrbiter(slug, config, width, height)
438929a4   Goutte   Rewrite the orbit...
652

f1f1e797   Goutte   Rewrite ugly java...
653
654
655
656
657
658
659
660
661
662
    @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...
663
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
664
665
666
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
667

f1f1e797   Goutte   Rewrite ugly java...
668
669
    this

abcf4c94   Goutte   Clean up.
670
671
  resizeOrbiter: (slug, config, width, height) ->
    console.debug("Resizing orbit of #{slug}…")
438929a4   Goutte   Rewrite the orbit...
672
673
674
675
676
677
678
679
680
681
682
683
684

    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 ...
685
#        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
438929a4   Goutte   Rewrite the orbit...
686
687
    @yScale.range([height, 0])

a21f81d9   Goutte   Enable Venus and ...
688
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
689
690
691
692
693
694

    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...
695
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
696
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
697
698
699
700
    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...
701
702
703
704
705
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
706
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
707
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
708
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
709
710
711
712
713
      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.
714
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
715
    this
ae0aa7d2   Goutte   Add an x axis lab...
716

8cb213b9   Goutte   Clean up, and pre...
717
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
718
719
720
721
722
    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...
723

667eeb24   Goutte   Resize the domain...
724
725
726
727
728
  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.