Blame view

web/static/js/swapp.ls 12.9 KB
f1f1e797   Goutte   Rewrite ugly java...
1
2
3
4
5
# Livescript transpiles to javascript, and is easier on the eyes and brain.
# http://livescript.net

const GOLDEN_RATIO = 2 / (1 + Math.sqrt(5))

ae0aa7d2   Goutte   Add an x axis lab...
6
7
8
9
###############################################################################

export class SpaceWeather

f75faf5f   Goutte   WIP
10
  (@configuration) ->
ae0aa7d2   Goutte   Add an x axis lab...
11
    console.info "Creating Space Weather app...", configuration
f75faf5f   Goutte   WIP
12
    @sources = {}
ae0aa7d2   Goutte   Add an x axis lab...
13

f75faf5f   Goutte   WIP
14
15
16
17
  buildDataUrlForSource: (source_slug) ->
    url = @configuration['api']['data_for_interval']
    url = url.replace('<source>', source_slug)
    url
ae0aa7d2   Goutte   Add an x axis lab...
18

f75faf5f   Goutte   WIP
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
  addSource: (source) ->
    @sources[source.slug] = source
    this

  showAllSources: ->
    for slug, source of @sources
      source.show()
    this

  showSource: (source) ->
    source.show()
    this

  hideSource: (source) ->
    source.hide()
    this

  loadData: (source_slug, started_at, stopped_at) ->
    sw = this
    promise = new Promise((resolve, reject) ->
      url = sw.buildDataUrlForSource(source_slug)
      d3.csv(url+"?started_at=#{started_at}&stopped_at=#{stopped_at}", (csv) ->
        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...
59
60
61
62



class Source
f75faf5f   Goutte   WIP
63
  (@slug, @config) ->
ae0aa7d2   Goutte   Add an x axis lab...
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
    @time_series = {}

  addTimeSeries: (ts) ->
    @time_series[ts.slug] = ts

  show: ->
    for slug, ts of @time_series
      $(ts.svg).show()
  hide: ->
    for slug, ts of @time_series
      $(ts.svg).hide()




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

f1f1e797   Goutte   Rewrite ugly java...
82
83
84
85
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

6f9a3852   Goutte   Hide time series ...
86
  (@slug, @title, @data, @container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
87
88
    # title : string
    # data : list of {x: <datetime>, y: <float>}
ae0aa7d2   Goutte   Add an x axis lab...
89
    console.info "Creating time series '#{@title}'..."
f1f1e797   Goutte   Rewrite ugly java...
90
91
92
    @init()

  init: ->
ae0aa7d2   Goutte   Add an x axis lab...
93
    console.info "Initializing time series '#{@title}'...", @data, @options
f1f1e797   Goutte   Rewrite ugly java...
94
95
96
97
98
99
100
101
102
103
104

    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
      left: 60
    }

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

541e2936   Goutte   Synchronize the t...
105
    #console.info("Y domain #{@title}", d3.extent(@data, (d) -> d.y))
2463bd16   Goutte   Add a circle foll...
106

f1f1e797   Goutte   Rewrite ugly java...
107
108
109
110
111
112
113
114
115
116
117
    @xAxis = d3.axisBottom()
               .ticks(7, ",f")
               .tickFormat(d3.timeFormat("%Y-%m-%d"))
    @yAxis = d3.axisLeft()
               .ticks(10)

    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

    @svg = d3.select(@container).append('svg')
6f9a3852   Goutte   Hide time series ...
118
    @svg.attr("class", @slug)
f1f1e797   Goutte   Rewrite ugly java...
119

2463bd16   Goutte   Add a circle foll...
120

f1f1e797   Goutte   Rewrite ugly java...
121
122
123
    @plotWrapper = @svg.append('g')
    @plotWrapper.attr('transform', 'translate(' + @margin.left + ',' + @margin.top + ')')

f1f1e797   Goutte   Rewrite ugly java...
124
125
126
127
    @path = @plotWrapper.append('path')
                        .datum(@data)
                        .classed('line', true)

2463bd16   Goutte   Add a circle foll...
128
129
130
131
    @mouseCanvas = @plotWrapper.append("rect")
                               .style("fill", "none")
                               .style("pointer-events", "all")
    @mouseCanvas
541e2936   Goutte   Synchronize the t...
132
133
134
        .on("mouseover", @onMouseOver)
        .on("mouseout",  @onMouseOut)
        .on("mousemove", @onMouseMove)
2463bd16   Goutte   Add a circle foll...
135

f1f1e797   Goutte   Rewrite ugly java...
136
137
138
139
140
141
142
143
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
        .attr("transform", "rotate(-90)")
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text(@title)

81c9b2e8   Goutte   Add the values to...
144
145
146
147
148
149
    @focus = @plotWrapper.append('g').style("display", "none")

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

541e2936   Goutte   Synchronize the t...
150
    dx = 8
81c9b2e8   Goutte   Add the values to...
151
    @cursorValueShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
152
153
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
154
                              .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
155

81c9b2e8   Goutte   Add the values to...
156
157
    @cursorValue = @focus.append("text")
                        .attr("class", "cursor-text cursor-value")
541e2936   Goutte   Synchronize the t...
158
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
159
160
                        .attr("dy", "-.3em")

541e2936   Goutte   Synchronize the t...
161

81c9b2e8   Goutte   Add the values to...
162
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
163
164
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
165
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
166

81c9b2e8   Goutte   Add the values to...
167
168
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
169
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
170
171
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
172
173
174
175
    @resize()

  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
541e2936   Goutte   Synchronize the t...
176
177
178
179
    height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width

    @plotWidth = width
    @plotHeight = height
f1f1e797   Goutte   Rewrite ugly java...
180
181
182
183
184
185
186
187
188
189
190
191
192
193

    console.log("Resize time series #{@title} : #{width} x #{height}")

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

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

    @path.attr('d', @line) # wip, do we need to put this in resize ?

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

2463bd16   Goutte   Add a circle foll...
194
195
196
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
    @xAxis.ticks(Math.floor(width / 60))
    @yAxis.ticks(Math.floor(height / 18))
f1f1e797   Goutte   Rewrite ugly java...
197
198
199
200
201
202
203
204
205
206
207

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

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

    @yAxisText.attr("y", 0 - @margin.left)
              .attr("x", 0 - (height / 2))

2463bd16   Goutte   Add a circle foll...
208
209
210
    @mouseCanvas.attr("width", width)
                .attr("height", height)

f1f1e797   Goutte   Rewrite ugly java...
211
212
    this

541e2936   Goutte   Synchronize the t...
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
    if @options.onMouseMove?
      @options.onMouseMove(x)
    else
      @moveCursor(x)

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

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

  showCursor: ->
    @focus.style("display", null)

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

  bisectDate: d3.bisector((d) -> d.x).left # /!\ complex
81c9b2e8   Goutte   Add the values to...
239
  timeFormat: d3.timeFormat("%Y-%m-%d %Hh")
2463bd16   Goutte   Add a circle foll...
240

541e2936   Goutte   Synchronize the t...
241
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
242
243
244
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
245
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
246
247
248
249
    d = if x0 - d0.x > d1.x - x0 then d1 else d0
    xx = @xScale(d.x)
    yy = @yScale(d.y)

81c9b2e8   Goutte   Add the values to...
250
251
    transform = "translate(#{xx}, #{yy})"

541e2936   Goutte   Synchronize the t...
252
253
254
255
256
257
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
#    console.log("xx", xx)

    dx = 8
    dx = -1 * dx if mirrored

81c9b2e8   Goutte   Add the values to...
258
259
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
260
261
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
262
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
263
264
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
265
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
266
267
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
268
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
269
270
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
271
272

    this
f1f1e797   Goutte   Rewrite ugly java...
273

ae0aa7d2   Goutte   Add an x axis lab...
274
275
276
277

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

f1f1e797   Goutte   Rewrite ugly java...
278
export class Orbits
ae0aa7d2   Goutte   Add an x axis lab...
279
280
  # View of the solar system from above, with orbits segments for selected time
  # interval, from real data.
f1f1e797   Goutte   Rewrite ugly java...
281

438929a4   Goutte   Rewrite the orbit...
282
  (@orbiters, @data, @container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
283
284
285
286
287
288
289
290
291
    console.log "Create orbits"
    @init()

  init: ->
    console.log "Initialize orbits", @data, @options

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
292
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
293
294
295
296
297
298
299
300
301
302
303
304
305
      left: 60
    }
    
    @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])

    @xAxis = d3.axisBottom().ticks(10)
    @yAxis = d3.axisLeft().ticks(10)

f1f1e797   Goutte   Rewrite ugly java...
306
307
308
309
310
    @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...
311
312
313
314
315
316
317
318
319
    @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 ...
320
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
321
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
322

11662eed   Goutte   Add Y axis label ...
323
324
325
326
327
328
329
    @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)')

f1f1e797   Goutte   Rewrite ugly java...
330
331
332
333
    @sun = @plotWrapper.append("svg:circle")
    @sun.append('svg:title').text("Sol")
    @sun.attr("r", 17).style("fill", "yellow")

438929a4   Goutte   Rewrite the orbit...
334
335
336
    for slug, config of @orbiters
      @initOrbiter(slug, config)

f1f1e797   Goutte   Rewrite ugly java...
337
338
    @resize()

438929a4   Goutte   Rewrite the orbit...
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
  orbitersElements: {}
  initOrbiter: (slug, config) ->
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

    # 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')
                                .datum(@data)  # fixme
                                .classed('orbit orbit_section', true)

    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

    this

f1f1e797   Goutte   Rewrite ugly java...
366
367
368
369
370
371
372
373
374
375
376
377
378
379
  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

    console.log("Resize orbits : #{width} x #{height}")

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

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

    @sun.attr("cx", width / 2).attr("cy", height / 2)

438929a4   Goutte   Rewrite the orbit...
380
381
382
    for slug, config of @orbiters
      @resizeOrbiter(slug, config)

f1f1e797   Goutte   Rewrite ugly java...
383
384
385
386
387
388
389
390
391
392
    @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...
393
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
394
395
396
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
397

f1f1e797   Goutte   Rewrite ugly java...
398
399
    this

438929a4   Goutte   Rewrite the orbit...
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
  resizeOrbiter: (slug, config) ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

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

    el = @orbitersElements[slug]
    el['orbit_section'].attr('d', el['orbit_line'])

    a = config['orbit']['a']
    b = config['orbit']['b']
    c = Math.sqrt(a*a - b*b)
    cx = (width / 2) - c
    cy = (height / 2)
    @yScale.range([0, height])
    el['orbit_ellipse'].attr('cx', cx).attr('cy', cy)
        .attr('rx', @xScale(a) - @xScale(0))
        .attr('ry', @yScale(b) - @yScale(0))
        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
    @yScale.range([height, 0])

ae0aa7d2   Goutte   Add an x axis lab...
421
    data = @data  # todo: multiple orbiters
438929a4   Goutte   Rewrite the orbit...
422
423
424
425
426
427

    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...
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
  repositionOrbiter: (slug, datum) ->
    data = @data  # todo
    datum ?= data[data.length - 1]
    el = @orbitersElements[slug]
    el['orbiter'].attr('x', @xScale(datum.x) - 16)
    el['orbiter'].attr('y', @yScale(datum.y) - 16)

    this

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

  moveToDate: (t) ->
    for slug, el of @orbitersElements
      data = @data  # todo: multiple orbiters
      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
      @repositionOrbiter(slug, d)  # fixme