Blame view

web/static/js/swapp.ls 20.9 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.
4cf497e0   Goutte   Make the targets ...
31
  """
ae0aa7d2   Goutte   Add an x axis lab...
32

f75faf5f   Goutte   WIP
33
  (@configuration) ->
2038c9fb   Goutte   Add a zoom reset ...
34
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
"""  # HelioPropa by CDPP
b60e7acd   Goutte   Rename "source" i...
49
50
51
52
    @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.
53
    )
b7fe650c   Goutte   Misc bundle of ol...
54
55
56
57
    @parameters = {}
    @configuration['parameters'].forEach((p) ~>
        @parameters[p['id']] = p
    )
ae0aa7d2   Goutte   Add an x axis lab...
58

b60e7acd   Goutte   Rename "source" i...
59
60
61
62
63
64
65
66
67
  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...
68
69
70
71
    @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...
72
73
74
75
76
77
78
79
80
81
82
83
84
    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
85
    url = @configuration['api']['data_for_interval']
b60e7acd   Goutte   Rename "source" i...
86
    url = url.replace('<target>', target_slug)
a4a9ef03   Goutte   Cache generated C...
87
88
    url = url.replace('<started_at>', started_at)
    url = url.replace('<stopped_at>', stopped_at)
f75faf5f   Goutte   WIP
89
    url
ae0aa7d2   Goutte   Add an x axis lab...
90

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

b60e7acd   Goutte   Rename "source" i...
95
96
97
  showAllTargets: ->
    for slug, target of @targets
      showTarget(slug)
f75faf5f   Goutte   WIP
98
99
    this

b60e7acd   Goutte   Rename "source" i...
100
101
102
  showTarget: (target_slug) ->
    timeSeries.forEach((ts) ~> $(ts.svg.node()).show() if ts.target.slug == target_slug && @parameters[ts.parameter].active)
    @targets[target_slug].active = true
f75faf5f   Goutte   WIP
103
104
    this

b60e7acd   Goutte   Rename "source" i...
105
106
107
  hideTarget: (target_slug) ->
    timeSeries.forEach((ts) -> $(ts.svg.node()).hide() if ts.target.slug == target_slug)
    @targets[target_slug].active = false
f75faf5f   Goutte   WIP
108
109
    this

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

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

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

b7fe650c   Goutte   Misc bundle of ol...
160
161
  enableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
162
    @parameters[parameter_slug].active = true
b60e7acd   Goutte   Rename "source" i...
163
    timeSeries.forEach((ts) ~> $(ts.svg.node()).show() if ts.parameter == parameter_slug && @targets[ts.target.slug].active)
b7fe650c   Goutte   Misc bundle of ol...
164
    this
4816cef4   Goutte   Refactor some more.
165

b7fe650c   Goutte   Misc bundle of ol...
166
167
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
168
    @parameters[parameter_slug].active = false
4cf497e0   Goutte   Make the targets ...
169
    timeSeries.forEach((ts) -> $(ts.svg.node()).hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
170
    this
ae0aa7d2   Goutte   Add an x axis lab...
171

8cb213b9   Goutte   Clean up, and pre...
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
  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
      console.warn "Please provide different start and stop dates."
      return

    if (@started_at <= started_at <= @stopped_at) and (@started_at <= stopped_at <= @stopped_at)
      console.info "Resizing the temporal domain without fetching new data..."
      timeSeries.forEach((ts) -> ts.resizeDomain started_at, stopped_at)
      @orbits.resizeDomain started_at, stopped_at
      return

    # todo: fetch new data and remake the plots



ae0aa7d2   Goutte   Add an x axis lab...
192

ae0aa7d2   Goutte   Add an x axis lab...
193
194
195
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
196

f1f1e797   Goutte   Rewrite ugly java...
197
198
199
200
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

b60e7acd   Goutte   Rename "source" i...
201
  (@parameter, @title, @target, @data, @active, @container, @options = {}) ->
4cf497e0   Goutte   Make the targets ...
202
    # parameter : slug of the parameter to observe, like magn or pdyn
f1f1e797   Goutte   Rewrite ugly java...
203
    # title : string
3ee0b596   Goutte   Fix an annoying b...
204
    # target : target object like described in configuration
f1f1e797   Goutte   Rewrite ugly java...
205
    # data : list of {x: <datetime>, y: <float>}
f1f1e797   Goutte   Rewrite ugly java...
206
207
    @init()

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

f1f1e797   Goutte   Rewrite ugly java...
210
  init: ->
2038c9fb   Goutte   Add a zoom reset ...
211
    console.info "Initializing time series #{@}..."
f1f1e797   Goutte   Rewrite ugly java...
212
213
214
215
216

    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
217
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
218
219
220
221
222
    }

    @xScale = d3.scaleTime().domain(d3.extent(@data, (d) -> d.x))
    @yScale = d3.scaleLinear().domain(d3.extent(@data, (d) -> d.y))

f1f1e797   Goutte   Rewrite ugly java...
223
    @xAxis = d3.axisBottom()
08569a6b   Goutte   Add a zooming bru...
224
#               .tickFormat(d3.timeFormat("%Y-%m-%d"))
d49a163c   Goutte   Fix the resize an...
225
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
226
227
228
229
230
231
232
233
    @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...
234
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
235

f1f1e797   Goutte   Rewrite ugly java...
236
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
237
238
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
239

f1f1e797   Goutte   Rewrite ugly java...
240
241
242
243
    @path = @plotWrapper.append('path')
                        .datum(@data)
                        .classed('line', true)

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

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

f1f1e797   Goutte   Rewrite ugly java...
252
253
254
255
256
257
258
    @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...
259
    @yAxisTextTarget = @plotWrapper.append("text")
fe3132dd   Goutte   Refactor even more.
260
261
262
263
        .attr("transform", "rotate(-90)")
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .style("font-style", "oblique")
b60e7acd   Goutte   Rename "source" i...
264
        .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
265

81c9b2e8   Goutte   Add the values to...
266
267
268
269
270
271
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
272
    dx = 8
81c9b2e8   Goutte   Add the values to...
273
    @cursorValueShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
274
275
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
276
                              .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
277

81c9b2e8   Goutte   Add the values to...
278
279
    @cursorValue = @focus.append("text")
                        .attr("class", "cursor-text cursor-value")
541e2936   Goutte   Synchronize the t...
280
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
281
282
                        .attr("dy", "-.3em")

541e2936   Goutte   Synchronize the t...
283

81c9b2e8   Goutte   Add the values to...
284
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
285
286
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
287
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
288

81c9b2e8   Goutte   Add the values to...
289
290
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
291
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
292
293
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
294
295
296
297
    @resize()

  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
541e2936   Goutte   Synchronize the t...
298
299
300
301
    height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width

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

08569a6b   Goutte   Add a zooming bru...
303
    #console.log("Resizing time series #{@title} of #{@target.name}: #{width} x #{height}")
f1f1e797   Goutte   Rewrite ugly java...
304
305
306
307
308
309
310

    @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...
311
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
312
313
314
315

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

2463bd16   Goutte   Add a circle foll...
316
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
317
318
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
319
320
321
322
323
324
325
326

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

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

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

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

2463bd16   Goutte   Add a circle foll...
333
334
335
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
336
    if not @brushFunction?
2038c9fb   Goutte   Add a zoom reset ...
337
      console.log "Creating the zooming brush for #{@}..."
08569a6b   Goutte   Add a zooming bru...
338
339
340
341
342
343
344
345
346
347
      # 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 ...
348
349
      # 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...
350
      @svg.select(".brush .overlay")
2038c9fb   Goutte   Add a zoom reset ...
351
352
353
354
          .on("mouseover.swappcursor", @onMouseOver)
          .on("mouseout.swappcursor",  @onMouseOut)
          .on("mousemove.swappcursor", @onMouseMove)
          .on("dblclick",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
355
356

    unless @active then $(@svg.node()).hide()
f1f1e797   Goutte   Rewrite ugly java...
357
358
    this

2038c9fb   Goutte   Add a zoom reset ...
359
  resizeDomain: (startDate, stopDate) ->
8cb213b9   Goutte   Clean up, and pre...
360
    # fixme
3ee0b596   Goutte   Fix an annoying b...
361
#    d3.behavior.zoom()
8cb213b9   Goutte   Clean up, and pre...
362

541e2936   Goutte   Synchronize the t...
363
364
365
366
367
368
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
    if @options.onMouseMove?
      @options.onMouseMove(x)
    else
      @moveCursor(x)
541e2936   Goutte   Synchronize the t...
369
370
371
372
373
374
375
376
377
378
379
380
381

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

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

2038c9fb   Goutte   Add a zoom reset ...
382
383
384
385
  onDoubleClick: ~>
    console.debug "Resetting zoom of #{@}."
    @resetZoom()

08569a6b   Goutte   Add a zooming bru...
386
387
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
388
389
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
390
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
2038c9fb   Goutte   Add a zoom reset ...
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
      @zoomIn(minmax[0], minmax[1])

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
    [minDate, maxDate] = d3.extent(@data, (d) -> d.x)
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
    @xScale.domain(d3.extent(@data, (d) -> d.x))
    @yScale.domain(d3.extent(@data, (d) -> d.y))
    @applyZoom()

  applyZoom: ->
08569a6b   Goutte   Add a zooming bru...
407
408
409
410
411
    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)

541e2936   Goutte   Synchronize the t...
412
413
414
415
416
417
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
421
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
422
423
424
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
425
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
426
427
428
429
    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...
430
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
431

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

8cb213b9   Goutte   Clean up, and pre...
435
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
436
437
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
438
439
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
440
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
441
442
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
443
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
444
445
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
446
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
447
448
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
449
450

    this
f1f1e797   Goutte   Rewrite ugly java...
451

ae0aa7d2   Goutte   Add an x axis lab...
452
453
454
455

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

f1f1e797   Goutte   Rewrite ugly java...
456
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
457
458
459
460
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
461

a21f81d9   Goutte   Enable Venus and ...
462
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
463
464
465
    @init()

  init: ->
a21f81d9   Goutte   Enable Venus and ...
466
    console.log "Initializing orbits...", @options
f1f1e797   Goutte   Rewrite ugly java...
467
468
469
470

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
471
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
472
473
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
474

a21f81d9   Goutte   Enable Venus and ...
475
476
477
    @data = {}  # slug => HCI array
    @orbiters = {}  # slug => config
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
478
479
480
481
482
483
    @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...
484
485
486
487
488
    @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...
489
490
491
492
493
494
495
496
497
    @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 ...
498
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
499
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
500

11662eed   Goutte   Add Y axis label ...
501
502
503
504
505
506
507
    @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...
508
509
510
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
511
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
512

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

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

a21f81d9   Goutte   Enable Venus and ...
521
522
523
524
525
526
    @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...
527
528
529
530
531
532
533
534
535
536
537
538
    # 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 ...
539
                                .datum(data)
438929a4   Goutte   Rewrite the orbit...
540
541
                                .classed('orbit orbit_section', true)

a21f81d9   Goutte   Enable Venus and ...
542
543
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
544
545
546
547
548
549
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
550
551
    @resize()

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

438929a4   Goutte   Rewrite the orbit...
554
555
    this

f1f1e797   Goutte   Rewrite ugly java...
556
557
558
559
  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

8cb213b9   Goutte   Clean up, and pre...
560
    #console.log("Resizing orbits : #{width} x #{height}")
f1f1e797   Goutte   Rewrite ugly java...
561
562
563
564
565
566
567

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

438929a4   Goutte   Rewrite the orbit...
570
571
572
    for slug, config of @orbiters
      @resizeOrbiter(slug, config)

f1f1e797   Goutte   Rewrite ugly java...
573
574
575
576
577
578
579
580
581
582
    @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...
583
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
584
585
586
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
587

f1f1e797   Goutte   Rewrite ugly java...
588
589
    this

438929a4   Goutte   Rewrite the orbit...
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
  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 ...
608
#        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
438929a4   Goutte   Rewrite the orbit...
609
610
    @yScale.range([height, 0])

a21f81d9   Goutte   Enable Venus and ...
611
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
612
613
614
615
616
617

    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...
618
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
619
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
620
621
622
623
    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...
624
625
626
627
628
    this

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

  moveToDate: (t) ->
a21f81d9   Goutte   Enable Venus and ...
629
    console.log("Trying to move to an undefined date") unless t
ae0aa7d2   Goutte   Add an x axis lab...
630
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
631
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
632
633
634
635
636
      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...
637
      @repositionOrbiter(slug, d)  # fixme <--(why?)
a21f81d9   Goutte   Enable Venus and ...
638
    this
ae0aa7d2   Goutte   Add an x axis lab...
639

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