├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── global.css
├── index.html
├── laureates.csv
├── preview.png
└── steps.json
├── rollup.config.js
├── src
├── App.svelte
├── components
│ ├── Canvas.svelte
│ ├── ClusterLabels.svelte
│ ├── HistogramAxis.svelte
│ ├── HoverLabel.svelte
│ ├── HoverLabels.svelte
│ ├── Laureate.svelte
│ ├── Laureates.svelte
│ ├── Legend.svelte
│ ├── ScrollLabel.svelte
│ ├── ScrollyVisualization.svelte
│ └── Sources.svelte
├── main.js
├── stores
│ ├── colors.js
│ ├── data.js
│ ├── devices.js
│ ├── dimensions.js
│ └── steps.js
└── utils
│ ├── canvas.js
│ ├── colors.js
│ ├── dictionary.js
│ ├── format.js
│ └── layout.js
└── svelte.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /public/build/
3 |
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nobel-Laureates
2 |
3 | > a DER SPIEGEL scrollytelling
4 |
5 |
6 |
7 | A scrollytelling to highlight all Nobel laureates since 1901. Built with [Svelte](https://svelte.dev) and [D3](https://d3js.org).
8 |
9 | Live [here](https://www.spiegel.de/wissenschaft/zirkel-der-genies-a-90c50289-30ac-4a4b-bc49-348676ce6687) (S+) or on your local machine (see below).
10 |
11 | `steps.json` contains the configuration of the scrollytelling chapters. `laureates.csv` holds the individual Nobel laureate data from the [The Nobel Foundation](https://www.nobelprize.org/about/developer-zone-2/).
12 |
13 | ## Run locally
14 |
15 | ```
16 | git clone https://github.com/spiegelgraphics/nobel-laureates.git
17 | cd nobel-laureates
18 | npm install
19 | npm run dev
20 | ```
21 |
22 |
23 | ## Built by
24 |
25 | the [DER SPIEGEL](https://www.spiegel.de) graphics department, 2022.
26 |
27 | The application was slightly changed compared to its original to be able to run it outside of a dedicated CMS.
28 |
29 |
30 | ## License
31 |
32 | Apache License Version 2.0
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nobel-laureates",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "rollup -c",
7 | "dev": "rollup -c -w",
8 | "start": "sirv public"
9 | },
10 | "devDependencies": {
11 | "@rollup/plugin-commonjs": "^17.0.0",
12 | "@rollup/plugin-node-resolve": "^11.0.0",
13 | "@sveltejs/svelte-scroller": "^2.0.7",
14 | "d3": "^7.4.4",
15 | "lodash-es": "^4.17.21",
16 | "rollup": "^2.41.4",
17 | "rollup-plugin-css-only": "^3.1.0",
18 | "rollup-plugin-livereload": "^2.0.0",
19 | "rollup-plugin-svelte": "^7.0.0",
20 | "rollup-plugin-terser": "^7.0.2",
21 | "svelte": "^3.0.0"
22 | },
23 | "dependencies": {
24 | "node-sass": "^4.14.1",
25 | "sirv-cli": "^2.0.2",
26 | "svelte-preprocess": "^4.6.9"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/public/global.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 |
7 | html, body {
8 | width: 100%;
9 | height: 100%;
10 | font-family: Arial, sans-serif;
11 | font-size: 16px;
12 | }
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Nobel laureates
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/public/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/spiegelgraphics/nobel-laureates/f7a66cac1065582225ca7c23b790fb93edf76668/public/preview.png
--------------------------------------------------------------------------------
/public/steps.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "showTick": true,
4 | "largeTick": true,
5 | "cluster_by": "prize_category",
6 | "inner_layout": {
7 | "type": "phyllotaxis"
8 | },
9 | "outer_layout": {
10 | "type": "clusterForce"
11 | },
12 | "highlightIds": [],
13 | "exclusiveIds": [997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009],
14 | "description": {
15 | "title": "Klimawandel, Kolonialismus, Meinungsfreiheit",
16 | "body": "Auch zwei Deutsche sind unter den aktuellen Gewinnern. Den Nobelpreis für Chemie bekamen Benjamin List und der in Schottland geborene David W.C. MacMillan."
17 | },
18 | "showCategoryLabels": false
19 | },
20 | {
21 | "showTick": true,
22 | "largeTick": false,
23 | "cluster_by": "prize_category",
24 | "inner_layout": {
25 | "type": "phyllotaxis"
26 | },
27 | "outer_layout": {
28 | "type": "clusterForce"
29 | },
30 | "highlightIds": [1002, 1003],
31 | "exclusiveIds": [997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009],
32 | "description": {
33 | "title": "Klimawandel, Kolonialismus, Meinungsfreiheit",
34 | "body": "Die Forscher fanden heraus, dass auch kleine organische Moleküle chemische Reaktionen vermitteln."
35 | },
36 | "showCategoryLabels": false
37 | },
38 | {
39 | "showTick": true,
40 | "largeTick": false,
41 | "cluster_by": "prize_category",
42 | "inner_layout": {
43 | "type": "phyllotaxis"
44 | },
45 | "outer_layout": {
46 | "type": "clusterForce"
47 | },
48 | "highlightIds": [999, 1000, 1001],
49 | "exclusiveIds": [997, 998, 999, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009],
50 | "description": {
51 | "title": "Klimawandel, Kolonialismus, Meinungsfreiheit",
52 | "body": "In der Kategorie Physik ging der Preis an den Deutschen Klaus Hasselmann sowie dessen Kollegen Giorgio Parisi und Syukuro Manabe. Hasselmann entwickelte unter anderem ein Modell, das zeigt, wie schnelle Temperaturschwankungen der Atmosphäre die Ozeantemperatur langfristig beeinflussen."
53 | },
54 | "showCategoryLabels": false
55 | },
56 | {
57 | "showTick": true,
58 | "largeTick": true,
59 | "cluster_by": "uniform",
60 | "inner_layout": {
61 | "type": "phyllotaxis"
62 | },
63 | "outer_layout": {
64 | "type": "clusterForce"
65 | },
66 | "highlightIds": [],
67 | "exclusiveIds": [],
68 | "description": {
69 | "title": "Geballte Genies",
70 | "body": "Seit 1901 erhielten insgesamt 947 Menschen die Ehrung. Sie kann auch an Organisationen verliehen werden, was in 28 Fällen in Form eines Friedensnobelpreises geschah. In manchen Jahren fiel die Preisvergabe aus, etwa in den Weltkriegsjahren 1914-18 und 1939-1945."
71 | },
72 | "showCategoryLabels": false
73 | },
74 | {
75 | "showTick": true,
76 | "largeTick": true,
77 | "cluster_by": "prize_category",
78 | "inner_layout": {
79 | "type": "phyllotaxis"
80 | },
81 | "outer_layout": {
82 | "type": "clusterForce"
83 | },
84 | "highlightIds": [],
85 | "exclusiveIds": [],
86 | "description": {
87 | "title": "Führende Medizin",
88 | "body": "Die meisten Preisträger stammen aus dem Bereich Medizin (224), dicht gefolgt von Physik (219), Chemie (186), Literatur (118) und Frieden (109). Erst seit 1968 werden Forscherinnen und Forscher der Wirtschaftswissenschaften (89) geehrt."
89 | },
90 | "showCategoryLabels": true
91 | },
92 | {
93 | "showTick": true,
94 | "largeTick": true,
95 | "cluster_by": "prize_category",
96 | "inner_layout": {
97 | "type": "phyllotaxis"
98 | },
99 | "outer_layout": {
100 | "type": "clusterForce"
101 | },
102 | "highlightIds": [222, 66, 217, 6],
103 | "exclusiveIds": [],
104 | "description": {
105 | "title": "Zweifach geehrt",
106 | "body": "In seltenen Fällen räumen Forscher doppelt ab. Bei insgesamt vier Wissenschaftlerinnen und Wissenschaftlern ist das in der Vergangenheit der Fall gewesen."
107 | },
108 | "showCategoryLabels": true
109 | },
110 | {
111 | "showTick": true,
112 | "largeTick": false,
113 | "cluster_by": "prize_category",
114 | "inner_layout": {
115 | "type": "phyllotaxis"
116 | },
117 | "outer_layout": {
118 | "type": "clusterForce"
119 | },
120 | "highlightIds": [6],
121 | "exclusiveIds": [],
122 | "description": {
123 | "title": "Zweifach geehrt",
124 | "body": "Ein besonders berühmtes Beispiel: Marie Curie. Ihr wurde 1903 der Physiknobelpreis für die Pionierleistung auf dem Gebiet der spontanen Radioaktivität verliehen und 1911 der Chemienobelpreis für die Isolierung des Elements Radium."
125 | },
126 | "showCategoryLabels": true
127 | },
128 | {
129 | "showTick": true,
130 | "largeTick": true,
131 | "cluster_by": "gender",
132 | "inner_layout": {
133 | "type": "phyllotaxis"
134 | },
135 | "outer_layout": {
136 | "type": "clusterForce"
137 | },
138 | "highlightIds": [
139 | 843, 892, 543, 990, 553, 428, 468, 535, 836, 453,
140 | 962, 817, 230, 782, 846, 835, 869, 506, 991, 983,
141 | 963, 824, 615, 438, 344, 597, 844, 194, 496, 992,
142 | 565, 870, 775, 993, 536, 914, 79, 1005, 6, 904, 540,
143 | 967, 668, 640, 979, 610, 554, 435, 413, 579, 773,
144 | 601, 924, 871, 670, 918, 783, 673, 1005
145 | ],
146 | "exclusiveIds": [],
147 | "description": {
148 | "title": "Unterrepräsentierte Frauen",
149 | "body": "Erst 58 Frauen haben bisher den Nobelpreis erhalten. Auch in diesem Jahr war das Männer-Frauen-Verhältnis unausgewogen."
150 | },
151 | "showCategoryLabels": true
152 | },
153 | {
154 | "showTick": true,
155 | "largeTick": false,
156 | "cluster_by": "gender",
157 | "inner_layout": {
158 | "type": "phyllotaxis"
159 | },
160 | "outer_layout": {
161 | "type": "clusterForce"
162 | },
163 | "highlightIds": [
164 | 1005
165 | ],
166 | "exclusiveIds": [],
167 | "description": {
168 | "title": "Unterrepräsentierte Frauen",
169 | "body": "Von den diesjährigen 13 Gewinnern ist nur eine Person weiblich: Die philippinische Journalistin Maria Ressa. Sie teilt sich den Friedensnobelpreis mit dem russischen Journalisten Dmitri Muratow. Beide werden für ihren Kampf für die Pressefreiheit und ihren Einsatz bei der Aufdeckung von Korruption und Machtmissbrauch geehrt."
170 | },
171 | "showCategoryLabels": true
172 | },
173 | {
174 | "showTick": true,
175 | "largeTick": true,
176 | "cluster_by": "gender",
177 | "inner_layout": {
178 | "type": "phyllotaxis"
179 | },
180 | "outer_layout": {
181 | "type": "clusterForce"
182 | },
183 | "highlightIds": [453],
184 | "exclusiveIds": [],
185 | "description": {
186 | "title": "Made in Germany",
187 | "body": "Eine der wenigen deutschen Preisträgerinnen ist die Biologin und Biochemikerin Christiane Nüsslein-Volhard. Sie bekam die Ehrung für die Erforschung genetischer Kontrolle der frühen Embryonalentwicklung."
188 | },
189 | "showCategoryLabels": true
190 | },
191 | {
192 | "showTick": true,
193 | "largeTick": true,
194 | "cluster_by": "uniform",
195 | "inner_layout": {
196 | "type": "histogram"
197 | },
198 | "outer_layout": {
199 | "type": "none"
200 | },
201 | "highlightIds": [],
202 | "exclusiveIds": [],
203 | "description": {
204 | "title": "Von jung bis alt",
205 | "body": "Die meisten Nobelpreisträger können auf eine lange Karriere zurückblicken. In der Regel sind die Gewinner älter als dreißig Jahre. Doch es gibt Ausnahmen und gleich zwei davon sind Frauen."
206 | },
207 | "showCategoryLabels": false
208 | },
209 | {
210 | "showTick": true,
211 | "largeTick": true,
212 | "cluster_by": "prize_category",
213 | "inner_layout": {
214 | "type": "histogram"
215 | },
216 | "outer_layout": {
217 | "type": "none"
218 | },
219 | "highlightIds": [],
220 | "exclusiveIds": [],
221 | "description": {
222 | "title": null,
223 | "body": null
224 | },
225 | "showCategoryLabels": false
226 | },
227 | {
228 | "showTick": true,
229 | "largeTick": true,
230 | "cluster_by": "prize_category",
231 | "inner_layout": {
232 | "type": "histogram"
233 | },
234 | "outer_layout": {
235 | "type": "none"
236 | },
237 | "highlightIds": [914],
238 | "exclusiveIds": [],
239 | "description": {
240 | "title": "Die jüngsten ...",
241 | "body": "2014 erhielt die damals 17-jährige Malala Yousafzai zusammen mit Kailash Satyarthi den Friedensnobelpreis für ihr Eintreten gegen die Unterdrückung von jungen Menschen und für deren Recht auf Bildung."
242 | },
243 | "showCategoryLabels": false
244 | },
245 | {
246 | "showTick": true,
247 | "largeTick": false,
248 | "cluster_by": "prize_category",
249 | "inner_layout": {
250 | "type": "histogram"
251 | },
252 | "outer_layout": {
253 | "type": "none"
254 | },
255 | "highlightIds": [967],
256 | "exclusiveIds": [],
257 | "description": {
258 | "title": "Die jüngsten ...",
259 | "body": "Ebenfalls den Friedensnobelpreis bekam die zur Zeit der Ehrung 25-jährige Irakerin Nadia Murad für ihren Einsatz gegen sexuelle Gewalt in Kriegsgebieten."
260 | },
261 | "showCategoryLabels": false
262 | },
263 | {
264 | "showTick": true,
265 | "largeTick": true,
266 | "cluster_by": "prize_category",
267 | "inner_layout": {
268 | "type": "histogram"
269 | },
270 | "outer_layout": {
271 | "type": "none"
272 | },
273 | "highlightIds": [976],
274 | "exclusiveIds": [],
275 | "description": {
276 | "title": "... und die Ältesten",
277 | "body": "John B. Goodenough war 97 Jahre alt, als er 2019 für die Entwicklung von Lithium-Ionen-Batterien den Chemie-Nobelpreis verliehen bekam. Posthum wird der Nobelpreis seit 1974 nicht mehr verliehen, es sei denn die Anwärter versterben zwischen Nominierung und Preisverleihung."
278 | },
279 | "showCategoryLabels": false
280 | },
281 | {
282 | "showTick": true,
283 | "largeTick": true,
284 | "cluster_by": "affiliation_country",
285 | "inner_layout": {
286 | "type": "phyllotaxis"
287 | },
288 | "outer_layout": {
289 | "type": "clusterForce"
290 | },
291 | "highlightIds": [],
292 | "exclusiveIds": [],
293 | "description": {
294 | "title": "Gebündelte Brainpower",
295 | "body": "Die Daten aller Nobelpreisgewinner zeigen: Es gibt Staaten die besonders gut darin sind, schlaue Köpfe ins Land zu locken. So lebte ein Großteil zum Zeitpunkt der Verleihung in den USA. Im Ranking folgen Großbritannien und Deutschland."
296 | },
297 | "showCategoryLabels": true
298 | },
299 | {
300 | "showTick": true,
301 | "largeTick": true,
302 | "cluster_by": "birth_country",
303 | "inner_layout": {
304 | "type": "phyllotaxis"
305 | },
306 | "outer_layout": {
307 | "type": "clusterForce"
308 | },
309 | "highlightIds": [],
310 | "exclusiveIds": [],
311 | "description": {
312 | "title": "Weltweites Wissen",
313 | "body": "Schlüsselt man die Preisträger nach Geburtsland auf ergibt sich hingegen ein diverseres Bild. Doch auch hier ist erkennbar – die meisten stammen aus Europa und den USA."
314 | },
315 | "showCategoryLabels": true
316 | },
317 | {
318 | "showTick": true,
319 | "largeTick": true,
320 | "cluster_by": "uniform",
321 | "inner_layout": {
322 | "type": "phyllotaxis"
323 | },
324 | "outer_layout": {
325 | "type": "clusterForce"
326 | },
327 | "highlightIds": [
328 | 199, 164, 185, 26, 304, 111, 1002, 445, 189, 500, 453, 166, 161, 340, 244, 127,
329 | 444, 379, 14, 253, 430, 128, 816, 240, 31, 491, 188, 136, 354, 329, 187, 823,
330 | 272, 647, 184, 530, 727, 617, 216, 156, 130, 80, 134, 30, 945, 270, 24, 976,
331 | 266, 228, 1000, 493, 233, 391, 23, 19, 640, 210, 202, 331, 312, 322, 580, 64,
332 | 941, 989, 176, 271, 799, 297, 578, 76, 394, 571, 793, 886, 602, 62, 361, 38,
333 | 1, 529, 739, 137
334 | ],
335 | "exclusiveIds": [],
336 | "description": {
337 | "title": "Dichter und Denker",
338 | "body": "Von den bisherigen Nobelpreisträgern sind 84 in Deutschland geboren. Berühmte Namen wie Max Planck, Albert Einstein oder Willy Brandt gehören dazu. Schon bei der ersten Preisverleihung 1901 wurde ein Mann geehrt, den heute wohl jedes Kind kennt. Zumindest jedes, das sich schon mal etwas gebrochen hat: Wilhelm Conrad Röntgen."
339 | },
340 | "showCategoryLabels": true
341 | },
342 | {
343 | "showTick": true,
344 | "largeTick": true,
345 | "cluster_by": "uniform",
346 | "inner_layout": {
347 | "type": "phyllotaxis"
348 | },
349 | "outer_layout": {
350 | "type": "clusterForce"
351 | },
352 | "highlightIds": [
353 | 199, 164, 185, 26, 304, 111, 1002, 445, 189, 500, 453, 166, 161, 340, 244, 127,
354 | 444, 379, 14, 253, 430, 128, 816, 240, 31, 491, 188, 136, 354, 329, 187, 823,
355 | 272, 647, 184, 530, 727, 617, 216, 156, 130, 80, 134, 30, 945, 270, 24, 976,
356 | 266, 228, 1000, 493, 233, 391, 23, 19, 640, 210, 202, 331, 312, 322, 580, 64,
357 | 941, 989, 176, 271, 799, 297, 578, 76, 394, 571, 793, 886, 602, 62, 361, 38,
358 | 1, 529, 739, 137
359 | ],
360 | "exclusiveIds": [],
361 | "description": {
362 | "title": null,
363 | "body": null
364 | },
365 | "showCategoryLabels": true
366 | }
367 | ]
--------------------------------------------------------------------------------
/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 | import preprocess from 'svelte-preprocess';
8 |
9 | const production = !process.env.ROLLUP_WATCH;
10 |
11 | function serve() {
12 | let server;
13 |
14 | function toExit() {
15 | if (server) server.kill(0);
16 | }
17 |
18 | return {
19 | writeBundle() {
20 | if (server) return;
21 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
22 | stdio: ['ignore', 'inherit', 'inherit'],
23 | shell: true
24 | });
25 |
26 | process.on('SIGTERM', toExit);
27 | process.on('exit', toExit);
28 | }
29 | };
30 | }
31 |
32 | export default {
33 | input: 'src/main.js',
34 | output: {
35 | sourcemap: true,
36 | format: 'iife',
37 | name: 'app',
38 | file: 'public/build/bundle.js'
39 | },
40 | plugins: [
41 | svelte({
42 | compilerOptions: {
43 | // enable run-time checks when not in production
44 | dev: !production
45 | },
46 | preprocess: preprocess()
47 | }),
48 | // we'll extract any component CSS out into
49 | // a separate file - better for performance
50 | css({ output: 'bundle.css' }),
51 |
52 | // If you have external dependencies installed from
53 | // npm, you'll most likely need these plugins. In
54 | // some cases you'll need additional configuration -
55 | // consult the documentation for details:
56 | // https://github.com/rollup/plugins/tree/master/packages/commonjs
57 | resolve({
58 | browser: true,
59 | dedupe: ['svelte']
60 | }),
61 | commonjs(),
62 |
63 | // In dev mode, call `npm run start` once
64 | // the bundle has been generated
65 | !production && serve(),
66 |
67 | // Watch the `public` directory and refresh the
68 | // browser on changes when not in production
69 | !production && livereload('public'),
70 |
71 | // If we're building for production (npm run build
72 | // instead of npm run dev), minify
73 | production && terser()
74 | ],
75 | watch: {
76 | clearScreen: false
77 | }
78 | };
79 |
--------------------------------------------------------------------------------
/src/App.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
20 |
21 |
25 |
30 |
34 |
38 |
39 |
40 | {#each $steps as { id, description, largeTick } (id)}
41 |
45 | {/each}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/components/Canvas.svelte:
--------------------------------------------------------------------------------
1 |
62 |
63 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/components/ClusterLabels.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 | {#if (showLabels)}
22 |
23 | {#each labels as { id, x, y, label } (id)}
24 |
29 | {label}
30 |
31 | {/each}
32 |
33 | {/if}
34 |
35 |
--------------------------------------------------------------------------------
/src/components/HistogramAxis.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
18 |
22 |
23 | {#each ticks as tick}
24 |
28 | {tick}
29 |
30 | {/each}
31 |
32 |
33 | {label}
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/components/HoverLabel.svelte:
--------------------------------------------------------------------------------
1 |
47 |
48 |
55 |
56 |
57 | {name}
58 |
59 | {#if (hasCloseButton)}
60 |
63 | +
64 |
65 | {/if}
66 |
67 | {#if dictionary[birth_country]}
68 |
Geburtsort: {dictionary[birth_country]}
69 | {/if}
70 |
71 | {years[0]}: {dictionary[categories[0]]}
72 |
73 |
74 | »{@html motivations[0].trim()}«
75 |
76 | {#if (years[1])}
77 |
78 | {years[1]}: {dictionary[categories[1]]}
79 |
80 | {/if}
81 | {#if (motivations[1])}
82 |
83 | »{@html motivations[1].trim()}«
84 |
85 | {/if}
86 |
87 |
88 |
--------------------------------------------------------------------------------
/src/components/HoverLabels.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | {#if (hoverItem)}
13 | hoverItem = null}
19 | />
20 | {/if}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/Laureate.svelte:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Laureates.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 |
54 |
61 | {#each renderedData as { id, xTransformed: x, yTransformed: y, radius, withinClusterIndex = 0, outlines, show } (id)}
62 | {#if (show)}
63 |
73 | {/if}
74 | {/each}
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/components/Legend.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 | {#each items as { label, color }}
16 |
17 |
18 | {label}
19 |
20 | {/each}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/components/ScrollLabel.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
11 |
12 | {@html title}
13 |
14 |
15 | {@html body}
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/components/ScrollyVisualization.svelte:
--------------------------------------------------------------------------------
1 |
43 |
44 | {#if (ready)}
45 |
81 | {/if}
82 |
83 |
--------------------------------------------------------------------------------
/src/components/Sources.svelte:
--------------------------------------------------------------------------------
1 |
4 |
Quelle: The Nobel Foundation
5 |
6 | Anm. d. Red.: Es sind ausschließlich natürliche Personen gezeigt.
7 | Zur Berechnung des Alters bei Preisvergabe wurde im Falle von mehreren Nobelpreisen pro Person der frühere Nobelpreis herangezogen.
8 | Bei Geburts- und Aufenthaltsland wurde jeweils das im Datensatz erstgenannte verwendet. Es handelt sich außerdem um die heutigen Länderbezeichnungen.
9 | Fehlen bei einer Person die jeweils gezeigten Daten, wurde diese Person von der Darstellung entfernt.
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/src/stores/colors.js:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 |
3 | export const laureateColor = writable('#ffffff');
4 | export const invertedLaureateColor = writable('#E64415');
--------------------------------------------------------------------------------
/src/stores/data.js:
--------------------------------------------------------------------------------
1 | import { readable, derived } from 'svelte/store';
2 | import { csv, autoType, scaleLinear, extent } from 'd3';
3 | import { sortBy } from 'lodash-es';
4 |
5 | import { currentStep } from './steps';
6 | import { radius, width, canvasHeight as height } from './dimensions';
7 | import { layoutClusterForce, layoutPhyllotaxis, layoutHistogram } from '../utils/layout';
8 | import { prizeCategoryColors } from '../utils/colors';
9 |
10 | const dataPath = 'laureates.csv';
11 |
12 | const splitProperty = (s) => {
13 | if (s === undefined || s === null) return [];
14 | const sString = s.toString();
15 | return sString ? sString.split('|') : [];
16 | };
17 |
18 | export const rawLaureates = readable([], async set => {
19 | const data = await csv(dataPath, autoType);
20 |
21 | const fData = data.map(d => {
22 | return {
23 | ...d,
24 | uniform: splitProperty(d.uniform),
25 | gender: splitProperty(d.gender),
26 | birth_country: splitProperty(d.birth_country),
27 | prize_category: splitProperty(d.prize_category),
28 | prize_motivation: splitProperty(d.prize_motivation),
29 | prize_year: splitProperty(d.prize_year).map(d => +d),
30 | affiliation_country: splitProperty(d.affiliation_country),
31 | this_year: d.this_year === 'TRUE'
32 | };
33 | });
34 |
35 | const sortedData = sortBy(fData, item => {
36 | return Object.keys(prizeCategoryColors).indexOf(item.prize_category[0])
37 | });
38 |
39 | set(sortedData);
40 | });
41 |
42 | export const ageScale = derived([rawLaureates, width], ([$rawLaureates, $width]) => {
43 | const padding = 16;
44 | const scale = scaleLinear()
45 | .domain(extent($rawLaureates.filter(d => !isNaN(d.age_at_prize) && d.age_at_prize !== 'NA'), d => d.age_at_prize))
46 | .range([padding, $width - padding]);
47 | return scale;
48 | });
49 |
50 | export const sortedLaureates = derived([rawLaureates, currentStep], ([$rawLaureates, $currentStep]) => {
51 | if (!$currentStep) return [];
52 | const { cluster_by: clusterBy, exclusiveIds = [] } = $currentStep || {};
53 | if (!clusterBy) return [];
54 |
55 | const categories = [...new Set($rawLaureates.map(d => d[clusterBy]).flat())];
56 |
57 | const clusterData = categories.map(cat => {
58 | const data = $rawLaureates
59 | .filter(d => d[clusterBy][0] === cat)
60 | .filter(d => exclusiveIds.length ? exclusiveIds.includes(d.id) : true);
61 | return {
62 | category: cat,
63 | data
64 | };
65 | });
66 |
67 | return clusterData;
68 | });
69 |
70 | export const laureates = derived(
71 | [sortedLaureates, currentStep, width, height, radius, ageScale],
72 | async (
73 | [$sortedLaureates, $currentStep, $width, $height, $radius, $ageScale],
74 | set
75 | ) => {
76 | const { inner_layout, outer_layout, exclusiveIds = [] } = $currentStep || {};
77 | const radiusFactor = exclusiveIds.length ? 6 : 1;
78 |
79 | const { type: typeInner } = inner_layout || {};
80 | const { type: typeOuter } = outer_layout || {};
81 |
82 | let layoutedInner = $sortedLaureates;
83 | if (typeInner === 'phyllotaxis') {
84 | layoutedInner = $sortedLaureates.map(({ category, data }) => {
85 | return layoutPhyllotaxis(data, $radius * radiusFactor, category);
86 | });
87 | } else if (typeInner === 'histogram') {
88 | layoutedInner = (await Promise.all($sortedLaureates.map(async ({ category, data }, i, arr) => {
89 | const beeData = await layoutHistogram(
90 | data,
91 | $radius * radiusFactor / 2,
92 | d => $ageScale(d.age_at_prize),
93 | (i + 1) / arr.length * $height * 0.8,
94 | 'age_at_prize',
95 | (arr.length === 1 ? $radius * 2 : $radius) * radiusFactor,
96 | category
97 | );
98 | return beeData;
99 | }))).map(d => ({index: 0, data: d}));
100 | }
101 |
102 | let layoutedOuter = layoutedInner;
103 | if (typeOuter === 'clusterForce') {
104 | layoutedOuter = await layoutClusterForce(layoutedInner, $width, $height, 2 * $radius * radiusFactor);
105 | }
106 |
107 | set(layoutedOuter);
108 | },
109 | []);
--------------------------------------------------------------------------------
/src/stores/devices.js:
--------------------------------------------------------------------------------
1 | import { derived, writable } from 'svelte/store';
2 |
3 | import { width, height } from './dimensions';
4 |
5 | export const isMobile = derived(width, $width => {
6 | return $width < 520;
7 | });
8 |
9 | export const isSmall = derived(width, $width => {
10 | return $width < 720;
11 | });
12 |
13 | export const headerHeight = writable(100);
14 |
15 | export const top = derived([height, headerHeight], ([$height, $headerHeight]) => {
16 | return $headerHeight / $height;
17 | });
18 |
19 | export const bottom = derived([height, headerHeight], ([$height, $headerHeight]) => {
20 | return ($headerHeight + $height + 20) / $height;
21 | });
22 |
23 | export const contentHeight = derived([height, headerHeight], ([$height, $headerHeight]) => {
24 | const availableHeight = $height - $headerHeight - 15;
25 | return Math.max(0, availableHeight);
26 | });
--------------------------------------------------------------------------------
/src/stores/dimensions.js:
--------------------------------------------------------------------------------
1 | import { writable, derived } from 'svelte/store';
2 |
3 | export const width = writable(0);
4 | export const height = writable(0);
5 | export const canvasHeight = writable(0);
6 |
7 | export const minDim = derived([width, height], ([$width, $height]) => {
8 | return Math.min($width, $height);
9 | });
10 |
11 | export const radius = derived(minDim, $minDim => {
12 | return Math.max(4, ($minDim ** 2) / 120000);
13 | });
--------------------------------------------------------------------------------
/src/stores/steps.js:
--------------------------------------------------------------------------------
1 | import { readable, writable } from 'svelte/store';
2 | import { json } from 'd3';
3 |
4 | const dataPath = 'steps.json';
5 |
6 | export const steps = readable([], async set => {
7 | const data = await json(dataPath);
8 |
9 | const n = data.length - 1;
10 | const fData = data.map((d, i) => {
11 | return {
12 | id: i,
13 | ...d,
14 | position: i / n - (0.5 / n)
15 | };
16 | });
17 |
18 | set(fData);
19 | });
20 |
21 | export const currentStep = writable(null);
--------------------------------------------------------------------------------
/src/utils/canvas.js:
--------------------------------------------------------------------------------
1 | // adapted from https://github.com/Wattenberger/svelte-recipes/blob/master/src/components/examples/scale-canvas.js
2 | export const scaleCanvas = (canvas, ctx, width, height, center = true) => {
3 | // assume the device pixel ratio is 1 if the browser doesn't specify it
4 | const devicePixelRatio = window.devicePixelRatio || 1;
5 |
6 | // determine the 'backing store ratio' of the canvas ctx
7 | const backingStoreRatio =
8 | ctx.webkitBackingStorePixelRatio ||
9 | ctx.mozBackingStorePixelRatio ||
10 | ctx.msBackingStorePixelRatio ||
11 | ctx.oBackingStorePixelRatio ||
12 | ctx.backingStorePixelRatio ||
13 | 1;
14 |
15 | // determine the actual ratio we want to draw at
16 | const ratio = devicePixelRatio / backingStoreRatio;
17 |
18 | if (devicePixelRatio !== backingStoreRatio) {
19 | // set the 'real' canvas size to the higher width/height
20 | canvas.width = width * ratio;
21 | canvas.height = height * ratio;
22 |
23 | // ...then scale it back down with CSS
24 | canvas.style.width = width + 'px';
25 | canvas.style.height = height + 'px';
26 | } else {
27 | // this is a normal 1:1 device; just scale it simply
28 | canvas.width = width;
29 | canvas.height = height;
30 | canvas.style.width = '';
31 | canvas.style.height = '';
32 | }
33 |
34 | // scale the drawing ctx so everything will work at the higher ratio
35 | ctx.scale(ratio, ratio);
36 |
37 | if (center) {
38 | ctx.translate(width / 2, height / 2);
39 | }
40 | };
--------------------------------------------------------------------------------
/src/utils/colors.js:
--------------------------------------------------------------------------------
1 | export const prizeCategoryColors = {
2 | 'Physiology or Medicine': '#69A2D4',
3 | 'Physics': '#A1C8E6',
4 | 'Chemistry': '#30709E',
5 | 'Literature': '#DBBF58',
6 | 'Peace': '#997B19',
7 | 'Economic Sciences': '#8FCC9C'
8 | };
--------------------------------------------------------------------------------
/src/utils/dictionary.js:
--------------------------------------------------------------------------------
1 | export const dictionary = {
2 | Chemistry: 'Chemie',
3 | 'Economic Sciences': 'Wirtschaft',
4 | Literature: 'Literatur',
5 | Peace: 'Frieden',
6 | Physics: 'Physik',
7 | 'Physiology or Medicine': 'Physiologie oder Medizin',
8 |
9 | female: 'Frauen',
10 | male: 'Männer',
11 |
12 | USA: 'USA',
13 | 'United Kingdom': 'Großbritannien',
14 | Germany: 'Deutschland',
15 | France: 'Frankreich',
16 | Sweden: 'Schweden',
17 | Japan: 'Japan',
18 | Poland: 'Polen',
19 | Russia: 'Russland',
20 | Canada: 'Kanada',
21 | Italy: 'Italien',
22 | Switzerland: 'Schweiz',
23 | Austria: 'Österreich',
24 | 'the Netherlands': 'Niederlande',
25 | China: 'China',
26 | Norway: 'Norwegen',
27 | Denmark: 'Dänemark',
28 | Scotland: 'Schottland',
29 | Philippines: 'Philippinen',
30 | Israel: 'Israel',
31 | Lithuania: 'Litauen',
32 | Turkey: 'Türkei',
33 | Egypt: 'Ägypten',
34 | 'South Africa' : 'Südafrika',
35 | NA : '',
36 | Pakistan : 'Pakistan',
37 | Vietnam : 'Vietnam',
38 | Ghana : 'Ghana',
39 | Croatia : 'Kroatien',
40 | Mexico : 'Mexiko',
41 | India : 'Indien',
42 | Spain : 'Spanien',
43 | Portugal : 'Portugal',
44 | Bangladesh : 'Bangladesch',
45 | Guatemala : 'Guatemala',
46 | Argentina : 'Argentinien',
47 | 'New Zealand': 'Neuseeland',
48 | Algeria: 'Algerien',
49 | Belgium: 'Belgien',
50 | Zimbabwe: 'Simbabwe',
51 | Hungary: 'Ungarn',
52 | Lebanon: 'Libanon',
53 | Finland: 'Finnland',
54 | Cyprus: 'Zypern',
55 | Iran: 'Iran',
56 | Bulgaria: 'Bulgarien',
57 | Romania: 'Rumänien',
58 | Venezuela: 'Venezuela',
59 | Liberia: 'Liberia',
60 | Ireland: 'Irland',
61 | Slovenia: 'Slowenien',
62 | Colombia: 'Kolumbien',
63 | Greece: 'Griechenland',
64 | Chile: 'Chile',
65 | Myanmar: 'Myanmar',
66 | Ukraine: 'Ukraine',
67 | Peru: 'Peru',
68 | Azerbaijan: 'Aserbaidschan',
69 | Belarus: 'Belarus',
70 | Morocco: 'Marokko',
71 | Brazil: 'Brasilien',
72 | Yemen: 'Jemen',
73 | Slovakia: 'Slowakei',
74 | Iraq: 'Irak',
75 | Iceland: 'Island',
76 | Luxembourg: 'Luxemburg',
77 | Kenya: 'Kenia',
78 | Indonesia: 'Indonesien',
79 | Nigeria: 'Nigeria',
80 | Taiwan: 'Taiwan',
81 | Latvia: 'Lettland',
82 | Ethiopia: 'Äthiopien',
83 | Madagascar: 'Madagaskar',
84 | 'Czech Republic': 'Tschechien',
85 | 'Trinidad and Tobago': 'Trinidad und Tobago',
86 | 'Faroe Islands (Denmark)': 'Färöer',
87 | 'Guadeloupe Island': 'Guadeloupe',
88 | 'Northern Ireland': 'Nordirland',
89 | 'East Timor': 'Osttimor',
90 | 'Costa Rica': 'Costa Rica',
91 | 'South Korea': 'Südkorea',
92 | 'Saint Lucia': 'Sankt Lucia',
93 | 'North Macedonia': 'Nordmazedonien',
94 | 'Bosnia and Herzegovina': 'Bosnien und Herzegowina',
95 | 'Democratic Republic of the Congo': 'Demokratische Republik Kongo',
96 | Australia: 'Australien'
97 | };
--------------------------------------------------------------------------------
/src/utils/format.js:
--------------------------------------------------------------------------------
1 | import { timeParse, formatDefaultLocale, timeFormatDefaultLocale, timeFormat } from 'd3';
2 |
3 | export const NA_STRING = '—';
4 | export const CURRENCY_FORMAT = '$,.0f';
5 | export const PERCENT_FORMAT = '.1%';
6 | export const INTEGER_FORMAT = ',.0f';
7 | export const FLOAT_FORMAT = ',.1f';
8 |
9 | // the basic formatting function sued
10 | const p = {
11 | ...formatDefaultLocale,
12 | decimal: '.',
13 | currency: ['', ' €'],
14 | percent: ' %',
15 | nan: NA_STRING,
16 | thousands: ',',
17 | grouping: [3],
18 | };
19 |
20 | export const formatTime = timeFormatDefaultLocale({
21 | ...p,
22 | dateTime: '%a %b %e %X %Y',
23 | date: '%d.%m.%Y',
24 | time: '%H:%M:%S',
25 | periods: ['AM', 'PM'],
26 | days: [
27 | 'Sonntag',
28 | 'Montag',
29 | 'Dienstag',
30 | 'Mittwoch',
31 | 'Donnerstag',
32 | 'Freitag',
33 | 'Samstag',
34 | ],
35 | shortDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
36 | months: [
37 | 'Januar',
38 | 'Februar',
39 | 'März',
40 | 'April',
41 | 'Mai',
42 | 'Juni',
43 | 'Juli',
44 | 'August',
45 | 'September',
46 | 'Oktober',
47 | 'November',
48 | 'Dezember',
49 | ],
50 | shortMonths: [
51 | 'Jan',
52 | 'Feb',
53 | 'Mär',
54 | 'Apr',
55 | 'Mai',
56 | 'Jun',
57 | 'Jul',
58 | 'Aug',
59 | 'Sep',
60 | 'Okt',
61 | 'Nov',
62 | 'Dez',
63 | ]
64 | }).format;
65 |
66 | const indicatorFormats = {
67 | 'year': timeFormat('%Y'),
68 | 'month-short': timeFormat('%b'),
69 | 'month-year': timeFormat('%b %Y'),
70 | 'day-month-year': timeFormat('%d. %B %Y')
71 | };
72 |
73 | export const formatValue = (value, indicatorId) => {
74 | const formatter = indicatorFormats[indicatorId];
75 | return formatter(value);
76 | };
77 |
78 | export const parseDate = timeParse('%d.%m.%y');
79 |
80 | export const formatPartyKeys = (obj) => {
81 | const keys = Object.keys(obj);
82 | let key;
83 | let n = keys.length;
84 | let newobj = {};
85 | while (n--) {
86 | key = keys[n];
87 | if (key === 'CDU' || key === 'CSU') key = 'CDU/CSU';
88 | newobj[key.toUpperCase()] = obj[key];
89 | }
90 | return newobj;
91 | };
--------------------------------------------------------------------------------
/src/utils/layout.js:
--------------------------------------------------------------------------------
1 | import { max, forceSimulation, forceCenter, forceCollide } from 'd3';
2 |
3 | const goldenRatio = (1 + Math.sqrt(5)) / 2;
4 |
5 | export const layoutPhyllotaxis = (
6 | data,
7 | radius = 4,
8 | category,
9 | radiusOffset = radius / 2,
10 | spacing = radius * 1.2,
11 | theta = 2 * Math.PI / goldenRatio
12 | ) => {
13 | const layoutedData = data.map((d, i) => {
14 | const scaledTheta = theta * i;
15 | const scaledRadius = spacing * Math.sqrt(i) + radiusOffset;
16 |
17 | return {
18 | ...d,
19 | radius,
20 | x: Math.cos(scaledTheta) * scaledRadius,
21 | y: Math.sin(scaledTheta) * scaledRadius,
22 | withinClusterIndex: i
23 | };
24 | });
25 |
26 | const clusterRadius = max(layoutedData.map(d => [Math.abs(d.x), Math.abs(d.y)]).flat());
27 |
28 | const metaResult = {
29 | clusterRadius,
30 | category,
31 | data: layoutedData
32 | };
33 |
34 | return metaResult;
35 | };
36 |
37 | export const layoutClusterForce = (data, width, height, spacing = 0) => {
38 | return new Promise((resolve) => {
39 | let dataCopy = [...data];
40 |
41 | forceSimulation()
42 | .nodes(dataCopy)
43 | .force('collision', forceCollide(d => (d.clusterRadius || 1) + spacing))
44 | .force('center', forceCenter(width / 2, height / 2))
45 | .on('tick', () => {
46 | for (let i = 0; i < dataCopy.length; i++) {
47 | const d = dataCopy[i];
48 | const r = (d.clusterRadius || 1) + spacing;
49 | d.x = Math.max(r, Math.min(width - r, d.x));
50 | d.y = Math.max(r, Math.min(height - r, d.y));
51 | }
52 | })
53 | .on('end', () => resolve(dataCopy))
54 | .alphaMin(0.8);
55 | });
56 | };
57 |
58 | export const layoutHistogram = (data, radius = 1, x = d => d, fy = 0, catType, offset = radius * 2, category) => {
59 | const scaledData = data.map(d => ({...d, x: +x(d), y: 0}));
60 |
61 | const histo = scaledData.reduce((acc, cur) => {
62 | const entry = cur[catType].toString();
63 | if (acc[entry]) {
64 | return {...acc, [entry]: [...acc[entry], {...cur, y: cur.y + offset * acc[entry].length}]};
65 | } else {
66 | return {...acc, [entry]: [cur]};
67 | }
68 | }, {});
69 |
70 | const result = Object.values(histo).flat().map((d, i) => {
71 | return {
72 | ...d,
73 | category,
74 | y: -d.y + fy,
75 | radius,
76 | withinClusterIndex: i * 10
77 | };
78 | });
79 |
80 | return result;
81 | };
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | // svelte.config.js
2 | const preprocess = require('svelte-preprocess');
3 |
4 | module.exports = {
5 | preprocess: preprocess()
6 | };
7 |
--------------------------------------------------------------------------------