Blame view

web/static/js/swapp.ls 22.5 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())
8cb213b9   Goutte   Clean up, and pre...
204
205


ae0aa7d2   Goutte   Add an x axis lab...
206

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

b60e7acd   Goutte   Rename "source" i...
210

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    this
f1f1e797   Goutte   Rewrite ugly java...
484

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

f1f1e797   Goutte   Rewrite ugly java...
621
622
    this

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

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

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

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

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

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

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

abcf4c94   Goutte   Clean up.