Blame view

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

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

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

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

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

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

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

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

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

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

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

6bb225d6   Goutte   Link the time ser...
190
191
    if (@started_at <= started_at <= @stopped_at) and
       (@started_at <= stopped_at <= @stopped_at) then
c3008fb2   Goutte   Clean up and refa...
192
      console.info "Resizing the temporal domain from #{started_at} to #{stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
193
194
195
196
      # 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...
197
198
199
200
201
      @orbits.resizeDomain started_at, stopped_at
      return

    # todo: fetch new data and remake the plots

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


ae0aa7d2   Goutte   Add an x axis lab...
207

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

b60e7acd   Goutte   Rename "source" i...
211

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    this
f1f1e797   Goutte   Rewrite ugly java...
485

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
622
623
    this

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

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

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

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

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

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

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

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