Blame view

web/static/js/swapp.ls 21.6 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")
b60e7acd   Goutte   Rename "source" i...
73
74
75
76
77
78
79
80
81
82
83
84
85
    active_targets.forEach((target) ~>
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
          console.info "Loaded CSV data for #{target.slug}."
          @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
86
    url = @configuration['api']['data_for_interval']
b60e7acd   Goutte   Rename "source" i...
87
    url = url.replace('<target>', target_slug)
a4a9ef03   Goutte   Cache generated C...
88
89
    url = url.replace('<started_at>', started_at)
    url = url.replace('<stopped_at>', stopped_at)
f75faf5f   Goutte   WIP
90
    url
ae0aa7d2   Goutte   Add an x axis lab...
91

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

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

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

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

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

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

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

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

b7fe650c   Goutte   Misc bundle of ol...
169
170
  disableParameter: (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 = false
6bb225d6   Goutte   Link the time ser...
172
    timeSeries.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
173
    this
ae0aa7d2   Goutte   Add an x axis lab...
174

8cb213b9   Goutte   Clean up, and pre...
175
176
177
178
179
180
181
  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...
182
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
183
184
      return

6bb225d6   Goutte   Link the time ser...
185
186
187
188
189
190
191
    if (@started_at <= started_at <= @stopped_at) and
       (@started_at <= stopped_at <= @stopped_at) then
      console.info "Resizing the temporal domain from #{started_at} to #{stopped_at} without fetching new data..."
      # 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...
192
193
194
195
196
      @orbits.resizeDomain started_at, stopped_at
      return

    # todo: fetch new data and remake the plots

6bb225d6   Goutte   Link the time ser...
197
198
  resetZoom: ->
    timeSeries.forEach((ts) -> ts.resetZoom())
8cb213b9   Goutte   Clean up, and pre...
199
200


ae0aa7d2   Goutte   Add an x axis lab...
201

ae0aa7d2   Goutte   Add an x axis lab...
202
203
204
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
205

f1f1e797   Goutte   Rewrite ugly java...
206
207
208
209
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

6bb225d6   Goutte   Link the time ser...
210
  (@swapp, @parameter, @title, @target, data, @visible, @container, @options = {}) ->
4cf497e0   Goutte   Make the targets ...
211
    # parameter : slug of the parameter to observe, like magn or pdyn
f1f1e797   Goutte   Rewrite ugly java...
212
    # title : string
3ee0b596   Goutte   Fix an annoying b...
213
    # target : target object like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
214
    # data : list of {x: <datetime>, y: <float>}
6bb225d6   Goutte   Link the time ser...
215
    @setData(data)
f1f1e797   Goutte   Rewrite ugly java...
216
217
    @init()

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

6bb225d6   Goutte   Link the time ser...
220
221
222
223
224
  setData: (data) ->
    @data = data
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)

f1f1e797   Goutte   Rewrite ugly java...
225
  init: ->
2038c9fb   Goutte   Add a zoom reset ...
226
    console.info "Initializing time series #{@}..."
f1f1e797   Goutte   Rewrite ugly java...
227
228
229
230
231

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
251
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
252
253
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
254

f1f1e797   Goutte   Rewrite ugly java...
255
256
257
258
    @path = @plotWrapper.append('path')
                        .datum(@data)
                        .classed('line', true)

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

2038c9fb   Goutte   Add a zoom reset ...
262
    # deprecated, use brush's 'overlay' child
2463bd16   Goutte   Add a circle foll...
263
264
    @mouseCanvas = @plotWrapper.append("rect")
                               .style("fill", "none")
08569a6b   Goutte   Add a zooming bru...
265
#                               .style("pointer-events", "all")
3ee0b596   Goutte   Fix an annoying b...
266

f1f1e797   Goutte   Rewrite ugly java...
267
268
269
270
271
272
273
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
        .attr("transform", "rotate(-90)")
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text(@title)
b60e7acd   Goutte   Rename "source" i...
274
    @yAxisTextTarget = @plotWrapper.append("text")
fe3132dd   Goutte   Refactor even more.
275
276
277
278
        .attr("transform", "rotate(-90)")
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .style("font-style", "oblique")
b60e7acd   Goutte   Rename "source" i...
279
        .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
280

81c9b2e8   Goutte   Add the values to...
281
282
283
284
285
286
    @focus = @plotWrapper.append('g').style("display", "none")

    @cursorCircle = @focus.append("circle")
                         .attr("class", "cursor-circle")
                         .attr("r", 3)

541e2936   Goutte   Synchronize the t...
287
    dx = 8
81c9b2e8   Goutte   Add the values to...
288
    @cursorValueShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
289
290
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
291
                              .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
292

81c9b2e8   Goutte   Add the values to...
293
294
    @cursorValue = @focus.append("text")
                        .attr("class", "cursor-text cursor-value")
541e2936   Goutte   Synchronize the t...
295
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
296
297
                        .attr("dy", "-.3em")

541e2936   Goutte   Synchronize the t...
298

81c9b2e8   Goutte   Add the values to...
299
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
300
301
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
302
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
303

81c9b2e8   Goutte   Add the values to...
304
305
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
306
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
307
308
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
309
310
311
312
    @resize()

  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
541e2936   Goutte   Synchronize the t...
313
314
315
316
    height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width

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

08569a6b   Goutte   Add a zooming bru...
318
    #console.log("Resizing time series #{@title} of #{@target.name}: #{width} x #{height}")
f1f1e797   Goutte   Rewrite ugly java...
319
320
321
322
323
324
325

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

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

08569a6b   Goutte   Add a zooming bru...
326
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
327
328
329
330

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

2463bd16   Goutte   Add a circle foll...
331
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
332
333
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
334
335
336
337
338
339
340
341

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

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

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

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

2463bd16   Goutte   Add a circle foll...
348
349
350
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
351
    if not @brushFunction?
2038c9fb   Goutte   Add a zoom reset ...
352
      console.log "Creating the zooming brush for #{@}..."
08569a6b   Goutte   Add a zooming bru...
353
354
355
356
357
358
359
360
361
362
      # 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 ...
363
364
      # 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...
365
      @svg.select(".brush .overlay")
2038c9fb   Goutte   Add a zoom reset ...
366
367
368
369
          .on("mouseover.swappcursor", @onMouseOver)
          .on("mouseout.swappcursor",  @onMouseOut)
          .on("mousemove.swappcursor", @onMouseMove)
          .on("dblclick",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
370

6bb225d6   Goutte   Link the time ser...
371
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
372
373
    this

6bb225d6   Goutte   Link the time ser...
374
375
376
377
378
379
380
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
382
383
384
385
386
387
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
    if @options.onMouseMove?
      @options.onMouseMove(x)
    else
      @moveCursor(x)
541e2936   Goutte   Synchronize the t...
388
389
390
391
392
393
394
395
396
397
398
399
400

  onMouseOver: ~>
    if @options.onMouseOver?
      @options.onMouseOver()
    else
      @showCursor()

  onMouseOut: ~>
    if @options.onMouseOut?
      @options.onMouseOut()
    else
      @hideCursor()

2038c9fb   Goutte   Add a zoom reset ...
401
  onDoubleClick: ~>
6bb225d6   Goutte   Link the time ser...
402
    @swapp.resetZoom()
2038c9fb   Goutte   Add a zoom reset ...
403

08569a6b   Goutte   Add a zooming bru...
404
405
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
406
407
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
408
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
6bb225d6   Goutte   Link the time ser...
409
      @swapp.resizeDomain(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
410
411
412

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
413
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
414
415
416
417
418
419
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
420
421
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
422
423
424
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
425
426
427
428
429
430
431
432
433
434
435
    if @visible
      console.log("Applying zoom to visible #{@}...")
      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
      console.log("Applying zoom to hidden #{@}...")
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
436

541e2936   Goutte   Synchronize the t...
437
438
439
440
441
442
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
446
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
447
448
449
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
450
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
451
452
453
454
    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...
455
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
456

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

8cb213b9   Goutte   Clean up, and pre...
460
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
461
462
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
463
464
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
465
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
466
467
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
468
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
469
470
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
471
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
472
473
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
474
475

    this
f1f1e797   Goutte   Rewrite ugly java...
476

ae0aa7d2   Goutte   Add an x axis lab...
477
478
479
480

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

f1f1e797   Goutte   Rewrite ugly java...
481
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
482
483
484
485
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
486

a21f81d9   Goutte   Enable Venus and ...
487
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
488
489
490
    @init()

  init: ->
a21f81d9   Goutte   Enable Venus and ...
491
    console.log "Initializing orbits...", @options
f1f1e797   Goutte   Rewrite ugly java...
492
493
494
495

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
496
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
497
498
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
499

a21f81d9   Goutte   Enable Venus and ...
500
501
502
    @data = {}  # slug => HCI array
    @orbiters = {}  # slug => config
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
503
504
505
506
507
508
    @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...
509
510
511
512
513
    @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...
514
515
516
517
518
519
520
521
522
    @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 ...
523
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
524
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
525

11662eed   Goutte   Add Y axis label ...
526
527
528
529
530
531
532
    @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...
533
534
535
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
536
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
537

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

438929a4   Goutte   Rewrite the orbit...
541
  orbitersElements: {}
a21f81d9   Goutte   Enable Venus and ...
542
543
  initOrbiter: (slug, config, data) ->
    console.log("Initializing target #{slug}'s orbit...", config, data)
438929a4   Goutte   Rewrite the orbit...
544
545
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

a21f81d9   Goutte   Enable Venus and ...
546
547
548
549
550
551
    @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...
552
553
554
555
556
557
558
559
560
561
562
563
    # 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 ...
564
                                .datum(data)
438929a4   Goutte   Rewrite the orbit...
565
566
                                .classed('orbit orbit_section', true)

a21f81d9   Goutte   Enable Venus and ...
567
568
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
569
570
571
572
573
574
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
575
576
    @resize()

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

438929a4   Goutte   Rewrite the orbit...
579
580
    this

f1f1e797   Goutte   Rewrite ugly java...
581
582
583
584
  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

8cb213b9   Goutte   Clean up, and pre...
585
    #console.log("Resizing orbits : #{width} x #{height}")
f1f1e797   Goutte   Rewrite ugly java...
586
587
588
589
590
591
592

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

438929a4   Goutte   Rewrite the orbit...
595
596
597
    for slug, config of @orbiters
      @resizeOrbiter(slug, config)

f1f1e797   Goutte   Rewrite ugly java...
598
599
600
601
602
603
604
605
606
607
    @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...
608
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
609
610
611
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
612

f1f1e797   Goutte   Rewrite ugly java...
613
614
    this

438929a4   Goutte   Rewrite the orbit...
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
  resizeOrbiter: (slug, config) ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

    console.log("Resize orbiter #{slug}")

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

a21f81d9   Goutte   Enable Venus and ...
636
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
637
638
639
640
641
642

    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...
643
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
644
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
645
646
647
648
    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...
649
650
651
652
653
    this

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

  moveToDate: (t) ->
a21f81d9   Goutte   Enable Venus and ...
654
    console.log("Trying to move to an undefined date") unless t
ae0aa7d2   Goutte   Add an x axis lab...
655
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
656
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
657
658
659
660
661
      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
8cb213b9   Goutte   Clean up, and pre...
662
      @repositionOrbiter(slug, d)  # fixme <--(why?)
a21f81d9   Goutte   Enable Venus and ...
663
    this
ae0aa7d2   Goutte   Add an x axis lab...
664

8cb213b9   Goutte   Clean up, and pre...
665
666
  resizeDomain: (started_at, stopped_at) ->
    # fixme
ae0aa7d2   Goutte   Add an x axis lab...