Commit 289db173f905e370cf9396476fdcd0d473048c33

Authored by Antoine Goutenoir
1 parent e0905f97
Exists in master

feat: add the distance circle under the pointer

Co-authored by: @Adrenesis
flaskr/static/js/plots/emissions-equidistant-map.js
1 // jQuery-free 1 // jQuery-free
2 function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissionsDataUrl) { 2 function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissionsDataUrl) {
  3 + const EARTH_RADIUS = 6371000; // meters
  4 +
3 let margin = {top: 48, right: 88, bottom: 68, left: 98}, 5 let margin = {top: 48, right: 88, bottom: 68, left: 98},
4 width = 960 - margin.left - margin.right, 6 width = 960 - margin.left - margin.right,
5 height = 540 - margin.top - margin.bottom; 7 height = 540 - margin.top - margin.bottom;
@@ -13,6 +15,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -13,6 +15,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
13 15
14 let svg = null; 16 let svg = null;
15 let cartaContainer = null; 17 let cartaContainer = null;
  18 + let attendeesLayer = null;
16 19
17 let geoPath = d3.geoPath(); 20 let geoPath = d3.geoPath();
18 let mapProjection = null; 21 let mapProjection = null;
@@ -147,8 +150,8 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -147,8 +150,8 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
147 // }; 150 // };
148 151
149 152
150 - const drawCircle = function (x, y, radius, color, className = "circle") {  
151 - svg.append("circle") 153 + const drawCircle = function (into, x, y, radius, color, className = "circle") {
  154 + into.append("circle")
152 .attr("class", className) 155 .attr("class", className)
153 .attr("cx", x) 156 .attr("cx", x)
154 .attr("cy", y) 157 .attr("cy", y)
@@ -181,7 +184,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -181,7 +184,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
181 let color = "rgba(" + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + 184 let color = "rgba(" + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) +
182 ", " + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + 185 ", " + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) +
183 ", 240, 0.7)"; 186 ", 240, 0.7)";
184 - drawCircle(x, y, radius, color); 187 + drawCircle(svg, x, y, radius, color);
185 for (let i = 1; i < legendAmount; i++) { 188 for (let i = 1; i < legendAmount; i++) {
186 svg.append("text") 189 svg.append("text")
187 .attr("class", "legend") 190 .attr("class", "legend")
@@ -196,7 +199,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -196,7 +199,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
196 let color = "rgba(" + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + 199 let color = "rgba(" + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) +
197 ", " + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + 200 ", " + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) +
198 ", 240, 0.7)"; 201 ", 240, 0.7)";
199 - drawCircle(x, y, radius, color, "legend"); 202 + drawCircle(svg, x, y, radius, color, "legend");
200 } 203 }
201 204
202 // todo: describe those in the legend 205 // todo: describe those in the legend
@@ -281,7 +284,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -281,7 +284,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
281 284
282 285
283 const redrawAttendees = () => { 286 const redrawAttendees = () => {
284 - svg.selectAll("circle.attendee-dot").remove(); 287 + attendeesLayer.selectAll("circle.attendee-dot").remove();
285 288
286 emissionsData.forEach((datum) => { 289 emissionsData.forEach((datum) => {
287 // console.log("Emission datum", datum); 290 // console.log("Emission datum", datum);
@@ -314,6 +317,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -314,6 +317,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
314 ) 317 )
315 ); 318 );
316 drawCircle( 319 drawCircle(
  320 + attendeesLayer,
317 x, y, radius, 321 x, y, radius,
318 `rgba(${color}, ${color}, 240.0, 0.618)`, 322 `rgba(${color}, ${color}, 240.0, 0.618)`,
319 "attendee-dot" 323 "attendee-dot"
@@ -323,12 +327,13 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -323,12 +327,13 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
323 327
324 328
325 const redrawWorldMap = () => { 329 const redrawWorldMap = () => {
326 - cartaContainer.selectAll("path").remove(); 330 + cartaContainer.selectAll("path.world-map").remove();
327 cartaContainer.selectAll("path") 331 cartaContainer.selectAll("path")
328 .data(worldData.features) 332 .data(worldData.features)
329 .enter() 333 .enter()
330 .append("path") 334 .append("path")
331 .attr("d", geoPath) 335 .attr("d", geoPath)
  336 + .classed("world-map", true)
332 .style("fill", "#d5d5d5"); 337 .style("fill", "#d5d5d5");
333 }; 338 };
334 339
@@ -360,6 +365,65 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -360,6 +365,65 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
360 //setupLegend(); 365 //setupLegend();
361 }; 366 };
362 367
  368 + const distanceCircles = {};
  369 +
  370 + const redrawDistanceCircle = (circle_name, distance_meters) => {
  371 + let distance_tooltip;
  372 + let distance_tooltip_shadow;
  373 + if ( ! distanceCircles.hasOwnProperty(circle_name)) {
  374 + distance_tooltip_shadow = svg
  375 + .append("text")
  376 + .classed("pointer-tooltip-"+circle_name, true)
  377 + .style("pointer-events", "none")
  378 + .style("stroke", "#FFFFFF99")
  379 + .style("stroke-width", "0.2em");
  380 + distance_tooltip = svg
  381 + .append("text")
  382 + .classed("pointer-tooltip-"+circle_name, true)
  383 + .style("pointer-events", "none");
  384 + distanceCircles[circle_name] = {
  385 + 'distance_tooltip': distance_tooltip,
  386 + 'distance_tooltip_shadow': distance_tooltip_shadow,
  387 + };
  388 + } else {
  389 + distance_tooltip = distanceCircles[circle_name]['distance_tooltip'];
  390 + distance_tooltip_shadow = distanceCircles[circle_name]['distance_tooltip_shadow'];
  391 + }
  392 +
  393 + const gCircleRadius = (distance_meters / EARTH_RADIUS) * 360 / Math.TAU;
  394 + const gCircle = d3.geoCircle();
  395 + gCircle
  396 + .center([center_longitude, center_latitude])
  397 + .radius(gCircleRadius);
  398 +
  399 + svg.selectAll("path.pointer-circle-"+circle_name).remove();
  400 + svg
  401 + .append("path")
  402 + .attr("d", geoPath(gCircle()))
  403 + .classed("pointer-circle-"+circle_name, true)
  404 + // .style("fill", "#21d51d");
  405 + .style("fill", "#00000000")
  406 + .style("stroke", "#0e5b0c")
  407 + .style("stroke-dasharray", 3);
  408 +
  409 + const tooltip_pos = mapProjection([center_longitude+gCircleRadius, center_latitude]);
  410 + distance_tooltip
  411 + .attr("transform", `translate(${tooltip_pos[0]}, ${tooltip_pos[1]})`)
  412 + .text(
  413 + ((distance_meters*0.001)).toFixed(0)
  414 + +
  415 + "km"
  416 + );
  417 + distance_tooltip_shadow
  418 + .attr("transform", `translate(${tooltip_pos[0]}, ${tooltip_pos[1]})`)
  419 + .text(
  420 + ((distance_meters*0.001)).toFixed(0)
  421 + +
  422 + "km"
  423 + );
  424 +
  425 + };
  426 +
363 427
364 document.addEventListener("DOMContentLoaded", () => { 428 document.addEventListener("DOMContentLoaded", () => {
365 console.info("[Emissions Equidistant Map] Starting…"); 429 console.info("[Emissions Equidistant Map] Starting…");
@@ -369,7 +433,8 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -369,7 +433,8 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
369 .append("svg") 433 .append("svg")
370 .attr("width", width) 434 .attr("width", width)
371 .attr("height", height); 435 .attr("height", height);
372 - cartaContainer = svg.append("g"); 436 + cartaContainer = svg.append("g").classed("carta-layer", true);
  437 + attendeesLayer = svg.append("g").classed("attendees-layer", true);
373 Promise.all([ 438 Promise.all([
374 d3.csv(emissionsDataUrl), 439 d3.csv(emissionsDataUrl),
375 d3.json(worldDataUrl), 440 d3.json(worldDataUrl),
@@ -388,5 +453,19 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio @@ -388,5 +453,19 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio
388 const pointerLonLat = mapProjection.invert(d3.pointer(event)); 453 const pointerLonLat = mapProjection.invert(d3.pointer(event));
389 recenterOnLatLon(pointerLonLat[1], pointerLonLat[0]); 454 recenterOnLatLon(pointerLonLat[1], pointerLonLat[0]);
390 }); 455 });
  456 +
  457 + d3.select(containerSelector+" svg").on("mousemove", function(event) {
  458 + if ( ! mapProjection) {
  459 + console.warn("Too fast! Wait a little.");
  460 + return;
  461 + }
  462 + const pointerLonLat = mapProjection.invert(d3.pointer(event));
  463 + const centerLonLat = [center_longitude, center_latitude];
  464 + // Great Circle Distance
  465 + const gcd_radians = d3.geoDistance(pointerLonLat, centerLonLat);
  466 + const gcd_meters = gcd_radians * EARTH_RADIUS;
  467 +
  468 + redrawDistanceCircle("pointer", gcd_meters)
  469 + });
391 }); 470 });
392 } 471 }
393 \ No newline at end of file 472 \ No newline at end of file
flaskr/static/js/plots/utils.js
1 /** POLYFILLS **/ 1 /** POLYFILLS **/
2 Math.log10 = Math.log10 || function(x) { return Math.log(x) * Math.LOG10E; }; 2 Math.log10 = Math.log10 || function(x) { return Math.log(x) * Math.LOG10E; };
3 3
  4 +Math.TAU = Math.TAU || Math.PI * 2;
  5 +
  6 +
4 /** 7 /**
5 * Useful for axes' domains on plots. 8 * Useful for axes' domains on plots.
6 * @param value 9 * @param value
flaskr/templates/estimation.html
@@ -267,19 +267,19 @@ var plots_config = { @@ -267,19 +267,19 @@ var plots_config = {
267 }; 267 };
268 268
269 {% if not estimation.is_many_to_many() %} 269 {% if not estimation.is_many_to_many() %}
270 -/** 270 +
271 draw_emissions_per_distance( 271 draw_emissions_per_distance(
272 "#emissions_per_distance_histogram", 272 "#emissions_per_distance_histogram",
273 "/estimation/{{ estimation.public_id }}.csv" 273 "/estimation/{{ estimation.public_id }}.csv"
274 ); 274 );
275 -**/ 275 +
276 draw_sorted_emissions_inequality( 276 draw_sorted_emissions_inequality(
277 "#sorted_emissions_inequality", 277 "#sorted_emissions_inequality",
278 "/estimation/{{ estimation.public_id }}.csv" 278 "/estimation/{{ estimation.public_id }}.csv"
279 ); 279 );
280 {% endif %} 280 {% endif %}
281 281
282 -/** 282 +
283 draw_emissions_equidistant_map( 283 draw_emissions_equidistant_map(
284 "#d3viz_emissions_equidistant_map", 284 "#d3viz_emissions_equidistant_map",
285 {#"/static/public/data/worldmap.geo.json",#} 285 {#"/static/public/data/worldmap.geo.json",#}
@@ -288,7 +288,7 @@ draw_emissions_equidistant_map( @@ -288,7 +288,7 @@ draw_emissions_equidistant_map(
288 "/estimation/{{ estimation.public_id }}.csv" 288 "/estimation/{{ estimation.public_id }}.csv"
289 {#"/estimation/{{ estimation.public_id }}/trips_to_destination_0.csv"#} 289 {#"/estimation/{{ estimation.public_id }}/trips_to_destination_0.csv"#}
290 ); 290 );
291 -**/ 291 +
292 292
293 draw_travel_legs_worldmap( 293 draw_travel_legs_worldmap(
294 "#d3viz_travels", 294 "#d3viz_travels",