Blame view

web/static/js/swapp.ls 25.2 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.
9c0c4509   Goutte   Add a loader to t...
14
15
16
17
# You also will need d3js v4 documentation : https://d3js.org/
# We're using a custom build of 4.9.1, one line changed, see d3-custom.js
# Event bubbling cannot trigger two rects unless we make an event dispatcher,
# and d3's brush is stopping propagation, as it should by default.
b60e7acd   Goutte   Rename "source" i...
18

4cf497e0   Goutte   Make the targets ...
19
20
21
###############################################################################

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

ae0aa7d2   Goutte   Add an x axis lab...
23
24
###############################################################################

b60e7acd   Goutte   Rename "source" i...
25
26
class Target
  (@slug, @name, @config) ->
2c0e1515   Goutte   Refactor loading ...
27
    @active = @config.active
b60e7acd   Goutte   Rename "source" i...
28
29
30

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

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

2c0e1515   Goutte   Refactor loading ...
38
  API_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss"
243cd8a4   Goutte   Timestamp party c...
39
  INPUT_TIME_FORMAT = "YYYY-MM-DD"
2c0e1515   Goutte   Refactor loading ...
40

f75faf5f   Goutte   WIP
41
  (@configuration) ->
2038c9fb   Goutte   Add a zoom reset ...
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    console.info """
  _   _      _ _       ____
 | | | | ___| (_) ___ |  _ \\ _ __ ___  _ __   __ _
 | |_| |/ _ \\ | |/ _ \\| |_) | '__/ _ \\| '_ \\ / _` |
 |  _  |  __/ | | (_) |  __/| | | (_) | |_) | (_| |
 |_| |_|\\___|_|_|\\___/|_|_  |_|_ \\___/| .__/ \\__,_|
 | |__  _   _   / ___|  _ \\|  _ \\|  _ \\_|
 | '_ \\| | | | | |   | | | | |_) | |_) |
 | |_) | |_| | | |___| |_| |  __/|  __/
 |_.__/ \\__, |  \\____|____/|_|   |_|
        |___/

The full source of this website is available at :
https://gitlab.irap.omp.eu/CDPP/SPACEWEATHERONLINE
6bb225d6   Goutte   Link the time ser...
56
"""  # HelioPropa by CDPP (mushed 'cause we need to escape backslashes)
b60e7acd   Goutte   Rename "source" i...
57
58
59
60
    @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.
61
    )
b7fe650c   Goutte   Misc bundle of ol...
62
63
64
65
    @parameters = {}
    @configuration['parameters'].forEach((p) ~>
        @parameters[p['id']] = p
    )
7994cf1a   Goutte   Hunt bugs.
66
67
    @orbiter = null   # our Orbiter
    @timeSeries = []  # a List of TimeSeries objects
ae0aa7d2   Goutte   Add an x axis lab...
68

b60e7acd   Goutte   Rename "source" i...
69
70
71
72
73
74
  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)
    """
2c0e1515   Goutte   Refactor loading ...
75
76
    # Default time interval is from one year ago to one week ahead.
    # We set the h/m/s to zero to benefit from a daily cache.
7994cf1a   Goutte   Hunt bugs.
77
78
79
    started_at = moment().subtract(1, 'years').hours(0).minutes(0).seconds(0)
    stopped_at = moment().add(7, 'days').hours(0).minutes(0).seconds(0)
    @setStartAndStop(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
80
    @loadAndCreatePlots(started_at, stopped_at)
243cd8a4   Goutte   Timestamp party c...
81

b60e7acd   Goutte   Rename "source" i...
82
83
84
    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

2c0e1515   Goutte   Refactor loading ...
95
96
97
98
#  enableAllTargets: ->
#    for slug, target of @targets
#      enableTarget(slug)
#    this
f75faf5f   Goutte   WIP
99

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

6bb225d6   Goutte   Link the time ser...
105
  disableTarget: (target_slug) ->
243cd8a4   Goutte   Timestamp party c...
106
    @timeSeries.forEach((ts) -> ts.hide() if ts.target.slug == target_slug)
b60e7acd   Goutte   Rename "source" i...
107
    @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();
243cd8a4   Goutte   Timestamp party c...
112
    @timeSeries.forEach((ts) -> ts.resize())
fe3132dd   Goutte   Refactor even more.
113

2c0e1515   Goutte   Refactor loading ...
114
115
116
  showLoader: ->
    $("\#plots_loader").show();

9c0c4509   Goutte   Add a loader to t...
117
118
119
  hideLoader: ->
    $("\#plots_loader").hide();

b60e7acd   Goutte   Rename "source" i...
120
121
122
123
124
  loadData: (target_slug, started_at, stopped_at) ->
    """
    Load the data as CSV for the specified target and interval,
    and return it in a Promise.
    """
243cd8a4   Goutte   Timestamp party c...
125

f75faf5f   Goutte   WIP
126
    sw = this
2c0e1515   Goutte   Refactor loading ...
127
    new Promise((resolve, reject) ->
b60e7acd   Goutte   Rename "source" i...
128
      url = sw.buildDataUrlForTarget(target_slug, started_at, stopped_at)
a4a9ef03   Goutte   Cache generated C...
129
      d3.csv(url, (csv) ->
f75faf5f   Goutte   WIP
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
        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)
      )
    )
2c0e1515   Goutte   Refactor loading ...
147
148

  loadAndCreatePlots: (started_at, stopped_at) ->
243cd8a4   Goutte   Timestamp party c...
149
150
151
152
    """
    started_at: moment(.js) object
    stopped_at: moment(.js) object
    """
2c0e1515   Goutte   Refactor loading ...
153
    @showLoader()
7994cf1a   Goutte   Hunt bugs.
154
155
156
    @started_at = started_at
    @stopped_at = stopped_at
    @orbits = new Orbits(@configuration.orbits_container, @configuration)
243cd8a4   Goutte   Timestamp party c...
157
158
    started_at = started_at.format(API_TIME_FORMAT)
    stopped_at = stopped_at.format(API_TIME_FORMAT)
2c0e1515   Goutte   Refactor loading ...
159
160
161
    active_targets = [@targets[k] for k of @targets when @targets[k].active]
    active_targets.forEach((target) ~>
      console.info "Loading CSV data of #{target.name}…"
4900d232   Goutte   Add the time inte...
162
      targetButton = $(".targets-filters .target.#{target.slug}")
2c0e1515   Goutte   Refactor loading ...
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
      targetButton.addClass('loading')
      @loadData(target.slug, started_at, stopped_at).then(
        (data) ~>
          console.info "Loaded CSV data of #{target.name}."
          @createTimeSeries(target, data)
          @orbits.initOrbiter(target.slug, target.config, data['hci'])
          targetButton.removeClass('loading')
          @hideLoader()
        ,
        (error) -> console.error("Failed loading CSV data of #{target.name}.", error)
      )
    )

  clearPlots: ->
    @orbits.clear()
243cd8a4   Goutte   Timestamp party c...
178
    @timeSeries.forEach((ts) -> ts.clear())
2c0e1515   Goutte   Refactor loading ...
179
    @orbits = null
243cd8a4   Goutte   Timestamp party c...
180
    @timeSeries = []  # do we de-reference everything ? listeners ? #memleak?
2c0e1515   Goutte   Refactor loading ...
181
    this
ae0aa7d2   Goutte   Add an x axis lab...
182

b60e7acd   Goutte   Rename "source" i...
183
  createTimeSeries: (target, data) ->
b7fe650c   Goutte   Misc bundle of ol...
184
    @configuration['parameters'].forEach((parameter) ~>
4816cef4   Goutte   Refactor some more.
185
186
187
      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)
243cd8a4   Goutte   Timestamp party c...
188
      @timeSeries.push(new TimeSeries(
c3008fb2   Goutte   Clean up and refa...
189
        id, title, target, data[id], @parameters[id].active, container
6bb225d6   Goutte   Link the time ser...
190
      ))
4816cef4   Goutte   Refactor some more.
191
    )
243cd8a4   Goutte   Timestamp party c...
192
193
194
195
196
    @timeSeries.forEach((ts) ~>  # returning true may be faster
      ts.options['onMouseOver'] = ~>
        @timeSeries.forEach((ts2) -> ts2.showCursor()) ; true
      ts.options['onMouseOut'] = ~>
        @timeSeries.forEach((ts2) -> ts2.hideCursor()) ; true
fe3132dd   Goutte   Refactor even more.
197
      ts.options['onMouseMove'] = (t) ~>
243cd8a4   Goutte   Timestamp party c...
198
        @timeSeries.forEach((ts2) -> ts2.moveCursor(t))
9c0c4509   Goutte   Add a loader to t...
199
        @orbits?.moveToDate(t) ; true
c3008fb2   Goutte   Clean up and refa...
200
      ts.options['onBrushEnd'] = (sta, sto) ~>
243cd8a4   Goutte   Timestamp party c...
201
        @resizeDomain(moment(sta), moment(sto)) ; true
c3008fb2   Goutte   Clean up and refa...
202
      ts.options['onDblClick'] = ~>
9c0c4509   Goutte   Add a loader to t...
203
        @resetZoom() ; $("\#zoom_controls_help")?.remove() ; true
4816cef4   Goutte   Refactor some more.
204
    )
243cd8a4   Goutte   Timestamp party c...
205
    @timeSeries
4816cef4   Goutte   Refactor some more.
206

b7fe650c   Goutte   Misc bundle of ol...
207
208
  enableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
209
    @parameters[parameter_slug].active = true
243cd8a4   Goutte   Timestamp party c...
210
    @timeSeries.forEach((ts) ~> ts.show() if ts.parameter == parameter_slug && @targets[ts.target.slug].active)
b7fe650c   Goutte   Misc bundle of ol...
211
    this
4816cef4   Goutte   Refactor some more.
212

b7fe650c   Goutte   Misc bundle of ol...
213
214
  disableParameter: (parameter_slug) ->
    if parameter_slug not of @parameters then console.error("Unknown parameter #{parameter_slug}.")
b7fe650c   Goutte   Misc bundle of ol...
215
    @parameters[parameter_slug].active = false
243cd8a4   Goutte   Timestamp party c...
216
    @timeSeries.forEach((ts) -> ts.hide() if ts.parameter == parameter_slug)
b7fe650c   Goutte   Misc bundle of ol...
217
    this
ae0aa7d2   Goutte   Add an x axis lab...
218

a06a0a67   Goutte   Prepare the time ...
219
220
221
222
223
  getDomain: ->
    if @current_started_at? and @current_stopped_at?
      return [@current_started_at, @current_stopped_at]
    return [@started_at, @stopped_at]

8cb213b9   Goutte   Clean up, and pre...
224
225
  resizeDomain: (started_at, stopped_at) ->
    if stopped_at < started_at
7994cf1a   Goutte   Hunt bugs.
226
      [started_at, stopped_at] = [stopped_at, started_at]
8cb213b9   Goutte   Clean up, and pre...
227
    if started_at == stopped_at
6bb225d6   Goutte   Link the time ser...
228
      console.warn "Please provide distinct start and stop dates."
8cb213b9   Goutte   Clean up, and pre...
229
230
      return

243cd8a4   Goutte   Timestamp party c...
231
232
233
    @setStartAndStop(started_at, stopped_at)
    formatted_started_at = started_at.format()
    formatted_stopped_at = stopped_at.format()
a06a0a67   Goutte   Prepare the time ...
234

6bb225d6   Goutte   Link the time ser...
235
236
    if (@started_at <= started_at <= @stopped_at) and
       (@started_at <= stopped_at <= @stopped_at) then
243cd8a4   Goutte   Timestamp party c...
237
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} without fetching new data…"
6bb225d6   Goutte   Link the time ser...
238
      # We first resize the hidden time series and only afterwards we resize
a06a0a67   Goutte   Prepare the time ...
239
      # the visible ones, for a smoother transition.
243cd8a4   Goutte   Timestamp party c...
240
241
      @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...
242
      @orbits.resizeDomain started_at, stopped_at
243cd8a4   Goutte   Timestamp party c...
243
244
245
246
247
248
    else
      console.info "Resizing the temporal domain from #{formatted_started_at} to #{formatted_stopped_at} and fetching new data…"
      console.warn "This might take a while… Why not see what else we're up to on http://cdpp.eu while you're waiting?"
      # fetch new data and remake the plots
      @clearPlots()
      @loadAndCreatePlots(started_at, stopped_at)
2c0e1515   Goutte   Refactor loading ...
249

243cd8a4   Goutte   Timestamp party c...
250
    this
8cb213b9   Goutte   Clean up, and pre...
251

6bb225d6   Goutte   Link the time ser...
252
  resetZoom: ->
243cd8a4   Goutte   Timestamp party c...
253
    @timeSeries.forEach((ts) -> ts.resetZoom())
667eeb24   Goutte   Resize the domain...
254
    @orbits.resetZoom()
243cd8a4   Goutte   Timestamp party c...
255
256
257
258
    @setStartAndStop(@started_at, @stopped_at)
    this

  setStartAndStop: (started_at, stopped_at) ->
7994cf1a   Goutte   Hunt bugs.
259
    console.info "Setting time interval from #{started_at} to #{stopped_at}…"
243cd8a4   Goutte   Timestamp party c...
260
261
262
263
264
    @current_started_at = started_at
    @current_stopped_at = stopped_at
    $("\#started_at").val(started_at.format(INPUT_TIME_FORMAT))
    $("\#stopped_at").val(stopped_at.format(INPUT_TIME_FORMAT))
    this
8cb213b9   Goutte   Clean up, and pre...
265
266


ae0aa7d2   Goutte   Add an x axis lab...
267

ae0aa7d2   Goutte   Add an x axis lab...
268
269
270
###############################################################################
###############################################################################

b60e7acd   Goutte   Rename "source" i...
271

f1f1e797   Goutte   Rewrite ugly java...
272
273
274
275
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

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

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

6bb225d6   Goutte   Link the time ser...
286
  setData: (data) ->
c3008fb2   Goutte   Clean up and refa...
287
    @data = data  # and pre-compute extents for performance when zooming
6bb225d6   Goutte   Link the time ser...
288
289
290
    @xDataExtent = d3.extent(@data, (d) -> d.x)
    @yDataExtent = d3.extent(@data, (d) -> d.y)

f1f1e797   Goutte   Rewrite ugly java...
291
  init: ->
c3008fb2   Goutte   Clean up and refa...
292
    console.info "Initializing plot of #{@}…"
f1f1e797   Goutte   Rewrite ugly java...
293
294
295
296
297

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

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

f1f1e797   Goutte   Rewrite ugly java...
304
    @xAxis = d3.axisBottom()
08569a6b   Goutte   Add a zooming bru...
305
#               .tickFormat(d3.timeFormat("%Y-%m-%d"))
d49a163c   Goutte   Fix the resize an...
306
               .ticks(7)
f1f1e797   Goutte   Rewrite ugly java...
307
308
309
310
311
312
313
314
    @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...
315
    @svg.attr("class", "#{@parameter} #{@target.slug}")
2463bd16   Goutte   Add a circle foll...
316

f1f1e797   Goutte   Rewrite ugly java...
317
    @plotWrapper = @svg.append('g')
2038c9fb   Goutte   Add a zoom reset ...
318
319
    @plotWrapper.attr('transform',
                      'translate(' + @margin.left + ',' + @margin.top + ')')
f1f1e797   Goutte   Rewrite ugly java...
320

123313cb   Goutte   Clip the paths of...
321
322
323
324
325
326
327
328
329
330
    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...
331
332
333
                        .datum(@data)
                        .classed('line', true)

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

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

f1f1e797   Goutte   Rewrite ugly java...
341
342
343
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
344
345
346
347
                             .attr("transform", "rotate(-90)")
                             .attr("dy", "1em")
                             .style("text-anchor", "middle")
                             .text(@title)
b60e7acd   Goutte   Rename "source" i...
348
    @yAxisTextTarget = @plotWrapper.append("text")
123313cb   Goutte   Clip the paths of...
349
350
351
352
353
                                   .attr("transform", "rotate(-90)")
                                   .attr("dy", "1em")
                                   .style("text-anchor", "middle")
                                   .style("font-style", "oblique")
                                   .text(@target.name)
f1f1e797   Goutte   Rewrite ugly java...
354

81c9b2e8   Goutte   Add the values to...
355
356
357
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
361
    dx = 8
81c9b2e8   Goutte   Add the values to...
362
    @cursorValueShadow = @focus.append("text")
123313cb   Goutte   Clip the paths of...
363
364
365
                               .attr("class", "cursor-text cursor-text-shadow")
                               .attr("dx", dx)
                               .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
366

81c9b2e8   Goutte   Add the values to...
367
    @cursorValue = @focus.append("text")
123313cb   Goutte   Clip the paths of...
368
369
370
                         .attr("class", "cursor-text cursor-value")
                         .attr("dx", dx)
                         .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
371

81c9b2e8   Goutte   Add the values to...
372
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
373
374
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
375
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
376

81c9b2e8   Goutte   Add the values to...
377
378
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
379
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
380
381
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
382
383
    @resize()

123313cb   Goutte   Clip the paths of...
384
  RATIO = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO
f1f1e797   Goutte   Rewrite ugly java...
385
  resize: ->
123313cb   Goutte   Clip the paths of...
386
387
    width  = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(RATIO * width)
541e2936   Goutte   Synchronize the t...
388
389
390

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

123313cb   Goutte   Clip the paths of...
392
    console.debug("Resizing #{@}: #{width} x #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
393
394
395
396
397
398
399

    @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...
400
401
402
    @clip.attr("width", width)
         .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
403
    @path.attr('d', @line)
f1f1e797   Goutte   Rewrite ugly java...
404
405
406
407

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

2463bd16   Goutte   Add a circle foll...
408
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
d49a163c   Goutte   Fix the resize an...
409
410
    @xAxis.ticks(Math.floor(width / 90.0))  # not working as expected
    @yAxis.ticks(Math.floor(height / 18.0))
f1f1e797   Goutte   Rewrite ugly java...
411
412
413
414
415
416
417
418

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

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

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

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

2463bd16   Goutte   Add a circle foll...
425
426
427
    @mouseCanvas.attr("width", width)
                .attr("height", height)

08569a6b   Goutte   Add a zooming bru...
428
    if not @brushFunction?
c3008fb2   Goutte   Clean up and refa...
429
      console.debug "Creating the zooming brush for #{@}…"
08569a6b   Goutte   Add a zooming bru...
430
431
432
433
434
435
436
437
438
439
      # 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 ...
440
441
      # 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...
442
      @svg.select(".brush .overlay")
c3008fb2   Goutte   Clean up and refa...
443
444
445
446
          .on("mouseover.swapp", @onMouseOver)
          .on("mouseout.swapp",  @onMouseOut)
          .on("mousemove.swapp", @onMouseMove)
          .on("dblclick.swapp",  @onDoubleClick)
b7fe650c   Goutte   Misc bundle of ol...
447

6bb225d6   Goutte   Link the time ser...
448
    unless @visible then @hide()
f1f1e797   Goutte   Rewrite ugly java...
449
450
    this

2c0e1515   Goutte   Refactor loading ...
451
452
453
454
  clear: ->
    $(@svg.node()).remove()
    @visible = false

6bb225d6   Goutte   Link the time ser...
455
456
457
458
459
460
461
  show: ->
    $(@svg.node()).show()
    @visible = true

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

541e2936   Goutte   Synchronize the t...
463
464
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
c3008fb2   Goutte   Clean up and refa...
465
    if @options.onMouseMove? then @options.onMouseMove(x) else @moveCursor(x)
541e2936   Goutte   Synchronize the t...
466
467

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

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

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

08569a6b   Goutte   Add a zooming bru...
476
477
  onBrushEnd: ~>
    s = d3.event.selection
08569a6b   Goutte   Add a zooming bru...
478
479
    if s
      minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
08569a6b   Goutte   Add a zooming bru...
480
      @brush.call(@brushFunction.move, null)  # some voodoo to hide the brush
c3008fb2   Goutte   Clean up and refa...
481
482
      if @options.onBrushEnd? then @options.onBrushEnd(minmax[0], minmax[1])
                              else @zoomIn(minmax[0], minmax[1])
2038c9fb   Goutte   Add a zoom reset ...
483
484
485

  zoomIn: (startDate, stopDate) ->
    console.debug "Zooming in #{@} from #{startDate} to #{stopDate}."
6bb225d6   Goutte   Link the time ser...
486
    [minDate, maxDate] = @xDataExtent
2038c9fb   Goutte   Add a zoom reset ...
487
488
489
490
491
492
    if startDate < minDate then startDate = minDate
    if stopDate > maxDate then stopDate = maxDate
    @xScale.domain([startDate, stopDate])
    @applyZoom()

  resetZoom: ->
6bb225d6   Goutte   Link the time ser...
493
494
    @xScale.domain(@xDataExtent)
    @yScale.domain(@yDataExtent)
2038c9fb   Goutte   Add a zoom reset ...
495
496
497
    @applyZoom()

  applyZoom: ->
6bb225d6   Goutte   Link the time ser...
498
    if @visible
c3008fb2   Goutte   Clean up and refa...
499
      console.debug("Applying zoom to visible #{@}…")
6bb225d6   Goutte   Link the time ser...
500
501
502
503
504
      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...
505
      console.debug("Applying zoom to hidden #{@}…")
6bb225d6   Goutte   Link the time ser...
506
507
508
      @svg.select('.x.axis').call(@xAxis);
      @svg.select('.y.axis').call(@yAxis);
      @path.attr('d', @line)
08569a6b   Goutte   Add a zooming bru...
509

541e2936   Goutte   Synchronize the t...
510
511
512
513
514
515
  showCursor: ->
    @focus.style("display", null)

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

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

541e2936   Goutte   Synchronize the t...
519
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
520
521
522
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
523
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
524
525
526
527
    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...
528
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
541e2936   Goutte   Synchronize the t...
529

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

8cb213b9   Goutte   Clean up, and pre...
533
    transform = "translate(#{xx}, #{yy})"
81c9b2e8   Goutte   Add the values to...
534
535
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
536
537
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
538
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
539
540
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
541
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
542
543
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
544
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
545
546
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
547
548

    this
f1f1e797   Goutte   Rewrite ugly java...
549

ae0aa7d2   Goutte   Add an x axis lab...
550
551
552
553

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

f1f1e797   Goutte   Rewrite ugly java...
554
export class Orbits
8cb213b9   Goutte   Clean up, and pre...
555
556
557
558
  """
  View of the solar system from above, with orbits segments for selected time
  interval, from real data.
  """
f1f1e797   Goutte   Rewrite ugly java...
559

a21f81d9   Goutte   Enable Venus and ...
560
  (@container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
561
562
563
    @init()

  init: ->
c3008fb2   Goutte   Clean up and refa...
564
    console.log "Initializing plot of orbits…"
f1f1e797   Goutte   Rewrite ugly java...
565
566
567
568

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
569
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
570
571
      left: 60
    }
f1f1e797   Goutte   Rewrite ugly java...
572

a21f81d9   Goutte   Enable Venus and ...
573
574
    @data = {}  # slug => HCI array
    @orbiters = {}  # slug => config
7994cf1a   Goutte   Hunt bugs.
575
    @orbitersElements = {}
a21f81d9   Goutte   Enable Venus and ...
576
    @extremum = 1
f1f1e797   Goutte   Rewrite ugly java...
577
578
579
580
581
582
    @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...
583
584
585
586
587
    @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...
588
589
590
591
592
593
594
595
596
    @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 ...
597
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
598
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
599

11662eed   Goutte   Add Y axis label ...
600
601
602
603
604
605
606
    @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...
607
608
609
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
610
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
611

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

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

a21f81d9   Goutte   Enable Venus and ...
619
620
621
622
623
624
    @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...
625
626
627
628
629
630
631
632
633
634
635
636
    # 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 ...
637
                                .datum(data)
438929a4   Goutte   Rewrite the orbit...
638
639
                                .classed('orbit orbit_section', true)

a21f81d9   Goutte   Enable Venus and ...
640
641
    @orbiters[slug] = config
    @data[slug] = data
438929a4   Goutte   Rewrite the orbit...
642
643
644
645
646
647
    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

a21f81d9   Goutte   Enable Venus and ...
648
649
    @resize()

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

438929a4   Goutte   Rewrite the orbit...
652
653
    this

2c0e1515   Goutte   Refactor loading ...
654
655
656
  clear: ->
    $(@svg.node()).remove()

f1f1e797   Goutte   Rewrite ugly java...
657
  resize: ->
abcf4c94   Goutte   Clean up.
658
659
    width = Math.ceil($(@container).width() - @margin.left - @margin.right)
    height = Math.ceil(1.0 * width)
f1f1e797   Goutte   Rewrite ugly java...
660

abcf4c94   Goutte   Clean up.
661
    console.debug("Resizing orbits : #{width} × #{height}…")
f1f1e797   Goutte   Rewrite ugly java...
662
663
664
665
666
667
668

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

438929a4   Goutte   Rewrite the orbit...
671
    for slug, config of @orbiters
abcf4c94   Goutte   Clean up.
672
      @resizeOrbiter(slug, config, width, height)
438929a4   Goutte   Rewrite the orbit...
673

f1f1e797   Goutte   Rewrite ugly java...
674
675
676
677
678
679
680
681
682
683
    @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...
684
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
685
686
687
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
688

f1f1e797   Goutte   Rewrite ugly java...
689
690
    this

abcf4c94   Goutte   Clean up.
691
692
  resizeOrbiter: (slug, config, width, height) ->
    console.debug("Resizing orbit of #{slug}…")
438929a4   Goutte   Rewrite the orbit...
693
694
695
696
697
698
699
700
701
702
703
704
705

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

a21f81d9   Goutte   Enable Venus and ...
709
    data = @data[slug]
438929a4   Goutte   Rewrite the orbit...
710
711
712
713
714
715

    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...
716
  repositionOrbiter: (slug, datum) ->
a21f81d9   Goutte   Enable Venus and ...
717
    data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
718
719
720
721
    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...
722
723
724
725
726
    this

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

  moveToDate: (t) ->
abcf4c94   Goutte   Clean up.
727
    console.warn("Trying to move to an undefined date !") unless t
ae0aa7d2   Goutte   Add an x axis lab...
728
    for slug, el of @orbitersElements
a21f81d9   Goutte   Enable Venus and ...
729
      data = @data[slug]
ae0aa7d2   Goutte   Add an x axis lab...
730
731
732
733
734
      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.
735
      @repositionOrbiter(slug, d)
a21f81d9   Goutte   Enable Venus and ...
736
    this
ae0aa7d2   Goutte   Add an x axis lab...
737

8cb213b9   Goutte   Clean up, and pre...
738
  resizeDomain: (started_at, stopped_at) ->
667eeb24   Goutte   Resize the domain...
739
740
741
742
743
    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...
744

667eeb24   Goutte   Resize the domain...
745
746
747
748
749
  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.