├── .gitignore ├── public ├── favicon.png ├── screenshot.jpeg ├── global.css └── index.html ├── src ├── main.js ├── components │ ├── Grid.svelte │ ├── CountrySelector.svelte │ ├── ColorSegment.svelte │ ├── Cell.svelte │ ├── TweenedPath.svelte │ ├── Canvas.svelte │ ├── GradientPath.svelte │ └── Chart.svelte ├── utils │ ├── datetime.js │ └── geometry.js ├── App.svelte └── stores │ └── data.js ├── package.json ├── README.md └── rollup.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/higsch/covid-spiral/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/higsch/covid-spiral/HEAD/public/screenshot.jpeg -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body 5 | }); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, body { 8 | position: relative; 9 | width: 100%; 10 | height: 100%; 11 | font-size: 16px; 12 | background-color: #e5f9ff; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Grid.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /src/utils/datetime.js: -------------------------------------------------------------------------------- 1 | const monthDays = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 2 | 3 | export const getDayOfYear = (date) => { 4 | const month = +date.getMonth(); 5 | const day = +date.getDate(); 6 | 7 | const previousMonthDays = monthDays.slice(0, month).reduce((acc, cur) => acc + cur, 0); 8 | 9 | return previousMonthDays + day - 1; 10 | }; 11 | 12 | export const getYearDays = (day, year, firstYear) => day + (366 * (year - firstYear)); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/CountrySelector.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "covid-spiral", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "d3": "^7.3.0", 14 | "d3-interpolate-path": "^2.2.3", 15 | "rollup": "^2.3.4", 16 | "rollup-plugin-css-only": "^3.1.0", 17 | "rollup-plugin-livereload": "^2.0.0", 18 | "rollup-plugin-svelte": "^7.0.0", 19 | "rollup-plugin-terser": "^7.0.0", 20 | "svelte": "^3.0.0" 21 | }, 22 | "dependencies": { 23 | "sirv-cli": "^2.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ColorSegment.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Cell.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
15 |

16 | {title} 17 |

18 |
19 | 23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /src/utils/geometry.js: -------------------------------------------------------------------------------- 1 | export const getPositionFromDistanceAndAngle = (distance, angle) => { 2 | const x = distance * Math.cos((angle * Math.PI) / 180); 3 | const y = distance * Math.sin((angle * Math.PI) / 180); 4 | return { x, y }; 5 | }; 6 | 7 | const pathToArray = path => path.split(/(?=[a-zA-Z])/g); 8 | 9 | const stripFirst = path => { 10 | const arr = pathToArray(path); 11 | return arr.slice(1).join(''); 12 | }; 13 | 14 | export const fusePaths = (path1, path2) => { 15 | if (!path1 || !path2) return null; 16 | const path2WithoutM = stripFirst(path2); 17 | const fused = `${path1}${path2WithoutM}Z`; 18 | return fused; 19 | }; 20 | 21 | export const getClosedPath = ({ x1, y1, x2, y2, x3, y3, x4, y4 }) => { 22 | return `M${x1} ${y1}L${x2} ${y2}L${x3} ${y3}L${x4} ${y4}Z`; 23 | }; -------------------------------------------------------------------------------- /src/components/TweenedPath.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if (maskId)} 20 | 21 | 27 | 28 | {:else} 29 | 35 | {/if} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # covid-spiral 2 | 3 | Small study to see how one can code a spiral-shaped data visualization of covid-19 cases and deaths. 4 | 5 | Inspired by the [New York Times](https://www.nytimes.com/2022/01/06/opinion/omicron-covid-us.html). 6 | 7 | ![The spiral](public/screenshot.jpeg) 8 | 9 | Spiral: Time (top: January, then clockwise through the months) 10 | 11 | Colored area: 7-day average of new cases per one million population 12 | 13 | Black area: 7-day average of new deaths per one million population **x 10** 14 | 15 | Data source: Our world in data 16 | 17 | 18 | ## Run the viz locally 19 | 20 | Install [npm](https://www.npmjs.com/get-npm). 21 | 22 | ``` 23 | git clone https://github.com/higsch/covid-spiral.git 24 | cd covid-spiral 25 | 26 | npm install 27 | 28 | npm run dev 29 | ``` 30 | 31 | Open your browser at `localhost:8080`. 32 | 33 | 34 | ## Created with 35 | 36 | HTML, CSS, JavaScript, Svelte, D3 37 | 38 | 39 | ## Created by 40 | 41 | Matthias Stahl, 2022 42 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
13 |
16 | 17 | {#each $continentData as { location, data }} 18 | 24 | {/each} 25 | 26 |
27 |
30 | 31 | {#each $wealthStatusData as { location, data }} 32 | 38 | {/each} 39 | 40 |
41 |
44 | 45 | {#each $countryData as { location, data }} 46 | 52 | {/each} 53 | 54 |
55 |
56 | 57 | -------------------------------------------------------------------------------- /src/components/Canvas.svelte: -------------------------------------------------------------------------------- 1 | 77 | 78 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import css from 'rollup-plugin-css-only'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | function serve() { 11 | let server; 12 | 13 | function toExit() { 14 | if (server) server.kill(0); 15 | } 16 | 17 | return { 18 | writeBundle() { 19 | if (server) return; 20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 21 | stdio: ['ignore', 'inherit', 'inherit'], 22 | shell: true 23 | }); 24 | 25 | process.on('SIGTERM', toExit); 26 | process.on('exit', toExit); 27 | } 28 | }; 29 | } 30 | 31 | export default { 32 | input: 'src/main.js', 33 | output: { 34 | sourcemap: true, 35 | format: 'iife', 36 | name: 'app', 37 | file: 'public/build/bundle.js' 38 | }, 39 | plugins: [ 40 | svelte({ 41 | compilerOptions: { 42 | // enable run-time checks when not in production 43 | dev: !production 44 | } 45 | }), 46 | // we'll extract any component CSS out into 47 | // a separate file - better for performance 48 | css({ output: 'bundle.css' }), 49 | 50 | // If you have external dependencies installed from 51 | // npm, you'll most likely need these plugins. In 52 | // some cases you'll need additional configuration - 53 | // consult the documentation for details: 54 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 55 | resolve({ 56 | browser: true, 57 | dedupe: ['svelte'] 58 | }), 59 | commonjs(), 60 | 61 | // In dev mode, call `npm run start` once 62 | // the bundle has been generated 63 | !production && serve(), 64 | 65 | // Watch the `public` directory and refresh the 66 | // browser on changes when not in production 67 | !production && livereload('public'), 68 | 69 | // If we're building for production (npm run build 70 | // instead of npm run dev), minify 71 | production && terser() 72 | ], 73 | watch: { 74 | clearScreen: false 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/GradientPath.svelte: -------------------------------------------------------------------------------- 1 | 57 | 58 | 63 | {#each renderedData as d} 64 | 68 | {/each} 69 | -------------------------------------------------------------------------------- /src/stores/data.js: -------------------------------------------------------------------------------- 1 | import { readable, writable, derived } from 'svelte/store'; 2 | import { csv, autoType, sum, groups } from 'd3'; 3 | 4 | import { getDayOfYear, getYearDays } from '../utils/datetime'; 5 | 6 | const dataPath = 'https://raw.githubusercontent.com/owid/covid-19-data/master/public/data/owid-covid-data.csv'; 7 | const exclude = [ 8 | 'International' 9 | ]; 10 | const continents = [ 11 | 'North America', 12 | 'South America', 13 | 'Europe', 14 | 'Africa', 15 | 'Oceania', 16 | 'Asia', 17 | 'World' 18 | ]; 19 | const wealthStatus = [ 20 | 'High income', 21 | 'Upper middle income', 22 | 'Lower middle income', 23 | 'Low income' 24 | ]; 25 | 26 | const getSevenDayIncidence = (data, i, population) => { 27 | return 100000 * sum(data.slice(Math.max(0, i - 7), i + 1)) / population; 28 | }; 29 | 30 | export const selectedLocation = writable('Germany'); 31 | 32 | export const rawData = readable([], async set => { 33 | const data = await csv(dataPath, autoType); 34 | set(data); 35 | }); 36 | 37 | export const locations = derived(rawData, $rawData => { 38 | return [...new Set($rawData.map(d => d.location))].sort(); 39 | }); 40 | 41 | export const data = derived(rawData, $rawData => { 42 | const formattedData = $rawData.map((d, _, arr) => { 43 | const dayOfYear = getDayOfYear(d.date); 44 | const incidence = +d.new_cases_smoothed_per_million; 45 | const deaths = +d.new_deaths_smoothed_per_million * 10; 46 | const icu = +d.icu_patients_per_million; 47 | const { date, location } = d; 48 | return { 49 | location, 50 | date, 51 | dayOfYear, 52 | cumDay: getYearDays(dayOfYear, date.getFullYear(), arr[0].date.getFullYear()), 53 | incidence, 54 | deaths, 55 | icu 56 | }; 57 | }).filter(d => !exclude.includes(d.location)); 58 | 59 | return formattedData; 60 | }); 61 | 62 | export const objData = derived(data, $data => { 63 | const objData = groups($data, d => d.location).map(([ key, value ]) => { 64 | return { 65 | location: key, 66 | data: value 67 | }; 68 | }); 69 | 70 | return objData; 71 | }); 72 | 73 | export const continentData = derived(objData, $objData => { 74 | return $objData.filter(d => continents.includes(d.location)); 75 | }); 76 | 77 | export const countryData = derived(objData, $objData => { 78 | return $objData.filter(d => !continents.includes(d.location)).filter(d => d.location === 'Germany'); 79 | }); 80 | 81 | export const wealthStatusData = derived(objData, $objData => { 82 | return $objData.filter(d => wealthStatus.includes(d.location)); 83 | }); -------------------------------------------------------------------------------- /src/components/Chart.svelte: -------------------------------------------------------------------------------- 1 | 97 | 98 |
103 | 109 | 113 | 116 | 120 | 121 | 122 |
123 | 124 | --------------------------------------------------------------------------------