diff --git a/flaskr/static/js/plots/emissions-equidistant-map.js b/flaskr/static/js/plots/emissions-equidistant-map.js index 446aa42..7aa480b 100644 --- a/flaskr/static/js/plots/emissions-equidistant-map.js +++ b/flaskr/static/js/plots/emissions-equidistant-map.js @@ -1,147 +1,157 @@ -function draw_emissions_equidistant_map(containerSelector, csvUrl) { +// jQuery-free +function draw_emissions_equidistant_map(containerSelector, worldDataUrl, countriesDataUrl, emissionsDataUrl) { let margin = {top: 48, right: 88, bottom: 68, left: 98}, width = 960 - margin.left - margin.right, height = 540 - margin.top - margin.bottom; - const baseAttendeeCircleRadius = 3*0.62; + const baseAttendeeCircleRadius = 2; const legendAmount = 5; - const baseAttendeeCircleRadiusRatio = 150.0*0.62; + const baseAttendeeCircleRadiusRatio = 10.0; const baseAttendeeCircleColorRatio = 1500.0; + let emissionsData = null; + let worldData = null; let svg = null; - let countriesPath = null; - let geoPath = null; - let mapProjection = null; + let cartaContainer = null; - let coordinatesFromFile = null; - let geoJsonFromFile = null; + let geoPath = d3.geoPath(); + let mapProjection = null; + let center_latitude = 0.0; + let center_longitude = 0.0; - let attendeeAmountPerCountry = []; - let countryCoordinates = []; + // Per city let maxAttendeeAmount = 0; + let maxFootprint = 0; - let rotateForm = document.createElement("select"); + // let coordinatesFromFile = null; + // let geoJsonFromFile = null; - let processGeoJson = function (geojson, firstRead = true) { - geoJsonFromFile = geojson; - countriesPath.selectAll("path") - .data(geojson.features) - .enter() - .append("path") - .attr("d", geoPath) - .style("fill", "#444444AA"); - if(firstRead) - { - // Prepare country data - geojson.features.forEach((element) => { - let countryFound = false; - coordinatesFromFile.forEach((countryCoord) => { - if (countryFound) { - return; - } - if (countryCoord.country === element.properties.iso_a2 || countryCoord.country === element.properties.wb_a2) { - countryCoordinates.push({ - name: element.properties.name_long, - latitude: countryCoord.latitude, - longitude: countryCoord.longitude - }); - countryFound = true; - } - }); - if (!countryFound) { - console.log("missing country:" + element.properties.name_long + " by the alpha2:" + element.properties.iso_a2 + " or " + element.properties.wb_a2); - } - }); - - // Sort country data - function compare( a, b ) { - if ( a.name < b.name ){ - return -1; - } - if ( a.name > b.name ){ - return 1; - } - return 0; - } - countryCoordinates.sort( compare ); - // Create Option Elements - countryCoordinates.forEach((element) =>{ - let option = document.createElement("option"); - option.text = element.name; - option.value = "[ " + -element.longitude + ", " + -element.latitude + "] "; - rotateForm.append(option); - }); - // Read actual data Sample - d3.csv(csvUrl, on_csv_datum) - .then(on_csv_ready); - } - }; + // let attendeeAmountPerCountry = []; + // let countryCoordinates = []; - let processCountryCoords = function (countryCoords) { - coordinatesFromFile = countryCoords; - if (geoJsonFromFile) { - processGeoJson(geoJsonFromFile, false); - on_csv_ready(); - } else { - d3.json('worldmap.geo.json').then(processGeoJson); + // let rotateForm = document.createElement("select"); - } - let selector = containerSelector.slice(1, containerSelector.length); - document.getElementById(selector).insertAdjacentElement("beforeend", rotateForm); - rotateForm.onchange = function (event) { - mapProjection = d3.geoAzimuthalEquidistant().scale(100).rotate(JSON.parse(rotateForm.value)).translate([width/2, height/2]); - geoPath.projection(mapProjection); - // console.log(rotateForm.value); - countriesPath.remove(); - countriesPath = svg.append("g"); - processCountryCoords(coordinatesFromFile, false); - }; - // d3.select("svg").on("mousedown", function(event) { - // console.log(mapProjection.invert(d3.pointer(event))); - // }); - }; + // let processGeoJson = function (geojson, firstRead = true) { + // geoJsonFromFile = geojson; + // cartaContainer.selectAll("path") + // .data(geojson.features) + // .enter() + // .append("path") + // .attr("d", geoPath) + // .style("fill", "#444444AA"); + // if (firstRead) { + // // Prepare country data + // geojson.features.forEach((element) => { + // let countryFound = false; + // coordinatesFromFile.forEach((countryCoord) => { + // if (countryFound) { + // return; + // } + // if (countryCoord.country === element.properties.iso_a2 || countryCoord.country === element.properties.wb_a2) { + // countryCoordinates.push({ + // name: element.properties.name_long, + // latitude: countryCoord.latitude, + // longitude: countryCoord.longitude + // }); + // countryFound = true; + // } + // }); + // if (!countryFound) { + // console.log("missing country:" + element.properties.name_long + " by the alpha2:" + element.properties.iso_a2 + " or " + element.properties.wb_a2); + // } + // }); + // + // // Sort country data + // function compare(a, b) { + // if (a.name < b.name) { + // return -1; + // } + // if (a.name > b.name) { + // return 1; + // } + // return 0; + // } + // + // countryCoordinates.sort(compare); + // // Create Option Elements + // countryCoordinates.forEach((element) => { + // let option = document.createElement("option"); + // option.text = element.name; + // option.value = JSON.stringify([ + // element.latitude, + // element.longitude, + // ]); + // rotateForm.append(option); + // }); + // // Read actual data Sample + // d3.csv(emissionsDataUrl, onEmissionsDatum) + // .then(onEmissionsReady); + // } + // }; - const on_csv_datum = function (datum) { - let trainAttendee = parseInt(datum["train trips_amount"]); - let planeAttendee = parseInt(datum["plane trips_amount"]); - if (trainAttendee === 0 && planeAttendee === 0) { - return; - } - let attendeeAmount = trainAttendee + planeAttendee; - let distance_km = datum.distance_km / attendeeAmount; - let co2_kg = parseFloat(datum.co2_kg); - if (co2_kg === "NaN" || distance_km === "NaN") { - return; - } - let countryFound = false; - let countryName = datum["country"].slice(1, datum["country"].length); - attendeeAmountPerCountry.forEach((element) =>{ - if (element.country === countryName) - { - element.attendeeAmount += attendeeAmount; - maxAttendeeAmount = Math.max(maxAttendeeAmount, element.attendeeAmount); - countryFound = true; - } + // let processCountryCoords = function (countryCoords) { + // coordinatesFromFile = countryCoords; + // if (geoJsonFromFile) { + // processGeoJson(geoJsonFromFile, false); + // onEmissionsReady(); + // } else { + // d3.json(worldDataUrl).then(processGeoJson); + // } + // let selector = containerSelector.slice(1, containerSelector.length); + // document.getElementById(selector).insertAdjacentElement("beforeend", rotateForm); + // rotateForm.onchange = function (event) { + // const [latitude, longitude] = JSON.parse(rotateForm.value); + // recenterOnLatLon(latitude, longitude); + // // console.log(rotateForm.value); + // cartaContainer.remove(); + // cartaContainer = svg.append("g"); + // processCountryCoords(coordinatesFromFile, false); + // }; + // // d3.select("svg").on("mousedown", function(event) { + // // console.log(mapProjection.invert(d3.pointer(event))); + // // }); + // }; - }); - if ( ! countryFound ) { - attendeeAmountPerCountry.push({ - country: countryName, - attendeeAmount : attendeeAmount - }); - maxAttendeeAmount = Math.max(maxAttendeeAmount, attendeeAmount); - } - }; + + // const onEmissionsDatum = function (datum) { + // let trainAttendee = parseInt(datum["train trips_amount"]); + // let planeAttendee = parseInt(datum["plane trips_amount"]); + // if (trainAttendee === 0 && planeAttendee === 0) { + // return; + // } + // let attendeeAmount = trainAttendee + planeAttendee; + // let distance_km = datum.distance_km / attendeeAmount; + // let co2_kg = parseFloat(datum.co2_kg); + // if (co2_kg === "NaN" || distance_km === "NaN") { + // return; + // } + // let countryFound = false; + // let countryName = datum["country"].slice(1, datum["country"].length); + // attendeeAmountPerCountry.forEach((element) => { + // if (element.country === countryName) { + // element.attendeeAmount += attendeeAmount; + // maxAttendeeAmount = Math.max(maxAttendeeAmount, element.attendeeAmount); + // countryFound = true; + // } + // + // }); + // if (!countryFound) { + // attendeeAmountPerCountry.push({ + // country: countryName, + // attendeeAmount: attendeeAmount + // }); + // maxAttendeeAmount = Math.max(maxAttendeeAmount, attendeeAmount); + // } + // }; - const drawCircle = function (x, y, radius, color, className = "legend") - { + const drawCircle = function (x, y, radius, color, className = "circle") { svg.append("circle") .attr("class", className) .attr("cx", x) - .attr("cy", y ) + .attr("cy", y) .attr("r", radius) .style("fill", color) .style("stroke", "rgba(0,0,0,0.7)") @@ -149,11 +159,11 @@ function draw_emissions_equidistant_map(containerSelector, csvUrl) { }; - const setupLegend = function() { + const setupLegend = function () { svg.append("rect") .attr("class", "legend") .attr("transform", "translate(" + 2 + "," + 2 + ")") - .attr("width", 150) + .attr("width", 155) .attr("height", legendAmount * 34 + 15) .style("fill", "#EEEEEEFF") .style("stroke", "#000000") @@ -161,33 +171,32 @@ function draw_emissions_equidistant_map(containerSelector, csvUrl) { svg.append("text") .attr("class", "legend") .attr("transform", - "translate(" + 60 + " ," + + "translate(" + 50 + " ," + (28) + ")") .style("text-anchor", "left") .text((1).toFixed(0) + " attendees"); let x = 10 + 20; let y = 25; - let radius = baseAttendeeCircleRadius + (baseAttendeeCircleRadiusRatio/maxAttendeeAmount); - let color = "rgba(" + (-(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + - ", " + (-(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + + let radius = baseAttendeeCircleRadius + (baseAttendeeCircleRadiusRatio / maxAttendeeAmount); + let color = "rgba(" + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + + ", " + (-(baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + ", 240, 0.7)"; drawCircle(x, y, radius, color); - for (let i = 1; i < legendAmount; i++) - { + for (let i = 1; i < legendAmount; i++) { svg.append("text") .attr("class", "legend") .attr("transform", - "translate(" + 60 + " ," + + "translate(" + 50 + " ," + (28 + 34 * i) + ")") .style("text-anchor", "left") .text((Math.floor(maxAttendeeAmount * (i / legendAmount))).toFixed(0) + " attendees"); let x = 10 + 20; let y = 25 + 34 * i; - let radius = baseAttendeeCircleRadius + Math.sqrt(maxAttendeeAmount*(i/legendAmount))*(baseAttendeeCircleRadiusRatio/maxAttendeeAmount); - let color = "rgba(" + (-Math.sqrt(maxAttendeeAmount*(i/legendAmount))*(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + - ", " + (-Math.sqrt(maxAttendeeAmount*(i/legendAmount))*(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + + let radius = baseAttendeeCircleRadius + Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleRadiusRatio / maxAttendeeAmount); + let color = "rgba(" + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + + ", " + (-Math.sqrt(maxAttendeeAmount * (i / legendAmount)) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + ", 240, 0.7)"; - drawCircle(x, y, radius, color); + drawCircle(x, y, radius, color, "legend"); } // todo: describe those in the legend @@ -205,54 +214,176 @@ function draw_emissions_equidistant_map(containerSelector, csvUrl) { }; - const on_csv_ready = function () { - svg.selectAll("circle.attendee-dot").remove(); - svg.selectAll("rect.legend").remove(); - svg.selectAll("circle.legend").remove(); - svg.selectAll("text.legend").remove(); - - setupLegend(); - attendeeAmountPerCountry.forEach((element) => - { - countryCoordinates.forEach((coordinate) => { - if (element.country === coordinate.name) - { - let x = mapProjection([coordinate.longitude, coordinate.latitude])[0]; - let y = mapProjection([coordinate.longitude, coordinate.latitude])[1]; - let radius = baseAttendeeCircleRadius + Math.sqrt(element.attendeeAmount)*(baseAttendeeCircleRadiusRatio/maxAttendeeAmount); - let color = "rgba(" + (-Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + - ", " + (-Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + - ", 240, 0.7)"; - - drawCircle(x, y, radius, color, "attendee-dot"); - } - }) + // const onEmissionsReady = function () { + // svg.selectAll("circle.attendee-dot").remove(); + // svg.selectAll("rect.legend").remove(); + // svg.selectAll("circle.legend").remove(); + // svg.selectAll("text.legend").remove(); + // + // setupLegend(); + // attendeeAmountPerCountry.forEach((element) => { + // countryCoordinates.forEach((coordinate) => { + // if (element.country === coordinate.name) { + // let x = mapProjection([coordinate.longitude, coordinate.latitude])[0]; + // let y = mapProjection([coordinate.longitude, coordinate.latitude])[1]; + // let radius = baseAttendeeCircleRadius + Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleRadiusRatio / maxAttendeeAmount); + // let color = "rgba(" + (-Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + + // ", " + (-Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleColorRatio / maxAttendeeAmount) + 255.0) + + // ", 240, 0.7)"; + // + // drawCircle(x, y, radius, color, "attendee-dot"); + // } + // }) + // }); + // svg.append("circle") + // .attr("class", "attendee-dot") + // .attr("cx", width / 2) + // .attr("cy", height / 2) + // .attr("r", 3) + // .style("fill", "rgba(255, 0, 0, 1.0)"); + // }; + + + const crunchEmissionsData = () => { + emissionsData.forEach((datum, idx) => { + let trainAttendeesAmount = parseInt(datum["train trips_amount"]); + let planeAttendeesAmount = parseInt(datum["plane trips_amount"]); + if (trainAttendeesAmount === 0 && planeAttendeesAmount === 0) { + return; + } + let attendeesAmount = trainAttendeesAmount + planeAttendeesAmount; + maxAttendeeAmount = Math.max(maxAttendeeAmount, attendeesAmount); + emissionsData[idx].attendeeAmount = attendeesAmount; + + maxFootprint = Math.max(maxFootprint, datum.co2_kg); }); + }; + + + const redrawCentralCircle = () => { + svg.selectAll("circle.central-dot").remove(); + svg.append("circle") - .attr("class", "attendee-dot") - .attr("cx", width /2) - .attr("cy", height/2) - .attr("r", 3) - .style("fill", "rgba(255, 0, 0, 1.0)"); + .attr("cx", width / 2) + .attr("cy", height / 2) + .attr("r", 2) + .classed("central-dot", true) + .style("fill", "rgba(255, 0, 0, 0.777)"); + }; + + + const redrawDistanceCircles = () => { + // TODO: draw a few circles and a label with the distance for each + // … + // or not. + // We might instead draw the circle under the mouse + }; + + + const redrawAttendees = () => { + svg.selectAll("circle.attendee-dot").remove(); + + emissionsData.forEach((datum) => { + // console.log("Emission datum", datum); + let x = mapProjection([datum.longitude, datum.latitude])[0]; + let y = mapProjection([datum.longitude, datum.latitude])[1]; + let radius = ( + baseAttendeeCircleRadius + + + ( + baseAttendeeCircleRadiusRatio + * + Math.sqrt( + datum.attendeeAmount + / + maxAttendeeAmount + ) + ) + ); + let color = ( + 255.0 + - + ( + baseAttendeeCircleColorRatio + * + Math.sqrt( + datum.co2_kg + / + maxFootprint + ) + ) + ); + drawCircle( + x, y, radius, + `rgba(${color}, ${color}, 240.0, 0.618)`, + "attendee-dot" + ); + }); + }; + + + const redrawWorldMap = () => { + cartaContainer.selectAll("path").remove(); + cartaContainer.selectAll("path") + .data(worldData.features) + .enter() + .append("path") + .attr("d", geoPath) + .style("fill", "#d5d5d5"); + }; + + + const rebuildProjection = () => { + mapProjection = d3.geoAzimuthalEquidistant() + .scale(79.4188) + .rotate([ + // Don't ask me why + -1 * center_longitude, + -1 * center_latitude, + ]) + .translate([width / 2, height / 2]); + geoPath.projection(mapProjection); + }; + + + const recenterOnLatLon = (latitude, longitude) => { + center_latitude = latitude; + center_longitude = longitude; + + rebuildProjection(); + // Draw in order from back to front + redrawWorldMap(); + redrawDistanceCircles(); + redrawAttendees(); + redrawCentralCircle(); + + //setupLegend(); }; document.addEventListener("DOMContentLoaded", () => { - width = Math.max(880, $(containerSelector).parent().width()); + width = document.querySelector(containerSelector).parentElement.offsetWidth; width = width - margin.left - margin.right; svg = d3.select(containerSelector) .append("svg") .attr("width", width) .attr("height", height); - svg.append("circle") - .attr("cx", width /2) - .attr("cy", height/2) - .attr("r", 25) - .style("fill", "rgba(255, 0, 0, 0.7)"); - geoPath = d3.geoPath(); - countriesPath = svg.append("g"); - mapProjection = d3.geoAzimuthalEquidistant().scale(100).rotate([-122, -13]).translate([width/2, height/2]); - geoPath.projection(mapProjection); - d3.csv('countries-coordinates.csv').then(processCountryCoords); + cartaContainer = svg.append("g"); + Promise.all([ + d3.csv(emissionsDataUrl), + d3.json(worldDataUrl), + ]).then((allTheData) => { + [emissionsData, worldData] = allTheData; + crunchEmissionsData(); + recenterOnLatLon( + parseFloat(emissionsData[0].latitude), + parseFloat(emissionsData[0].longitude) + ); + }); + + d3.select(containerSelector+" svg").on("mousedown", function(event) { + const pointerLonLat = mapProjection.invert(d3.pointer(event)); + recenterOnLatLon(pointerLonLat[1], pointerLonLat[0]); + }); }); } \ No newline at end of file diff --git a/flaskr/templates/estimation.html b/flaskr/templates/estimation.html index c94e40b..bd43d5f 100644 --- a/flaskr/templates/estimation.html +++ b/flaskr/templates/estimation.html @@ -212,6 +212,8 @@ <hr> + <div id="d3viz_emissions_equidistant_map" class="plot-container-noborder"></div> + <div id="d3viz_travels" class="plot-container-noborder"></div> </div> @@ -254,6 +256,7 @@ <script src="/static/js/vendor/d3-geo-projection.v2.min.js"></script> <script src="/static/js/plots/utils.js"></script> <script src="/static/js/plots/emissions-per-distance.js"></script> +<script src="/static/js/plots/emissions-equidistant-map.js"></script> <script src="/static/js/plots/sorted-emissions-inequality.js"></script> <script src="/static/js/plots/travel-legs-worldmap.js"></script> @@ -275,6 +278,15 @@ draw_sorted_emissions_inequality( {% endif %} +draw_emissions_equidistant_map( + "#d3viz_emissions_equidistant_map", + {#"/static/public/data/worldmap.geo.json",#} + "/static/public/data/world-earth.geojson", + "/static/public/data/countries-coordinates.csv", + "/estimation/{{ estimation.public_id }}.csv" + {#"/estimation/{{ estimation.public_id }}/trips_to_destination_0.csv"#} +); + draw_travel_legs_worldmap( "#d3viz_travels", "/static/public/data/world-earth.geojson", -- libgit2 0.21.2