├── .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 | 
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 |
--------------------------------------------------------------------------------
/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 |
122 |
123 |
124 |
--------------------------------------------------------------------------------