Compare View
Commits (5)
-
Co-authored by: @Adrenesis
-
Co-authored by: Adrenesis <adrenesis@gmail.com>
-
Co-authored by: @Adrenesis
Showing
5 changed files
Show diff stats
flaskr/static/js/plots/emissions-equidistant-map.js
1 | 1 | // jQuery-free |
2 | 2 | function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissionsDataUrl) { |
3 | + const EARTH_RADIUS = 6371000; // meters | |
4 | + | |
3 | 5 | let margin = {top: 48, right: 88, bottom: 68, left: 98}, |
4 | 6 | width = 960 - margin.left - margin.right, |
5 | 7 | height = 540 - margin.top - margin.bottom; |
... | ... | @@ -13,12 +15,15 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
13 | 15 | |
14 | 16 | let svg = null; |
15 | 17 | let cartaContainer = null; |
18 | + let attendeesLayer = null; | |
16 | 19 | |
17 | 20 | let geoPath = d3.geoPath(); |
18 | 21 | let mapProjection = null; |
19 | 22 | let center_latitude = 0.0; |
20 | 23 | let center_longitude = 0.0; |
21 | 24 | |
25 | + let projectionScale = 79.4188; | |
26 | + | |
22 | 27 | // Per city |
23 | 28 | let maxAttendeeAmount = 0; |
24 | 29 | let maxFootprint = 0; |
... | ... | @@ -147,8 +152,8 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
147 | 152 | // }; |
148 | 153 | |
149 | 154 | |
150 | - const drawCircle = function (x, y, radius, color, className = "circle") { | |
151 | - svg.append("circle") | |
155 | + const drawCircle = function (into, x, y, radius, color, className = "circle") { | |
156 | + into.append("circle") | |
152 | 157 | .attr("class", className) |
153 | 158 | .attr("cx", x) |
154 | 159 | .attr("cy", y) |
... | ... | @@ -181,7 +186,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
181 | 186 | let color = "rgba(" + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + |
182 | 187 | ", " + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + |
183 | 188 | ", 240, 0.7)"; |
184 | - drawCircle(x, y, radius, color); | |
189 | + drawCircle(svg, x, y, radius, color); | |
185 | 190 | for (let i = 1; i < legendAmount; i++) { |
186 | 191 | svg.append("text") |
187 | 192 | .attr("class", "legend") |
... | ... | @@ -196,7 +201,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
196 | 201 | let color = "rgba(" + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + |
197 | 202 | ", " + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + |
198 | 203 | ", 240, 0.7)"; |
199 | - drawCircle(x, y, radius, color, "legend"); | |
204 | + drawCircle(svg, x, y, radius, color, "legend"); | |
200 | 205 | } |
201 | 206 | |
202 | 207 | // todo: describe those in the legend |
... | ... | @@ -281,7 +286,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
281 | 286 | |
282 | 287 | |
283 | 288 | const redrawAttendees = () => { |
284 | - svg.selectAll("circle.attendee-dot").remove(); | |
289 | + attendeesLayer.selectAll("circle.attendee-dot").remove(); | |
285 | 290 | |
286 | 291 | emissionsData.forEach((datum) => { |
287 | 292 | // console.log("Emission datum", datum); |
... | ... | @@ -314,6 +319,7 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
314 | 319 | ) |
315 | 320 | ); |
316 | 321 | drawCircle( |
322 | + attendeesLayer, | |
317 | 323 | x, y, radius, |
318 | 324 | `rgba(${color}, ${color}, 240.0, 0.618)`, |
319 | 325 | "attendee-dot" |
... | ... | @@ -323,19 +329,20 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
323 | 329 | |
324 | 330 | |
325 | 331 | const redrawWorldMap = () => { |
326 | - cartaContainer.selectAll("path").remove(); | |
332 | + cartaContainer.selectAll("path.world-map").remove(); | |
327 | 333 | cartaContainer.selectAll("path") |
328 | 334 | .data(worldData.features) |
329 | 335 | .enter() |
330 | 336 | .append("path") |
331 | 337 | .attr("d", geoPath) |
338 | + .classed("world-map", true) | |
332 | 339 | .style("fill", "#d5d5d5"); |
333 | 340 | }; |
334 | 341 | |
335 | 342 | |
336 | 343 | const rebuildProjection = () => { |
337 | 344 | mapProjection = d3.geoAzimuthalEquidistant() |
338 | - .scale(79.4188) | |
345 | + .scale(projectionScale) | |
339 | 346 | .rotate([ |
340 | 347 | // Don't ask me why |
341 | 348 | -1 * center_longitude, |
... | ... | @@ -346,20 +353,82 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
346 | 353 | }; |
347 | 354 | |
348 | 355 | |
356 | + const redrawProjected = () => { | |
357 | + redrawWorldMap(); | |
358 | + redrawDistanceCircles(); | |
359 | + redrawAttendees(); | |
360 | + redrawCentralCircle(); | |
361 | + }; | |
362 | + | |
349 | 363 | const recenterOnLatLon = (latitude, longitude) => { |
350 | 364 | center_latitude = latitude; |
351 | 365 | center_longitude = longitude; |
352 | 366 | |
353 | 367 | rebuildProjection(); |
354 | 368 | // Draw in order from back to front |
355 | - redrawWorldMap(); | |
356 | - redrawDistanceCircles(); | |
357 | - redrawAttendees(); | |
358 | - redrawCentralCircle(); | |
359 | - | |
369 | + redrawProjected() | |
360 | 370 | //setupLegend(); |
361 | 371 | }; |
362 | 372 | |
373 | + const distanceCircles = {}; | |
374 | + | |
375 | + const redrawDistanceCircle = (circle_name, distance_meters) => { | |
376 | + let distance_tooltip; | |
377 | + let distance_tooltip_shadow; | |
378 | + if ( ! distanceCircles.hasOwnProperty(circle_name)) { | |
379 | + distance_tooltip_shadow = svg | |
380 | + .append("text") | |
381 | + .classed("pointer-tooltip-"+circle_name, true) | |
382 | + .style("pointer-events", "none") | |
383 | + .style("stroke", "#FFFFFF99") | |
384 | + .style("stroke-width", "0.2em"); | |
385 | + distance_tooltip = svg | |
386 | + .append("text") | |
387 | + .classed("pointer-tooltip-"+circle_name, true) | |
388 | + .style("pointer-events", "none"); | |
389 | + distanceCircles[circle_name] = { | |
390 | + 'distance_tooltip': distance_tooltip, | |
391 | + 'distance_tooltip_shadow': distance_tooltip_shadow, | |
392 | + }; | |
393 | + } else { | |
394 | + distance_tooltip = distanceCircles[circle_name]['distance_tooltip']; | |
395 | + distance_tooltip_shadow = distanceCircles[circle_name]['distance_tooltip_shadow']; | |
396 | + } | |
397 | + | |
398 | + const gCircleRadius = (distance_meters / EARTH_RADIUS) * 360 / Math.TAU; | |
399 | + const gCircle = d3.geoCircle(); | |
400 | + gCircle | |
401 | + .center([center_longitude, center_latitude]) | |
402 | + .radius(gCircleRadius); | |
403 | + | |
404 | + svg.selectAll("path.pointer-circle-"+circle_name).remove(); | |
405 | + svg | |
406 | + .append("path") | |
407 | + .attr("d", geoPath(gCircle())) | |
408 | + .classed("pointer-circle-"+circle_name, true) | |
409 | + // .style("fill", "#21d51d"); | |
410 | + .style("fill", "#00000000") | |
411 | + .style("stroke", "#0e5b0c") | |
412 | + .style("stroke-dasharray", 3); | |
413 | + | |
414 | + const tooltip_pos = mapProjection([center_longitude+gCircleRadius, center_latitude]); | |
415 | + distance_tooltip | |
416 | + .attr("transform", `translate(${tooltip_pos[0]}, ${tooltip_pos[1]})`) | |
417 | + .text( | |
418 | + ((distance_meters*0.001)).toFixed(0) | |
419 | + + | |
420 | + "km" | |
421 | + ); | |
422 | + distance_tooltip_shadow | |
423 | + .attr("transform", `translate(${tooltip_pos[0]}, ${tooltip_pos[1]})`) | |
424 | + .text( | |
425 | + ((distance_meters*0.001)).toFixed(0) | |
426 | + + | |
427 | + "km" | |
428 | + ); | |
429 | + | |
430 | + }; | |
431 | + | |
363 | 432 | |
364 | 433 | document.addEventListener("DOMContentLoaded", () => { |
365 | 434 | console.info("[Emissions Equidistant Map] Starting…"); |
... | ... | @@ -369,7 +438,8 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
369 | 438 | .append("svg") |
370 | 439 | .attr("width", width) |
371 | 440 | .attr("height", height); |
372 | - cartaContainer = svg.append("g"); | |
441 | + cartaContainer = svg.append("g").classed("carta-layer", true); | |
442 | + attendeesLayer = svg.append("g").classed("attendees-layer", true); | |
373 | 443 | Promise.all([ |
374 | 444 | d3.csv(emissionsDataUrl), |
375 | 445 | d3.json(worldDataUrl), |
... | ... | @@ -384,9 +454,55 @@ function draw_emissions_equidistant_map(containerSelector, worldDataUrl, emissio |
384 | 454 | console.info("[Emissions Equidistant Map] Done.-"); |
385 | 455 | }); |
386 | 456 | |
457 | + // console.log(d3.select(containerSelector)); | |
458 | + let buttonContainer = d3.select(containerSelector) | |
459 | + .append("div") | |
460 | + .style("position", 'absolute') | |
461 | + .style("top", d3.select(containerSelector).property("clientTop") + 12) | |
462 | + .style("left", | |
463 | + d3.select(containerSelector).property("clientLeft") + | |
464 | + svg.attr("width") | |
465 | + - 40); | |
466 | + let zoomOutButton = buttonContainer | |
467 | + .append("input") | |
468 | + .attr("value", "-") | |
469 | + .attr("type", "button") | |
470 | + .style("width", 22); | |
471 | + let zoomInButton = buttonContainer | |
472 | + .append("input") | |
473 | + .attr("value", "+") | |
474 | + .attr("type", "button") | |
475 | + .style("width", 22); | |
476 | + | |
477 | + zoomOutButton.on("click", () => { | |
478 | + projectionScale *= 0.9; | |
479 | + rebuildProjection(); | |
480 | + redrawProjected(); | |
481 | + }); | |
482 | + | |
483 | + zoomInButton.on("click", () => { | |
484 | + projectionScale *= 1.10; | |
485 | + rebuildProjection(); | |
486 | + redrawProjected(); | |
487 | + }); | |
488 | + | |
387 | 489 | d3.select(containerSelector+" svg").on("mousedown", function(event) { |
388 | 490 | const pointerLonLat = mapProjection.invert(d3.pointer(event)); |
389 | 491 | recenterOnLatLon(pointerLonLat[1], pointerLonLat[0]); |
390 | 492 | }); |
493 | + | |
494 | + d3.select(containerSelector+" svg").on("mousemove", function(event) { | |
495 | + if ( ! mapProjection) { | |
496 | + console.warn("Too fast! Wait a little."); | |
497 | + return; | |
498 | + } | |
499 | + const pointerLonLat = mapProjection.invert(d3.pointer(event)); | |
500 | + const centerLonLat = [center_longitude, center_latitude]; | |
501 | + // Great Circle Distance | |
502 | + const gcd_radians = d3.geoDistance(pointerLonLat, centerLonLat); | |
503 | + const gcd_meters = gcd_radians * EARTH_RADIUS; | |
504 | + | |
505 | + redrawDistanceCircle("pointer", gcd_meters) | |
506 | + }); | |
391 | 507 | }); |
392 | 508 | } |
393 | 509 | \ No newline at end of file | ... | ... |
flaskr/static/js/plots/emissions-per-distance.js
... | ... | @@ -8,10 +8,20 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
8 | 8 | |
9 | 9 | |
10 | 10 | function getTicks(maxValue, interval, startValue = 0) { |
11 | + // console.log("getTicks", maxValue, interval, startValue); | |
12 | + if (0 === interval) { | |
13 | + console.error("No interval for ticks."); | |
14 | + return [0]; | |
15 | + } | |
16 | + if (Math.sign(maxValue-startValue) !== Math.sign(interval)) { | |
17 | + console.warn("Wrong interval sign for ticks. Corrected."); | |
18 | + interval *= -1; | |
19 | + } | |
11 | 20 | let range = []; |
12 | 21 | for (let i = startValue; i <= maxValue; i += interval) { |
13 | 22 | range.push(i); |
14 | 23 | } |
24 | + // console.log("range", range); | |
15 | 25 | return range; |
16 | 26 | } |
17 | 27 | |
... | ... | @@ -21,8 +31,30 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
21 | 31 | return range; |
22 | 32 | } |
23 | 33 | |
24 | - function getLeftTicks(maxemissions) { | |
25 | - return getTicks(maxemissions, Math.floor((maxemissions / 8) / 1000) * 1000); | |
34 | + function getLeftTicks(maximumValue) { | |
35 | + maximumValue = 3200+1; | |
36 | + if (0 > maximumValue) { | |
37 | + console.error("Only positive values are supported on left axis."); | |
38 | + } | |
39 | + if (0 === maximumValue) { | |
40 | + return [0]; | |
41 | + } | |
42 | + let ticksAmount = 8.0; // MUST BE > 2 | |
43 | + let magnitude = 100000.0; | |
44 | + let interval = 0; | |
45 | + while (interval * (ticksAmount) < maximumValue) { | |
46 | + interval = ( | |
47 | + Math.floor( | |
48 | + (maximumValue / (ticksAmount-1)) / magnitude | |
49 | + ) | |
50 | + * | |
51 | + magnitude | |
52 | + ); | |
53 | + //console.log("interval + magnitude", interval, magnitude); | |
54 | + magnitude *= 0.1; | |
55 | + } | |
56 | + | |
57 | + return getTicks(maximumValue, interval); | |
26 | 58 | } |
27 | 59 | |
28 | 60 | function getRightTicks(maxemissionsPercent) { |
... | ... | @@ -134,6 +166,7 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
134 | 166 | } |
135 | 167 | |
136 | 168 | document.addEventListener("DOMContentLoaded", () => { |
169 | + console.info("[Emissions Per Distance] Starting…"); | |
137 | 170 | width = Math.max(880, $(containerSelector).parent().width()); |
138 | 171 | width = width - margin.left - margin.right; |
139 | 172 | let maxemissions = 0; |
... | ... | @@ -163,7 +196,13 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
163 | 196 | let attendeeNumber = trainAttendee + planeAttendee; |
164 | 197 | let distance_km = datum.distance_km / attendeeNumber; |
165 | 198 | let co2_kg = parseFloat(datum.co2_kg); |
166 | - if (co2_kg === "NaN" || distance_km / sliceThickness > 37 || distance_km === "NaN") { | |
199 | + if ( | |
200 | + (co2_kg === "NaN") | |
201 | + || | |
202 | + (distance_km === "NaN") | |
203 | + // || | |
204 | + // (distance_km / sliceThickness > 37) | |
205 | + ) { | |
167 | 206 | return; |
168 | 207 | } |
169 | 208 | rows.push(datum); |
... | ... | @@ -173,6 +212,7 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
173 | 212 | }; |
174 | 213 | |
175 | 214 | const on_csv_ready = function () { |
215 | + console.info("[Emissions Per Distance] Generating…"); | |
176 | 216 | for (let i = 0; i <= maxDistance / sliceThickness; i++) { |
177 | 217 | emissionsPerGroup[i] = 0; |
178 | 218 | attendeeNumberPerGroup[i] = 0; |
... | ... | @@ -192,7 +232,6 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
192 | 232 | maxemissionsPercent = Math.max(maxemissionsPercent, element / emissionsSum * 100.0) |
193 | 233 | }); |
194 | 234 | maxDistance += 2000; |
195 | - // console.log(maxDistance); | |
196 | 235 | |
197 | 236 | // Title |
198 | 237 | svg.append("text") |
... | ... | @@ -207,7 +246,9 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
207 | 246 | .domain([0, maxDistance]) |
208 | 247 | .range([0, width]); |
209 | 248 | let xAxis = d3.axisBottom(x) |
210 | - .tickValues(getBottomTicks(maxDistance)); | |
249 | + .ticks(11) | |
250 | + // .tickValues(getBottomTicks(maxDistance)) | |
251 | + ; | |
211 | 252 | svg.append("g") |
212 | 253 | .attr("transform", "translate(0," + height + ")") |
213 | 254 | .attr("class", "x axis") |
... | ... | @@ -224,7 +265,9 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
224 | 265 | .domain([0, maxemissions]) |
225 | 266 | .range([height, 0]); |
226 | 267 | let ylAxis = d3.axisLeft(yl) |
227 | - .tickValues(getLeftTicks(maxemissions)); | |
268 | + .ticks(13) | |
269 | + // .tickValues(getLeftTicks(maxemissions)) | |
270 | + ; | |
228 | 271 | svg.append("g") |
229 | 272 | .attr("class", "yl axis") |
230 | 273 | .call(ylAxis); |
... | ... | @@ -234,7 +277,9 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
234 | 277 | .domain([0, maxemissionsPercent]) |
235 | 278 | .range([height, 0]); |
236 | 279 | let yrAxis = d3.axisRight(yr) |
237 | - .tickValues(getRightTicks(maxemissionsPercent)); | |
280 | + .ticks(20) | |
281 | + // .tickValues(getRightTicks(maxemissionsPercent)) | |
282 | + ; | |
238 | 283 | svg.append("g") |
239 | 284 | .attr("transform", "translate(" + width + ", 0)") |
240 | 285 | .attr("class", "yr axis") |
... | ... | @@ -268,7 +313,6 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
268 | 313 | .thresholds(x.ticks(Math.floor(maxDistance / sliceThickness))); // then the numbers of bins |
269 | 314 | |
270 | 315 | let histolol = histogram(0); |
271 | - // console.log(histolol); | |
272 | 316 | let barSettings = []; |
273 | 317 | emissionsPerGroup.forEach((element, index) => { |
274 | 318 | barSettings[index] = { |
... | ... | @@ -306,6 +350,7 @@ function draw_emissions_per_distance(containerSelector, csvUrl) { |
306 | 350 | .style("z-index", "500") |
307 | 351 | .style("fill", "#4444E5"); |
308 | 352 | addVerticalLineAndListenCursor(x, attendeeNumberPerGroup, attendeeSum); |
353 | + console.info("[Emissions Per Distance] Done."); | |
309 | 354 | }; |
310 | 355 | |
311 | 356 | d3.csv(csvUrl, on_csv_datum) | ... | ... |
flaskr/static/js/plots/sorted-emissions-inequality.js
... | ... | @@ -193,13 +193,13 @@ function draw_sorted_emissions_inequality(containerSelector, csvUrl) { |
193 | 193 | .attr("class", "no-pointer-events") |
194 | 194 | .style("display", "none") |
195 | 195 | .style("position", "absolute") |
196 | - .style("z-index", "-50") | |
196 | + .style("z-index", "10") | |
197 | 197 | .style("width", "0px") |
198 | 198 | .style("height", (height) + "px") |
199 | 199 | .style("top", (margin.top) + "px") |
200 | 200 | .style("bottom", "30px") |
201 | 201 | .style("left", "0px") |
202 | - .style("background", "rgba(60, 200, 60, 0.3)"); | |
202 | + .style("background", "rgba(60, 200, 60, 0.2)"); | |
203 | 203 | |
204 | 204 | d3.select(containerSelector) |
205 | 205 | .on("mousemove", function (event) { | ... | ... |
flaskr/static/js/plots/utils.js
flaskr/templates/estimation.html
... | ... | @@ -267,19 +267,19 @@ var plots_config = { |
267 | 267 | }; |
268 | 268 | |
269 | 269 | {% if not estimation.is_many_to_many() %} |
270 | -/** | |
270 | + | |
271 | 271 | draw_emissions_per_distance( |
272 | 272 | "#emissions_per_distance_histogram", |
273 | 273 | "/estimation/{{ estimation.public_id }}.csv" |
274 | 274 | ); |
275 | -**/ | |
275 | + | |
276 | 276 | draw_sorted_emissions_inequality( |
277 | 277 | "#sorted_emissions_inequality", |
278 | 278 | "/estimation/{{ estimation.public_id }}.csv" |
279 | 279 | ); |
280 | 280 | {% endif %} |
281 | 281 | |
282 | -/** | |
282 | + | |
283 | 283 | draw_emissions_equidistant_map( |
284 | 284 | "#d3viz_emissions_equidistant_map", |
285 | 285 | {#"/static/public/data/worldmap.geo.json",#} |
... | ... | @@ -288,7 +288,7 @@ draw_emissions_equidistant_map( |
288 | 288 | "/estimation/{{ estimation.public_id }}.csv" |
289 | 289 | {#"/estimation/{{ estimation.public_id }}/trips_to_destination_0.csv"#} |
290 | 290 | ); |
291 | -**/ | |
291 | + | |
292 | 292 | |
293 | 293 | draw_travel_legs_worldmap( |
294 | 294 | "#d3viz_travels", | ... | ... |