Blame view

web/static/js/swapp.ls 23 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.
b60e7acd   Goutte   Rename "source" i...
14

4cf497e0   Goutte   Make the targets ...
15
16
17
###############################################################################

const GOLDEN_RATIO = 2 / (1 + Math.sqrt(5))  # Between 0 and 1 (0.618...)
f1f1e797   Goutte   Rewrite ugly java...
18

ae0aa7d2   Goutte   Add an x axis lab...
19
20
###############################################################################

b60e7acd   Goutte   Rename "source" i...
21
22
23
24
25
26
class Target
  (@slug, @name, @config) ->
    @active = true  # by default, all targets are active at first

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

ae0aa7d2   Goutte   Add an x axis lab...
27
export class SpaceWeather
4cf497e0   Goutte   Make the targets ...
28
29
  """
  The main app, instanciated from an inline script.
b60e7acd   Goutte   Rename "source" i...
30
  It defaults to an interval starting a year ago, and ending in seven days.
6bb225d6   Goutte   Link the time ser...
31
  (both at midnight)
4cf497e0   Goutte   Make the targets ...
32
  """
ae0aa7d2   Goutte   Add an x axis lab...
33

f75faf5f   Goutte   WIP
34
  (@configuration) ->
2038c9fb   Goutte   Add a zoom reset ...
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    console.info """
  _   _      _ _       ____
 | | | | ___| (_) ___ |  _ \\ _ __ ___  _ __   __ _
 | |_| |/ _ \\ | |/ _ \\| |_) | '__/ _ \\| '_ \\ / _` |
 |  _  |  __/ | | (_) |  __/| | | (_) | |_) | (_| |
 |_| |_|\\___|_|_|\\___/|_|_  |_|_ \\___/| .__/ \\__,_|
 | |__  _   _   / ___|  _ \\|  _ \\|  _ \\_|
 | '_ \\| | | | | |   | | | | |_) | |_) |
 | |_) | |_| | | |___| |_| |  __/|  __/
 |_.__/ \\__, |  \\____|____/|_|   |_|
        |___/

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

b60e7acd   Goutte   Rename "source" i...
60
61
62
63
64
65
66
67
68
  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)
    """
    active_targets = [ @targets[k] for k of @targets when @targets[k].config.active ]
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
    # Set the h/m/s to zero so that files are cached per whole days
8cb213b9   Goutte   Clean up, and pre...
69
70
71
72
    @started_at = moment().subtract(1, 'years').hours(0).minutes(0).seconds(0)
    @stopped_at = moment().add(7, 'days').hours(0).minutes(0).seconds(0)
    started_at = @started_at.format("YYYY-MM-DDTHH:mm:ss")
    stopped_at = @stopped_at.format("YYYY-MM-DDTHH:mm:ss")
ebe77ce4   Goutte   Clean up.
73
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
b60e7acd   Goutte   Rename "source" i...
74
    active_targets.forEach((target) ~>
c3008fb2   Goutte   Clean up and refa...
75
      console.info "Loading CSV data of #{target.name}…"
b60e7acd   Goutte   Rename "source" i...
76
77
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
c3008fb2   Goutte   Clean up and refa...
78
          console.info "Loaded CSV data of #{target.name}."
b60e7acd   Goutte   Rename "source" i...
79
80
81
82
83
84
85
86
87
          @createTimeSeries(target, data)
          @orbits.initOrbiter(target.slug, target.config, data['hci'])
        ,
        (error) -> console.error('Failed to load CSV data.', error)
      )
    )
    window.addEventListener 'resize', ~> @resize()

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

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

6bb225d6   Goutte   Link the time ser...
98
  enableAllTargets: ->
b60e7acd   Goutte   Rename "source" i...
99
100
    for slug, target of @targets
      showTarget(slug)
f75faf5f   Goutte   WIP
101
102
    this

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

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

fe3132dd   Goutte   Refactor even more.
113
  resize: ->
a21f81d9   Goutte   Enable Venus and ...
114
    @orbits?.resize();
d49a163c   Goutte   Fix the resize an...
115
    timeSeries.forEach((ts) -> ts.resize())
fe3132dd   Goutte   Refactor even more.
116

b60e7acd   Goutte   Rename "source" i...
117
118
119
120
121
  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
122
123
    sw = this
    promise = new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
124
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
125
      d3.csv(url, (csv) ->
f75faf5f   Goutte   WIP
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
        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)
      )
    )
    promise
ae0aa7d2   Goutte   Add an x axis lab...
144

8cb213b9   Goutte   Clean up, and pre...
145
  timeSeries = []  # Not sure why this ain't an instance prop. Probably should.
b60e7acd   Goutte   Rename "source" i...
146
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
147
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
148
149
150
      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...
151
      timeSeries.push(new TimeSeries(
c3008fb2   Goutte   Clean up and refa...
152
        id, title, target, data[id], @parameters[id].active, container
6bb225d6   Goutte   Link the time ser...
153
      ))
4816cef4   Goutte   Refactor some more.
154
    )
fe3132dd   Goutte   Refactor even more.
155
    timeSeries.forEach((ts) ~>
4816cef4   Goutte   Refactor some more.
156
157
158
159
      ts.options['onMouseOver'] = ->
        timeSeries.forEach((ts2) -> ts2.showCursor())
      ts.options['onMouseOut'] = ->
        timeSeries.forEach((ts2) -> ts2.hideCursor())
fe3132dd   Goutte   Refactor even more.
160
      ts.options['onMouseMove'] = (t) ~>
4816cef4   Goutte   Refactor some more.
161
        timeSeries.forEach((ts2) -> ts2.moveCursor(t))
fe3132dd   Goutte   Refactor even more.
162
        @orbits?.moveToDate(t)
c3008fb2   Goutte   Clean up and refa...
163
164
165
166
      ts.options['onBrushEnd'] = (sta, sto) ~>
        @resizeDomain(sta, sto)
      ts.options['onDblClick'] = ~>
        @resetZoom()
4816cef4   Goutte   Refactor some more.
167
168
    )

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

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

8cb213b9   Goutte   Clean up, and pre...
181
182
183
184
185
186
187
  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...
188
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
189
190
      return

6bb225d6   Goutte   Link the time ser...
191
192
    if (@started_at <= started_at <= @stopped_at) and
       (@started_at <= stopped_at <= @stopped_at) then
c3008fb2   Goutte   Clean up and refa...
193
      console.info "Resizing the temporal domain from #{started_at} to #{stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
194
195
196
197
      # We first resize the hidden time series and only afterwards we resize
      # the visible ones, for a smoother transition
      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...
198
199
200
201
202
      @orbits.resizeDomain started_at, stopped_at
      return

    # todo: fetch new data and remake the plots

6bb225d6   Goutte   Link the time ser...
203
204
  resetZoom: ->
    timeSeries.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
205
    @orbits.resetZoom()
8cb213b9   Goutte   Clean up, and pre...
206
207


ae0aa7d2   Goutte   Add an x axis lab...
208

ae0aa7d2   Goutte   Add an x axis lab...
209
210
211
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
212

f1f1e797   Goutte   Rewrite ugly java...
213
214
215
216
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

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

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

6bb225d6   Goutte   Link the time ser...
227
  setData: (data) ->
c3008fb2   Goutte   Clean up and refa...
228
    @data = data  # and pre-compute extents for performance when zooming
6bb225d6   Goutte   Link the time ser...
229
230
231
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)

f1f1e797   Goutte   Rewrite ugly java...
232
  init: ->
c3008fb2   Goutte   Clean up and refa...
233
    console.info "Initializing plot of #{@}…"
f1f1e797   Goutte   Rewrite ugly java...
234
235
236
237
238

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

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

f1f1e797   Goutte   Rewrite ugly java...
245
    @xAxis = d3.axisBottom()
08569a6b   Goutte   Add a zooming bru...
246
#               .tickFormat(d3.timeFormat("%Y-%m-%d"))
d49a163c   Goutte   Fix the resize an...
247
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
248
249
250
251
252
253
254
255
    @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...
256
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
257

f1f1e797   Goutte   Rewrite ugly java...
258
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
259
260
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
261

123313cb   Goutte   Clip the paths of...
262
263
264
265
266
267
268
269
270
271
    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...
272
273
274
                        .datum(@data)
                        .classed('line', true)

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

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

f1f1e797   Goutte   Rewrite ugly java...
282
283
284
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
285
286
287
288
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
289
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
290
291
292
293
294
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
295

81c9b2e8   Goutte   Add the values to...
296
297
298
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
302
    dx = 8
81c9b2e8   Goutte   Add the values to...
303
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
304
305
306
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
307

81c9b2e8   Goutte   Add the values to...
308
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
309
310
311
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
312

81c9b2e8   Goutte   Add the values to...
313
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
314
315
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
316
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
317

81c9b2e8   Goutte   Add the values to...
318
319
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
320
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
321
322
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
323
324
    @resize()

123313cb   Goutte   Clip the paths of...
325
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
326
  resize: ->
123313cb   Goutte   Clip the paths of...
327
328
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
329
330
331

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

123313cb   Goutte   Clip the paths of...
333
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
334
335
336
337
338
339
340

    @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...
341
342
343
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
344
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
345
346
347
348

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

2463bd16   Goutte   Add a circle foll...
349
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
350
351
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
352
353
354
355
356
357
358
359

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

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

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

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

2463bd16   Goutte   Add a circle foll...
366
367
368
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
369
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
370
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
371
372
373
374
375
376
377
378
379
380
      # 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 ...
381
382
      # 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...
383
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
384
385
386
387
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
388

6bb225d6   Goutte   Link the time ser...
389
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
390
391
    this

6bb225d6   Goutte   Link the time ser...
392
393
394
395
396
397
398
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
400
401
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
402
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
403
404

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

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

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

08569a6b   Goutte   Add a zooming bru...
413
414
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
415
416
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
417
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
418
419
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
420
421
422

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
423
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
424
425
426
427
428
429
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
430
431
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
432
433
434
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
435
    if @visible
c3008fb2   Goutte   Clean up and refa...
436
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
437
438
439
440
441
      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...
442
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
443
444
445
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
446

541e2936   Goutte   Synchronize the t...
447
448
449
450
451
452
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
456
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
457
458
459
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
460
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
461
462
463
464
    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...
465
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
466

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

8cb213b9   Goutte   Clean up, and pre...
470
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
471
472
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
473
474
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
475
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
476
477
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
478
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
479
480
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
481
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
482
483
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
484
485

    this
f1f1e797   Goutte   Rewrite ugly java...
486

ae0aa7d2   Goutte   Add an x axis lab...
487
488
489
490

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

f1f1e797   Goutte   Rewrite ugly java...
491
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
492
493
494
495
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
496

a21f81d9   Goutte   Enable Venus and ...
497
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
498
499
500
    @init()

  init: ->
c3008fb2   Goutte   Clean up and refa...
501
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
502
503
504
505

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
506
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
507
508
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
509

a21f81d9   Goutte   Enable Venus and ...
510
511
512
    @data = {}  # slug => HCI array
    @orbiters = {}  # slug => config
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
513
514
515
516
517
518
    @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...
519
520
521
522
523
    @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...
524
525
526
527
528
529
530
531
532
    @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 ...
533
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
534
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
535

11662eed   Goutte   Add Y axis label ...
536
537
538
539
540
541
542
    @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...
543
544
545
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
546
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
547

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

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

a21f81d9   Goutte   Enable Venus and ...
556
557
558
559
560
561
    @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...
562
563
564
565
566
567
568
569
570
571
572
573
    # 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 ...
574
                                .datum(data)
438929a4   Goutte   Rewrite the orbit...
575
576
                                .classed('orbit orbit_section', true)

a21f81d9   Goutte   Enable Venus and ...
577
578
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
579
580
581
582
583
584
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
585
586
    @resize()

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

438929a4   Goutte   Rewrite the orbit...
589
590
    this

f1f1e797   Goutte   Rewrite ugly java...
591
  resize: ->
abcf4c94   Goutte   Clean up.
592
593
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
594

abcf4c94   Goutte   Clean up.
595
    console.debug("Resizing orbits : #{width} × #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
596
597
598
599
600
601
602

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

438929a4   Goutte   Rewrite the orbit...
605
    for slug, config of @orbiters
abcf4c94   Goutte   Clean up.
606
      @resizeOrbiter(slug, config, width, height)
438929a4   Goutte   Rewrite the orbit...
607

f1f1e797   Goutte   Rewrite ugly java...
608
609
610
611
612
613
614
615
616
617
    @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...
618
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
619
620
621
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
622

f1f1e797   Goutte   Rewrite ugly java...
623
624
    this

abcf4c94   Goutte   Clean up.
625
626
  resizeOrbiter: (slug, config, width, height) ->
    console.debug("Resizing orbit of #{slug}…")
438929a4   Goutte   Rewrite the orbit...
627
628
629
630
631
632
633
634
635
636
637
638
639

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

a21f81d9   Goutte   Enable Venus and ...
643
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
644
645
646
647
648
649

    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...
650
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
651
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
652
653
654
655
    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...
656
657
658
659
660
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
661
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
662
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
663
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
664
665
666
667
668
      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.
669
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
670
    this
ae0aa7d2   Goutte   Add an x axis lab...
671

8cb213b9   Goutte   Clean up, and pre...
672
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
673
674
675
676
677
    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...
678

667eeb24   Goutte   Resize the domain...
679
680
681
682
683
  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.