├── .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 | ![](https://github.com/codingedward/ailing-planet/blob/master/screenshot.png) 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 | [![](https://img.icons8.com/ios/38/ff0000/kawaii-coffee.png) 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 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 52 | 116 | 117 | 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 | --------------------------------------------------------------------------------