Commit 08569a6b50caa694b921c4f44cc303900dda965e

Authored by Goutte
1 parent 908b4423

Add a zooming brush on each time series.

Implement changes suggested by Vincent Génot.
@@ -23,15 +23,12 @@ authors: @@ -23,15 +23,12 @@ authors:
23 role: Software Ninja 23 role: Software Ninja
24 mail: antoine.goutenoir@irap.omp.eu 24 mail: antoine.goutenoir@irap.omp.eu
25 - name: Myriam Bouchemit 25 - name: Myriam Bouchemit
26 - role: Server Maintenance 26 + role: Code Reviewer
27 mail: myriam.bouchemit@irap.omp.eu 27 mail: myriam.bouchemit@irap.omp.eu
28 - - name: Mikel Indurain  
29 - role: Simulation Software  
30 - mail: mindurain@irap.omp.eu  
31 - name: Nicolas André 28 - name: Nicolas André
32 role: Project Lead 29 role: Project Lead
33 mail: nicolas.andre@irap.omp.eu 30 mail: nicolas.andre@irap.omp.eu
34 - - name: Vincent Genot 31 + - name: Vincent Génot
35 role: Project Coordinator 32 role: Project Coordinator
36 mail: vincent.genot@irap.omp.eu 33 mail: vincent.genot@irap.omp.eu
37 34
web/static/js/swapp.js
@@ -243,6 +243,7 @@ @@ -243,6 +243,7 @@
243 this.options = options != null 243 this.options = options != null
244 ? options 244 ? options
245 : {}; 245 : {};
  246 + this.onBrushEnd = bind$(this, 'onBrushEnd', prototype);
246 this.onMouseOut = bind$(this, 'onMouseOut', prototype); 247 this.onMouseOut = bind$(this, 'onMouseOut', prototype);
247 this.onMouseOver = bind$(this, 'onMouseOver', prototype); 248 this.onMouseOver = bind$(this, 'onMouseOver', prototype);
248 this.onMouseMove = bind$(this, 'onMouseMove', prototype); 249 this.onMouseMove = bind$(this, 'onMouseMove', prototype);
@@ -263,7 +264,7 @@ @@ -263,7 +264,7 @@
263 this.yScale = d3.scaleLinear().domain(d3.extent(this.data, function(d){ 264 this.yScale = d3.scaleLinear().domain(d3.extent(this.data, function(d){
264 return d.y; 265 return d.y;
265 })); 266 }));
266 - this.xAxis = d3.axisBottom().tickFormat(d3.timeFormat("%Y-%m-%d")).ticks(7); 267 + this.xAxis = d3.axisBottom().ticks(7);
267 this.yAxis = d3.axisLeft().ticks(10); 268 this.yAxis = d3.axisLeft().ticks(10);
268 this.line = d3.line().x(function(d){ 269 this.line = d3.line().x(function(d){
269 return this$.xScale(d.x); 270 return this$.xScale(d.x);
@@ -275,8 +276,8 @@ @@ -275,8 +276,8 @@
275 this.plotWrapper = this.svg.append('g'); 276 this.plotWrapper = this.svg.append('g');
276 this.plotWrapper.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); 277 this.plotWrapper.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
277 this.path = this.plotWrapper.append('path').datum(this.data).classed('line', true); 278 this.path = this.plotWrapper.append('path').datum(this.data).classed('line', true);
278 - this.mouseCanvas = this.plotWrapper.append("rect").style("fill", "none").style("pointer-events", "all");  
279 - this.mouseCanvas.on("mouseover", this.onMouseOver).on("mouseout", this.onMouseOut).on("mousemove", this.onMouseMove); 279 + this.brush = this.plotWrapper.append("g").attr("class", "brush");
  280 + this.mouseCanvas = this.plotWrapper.append("rect").style("fill", "none");
280 this.plotWrapper.append('g').classed('x axis', true); 281 this.plotWrapper.append('g').classed('x axis', true);
281 this.plotWrapper.append('g').classed('y axis', true); 282 this.plotWrapper.append('g').classed('y axis', true);
282 this.yAxisText = this.plotWrapper.append("text").attr("transform", "rotate(-90)").attr("dy", "1em").style("text-anchor", "middle").text(this.title); 283 this.yAxisText = this.plotWrapper.append("text").attr("transform", "rotate(-90)").attr("dy", "1em").style("text-anchor", "middle").text(this.title);
@@ -296,7 +297,6 @@ @@ -296,7 +297,6 @@
296 height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width; 297 height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width;
297 this.plotWidth = width; 298 this.plotWidth = width;
298 this.plotHeight = height; 299 this.plotHeight = height;
299 - console.log("Resizing time series " + this.title + " of " + this.target.name + ": " + width + " x " + height);  
300 this.xScale.range([0, width]); 300 this.xScale.range([0, width]);
301 this.yScale.range([height, 0]); 301 this.yScale.range([height, 0]);
302 this.svg.attr('width', width + this.margin.right + this.margin.left).attr('height', height + this.margin.top + this.margin.bottom); 302 this.svg.attr('width', width + this.margin.right + this.margin.left).attr('height', height + this.margin.top + this.margin.bottom);
@@ -310,6 +310,12 @@ @@ -310,6 +310,12 @@
310 this.yAxisText.attr("y", 20 - this.margin.left).attr("x", 0 - height / 2); 310 this.yAxisText.attr("y", 20 - this.margin.left).attr("x", 0 - height / 2);
311 this.yAxisTextTarget.attr("y", 0 - this.margin.left).attr("x", 0 - height / 2); 311 this.yAxisTextTarget.attr("y", 0 - this.margin.left).attr("x", 0 - height / 2);
312 this.mouseCanvas.attr("width", width).attr("height", height); 312 this.mouseCanvas.attr("width", width).attr("height", height);
  313 + if (this.brushFunction == null) {
  314 + console.log("Creating the zooming brush for time series " + this.title + " of " + this.target.name + "...");
  315 + this.brushFunction = d3.brushX().extent([[0, 0], [width, height]]).handleSize(0).on("end", this.onBrushEnd);
  316 + this.brush.call(this.brushFunction);
  317 + this.svg.select(".brush .overlay").on("mouseover", this.onMouseOver).on("mouseout", this.onMouseOut).on("mousemove", this.onMouseMove);
  318 + }
313 if (!this.active) { 319 if (!this.active) {
314 $(this.svg.node()).hide(); 320 $(this.svg.node()).hide();
315 } 321 }
@@ -320,10 +326,11 @@ @@ -320,10 +326,11 @@
320 var x; 326 var x;
321 x = this.xScale.invert(d3.mouse(this.mouseCanvas.node())[0]); 327 x = this.xScale.invert(d3.mouse(this.mouseCanvas.node())[0]);
322 if (this.options.onMouseMove != null) { 328 if (this.options.onMouseMove != null) {
323 - return this.options.onMouseMove(x); 329 + this.options.onMouseMove(x);
324 } else { 330 } else {
325 - return this.moveCursor(x); 331 + this.moveCursor(x);
326 } 332 }
  333 + return true;
327 }; 334 };
328 TimeSeries.prototype.onMouseOver = function(){ 335 TimeSeries.prototype.onMouseOver = function(){
329 if (this.options.onMouseOver != null) { 336 if (this.options.onMouseOver != null) {
@@ -339,6 +346,24 @@ @@ -339,6 +346,24 @@
339 return this.hideCursor(); 346 return this.hideCursor();
340 } 347 }
341 }; 348 };
  349 + TimeSeries.prototype.onBrushEnd = function(){
  350 + var s, minmax;
  351 + s = d3.event.selection;
  352 + console.log("on brush end", s);
  353 + if (s) {
  354 + minmax = [s[0], s[1]].map(this.xScale.invert, this.xScale);
  355 + console.info("Zooming in from " + minmax[0], this.xScale.domain([s[0], s[1]].map(this.xScale.invert, this.xScale)));
  356 + this.brush.call(this.brushFunction.move, null);
  357 + return this.zoomIn();
  358 + }
  359 + };
  360 + TimeSeries.prototype.zoomIn = function(){
  361 + var t;
  362 + t = this.svg.transition().duration(750);
  363 + this.svg.select('.x.axis').transition(t).call(this.xAxis);
  364 + this.svg.select('.y.axis').transition(t).call(this.yAxis);
  365 + return this.path.transition(t).attr('d', this.line);
  366 + };
342 TimeSeries.prototype.showCursor = function(){ 367 TimeSeries.prototype.showCursor = function(){
343 return this.focus.style("display", null); 368 return this.focus.style("display", null);
344 }; 369 };
web/static/js/swapp.ls
@@ -207,7 +207,7 @@ export class TimeSeries @@ -207,7 +207,7 @@ export class TimeSeries
207 #console.info("Y domain #{@title}", d3.extent(@data, (d) -> d.y)) 207 #console.info("Y domain #{@title}", d3.extent(@data, (d) -> d.y))
208 208
209 @xAxis = d3.axisBottom() 209 @xAxis = d3.axisBottom()
210 - .tickFormat(d3.timeFormat("%Y-%m-%d")) 210 +# .tickFormat(d3.timeFormat("%Y-%m-%d"))
211 .ticks(7) 211 .ticks(7)
212 @yAxis = d3.axisLeft() 212 @yAxis = d3.axisLeft()
213 .ticks(10) 213 .ticks(10)
@@ -226,17 +226,16 @@ export class TimeSeries @@ -226,17 +226,16 @@ export class TimeSeries
226 .datum(@data) 226 .datum(@data)
227 .classed('line', true) 227 .classed('line', true)
228 228
  229 + @brush = @plotWrapper.append("g")
  230 + .attr("class", "brush")
  231 +
229 @mouseCanvas = @plotWrapper.append("rect") 232 @mouseCanvas = @plotWrapper.append("rect")
230 .style("fill", "none") 233 .style("fill", "none")
231 - .style("pointer-events", "all")  
232 - @mouseCanvas  
233 - .on("mouseover", @onMouseOver)  
234 - .on("mouseout", @onMouseOut)  
235 - .on("mousemove", @onMouseMove)  
236 -  
237 -# @plotWrapper.append("g")  
238 -# .attr("class", "brush")  
239 -# .call(d3.brush().on("brush", @onBrushEnd)); 234 +# .style("pointer-events", "all")
  235 +# @mouseCanvas
  236 +# .on("mouseover", @onMouseOver)
  237 +# .on("mouseout", @onMouseOut)
  238 +# .on("mousemove", @onMouseMove)
240 239
241 @plotWrapper.append('g').classed('x axis', true) 240 @plotWrapper.append('g').classed('x axis', true)
242 @plotWrapper.append('g').classed('y axis', true) 241 @plotWrapper.append('g').classed('y axis', true)
@@ -289,7 +288,7 @@ export class TimeSeries @@ -289,7 +288,7 @@ export class TimeSeries
289 @plotWidth = width 288 @plotWidth = width
290 @plotHeight = height 289 @plotHeight = height
291 290
292 - console.log("Resizing time series #{@title} of #{@target.name}: #{width} x #{height}") 291 + #console.log("Resizing time series #{@title} of #{@target.name}: #{width} x #{height}")
293 292
294 @xScale.range([0, width]); 293 @xScale.range([0, width]);
295 @yScale.range([height, 0]); 294 @yScale.range([height, 0]);
@@ -297,7 +296,7 @@ export class TimeSeries @@ -297,7 +296,7 @@ export class TimeSeries
297 @svg.attr('width', width + @margin.right + @margin.left) 296 @svg.attr('width', width + @margin.right + @margin.left)
298 .attr('height', height + @margin.top + @margin.bottom) 297 .attr('height', height + @margin.top + @margin.bottom)
299 298
300 - @path.attr('d', @line) # wip, do we need to put this in resize ? 299 + @path.attr('d', @line)
301 300
302 @xAxis.scale(@xScale) 301 @xAxis.scale(@xScale)
303 @yAxis.scale(@yScale) 302 @yAxis.scale(@yScale)
@@ -322,6 +321,26 @@ export class TimeSeries @@ -322,6 +321,26 @@ export class TimeSeries
322 @mouseCanvas.attr("width", width) 321 @mouseCanvas.attr("width", width)
323 .attr("height", height) 322 .attr("height", height)
324 323
  324 + if not @brushFunction?
  325 + console.log "Creating the zooming brush for time series #{@title} of #{@target.name}..."
  326 + # looks like d3.brush handles its own resizing on window.resize
  327 + @brushFunction =
  328 + d3.brushX()
  329 + .extent([[0, 0], [width, height]])
  330 + .handleSize(0)
  331 + .on("end", @onBrushEnd)
  332 +# .on("start", @onBrushStart)
  333 +# .on("move", @onBrushMove)
  334 + @brush.call(@brushFunction)
  335 +
  336 +# @mouseCanvas = @brush.append("rect")
  337 +# .style("fill", "none")
  338 +# .style("pointer-events", "all")
  339 + @svg.select(".brush .overlay")
  340 + .on("mouseover", @onMouseOver)
  341 + .on("mouseout", @onMouseOut)
  342 + .on("mousemove", @onMouseMove)
  343 +
325 344
326 unless @active then $(@svg.node()).hide() 345 unless @active then $(@svg.node()).hide()
327 this 346 this
@@ -336,6 +355,7 @@ export class TimeSeries @@ -336,6 +355,7 @@ export class TimeSeries
336 @options.onMouseMove(x) 355 @options.onMouseMove(x)
337 else 356 else
338 @moveCursor(x) 357 @moveCursor(x)
  358 + true # fixme: figure out what to return
339 359
340 onMouseOver: ~> 360 onMouseOver: ~>
341 if @options.onMouseOver? 361 if @options.onMouseOver?
@@ -349,17 +369,25 @@ export class TimeSeries @@ -349,17 +369,25 @@ export class TimeSeries
349 else 369 else
350 @hideCursor() 370 @hideCursor()
351 371
352 -# onBrushEnd: ~>  
353 -# s = d3.event.selection  
354 -# console.info("on brush end", s) 372 + onBrushEnd: ~>
  373 + s = d3.event.selection
  374 + console.log("on brush end", s)
355 # if not s 375 # if not s
356 # @xScale.domain(d3.extent(@data, (d) -> d.x)).nice() 376 # @xScale.domain(d3.extent(@data, (d) -> d.x)).nice()
357 # @yScale.domain(d3.extent(@data, (d) -> d.y)).nice() 377 # @yScale.domain(d3.extent(@data, (d) -> d.y)).nice()
358 -# else  
359 -# console.info "brush end", s  
360 -# @xScale.domain([s[0][0], s[1][0]].map(x.invert, x))  
361 -# @yScale.domain([s[1][1], s[0][1]].map(y.invert, y))  
362 -# scatter.select(".brush").call(brush.move, null) 378 + if s
  379 + minmax = [s[0], s[1]].map(@xScale.invert, @xScale)
  380 + console.info "Zooming in from #{minmax[0]}",
  381 + @xScale.domain([s[0], s[1]].map(@xScale.invert, @xScale))
  382 + @brush.call(@brushFunction.move, null) # some voodoo to hide the brush
  383 + @zoomIn()
  384 +
  385 + zoomIn: ->
  386 + t = @svg.transition().duration(750);
  387 + @svg.select('.x.axis').transition(t).call(@xAxis);
  388 + @svg.select('.y.axis').transition(t).call(@yAxis);
  389 + @path.transition(t).attr('d', @line)
  390 +
363 391
364 showCursor: -> 392 showCursor: ->
365 @focus.style("display", null) 393 @focus.style("display", null)
web/view/home.html.jinja2
@@ -51,7 +51,7 @@ @@ -51,7 +51,7 @@
51 <a class="mdl-navigation__link parameter" data-ts-slug="vlen" href="#">Velocity</a> 51 <a class="mdl-navigation__link parameter" data-ts-slug="vlen" href="#">Velocity</a>
52 <a class="mdl-navigation__link parameter" data-ts-slug="temp" href="#">Temperature</a> 52 <a class="mdl-navigation__link parameter" data-ts-slug="temp" href="#">Temperature</a>
53 <a class="mdl-navigation__link parameter" data-ts-slug="dens" href="#">Density</a> 53 <a class="mdl-navigation__link parameter" data-ts-slug="dens" href="#">Density</a>
54 - <a class="mdl-navigation__link parameter" data-ts-slug="angl" href="#">Angle Source-Sun-Earth</a> 54 + <a class="mdl-navigation__link parameter" data-ts-slug="angl" href="#">Angle Target-Sun-Earth</a>
55 </nav> 55 </nav>
56 56
57 <div class="mdl-layout-spacer"></div> 57 <div class="mdl-layout-spacer"></div>
@@ -98,8 +98,9 @@ @@ -98,8 +98,9 @@
98 svg text { 98 svg text {
99 {# fill: #f4f4f4;#} 99 {# fill: #f4f4f4;#}
100 } 100 }
101 - svg .brush {  
102 - border: 1px solid black; 101 + #time_series svg .brush .selection {
  102 + fill: #efa02c;
  103 + fill-opacity: 0.382;
103 } 104 }
104 path.line { 105 path.line {
105 fill: none; 106 fill: none;
@@ -241,7 +242,7 @@ var configuration = { @@ -241,7 +242,7 @@ var configuration = {
241 }, 242 },
242 { 243 {
243 id: 'angl', 244 id: 'angl',
244 - title: "Angle S-S-E (deg)", 245 + title: "Angle T-S-E (deg)",
245 active: false, 246 active: false,
246 unit: "deg" 247 unit: "deg"
247 } 248 }