├── .babelrc
├── .eslintrc
├── .gitignore
├── README.md
├── deploy.sh
├── package-lock.json
├── package.json
├── postcss.config.js
├── screenshot.png
├── src
├── data
│ ├── .gitignore
│ ├── countries-lat-lng.json
│ ├── data.json
│ ├── data_cleaner.py
│ └── generate.sh
├── images
│ ├── body_background.png
│ ├── favicon.ico
│ ├── favicon.png
│ ├── loading.gif
│ ├── space_bk.png
│ ├── space_dn.png
│ ├── space_ft.png
│ ├── space_lf.png
│ ├── space_rt.png
│ ├── space_up.png
│ └── world.jpg
├── index.html
├── js
│ ├── app.js
│ ├── countries.js
│ ├── country-iso-to-latlng.js
│ ├── globe.js
│ ├── player.js
│ ├── search.js
│ ├── stats.js
│ ├── utils
│ │ ├── debounce.js
│ │ ├── nonBlockingWait.js
│ │ └── three-buffer-geometry-utils.js
│ └── webgl-check.js
└── scss
│ ├── app.scss
│ └── nouislider.scss
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env"]
4 | ],
5 | "plugins": [
6 | ["@babel/transform-runtime"],
7 | ],
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "env": {
4 | "node": true,
5 | "jasmine": true,
6 | "jquery": true
7 | },
8 | "rules": {
9 | "no-use-before-define": 0,
10 | "func-names": 0,
11 | "prefer-arrow-callback": 0,
12 | "no-var": 0,
13 | "max-len": 0,
14 | "guard-for-in": 0,
15 | "object-shorthand": 0,
16 | "no-restricted-syntax": 0,
17 | "prefer-template": 0,
18 | "import/no-amd": 0,
19 | "space-before-function-paren": 0,
20 | "jsx-a11y/href-no-hash": "off",
21 | "jsx-a11y/anchor-is-valid": ["warn", { "aspects": ["invalidHref"] }],
22 | "import/no-unresolved": 0,
23 | "import/extensions": 0
24 | },
25 | "globals": {
26 | "browser": false,
27 | "window": true,
28 | "document": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Mac OS X
4 | .DS_Store
5 | ._.*
6 | ._*
7 |
8 | # Ignore local editor
9 | .project
10 | .settings
11 | .idea
12 | *.swp
13 | tags
14 | nbproject/*
15 |
16 | # Windows
17 | Thumbs.db
18 |
19 | npm-debug.log
20 |
21 | node_modules/
22 |
23 | dist/
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ailing Planet
2 |
3 | 
4 |
5 | The goal of this project is to provide an interactive timelapse of the COVID-19
6 | pandemic as it has occurred.
7 |
8 | At every point in the time of the visualisation, the red bars represent the impact
9 | of the virus on countries relative to the most impacted country.
10 |
11 | You can view it [here](http://covid-live.com).
12 |
13 |
14 |
15 | ### Credits
16 |
17 | This project would not have been possible without:
18 | * [Our World In Data](https://ourworldindata.org/) (OWID) which has a [project](https://ourworldindata.org/coronavirus) to collect, clean and make public COVID-19 data. All the data used for this visualisation is sourced from OWID.
19 | * [Experiments with Google](https://experiments.withgoogle.com/chrome/globe) for inspiration and providing the base code for the globe used on this project.
20 | * [ObservableHQ](https://observablehq.com/@d3/bar-chart-race-explained)'s resource on creating a data race chart.
21 |
22 | ### Attributions
23 |
24 | * Kawaii Coffee icon by [Icons8](https://icons8.com/icon/120078/kawaii-coffee).
25 | * Ulukai's Space Skyboxes by [Calinou](https://opengameart.org/content/ulukais-space-skyboxes).
26 |
27 | ### Running Locally
28 |
29 | To run this locally,
30 |
31 | 1. First clone this repo
32 | ```
33 | $ git clone https://codingedward/ailing-planet.git
34 | ```
35 |
36 | 2. Install the dependancies:
37 | ```
38 | $ npm install
39 | ```
40 |
41 | 3. To start a local server, and for hot module reloading, you can run:
42 | ```
43 | $ npm run watch
44 | ```
45 |
46 | 4. Once you have built everything and you are ready to make a production build, run:
47 | ```
48 | $ npm run production
49 | ```
50 |
51 | ### Deployment
52 |
53 | This app is currently being deployed to GitHub Pages and deployment is easily done by running:
54 | ```
55 | $ ./deploy.sh
56 | ```
57 | after having made a production build. For now, only the repo owners can run this command.
58 |
59 |
60 | ### Contributions
61 |
62 | Any contribution is most welcome. Some key key areas that may currently need refining:
63 |
64 | 1. Browser compatibility - this is only currently tested on Chrome, Firefox and Safari. If you identify any browser incompatibility issues, feel free to raise a PR.
65 | 2. Performance - while it is somewhat expected that the visualisation is CPU and GPU intensive, if you can identify any places we can improve performance, that would be awesome!
66 | 3. Any other issue really! Feel free to raise a PR to improve the code, UI/UX, etc.
67 |
68 |
69 | ### Developer
70 |
71 | Built with <3 by [Edward Njoroge](https://github.com/codingedward) ([@codingedward](https://twitter.com/codingedward)).
72 |
73 | If you'd like to appreciate this work, you can:
74 |
75 | [ Buy me a Coffee](https://www.buymeacoffee.com/codingedward)
76 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | git checkout master
3 |
4 | echo "Checking out to temporary directory ..."
5 | echo
6 | git branch -D deployment/temp 2>/dev/null
7 | git checkout -b deployment/temp
8 | echo
9 |
10 | echo "Generating data and building assets ..."
11 | echo
12 | pushd src/data/
13 | ./generate.sh
14 | popd
15 | npm run production
16 | echo
17 |
18 | echo "Current directory is: "
19 | pwd
20 | echo "To deploy changes, we need to run 'rm -rf' on the current directory ..."
21 | echo
22 | echo "The following files will be deleted... "
23 | ls -Al1 | grep -E -v "dist|CNAME|.git|node_modules"
24 | echo
25 | read -p "Continue(Y/y)? " -n 1 -r
26 | echo
27 | if [[ $REPLY =~ ^[Yy]$ ]]
28 | then
29 | ls -Al1 | grep -E -v "dist|CNAME|.git|node_modules" | xargs rm -rf
30 | echo "node_modules/" > .gitignore
31 | fi
32 |
33 | echo "Setting up deployment branch ..."
34 | echo
35 | cp -r ./dist/* .
36 | rm -rf dist/
37 | git add --all
38 | git commit -m "New deployment"
39 | git push origin deployment/temp:gh-pages -f
40 | git checkout master
41 | echo
42 |
43 |
44 | echo "Cleaning up ..."
45 | echo
46 | git clean -f
47 | git branch -D deployment/temp 2>/dev/null
48 | echo
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ailing-planet",
3 | "version": "4.5.0",
4 | "description": "COVID-19 timelapse visualization",
5 | "homepage": "https://github.com/codingedward/ailing-planet",
6 | "scripts": {
7 | "build": "webpack --mode=development",
8 | "watch": "webpack --mode=development --watch",
9 | "watch:externalServer": "webpack --mode=development --watch --externalServer",
10 | "bundle": "npm ci && npm run watch",
11 | "bundle:externalServer": "npm ci && npm run watch:externalServer",
12 | "production": "cross-env NODE_ENV=production webpack --mode=production",
13 | "lint-sass": "sass-lint -v -q --format=compact",
14 | "lint-js": "eslint --ext .js src/js/"
15 | },
16 | "keywords": [
17 | "covid-19",
18 | "visualization"
19 | ],
20 | "author": "Edward Njoroge",
21 | "license": "MIT",
22 | "repository": {
23 | "type": "git",
24 | "url": "git@github.com:codingedward/ailing-planet.git"
25 | },
26 | "engines": {
27 | "node": "^10 || ^12 || >=14"
28 | },
29 | "dependencies": {
30 | "autoprefixer": "^10.0.0",
31 | "d3": "^6.2.0",
32 | "frs-hide-scrollbar": "^1.0.0",
33 | "hammerjs": "^2.0.8",
34 | "nouislider": "^14.6.2",
35 | "three": "^0.121.1",
36 | "three-geojson-geometry": "^1.1.0"
37 | },
38 | "devDependencies": {
39 | "@babel/core": "^7.11.6",
40 | "@babel/plugin-transform-runtime": "^7.11.5",
41 | "@babel/preset-env": "^7.11.5",
42 | "ajv": "^6.12.5",
43 | "babel-loader": "^8.1.0",
44 | "browser-sync": "^2.26.12",
45 | "browser-sync-webpack-plugin": "2.2.2",
46 | "clean-webpack-plugin": "^3.0.0",
47 | "copy-webpack-plugin": "^6.1.1",
48 | "cross-env": "^7.0.2",
49 | "css-loader": "^4.3.0",
50 | "cssnano": "^4.1.10",
51 | "eslint": "^7.10.0",
52 | "eslint-config-airbnb": "^18.2.0",
53 | "eslint-plugin-import": "^2.22.1",
54 | "eslint-plugin-jsx-a11y": "^6.3.1",
55 | "eslint-plugin-react": "^7.21.2",
56 | "eslint-plugin-react-hooks": "^4.1.2",
57 | "file-loader": "^6.1.0",
58 | "html-webpack-plugin": "^4.5.0",
59 | "imagemin-webpack-plugin": "^2.4.2",
60 | "mini-css-extract-plugin": "^0.11.2",
61 | "node-sass": "^4.14.1",
62 | "optimize-css-assets-webpack-plugin": "^5.0.4",
63 | "postcss": "^8.1.0",
64 | "postcss-loader": "^4.0.2",
65 | "sass": "^1.26.11",
66 | "sass-lint": "^1.13.1",
67 | "sass-loader": "^10.0.2",
68 | "style-loader": "^1.2.1",
69 | "terser-webpack-plugin": "^4.2.2",
70 | "url-loader": "^4.1.0",
71 | "webpack": "^4.44.2",
72 | "webpack-cdn-plugin": "^3.3.1",
73 | "webpack-cli": "^3.3.12"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer'),
4 | ],
5 | };
6 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/screenshot.png
--------------------------------------------------------------------------------
/src/data/.gitignore:
--------------------------------------------------------------------------------
1 | ./owid-covid-data.csv
2 |
--------------------------------------------------------------------------------
/src/data/countries-lat-lng.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "country":"AND",
4 | "lat":42.546245,
5 | "lng":1.601554
6 | },
7 | {
8 | "country":"ARE",
9 | "lat":23.424076,
10 | "lng":53.847818
11 | },
12 | {
13 | "country":"AFG",
14 | "lat":33.93911,
15 | "lng":67.709953
16 | },
17 | {
18 | "country":"ATG",
19 | "lat":17.060816,
20 | "lng":-61.796428
21 | },
22 | {
23 | "country":"AIA",
24 | "lat":18.220554,
25 | "lng":-63.068615
26 | },
27 | {
28 | "country":"ALB",
29 | "lat":41.153332,
30 | "lng":20.168331
31 | },
32 | {
33 | "country":"ARM",
34 | "lat":40.069099,
35 | "lng":45.038189
36 | },
37 | {
38 | "lat":12.226079,
39 | "lng":-69.060087
40 | },
41 | {
42 | "country":"AGO",
43 | "lat":-11.202692,
44 | "lng":17.873887
45 | },
46 | {
47 | "country":"ATA",
48 | "lat":-75.250973,
49 | "lng":-0.071389
50 | },
51 | {
52 | "country":"ARG",
53 | "lat":-38.416097,
54 | "lng":-63.616672
55 | },
56 | {
57 | "country":"ASM",
58 | "lat":-14.270972,
59 | "lng":-170.132217
60 | },
61 | {
62 | "country":"AUT",
63 | "lat":47.516231,
64 | "lng":14.550072
65 | },
66 | {
67 | "country":"AUS",
68 | "lat":-25.274398,
69 | "lng":133.775136
70 | },
71 | {
72 | "country":"ABW",
73 | "lat":12.52111,
74 | "lng":-69.968338
75 | },
76 | {
77 | "country":"AZE",
78 | "lat":40.143105,
79 | "lng":47.576927
80 | },
81 | {
82 | "country":"BIH",
83 | "lat":43.915886,
84 | "lng":17.679076
85 | },
86 | {
87 | "country":"BRB",
88 | "lat":13.193887,
89 | "lng":-59.543198
90 | },
91 | {
92 | "country":"BGD",
93 | "lat":23.684994,
94 | "lng":90.356331
95 | },
96 | {
97 | "country":"BEL",
98 | "lat":50.503887,
99 | "lng":4.469936
100 | },
101 | {
102 | "country":"BES",
103 | "lat":12.1784,
104 | "lng":68.2385
105 | },
106 | {
107 | "country":"BFA",
108 | "lat":12.238333,
109 | "lng":-1.561593
110 | },
111 | {
112 | "country":"BGR",
113 | "lat":42.733883,
114 | "lng":25.48583
115 | },
116 | {
117 | "country":"BHR",
118 | "lat":25.930414,
119 | "lng":50.637772
120 | },
121 | {
122 | "country":"BDI",
123 | "lat":-3.373056,
124 | "lng":29.918886
125 | },
126 | {
127 | "country":"BEN",
128 | "lat":9.30769,
129 | "lng":2.315834
130 | },
131 | {
132 | "country":"BMU",
133 | "lat":32.321384,
134 | "lng":-64.75737
135 | },
136 | {
137 | "country":"BRN",
138 | "lat":4.535277,
139 | "lng":114.727669
140 | },
141 | {
142 | "country":"BOL",
143 | "lat":-16.290154,
144 | "lng":-63.588653
145 | },
146 | {
147 | "country":"BRA",
148 | "lat":-14.235004,
149 | "lng":-51.92528
150 | },
151 | {
152 | "country":"BHS",
153 | "lat":25.03428,
154 | "lng":-77.39628
155 | },
156 | {
157 | "country":"BTN",
158 | "lat":27.514162,
159 | "lng":90.433601
160 | },
161 | {
162 | "country":"BVT",
163 | "lat":-54.423199,
164 | "lng":3.413194
165 | },
166 | {
167 | "country":"BWA",
168 | "lat":-22.328474,
169 | "lng":24.684866
170 | },
171 | {
172 | "country":"BLR",
173 | "lat":53.709807,
174 | "lng":27.953389
175 | },
176 | {
177 | "country":"BLZ",
178 | "lat":17.189877,
179 | "lng":-88.49765
180 | },
181 | {
182 | "country":"CAN",
183 | "lat":56.130366,
184 | "lng":-106.346771
185 | },
186 | {
187 | "country":"CCK",
188 | "lat":-12.164165,
189 | "lng":96.870956
190 | },
191 | {
192 | "country":"COD",
193 | "lat":-4.038333,
194 | "lng":21.758664
195 | },
196 | {
197 | "country":"CAF",
198 | "lat":6.611111,
199 | "lng":20.939444
200 | },
201 | {
202 | "country":"COG",
203 | "lat":-0.228021,
204 | "lng":15.827659
205 | },
206 | {
207 | "country":"CHE",
208 | "lat":46.818188,
209 | "lng":8.227512
210 | },
211 | {
212 | "country":"CIV",
213 | "lat":7.539989,
214 | "lng":-5.54708
215 | },
216 | {
217 | "country":"COK",
218 | "lat":-21.236736,
219 | "lng":-159.777671
220 | },
221 | {
222 | "country":"CHL",
223 | "lat":-35.675147,
224 | "lng":-71.542969
225 | },
226 | {
227 | "country":"CMR",
228 | "lat":7.369722,
229 | "lng":12.354722
230 | },
231 | {
232 | "country":"CHN",
233 | "lat":35.86166,
234 | "lng":104.195397
235 | },
236 | {
237 | "country":"COL",
238 | "lat":4.570868,
239 | "lng":-74.297333
240 | },
241 | {
242 | "country":"CRI",
243 | "lat":9.748917,
244 | "lng":-83.753428
245 | },
246 | {
247 | "country":"CUB",
248 | "lat":21.521757,
249 | "lng":-77.781167
250 | },
251 | {
252 | "country": "CUW",
253 | "lat": 12.1696,
254 | "lng": 68.9900
255 | },
256 | {
257 | "country":"CPV",
258 | "lat":16.002082,
259 | "lng":-24.013197
260 | },
261 | {
262 | "country":"CXR",
263 | "lat":-10.447525,
264 | "lng":105.690449
265 | },
266 | {
267 | "country":"CYP",
268 | "lat":35.126413,
269 | "lng":33.429859
270 | },
271 | {
272 | "country":"CZE",
273 | "lat":49.817492,
274 | "lng":15.472962
275 | },
276 | {
277 | "country":"DEU",
278 | "lat":51.165691,
279 | "lng":10.451526
280 | },
281 | {
282 | "country":"DJI",
283 | "lat":11.825138,
284 | "lng":42.590275
285 | },
286 | {
287 | "country":"DNK",
288 | "lat":56.26392,
289 | "lng":9.501785
290 | },
291 | {
292 | "country":"DMA",
293 | "lat":15.414999,
294 | "lng":-61.370976
295 | },
296 | {
297 | "country":"DOM",
298 | "lat":18.735693,
299 | "lng":-70.162651
300 | },
301 | {
302 | "country":"DZA",
303 | "lat":28.033886,
304 | "lng":1.659626
305 | },
306 | {
307 | "country":"ECU",
308 | "lat":-1.831239,
309 | "lng":-78.183406
310 | },
311 | {
312 | "country":"EST",
313 | "lat":58.595272,
314 | "lng":25.013607
315 | },
316 | {
317 | "country":"EGY",
318 | "lat":26.820553,
319 | "lng":30.802498
320 | },
321 | {
322 | "country":"ESH",
323 | "lat":24.215527,
324 | "lng":-12.885834
325 | },
326 | {
327 | "country":"ERI",
328 | "lat":15.179384,
329 | "lng":39.782334
330 | },
331 | {
332 | "country":"ESP",
333 | "lat":40.463667,
334 | "lng":-3.74922
335 | },
336 | {
337 | "country":"ETH",
338 | "lat":9.145,
339 | "lng":40.489673
340 | },
341 | {
342 | "country":"FIN",
343 | "lat":61.92411,
344 | "lng":25.748151
345 | },
346 | {
347 | "country":"FJI",
348 | "lat":-16.578193,
349 | "lng":179.414413
350 | },
351 | {
352 | "country":"FLK",
353 | "lat":-51.796253,
354 | "lng":-59.523613
355 | },
356 | {
357 | "country":"FSM",
358 | "lat":7.425554,
359 | "lng":150.550812
360 | },
361 | {
362 | "country":"FRO",
363 | "lat":61.892635,
364 | "lng":-6.911806
365 | },
366 | {
367 | "country":"FRA",
368 | "lat":46.227638,
369 | "lng":2.213749
370 | },
371 | {
372 | "country":"GAB",
373 | "lat":-0.803689,
374 | "lng":11.609444
375 | },
376 | {
377 | "country":"GBR",
378 | "lat":55.378051,
379 | "lng":-3.435973
380 | },
381 | {
382 | "country":"GRD",
383 | "lat":12.262776,
384 | "lng":-61.604171
385 | },
386 | {
387 | "country":"GEO",
388 | "lat":42.315407,
389 | "lng":43.356892
390 | },
391 | {
392 | "country":"GUF",
393 | "lat":3.933889,
394 | "lng":-53.125782
395 | },
396 | {
397 | "country":"GGY",
398 | "lat":49.465691,
399 | "lng":-2.585278
400 | },
401 | {
402 | "country":"GHA",
403 | "lat":7.946527,
404 | "lng":-1.023194
405 | },
406 | {
407 | "country":"GIB",
408 | "lat":36.137741,
409 | "lng":-5.345374
410 | },
411 | {
412 | "country":"GRL",
413 | "lat":71.706936,
414 | "lng":-42.604303
415 | },
416 | {
417 | "country":"GMB",
418 | "lat":13.443182,
419 | "lng":-15.310139
420 | },
421 | {
422 | "country":"GIN",
423 | "lat":9.945587,
424 | "lng":-9.696645
425 | },
426 | {
427 | "country":"GLP",
428 | "lat":16.995971,
429 | "lng":-62.067641
430 | },
431 | {
432 | "country":"GNQ",
433 | "lat":1.650801,
434 | "lng":10.267895
435 | },
436 | {
437 | "country":"GRC",
438 | "lat":39.074208,
439 | "lng":21.824312
440 | },
441 | {
442 | "country":"SGS",
443 | "lat":-54.429579,
444 | "lng":-36.587909
445 | },
446 | {
447 | "country":"SXM",
448 | "lat": 18.0425,
449 | "lng": 63.0548
450 | },
451 | {
452 | "country":"GTM",
453 | "lat":15.783471,
454 | "lng":-90.230759
455 | },
456 | {
457 | "country":"GUM",
458 | "lat":13.444304,
459 | "lng":144.793731
460 | },
461 | {
462 | "country":"GNB",
463 | "lat":11.803749,
464 | "lng":-15.180413
465 | },
466 | {
467 | "country":"GUY",
468 | "lat":4.860416,
469 | "lng":-58.93018
470 | },
471 | {
472 | "lat":31.354676,
473 | "lng":34.308825
474 | },
475 | {
476 | "country":"HKG",
477 | "lat":22.396428,
478 | "lng":114.109497
479 | },
480 | {
481 | "country":"HMD",
482 | "lat":-53.08181,
483 | "lng":73.504158
484 | },
485 | {
486 | "country":"HND",
487 | "lat":15.199999,
488 | "lng":-86.241905
489 | },
490 | {
491 | "country":"HRV",
492 | "lat":45.1,
493 | "lng":15.2
494 | },
495 | {
496 | "country":"HTI",
497 | "lat":18.971187,
498 | "lng":-72.285215
499 | },
500 | {
501 | "country":"HUN",
502 | "lat":47.162494,
503 | "lng":19.503304
504 | },
505 | {
506 | "country":"IDN",
507 | "lat":-0.789275,
508 | "lng":113.921327
509 | },
510 | {
511 | "country":"IRL",
512 | "lat":53.41291,
513 | "lng":-8.24389
514 | },
515 | {
516 | "country":"ISR",
517 | "lat":31.046051,
518 | "lng":34.851612
519 | },
520 | {
521 | "country":"IMN",
522 | "lat":54.236107,
523 | "lng":-4.548056
524 | },
525 | {
526 | "country":"IND",
527 | "lat":20.593684,
528 | "lng":78.96288
529 | },
530 | {
531 | "country":"IOT",
532 | "lat":-6.343194,
533 | "lng":71.876519
534 | },
535 | {
536 | "country":"IRQ",
537 | "lat":33.223191,
538 | "lng":43.679291
539 | },
540 | {
541 | "country":"IRN",
542 | "lat":32.427908,
543 | "lng":53.688046
544 | },
545 | {
546 | "country":"ISL",
547 | "lat":64.963051,
548 | "lng":-19.020835
549 | },
550 | {
551 | "country":"ITA",
552 | "lat":41.87194,
553 | "lng":12.56738
554 | },
555 | {
556 | "country":"JEY",
557 | "lat":49.214439,
558 | "lng":-2.13125
559 | },
560 | {
561 | "country":"JAM",
562 | "lat":18.109581,
563 | "lng":-77.297508
564 | },
565 | {
566 | "country":"JOR",
567 | "lat":30.585164,
568 | "lng":36.238414
569 | },
570 | {
571 | "country":"JPN",
572 | "lat":36.204824,
573 | "lng":138.252924
574 | },
575 | {
576 | "country":"KEN",
577 | "lat":-0.023559,
578 | "lng":37.906193
579 | },
580 | {
581 | "country":"KGZ",
582 | "lat":41.20438,
583 | "lng":74.766098
584 | },
585 | {
586 | "country":"KHM",
587 | "lat":12.565679,
588 | "lng":104.990963
589 | },
590 | {
591 | "country":"KIR",
592 | "lat":-3.370417,
593 | "lng":-168.734039
594 | },
595 | {
596 | "country":"COM",
597 | "lat":-11.875001,
598 | "lng":43.872219
599 | },
600 | {
601 | "country":"KNA",
602 | "lat":17.357822,
603 | "lng":-62.782998
604 | },
605 | {
606 | "country":"PRK",
607 | "lat":40.339852,
608 | "lng":127.510093
609 | },
610 | {
611 | "country": "PSE",
612 | "lat":31.9522,
613 | "lng":35.2332
614 | },
615 | {
616 | "country":"KOR",
617 | "lat":35.907757,
618 | "lng":127.766922
619 | },
620 | {
621 | "country":"KWT",
622 | "lat":29.31166,
623 | "lng":47.481766
624 | },
625 | {
626 | "country":"CYM",
627 | "lat":19.513469,
628 | "lng":-80.566956
629 | },
630 | {
631 | "country":"KAZ",
632 | "lat":48.019573,
633 | "lng":66.923684
634 | },
635 | {
636 | "country":"LAO",
637 | "lat":19.85627,
638 | "lng":102.495496
639 | },
640 | {
641 | "country":"LBN",
642 | "lat":33.854721,
643 | "lng":35.862285
644 | },
645 | {
646 | "country":"LCA",
647 | "lat":13.909444,
648 | "lng":-60.978893
649 | },
650 | {
651 | "country":"LIE",
652 | "lat":47.166,
653 | "lng":9.555373
654 | },
655 | {
656 | "country":"LKA",
657 | "lat":7.873054,
658 | "lng":80.771797
659 | },
660 | {
661 | "country":"LBR",
662 | "lat":6.428055,
663 | "lng":-9.429499
664 | },
665 | {
666 | "country":"LSO",
667 | "lat":-29.609988,
668 | "lng":28.233608
669 | },
670 | {
671 | "country":"LTU",
672 | "lat":55.169438,
673 | "lng":23.881275
674 | },
675 | {
676 | "country":"LUX",
677 | "lat":49.815273,
678 | "lng":6.129583
679 | },
680 | {
681 | "country":"LVA",
682 | "lat":56.879635,
683 | "lng":24.603189
684 | },
685 | {
686 | "country":"LBY",
687 | "lat":26.3351,
688 | "lng":17.228331
689 | },
690 | {
691 | "country":"MAR",
692 | "lat":31.791702,
693 | "lng":-7.09262
694 | },
695 | {
696 | "country":"MCO",
697 | "lat":43.750298,
698 | "lng":7.412841
699 | },
700 | {
701 | "country":"MDA",
702 | "lat":47.411631,
703 | "lng":28.369885
704 | },
705 | {
706 | "country":"MNE",
707 | "lat":42.708678,
708 | "lng":19.37439
709 | },
710 | {
711 | "country":"MDG",
712 | "lat":-18.766947,
713 | "lng":46.869107
714 | },
715 | {
716 | "country":"MHL",
717 | "lat":7.131474,
718 | "lng":171.184478
719 | },
720 | {
721 | "country":"MKD",
722 | "lat":41.608635,
723 | "lng":21.745275
724 | },
725 | {
726 | "country":"MLI",
727 | "lat":17.570692,
728 | "lng":-3.996166
729 | },
730 | {
731 | "country":"MMR",
732 | "lat":21.913965,
733 | "lng":95.956223
734 | },
735 | {
736 | "country":"MNG",
737 | "lat":46.862496,
738 | "lng":103.846656
739 | },
740 | {
741 | "country":"MAC",
742 | "lat":22.198745,
743 | "lng":113.543873
744 | },
745 | {
746 | "country":"MNP",
747 | "lat":17.33083,
748 | "lng":145.38469
749 | },
750 | {
751 | "country":"MTQ",
752 | "lat":14.641528,
753 | "lng":-61.024174
754 | },
755 | {
756 | "country":"MRT",
757 | "lat":21.00789,
758 | "lng":-10.940835
759 | },
760 | {
761 | "country":"MSR",
762 | "lat":16.742498,
763 | "lng":-62.187366
764 | },
765 | {
766 | "country":"MLT",
767 | "lat":35.937496,
768 | "lng":14.375416
769 | },
770 | {
771 | "country":"MUS",
772 | "lat":-20.348404,
773 | "lng":57.552152
774 | },
775 | {
776 | "country":"MDV",
777 | "lat":3.202778,
778 | "lng":73.22068
779 | },
780 | {
781 | "country":"MWI",
782 | "lat":-13.254308,
783 | "lng":34.301525
784 | },
785 | {
786 | "country":"MEX",
787 | "lat":23.634501,
788 | "lng":-102.552784
789 | },
790 | {
791 | "country":"MYS",
792 | "lat":4.210484,
793 | "lng":101.975766
794 | },
795 | {
796 | "country":"MOZ",
797 | "lat":-18.665695,
798 | "lng":35.529562
799 | },
800 | {
801 | "country":"NAM",
802 | "lat":-22.95764,
803 | "lng":18.49041
804 | },
805 | {
806 | "country":"NCL",
807 | "lat":-20.904305,
808 | "lng":165.618042
809 | },
810 | {
811 | "country":"NER",
812 | "lat":17.607789,
813 | "lng":8.081666
814 | },
815 | {
816 | "country":"NFK",
817 | "lat":-29.040835,
818 | "lng":167.954712
819 | },
820 | {
821 | "country":"NGA",
822 | "lat":9.081999,
823 | "lng":8.675277
824 | },
825 | {
826 | "country":"NIC",
827 | "lat":12.865416,
828 | "lng":-85.207229
829 | },
830 | {
831 | "country":"NLD",
832 | "lat":52.132633,
833 | "lng":5.291266
834 | },
835 | {
836 | "country":"NOR",
837 | "lat":60.472024,
838 | "lng":8.468946
839 | },
840 | {
841 | "country":"NPL",
842 | "lat":28.394857,
843 | "lng":84.124008
844 | },
845 | {
846 | "country":"NRU",
847 | "lat":-0.522778,
848 | "lng":166.931503
849 | },
850 | {
851 | "country":"NIU",
852 | "lat":-19.054445,
853 | "lng":-169.867233
854 | },
855 | {
856 | "country":"NZL",
857 | "lat":-40.900557,
858 | "lng":174.885971
859 | },
860 | {
861 | "country":"OMN",
862 | "lat":21.512583,
863 | "lng":55.923255
864 | },
865 | {
866 | "country":"PAN",
867 | "lat":8.537981,
868 | "lng":-80.782127
869 | },
870 | {
871 | "country":"PER",
872 | "lat":-9.189967,
873 | "lng":-75.015152
874 | },
875 | {
876 | "country":"PYF",
877 | "lat":-17.679742,
878 | "lng":-149.406843
879 | },
880 | {
881 | "country":"PNG",
882 | "lat":-6.314993,
883 | "lng":143.95555
884 | },
885 | {
886 | "country":"PHL",
887 | "lat":12.879721,
888 | "lng":121.774017
889 | },
890 | {
891 | "country":"PAK",
892 | "lat":30.375321,
893 | "lng":69.345116
894 | },
895 | {
896 | "country":"POL",
897 | "lat":51.919438,
898 | "lng":19.145136
899 | },
900 | {
901 | "country":"SPM",
902 | "lat":46.941936,
903 | "lng":-56.27111
904 | },
905 | {
906 | "country":"PCN",
907 | "lat":-24.703615,
908 | "lng":-127.439308
909 | },
910 | {
911 | "country":"PRI",
912 | "lat":18.220833,
913 | "lng":-66.590149
914 | },
915 | {
916 | "lat":31.952162,
917 | "lng":35.233154
918 | },
919 | {
920 | "country":"PRT",
921 | "lat":39.399872,
922 | "lng":-8.224454
923 | },
924 | {
925 | "country":"PLW",
926 | "lat":7.51498,
927 | "lng":134.58252
928 | },
929 | {
930 | "country":"PRY",
931 | "lat":-23.442503,
932 | "lng":-58.443832
933 | },
934 | {
935 | "country":"QAT",
936 | "lat":25.354826,
937 | "lng":51.183884
938 | },
939 | {
940 | "country":"REU",
941 | "lat":-21.115141,
942 | "lng":55.536384
943 | },
944 | {
945 | "country":"ROU",
946 | "lat":45.943161,
947 | "lng":24.96676
948 | },
949 | {
950 | "country":"SRB",
951 | "lat":44.016521,
952 | "lng":21.005859
953 | },
954 | {
955 | "country":"RUS",
956 | "lat":61.52401,
957 | "lng":105.318756
958 | },
959 | {
960 | "country":"RWA",
961 | "lat":-1.940278,
962 | "lng":29.873888
963 | },
964 | {
965 | "country":"SAU",
966 | "lat":23.885942,
967 | "lng":45.079162
968 | },
969 | {
970 | "country":"SLB",
971 | "lat":-9.64571,
972 | "lng":160.156194
973 | },
974 | {
975 | "country":"SYC",
976 | "lat":-4.679574,
977 | "lng":55.491977
978 | },
979 | {
980 | "country":"SDN",
981 | "lat":12.862807,
982 | "lng":30.217636
983 | },
984 | {
985 | "country":"SSD",
986 | "lat":6.8770,
987 | "lng":31.3070
988 | },
989 | {
990 | "country":"SWE",
991 | "lat":60.128161,
992 | "lng":18.643501
993 | },
994 | {
995 | "country":"SGP",
996 | "lat":1.352083,
997 | "lng":103.819836
998 | },
999 | {
1000 | "lat":-24.143474,
1001 | "lng":-10.030696
1002 | },
1003 | {
1004 | "country":"SVN",
1005 | "lat":46.151241,
1006 | "lng":14.995463
1007 | },
1008 | {
1009 | "country":"SJM",
1010 | "lat":77.553604,
1011 | "lng":23.670272
1012 | },
1013 | {
1014 | "country":"SVK",
1015 | "lat":48.669026,
1016 | "lng":19.699024
1017 | },
1018 | {
1019 | "country":"SLE",
1020 | "lat":8.460555,
1021 | "lng":-11.779889
1022 | },
1023 | {
1024 | "country":"SMR",
1025 | "lat":43.94236,
1026 | "lng":12.457777
1027 | },
1028 | {
1029 | "country":"SEN",
1030 | "lat":14.497401,
1031 | "lng":-14.452362
1032 | },
1033 | {
1034 | "country":"SOM",
1035 | "lat":5.152149,
1036 | "lng":46.199616
1037 | },
1038 | {
1039 | "country":"SUR",
1040 | "lat":3.919305,
1041 | "lng":-56.027783
1042 | },
1043 | {
1044 | "country":"STP",
1045 | "lat":0.18636,
1046 | "lng":6.613081
1047 | },
1048 | {
1049 | "country":"SLV",
1050 | "lat":13.794185,
1051 | "lng":-88.89653
1052 | },
1053 | {
1054 | "country":"SYR",
1055 | "lat":34.802075,
1056 | "lng":38.996815
1057 | },
1058 | {
1059 | "country":"SWZ",
1060 | "lat":-26.522503,
1061 | "lng":31.465866
1062 | },
1063 | {
1064 | "country":"TCA",
1065 | "lat":21.694025,
1066 | "lng":-71.797928
1067 | },
1068 | {
1069 | "country":"TCD",
1070 | "lat":15.454166,
1071 | "lng":18.732207
1072 | },
1073 | {
1074 | "country":"ATF",
1075 | "lat":-49.280366,
1076 | "lng":69.348557
1077 | },
1078 | {
1079 | "country":"TGO",
1080 | "lat":8.619543,
1081 | "lng":0.824782
1082 | },
1083 | {
1084 | "country":"THA",
1085 | "lat":15.870032,
1086 | "lng":100.992541
1087 | },
1088 | {
1089 | "country":"TJK",
1090 | "lat":38.861034,
1091 | "lng":71.276093
1092 | },
1093 | {
1094 | "country":"TKL",
1095 | "lat":-8.967363,
1096 | "lng":-171.855881
1097 | },
1098 | {
1099 | "country":"TLS",
1100 | "lat":-8.874217,
1101 | "lng":125.727539
1102 | },
1103 | {
1104 | "country":"TKM",
1105 | "lat":38.969719,
1106 | "lng":59.556278
1107 | },
1108 | {
1109 | "country":"TUN",
1110 | "lat":33.886917,
1111 | "lng":9.537499
1112 | },
1113 | {
1114 | "country":"TON",
1115 | "lat":-21.178986,
1116 | "lng":-175.198242
1117 | },
1118 | {
1119 | "country":"TUR",
1120 | "lat":38.963745,
1121 | "lng":35.243322
1122 | },
1123 | {
1124 | "country":"TTO",
1125 | "lat":10.691803,
1126 | "lng":-61.222503
1127 | },
1128 | {
1129 | "country":"TUV",
1130 | "lat":-7.109535,
1131 | "lng":177.64933
1132 | },
1133 | {
1134 | "country":"TWN",
1135 | "lat":23.69781,
1136 | "lng":120.960515
1137 | },
1138 | {
1139 | "country": "TZA",
1140 | "lat":-6.369028,
1141 | "lng":34.888822
1142 | },
1143 | {
1144 | "country":"UKR",
1145 | "lat":48.379433,
1146 | "lng":31.16558
1147 | },
1148 | {
1149 | "country":"UGA",
1150 | "lat":1.373333,
1151 | "lng":32.290275
1152 | },
1153 | {
1154 | "country":"USA",
1155 | "lat":37.09024,
1156 | "lng":-95.712891
1157 | },
1158 | {
1159 | "country":"URY",
1160 | "lat":-32.522779,
1161 | "lng":-55.765835
1162 | },
1163 | {
1164 | "country":"UZB",
1165 | "lat":41.377491,
1166 | "lng":64.585262
1167 | },
1168 | {
1169 | "country":"VAT",
1170 | "lat":41.902916,
1171 | "lng":12.453389
1172 | },
1173 | {
1174 | "country":"VCT",
1175 | "lat":12.984305,
1176 | "lng":-61.287228
1177 | },
1178 | {
1179 | "country":"VEN",
1180 | "lat":6.42375,
1181 | "lng":-66.58973
1182 | },
1183 | {
1184 | "country":"VGB",
1185 | "lat":18.420695,
1186 | "lng":-64.639968
1187 | },
1188 | {
1189 | "country":"VIR",
1190 | "lat":18.335765,
1191 | "lng":-64.896335
1192 | },
1193 | {
1194 | "country":"VNM",
1195 | "lat":14.058324,
1196 | "lng":108.277199
1197 | },
1198 | {
1199 | "country":"VUT",
1200 | "lat":-15.376706,
1201 | "lng":166.959158
1202 | },
1203 | {
1204 | "country":"WLF",
1205 | "lat":-13.768752,
1206 | "lng":-177.156097
1207 | },
1208 | {
1209 | "country":"WSM",
1210 | "lat":-13.759029,
1211 | "lng":-172.104629
1212 | },
1213 | {
1214 | "country": "OWID_KOS",
1215 | "lat":42.602636,
1216 | "lng":20.902977
1217 | },
1218 | {
1219 | "country":"YEM",
1220 | "lat":15.552727,
1221 | "lng":48.516388
1222 | },
1223 | {
1224 | "country":"MYT",
1225 | "lat":-12.8275,
1226 | "lng":45.166244
1227 | },
1228 | {
1229 | "country":"ZAF",
1230 | "lat":-30.559482,
1231 | "lng":22.937506
1232 | },
1233 | {
1234 | "country":"ZMB",
1235 | "lat":-13.133897,
1236 | "lng":27.849332
1237 | },
1238 | {
1239 | "country":"ZWE",
1240 | "lat":-19.015438,
1241 | "lng":29.154857
1242 | }
1243 | ]
1244 |
--------------------------------------------------------------------------------
/src/data/data_cleaner.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: utf-8 -*-
3 | import csv
4 | import json
5 |
6 | from datetime import datetime
7 |
8 | # data columns...
9 | # iso_code, continent, location, date, total_cases, new_cases, ...
10 |
11 | cols_index = {
12 | 'iso_code': 0,
13 | 'location': 2,
14 | 'date': 3,
15 | 'total_cases': 4,
16 | 'total_deaths': 7,
17 | }
18 |
19 | if __name__ == '__main__':
20 | with open('countries-lat-lng.json', 'r') as fp:
21 | countries_lat_lng = json.load(fp)
22 | data_dict = {'data': {}, 'locations': {},
23 | 'meta': {'cases_index': 0, 'deaths_index': 1}}
24 | with open('owid-covid-data.csv', 'r') as fp:
25 | covid_data = csv.reader(fp)
26 | next(covid_data)
27 | covid_data = sorted(covid_data, key=lambda row: \
28 | datetime.strptime(row[cols_index['date']],
29 | '%Y-%m-%d'))
30 | previous_cases = {}
31 | previous_deaths = {}
32 | for (line, row) in enumerate(covid_data):
33 | iso_code = row[cols_index['iso_code']]
34 | if iso_code == '':
35 | continue
36 | date = row[cols_index['date']]
37 | for previous_iso_code in previous_cases.keys():
38 | country_data = \
39 | {previous_iso_code: [previous_cases[previous_iso_code],
40 | previous_deaths[previous_iso_code]]}
41 | if data_dict['data'].get(date) is None:
42 | data_dict['data'][date] = country_data
43 | else:
44 | data_dict['data'][date].update(country_data)
45 | location = row[cols_index['location']]
46 | total_cases = previous_cases.get(iso_code) or 0
47 | total_deaths = previous_deaths.get(iso_code) or 0
48 | try:
49 | total_cases = max(total_cases,
50 | int(float(row[cols_index['total_cases'
51 | ]])))
52 | except:
53 | pass
54 | try:
55 | total_deaths = max(total_deaths,
56 | int(float(row[cols_index['total_deaths'
57 | ]])))
58 | except:
59 | pass
60 | previous_cases[iso_code] = total_cases
61 | previous_deaths[iso_code] = total_deaths
62 | if iso_code != 'OWID_WRL':
63 | lat_lng = [x for x in countries_lat_lng if x.get('country')
64 | == iso_code]
65 | else:
66 | lat_lng = [{'lat': -1.0, 'lng': -1.0}]
67 | data_dict['locations'].update({iso_code: {'name': location,
68 | 'lat': lat_lng[0]['lat'], 'lng': lat_lng[0]['lng'
69 | ]}})
70 | country_data = {iso_code: [total_cases, total_deaths]}
71 | if data_dict['data'].get(date) is None:
72 | data_dict['data'][date] = country_data
73 | else:
74 | data_dict['data'][date].update(country_data)
75 | with open('data.json', 'w') as fp:
76 | json.dump(data_dict, fp, sort_keys=True)
77 |
--------------------------------------------------------------------------------
/src/data/generate.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "Fetching data..."
3 | curl https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/owid-covid-data.csv --output owid-covid-data.csv
4 | echo "Cleaning and generating JSON..."
5 | python3 ./data_cleaner.py
6 | rm owid-covid-data.csv
7 | echo "Done!"
8 |
--------------------------------------------------------------------------------
/src/images/body_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/body_background.png
--------------------------------------------------------------------------------
/src/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/favicon.ico
--------------------------------------------------------------------------------
/src/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/favicon.png
--------------------------------------------------------------------------------
/src/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/loading.gif
--------------------------------------------------------------------------------
/src/images/space_bk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/space_bk.png
--------------------------------------------------------------------------------
/src/images/space_dn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/space_dn.png
--------------------------------------------------------------------------------
/src/images/space_ft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/space_ft.png
--------------------------------------------------------------------------------
/src/images/space_lf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/space_lf.png
--------------------------------------------------------------------------------
/src/images/space_rt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/space_rt.png
--------------------------------------------------------------------------------
/src/images/space_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/space_up.png
--------------------------------------------------------------------------------
/src/images/world.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codingedward/ailing-planet/f9e0bf41a89951bec4a5e4f0a1b62a52c8e1ae1b/src/images/world.jpg
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ailing Planet
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Loading...
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
50 |
51 |
52 |
116 |
117 |
118 |
124 |
125 |
Tip: Click on a bar to focus on its country
126 |
127 |
128 |
129 |
130 |
Tip: You can click on a country to focus on its values.
131 |
132 |
133 |
134 |
135 | ×
136 | 1
137 |
138 |
139 |
140 |
141 |
142 |
143 |
154 |
155 |
156 | Cases
157 | Deaths
158 |
159 |
160 |
161 |
162 |
163 |
164 | ?
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
180 |
181 |
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | import { FRSHideScrollbar } from 'frs-hide-scrollbar';
2 |
3 | import globe from './globe';
4 | import stats from './stats';
5 | import player from './player';
6 | import search from './search';
7 | import isWebGLAvailable from './webgl-check';
8 | import '../scss/app.scss';
9 |
10 | let activeDataIndex = 1;
11 | const WORLD_ISO_CODE = 'OWID_WRL';
12 | const [contentElement] = document.getElementsByClassName('content');
13 | const [chartElement] = document.getElementsByClassName('chart');
14 | const [countryStatElement] = document.getElementsByClassName(
15 | 'country-stats',
16 | );
17 | const [controllerElement] = document.getElementsByClassName('controller');
18 |
19 | async function computeDataSets(networkData) {
20 | const { data, locations } = networkData;
21 | const locationIsoCodes = Object.keys(locations);
22 | const countryIsoCodes = locationIsoCodes.filter((key) => key !== WORLD_ISO_CODE);
23 | const sortedDates = Object.keys(data).sort((a, b) => new Date(a) - new Date(b));
24 | const cleanedData = sortedDates.reduce(
25 | (datesData, date) => ({
26 | ...datesData,
27 | [date]: locationIsoCodes.reduce(
28 | (dateData, isoCode) => ({
29 | ...dateData,
30 | [isoCode]: data[date][isoCode] || [0, 0],
31 | }),
32 | {},
33 | ),
34 | }),
35 | {},
36 | );
37 | const locationIsoCodeToNameMap = new Map(
38 | locationIsoCodes.map((isoCode) => [isoCode, locations[isoCode].name]),
39 | );
40 |
41 | await Promise.all(
42 | [0, 1].map((dataIndex) => {
43 | const statsData = sortedDates.reduce(
44 | (datesData, date) => [
45 | ...datesData,
46 | [
47 | new Date(`${date}T00:00:00Z`),
48 | new Map(
49 | locationIsoCodes.map((isoCode) => [
50 | isoCode,
51 | cleanedData[date][isoCode][dataIndex],
52 | ]),
53 | ),
54 | ],
55 | ],
56 | [],
57 | );
58 | const globeData = sortedDates.map((date) => {
59 | const max = countryIsoCodes.reduce(
60 | (maxValue, isoCode) => Math.max(maxValue, cleanedData[date][isoCode][dataIndex]),
61 | 1,
62 | );
63 | return countryIsoCodes
64 | .map((isoCode) => [
65 | locations[isoCode].lat,
66 | locations[isoCode].lng,
67 | cleanedData[date][isoCode][dataIndex] / max,
68 | ])
69 | .reduce((flatArr, arr) => [...flatArr, ...arr], []);
70 | });
71 | return {
72 | dataSetName: dataIndex === 0 ? 'Total Cases' : 'Total Deaths',
73 | statsData,
74 | globeData,
75 | };
76 | })
77 | .map((dataSet, dataIndex) => {
78 | const { dataSetName, globeData, statsData } = dataSet;
79 | return [
80 | globe.loadAnimationData({
81 | globeData,
82 | dataSetName,
83 | dataSetKey: dataIndex,
84 | }),
85 | stats.loadAnimationData({
86 | statsData,
87 | dataSetName,
88 | dataSetKey: dataIndex,
89 | locationIsoCodeToNameMap,
90 | }),
91 | ];
92 | })
93 | .reduce((flatArr, arr) => [...flatArr, ...arr], []),
94 | );
95 | }
96 |
97 | function setActiveDataSet(dataIndex) {
98 | player.pause();
99 | contentElement.classList.add('blurred');
100 | setTimeout(() => {
101 | activeDataIndex = dataIndex;
102 | globe.setActiveDataSet(dataIndex);
103 | stats.setActiveDataSet(dataIndex);
104 | document.body.style.backgroundImage = 'none';
105 | contentElement.classList.remove('blurred');
106 | chartElement.classList.remove('hidden');
107 | controllerElement.classList.remove('hidden');
108 | countryStatElement.classList.remove('hidden');
109 |
110 | player.play();
111 | }, 50);
112 | }
113 |
114 | function initialize() {
115 | const [dataSelectButton] = document.getElementsByClassName(
116 | 'dataset-select-button',
117 | );
118 | const [dataSelectList] = document.getElementsByClassName(
119 | 'dataset-select-list',
120 | );
121 | dataSelectButton.addEventListener(
122 | 'click',
123 | (e) => {
124 | e.stopPropagation();
125 | dataSelectList.classList.toggle('invisible');
126 | },
127 | false,
128 | );
129 | document.addEventListener('click', (e) => {
130 | if (
131 | !dataSelectList.classList.contains('invisible')
132 | && !e.target.classList.contains('dataset-select-list')
133 | ) {
134 | e.stopPropagation();
135 | dataSelectList.classList.toggle('invisible');
136 | }
137 | });
138 | const dataSelectItems = Array.from(
139 | document.getElementsByClassName('dataset-select-list-item'),
140 | );
141 | dataSelectItems.forEach((item) => {
142 | item.addEventListener(
143 | 'click',
144 | (e) => {
145 | const value = parseInt(e.target.getAttribute('data-value'), 10);
146 | if (value !== activeDataIndex) {
147 | activeDataIndex = value;
148 | setActiveDataSet(value);
149 | dataSelectItems.forEach((elem) => {
150 | elem.classList.toggle('active');
151 | });
152 | }
153 | },
154 | false,
155 | );
156 | });
157 | globe.initialize();
158 | stats.initialize();
159 | player.initialize();
160 | search.initialize();
161 | player.addItem(globe);
162 | player.addItem(stats);
163 | }
164 |
165 | window.addEventListener('load', () => {
166 | if (!isWebGLAvailable()) {
167 | console.log(
168 | "%c Here's a nickel kid, go buy yourself a decent browser",
169 | 'color: #00ff00; font-size: 20px; font-weight: bold; font-family: monospace',
170 | );
171 | } else {
172 | window.fetch('./data.json')
173 | .then((res) => res.json())
174 | .then(async (res) => {
175 | initialize();
176 | await computeDataSets(res);
177 | const interval = setInterval(() => {
178 | if (globe.isReady()) {
179 | const [loadingElement] = document.getElementsByClassName('loading');
180 | loadingElement.parentNode.removeChild(loadingElement);
181 | contentElement.classList.remove('hidden');
182 | clearInterval(interval);
183 | setActiveDataSet(0);
184 | }
185 | }, 500);
186 | });
187 |
188 | console.log(
189 | `%c
190 | @@@@@@ @@@ @@@ @@@ @@@ @@@ @@@@@@@
191 | @@! @@@ @@! @@! @@! @@!@!@@@ !@@
192 | @!@!@!@! !!@ @!! !!@ @!@@!!@! !@! @!@!@
193 | !!: !!! !!: !!: !!: !!: !!! :!! !!:
194 | : : : : : ::.: : : :: : :: :: :
195 |
196 |
197 | @@@@@@@ @@@ @@@@@@ @@@ @@@ @@@@@@@@ @@@@@@@
198 | @@! @@@ @@! @@! @@@ @@!@!@@@ @@! @!!
199 | @!@@!@! @!! @!@!@!@! @!@@!!@! @!!!:! @!!
200 | !!: !!: !!: !!! !!: !!! !!: !!:
201 | : : ::.: : : : : :: : : :: :: :
202 |
203 | By Edward Njoroge
204 |
205 | Find the source code here: https://github.com/codingedward/ailing-planet
206 | `,
207 | 'color: #ff0000; font-size:12px;',
208 | );
209 | }
210 | });
211 |
--------------------------------------------------------------------------------
/src/js/country-iso-to-latlng.js:
--------------------------------------------------------------------------------
1 | export default new Map([
2 | [
3 | 'AND',
4 | {
5 | lat: 42.546245,
6 | lng: 1.601554,
7 | },
8 | ],
9 | [
10 | 'ARE',
11 | {
12 | lat: 23.424076,
13 | lng: 53.847818,
14 | },
15 | ],
16 | [
17 | 'AFG',
18 | {
19 | lat: 33.93911,
20 | lng: 67.709953,
21 | },
22 | ],
23 | [
24 | 'ATG',
25 | {
26 | lat: 17.060816,
27 | lng: -61.796428,
28 | },
29 | ],
30 | [
31 | 'AIA',
32 | {
33 | lat: 18.220554,
34 | lng: -63.068615,
35 | },
36 | ],
37 | [
38 | 'ALB',
39 | {
40 | lat: 41.153332,
41 | lng: 20.168331,
42 | },
43 | ],
44 | [
45 | 'ARM',
46 | {
47 | lat: 40.069099,
48 | lng: 45.038189,
49 | },
50 | ],
51 | [
52 | 'AGO',
53 | {
54 | lat: -11.202692,
55 | lng: 17.873887,
56 | },
57 | ],
58 | [
59 | 'ATA',
60 | {
61 | lat: -75.250973,
62 | lng: -0.071389,
63 | },
64 | ],
65 | [
66 | 'ARG',
67 | {
68 | lat: -38.416097,
69 | lng: -63.616672,
70 | },
71 | ],
72 | [
73 | 'ASM',
74 | {
75 | lat: -14.270972,
76 | lng: -170.132217,
77 | },
78 | ],
79 | [
80 | 'AUT',
81 | {
82 | lat: 47.516231,
83 | lng: 14.550072,
84 | },
85 | ],
86 | [
87 | 'AUS',
88 | {
89 | lat: -25.274398,
90 | lng: 133.775136,
91 | },
92 | ],
93 | [
94 | 'ABW',
95 | {
96 | lat: 12.52111,
97 | lng: -69.968338,
98 | },
99 | ],
100 | [
101 | 'AZE',
102 | {
103 | lat: 40.143105,
104 | lng: 47.576927,
105 | },
106 | ],
107 | [
108 | 'BIH',
109 | {
110 | lat: 43.915886,
111 | lng: 17.679076,
112 | },
113 | ],
114 | [
115 | 'BRB',
116 | {
117 | lat: 13.193887,
118 | lng: -59.543198,
119 | },
120 | ],
121 | [
122 | 'BGD',
123 | {
124 | lat: 23.684994,
125 | lng: 90.356331,
126 | },
127 | ],
128 | [
129 | 'BEL',
130 | {
131 | lat: 50.503887,
132 | lng: 4.469936,
133 | },
134 | ],
135 | [
136 | 'BES',
137 | {
138 | lat: 12.1784,
139 | lng: 68.2385,
140 | },
141 | ],
142 | [
143 | 'BFA',
144 | {
145 | lat: 12.238333,
146 | lng: -1.561593,
147 | },
148 | ],
149 | [
150 | 'BGR',
151 | {
152 | lat: 42.733883,
153 | lng: 25.48583,
154 | },
155 | ],
156 | [
157 | 'BHR',
158 | {
159 | lat: 25.930414,
160 | lng: 50.637772,
161 | },
162 | ],
163 | [
164 | 'BDI',
165 | {
166 | lat: -3.373056,
167 | lng: 29.918886,
168 | },
169 | ],
170 | [
171 | 'BEN',
172 | {
173 | lat: 9.30769,
174 | lng: 2.315834,
175 | },
176 | ],
177 | [
178 | 'BMU',
179 | {
180 | lat: 32.321384,
181 | lng: -64.75737,
182 | },
183 | ],
184 | [
185 | 'BRN',
186 | {
187 | lat: 4.535277,
188 | lng: 114.727669,
189 | },
190 | ],
191 | [
192 | 'BOL',
193 | {
194 | lat: -16.290154,
195 | lng: -63.588653,
196 | },
197 | ],
198 | [
199 | 'BRA',
200 | {
201 | lat: -14.235004,
202 | lng: -51.92528,
203 | },
204 | ],
205 | [
206 | 'BHS',
207 | {
208 | lat: 25.03428,
209 | lng: -77.39628,
210 | },
211 | ],
212 | [
213 | 'BTN',
214 | {
215 | lat: 27.514162,
216 | lng: 90.433601,
217 | },
218 | ],
219 | [
220 | 'BVT',
221 | {
222 | lat: -54.423199,
223 | lng: 3.413194,
224 | },
225 | ],
226 | [
227 | 'BWA',
228 | {
229 | lat: -22.328474,
230 | lng: 24.684866,
231 | },
232 | ],
233 | [
234 | 'BLR',
235 | {
236 | lat: 53.709807,
237 | lng: 27.953389,
238 | },
239 | ],
240 | [
241 | 'BLZ',
242 | {
243 | lat: 17.189877,
244 | lng: -88.49765,
245 | },
246 | ],
247 | [
248 | 'CAN',
249 | {
250 | lat: 56.130366,
251 | lng: -106.346771,
252 | },
253 | ],
254 | [
255 | 'CCK',
256 | {
257 | lat: -12.164165,
258 | lng: 96.870956,
259 | },
260 | ],
261 | [
262 | 'COD',
263 | {
264 | lat: -4.038333,
265 | lng: 21.758664,
266 | },
267 | ],
268 | [
269 | 'CAF',
270 | {
271 | lat: 6.611111,
272 | lng: 20.939444,
273 | },
274 | ],
275 | [
276 | 'COG',
277 | {
278 | lat: -0.228021,
279 | lng: 15.827659,
280 | },
281 | ],
282 | [
283 | 'CHE',
284 | {
285 | lat: 46.818188,
286 | lng: 8.227512,
287 | },
288 | ],
289 | [
290 | 'CIV',
291 | {
292 | lat: 7.539989,
293 | lng: -5.54708,
294 | },
295 | ],
296 | [
297 | 'COK',
298 | {
299 | lat: -21.236736,
300 | lng: -159.777671,
301 | },
302 | ],
303 | [
304 | 'CHL',
305 | {
306 | lat: -35.675147,
307 | lng: -71.542969,
308 | },
309 | ],
310 | [
311 | 'CMR',
312 | {
313 | lat: 7.369722,
314 | lng: 12.354722,
315 | },
316 | ],
317 | [
318 | 'CHN',
319 | {
320 | lat: 35.86166,
321 | lng: 104.195397,
322 | },
323 | ],
324 | [
325 | 'COL',
326 | {
327 | lat: 4.570868,
328 | lng: -74.297333,
329 | },
330 | ],
331 | [
332 | 'CRI',
333 | {
334 | lat: 9.748917,
335 | lng: -83.753428,
336 | },
337 | ],
338 | [
339 | 'CUB',
340 | {
341 | lat: 21.521757,
342 | lng: -77.781167,
343 | },
344 | ],
345 | [
346 | 'CUW',
347 | {
348 | lat: 12.1696,
349 | lng: 68.99,
350 | },
351 | ],
352 | [
353 | 'CPV',
354 | {
355 | lat: 16.002082,
356 | lng: -24.013197,
357 | },
358 | ],
359 | [
360 | 'CXR',
361 | {
362 | lat: -10.447525,
363 | lng: 105.690449,
364 | },
365 | ],
366 | [
367 | 'CYP',
368 | {
369 | lat: 35.126413,
370 | lng: 33.429859,
371 | },
372 | ],
373 | [
374 | 'CZE',
375 | {
376 | lat: 49.817492,
377 | lng: 15.472962,
378 | },
379 | ],
380 | [
381 | 'DEU',
382 | {
383 | lat: 51.165691,
384 | lng: 10.451526,
385 | },
386 | ],
387 | [
388 | 'DJI',
389 | {
390 | lat: 11.825138,
391 | lng: 42.590275,
392 | },
393 | ],
394 | [
395 | 'DNK',
396 | {
397 | lat: 56.26392,
398 | lng: 9.501785,
399 | },
400 | ],
401 | [
402 | 'DMA',
403 | {
404 | lat: 15.414999,
405 | lng: -61.370976,
406 | },
407 | ],
408 | [
409 | 'DOM',
410 | {
411 | lat: 18.735693,
412 | lng: -70.162651,
413 | },
414 | ],
415 | [
416 | 'DZA',
417 | {
418 | lat: 28.033886,
419 | lng: 1.659626,
420 | },
421 | ],
422 | [
423 | 'ECU',
424 | {
425 | lat: -1.831239,
426 | lng: -78.183406,
427 | },
428 | ],
429 | [
430 | 'EST',
431 | {
432 | lat: 58.595272,
433 | lng: 25.013607,
434 | },
435 | ],
436 | [
437 | 'EGY',
438 | {
439 | lat: 26.820553,
440 | lng: 30.802498,
441 | },
442 | ],
443 | [
444 | 'ESH',
445 | {
446 | lat: 24.215527,
447 | lng: -12.885834,
448 | },
449 | ],
450 | [
451 | 'ERI',
452 | {
453 | lat: 15.179384,
454 | lng: 39.782334,
455 | },
456 | ],
457 | [
458 | 'ESP',
459 | {
460 | lat: 40.463667,
461 | lng: -3.74922,
462 | },
463 | ],
464 | [
465 | 'ETH',
466 | {
467 | lat: 9.145,
468 | lng: 40.489673,
469 | },
470 | ],
471 | [
472 | 'FIN',
473 | {
474 | lat: 61.92411,
475 | lng: 25.748151,
476 | },
477 | ],
478 | [
479 | 'FJI',
480 | {
481 | lat: -16.578193,
482 | lng: 179.414413,
483 | },
484 | ],
485 | [
486 | 'FLK',
487 | {
488 | lat: -51.796253,
489 | lng: -59.523613,
490 | },
491 | ],
492 | [
493 | 'FSM',
494 | {
495 | lat: 7.425554,
496 | lng: 150.550812,
497 | },
498 | ],
499 | [
500 | 'FRO',
501 | {
502 | lat: 61.892635,
503 | lng: -6.911806,
504 | },
505 | ],
506 | [
507 | 'FRA',
508 | {
509 | lat: 46.227638,
510 | lng: 2.213749,
511 | },
512 | ],
513 | [
514 | 'GAB',
515 | {
516 | lat: -0.803689,
517 | lng: 11.609444,
518 | },
519 | ],
520 | [
521 | 'GBR',
522 | {
523 | lat: 55.378051,
524 | lng: -3.435973,
525 | },
526 | ],
527 | [
528 | 'GRD',
529 | {
530 | lat: 12.262776,
531 | lng: -61.604171,
532 | },
533 | ],
534 | [
535 | 'GEO',
536 | {
537 | lat: 42.315407,
538 | lng: 43.356892,
539 | },
540 | ],
541 | [
542 | 'GUF',
543 | {
544 | lat: 3.933889,
545 | lng: -53.125782,
546 | },
547 | ],
548 | [
549 | 'GGY',
550 | {
551 | lat: 49.465691,
552 | lng: -2.585278,
553 | },
554 | ],
555 | [
556 | 'GHA',
557 | {
558 | lat: 7.946527,
559 | lng: -1.023194,
560 | },
561 | ],
562 | [
563 | 'GIB',
564 | {
565 | lat: 36.137741,
566 | lng: -5.345374,
567 | },
568 | ],
569 | [
570 | 'GRL',
571 | {
572 | lat: 71.706936,
573 | lng: -42.604303,
574 | },
575 | ],
576 | [
577 | 'GMB',
578 | {
579 | lat: 13.443182,
580 | lng: -15.310139,
581 | },
582 | ],
583 | [
584 | 'GIN',
585 | {
586 | lat: 9.945587,
587 | lng: -9.696645,
588 | },
589 | ],
590 | [
591 | 'GLP',
592 | {
593 | lat: 16.995971,
594 | lng: -62.067641,
595 | },
596 | ],
597 | [
598 | 'GNQ',
599 | {
600 | lat: 1.650801,
601 | lng: 10.267895,
602 | },
603 | ],
604 | [
605 | 'GRC',
606 | {
607 | lat: 39.074208,
608 | lng: 21.824312,
609 | },
610 | ],
611 | [
612 | 'SGS',
613 | {
614 | lat: -54.429579,
615 | lng: -36.587909,
616 | },
617 | ],
618 | [
619 | 'SXM',
620 | {
621 | lat: 18.0425,
622 | lng: 63.0548,
623 | },
624 | ],
625 | [
626 | 'GTM',
627 | {
628 | lat: 15.783471,
629 | lng: -90.230759,
630 | },
631 | ],
632 | [
633 | 'GUM',
634 | {
635 | lat: 13.444304,
636 | lng: 144.793731,
637 | },
638 | ],
639 | [
640 | 'GNB',
641 | {
642 | lat: 11.803749,
643 | lng: -15.180413,
644 | },
645 | ],
646 | [
647 | 'GUY',
648 | {
649 | lat: 4.860416,
650 | lng: -58.93018,
651 | },
652 | ],
653 | [
654 | 'HKG',
655 | {
656 | lat: 22.396428,
657 | lng: 114.109497,
658 | },
659 | ],
660 | [
661 | 'HMD',
662 | {
663 | lat: -53.08181,
664 | lng: 73.504158,
665 | },
666 | ],
667 | [
668 | 'HND',
669 | {
670 | lat: 15.199999,
671 | lng: -86.241905,
672 | },
673 | ],
674 | [
675 | 'HRV',
676 | {
677 | lat: 45.1,
678 | lng: 15.2,
679 | },
680 | ],
681 | [
682 | 'HTI',
683 | {
684 | lat: 18.971187,
685 | lng: -72.285215,
686 | },
687 | ],
688 | [
689 | 'HUN',
690 | {
691 | lat: 47.162494,
692 | lng: 19.503304,
693 | },
694 | ],
695 | [
696 | 'IDN',
697 | {
698 | lat: -0.789275,
699 | lng: 113.921327,
700 | },
701 | ],
702 | [
703 | 'IRL',
704 | {
705 | lat: 53.41291,
706 | lng: -8.24389,
707 | },
708 | ],
709 | [
710 | 'ISR',
711 | {
712 | lat: 31.046051,
713 | lng: 34.851612,
714 | },
715 | ],
716 | [
717 | 'IMN',
718 | {
719 | lat: 54.236107,
720 | lng: -4.548056,
721 | },
722 | ],
723 | [
724 | 'IND',
725 | {
726 | lat: 20.593684,
727 | lng: 78.96288,
728 | },
729 | ],
730 | [
731 | 'IOT',
732 | {
733 | lat: -6.343194,
734 | lng: 71.876519,
735 | },
736 | ],
737 | [
738 | 'IRQ',
739 | {
740 | lat: 33.223191,
741 | lng: 43.679291,
742 | },
743 | ],
744 | [
745 | 'IRN',
746 | {
747 | lat: 32.427908,
748 | lng: 53.688046,
749 | },
750 | ],
751 | [
752 | 'ISL',
753 | {
754 | lat: 64.963051,
755 | lng: -19.020835,
756 | },
757 | ],
758 | [
759 | 'ITA',
760 | {
761 | lat: 41.87194,
762 | lng: 12.56738,
763 | },
764 | ],
765 | [
766 | 'JEY',
767 | {
768 | lat: 49.214439,
769 | lng: -2.13125,
770 | },
771 | ],
772 | [
773 | 'JAM',
774 | {
775 | lat: 18.109581,
776 | lng: -77.297508,
777 | },
778 | ],
779 | [
780 | 'JOR',
781 | {
782 | lat: 30.585164,
783 | lng: 36.238414,
784 | },
785 | ],
786 | [
787 | 'JPN',
788 | {
789 | lat: 36.204824,
790 | lng: 138.252924,
791 | },
792 | ],
793 | [
794 | 'KEN',
795 | {
796 | lat: -0.023559,
797 | lng: 37.906193,
798 | },
799 | ],
800 | [
801 | 'KGZ',
802 | {
803 | lat: 41.20438,
804 | lng: 74.766098,
805 | },
806 | ],
807 | [
808 | 'KHM',
809 | {
810 | lat: 12.565679,
811 | lng: 104.990963,
812 | },
813 | ],
814 | [
815 | 'KIR',
816 | {
817 | lat: -3.370417,
818 | lng: -168.734039,
819 | },
820 | ],
821 | [
822 | 'COM',
823 | {
824 | lat: -11.875001,
825 | lng: 43.872219,
826 | },
827 | ],
828 | [
829 | 'KNA',
830 | {
831 | lat: 17.357822,
832 | lng: -62.782998,
833 | },
834 | ],
835 | [
836 | 'PRK',
837 | {
838 | lat: 40.339852,
839 | lng: 127.510093,
840 | },
841 | ],
842 | [
843 | 'PSE',
844 | {
845 | lat: 31.9522,
846 | lng: 35.2332,
847 | },
848 | ],
849 | [
850 | 'KOR',
851 | {
852 | lat: 35.907757,
853 | lng: 127.766922,
854 | },
855 | ],
856 | [
857 | 'KWT',
858 | {
859 | lat: 29.31166,
860 | lng: 47.481766,
861 | },
862 | ],
863 | [
864 | 'CYM',
865 | {
866 | lat: 19.513469,
867 | lng: -80.566956,
868 | },
869 | ],
870 | [
871 | 'KAZ',
872 | {
873 | lat: 48.019573,
874 | lng: 66.923684,
875 | },
876 | ],
877 | [
878 | 'LAO',
879 | {
880 | lat: 19.85627,
881 | lng: 102.495496,
882 | },
883 | ],
884 | [
885 | 'LBN',
886 | {
887 | lat: 33.854721,
888 | lng: 35.862285,
889 | },
890 | ],
891 | [
892 | 'LCA',
893 | {
894 | lat: 13.909444,
895 | lng: -60.978893,
896 | },
897 | ],
898 | [
899 | 'LIE',
900 | {
901 | lat: 47.166,
902 | lng: 9.555373,
903 | },
904 | ],
905 | [
906 | 'LKA',
907 | {
908 | lat: 7.873054,
909 | lng: 80.771797,
910 | },
911 | ],
912 | [
913 | 'LBR',
914 | {
915 | lat: 6.428055,
916 | lng: -9.429499,
917 | },
918 | ],
919 | [
920 | 'LSO',
921 | {
922 | lat: -29.609988,
923 | lng: 28.233608,
924 | },
925 | ],
926 | [
927 | 'LTU',
928 | {
929 | lat: 55.169438,
930 | lng: 23.881275,
931 | },
932 | ],
933 | [
934 | 'LUX',
935 | {
936 | lat: 49.815273,
937 | lng: 6.129583,
938 | },
939 | ],
940 | [
941 | 'LVA',
942 | {
943 | lat: 56.879635,
944 | lng: 24.603189,
945 | },
946 | ],
947 | [
948 | 'LBY',
949 | {
950 | lat: 26.3351,
951 | lng: 17.228331,
952 | },
953 | ],
954 | [
955 | 'MAR',
956 | {
957 | lat: 31.791702,
958 | lng: -7.09262,
959 | },
960 | ],
961 | [
962 | 'MCO',
963 | {
964 | lat: 43.750298,
965 | lng: 7.412841,
966 | },
967 | ],
968 | [
969 | 'MDA',
970 | {
971 | lat: 47.411631,
972 | lng: 28.369885,
973 | },
974 | ],
975 | [
976 | 'MNE',
977 | {
978 | lat: 42.708678,
979 | lng: 19.37439,
980 | },
981 | ],
982 | [
983 | 'MDG',
984 | {
985 | lat: -18.766947,
986 | lng: 46.869107,
987 | },
988 | ],
989 | [
990 | 'MHL',
991 | {
992 | lat: 7.131474,
993 | lng: 171.184478,
994 | },
995 | ],
996 | [
997 | 'MKD',
998 | {
999 | lat: 41.608635,
1000 | lng: 21.745275,
1001 | },
1002 | ],
1003 | [
1004 | 'MLI',
1005 | {
1006 | lat: 17.570692,
1007 | lng: -3.996166,
1008 | },
1009 | ],
1010 | [
1011 | 'MMR',
1012 | {
1013 | lat: 21.913965,
1014 | lng: 95.956223,
1015 | },
1016 | ],
1017 | [
1018 | 'MNG',
1019 | {
1020 | lat: 46.862496,
1021 | lng: 103.846656,
1022 | },
1023 | ],
1024 | [
1025 | 'MAC',
1026 | {
1027 | lat: 22.198745,
1028 | lng: 113.543873,
1029 | },
1030 | ],
1031 | [
1032 | 'MNP',
1033 | {
1034 | lat: 17.33083,
1035 | lng: 145.38469,
1036 | },
1037 | ],
1038 | [
1039 | 'MTQ',
1040 | {
1041 | lat: 14.641528,
1042 | lng: -61.024174,
1043 | },
1044 | ],
1045 | [
1046 | 'MRT',
1047 | {
1048 | lat: 21.00789,
1049 | lng: -10.940835,
1050 | },
1051 | ],
1052 | [
1053 | 'MSR',
1054 | {
1055 | lat: 16.742498,
1056 | lng: -62.187366,
1057 | },
1058 | ],
1059 | [
1060 | 'MLT',
1061 | {
1062 | lat: 35.937496,
1063 | lng: 14.375416,
1064 | },
1065 | ],
1066 | [
1067 | 'MUS',
1068 | {
1069 | lat: -20.348404,
1070 | lng: 57.552152,
1071 | },
1072 | ],
1073 | [
1074 | 'MDV',
1075 | {
1076 | lat: 3.202778,
1077 | lng: 73.22068,
1078 | },
1079 | ],
1080 | [
1081 | 'MWI',
1082 | {
1083 | lat: -13.254308,
1084 | lng: 34.301525,
1085 | },
1086 | ],
1087 | [
1088 | 'MEX',
1089 | {
1090 | lat: 23.634501,
1091 | lng: -102.552784,
1092 | },
1093 | ],
1094 | [
1095 | 'MYS',
1096 | {
1097 | lat: 4.210484,
1098 | lng: 101.975766,
1099 | },
1100 | ],
1101 | [
1102 | 'MOZ',
1103 | {
1104 | lat: -18.665695,
1105 | lng: 35.529562,
1106 | },
1107 | ],
1108 | [
1109 | 'NAM',
1110 | {
1111 | lat: -22.95764,
1112 | lng: 18.49041,
1113 | },
1114 | ],
1115 | [
1116 | 'NCL',
1117 | {
1118 | lat: -20.904305,
1119 | lng: 165.618042,
1120 | },
1121 | ],
1122 | [
1123 | 'NER',
1124 | {
1125 | lat: 17.607789,
1126 | lng: 8.081666,
1127 | },
1128 | ],
1129 | [
1130 | 'NFK',
1131 | {
1132 | lat: -29.040835,
1133 | lng: 167.954712,
1134 | },
1135 | ],
1136 | [
1137 | 'NGA',
1138 | {
1139 | lat: 9.081999,
1140 | lng: 8.675277,
1141 | },
1142 | ],
1143 | [
1144 | 'NIC',
1145 | {
1146 | lat: 12.865416,
1147 | lng: -85.207229,
1148 | },
1149 | ],
1150 | [
1151 | 'NLD',
1152 | {
1153 | lat: 52.132633,
1154 | lng: 5.291266,
1155 | },
1156 | ],
1157 | [
1158 | 'NOR',
1159 | {
1160 | lat: 60.472024,
1161 | lng: 8.468946,
1162 | },
1163 | ],
1164 | [
1165 | 'NPL',
1166 | {
1167 | lat: 28.394857,
1168 | lng: 84.124008,
1169 | },
1170 | ],
1171 | [
1172 | 'NRU',
1173 | {
1174 | lat: -0.522778,
1175 | lng: 166.931503,
1176 | },
1177 | ],
1178 | [
1179 | 'NIU',
1180 | {
1181 | lat: -19.054445,
1182 | lng: -169.867233,
1183 | },
1184 | ],
1185 | [
1186 | 'NZL',
1187 | {
1188 | lat: -40.900557,
1189 | lng: 174.885971,
1190 | },
1191 | ],
1192 | [
1193 | 'OMN',
1194 | {
1195 | lat: 21.512583,
1196 | lng: 55.923255,
1197 | },
1198 | ],
1199 | [
1200 | 'PAN',
1201 | {
1202 | lat: 8.537981,
1203 | lng: -80.782127,
1204 | },
1205 | ],
1206 | [
1207 | 'PER',
1208 | {
1209 | lat: -9.189967,
1210 | lng: -75.015152,
1211 | },
1212 | ],
1213 | [
1214 | 'PYF',
1215 | {
1216 | lat: -17.679742,
1217 | lng: -149.406843,
1218 | },
1219 | ],
1220 | [
1221 | 'PNG',
1222 | {
1223 | lat: -6.314993,
1224 | lng: 143.95555,
1225 | },
1226 | ],
1227 | [
1228 | 'PHL',
1229 | {
1230 | lat: 12.879721,
1231 | lng: 121.774017,
1232 | },
1233 | ],
1234 | [
1235 | 'PAK',
1236 | {
1237 | lat: 30.375321,
1238 | lng: 69.345116,
1239 | },
1240 | ],
1241 | [
1242 | 'POL',
1243 | {
1244 | lat: 51.919438,
1245 | lng: 19.145136,
1246 | },
1247 | ],
1248 | [
1249 | 'SPM',
1250 | {
1251 | lat: 46.941936,
1252 | lng: -56.27111,
1253 | },
1254 | ],
1255 | [
1256 | 'PCN',
1257 | {
1258 | lat: -24.703615,
1259 | lng: -127.439308,
1260 | },
1261 | ],
1262 | [
1263 | 'PRI',
1264 | {
1265 | lat: 18.220833,
1266 | lng: -66.590149,
1267 | },
1268 | ],
1269 | [
1270 | 'PRT',
1271 | {
1272 | lat: 39.399872,
1273 | lng: -8.224454,
1274 | },
1275 | ],
1276 | [
1277 | 'PLW',
1278 | {
1279 | lat: 7.51498,
1280 | lng: 134.58252,
1281 | },
1282 | ],
1283 | [
1284 | 'PRY',
1285 | {
1286 | lat: -23.442503,
1287 | lng: -58.443832,
1288 | },
1289 | ],
1290 | [
1291 | 'QAT',
1292 | {
1293 | lat: 25.354826,
1294 | lng: 51.183884,
1295 | },
1296 | ],
1297 | [
1298 | 'REU',
1299 | {
1300 | lat: -21.115141,
1301 | lng: 55.536384,
1302 | },
1303 | ],
1304 | [
1305 | 'ROU',
1306 | {
1307 | lat: 45.943161,
1308 | lng: 24.96676,
1309 | },
1310 | ],
1311 | [
1312 | 'SRB',
1313 | {
1314 | lat: 44.016521,
1315 | lng: 21.005859,
1316 | },
1317 | ],
1318 | [
1319 | 'RUS',
1320 | {
1321 | lat: 61.52401,
1322 | lng: 105.318756,
1323 | },
1324 | ],
1325 | [
1326 | 'RWA',
1327 | {
1328 | lat: -1.940278,
1329 | lng: 29.873888,
1330 | },
1331 | ],
1332 | [
1333 | 'SAU',
1334 | {
1335 | lat: 23.885942,
1336 | lng: 45.079162,
1337 | },
1338 | ],
1339 | [
1340 | 'SLB',
1341 | {
1342 | lat: -9.64571,
1343 | lng: 160.156194,
1344 | },
1345 | ],
1346 | [
1347 | 'SYC',
1348 | {
1349 | lat: -4.679574,
1350 | lng: 55.491977,
1351 | },
1352 | ],
1353 | [
1354 | 'SDN',
1355 | {
1356 | lat: 12.862807,
1357 | lng: 30.217636,
1358 | },
1359 | ],
1360 | [
1361 | 'SSD',
1362 | {
1363 | lat: 6.877,
1364 | lng: 31.307,
1365 | },
1366 | ],
1367 | [
1368 | 'SWE',
1369 | {
1370 | lat: 60.128161,
1371 | lng: 18.643501,
1372 | },
1373 | ],
1374 | [
1375 | 'SGP',
1376 | {
1377 | lat: 1.352083,
1378 | lng: 103.819836,
1379 | },
1380 | ],
1381 | [
1382 | 'SVN',
1383 | {
1384 | lat: 46.151241,
1385 | lng: 14.995463,
1386 | },
1387 | ],
1388 | [
1389 | 'SJM',
1390 | {
1391 | lat: 77.553604,
1392 | lng: 23.670272,
1393 | },
1394 | ],
1395 | [
1396 | 'SVK',
1397 | {
1398 | lat: 48.669026,
1399 | lng: 19.699024,
1400 | },
1401 | ],
1402 | [
1403 | 'SLE',
1404 | {
1405 | lat: 8.460555,
1406 | lng: -11.779889,
1407 | },
1408 | ],
1409 | [
1410 | 'SMR',
1411 | {
1412 | lat: 43.94236,
1413 | lng: 12.457777,
1414 | },
1415 | ],
1416 | [
1417 | 'SEN',
1418 | {
1419 | lat: 14.497401,
1420 | lng: -14.452362,
1421 | },
1422 | ],
1423 | [
1424 | 'SOM',
1425 | {
1426 | lat: 5.152149,
1427 | lng: 46.199616,
1428 | },
1429 | ],
1430 | [
1431 | 'SUR',
1432 | {
1433 | lat: 3.919305,
1434 | lng: -56.027783,
1435 | },
1436 | ],
1437 | [
1438 | 'STP',
1439 | {
1440 | lat: 0.18636,
1441 | lng: 6.613081,
1442 | },
1443 | ],
1444 | [
1445 | 'SLV',
1446 | {
1447 | lat: 13.794185,
1448 | lng: -88.89653,
1449 | },
1450 | ],
1451 | [
1452 | 'SYR',
1453 | {
1454 | lat: 34.802075,
1455 | lng: 38.996815,
1456 | },
1457 | ],
1458 | [
1459 | 'SWZ',
1460 | {
1461 | lat: -26.522503,
1462 | lng: 31.465866,
1463 | },
1464 | ],
1465 | [
1466 | 'TCA',
1467 | {
1468 | lat: 21.694025,
1469 | lng: -71.797928,
1470 | },
1471 | ],
1472 | [
1473 | 'TCD',
1474 | {
1475 | lat: 15.454166,
1476 | lng: 18.732207,
1477 | },
1478 | ],
1479 | [
1480 | 'ATF',
1481 | {
1482 | lat: -49.280366,
1483 | lng: 69.348557,
1484 | },
1485 | ],
1486 | [
1487 | 'TGO',
1488 | {
1489 | lat: 8.619543,
1490 | lng: 0.824782,
1491 | },
1492 | ],
1493 | [
1494 | 'THA',
1495 | {
1496 | lat: 15.870032,
1497 | lng: 100.992541,
1498 | },
1499 | ],
1500 | [
1501 | 'TJK',
1502 | {
1503 | lat: 38.861034,
1504 | lng: 71.276093,
1505 | },
1506 | ],
1507 | [
1508 | 'TKL',
1509 | {
1510 | lat: -8.967363,
1511 | lng: -171.855881,
1512 | },
1513 | ],
1514 | [
1515 | 'TLS',
1516 | {
1517 | lat: -8.874217,
1518 | lng: 125.727539,
1519 | },
1520 | ],
1521 | [
1522 | 'TKM',
1523 | {
1524 | lat: 38.969719,
1525 | lng: 59.556278,
1526 | },
1527 | ],
1528 | [
1529 | 'TUN',
1530 | {
1531 | lat: 33.886917,
1532 | lng: 9.537499,
1533 | },
1534 | ],
1535 | [
1536 | 'TON',
1537 | {
1538 | lat: -21.178986,
1539 | lng: -175.198242,
1540 | },
1541 | ],
1542 | [
1543 | 'TUR',
1544 | {
1545 | lat: 38.963745,
1546 | lng: 35.243322,
1547 | },
1548 | ],
1549 | [
1550 | 'TTO',
1551 | {
1552 | lat: 10.691803,
1553 | lng: -61.222503,
1554 | },
1555 | ],
1556 | [
1557 | 'TUV',
1558 | {
1559 | lat: -7.109535,
1560 | lng: 177.64933,
1561 | },
1562 | ],
1563 | [
1564 | 'TWN',
1565 | {
1566 | lat: 23.69781,
1567 | lng: 120.960515,
1568 | },
1569 | ],
1570 | [
1571 | 'TZA',
1572 | {
1573 | lat: -6.369028,
1574 | lng: 34.888822,
1575 | },
1576 | ],
1577 | [
1578 | 'UKR',
1579 | {
1580 | lat: 48.379433,
1581 | lng: 31.16558,
1582 | },
1583 | ],
1584 | [
1585 | 'UGA',
1586 | {
1587 | lat: 1.373333,
1588 | lng: 32.290275,
1589 | },
1590 | ],
1591 | [
1592 | 'USA',
1593 | {
1594 | lat: 37.09024,
1595 | lng: -95.712891,
1596 | },
1597 | ],
1598 | [
1599 | 'URY',
1600 | {
1601 | lat: -32.522779,
1602 | lng: -55.765835,
1603 | },
1604 | ],
1605 | [
1606 | 'UZB',
1607 | {
1608 | lat: 41.377491,
1609 | lng: 64.585262,
1610 | },
1611 | ],
1612 | [
1613 | 'VAT',
1614 | {
1615 | lat: 41.902916,
1616 | lng: 12.453389,
1617 | },
1618 | ],
1619 | [
1620 | 'VCT',
1621 | {
1622 | lat: 12.984305,
1623 | lng: -61.287228,
1624 | },
1625 | ],
1626 | [
1627 | 'VEN',
1628 | {
1629 | lat: 6.42375,
1630 | lng: -66.58973,
1631 | },
1632 | ],
1633 | [
1634 | 'VGB',
1635 | {
1636 | lat: 18.420695,
1637 | lng: -64.639968,
1638 | },
1639 | ],
1640 | [
1641 | 'VIR',
1642 | {
1643 | lat: 18.335765,
1644 | lng: -64.896335,
1645 | },
1646 | ],
1647 | [
1648 | 'VNM',
1649 | {
1650 | lat: 14.058324,
1651 | lng: 108.277199,
1652 | },
1653 | ],
1654 | [
1655 | 'VUT',
1656 | {
1657 | lat: -15.376706,
1658 | lng: 166.959158,
1659 | },
1660 | ],
1661 | [
1662 | 'WLF',
1663 | {
1664 | lat: -13.768752,
1665 | lng: -177.156097,
1666 | },
1667 | ],
1668 | [
1669 | 'WSM',
1670 | {
1671 | lat: -13.759029,
1672 | lng: -172.104629,
1673 | },
1674 | ],
1675 | [
1676 | 'OWID_KOS',
1677 | {
1678 | lat: 42.602636,
1679 | lng: 20.902977,
1680 | },
1681 | ],
1682 | [
1683 | 'YEM',
1684 | {
1685 | lat: 15.552727,
1686 | lng: 48.516388,
1687 | },
1688 | ],
1689 | [
1690 | 'MYT',
1691 | {
1692 | lat: -12.8275,
1693 | lng: 45.166244,
1694 | },
1695 | ],
1696 | [
1697 | 'ZAF',
1698 | {
1699 | lat: -30.559482,
1700 | lng: 22.937506,
1701 | },
1702 | ],
1703 | [
1704 | 'ZMB',
1705 | {
1706 | lat: -13.133897,
1707 | lng: 27.849332,
1708 | },
1709 | ],
1710 | [
1711 | 'ZWE',
1712 | {
1713 | lat: -19.015438,
1714 | lng: 29.154857,
1715 | },
1716 | ],
1717 | ]);
1718 |
--------------------------------------------------------------------------------
/src/js/globe.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 | import * as THREE from 'three';
3 | import Hammer from 'hammerjs';
4 |
5 | import stats from './stats';
6 | import player from './player';
7 | import countries from './countries';
8 | import debounce from './utils/debounce';
9 | import countryIsoCodeToLatLng from './country-iso-to-latlng';
10 | import BufferGeometryUtils from './utils/three-buffer-geometry-utils';
11 | import nonBlockingWait from './utils/nonBlockingWait';
12 |
13 | Math.deg2Rad = (deg) => (deg * Math.PI) / 180;
14 | Math.rad2Deg = (rad) => (rad * 180) / Math.PI;
15 | Math.HALF_PI = Math.PI / 2;
16 | Math.QUARTER_PI = Math.PI / 4;
17 | Math.TAU = Math.PI * 2;
18 |
19 | let isInitialized = false;
20 | let isWorldTextureReady = false;
21 | let isSkyboxTextureReady = false;
22 |
23 | const WIDTH = window.innerWidth;
24 | const HEIGHT = window.innerHeight;
25 | const EPSILON = 1e-6;
26 | const EPSILON2 = 1e-12;
27 | const DATA_STEP = 3;
28 | const GLOBE_RADIUS = 200;
29 | const POINTS_GLOBE_RADIUS = GLOBE_RADIUS + 0.5;
30 | const SKYBOX_TEXTURE = 'images/space';
31 | const WORLD_TEXTURE = 'images/world.jpg';
32 | const SHADERS = {
33 | earth: {
34 | uniforms: {
35 | worldTexture: {
36 | value: new THREE.TextureLoader().load(WORLD_TEXTURE, () => {
37 | isWorldTextureReady = true;
38 | }),
39 | },
40 | },
41 | vertexShader: `
42 | varying vec2 vUv;
43 | varying vec3 vNormal;
44 | void main() {
45 | vUv = uv;
46 | vNormal = normalize(normalMatrix * normal);
47 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
48 | }
49 | `,
50 | fragmentShader: `
51 | varying vec2 vUv;
52 | varying vec3 vNormal;
53 | uniform sampler2D worldTexture;
54 | void main() {
55 | vec3 diffuse = texture2D(worldTexture, vUv).rgb;
56 | float intensity = 1.05 - dot(vNormal, vec3(0.0, 0.0, 1.0));
57 | vec3 atmosphere = vec3(0.5, 0.0, 0.0) * pow(intensity, 1.5);
58 | gl_FragColor = vec4(diffuse + atmosphere, 1.0);
59 | }
60 | `,
61 | },
62 | atmosphere: {
63 | vertexShader: `
64 | varying vec3 vNormal;
65 | void main() {
66 | vNormal = normalize(normalMatrix * normal);
67 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
68 | }
69 | `,
70 | fragmentShader: `
71 | varying vec3 vNormal;
72 | void main() {
73 | float intensity = pow(0.8 - dot(vNormal, vec3(0, 0, 0.85)), 15.0);
74 | gl_FragColor = vec4(0.5, 0.0, 0.0, 1.0) * intensity;
75 | }
76 | `,
77 | },
78 | };
79 |
80 | let scene;
81 | let earth;
82 | let camera;
83 | let renderer;
84 | let raycaster;
85 | const dataSetPoints = {};
86 | let point;
87 | let morphs;
88 | const zoomSpeed = 0;
89 | const mouse = { x: 0, y: 0 };
90 | const mouseOnDown = { x: 0, y: 0 };
91 | const rotation = { x: -0.5, y: 0.2 };
92 | const targetRotation = { x: -0.5, y: 0.2 };
93 | const targetRotationOnDown = { x: 0, y: 0 };
94 | let distance = 1400;
95 | let distanceTarget = 1400;
96 | let isPanning;
97 | let isAnimating;
98 | let pointsMaterial;
99 | let currentDataSetIndex;
100 | let focusedCountry = {
101 | clicked: { isoCode: null, name: '', mesh: null },
102 | hovered: { isoCode: null, name: '', mesh: null },
103 | };
104 | const focusScopeColor = {
105 | clicked: '#ff0000',
106 | hovered: '#aa0000',
107 | };
108 | const container = document.getElementsByClassName('container')[0];
109 | const containerEvents = new Hammer(container);
110 | containerEvents.get('pinch').set({ enable: true });
111 | containerEvents.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 0 });
112 |
113 | function initialize() {
114 | raycaster = new THREE.Raycaster();
115 | scene = new THREE.Scene();
116 | scene.background = new THREE.CubeTextureLoader().load(
117 | ['lf', 'rt', 'up', 'dn', 'ft', 'bk'].map(
118 | (side) => `${SKYBOX_TEXTURE}_${side}.png`,
119 | ),
120 | () => {
121 | isSkyboxTextureReady = true;
122 | },
123 | );
124 | const countryPolygons = new THREE.LineSegments(
125 | BufferGeometryUtils.mergeBufferGeometries(
126 | countries.features.map(
127 | (country) => new THREE.GeoJsonGeometry(country.geometry, GLOBE_RADIUS),
128 | ),
129 | false,
130 | ),
131 | new THREE.LineBasicMaterial({ color: '#440000' }),
132 | );
133 | countryPolygons.rotation.y = 1.5 * Math.PI;
134 | countryPolygons.matrixAutoUpdate = false;
135 | countryPolygons.updateMatrix();
136 | scene.add(countryPolygons);
137 |
138 | const geometry = new THREE.SphereBufferGeometry(GLOBE_RADIUS, 40, 30);
139 | const earthMaterial = new THREE.ShaderMaterial(SHADERS.earth);
140 | earth = new THREE.Mesh(geometry, earthMaterial);
141 | earth.rotation.y = Math.PI;
142 | earth.matrixAutoUpdate = false;
143 | earth.updateMatrix();
144 | scene.add(earth);
145 | const atmosphereMaterial = new THREE.ShaderMaterial({
146 | ...SHADERS.atmosphere,
147 | side: THREE.BackSide,
148 | blending: THREE.AdditiveBlending,
149 | transparent: true,
150 | });
151 | const atmosphere = new THREE.Mesh(geometry, atmosphereMaterial);
152 | atmosphere.scale.set(1.08, 1.08, 1.08);
153 | atmosphere.matrixAutoUpdate = false;
154 | atmosphere.updateMatrix();
155 | scene.add(atmosphere);
156 |
157 | point = new THREE.Mesh(
158 | (new THREE.BoxBufferGeometry(1.0, 1.0, 1.5))
159 | .applyMatrix4(
160 | new THREE.Matrix4().makeTranslation(0, 0, -0.75),
161 | ),
162 | );
163 | pointsMaterial = new THREE.MeshBasicMaterial({
164 | color: '#ff0000',
165 | morphTargets: true,
166 | });
167 |
168 | camera = new THREE.PerspectiveCamera(30, WIDTH / HEIGHT, 1, 10000);
169 | camera.position.z = distance;
170 |
171 | renderer = new THREE.WebGLRenderer({ antialias: true });
172 | renderer.setSize(WIDTH, HEIGHT);
173 | renderer.domElement.style.position = 'absolute';
174 |
175 | window.addEventListener('resize', onWindowResize, false);
176 | document.addEventListener('keydown', onDocumentKeyDown, false);
177 | containerEvents.on('panstart', onPanStart);
178 | containerEvents.on('panmove', onPanMove);
179 | containerEvents.on('tap', onTap);
180 | containerEvents.on('pinch pinchmove', onZoom);
181 | container.addEventListener('wheel', onZoom, false);
182 | container.addEventListener('mousemove', onMouseMove, false);
183 |
184 | isInitialized = true;
185 | }
186 |
187 | function findCountryByIsoCode(isoCode) {
188 | return countries.features.find((country) => country.properties.isoCode === isoCode);
189 | }
190 |
191 | // Mostly borrowed from: https://github.com/d3/d3-geo/blob/master/src/polygonContains.js
192 | function findCountryByLngLat(lngLat) {
193 | const latLngPoint = [Math.deg2Rad(lngLat.lng), Math.deg2Rad(lngLat.lat)];
194 | const cartesian = (spherical) => {
195 | const lambda = spherical[0];
196 | const phi = spherical[1];
197 | const cosPhi = Math.cos(phi);
198 | return [
199 | cosPhi * Math.cos(lambda),
200 | cosPhi * Math.sin(lambda),
201 | Math.sin(phi),
202 | ];
203 | };
204 | const cartesianCross = (a, b) => [
205 | a[1] * b[2] - a[2] * b[1],
206 | a[2] * b[0] - a[0] * b[2],
207 | a[0] * b[1] - a[1] * b[0],
208 | ];
209 | const cartesianNormalizeInPlace = (d) => {
210 | const l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]);
211 | (d[0] /= l), (d[1] /= l), (d[2] /= l);
212 | };
213 | const longitude = (aPoint) => {
214 | if (Math.abs(aPoint[0]) <= Math.PI) {
215 | return aPoint[0];
216 | }
217 | return (
218 | Math.sign(aPoint[0])
219 | * (((Math.abs(aPoint[0]) + Math.PI) % Math.TAU) - Math.PI)
220 | );
221 | };
222 | return countries.features.find((country) => {
223 | const multiPolygonCoords = country.geometry.type === 'Polygon'
224 | ? [country.geometry.coordinates]
225 | : country.geometry.coordinates;
226 | const isWithinCountry = multiPolygonCoords.some((polygonCoords) => {
227 | const polygon = polygonCoords.map(
228 | (ring) => ring.map((p) => [Math.deg2Rad(p[0]), Math.deg2Rad(p[1])]),
229 | );
230 | const lambda = longitude(latLngPoint);
231 | let phi = latLngPoint[1];
232 | const sinPhi = Math.sin(phi);
233 | const normal = [Math.sin(lambda), -Math.cos(lambda), 0];
234 | let angle = 0;
235 | let winding = 0;
236 | const sum = new d3.Adder();
237 | if (sinPhi === 1) {
238 | phi = Math.HALF_PI + EPSILON;
239 | } else if (sinPhi === -1) {
240 | phi = -Math.HALF_PI - EPSILON;
241 | }
242 | for (let i = 0, n = polygon.length; i < n; ++i) {
243 | let ring;
244 | let m;
245 | if (!(m = (ring = polygon[i]).length)) {
246 | continue;
247 | }
248 | let point0 = ring[m - 1];
249 | let lambda0 = longitude(point0);
250 | const phi0 = point0[1] / 2 + Math.QUARTER_PI;
251 | let sinPhi0 = Math.sin(phi0);
252 | let cosPhi0 = Math.cos(phi0);
253 | let lambda1;
254 | let point1;
255 | let sinPhi1;
256 | let cosPhi1;
257 | for (
258 | let j = 0;
259 | j < m;
260 | ++j,
261 | lambda0 = lambda1,
262 | sinPhi0 = sinPhi1,
263 | cosPhi0 = cosPhi1,
264 | point0 = point1
265 | ) {
266 | point1 = ring[j];
267 | lambda1 = longitude(point1);
268 | const phi1 = point1[1] / 2 + Math.QUARTER_PI;
269 | sinPhi1 = Math.sin(phi1);
270 | cosPhi1 = Math.cos(phi1);
271 | const delta = lambda1 - lambda0;
272 | const sign = delta >= 0 ? 1 : -1;
273 | const absDelta = sign * delta;
274 | const antimeridian = absDelta > Math.PI;
275 | const k = sinPhi0 * sinPhi1;
276 | sum.add(
277 | Math.atan2(
278 | k * sign * Math.sin(absDelta),
279 | cosPhi0 * cosPhi1 + k * Math.cos(absDelta),
280 | ),
281 | );
282 | angle += antimeridian ? delta + sign * Math.TAU : delta;
283 | // Are the longitudes either side of the point’s meridian (lambda),
284 | // and are the latitudes smaller than the parallel (phi)?
285 | if (antimeridian ^ (lambda0 >= lambda) ^ (lambda1 >= lambda)) {
286 | const arc = cartesianCross(cartesian(point0), cartesian(point1));
287 | cartesianNormalizeInPlace(arc);
288 | const intersection = cartesianCross(normal, arc);
289 | cartesianNormalizeInPlace(intersection);
290 | const phiArc = (antimeridian ^ (delta >= 0) ? -1 : 1)
291 | * Math.asin(intersection[2]);
292 | if (phi > phiArc || (phi === phiArc && (arc[0] || arc[1]))) {
293 | winding += antimeridian ^ (delta >= 0) ? 1 : -1;
294 | }
295 | }
296 | }
297 | }
298 | // First, determine whether the South pole is inside or outside:
299 | //
300 | // It is inside if:
301 | // * the polygon winds around it in a clockwise direction.
302 | // * the polygon does not (cumulatively) wind around it, but has a negative
303 | // (counter-clockwise) area.
304 | //
305 | // Second, count the (signed) number of times a segment crosses a lambda
306 | // from the point to the South pole. If it is zero, then the point is the
307 | // same side as the South pole.
308 | return (
309 | (angle < -EPSILON || (angle < EPSILON && sum < -EPSILON2))
310 | ^ (winding & 1)
311 | );
312 | });
313 | if (isWithinCountry) {
314 | return country;
315 | }
316 | return null;
317 | });
318 | }
319 |
320 | function mapClientToWorldPoint(clientPoint) {
321 | const x = (
322 | (clientPoint.x - renderer.domElement.offsetLeft + 0.5) / window.innerWidth)
323 | * 2
324 | - 1;
325 | const y = -(
326 | ((clientPoint.y - renderer.domElement.offsetTop + 0.5) / window.innerHeight)
327 | * 2
328 | ) + 1;
329 | return { x, y };
330 | }
331 |
332 | function findCountryForWorldPoint(worldPoint) {
333 | raycaster.setFromCamera(worldPoint, camera);
334 | const intersects = raycaster.intersectObject(earth);
335 | if (intersects.length > 0) {
336 | const { point: intersectionPoint } = intersects[0];
337 | const lat = 90 - Math.rad2Deg(Math.acos(intersectionPoint.y / GLOBE_RADIUS));
338 | const lng = (
339 | (
340 | 270 + Math.rad2Deg(Math.atan2(intersectionPoint.x, intersectionPoint.z))
341 | ) % 360) - 180;
342 | return findCountryByLngLat({ lng, lat });
343 | }
344 | return null;
345 | }
346 |
347 | function shortestAngleDiff(angle1, angle2) {
348 | const diff = ((angle2 - angle1 + 180) % 360) - 180;
349 | return diff < -180 ? diff + 360 : diff;
350 | }
351 |
352 | function setFocusOnCountryByIsoCode({ isoCode, scope, shouldFlyToCountry }) {
353 | const country = findCountryByIsoCode(isoCode);
354 | setFocusOnCountry({ country, scope, shouldFlyToCountry });
355 | }
356 |
357 | function setFocusOnCountry({ country, scope, shouldFlyToCountry }) {
358 | if (
359 | country
360 | && country.properties.isoCode !== focusedCountry[scope].isoCode
361 | ) {
362 | if (focusedCountry[scope].mesh) {
363 | scene.remove(focusedCountry[scope].mesh);
364 | focusedCountry[scope].mesh.geometry.dispose();
365 | focusedCountry[scope].mesh.material.dispose();
366 | }
367 | const mesh = new THREE.LineSegments(
368 | new THREE.GeoJsonGeometry(country.geometry, GLOBE_RADIUS + 0.5),
369 | new THREE.LineBasicMaterial({ color: focusScopeColor[scope] }),
370 | );
371 | mesh.rotation.y = 1.5 * Math.PI;
372 | mesh.matrixAutoUpdate = false;
373 | mesh.updateMatrix();
374 | focusedCountry = {
375 | ...focusedCountry,
376 | [scope]: {
377 | mesh,
378 | name: country.properties.name,
379 | isoCode: country.properties.isoCode,
380 | },
381 | };
382 | scene.add(mesh);
383 | } else if (!country && focusedCountry[scope].mesh) {
384 | scene.remove(focusedCountry[scope].mesh);
385 | focusedCountry[scope].mesh.geometry.dispose();
386 | focusedCountry[scope].mesh.material.dispose();
387 | focusedCountry = {
388 | ...focusedCountry,
389 | [scope]: { mesh: null, isoCode: null, name: '' },
390 | };
391 | }
392 | if (country || (scope === 'hovered' && focusedCountry.clicked.mesh)) {
393 | stats.setActiveCountry(focusedCountry[country ? scope : 'clicked']);
394 | } else {
395 | stats.setActiveCountry(null);
396 | }
397 | if (country && shouldFlyToCountry) {
398 | const countryLatLng = countryIsoCodeToLatLng.get(country.properties.isoCode);
399 | if (countryLatLng) {
400 | const { lat, lng } = countryLatLng;
401 | const delta = 270 + (lng >= 0 ? lng : 360 + lng);
402 | const diff = shortestAngleDiff(Math.rad2Deg(targetRotation.x), delta);
403 | targetRotation.x += Math.deg2Rad(diff);
404 | targetRotation.y = Math.deg2Rad(lat);
405 | }
406 | }
407 | }
408 |
409 | function onCountryClicked(worldPoint) {
410 | const country = findCountryForWorldPoint(
411 | mapClientToWorldPoint(worldPoint),
412 | );
413 | setFocusOnCountry({ country, scope: 'clicked' });
414 | }
415 |
416 | const onCountryHovered = debounce(
417 | (worldPoint) => {
418 | const country = findCountryForWorldPoint(
419 | mapClientToWorldPoint(worldPoint),
420 | );
421 | setFocusOnCountry({ country, scope: 'hovered' });
422 | },
423 | 5,
424 | );
425 |
426 | function onPanStart(event) {
427 | containerEvents.on('panend', onPanEnd);
428 | mouseOnDown.x = -event.center.x;
429 | mouseOnDown.y = event.center.y;
430 | targetRotationOnDown.x = targetRotation.x;
431 | targetRotationOnDown.y = targetRotation.y;
432 | isPanning = true;
433 | }
434 |
435 | function onPanMove(event) {
436 | container.style.cursor = 'grabbing';
437 | const zoomDamp = distance / 800;
438 | mouse.x = -event.center.x;
439 | mouse.y = event.center.y;
440 | targetRotation.x = targetRotationOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp;
441 | targetRotation.y = targetRotationOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp;
442 | targetRotation.y = Math.max(Math.min(Math.HALF_PI, targetRotation.y), -Math.HALF_PI);
443 | }
444 |
445 | function onMouseMove(event) {
446 | if (!isPanning) {
447 | onCountryHovered({ x: event.clientX, y: event.clientY });
448 | }
449 | }
450 |
451 | function onPanEnd() {
452 | isPanning = false;
453 | containerEvents.off('panend', onPanEnd);
454 | container.style.cursor = 'auto';
455 | }
456 |
457 | function onTap(event) {
458 | onCountryClicked(event.center);
459 | }
460 |
461 | function onZoom(event) {
462 | zoom(event.deltaY > 0 ? -120 : 120);
463 | }
464 |
465 | function onDocumentKeyDown(event) {
466 | switch (event.keyCode) {
467 | case 38: /* up */
468 | zoom(100);
469 | event.preventDefault();
470 | break;
471 | case 40: /* down */
472 | zoom(-100);
473 | event.preventDefault();
474 | break;
475 | default:
476 | break;
477 | }
478 | }
479 |
480 | function onWindowResize() {
481 | camera.aspect = window.innerWidth / window.innerHeight;
482 | camera.updateProjectionMatrix();
483 | renderer.setSize(window.innerWidth, window.innerHeight);
484 | }
485 |
486 | function zoom(delta) {
487 | distanceTarget -= delta;
488 | distanceTarget = distanceTarget > 1600 ? 1600 : distanceTarget;
489 | distanceTarget = distanceTarget < 600 ? 600 : distanceTarget;
490 | }
491 |
492 | function render() {
493 | zoom(zoomSpeed);
494 | rotation.x += (targetRotation.x - rotation.x) * 0.1;
495 | rotation.y += (targetRotation.y - rotation.y) * 0.1;
496 | distance += (distanceTarget - distance) * 0.3;
497 | camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
498 | camera.position.y = distance * Math.sin(rotation.y);
499 | camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
500 | camera.lookAt(earth.position);
501 | renderer.render(scene, camera);
502 | }
503 |
504 | function animate() {
505 | if (player.checkIsAnimationPlaying()) {
506 | targetRotation.x -= 0.0002;
507 | }
508 | requestAnimationFrame(animate);
509 | render();
510 | }
511 |
512 | function createGeometryFromData ({ dataArray, shouldUseMagnitude }) {
513 | const geometries = [];
514 | for (let i = 0; i < dataArray.length; i += DATA_STEP) {
515 | const lat = dataArray[i];
516 | const lng = dataArray[i + 1];
517 | const magnitude = shouldUseMagnitude ? dataArray[i + 2] : 0;
518 | const phi = Math.deg2Rad(90 - lat);
519 | const theta = Math.deg2Rad(180 - lng);
520 | const radius = magnitude > 0 ? POINTS_GLOBE_RADIUS : 1;
521 | point.position.x = radius * Math.sin(phi) * Math.cos(theta);
522 | point.position.y = radius * Math.cos(phi);
523 | point.position.z = radius * Math.sin(phi) * Math.sin(theta);
524 | point.lookAt(earth.position);
525 | point.scale.z = radius * magnitude;
526 | point.updateMatrix();
527 | geometries.push(point.geometry.clone().applyMatrix4(point.matrix));
528 | }
529 | return BufferGeometryUtils.mergeBufferGeometries(geometries);
530 | }
531 |
532 | async function loadAnimationData({ globeData, dataSetKey }) {
533 | const geometry = createGeometryFromData({
534 | dataArray: globeData[0],
535 | shouldUseMagnitude: false,
536 | });
537 | geometry.morphAttributes.position = [];
538 |
539 | let index = 0;
540 | for await (const dataArray of globeData) {
541 | await nonBlockingWait(10); /* allow UI to render */
542 | geometry.morphAttributes.position[index] = createGeometryFromData({
543 | dataArray,
544 | shouldUseMagnitude: true,
545 | }).attributes.position;
546 | index += 1;
547 | }
548 |
549 | dataSetPoints[dataSetKey] = new THREE.Mesh(geometry, pointsMaterial);
550 | }
551 |
552 | function setActiveDataSet(dataIndex) {
553 | if (dataIndex === currentDataSetIndex) {
554 | return;
555 | }
556 | scene.remove(dataSetPoints[currentDataSetIndex]);
557 | scene.add(dataSetPoints[dataIndex]);
558 | morphs = Object.keys(dataSetPoints[dataIndex].morphTargetDictionary);
559 | currentDataSetIndex = dataIndex;
560 |
561 | if (!isAnimating) {
562 | container.appendChild(renderer.domElement);
563 | isAnimating = true;
564 | animate();
565 | }
566 | }
567 |
568 | export default {
569 | initialize,
570 | loadAnimationData,
571 | setActiveDataSet,
572 | setFocusOnCountryByIsoCode,
573 | isReady: () => isInitialized && isWorldTextureReady && isSkyboxTextureReady,
574 | setTime: (time) => {
575 | const points = dataSetPoints[currentDataSetIndex];
576 | morphs.forEach((_, morphIndex) => {
577 | points.morphTargetInfluences[morphs[morphIndex]] = 0;
578 | });
579 | const last = morphs.length - 1;
580 | const scaledTime = time * last + 1;
581 | const index = Math.min(Math.floor(scaledTime), last);
582 | const lastIndex = index - 1;
583 | const leftOver = scaledTime - index;
584 | if (lastIndex >= 0) {
585 | points.morphTargetInfluences[lastIndex] = 1 - leftOver;
586 | }
587 | points.morphTargetInfluences[index] = leftOver;
588 | },
589 | };
590 |
--------------------------------------------------------------------------------
/src/js/player.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 | import noUiSlider from 'nouislider';
3 |
4 | const PLAYBACK_TIME = 1000 * 60 * 3;
5 | export const PLAYBACK_SPEEDS = [1, 2, 3, 4, 5];
6 |
7 | let items = [];
8 | let slider;
9 | let playButton;
10 | let playButtonPath;
11 | let replayButton;
12 | let playbackSpeed = 1;
13 | let playbackFraction = 0;
14 | let previousAnimationTime;
15 | let isInitialized = false;
16 | let isSliderDragging = false;
17 | let isReplayEnabled = false;
18 | let isFinished = false;
19 | let isAnimationPlaying = false;
20 | const shouldAllowUpdate = true;
21 |
22 | function initialize() {
23 | previousAnimationTime = performance.now();
24 |
25 | [slider] = document.getElementsByClassName('progress');
26 | noUiSlider.create(slider, {
27 | range: { min: 0, max: 1 },
28 | connect: 'lower',
29 | step: 0.0001,
30 | start: 0,
31 | animate: false,
32 | animationDuration: 100,
33 | });
34 | const sliderChange = (values) => {
35 | playbackFraction = parseFloat(values[0]);
36 | isFinished = playbackFraction === 1.0;
37 | items.forEach((item) => {
38 | if (item !== slider) {
39 | item.setTime(playbackFraction);
40 | }
41 | });
42 | };
43 | slider.noUiSlider.on('start', () => {
44 | isSliderDragging = true;
45 | });
46 | slider.noUiSlider.on('end', () => {
47 | isSliderDragging = false;
48 | });
49 | slider.noUiSlider.on('slide', sliderChange);
50 | slider.setTime = (time) => {
51 | if (shouldAllowUpdate) {
52 | slider.noUiSlider.set(time);
53 | }
54 | };
55 | items.push(slider);
56 | [replayButton] = document.getElementsByClassName('replay-button');
57 | replayButton.addEventListener(
58 | 'click',
59 | (e) => {
60 | e.stopPropagation();
61 | toggleReplayEnabled();
62 | },
63 | false,
64 | );
65 |
66 | {
67 | playButton = document.querySelector('.playback-button');
68 | const useEl = playButton.querySelector('use');
69 | const iconEl = playButton.querySelector(useEl.getAttribute('xlink:href'));
70 | const nextState = iconEl.getAttribute('data-next-state');
71 | const iconPath = iconEl.getAttribute('d');
72 | playButtonPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
73 | playButtonPath.setAttribute('data-next-state', nextState);
74 | playButtonPath.setAttribute('d', iconPath);
75 | const svgEl = playButton.querySelector('svg');
76 | svgEl.replaceChild(playButtonPath, useEl);
77 | playButton.addEventListener(
78 | 'click',
79 | (e) => {
80 | e.stopPropagation();
81 | toggleIsAnimationPlaying();
82 | },
83 | );
84 | }
85 | const [aboutCard] = document.getElementsByClassName('about-card');
86 | const [aboutButton] = document.getElementsByClassName('about');
87 | const [aboutCardClose] = document.getElementsByClassName('about-card-close');
88 | const [contentElement] = document.getElementsByClassName('content');
89 | aboutButton.addEventListener(
90 | 'click',
91 | (e) => {
92 | e.stopPropagation();
93 | contentElement.classList.add('blurred');
94 | aboutCard.classList.remove('hidden');
95 | },
96 | );
97 | aboutCardClose.addEventListener(
98 | 'click',
99 | (e) => {
100 | e.stopPropagation();
101 | contentElement.classList.remove('blurred');
102 | aboutCard.classList.add('hidden');
103 | },
104 | );
105 |
106 | const [speedValue] = document.getElementsByClassName('playback-speed-value');
107 | const [speedButton] = document.getElementsByClassName('playback-speed');
108 | speedButton.addEventListener(
109 | 'click',
110 | (e) => {
111 | e.stopPropagation();
112 | const currentPlayBackSpeedIndex = PLAYBACK_SPEEDS.findIndex(
113 | (speed) => speed === playbackSpeed,
114 | );
115 | playbackSpeed = PLAYBACK_SPEEDS[
116 | (currentPlayBackSpeedIndex + 1) % PLAYBACK_SPEEDS.length
117 | ];
118 | speedValue.innerHTML = playbackSpeed.toString();
119 | },
120 | );
121 | }
122 |
123 | function toggleIsAnimationPlaying() {
124 | isAnimationPlaying = !isAnimationPlaying;
125 | const nextIconEl = playButton.querySelector(
126 | `[data-state="${playButtonPath.getAttribute('data-next-state')}"]`,
127 | );
128 | const iconPath = nextIconEl.getAttribute('d');
129 | const nextState = nextIconEl.getAttribute('data-next-state');
130 | d3.select(playButtonPath)
131 | .attr('data-next-state', nextState)
132 | .transition()
133 | .duration(100)
134 | .attr('d', iconPath);
135 | }
136 |
137 | function toggleReplayEnabled() {
138 | isReplayEnabled = !isReplayEnabled;
139 | replayButton.classList.toggle('enabled');
140 | }
141 |
142 | function animate() {
143 | requestAnimationFrame(animate);
144 | const now = performance.now();
145 | if (
146 | isSliderDragging
147 | || !isAnimationPlaying
148 | || (playbackFraction === 1.0 && !isReplayEnabled)
149 | ) {
150 | previousAnimationTime = now;
151 | return;
152 | }
153 | const elapsedTime = now - previousAnimationTime;
154 | playbackFraction += (elapsedTime / PLAYBACK_TIME) * playbackSpeed;
155 | playbackFraction = Math.min(playbackFraction, 1.0);
156 | isFinished = playbackFraction === 1.0;
157 | if (playbackFraction === 1.0 && isReplayEnabled) {
158 | playbackFraction = 0;
159 | } else {
160 | items.forEach((item) => {
161 | item.setTime(playbackFraction);
162 | });
163 | }
164 | previousAnimationTime = now;
165 | }
166 |
167 | export default {
168 | initialize: () => {
169 | items = [];
170 | },
171 | addItem: (item) => items.push(item),
172 | pause: () => {
173 | if (isAnimationPlaying) {
174 | toggleIsAnimationPlaying();
175 | }
176 | },
177 | checkIsAnimationPlaying: () => isAnimationPlaying,
178 | checkIsFinished: () => isFinished,
179 | play: () => {
180 | if (!isInitialized) {
181 | isInitialized = true;
182 | initialize();
183 | animate();
184 | }
185 | if (!isAnimationPlaying) {
186 | toggleIsAnimationPlaying();
187 | }
188 | items.forEach((item) => {
189 | item.setTime(playbackFraction, true);
190 | });
191 | },
192 | };
193 |
--------------------------------------------------------------------------------
/src/js/search.js:
--------------------------------------------------------------------------------
1 | import globe from './globe';
2 | import countries from './countries';
3 |
4 | let isSearchShown = false;
5 | let currentSearchFirstCountryIsoCode = null;
6 | const [searchElement] = document.getElementsByClassName('search');
7 | const [contentElement] = document.getElementsByClassName('content');
8 | const [searchInput] = document.getElementsByClassName('search-input');
9 | const [searchButton] = document.getElementsByClassName('search-button');
10 | const [closeSearchButton] = document.getElementsByClassName('search-close');
11 | const [searchList] = document.getElementsByClassName('search-suggestions-list');
12 |
13 | const countryNameToIsoCodeMap = new Map(
14 | countries.features.reduce(
15 | (allCountries, country) => [
16 | ...allCountries,
17 | [country.properties.name, country.properties.isoCode],
18 | ], [],
19 | ),
20 | );
21 | const countryNames = Array.from(countryNameToIsoCodeMap.keys()).sort();
22 |
23 | function setFocusOnCountry(isoCode) {
24 | globe.setFocusOnCountryByIsoCode({
25 | isoCode,
26 | scope: 'clicked',
27 | shouldFlyToCountry: true,
28 | });
29 | }
30 |
31 | function createCountryListItem(countryName) {
32 | return `
33 |
37 | ${countryName}
38 |
39 | `;
40 | }
41 |
42 | function toggleIsSearchShown() {
43 | isSearchShown = !isSearchShown;
44 | if (isSearchShown) {
45 | contentElement.classList.add('blurred');
46 | searchElement.classList.remove('hidden');
47 | searchInput.focus();
48 | searchInput.select();
49 | } else {
50 | contentElement.classList.remove('blurred');
51 | searchElement.classList.add('hidden');
52 | }
53 | }
54 |
55 | function searchListAttachEvents() {
56 | Array.from(document.getElementsByClassName('search-suggestions-list-item'))
57 | .forEach(
58 | (el) => {
59 | el.addEventListener(
60 | 'click',
61 | (elEvt) => {
62 | const isoCode = elEvt.target.getAttribute('data-value');
63 | if (isoCode) {
64 | toggleIsSearchShown();
65 | setFocusOnCountry(isoCode);
66 | }
67 | },
68 | );
69 | },
70 | );
71 | }
72 |
73 | function initialize() {
74 | searchList.innerHTML = countryNames.map(createCountryListItem).join('');
75 | searchListAttachEvents();
76 | searchInput.addEventListener(
77 | 'keyup',
78 | (e) => {
79 | const value = e.target.value.trim().toLowerCase();
80 | if (!value) {
81 | searchList.innerHTML = countryNames.map(createCountryListItem).join('');
82 | currentSearchFirstCountryIsoCode = null;
83 | } else {
84 | const matches = countryNames
85 | .filter(
86 | (name) => name.toLowerCase().indexOf(value) !== -1,
87 | )
88 | .sort((a, b) => {
89 | /*
90 | * Move names starting with term to the top otherwise sort
91 | * alphabetically...
92 | */
93 | const aStartsWithTerm = a.toLowerCase().startsWith(value);
94 | const bStartsWithTerm = b.toLowerCase().startsWith(value);
95 | if (aStartsWithTerm && !bStartsWithTerm) {
96 | return -1;
97 | }
98 | if (!aStartsWithTerm && bStartsWithTerm) {
99 | return 1;
100 | }
101 | const aHasWordStartingWithTerm = a.split(' ').some((word) => word.toLowerCase().startsWith(value));
102 | const bHasWordStartingWithTerm = b.split(' ').some((word) => word.toLowerCase().startsWith(value));
103 | if (aHasWordStartingWithTerm && !bHasWordStartingWithTerm) {
104 | return -1;
105 | }
106 | if (!aHasWordStartingWithTerm && bHasWordStartingWithTerm) {
107 | return 1;
108 | }
109 | return a > b;
110 | });
111 | currentSearchFirstCountryIsoCode = matches.length > 0
112 | ? countryNameToIsoCodeMap.get(matches[0])
113 | : null;
114 | searchList.innerHTML = matches
115 | .map(createCountryListItem)
116 | .join('');
117 | }
118 | searchListAttachEvents();
119 | },
120 | );
121 | searchButton.addEventListener(
122 | 'click',
123 | () => {
124 | if (!isSearchShown) {
125 | toggleIsSearchShown();
126 | }
127 | },
128 | );
129 | closeSearchButton.addEventListener(
130 | 'click',
131 | () => {
132 | if (isSearchShown) {
133 | toggleIsSearchShown();
134 | }
135 | },
136 | );
137 | document.addEventListener(
138 | 'keydown',
139 | (e) => {
140 | switch (e.keyCode) {
141 | case 27: /* escape */
142 | if (isSearchShown) {
143 | toggleIsSearchShown();
144 | e.preventDefault();
145 | }
146 | break;
147 | case 13: /* enter */
148 | if (isSearchShown && currentSearchFirstCountryIsoCode) {
149 | toggleIsSearchShown();
150 | setFocusOnCountry(currentSearchFirstCountryIsoCode);
151 | }
152 | break;
153 | case 114: /* F3 */
154 | case 70: /* Ctrl+F */
155 | e.preventDefault();
156 | if (!isSearchShown) {
157 | toggleIsSearchShown();
158 | }
159 | break;
160 | default:
161 | break;
162 | }
163 | },
164 | );
165 | }
166 |
167 | export default {
168 | initialize,
169 | };
170 |
--------------------------------------------------------------------------------
/src/js/stats.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 |
3 | import globe from './globe';
4 | import player from './player';
5 | import debounce from './utils/debounce';
6 |
7 | const WORLD_ISO_CODE = 'OWID_WRL';
8 | const k = 4;
9 | const n = window.innerWidth >= 1024 ? 10 : 5;
10 | let width = getChartWidth();
11 | const barSize = 48;
12 | const duration = 250;
13 | const margin = {
14 | top: 16, right: 6, bottom: 6, left: 0,
15 | };
16 | const height = margin.top + barSize * n + margin.bottom;
17 | const defaultActiveLocation = { name: 'World', isoCode: WORLD_ISO_CODE };
18 |
19 | let svg;
20 | let dateSvg;
21 | let countrySvg;
22 | const dataSetKeyFrames = {};
23 | let activeDataSetName = '';
24 | let activeKeyframes;
25 | let currentKeyFrameIndex;
26 | let updateBars;
27 | let updateLabels;
28 | let updateTitleAndDate;
29 | let updateCountryStats;
30 | let activeLocation = defaultActiveLocation;
31 |
32 | const formatNumber = d3.format(',d');
33 | const formatDate = d3.utcFormat('%d %B %Y');
34 | let x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
35 | const y = d3
36 | .scaleBand()
37 | .domain(d3.range(n + 1))
38 | .rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
39 | .padding(0.1);
40 |
41 | function getChartWidth() {
42 | /* An attempt at finding a formula to resize the chart by */
43 | const w = window.innerWidth;
44 | if (w <= 1024) {
45 | return Math.max(250, 250 + (w - 400) / 2);
46 | }
47 | if (w <= 1200) {
48 | return 450;
49 | }
50 | if (w <= 1440) {
51 | return 500;
52 | }
53 | return 600;
54 | }
55 |
56 | function initialize() {
57 | svg = d3
58 | .select('.chart')
59 | .append('svg')
60 | .attr('preserveAspectRatio', 'none')
61 | .attr('viewBox', `0 0 ${width} ${height}`);
62 |
63 | dateSvg = d3
64 | .select('.date')
65 | .append('svg')
66 | .attr('width', 300)
67 | .attr('height', 100);
68 |
69 | countrySvg = d3
70 | .select('.country-stats')
71 | .append('svg')
72 | .attr('width', 400)
73 | .attr('height', 100);
74 |
75 | window.addEventListener('resize', () => {
76 | width = getChartWidth();
77 | x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
78 | svg.attr('viewBox', `0 0 ${width} ${height}`);
79 | });
80 | }
81 |
82 | function loadAnimationData({
83 | statsData,
84 | dataSetKey,
85 | dataSetName,
86 | locationIsoCodeToNameMap,
87 | }) {
88 | const rank = (value) => {
89 | const data = Array.from(locationIsoCodeToNameMap.keys(), (isoCode) => ({
90 | isoCode,
91 | value: value(isoCode),
92 | name: locationIsoCodeToNameMap.get(isoCode),
93 | }));
94 | data.sort((a, b) => {
95 | if (a.isoCode === WORLD_ISO_CODE) {
96 | return 1;
97 | }
98 | if (b.isoCode === WORLD_ISO_CODE) {
99 | return -1;
100 | }
101 | return b.value - a.value;
102 | });
103 | for (let i = 0; i < data.length; ++i) {
104 | if (data[i].isoCode === WORLD_ISO_CODE) {
105 | data[i].rank = n;
106 | } else {
107 | data[i].rank = Math.min(n, i);
108 | }
109 | }
110 | return data;
111 | };
112 |
113 | const keyframes = [];
114 | let ka;
115 | let a;
116 | let kb;
117 | let b;
118 | for ([[ka, a], [kb, b]] of d3.pairs(statsData)) {
119 | for (let i = 0; i < k; ++i) {
120 | const t = i / k;
121 | keyframes.push([
122 | new Date(ka * (1 - t) + kb * t),
123 | rank((name) => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t),
124 | ]);
125 | }
126 | }
127 | keyframes.push([new Date(kb), rank((name) => b.get(name) || 0)]);
128 | const keyframesStartIndex = keyframes.findIndex(
129 | (keyframe) => keyframe[1].some(({ value }) => value > 0),
130 | ) || 0;
131 |
132 | const nameframes = d3.groups(
133 | keyframes.flatMap(([, data]) => data),
134 | (d) => d.name,
135 | );
136 | const prev = new Map(
137 | nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])),
138 | );
139 | const next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));
140 | dataSetKeyFrames[dataSetKey] = {
141 | dataSetName,
142 | prev,
143 | next,
144 | keyframes,
145 | keyframesStartIndex,
146 | };
147 | }
148 |
149 | const createDebouncedSetFocusOnCountry = (scope) => debounce((_, d) => {
150 | globe.setFocusOnCountryByIsoCode({
151 | scope,
152 | isoCode: d.isoCode,
153 | shouldFlyToCountry: true,
154 | });
155 | }, 100, true);
156 |
157 | function setActiveDataSet(dataSetKey) {
158 | const {
159 | dataSetName,
160 | prev,
161 | next,
162 | keyframes,
163 | keyframesStartIndex,
164 | } = dataSetKeyFrames[dataSetKey];
165 |
166 | activeDataSetName = dataSetName;
167 | activeKeyframes = keyframes;
168 |
169 | function bars(theSvg) {
170 | let bar = theSvg
171 | .append('g')
172 | .selectAll('rect');
173 | const onClick = createDebouncedSetFocusOnCountry('clicked');
174 | return ([, data], transition) => {
175 | if (currentKeyFrameIndex < keyframesStartIndex) {
176 | bar = bar.attr('display', 'none');
177 | } else {
178 | bar = bar
179 | .attr('display', 'block')
180 | .data(data.slice(0, n), (d) => d.name)
181 | .join(
182 | (enter) => enter
183 | .append('rect')
184 | .attr('pointer-events', 'all')
185 | .attr('cursor', 'pointer')
186 | .on('click', onClick)
187 | .attr('fill', '#4d0000')
188 | .attr('height', y.bandwidth())
189 | .attr('x', x(0))
190 | .attr('y', (d) => y((prev.get(d) || d).rank))
191 | .attr('width', (d) => x((prev.get(d) || d).value) - x(0)),
192 | (update) => update,
193 | (exit) => exit
194 | .transition(transition)
195 | .remove()
196 | .attr('y', (d) => y((next.get(d) || d).rank))
197 | .attr('width', (d) => x((next.get(d) || d).value) - x(0)),
198 | )
199 | .call((theBar) => theBar
200 | .transition(transition)
201 | .attr('y', (d) => y(d.rank))
202 | .attr('width', (d) => x(d.value) - x(0)));
203 | }
204 | return bar;
205 | };
206 | }
207 |
208 | function labels(theSvg) {
209 | let label = theSvg
210 | .append('g')
211 | .attr('text-anchor', 'end')
212 | .selectAll('text');
213 | return ([, data], transition) => {
214 | if (currentKeyFrameIndex < keyframesStartIndex) {
215 | label = label.attr('display', 'none');
216 | } else {
217 | label = label
218 | .attr('display', 'block')
219 | .data(data.slice(0, n), (d) => d.name)
220 | .join(
221 | (enter) => enter
222 | .append('text')
223 | .attr(
224 | 'transform',
225 | (d) => `translate(${x((prev.get(d) || d).value)},${y(
226 | (prev.get(d) || d).rank,
227 | )})`,
228 | )
229 | .attr('fill', '#fff')
230 | .attr('y', y.bandwidth() / 2)
231 | .attr('x', -6)
232 | .attr('dy', '-0.25em')
233 | .text((d) => d.name)
234 | .call((text) => text
235 | .append('tspan')
236 | .attr('fill-opacity', 0.7)
237 | .attr('font-weight', 'normal')
238 | .attr('x', -6)
239 | .attr('dy', '1.15em')),
240 | (update) => update,
241 | (exit) => exit
242 | .transition(transition)
243 | .remove()
244 | .attr(
245 | 'transform',
246 | (d) => `translate(${x((next.get(d) || d).value)},${y(
247 | (next.get(d) || d).rank,
248 | )})`,
249 | )
250 | .call((g) => g
251 | .select('tspan')
252 | .tween('text', (d) => textTween(
253 | d.value, (next.get(d) || d).value,
254 | ))),
255 | )
256 | .call((bar) => bar
257 | .transition(transition)
258 | .attr('transform', (d) => `translate(${x(d.value)},${y(d.rank)})`)
259 | .call((g) => g
260 | .select('tspan')
261 | .tween('text', (d) => textTween(
262 | (prev.get(d) || d).value, d.value,
263 | ))));
264 | }
265 | return label;
266 | };
267 | }
268 |
269 | function titleAndDate(theSvg) {
270 | const label = theSvg
271 | .append('text')
272 | .attr('fill', '#999999')
273 | .attr('text-anchor', 'middle')
274 | .call((text) => text
275 | .append('tspan')
276 | .attr('y', 35)
277 | .attr('x', 150)
278 | .attr('font-size', '1.45rem')
279 | .attr('class', 'title')
280 | .text(`COVID-19 ${activeDataSetName.toUpperCase()}`))
281 | .call((text) => text
282 | .append('tspan')
283 | .attr('y', 65)
284 | .attr('x', 150)
285 | .attr('font-size', '1rem')
286 | .attr('class', 'date')
287 | .text(formatDate(keyframes[0][0])));
288 | return ([date]) => {
289 | label
290 | .call((text) => text
291 | .select('.title')
292 | .text(`COVID-19 ${activeDataSetName.toUpperCase()}`))
293 | .call((text) => text
294 | .select('.date')
295 | .text(formatDate(date)));
296 | };
297 | }
298 |
299 | function countryStats(theSvg) {
300 | let stats = theSvg
301 | .append('g')
302 | .style('font', 'bold 12px var(--sans-serif)')
303 | .style('font-variant-numeric', 'tabular-nums')
304 | .selectAll('text');
305 | return ([, data], transition) => {
306 | stats = stats
307 | .data(
308 | data.filter(({ isoCode }) => isoCode === activeLocation.isoCode),
309 | (d) => d,
310 | )
311 | .join(
312 | (enter) => enter
313 | .append('text')
314 | .attr('fill', '#999999')
315 | .attr('y', 35)
316 | .attr('x', 0)
317 | .call((text) => text
318 | .append('tspan')
319 | .attr('class', 'countryName')
320 | .attr('font-size', '1.25rem')
321 | .text((d) => d.name))
322 | .call((text) => text
323 | .append('tspan')
324 | .attr('class', 'countryStatValue')
325 | .attr('font-size', '1rem')
326 | .attr('x', 0)
327 | .attr('y', 60)),
328 | (update) => update,
329 | (exit) => exit
330 | .transition(transition)
331 | .remove()
332 | .call((g) => g.select('.countryName').text((d) => d.name))
333 | .call((g) => g
334 | .select('.countryStatValue')
335 | .tween('text', (d) => textTween(
336 | d.value,
337 | (next.get(d) || d).value,
338 | `${activeDataSetName}: ' '`,
339 | ))),
340 | )
341 | .call((text) => text
342 | .transition(transition)
343 | .call((g) => g.select('.countryName').text((d) => d.name))
344 | .call((g) => g
345 | .select('.countryStatValue')
346 | .tween('text', (d) => textTween(
347 | (prev.get(d) || d).value,
348 | d.value,
349 | `${activeDataSetName}: `,
350 | ))));
351 | return stats;
352 | };
353 | }
354 |
355 | function textTween(a, b, prefix = '') {
356 | const i = d3.interpolateNumber(a, b);
357 | return function(t) {
358 | this.textContent = `${prefix}${formatNumber(i(t))}`;
359 | };
360 | }
361 |
362 | svg.selectAll('*').remove();
363 | dateSvg.selectAll('*').remove();
364 | countrySvg.selectAll('*').remove();
365 | updateBars = bars(svg);
366 | updateLabels = labels(svg);
367 | updateTitleAndDate = titleAndDate(dateSvg);
368 | updateCountryStats = countryStats(countrySvg);
369 | }
370 |
371 | function setActiveCountry(country) {
372 | if (country) {
373 | const { isoCode, name } = country;
374 | activeLocation = { isoCode, name };
375 | } else {
376 | activeLocation = defaultActiveLocation;
377 | }
378 | if (
379 | currentKeyFrameIndex
380 | && (!player.checkIsAnimationPlaying() || player.checkIsFinished())
381 | ) {
382 | const keyframe = activeKeyframes[currentKeyFrameIndex];
383 | updateCountryStats(keyframe, countrySvg.transition().duration(0));
384 | }
385 | }
386 |
387 | export default {
388 | initialize,
389 | loadAnimationData,
390 | setActiveDataSet,
391 | setActiveCountry,
392 | setTime: (time, shouldRender) => {
393 | const last = activeKeyframes.length - 1;
394 | const scaledTime = time * last + 1;
395 | const newIndex = Math.min(Math.floor(scaledTime), last);
396 | if (!shouldRender && newIndex === currentKeyFrameIndex) {
397 | return;
398 | }
399 | currentKeyFrameIndex = newIndex;
400 | const keyframe = activeKeyframes[currentKeyFrameIndex];
401 | const createTransition = (theSvg) => theSvg
402 | .transition()
403 | .duration(duration)
404 | .ease(d3.easeLinear);
405 | x.domain([0, keyframe[1][0].value]);
406 | const chartTransition = createTransition(svg);
407 | updateBars(keyframe, chartTransition);
408 | updateLabels(keyframe, chartTransition);
409 | updateTitleAndDate(keyframe);
410 | const countryStatsTransition = createTransition(countrySvg);
411 | updateCountryStats(keyframe, countryStatsTransition);
412 | },
413 | };
414 |
--------------------------------------------------------------------------------
/src/js/utils/debounce.js:
--------------------------------------------------------------------------------
1 | const debounce = (func, delay) => {
2 | let debounceTimer;
3 | return function(...args) {
4 | const context = this;
5 | clearTimeout(debounceTimer);
6 | debounceTimer = setTimeout(() => func.apply(context, args), delay);
7 | };
8 | };
9 |
10 | export default debounce;
11 |
--------------------------------------------------------------------------------
/src/js/utils/nonBlockingWait.js:
--------------------------------------------------------------------------------
1 | export default function nonBlockingWait(timeout) {
2 | return new Promise((resolve) => setTimeout(resolve, timeout));
3 | }
4 |
--------------------------------------------------------------------------------
/src/js/utils/three-buffer-geometry-utils.js:
--------------------------------------------------------------------------------
1 | const BufferGeometryUtils = {
2 | computeTangents: function (geometry) {
3 | var { index } = geometry;
4 | var { attributes } = geometry;
5 |
6 | // based on http://www.terathon.com/code/tangent.html
7 | // (per vertex tangents)
8 |
9 | if (index === null
10 | || attributes.position === undefined
11 | || attributes.normal === undefined
12 | || attributes.uv === undefined) {
13 | console.error('THREE.BufferGeometryUtils: .computeTangents() failed. Missing required attributes (index, position, normal or uv)');
14 | return;
15 | }
16 |
17 | var indices = index.array;
18 | var positions = attributes.position.array;
19 | var normals = attributes.normal.array;
20 | var uvs = attributes.uv.array;
21 |
22 | var nVertices = positions.length / 3;
23 |
24 | if (attributes.tangent === undefined) {
25 | geometry.setAttribute('tangent', new THREE.BufferAttribute(new Float32Array(4 * nVertices), 4));
26 | }
27 |
28 | var tangents = attributes.tangent.array;
29 |
30 | var tan1 = []; var
31 | tan2 = [];
32 |
33 | for (var i = 0; i < nVertices; i++) {
34 | tan1[i] = new THREE.Vector3();
35 | tan2[i] = new THREE.Vector3();
36 | }
37 |
38 | var vA = new THREE.Vector3();
39 | var vB = new THREE.Vector3();
40 | var vC = new THREE.Vector3();
41 |
42 | var uvA = new THREE.Vector2();
43 | var uvB = new THREE.Vector2();
44 | var uvC = new THREE.Vector2();
45 |
46 | var sdir = new THREE.Vector3();
47 | var tdir = new THREE.Vector3();
48 |
49 | function handleTriangle(a, b, c) {
50 | vA.fromArray(positions, a * 3);
51 | vB.fromArray(positions, b * 3);
52 | vC.fromArray(positions, c * 3);
53 |
54 | uvA.fromArray(uvs, a * 2);
55 | uvB.fromArray(uvs, b * 2);
56 | uvC.fromArray(uvs, c * 2);
57 |
58 | vB.sub(vA);
59 | vC.sub(vA);
60 |
61 | uvB.sub(uvA);
62 | uvC.sub(uvA);
63 |
64 | var r = 1.0 / (uvB.x * uvC.y - uvC.x * uvB.y);
65 |
66 | // silently ignore degenerate uv triangles having coincident or colinear vertices
67 |
68 | if (!isFinite(r)) return;
69 |
70 | sdir.copy(vB).multiplyScalar(uvC.y).addScaledVector(vC, -uvB.y).multiplyScalar(r);
71 | tdir.copy(vC).multiplyScalar(uvB.x).addScaledVector(vB, -uvC.x).multiplyScalar(r);
72 |
73 | tan1[a].add(sdir);
74 | tan1[b].add(sdir);
75 | tan1[c].add(sdir);
76 |
77 | tan2[a].add(tdir);
78 | tan2[b].add(tdir);
79 | tan2[c].add(tdir);
80 | }
81 |
82 | var { groups } = geometry;
83 |
84 | if (groups.length === 0) {
85 | groups = [{
86 | start: 0,
87 | count: indices.length,
88 | }];
89 | }
90 |
91 | for (var i = 0, il = groups.length; i < il; ++i) {
92 | var group = groups[i];
93 |
94 | var { start } = group;
95 | var { count } = group;
96 |
97 | for (var j = start, jl = start + count; j < jl; j += 3) {
98 | handleTriangle(
99 | indices[j + 0],
100 | indices[j + 1],
101 | indices[j + 2],
102 | );
103 | }
104 | }
105 |
106 | var tmp = new THREE.Vector3(); var
107 | tmp2 = new THREE.Vector3();
108 | var n = new THREE.Vector3(); var
109 | n2 = new THREE.Vector3();
110 | var w; var t; var
111 | test;
112 |
113 | function handleVertex(v) {
114 | n.fromArray(normals, v * 3);
115 | n2.copy(n);
116 |
117 | t = tan1[v];
118 |
119 | // Gram-Schmidt orthogonalize
120 |
121 | tmp.copy(t);
122 | tmp.sub(n.multiplyScalar(n.dot(t))).normalize();
123 |
124 | // Calculate handedness
125 |
126 | tmp2.crossVectors(n2, t);
127 | test = tmp2.dot(tan2[v]);
128 | w = (test < 0.0) ? -1.0 : 1.0;
129 |
130 | tangents[v * 4] = tmp.x;
131 | tangents[v * 4 + 1] = tmp.y;
132 | tangents[v * 4 + 2] = tmp.z;
133 | tangents[v * 4 + 3] = w;
134 | }
135 |
136 | for (var i = 0, il = groups.length; i < il; ++i) {
137 | var group = groups[i];
138 |
139 | var { start } = group;
140 | var { count } = group;
141 |
142 | for (var j = start, jl = start + count; j < jl; j += 3) {
143 | handleVertex(indices[j + 0]);
144 | handleVertex(indices[j + 1]);
145 | handleVertex(indices[j + 2]);
146 | }
147 | }
148 | },
149 |
150 | /**
151 | * @param {Array} geometries
152 | * @param {Boolean} useGroups
153 | * @return {THREE.BufferGeometry}
154 | */
155 | mergeBufferGeometries: function (geometries, useGroups) {
156 | var isIndexed = geometries[0].index !== null;
157 |
158 | var attributesUsed = new Set(Object.keys(geometries[0].attributes));
159 | var morphAttributesUsed = new Set(Object.keys(geometries[0].morphAttributes));
160 |
161 | var attributes = {};
162 | var morphAttributes = {};
163 |
164 | var { morphTargetsRelative } = geometries[0];
165 |
166 | var mergedGeometry = new THREE.BufferGeometry();
167 |
168 | var offset = 0;
169 |
170 | for (var i = 0; i < geometries.length; ++i) {
171 | var geometry = geometries[i];
172 | var attributesCount = 0;
173 |
174 | // ensure that all geometries are indexed, or none
175 |
176 | if (isIndexed !== (geometry.index !== null)) {
177 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.');
178 | return null;
179 | }
180 |
181 | // gather attributes, exit early if they're different
182 |
183 | for (var name in geometry.attributes) {
184 | if (!attributesUsed.has(name)) {
185 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. All geometries must have compatible attributes; make sure "' + name + '" attribute exists among all geometries, or in none of them.');
186 | return null;
187 | }
188 |
189 | if (attributes[name] === undefined) attributes[name] = [];
190 |
191 | attributes[name].push(geometry.attributes[name]);
192 |
193 | attributesCount++;
194 | }
195 |
196 | // ensure geometries have the same number of attributes
197 |
198 | if (attributesCount !== attributesUsed.size) {
199 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. Make sure all geometries have the same number of attributes.');
200 | return null;
201 | }
202 |
203 | // gather morph attributes, exit early if they're different
204 |
205 | if (morphTargetsRelative !== geometry.morphTargetsRelative) {
206 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. .morphTargetsRelative must be consistent throughout all geometries.');
207 | return null;
208 | }
209 |
210 | for (var name in geometry.morphAttributes) {
211 | if (!morphAttributesUsed.has(name)) {
212 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.');
213 | return null;
214 | }
215 |
216 | if (morphAttributes[name] === undefined) morphAttributes[name] = [];
217 |
218 | morphAttributes[name].push(geometry.morphAttributes[name]);
219 | }
220 |
221 | // gather .userData
222 |
223 | mergedGeometry.userData.mergedUserData = mergedGeometry.userData.mergedUserData || [];
224 | mergedGeometry.userData.mergedUserData.push(geometry.userData);
225 |
226 | if (useGroups) {
227 | var count;
228 |
229 | if (isIndexed) {
230 | count = geometry.index.count;
231 | } else if (geometry.attributes.position !== undefined) {
232 | count = geometry.attributes.position.count;
233 | } else {
234 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. The geometry must have either an index or a position attribute');
235 | return null;
236 | }
237 |
238 | mergedGeometry.addGroup(offset, count, i);
239 |
240 | offset += count;
241 | }
242 | }
243 |
244 | // merge indices
245 |
246 | if (isIndexed) {
247 | var indexOffset = 0;
248 | var mergedIndex = [];
249 |
250 | for (var i = 0; i < geometries.length; ++i) {
251 | var { index } = geometries[i];
252 |
253 | for (var j = 0; j < index.count; ++j) {
254 | mergedIndex.push(index.getX(j) + indexOffset);
255 | }
256 |
257 | indexOffset += geometries[i].attributes.position.count;
258 | }
259 |
260 | mergedGeometry.setIndex(mergedIndex);
261 | }
262 |
263 | // merge attributes
264 |
265 | for (var name in attributes) {
266 | var mergedAttribute = this.mergeBufferAttributes(attributes[name]);
267 |
268 | if (!mergedAttribute) {
269 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' attribute.');
270 | return null;
271 | }
272 |
273 | mergedGeometry.setAttribute(name, mergedAttribute);
274 | }
275 |
276 | // merge morph attributes
277 |
278 | for (var name in morphAttributes) {
279 | var numMorphTargets = morphAttributes[name][0].length;
280 |
281 | if (numMorphTargets === 0) break;
282 |
283 | mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {};
284 | mergedGeometry.morphAttributes[name] = [];
285 |
286 | for (var i = 0; i < numMorphTargets; ++i) {
287 | var morphAttributesToMerge = [];
288 |
289 | for (var j = 0; j < morphAttributes[name].length; ++j) {
290 | morphAttributesToMerge.push(morphAttributes[name][j][i]);
291 | }
292 |
293 | var mergedMorphAttribute = this.mergeBufferAttributes(morphAttributesToMerge);
294 |
295 | if (!mergedMorphAttribute) {
296 | console.error('THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' morphAttribute.');
297 | return null;
298 | }
299 |
300 | mergedGeometry.morphAttributes[name].push(mergedMorphAttribute);
301 | }
302 | }
303 |
304 | return mergedGeometry;
305 | },
306 |
307 | /**
308 | * @param {Array} attributes
309 | * @return {THREE.BufferAttribute}
310 | */
311 | mergeBufferAttributes: function (attributes) {
312 | var TypedArray;
313 | var itemSize;
314 | var normalized;
315 | var arrayLength = 0;
316 |
317 | for (var i = 0; i < attributes.length; ++i) {
318 | var attribute = attributes[i];
319 |
320 | if (attribute.isInterleavedBufferAttribute) {
321 | console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. InterleavedBufferAttributes are not supported.');
322 | return null;
323 | }
324 |
325 | if (TypedArray === undefined) TypedArray = attribute.array.constructor;
326 | if (TypedArray !== attribute.array.constructor) {
327 | console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.');
328 | return null;
329 | }
330 |
331 | if (itemSize === undefined) itemSize = attribute.itemSize;
332 | if (itemSize !== attribute.itemSize) {
333 | console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.');
334 | return null;
335 | }
336 |
337 | if (normalized === undefined) normalized = attribute.normalized;
338 | if (normalized !== attribute.normalized) {
339 | console.error('THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.');
340 | return null;
341 | }
342 |
343 | arrayLength += attribute.array.length;
344 | }
345 |
346 | var array = new TypedArray(arrayLength);
347 | var offset = 0;
348 |
349 | for (var i = 0; i < attributes.length; ++i) {
350 | array.set(attributes[i].array, offset);
351 |
352 | offset += attributes[i].array.length;
353 | }
354 |
355 | return new THREE.BufferAttribute(array, itemSize, normalized);
356 | },
357 |
358 | /**
359 | * @param {Array} attributes
360 | * @return {Array}
361 | */
362 | interleaveAttributes: function (attributes) {
363 | // Interleaves the provided attributes into an InterleavedBuffer and returns
364 | // a set of InterleavedBufferAttributes for each attribute
365 | var TypedArray;
366 | var arrayLength = 0;
367 | var stride = 0;
368 |
369 | // calculate the the length and type of the interleavedBuffer
370 | for (var i = 0, l = attributes.length; i < l; ++i) {
371 | var attribute = attributes[i];
372 |
373 | if (TypedArray === undefined) TypedArray = attribute.array.constructor;
374 | if (TypedArray !== attribute.array.constructor) {
375 | console.error('AttributeBuffers of different types cannot be interleaved');
376 | return null;
377 | }
378 |
379 | arrayLength += attribute.array.length;
380 | stride += attribute.itemSize;
381 | }
382 |
383 | // Create the set of buffer attributes
384 | var interleavedBuffer = new THREE.InterleavedBuffer(new TypedArray(arrayLength), stride);
385 | var offset = 0;
386 | var res = [];
387 | var getters = ['getX', 'getY', 'getZ', 'getW'];
388 | var setters = ['setX', 'setY', 'setZ', 'setW'];
389 |
390 | for (var j = 0, l = attributes.length; j < l; j++) {
391 | var attribute = attributes[j];
392 | var { itemSize } = attribute;
393 | var { count } = attribute;
394 | var iba = new THREE.InterleavedBufferAttribute(interleavedBuffer, itemSize, offset, attribute.normalized);
395 | res.push(iba);
396 |
397 | offset += itemSize;
398 |
399 | // Move the data for each attribute into the new interleavedBuffer
400 | // at the appropriate offset
401 | for (var c = 0; c < count; c++) {
402 | for (var k = 0; k < itemSize; k++) {
403 | iba[setters[k]](c, attribute[getters[k]](c));
404 | }
405 | }
406 | }
407 |
408 | return res;
409 | },
410 |
411 | /**
412 | * @param {Array} geometry
413 | * @return {number}
414 | */
415 | estimateBytesUsed: function (geometry) {
416 | // Return the estimated memory used by this geometry in bytes
417 | // Calculate using itemSize, count, and BYTES_PER_ELEMENT to account
418 | // for InterleavedBufferAttributes.
419 | var mem = 0;
420 | for (var name in geometry.attributes) {
421 | var attr = geometry.getAttribute(name);
422 | mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT;
423 | }
424 |
425 | var indices = geometry.getIndex();
426 | mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0;
427 | return mem;
428 | },
429 |
430 | /**
431 | * @param {THREE.BufferGeometry} geometry
432 | * @param {number} tolerance
433 | * @return {THREE.BufferGeometry>}
434 | */
435 | mergeVertices: function (geometry, tolerance = 1e-4) {
436 | tolerance = Math.max(tolerance, Number.EPSILON);
437 |
438 | // Generate an index buffer if the geometry doesn't have one, or optimize it
439 | // if it's already available.
440 | var hashToIndex = {};
441 | var indices = geometry.getIndex();
442 | var positions = geometry.getAttribute('position');
443 | var vertexCount = indices ? indices.count : positions.count;
444 |
445 | // next value for triangle indices
446 | var nextIndex = 0;
447 |
448 | // attributes and new attribute arrays
449 | var attributeNames = Object.keys(geometry.attributes);
450 | var attrArrays = {};
451 | var morphAttrsArrays = {};
452 | var newIndices = [];
453 | var getters = ['getX', 'getY', 'getZ', 'getW'];
454 |
455 | // initialize the arrays
456 | for (var i = 0, l = attributeNames.length; i < l; i++) {
457 | var name = attributeNames[i];
458 |
459 | attrArrays[name] = [];
460 |
461 | var morphAttr = geometry.morphAttributes[name];
462 | if (morphAttr) {
463 | morphAttrsArrays[name] = new Array(morphAttr.length).fill().map(() => []);
464 | }
465 | }
466 |
467 | // convert the error tolerance to an amount of decimal places to truncate to
468 | var decimalShift = Math.log10(1 / tolerance);
469 | var shiftMultiplier = Math.pow(10, decimalShift);
470 | for (var i = 0; i < vertexCount; i++) {
471 | var index = indices ? indices.getX(i) : i;
472 |
473 | // Generate a hash for the vertex attributes at the current index 'i'
474 | var hash = '';
475 | for (var j = 0, l = attributeNames.length; j < l; j++) {
476 | var name = attributeNames[j];
477 | var attribute = geometry.getAttribute(name);
478 | var { itemSize } = attribute;
479 |
480 | for (var k = 0; k < itemSize; k++) {
481 | // double tilde truncates the decimal value
482 | hash += `${~~(attribute[getters[k]](index) * shiftMultiplier)},`;
483 | }
484 | }
485 |
486 | // Add another reference to the vertex if it's already
487 | // used by another index
488 | if (hash in hashToIndex) {
489 | newIndices.push(hashToIndex[hash]);
490 | } else {
491 | // copy data to the new index in the attribute arrays
492 | for (var j = 0, l = attributeNames.length; j < l; j++) {
493 | var name = attributeNames[j];
494 | var attribute = geometry.getAttribute(name);
495 | var morphAttr = geometry.morphAttributes[name];
496 | var { itemSize } = attribute;
497 | var newarray = attrArrays[name];
498 | var newMorphArrays = morphAttrsArrays[name];
499 |
500 | for (var k = 0; k < itemSize; k++) {
501 | var getterFunc = getters[k];
502 | newarray.push(attribute[getterFunc](index));
503 |
504 | if (morphAttr) {
505 | for (var m = 0, ml = morphAttr.length; m < ml; m++) {
506 | newMorphArrays[m].push(morphAttr[m][getterFunc](index));
507 | }
508 | }
509 | }
510 | }
511 |
512 | hashToIndex[hash] = nextIndex;
513 | newIndices.push(nextIndex);
514 | nextIndex++;
515 | }
516 | }
517 |
518 | // Generate typed arrays from new attribute arrays and update
519 | // the attributeBuffers
520 | const result = geometry.clone();
521 | for (var i = 0, l = attributeNames.length; i < l; i++) {
522 | var name = attributeNames[i];
523 | var oldAttribute = geometry.getAttribute(name);
524 |
525 | var buffer = new oldAttribute.array.constructor(attrArrays[name]);
526 | var attribute = new THREE.BufferAttribute(buffer, oldAttribute.itemSize, oldAttribute.normalized);
527 |
528 | result.setAttribute(name, attribute);
529 |
530 | // Update the attribute arrays
531 | if (name in morphAttrsArrays) {
532 | for (var j = 0; j < morphAttrsArrays[name].length; j++) {
533 | var oldMorphAttribute = geometry.morphAttributes[name][j];
534 |
535 | var buffer = new oldMorphAttribute.array.constructor(morphAttrsArrays[name][j]);
536 | var morphAttribute = new THREE.BufferAttribute(buffer, oldMorphAttribute.itemSize, oldMorphAttribute.normalized);
537 | result.morphAttributes[name][j] = morphAttribute;
538 | }
539 | }
540 | }
541 |
542 | // indices
543 |
544 | result.setIndex(newIndices);
545 |
546 | return result;
547 | },
548 |
549 | /**
550 | * @param {THREE.BufferGeometry} geometry
551 | * @param {number} drawMode
552 | * @return {THREE.BufferGeometry>}
553 | */
554 | toTrianglesDrawMode: function (geometry, drawMode) {
555 | if (drawMode === THREE.TrianglesDrawMode) {
556 | console.warn('THREE.BufferGeometryUtils.toTrianglesDrawMode(): Geometry already defined as triangles.');
557 | return geometry;
558 | }
559 |
560 | if (drawMode === THREE.TriangleFanDrawMode || drawMode === THREE.TriangleStripDrawMode) {
561 | var index = geometry.getIndex();
562 |
563 | // generate index if not present
564 |
565 | if (index === null) {
566 | var indices = [];
567 |
568 | var position = geometry.getAttribute('position');
569 |
570 | if (position !== undefined) {
571 | for (var i = 0; i < position.count; i++) {
572 | indices.push(i);
573 | }
574 |
575 | geometry.setIndex(indices);
576 | index = geometry.getIndex();
577 | } else {
578 | console.error('THREE.BufferGeometryUtils.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.');
579 | return geometry;
580 | }
581 | }
582 |
583 | //
584 |
585 | var numberOfTriangles = index.count - 2;
586 | var newIndices = [];
587 |
588 | if (drawMode === THREE.TriangleFanDrawMode) {
589 | // gl.TRIANGLE_FAN
590 |
591 | for (var i = 1; i <= numberOfTriangles; i++) {
592 | newIndices.push(index.getX(0));
593 | newIndices.push(index.getX(i));
594 | newIndices.push(index.getX(i + 1));
595 | }
596 | } else {
597 | // gl.TRIANGLE_STRIP
598 |
599 | for (var i = 0; i < numberOfTriangles; i++) {
600 | if (i % 2 === 0) {
601 | newIndices.push(index.getX(i));
602 | newIndices.push(index.getX(i + 1));
603 | newIndices.push(index.getX(i + 2));
604 | } else {
605 | newIndices.push(index.getX(i + 2));
606 | newIndices.push(index.getX(i + 1));
607 | newIndices.push(index.getX(i));
608 | }
609 | }
610 | }
611 |
612 | if ((newIndices.length / 3) !== numberOfTriangles) {
613 | console.error('THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unable to generate correct amount of triangles.');
614 | }
615 |
616 | // build final geometry
617 |
618 | var newGeometry = geometry.clone();
619 | newGeometry.setIndex(newIndices);
620 | newGeometry.clearGroups();
621 |
622 | return newGeometry;
623 | }
624 |
625 | console.error('THREE.BufferGeometryUtils.toTrianglesDrawMode(): Unknown draw mode:', drawMode);
626 | return geometry;
627 | },
628 |
629 | };
630 |
631 | export default BufferGeometryUtils;
632 |
--------------------------------------------------------------------------------
/src/js/webgl-check.js:
--------------------------------------------------------------------------------
1 | const WEBGL = {
2 | isWebGLAvailable() {
3 | try {
4 | const canvas = document.createElement('canvas');
5 | return !!(
6 | window.WebGLRenderingContext
7 | && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))
8 | );
9 | } catch (e) {
10 | return false;
11 | }
12 | },
13 |
14 | isWebGL2Available() {
15 | try {
16 | const canvas = document.createElement('canvas');
17 | return !!(window.WebGL2RenderingContext && canvas.getContext('webgl2'));
18 | } catch (e) {
19 | return false;
20 | }
21 | },
22 |
23 | getWebGLErrorMessage() {
24 | return this.getErrorMessage(1);
25 | },
26 |
27 | getWebGL2ErrorMessage() {
28 | return this.getErrorMessage(2);
29 | },
30 |
31 | getErrorMessage(version) {
32 | const names = {
33 | 1: 'WebGL',
34 | 2: 'WebGL 2',
35 | };
36 |
37 | const contexts = {
38 | 1: window.WebGLRenderingContext,
39 | 2: window.WebGL2RenderingContext,
40 | };
41 |
42 | let message = 'Your $0 does not seem to support $1 which is required by this project';
43 |
44 | const element = document.createElement('div');
45 | element.id = 'webglmessage';
46 | element.style.fontFamily = 'monospace';
47 | element.style.fontSize = '13px';
48 | element.style.fontWeight = 'normal';
49 | element.style.textAlign = 'center';
50 | element.style.background = '#fff';
51 | element.style.color = '#000';
52 | element.style.padding = '1.5em';
53 | element.style.width = '400px';
54 | element.style.margin = '5em auto 0';
55 |
56 | if (contexts[version]) {
57 | message = message.replace('$0', 'graphics card');
58 | } else {
59 | message = message.replace('$0', 'browser');
60 | }
61 | message = message.replace('$1', names[version]);
62 | element.innerHTML = message;
63 | return element;
64 | },
65 | };
66 |
67 | export default function() {
68 | if (!WEBGL.isWebGLAvailable()) {
69 | const element = document.getElementsByClassName('error-message')[0];
70 | element.classList.remove('hidden');
71 | element.appendChild(WEBGL.getErrorMessage(2));
72 | return false;
73 | }
74 | return true;
75 | }
76 |
--------------------------------------------------------------------------------
/src/scss/app.scss:
--------------------------------------------------------------------------------
1 | @import 'nouislider';
2 |
3 | html {
4 | height: 100%;
5 | }
6 | body {
7 | height: 100%;
8 | margin: 0;
9 | padding: 0;
10 | color: #999999;
11 | font-size: 13px;
12 | font-family: sans-serif;
13 | line-height: 20px;
14 | background-color: #222;
15 | background-image: url('../images/body_background.png');
16 | background-size: cover;
17 | background-position: center;
18 | }
19 | .content {
20 | height: 100%;
21 | transition: 0.1s all;
22 | }
23 | * {
24 | font-family: 'PT Mono', monospace;
25 | }
26 | canvas {
27 | display: block;
28 | }
29 | .content.blurred {
30 | /* filter: blur(4px); -- very slow on Mozilla */
31 | opacity: 0.1;
32 | pointer-events: all;
33 | }
34 | .hidden {
35 | display: none !important;
36 | }
37 | .invisible {
38 | visibility: hidden !important;
39 | }
40 | .error-message {
41 | margin-top: 50px;
42 | }
43 | .chart {
44 | width: 70%;
45 | position: absolute;
46 | z-index: 10;
47 | user-select: none;
48 | pointer-events: none;
49 | top: 68px;
50 | left: 0;
51 |
52 | &-tip {
53 | display: none;
54 | font-style: italic;
55 | margin-left: 20px;
56 | margin-bottom: -10px;
57 |
58 | @media (min-width: 1024px) {
59 | display: block;
60 | }
61 | }
62 | }
63 | .date {
64 | position: absolute;
65 | z-index: 10;
66 | user-select: none;
67 | pointer-events: none;
68 | top: 0;
69 | width: 300px;
70 | left: calc(50vw - 150px);
71 | }
72 | .container-tip {
73 | display: none;
74 | position: absolute;
75 | font-style: italic;
76 |
77 | @media (min-width: 600px) {
78 | display: block;
79 | bottom: 20%;
80 | right: 0;
81 | width: 200px;
82 | }
83 |
84 | @media (min-width: 1200px) {
85 | bottom: 15%;
86 | width: 300px;
87 | margin-left: 18px;
88 | }
89 | }
90 | .country-stats {
91 | z-index: 11;
92 | user-select: none;
93 | pointer-events: none;
94 | padding-left: 8px;
95 | position: absolute;
96 | min-width: 180px;
97 | bottom: 120px;
98 | transform: scale(0.8);
99 | }
100 | .controller {
101 | height: 84px;
102 | z-index: 11;
103 | position: absolute;
104 | bottom: 40px;
105 | width: 100%;
106 | .controls {
107 | display: flex;
108 | justify-content: center;
109 | position: relative;
110 | }
111 | .progress {
112 | border: none;
113 | height: 6px;
114 | cursor: pointer;
115 | margin-top: 20px;
116 | }
117 | .noUi-handle {
118 | outline: none;
119 | width: 16px;
120 | height: 16px;
121 | right: -4px;
122 | box-shadow: none;
123 | border-radius: 50%;
124 | border-color: #ff0000;
125 | border: 1px solid #ff0000;
126 | background-color: #ff0000;
127 | transition: 0.1s all;
128 | cursor: pointer;
129 | }
130 | .noUi-active {
131 | width: 18px;
132 | height: 18px;
133 | cursor: grabbing;
134 | }
135 | .noUi-connect {
136 | background-color: #ff0000;
137 | }
138 | }
139 | .controller-inner {
140 | margin: auto;
141 | width: 90%;
142 | }
143 | .noUi-handle {
144 | &:after {
145 | display: none;
146 | }
147 | &:before {
148 | display: none;
149 | }
150 | }
151 | .noUi-connects {
152 | background-color: #999;
153 | }
154 | .playback-button-wrapper {
155 | cursor: pointer;
156 | width: 36px;
157 | height: 36px;
158 | border: 1px solid #ffffff;
159 | border-radius: 50%;
160 | margin-bottom: 10px;
161 | position: relative;
162 | margin-right: 20px;
163 | margin-left: 20px;
164 | }
165 | .playback-button {
166 | cursor: pointer;
167 | outline: none;
168 | border: 0;
169 | background: transparent;
170 | fill: #ffffff;
171 | opacity: 0.85;
172 | padding: 0;
173 | margin: 0;
174 | width: 36px;
175 | position: absolute;
176 | top: 0;
177 | left: 0;
178 | }
179 | .replay-button {
180 | cursor: pointer;
181 | outline: none;
182 | border: 0;
183 | background: transparent;
184 | opacity: 0.85;
185 | padding: 0;
186 | margin: 0;
187 | margin-left: 10px;
188 | font-size: 24px;
189 | svg {
190 | fill: #ffffff;
191 | }
192 | }
193 | .replay-button.enabled {
194 | svg {
195 | fill: #ff0000;
196 | }
197 | }
198 | .dataset-select {
199 | margin-right: 10px;
200 | margin-top: 12px;
201 | }
202 | .dataset-select-list {
203 | position: absolute;
204 | background-color: #ffffff;
205 | list-style: none;
206 | margin: 12px 0;
207 | padding: 0;
208 | bottom: 44px;
209 | }
210 | .dataset-select-list-item {
211 | color: #000;
212 | cursor: pointer;
213 | transition: color 0.3s, background-color 0.3s;
214 | padding: 4px 14px;
215 | font-size: 14px;
216 | &:first-of-type {
217 | border-bottom: 1px solid #cccccc;
218 | }
219 | &:hover {
220 | color: #fff;
221 | background-color: #cc0000;
222 | }
223 | }
224 | .dataset-select-list-item.active {
225 | color: #fff;
226 | background-color: #cc0000;
227 | }
228 | .dataset-select-button {
229 | cursor: pointer;
230 | outline: none;
231 | border: 0;
232 | background: transparent;
233 | opacity: 0.85;
234 | padding: 0;
235 | margin: 0;
236 | font-size: 22px;
237 | }
238 | .about {
239 | right: 0;
240 | cursor: pointer;
241 | border-radius: 50%;
242 | border: 1px solid #fff;
243 | width: 24px;
244 | height: 24px;
245 | text-align: center;
246 | position: absolute;
247 | margin-top: 15px;
248 | span {
249 | color: #fff;
250 | margin-top: 2px;
251 | display: inline-block;
252 | font-size: 16px;
253 | }
254 | }
255 | .about-card {
256 | position: absolute;
257 | z-index: 20;
258 | color: #ddd;
259 | line-height: 20px;
260 | background-color: rgba(0,0,0,0);
261 | width: 100%;
262 | >h1 {
263 | text-transform: uppercase;
264 | }
265 | h1 {
266 | text-align: center;
267 | }
268 | h3 {
269 | margin-top: 18px;
270 | margin-bottom: 0;
271 | }
272 | >p {
273 | margin-top: 6px;
274 | }
275 | a {
276 | color: #ff0000;
277 | &:hover {
278 | color: #cc0000;
279 | }
280 | }
281 | .about-card-close {
282 | fill: #fff;
283 | width: 20px;
284 | cursor: pointer;
285 | margin-top: 16px;
286 | position: absolute;
287 | right: 14px;
288 | }
289 | }
290 | .about-card-inner {
291 | position: relative;
292 | margin-top: 30px;
293 | padding: 20px;
294 | max-width: 600px;
295 | margin-left: auto;
296 | margin-right: auto;
297 | }
298 | h3 {
299 | text-transform: uppercase;
300 | }
301 | ul {
302 | margin-top: 6px;
303 | }
304 | .a-coffee-stuff {
305 | display: flex;
306 | margin-bottom: 70px;
307 | justify-content: center;
308 | a {
309 | display: inline-block;
310 | margin: auto;
311 | text-align: center;
312 | }
313 | }
314 | .playback-speed {
315 | user-select: none;
316 | cursor: pointer;
317 | color: white;
318 | text-align: center;
319 | vertical-align: bottom;
320 | margin: 0;
321 | padding: 0;
322 | height: 24px;
323 | margin-top: 12px;
324 | margin-left: -28px;
325 | }
326 | .playback-speed-times {
327 | vertical-align: bottom;
328 | font-size: 24px;
329 | margin-bottom: -1px;
330 | display: inline-block;
331 | }
332 | .playback-speed-value {
333 | font-size: 16px;
334 | margin-left: -8px;
335 | }
336 |
337 | .search {
338 | width: 100%;
339 | z-index: 20;
340 | color: #ddd;
341 | line-height: 18px;
342 | position: absolute;
343 | background-color: rgba(0,0,0,0);
344 |
345 | input {
346 | color: #fff;
347 | width: calc(100% - 35px);
348 | font-size: 16px;
349 | background: none;
350 | border: none;
351 | border-bottom: 1px solid #fff;
352 | padding: 16px;
353 | outline: none !important;
354 |
355 | @media (min-width: 600px) {
356 | font-size: 24px;
357 | }
358 | }
359 |
360 | &-close {
361 | fill: #fff;
362 | width: 20px;
363 | cursor: pointer;
364 | margin-top: 16px;
365 | position: absolute;
366 | right: 25px;
367 | }
368 |
369 | &-suggestions {
370 | position: relative;
371 |
372 | .frs-hide-scroll {
373 | margin-bottom: -20px;
374 | min-height: calc(60vh + 30px);
375 |
376 | &::after {
377 | content: '';
378 | display: block;
379 | height: 50px;
380 | left: 0;
381 | position: absolute;
382 | bottom: -50px;
383 | width: 100%;
384 | z-index: -1;
385 | box-shadow: 0 0 40px 0px rgba(255, 255, 255, 0.2);
386 | }
387 | }
388 |
389 | &-list {
390 | list-style: none;
391 | margin: 0;
392 | padding: 0;
393 | margin-top: 10px;
394 | max-height: 60vh;
395 |
396 | &-item {
397 | cursor: pointer;
398 | width: 100%;
399 | font-size: 14px;
400 | padding: 16px;
401 | transition: 0.2s all;
402 |
403 | @media (min-width: 600px) {
404 | font-size: 20px;
405 | }
406 |
407 | &:hover {
408 | background-color: rgba(255, 255, 255, 0.1);
409 | padding-left: 22px;
410 | }
411 | }
412 | }
413 | }
414 |
415 | &-button {
416 | cursor: pointer;
417 | outline: none;
418 | background: transparent;
419 | opacity: 0.85;
420 | padding: 0;
421 | color: #fff;
422 | padding: 4px 10px;
423 | font-size: 12px;
424 | border: 1px solid #fff;
425 | transition: 0.3s all;
426 | margin-left: auto;
427 |
428 | &:hover {
429 | color: #ff0000;
430 | padding: 4px 12px;
431 | border: 1px solid #ff0000;
432 | }
433 |
434 | @media (min-width: 600px) {
435 | font-size: 16px;
436 | }
437 |
438 | &-wrapper {
439 | z-index: 10;
440 | right: 0;
441 | position: absolute;
442 | display: flex;
443 | flex-direction: column;
444 | margin-top: 80px;
445 | margin-right: 10px;
446 |
447 | @media (min-width: 600px) {
448 | margin-top: 32px;
449 | margin-right: 32px;
450 | }
451 | }
452 |
453 | &-tip {
454 | display: none;
455 | font-style: italic;
456 |
457 | @media (min-width: 1024px) {
458 | display: block;
459 | }
460 | }
461 | }
462 |
463 | &-inner {
464 | position: relative;
465 | margin-top: 30px;
466 | padding: 20px;
467 | max-width: 600px;
468 | margin-left: auto;
469 | margin-right: auto;
470 | }
471 | }
472 |
473 |
474 | @media (min-width: 380px) {
475 | .chart {
476 | width: 60%;
477 | }
478 | }
479 | @media (min-width: 500px) {
480 | .country-stats {
481 | left: 100px;
482 | transform: scale(0.9);
483 | }
484 | }
485 | @media (min-width: 700px) {
486 | .country-stats {
487 | left: 150px;
488 | }
489 | }
490 | @media (min-width: 800px) {
491 | .chart {
492 | width: 50%;
493 | }
494 | }
495 | @media (min-width: 1024px) {
496 | .chart {
497 | top: 20px;
498 | width: 40%;
499 | }
500 | .controller-inner {
501 | width: 60%;
502 | }
503 | }
504 | @media (min-width: 1200px) {
505 | .chart {
506 | width: 35%;
507 | }
508 | .country-stats {
509 | top: 15vh;
510 | transform: none;
511 | left: calc(50vw - 200px);
512 | }
513 | .controller-inner {
514 | width: 40%;
515 | }
516 | }
517 |
518 | @media (min-width: 1440px) {
519 | .chart {
520 | width: 30%;
521 | }
522 | .country-stats {
523 | left: calc(50vw - 300px);
524 | }
525 | }
526 |
527 | .loading {
528 | margin: 0;
529 | padding: 0;
530 | height: 100%;
531 |
532 | &-content {
533 | width: 200px;
534 | top: 20%;
535 | left: calc(50vw - 100px);
536 | position: absolute;
537 | text-align: center;
538 | color: #ddd;
539 |
540 | @media (min-width: 600px) {
541 | left: 20%;
542 | width: 300px;
543 | text-align: left;
544 | }
545 | }
546 | }
547 |
548 | .loader {
549 | position: absolute;
550 | height: 4px;
551 | display: block;
552 | width: 100%;
553 | background-color: #E9EBF0;
554 | border-radius: 2px;
555 | background-clip: padding-box;
556 | margin: 0.5rem 0 1rem 0;
557 | overflow: hidden;
558 |
559 | &-wrapper {
560 | position: relative;
561 | width: 100%;
562 | }
563 | }
564 | .loader .indeterminate {
565 | background-color: #ff0000;
566 | }
567 |
568 | .loader .indeterminate:before {
569 | content: '';
570 | position: absolute;
571 | background-color: inherit;
572 | top: 0;
573 | left: 0;
574 | bottom: 0;
575 | will-change: left, right;
576 | -webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
577 | animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
578 | }
579 | .loader .indeterminate:after {
580 | content: '';
581 | position: absolute;
582 | background-color: inherit;
583 | top: 0;
584 | left: 0;
585 | bottom: 0;
586 | will-change: left, right;
587 | -webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
588 | animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
589 | -webkit-animation-delay: 1.15s;
590 | animation-delay: 1.15s;
591 | }
592 | @-webkit-keyframes indeterminate {
593 | 0% {
594 | left: -35%;
595 | right: 100%;
596 | }
597 | 60% {
598 | left: 100%;
599 | right: -90%;
600 | }
601 | 100% {
602 | left: 100%;
603 | right: -90%;
604 | }
605 | }
606 | @keyframes indeterminate {
607 | 0% {
608 | left: -35%;
609 | right: 100%;
610 | }
611 | 60% {
612 | left: 100%;
613 | right: -90%;
614 | }
615 | 100% {
616 | left: 100%;
617 | right: -90%;
618 | }
619 | }
620 | @-webkit-keyframes indeterminate-short {
621 | 0% {
622 | left: -200%;
623 | right: 100%; }
624 | 60% {
625 | left: 107%;
626 | right: -8%; }
627 | 100% {
628 | left: 107%;
629 | right: -8%; }
630 | }
631 | @keyframes indeterminate-short {
632 | 0% {
633 | left: -200%;
634 | right: 100%; }
635 | 60% {
636 | left: 107%;
637 | right: -8%; }
638 | 100% {
639 | left: 107%;
640 | right: -8%; }
641 | }
642 |
--------------------------------------------------------------------------------
/src/scss/nouislider.scss:
--------------------------------------------------------------------------------
1 | /*! nouislider - 14.6.2 - 9/16/2020 */
2 | /* Functional styling;
3 | * These styles are required for noUiSlider to function.
4 | * You don't need to change these rules to apply your design.
5 | */
6 | .noUi-target,
7 | .noUi-target * {
8 | -webkit-touch-callout: none;
9 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
10 | -webkit-user-select: none;
11 | -ms-touch-action: none;
12 | touch-action: none;
13 | -ms-user-select: none;
14 | -moz-user-select: none;
15 | user-select: none;
16 | -moz-box-sizing: border-box;
17 | box-sizing: border-box;
18 | }
19 | .noUi-target {
20 | position: relative;
21 | }
22 | .noUi-base,
23 | .noUi-connects {
24 | width: 100%;
25 | height: 100%;
26 | position: relative;
27 | z-index: 1;
28 | }
29 | /* Wrapper for all connect elements.
30 | */
31 | .noUi-connects {
32 | overflow: hidden;
33 | z-index: 0;
34 | }
35 | .noUi-connect,
36 | .noUi-origin {
37 | will-change: transform;
38 | position: absolute;
39 | z-index: 1;
40 | top: 0;
41 | right: 0;
42 | -ms-transform-origin: 0 0;
43 | -webkit-transform-origin: 0 0;
44 | -webkit-transform-style: preserve-3d;
45 | transform-origin: 0 0;
46 | transform-style: flat;
47 | }
48 | .noUi-connect {
49 | height: 100%;
50 | width: 100%;
51 | }
52 | .noUi-origin {
53 | height: 10%;
54 | width: 10%;
55 | }
56 | /* Offset direction
57 | */
58 | .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin {
59 | left: 0;
60 | right: auto;
61 | }
62 | /* Give origins 0 height/width so they don't interfere with clicking the
63 | * connect elements.
64 | */
65 | .noUi-vertical .noUi-origin {
66 | width: 0;
67 | }
68 | .noUi-horizontal .noUi-origin {
69 | height: 0;
70 | }
71 | .noUi-handle {
72 | -webkit-backface-visibility: hidden;
73 | backface-visibility: hidden;
74 | position: absolute;
75 | }
76 | .noUi-touch-area {
77 | height: 100%;
78 | width: 100%;
79 | }
80 | .noUi-state-tap .noUi-connect,
81 | .noUi-state-tap .noUi-origin {
82 | -webkit-transition: transform 0.3s;
83 | transition: transform 0.3s;
84 | }
85 | .noUi-state-drag * {
86 | cursor: inherit !important;
87 | }
88 | /* Slider size and handle placement;
89 | */
90 | .noUi-horizontal {
91 | height: 18px;
92 | }
93 | .noUi-horizontal .noUi-handle {
94 | width: 34px;
95 | height: 28px;
96 | right: -17px;
97 | top: -6px;
98 | }
99 | .noUi-vertical {
100 | width: 18px;
101 | }
102 | .noUi-vertical .noUi-handle {
103 | width: 28px;
104 | height: 34px;
105 | right: -6px;
106 | top: -17px;
107 | }
108 | .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle {
109 | left: -17px;
110 | right: auto;
111 | }
112 | /* Styling;
113 | * Giving the connect element a border radius causes issues with using transform: scale
114 | */
115 | .noUi-target {
116 | background: #FAFAFA;
117 | border-radius: 4px;
118 | border: 1px solid #D3D3D3;
119 | box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB;
120 | }
121 | .noUi-connects {
122 | border-radius: 3px;
123 | }
124 | .noUi-connect {
125 | background: #3FB8AF;
126 | }
127 | /* Handles and cursors;
128 | */
129 | .noUi-draggable {
130 | cursor: ew-resize;
131 | }
132 | .noUi-vertical .noUi-draggable {
133 | cursor: ns-resize;
134 | }
135 | .noUi-handle {
136 | border: 1px solid #D9D9D9;
137 | border-radius: 3px;
138 | background: #FFF;
139 | cursor: default;
140 | box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB;
141 | }
142 | .noUi-active {
143 | box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB;
144 | }
145 | /* Handle stripes;
146 | */
147 | .noUi-handle:before,
148 | .noUi-handle:after {
149 | content: "";
150 | display: block;
151 | position: absolute;
152 | height: 14px;
153 | width: 1px;
154 | background: #E8E7E6;
155 | left: 14px;
156 | top: 6px;
157 | }
158 | .noUi-handle:after {
159 | left: 17px;
160 | }
161 | .noUi-vertical .noUi-handle:before,
162 | .noUi-vertical .noUi-handle:after {
163 | width: 14px;
164 | height: 1px;
165 | left: 6px;
166 | top: 14px;
167 | }
168 | .noUi-vertical .noUi-handle:after {
169 | top: 17px;
170 | }
171 | /* Disabled state;
172 | */
173 | [disabled] .noUi-connect {
174 | background: #B8B8B8;
175 | }
176 | [disabled].noUi-target,
177 | [disabled].noUi-handle,
178 | [disabled] .noUi-handle {
179 | cursor: not-allowed;
180 | }
181 | /* Base;
182 | *
183 | */
184 | .noUi-pips,
185 | .noUi-pips * {
186 | -moz-box-sizing: border-box;
187 | box-sizing: border-box;
188 | }
189 | .noUi-pips {
190 | position: absolute;
191 | color: #999;
192 | }
193 | /* Values;
194 | *
195 | */
196 | .noUi-value {
197 | position: absolute;
198 | white-space: nowrap;
199 | text-align: center;
200 | }
201 | .noUi-value-sub {
202 | color: #ccc;
203 | font-size: 10px;
204 | }
205 | /* Markings;
206 | *
207 | */
208 | .noUi-marker {
209 | position: absolute;
210 | background: #CCC;
211 | }
212 | .noUi-marker-sub {
213 | background: #AAA;
214 | }
215 | .noUi-marker-large {
216 | background: #AAA;
217 | }
218 | /* Horizontal layout;
219 | *
220 | */
221 | .noUi-pips-horizontal {
222 | padding: 10px 0;
223 | height: 80px;
224 | top: 100%;
225 | left: 0;
226 | width: 100%;
227 | }
228 | .noUi-value-horizontal {
229 | -webkit-transform: translate(-50%, 50%);
230 | transform: translate(-50%, 50%);
231 | }
232 | .noUi-rtl .noUi-value-horizontal {
233 | -webkit-transform: translate(50%, 50%);
234 | transform: translate(50%, 50%);
235 | }
236 | .noUi-marker-horizontal.noUi-marker {
237 | margin-left: -1px;
238 | width: 2px;
239 | height: 5px;
240 | }
241 | .noUi-marker-horizontal.noUi-marker-sub {
242 | height: 10px;
243 | }
244 | .noUi-marker-horizontal.noUi-marker-large {
245 | height: 15px;
246 | }
247 | /* Vertical layout;
248 | *
249 | */
250 | .noUi-pips-vertical {
251 | padding: 0 10px;
252 | height: 100%;
253 | top: 0;
254 | left: 100%;
255 | }
256 | .noUi-value-vertical {
257 | -webkit-transform: translate(0, -50%);
258 | transform: translate(0, -50%);
259 | padding-left: 25px;
260 | }
261 | .noUi-rtl .noUi-value-vertical {
262 | -webkit-transform: translate(0, 50%);
263 | transform: translate(0, 50%);
264 | }
265 | .noUi-marker-vertical.noUi-marker {
266 | width: 5px;
267 | height: 2px;
268 | margin-top: -1px;
269 | }
270 | .noUi-marker-vertical.noUi-marker-sub {
271 | width: 10px;
272 | }
273 | .noUi-marker-vertical.noUi-marker-large {
274 | width: 15px;
275 | }
276 | .noUi-tooltip {
277 | display: block;
278 | position: absolute;
279 | border: 1px solid #D9D9D9;
280 | border-radius: 3px;
281 | background: #fff;
282 | color: #000;
283 | padding: 5px;
284 | text-align: center;
285 | white-space: nowrap;
286 | }
287 | .noUi-horizontal .noUi-tooltip {
288 | -webkit-transform: translate(-50%, 0);
289 | transform: translate(-50%, 0);
290 | left: 50%;
291 | bottom: 120%;
292 | }
293 | .noUi-vertical .noUi-tooltip {
294 | -webkit-transform: translate(0, -50%);
295 | transform: translate(0, -50%);
296 | top: 50%;
297 | right: 120%;
298 | }
299 | .noUi-horizontal .noUi-origin > .noUi-tooltip {
300 | -webkit-transform: translate(50%, 0);
301 | transform: translate(50%, 0);
302 | left: auto;
303 | bottom: 10px;
304 | }
305 | .noUi-vertical .noUi-origin > .noUi-tooltip {
306 | -webkit-transform: translate(0, -18px);
307 | transform: translate(0, -18px);
308 | top: auto;
309 | right: 28px;
310 | }
311 |
312 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Assets Config file
3 | */
4 |
5 | const serverConfiguration = {
6 | internal: {
7 | server: {
8 | baseDir: 'dist',
9 | },
10 | port: 3000,
11 | },
12 | external: {
13 | proxy: 'http://localhost:9000/path/to/project/',
14 | },
15 | };
16 |
17 | const path = require('path');
18 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
19 | const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
20 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
21 | const HtmlWebpackPlugin = require('html-webpack-plugin');
22 | const WebpackCdnPlugin = require('webpack-cdn-plugin');
23 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
24 | const TerserPlugin = require('terser-webpack-plugin');
25 | const CopyWebpackPlugin = require('copy-webpack-plugin');
26 | const ImageMinPlugin = require('imagemin-webpack-plugin').default;
27 |
28 | let targetServerConfiguration = serverConfiguration.internal;
29 |
30 | const config = function(env, args) {
31 | if (args.externalServer !== undefined && args.externalServer) {
32 | targetServerConfiguration = serverConfiguration.external;
33 | }
34 |
35 | return {
36 | entry: {
37 | app: './src/js/app.js',
38 | },
39 | output: {
40 | filename: 'js/[name].js',
41 | path: path.resolve(__dirname, 'dist'),
42 | },
43 | module: {
44 | rules: [
45 | {
46 | test: /\.scss$/,
47 | use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'],
48 | },
49 | {
50 | test: /\.js$/,
51 | exclude: /(node_modules|bower_components)/,
52 | loader: 'babel-loader',
53 | },
54 | {
55 | test: /\.(png|gif|jpg|jpeg)$/,
56 | use: [
57 | {
58 | loader: 'url-loader',
59 | options: { name: 'images/design/[name].[hash:6].[ext]', publicPath: '../', limit: 8192 },
60 | },
61 | ],
62 | },
63 | {
64 | test: /\.(eot|svg|ttf|woff|woff2)$/,
65 | use: [
66 | {
67 | loader: 'url-loader',
68 | options: { name: 'fonts/[name].[hash:6].[ext]', publicPath: '../', limit: 8192 },
69 | },
70 | ],
71 | },
72 | ],
73 | },
74 | optimization: {
75 | minimizer: [
76 | new TerserPlugin({
77 | parallel: true,
78 | }),
79 | new OptimizeCssAssetsPlugin({}),
80 | ],
81 | },
82 | watchOptions: {
83 | poll: 1000,
84 | ignored: /node_modules/,
85 | },
86 | plugins: [
87 | new BrowserSyncPlugin({
88 | ...targetServerConfiguration,
89 | files: ['src/*'],
90 | ghostMode: {
91 | clicks: false,
92 | location: false,
93 | forms: false,
94 | scroll: false,
95 | },
96 | injectChanges: true,
97 | logFileChanges: true,
98 | logLevel: 'debug',
99 | logPrefix: 'wepback',
100 | notify: true,
101 | reloadDelay: 0,
102 | }),
103 | new HtmlWebpackPlugin({
104 | inject: true,
105 | hash: false,
106 | filename: 'index.html',
107 | template: path.resolve(__dirname, 'src', 'index.html'),
108 | favicon: path.resolve(__dirname, 'src', 'images', 'favicon.ico'),
109 | }),
110 | new WebpackCdnPlugin({
111 | modules: [
112 | {
113 | name: 'd3',
114 | var: 'd3',
115 | path: 'dist/d3.min.js',
116 | },
117 | {
118 | name: 'three',
119 | var: 'THREE',
120 | path: 'build/three.min.js',
121 | },
122 | {
123 | name: 'three-geojson-geometry',
124 | var: 'GeoJsonGeometry',
125 | path: 'dist/three-geojson-geometry.min.js',
126 | },
127 | {
128 | name: 'nouislider',
129 | var: 'noUiSlider',
130 | path: 'distribute/nouislider.min.js',
131 | },
132 | {
133 | name: 'hammerjs',
134 | var: 'Hammer',
135 | path: 'hammer.min.js',
136 | },
137 | {
138 | name: 'frs-hide-scrollbar',
139 | var: 'FRSHideScrollbar',
140 | path: 'dist/FRS-hide-scrollbar.umd.js',
141 | },
142 | ],
143 | publicPath: '/node_modules',
144 | }),
145 | new MiniCssExtractPlugin({
146 | filename: 'css/[name].css',
147 | }),
148 | // new ImageMinPlugin({ test: /\.(jpg|jpeg|png|gif|svg)$/i }),
149 | new CleanWebpackPlugin({
150 | /**
151 | * Some plugins used do not correctly save to webpack's asset list.
152 | * Disable automatic asset cleaning until resolved
153 | */
154 | cleanStaleWebpackAssets: false,
155 | verbose: true,
156 | }),
157 | new CopyWebpackPlugin({
158 | patterns: [
159 | {
160 | from: path.resolve(__dirname, 'src', 'images'),
161 | to: path.resolve(__dirname, 'dist', 'images'),
162 | toType: 'dir',
163 | },
164 | {
165 | from: path.resolve(__dirname, 'src', 'data/data.json'),
166 | to: path.resolve(__dirname, 'dist', 'data.json'),
167 | },
168 | {
169 | from: path.resolve(__dirname, 'CNAME'),
170 | to: path.resolve(__dirname, 'dist'),
171 | },
172 | ],
173 | }),
174 | ],
175 | };
176 | };
177 |
178 | module.exports = config;
179 |
--------------------------------------------------------------------------------