Commit 7ab0c489671dd05814137586e1a010e5597db2f1
1 parent
be2eb14f
Exists in
master
add equidistant-emissions-map.js
Showing
1 changed file
with
258 additions
and
0 deletions
Show diff stats
@@ -0,0 +1,258 @@ | @@ -0,0 +1,258 @@ | ||
1 | +function draw_equidistant_emissions_map(containerSelector, csvUrl) { | ||
2 | + let margin = {top: 48, right: 88, bottom: 68, left: 98}, | ||
3 | + width = 960 - margin.left - margin.right, | ||
4 | + height = 540 - margin.top - margin.bottom; | ||
5 | + const baseAttendeeCircleRadius = 3*0.62; | ||
6 | + const legendAmount = 5; | ||
7 | + const baseAttendeeCircleRadiusRatio = 150.0*0.62; | ||
8 | + const baseAttendeeCircleColorRatio = 1500.0; | ||
9 | + | ||
10 | + | ||
11 | + let svg = null; | ||
12 | + let countriesPath = null; | ||
13 | + let geoPath = null; | ||
14 | + let mapProjection = null; | ||
15 | + | ||
16 | + let coordinatesFromFile = null; | ||
17 | + let geoJsonFromFile = null; | ||
18 | + | ||
19 | + let attendeeAmountPerCountry = []; | ||
20 | + let countryCoordinates = []; | ||
21 | + let maxAttendeeAmount = 0; | ||
22 | + | ||
23 | + let rotateForm = document.createElement("select"); | ||
24 | + | ||
25 | + let processGeoJson = function (geojson, firstRead = true) { | ||
26 | + geoJsonFromFile = geojson; | ||
27 | + countriesPath.selectAll("path") | ||
28 | + .data(geojson.features) | ||
29 | + .enter() | ||
30 | + .append("path") | ||
31 | + .attr("d", geoPath) | ||
32 | + .style("fill", "#444444AA"); | ||
33 | + if(firstRead) | ||
34 | + { | ||
35 | + // Prepare country data | ||
36 | + geojson.features.forEach((element) => { | ||
37 | + let countryFound = false; | ||
38 | + coordinatesFromFile.forEach((countryCoord) => { | ||
39 | + if (countryFound) { | ||
40 | + return; | ||
41 | + } | ||
42 | + if (countryCoord.country === element.properties.iso_a2 || countryCoord.country === element.properties.wb_a2) { | ||
43 | + countryCoordinates.push({ | ||
44 | + name: element.properties.name_long, | ||
45 | + latitude: countryCoord.latitude, | ||
46 | + longitude: countryCoord.longitude | ||
47 | + }); | ||
48 | + countryFound = true; | ||
49 | + } | ||
50 | + }); | ||
51 | + if (!countryFound) { | ||
52 | + console.log("missing country:" + element.properties.name_long + " by the alpha2:" + element.properties.iso_a2 + " or " + element.properties.wb_a2); | ||
53 | + } | ||
54 | + }); | ||
55 | + | ||
56 | + // Sort country data | ||
57 | + function compare( a, b ) { | ||
58 | + if ( a.name < b.name ){ | ||
59 | + return -1; | ||
60 | + } | ||
61 | + if ( a.name > b.name ){ | ||
62 | + return 1; | ||
63 | + } | ||
64 | + return 0; | ||
65 | + } | ||
66 | + countryCoordinates.sort( compare ); | ||
67 | + // Create Option Elements | ||
68 | + countryCoordinates.forEach((element) =>{ | ||
69 | + let option = document.createElement("option"); | ||
70 | + option.text = element.name; | ||
71 | + option.value = "[ " + -element.longitude + ", " + -element.latitude + "] "; | ||
72 | + rotateForm.append(option); | ||
73 | + }); | ||
74 | + // Read actual data Sample | ||
75 | + d3.csv(csvUrl, on_csv_datum) | ||
76 | + .then(on_csv_ready); | ||
77 | + } | ||
78 | + }; | ||
79 | + | ||
80 | + | ||
81 | + let processCountryCoords = function (countryCoords) { | ||
82 | + coordinatesFromFile = countryCoords; | ||
83 | + if (geoJsonFromFile) { | ||
84 | + processGeoJson(geoJsonFromFile, false); | ||
85 | + on_csv_ready(); | ||
86 | + } else { | ||
87 | + d3.json('worldmap.geo.json').then(processGeoJson); | ||
88 | + | ||
89 | + } | ||
90 | + let selector = containerSelector.slice(1, containerSelector.length); | ||
91 | + document.getElementById(selector).insertAdjacentElement("beforeend", rotateForm); | ||
92 | + rotateForm.onchange = function (event) { | ||
93 | + mapProjection = d3.geoAzimuthalEquidistant().scale(100).rotate(JSON.parse(rotateForm.value)).translate([width/2, height/2]); | ||
94 | + geoPath.projection(mapProjection); | ||
95 | + // console.log(rotateForm.value); | ||
96 | + countriesPath.remove(); | ||
97 | + countriesPath = svg.append("g"); | ||
98 | + processCountryCoords(coordinatesFromFile, false); | ||
99 | + }; | ||
100 | + // d3.select("svg").on("mousedown", function(event) { | ||
101 | + // console.log(mapProjection.invert(d3.pointer(event))); | ||
102 | + // }); | ||
103 | + }; | ||
104 | + | ||
105 | + | ||
106 | + const on_csv_datum = function (datum) { | ||
107 | + let trainAttendee = parseInt(datum["train trips_amount"]); | ||
108 | + let planeAttendee = parseInt(datum["plane trips_amount"]); | ||
109 | + if (trainAttendee === 0 && planeAttendee === 0) { | ||
110 | + return; | ||
111 | + } | ||
112 | + let attendeeAmount = trainAttendee + planeAttendee; | ||
113 | + let distance_km = datum.distance_km / attendeeAmount; | ||
114 | + let co2_kg = parseFloat(datum.co2_kg); | ||
115 | + if (co2_kg === "NaN" || distance_km === "NaN") { | ||
116 | + return; | ||
117 | + } | ||
118 | + let countryFound = false; | ||
119 | + let countryName = datum["country"].slice(1, datum["country"].length); | ||
120 | + attendeeAmountPerCountry.forEach((element) =>{ | ||
121 | + if (element.country === countryName) | ||
122 | + { | ||
123 | + element.attendeeAmount += attendeeAmount; | ||
124 | + maxAttendeeAmount = Math.max(maxAttendeeAmount, element.attendeeAmount); | ||
125 | + countryFound = true; | ||
126 | + } | ||
127 | + | ||
128 | + }); | ||
129 | + if ( ! countryFound ) { | ||
130 | + attendeeAmountPerCountry.push({ | ||
131 | + country: countryName, | ||
132 | + attendeeAmount : attendeeAmount | ||
133 | + }); | ||
134 | + maxAttendeeAmount = Math.max(maxAttendeeAmount, attendeeAmount); | ||
135 | + } | ||
136 | + }; | ||
137 | + | ||
138 | + | ||
139 | + const drawCircle = function (x, y, radius, color, className = "legend") | ||
140 | + { | ||
141 | + svg.append("circle") | ||
142 | + .attr("class", className) | ||
143 | + .attr("cx", x) | ||
144 | + .attr("cy", y ) | ||
145 | + .attr("r", radius) | ||
146 | + .style("fill", color) | ||
147 | + .style("stroke", "rgba(0,0,0,0.7)") | ||
148 | + .style("stroke-width", "1"); | ||
149 | + }; | ||
150 | + | ||
151 | + | ||
152 | + const setupLegend = function() { | ||
153 | + svg.append("rect") | ||
154 | + .attr("class", "legend") | ||
155 | + .attr("transform", "translate(" + 2 + "," + 2 + ")") | ||
156 | + .attr("width", 150) | ||
157 | + .attr("height", legendAmount * 34 + 15) | ||
158 | + .style("fill", "#EEEEEEFF") | ||
159 | + .style("stroke", "#000000") | ||
160 | + .style("stroke-width", "2"); | ||
161 | + svg.append("text") | ||
162 | + .attr("class", "legend") | ||
163 | + .attr("transform", | ||
164 | + "translate(" + 60 + " ," + | ||
165 | + (28) + ")") | ||
166 | + .style("text-anchor", "left") | ||
167 | + .text((1).toFixed(0) + " attendees"); | ||
168 | + let x = 10 + 20; | ||
169 | + let y = 25; | ||
170 | + let radius = baseAttendeeCircleRadius + (baseAttendeeCircleRadiusRatio/maxAttendeeAmount); | ||
171 | + let color = "rgba(" + (-(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + | ||
172 | + ", " + (-(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + | ||
173 | + ", 240, 0.7)"; | ||
174 | + drawCircle(x, y, radius, color); | ||
175 | + for (let i = 1; i < legendAmount; i++) | ||
176 | + { | ||
177 | + svg.append("text") | ||
178 | + .attr("class", "legend") | ||
179 | + .attr("transform", | ||
180 | + "translate(" + 60 + " ," + | ||
181 | + (28 + 34 * i) + ")") | ||
182 | + .style("text-anchor", "left") | ||
183 | + .text((Math.floor(maxAttendeeAmount * (i / legendAmount))).toFixed(0) + " attendees"); | ||
184 | + let x = 10 + 20; | ||
185 | + let y = 25 + 34 * i; | ||
186 | + let radius = baseAttendeeCircleRadius + Math.sqrt(maxAttendeeAmount*(i/legendAmount))*(baseAttendeeCircleRadiusRatio/maxAttendeeAmount); | ||
187 | + let color = "rgba(" + (-Math.sqrt(maxAttendeeAmount*(i/legendAmount))*(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + | ||
188 | + ", " + (-Math.sqrt(maxAttendeeAmount*(i/legendAmount))*(baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + | ||
189 | + ", 240, 0.7)"; | ||
190 | + drawCircle(x, y, radius, color); | ||
191 | + } | ||
192 | + | ||
193 | + // todo: describe those in the legend | ||
194 | + // svg.append("circle") | ||
195 | + // .attr("cx", function (d) { return width /2; }) | ||
196 | + // .attr("cy", function (d) { return height/2; }) | ||
197 | + // .attr("r", function (d) { return 25; }) | ||
198 | + // .style("fill", function(d) { return "rgba(255, 0, 0, 0.7)"; }); | ||
199 | + // svg.append("circle") | ||
200 | + // .attr("class", "attendee-dot") | ||
201 | + // .attr("cx", function (d) { return width /2; }) | ||
202 | + // .attr("cy", function (d) { return height/2; }) | ||
203 | + // .attr("r", function (d) { return 3; }) | ||
204 | + // .style("fill", function(d) { return "rgba(255, 0, 0, 1.0)"; }); | ||
205 | + }; | ||
206 | + | ||
207 | + | ||
208 | + const on_csv_ready = function () { | ||
209 | + svg.selectAll("circle.attendee-dot").remove(); | ||
210 | + svg.selectAll("rect.legend").remove(); | ||
211 | + svg.selectAll("circle.legend").remove(); | ||
212 | + svg.selectAll("text.legend").remove(); | ||
213 | + | ||
214 | + setupLegend(); | ||
215 | + attendeeAmountPerCountry.forEach((element) => | ||
216 | + { | ||
217 | + countryCoordinates.forEach((coordinate) => { | ||
218 | + if (element.country === coordinate.name) | ||
219 | + { | ||
220 | + let x = mapProjection([coordinate.longitude, coordinate.latitude])[0]; | ||
221 | + let y = mapProjection([coordinate.longitude, coordinate.latitude])[1]; | ||
222 | + let radius = baseAttendeeCircleRadius + Math.sqrt(element.attendeeAmount)*(baseAttendeeCircleRadiusRatio/maxAttendeeAmount); | ||
223 | + let color = "rgba(" + (-Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + | ||
224 | + ", " + (-Math.sqrt(element.attendeeAmount) * (baseAttendeeCircleColorRatio/maxAttendeeAmount) + 255.0) + | ||
225 | + ", 240, 0.7)"; | ||
226 | + | ||
227 | + drawCircle(x, y, radius, color, "attendee-dot"); | ||
228 | + } | ||
229 | + }) | ||
230 | + }); | ||
231 | + svg.append("circle") | ||
232 | + .attr("class", "attendee-dot") | ||
233 | + .attr("cx", width /2) | ||
234 | + .attr("cy", height/2) | ||
235 | + .attr("r", 3) | ||
236 | + .style("fill", "rgba(255, 0, 0, 1.0)"); | ||
237 | + }; | ||
238 | + | ||
239 | + | ||
240 | + document.addEventListener("DOMContentLoaded", () => { | ||
241 | + width = Math.max(880, $(containerSelector).parent().width()); | ||
242 | + width = width - margin.left - margin.right; | ||
243 | + svg = d3.select(containerSelector) | ||
244 | + .append("svg") | ||
245 | + .attr("width", width) | ||
246 | + .attr("height", height); | ||
247 | + svg.append("circle") | ||
248 | + .attr("cx", width /2) | ||
249 | + .attr("cy", height/2) | ||
250 | + .attr("r", 25) | ||
251 | + .style("fill", "rgba(255, 0, 0, 0.7)"); | ||
252 | + geoPath = d3.geoPath(); | ||
253 | + countriesPath = svg.append("g"); | ||
254 | + mapProjection = d3.geoAzimuthalEquidistant().scale(100).rotate([-122, -13]).translate([width/2, height/2]); | ||
255 | + geoPath.projection(mapProjection); | ||
256 | + d3.csv('countries-coordinates.csv').then(processCountryCoords); | ||
257 | + }); | ||
258 | +} | ||
0 | \ No newline at end of file | 259 | \ No newline at end of file |