@nstarkman/collaborator-map 0.0.0-stage → 20261023.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/package.json +32 -4
- package/src/CollabMap.astro +150 -0
- package/src/collabmap.js +165 -0
- package/src/map.css +231 -0
- package/src/mappick.js +114 -0
- package/src/mapzoom.js +117 -0
- package/src/world-equal-earth.json +8 -0
- package/src/worldmap.js +106 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Versions are dates, `YYYYMMDD.0.N`; the four research-kit packages are released together at one
|
|
4
|
+
version. A breaking change says how to migrate.
|
|
5
|
+
|
|
6
|
+
## 20261023.0.1
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Released with `@nstarkman/data-resolver` 20261023.0.1 (a paper's map: its own co-authors only).
|
|
11
|
+
|
|
12
|
+
## 20261023.0.0
|
|
13
|
+
|
|
14
|
+
First release.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nathaniel Starkman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nstarkman/collaborator-map
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Part of [@nstarkman/research-kit](https://www.npmjs.com/package/@nstarkman/research-kit), which says how to
|
|
4
|
+
use it; released with it at one version. Developed in
|
|
5
|
+
[nstarman/nstarman.github.io](https://github.com/nstarman/nstarman.github.io) (`packages/collaborator-map/`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nstarkman/collaborator-map",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "20261023.0.1",
|
|
4
|
+
"description": "The nstarkman.space collaborator map, drawn from a research store.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Nathaniel Starkman",
|
|
8
|
+
"homepage": "https://nstarkman.space",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/nstarman/nstarman.github.io.git",
|
|
12
|
+
"directory": "packages/collaborator-map"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"astro-component"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
"./*": "./src/*"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"astro": "^7"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@nstarkman/data-resolver": "20261023.0.1"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"provenance": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Where the people I have written with have worked, and when.
|
|
3
|
+
//
|
|
4
|
+
// Inline SVG on a committed Equal Earth outline — no tiles, so no third party
|
|
5
|
+
// sees a visitor's viewport, and the whole thing works offline. See issue #22.
|
|
6
|
+
//
|
|
7
|
+
// The list underneath is not a fallback: it is the same data in the form a
|
|
8
|
+
// screen reader, a keyboard, or a small phone can actually use, and it is where
|
|
9
|
+
// the paper titles live in full.
|
|
10
|
+
import './map.css';
|
|
11
|
+
import { collaboratorMap, lastFirst, map } from './collabmap.js';
|
|
12
|
+
|
|
13
|
+
const people = collaboratorMap();
|
|
14
|
+
const pins = people.flatMap((p) => p.pins.map((pin) => ({ ...pin, person: p })));
|
|
15
|
+
const placed = people.filter((p) => p.pins.length > 0).length;
|
|
16
|
+
const trails = people.filter((p) => p.pins.length > 1);
|
|
17
|
+
// "2 papers together, 1 assist" — an assist is a paper of theirs that thanks me.
|
|
18
|
+
const plural = (n, w) => `${n} ${w}${n > 1 ? 's' : ''}`;
|
|
19
|
+
const together = (papers) => {
|
|
20
|
+
const assists = papers.filter((p) => p.assist).length;
|
|
21
|
+
const written = papers.length - assists;
|
|
22
|
+
return [written && `${plural(written, 'paper')} together`, assists && plural(assists, 'assist')]
|
|
23
|
+
.filter(Boolean).join(', ');
|
|
24
|
+
};
|
|
25
|
+
const label = (pin) =>
|
|
26
|
+
[pin.organization, pin.place].filter(Boolean).join(' — ')
|
|
27
|
+
+ ` (${pin.start}${pin.end ? `–${pin.end}` : '–present'})`;
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
<figure class="cmap">
|
|
31
|
+
<figcaption>
|
|
32
|
+
<span class="cmap-key cmap-key--shared">
|
|
33
|
+
<svg viewBox="0 0 12 12" aria-hidden="true"><circle cx="6" cy="6" r="4" /></svg>
|
|
34
|
+
wrote or assisted on a paper together
|
|
35
|
+
</span>
|
|
36
|
+
<span class="cmap-key">
|
|
37
|
+
<svg viewBox="0 0 12 12" aria-hidden="true"><circle cx="6" cy="6" r="4" /></svg>
|
|
38
|
+
another post
|
|
39
|
+
</span>
|
|
40
|
+
<span class="cmap-key cmap-key--trail">
|
|
41
|
+
<svg viewBox="0 0 24 12" aria-hidden="true"><line x1="1" y1="6" x2="23" y2="6" /></svg>
|
|
42
|
+
one person over time
|
|
43
|
+
</span>
|
|
44
|
+
<a class="cmap-proj" href="https://en.wikipedia.org/wiki/Equal_Earth_projection">Equal Earth projection</a>
|
|
45
|
+
</figcaption>
|
|
46
|
+
|
|
47
|
+
<div class="cmap-stage">
|
|
48
|
+
<svg viewBox={`0 0 ${map.width} ${map.height}`} role="img"
|
|
49
|
+
aria-label={`World map: ${pins.length} posts held by ${placed} people, `
|
|
50
|
+
+ `on an Equal Earth projection. The same information is listed below.`}>
|
|
51
|
+
<path class="cmap-land" d={map.land} />
|
|
52
|
+
{/* Drawn over the land in the ocean's colour rather than cut out of it:
|
|
53
|
+
two paths and no fill-rule to get wrong. */}
|
|
54
|
+
<path class="cmap-lakes" d={map.lakes} />
|
|
55
|
+
|
|
56
|
+
{/* Trajectories first, so pins sit on top of their own lines. */}
|
|
57
|
+
{trails.map((p) => (
|
|
58
|
+
<polyline class="cmap-trail" data-c={people.indexOf(p)} data-mentee={p.mentee ? '' : undefined} style={`--h:${p.hue}`}
|
|
59
|
+
points={p.pins.map((pin) => `${pin.x.toFixed(2)},${pin.y.toFixed(2)}`).join(' ')} />
|
|
60
|
+
))}
|
|
61
|
+
|
|
62
|
+
{pins.map((pin) => (
|
|
63
|
+
<g class="cmap-pin" data-c={people.indexOf(pin.person)} data-mentee={pin.person.mentee ? '' : undefined} style={`--h:${pin.person.hue}`}>
|
|
64
|
+
{/* A link to the person's record below, for a reader with no script to
|
|
65
|
+
pick them with; the script's click handler takes over where there is
|
|
66
|
+
one. Out of the tab order: the record's own heading is the link for
|
|
67
|
+
the keyboard. */}
|
|
68
|
+
<a href={`#collab-${people.indexOf(pin.person)}`} tabindex="-1">
|
|
69
|
+
{/* <title> is the native tooltip and the accessible name at once. */}
|
|
70
|
+
<title>{pin.person.name} — {label(pin)}{pin.papers.length ? `; ${together(pin.papers)}` : ''}</title>
|
|
71
|
+
<circle cx={pin.x.toFixed(2)} cy={pin.y.toFixed(2)}
|
|
72
|
+
r={pin.papers.length ? 4.5 : 3} class={pin.papers.length ? 'is-shared' : ''} />
|
|
73
|
+
</a>
|
|
74
|
+
</g>
|
|
75
|
+
))}
|
|
76
|
+
</svg>
|
|
77
|
+
{/* Shown by mapzoom.js: with no script there is nothing for them to do. */}
|
|
78
|
+
<div class="cmap-zoom" role="group" aria-label="Zoom the map" hidden>
|
|
79
|
+
<button type="button" data-zoom="in" aria-label="Zoom in">+</button>
|
|
80
|
+
<button type="button" data-zoom="out" aria-label="Zoom out" disabled>−</button>
|
|
81
|
+
<button type="button" data-zoom="reset" aria-label="Show the whole world" disabled>⤢</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="cmap-pick">
|
|
86
|
+
<label for="cmap-who" hidden>Show</label>
|
|
87
|
+
<select id="cmap-who" class="cmap-who btn btn--sel" hidden>
|
|
88
|
+
<option value="">Everyone</option>
|
|
89
|
+
<option value="mentees">Mentees</option>
|
|
90
|
+
<option disabled>──────────</option>
|
|
91
|
+
{people.map((p, i) => <option value={String(i)}>{lastFirst(p.name)}</option>)}
|
|
92
|
+
</select>
|
|
93
|
+
<span class="cmap-partial"
|
|
94
|
+
title="Mostly scraped from public ORCID records: it shows what each person has chosen to publish there, so posts can be missing, undated, or out of date.">May be incomplete / wrong</span>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
<ol class="cmap-list">
|
|
100
|
+
{people.map((p) => (
|
|
101
|
+
<li id={`collab-${people.indexOf(p)}`} data-key={p.key} data-slug={p.mentee?.slug} data-mentee={p.mentee ? '' : undefined} data-c={people.indexOf(p)} style={`--h:${p.hue}`}>
|
|
102
|
+
{/* Where the record came from, and the way to check it. */}
|
|
103
|
+
{p.orcid && <a class="cmap-orcid" href={`https://orcid.org/${p.orcid}`}
|
|
104
|
+
aria-label={`${p.name} on ORCID`} title={`${p.name} on ORCID — ${p.orcid}`}>
|
|
105
|
+
<svg class="ico" aria-hidden="true" focusable="false"><use href="#i-orcid" /></svg>
|
|
106
|
+
</a>}
|
|
107
|
+
<h3><a href={`#collab-${people.indexOf(p)}`}>{lastFirst(p.name)}</a></h3>
|
|
108
|
+
<ol hidden>
|
|
109
|
+
{p.pins.map((pin) => (
|
|
110
|
+
<li>
|
|
111
|
+
<div class="cmap-post">
|
|
112
|
+
<span class="cmap-when">{pin.start}–{pin.end ?? 'present'}</span>
|
|
113
|
+
<span class="cmap-where">{pin.organization}{pin.place && <>, {pin.place}</>}</span>
|
|
114
|
+
{pin.role && <span class="cmap-role">{pin.role}</span>}
|
|
115
|
+
</div>
|
|
116
|
+
{pin.papers.length > 0 && (
|
|
117
|
+
<ul class="cmap-papers">
|
|
118
|
+
{pin.papers.map((paper) => (
|
|
119
|
+
<li><span class="inline-card">{paper.url ? <a href={paper.url}>{paper.title}</a> : paper.title}{paper.doi && !paper.doi.startsWith('10.48550/') && <> <span class="cmap-sep" aria-hidden="true">·</span> <a class="cmap-doi" href={`https://doi.org/${paper.doi}`}>{paper.doi}</a></>}</span>{paper.assist && <span class="cmap-assist"> — assist</span>}</li>
|
|
120
|
+
))}
|
|
121
|
+
</ul>
|
|
122
|
+
)}
|
|
123
|
+
</li>
|
|
124
|
+
))}
|
|
125
|
+
</ol>
|
|
126
|
+
{p.mentee && (
|
|
127
|
+
<div class="cmap-mentee" hidden>
|
|
128
|
+
{p.mentee.research && (
|
|
129
|
+
<p>{p.mentee.research.map((sp) => (sp.url ? <a href={sp.url}>{sp.t}</a> : sp.t))}</p>
|
|
130
|
+
)}
|
|
131
|
+
{p.mentee.top.length > 0 && <ul>{p.mentee.top.map((a) => <li>{a.title}</li>)}</ul>}
|
|
132
|
+
<a href={p.mentee.href}>Full profile →</a>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
{p.unplacedPapers.length > 0 && (
|
|
136
|
+
<p class="cmap-note" hidden>
|
|
137
|
+
Also {together(p.unplacedPapers)}, from when their ORCID record names no post.
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ol>
|
|
143
|
+
</figure>
|
|
144
|
+
|
|
145
|
+
<script>
|
|
146
|
+
import { wireAll } from './mappick.js';
|
|
147
|
+
import { wireZoom } from './mapzoom.js';
|
|
148
|
+
wireAll();
|
|
149
|
+
wireZoom();
|
|
150
|
+
</script>
|
package/src/collabmap.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The collaborator map's render model.
|
|
2
|
+
//
|
|
3
|
+
// A pin is one post a collaborator held somewhere; a trajectory is the dotted
|
|
4
|
+
// line joining one person's pins in the order they held them. Each pin carries
|
|
5
|
+
// the papers we wrote while they were there, which is the part that makes it a
|
|
6
|
+
// map of collaboration rather than of employment.
|
|
7
|
+
//
|
|
8
|
+
// Geometry is resolved here rather than in the component, so the page renders
|
|
9
|
+
// plain numbers and the maths is testable without a DOM. The projection itself
|
|
10
|
+
// lives in worldmap.js, shared with the conference map.
|
|
11
|
+
|
|
12
|
+
import { locate } from '@nstarkman/data-resolver/places.js';
|
|
13
|
+
import { map, project, toXY, spread, hueFor, KM_PER_UNIT, MAX_DRIFT_MILES, MAX_PIN_DRIFT }
|
|
14
|
+
from './worldmap.js';
|
|
15
|
+
import { mentees, menteeKey, menteeName, profileHref, topAchievements, researchSpans, firstSentenceSpans } from '@nstarkman/data-resolver/mentees.js';
|
|
16
|
+
import { byType, venueUrl, links, assists, primaryLink, config } from '@nstarkman/research-store';
|
|
17
|
+
|
|
18
|
+
// Re-exported because the map's callers and tests have always reached for them
|
|
19
|
+
// here, and where the maths lives is not their concern.
|
|
20
|
+
export { map, project, toXY, KM_PER_UNIT, MAX_DRIFT_MILES, MAX_PIN_DRIFT };
|
|
21
|
+
|
|
22
|
+
/** Papers we co-wrote, and papers of theirs that thank me (`assist`), by the
|
|
23
|
+
* author's ORCID. */
|
|
24
|
+
function papersByAuthor() {
|
|
25
|
+
const out = new Map();
|
|
26
|
+
for (const pub of byType('publication')) {
|
|
27
|
+
for (const a of pub.authors ?? []) {
|
|
28
|
+
if (a.me || !a.orcid) continue;
|
|
29
|
+
if (!out.has(a.orcid)) out.set(a.orcid, []);
|
|
30
|
+
// The publications page carries no per-item anchor, so a paper links to
|
|
31
|
+
// itself: the journal where it is published, the preprint otherwise, and
|
|
32
|
+
// plain text when it is neither yet.
|
|
33
|
+
const preprint = (links(pub) ?? []).find((l) => l.rel === 'preprint');
|
|
34
|
+
out.get(a.orcid).push({
|
|
35
|
+
id: pub.id,
|
|
36
|
+
title: pub.nickTitle ?? pub.shortTitle ?? pub.title,
|
|
37
|
+
doi: pub.doi,
|
|
38
|
+
date: String(pub.date.start),
|
|
39
|
+
url: venueUrl(pub) ?? preprint?.url ?? null,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const a of assists) {
|
|
44
|
+
for (const au of a.authors) {
|
|
45
|
+
if (!au.orcid) continue;
|
|
46
|
+
if (!out.has(au.orcid)) out.set(au.orcid, []);
|
|
47
|
+
out.get(au.orcid).push({
|
|
48
|
+
id: a.bibcode, title: a.title, doi: a.doi, date: a.date, url: primaryLink(a)?.url ?? null, assist: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Nobiliary particles travel with the surname: "van Kerkwijk", not "Kerkwijk".
|
|
56
|
+
const PARTICLES = new Set(['van', 'von', 'de', 'der', 'den', 'del', 'della', 'di', 'da', 'dos', 'la', 'le']);
|
|
57
|
+
|
|
58
|
+
/** "Marten H. van Kerkwijk" -> "van Kerkwijk, Marten H." — index order, for
|
|
59
|
+
* lists a reader scans by surname. Anything without a given name is left be. */
|
|
60
|
+
export function lastFirst(name) {
|
|
61
|
+
const parts = name.split(' ');
|
|
62
|
+
// ponytail: a two-word name always splits, so a particle surname on its own
|
|
63
|
+
// ("Le Guin") comes out wrong. No collaborator has one; widen if one arrives.
|
|
64
|
+
let i = parts.length - 1;
|
|
65
|
+
while (i > 1 && PARTICLES.has(parts[i - 1].toLowerCase())) i -= 1;
|
|
66
|
+
const given = parts.slice(0, i).join(' ');
|
|
67
|
+
return given ? `${parts.slice(i).join(' ')}, ${given}` : name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Did a post that ran `start`..`end` cover this date? */
|
|
71
|
+
const covers = (post, date) =>
|
|
72
|
+
Boolean(post.start) && post.start <= date && (!post.end || post.end >= date);
|
|
73
|
+
|
|
74
|
+
// An end of "2023" runs to the end of 2023, so it must sort after "2023-09":
|
|
75
|
+
// '~' follows every digit and '-'. An open end runs on for ever.
|
|
76
|
+
const last = (post) => (post.end ?? '9999') + '~';
|
|
77
|
+
/** Do two posts' date ranges meet? Partial dates cover their whole year or month. */
|
|
78
|
+
export const meets = (a, b) => a.start <= last(b) && b.start <= last(a);
|
|
79
|
+
|
|
80
|
+
/** The posts to put on the map for one person: the ORCID record's, then each
|
|
81
|
+
* `journey` post of a mentee whose institution the record has no overlapping
|
|
82
|
+
* post for. ORCID wins; the record fills its gaps. */
|
|
83
|
+
export function postsOf(orcidPosts, journey = []) {
|
|
84
|
+
const extra = journey
|
|
85
|
+
.filter((j) => !orcidPosts.some((o) => o.start && locate(o.organization) === locate(j.organization) && meets(o, j)))
|
|
86
|
+
.map((j) => ({ ...j, role: j.role }));
|
|
87
|
+
return [...orcidPosts, ...extra]
|
|
88
|
+
.filter((a) => a.start && a.organization && locate(a.organization))
|
|
89
|
+
// Newest first: what someone is doing now is the more useful fact, and it
|
|
90
|
+
// is the order every other dated list on this site uses. The trajectory
|
|
91
|
+
// is a line through the same points either way.
|
|
92
|
+
.sort((a, b) => b.start.localeCompare(a.start));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** First sentence of a mentee's research, as spans with item links resolved. */
|
|
96
|
+
const firstSentence = (text) => (text ? firstSentenceSpans(researchSpans(text)) : null);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* One entry per collaborator who can be put on the map at all: they have at
|
|
100
|
+
* least one dated post at an institution with known coordinates. A mentee is
|
|
101
|
+
* always in, with `pins: []` if nothing of theirs can be placed.
|
|
102
|
+
*
|
|
103
|
+
* `key` is the ORCID, or a mentee's slug when they have none.
|
|
104
|
+
* `hue` comes from worldmap.js, spread by the golden angle.
|
|
105
|
+
*/
|
|
106
|
+
export function collaboratorMap(menteeList = mentees()) {
|
|
107
|
+
const papers = papersByAuthor();
|
|
108
|
+
const people = [];
|
|
109
|
+
const byKey = new Map(menteeList.map((m) => [menteeKey(m), m]));
|
|
110
|
+
|
|
111
|
+
const entries = config.collaborators.people.map((c) => ({
|
|
112
|
+
orcid: c.orcid, name: c.name, affiliations: c.affiliations, m: byKey.get(c.orcid),
|
|
113
|
+
}));
|
|
114
|
+
for (const m of menteeList) {
|
|
115
|
+
if (!entries.some((e) => e.m === m)) {
|
|
116
|
+
entries.push({ orcid: m.person.orcid, name: menteeName(m), affiliations: [], m });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const person of entries) {
|
|
121
|
+
const m = person.m;
|
|
122
|
+
const posts = postsOf(person.affiliations, m?.journey);
|
|
123
|
+
if (posts.length === 0 && !m) continue;
|
|
124
|
+
|
|
125
|
+
const mine = papers.get(person.orcid) ?? [];
|
|
126
|
+
const pins = posts.map((post) => {
|
|
127
|
+
const { lat, lon } = locate(post.organization);
|
|
128
|
+
const [x, y] = toXY(lon, lat);
|
|
129
|
+
return {
|
|
130
|
+
organization: post.organization,
|
|
131
|
+
place: [post.city, post.country].filter(Boolean).join(', '),
|
|
132
|
+
role: post.role,
|
|
133
|
+
start: post.start,
|
|
134
|
+
end: post.end,
|
|
135
|
+
lat, lon, x, y,
|
|
136
|
+
papers: mine.filter((p) => covers(post, p.date)),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
people.push({
|
|
140
|
+
key: person.orcid ?? menteeKey(m),
|
|
141
|
+
orcid: person.orcid,
|
|
142
|
+
name: person.name,
|
|
143
|
+
pins,
|
|
144
|
+
// Only papers that landed inside a known post can be pinned; the rest are
|
|
145
|
+
// still collaborations, and saying so beats pretending they do not exist.
|
|
146
|
+
unplacedPapers: mine.filter((p) => !posts.some((post) => covers(post, p.date))),
|
|
147
|
+
...(m && {
|
|
148
|
+
mentee: { slug: m.slug, href: profileHref(m), research: firstSentence(m.research), top: topAchievements(m) },
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
people.sort((a, b) => lastFirst(a.name).localeCompare(lastFirst(b.name)));
|
|
154
|
+
people.forEach((p, i) => { p.hue = hueFor(i); });
|
|
155
|
+
spread(people.flatMap((p) => p.pins));
|
|
156
|
+
return people;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let mapped;
|
|
160
|
+
/** A person on the map at /research/ (by ORCID, or a mentee's slug), picked
|
|
161
|
+
* there by mappick.js from the query; null for someone the map has no entry for. */
|
|
162
|
+
export function collabHref(key) {
|
|
163
|
+
mapped ??= new Set(collaboratorMap().map((p) => p.key));
|
|
164
|
+
return mapped.has(key) ? `/research/?collab=${key}#collaborators` : null;
|
|
165
|
+
}
|
package/src/map.css
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* The world maps on /research/ — collaborators and conferences.
|
|
2
|
+
*
|
|
3
|
+
* Imported by CollabMap.astro and ConfMap.astro, so only the page that draws a
|
|
4
|
+
* map ships these rules. The CV's small gutter maps are cv/minimap.css.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* ---------- the collaborator map ---------- */
|
|
8
|
+
/* Hue comes from the render model, lightness from here: one number cannot be
|
|
9
|
+
legible on both paper-white and near-black, so the model picks the hue and
|
|
10
|
+
each theme picks how light to make it. */
|
|
11
|
+
.cmap{margin:1.4rem 0 0}
|
|
12
|
+
.cmap svg{
|
|
13
|
+
display:block; width:100%; height:auto;
|
|
14
|
+
/* The faintest wash of sea. Cyan rather than the site's navy accent: mixing
|
|
15
|
+
the accent in brought its darkness with it, and water reads as water at a
|
|
16
|
+
lighter, greener blue. Mixed into the page's own ground rather than --sunk:
|
|
17
|
+
against --sunk the sea sat at 229 and read as another shade of land, since
|
|
18
|
+
land is grey ink on the same ground. Over white it sits at 244, so the
|
|
19
|
+
water is the page showing through and the continents are the only
|
|
20
|
+
substance on it. */
|
|
21
|
+
background:color-mix(in srgb, var(--sea) 6%, var(--ground));
|
|
22
|
+
border-radius:12px;
|
|
23
|
+
}
|
|
24
|
+
/* The empty half of the map is the point: it says where the collaborations are
|
|
25
|
+
not. Equal Earth's 2.05:1 is kept, so areas stay comparable. */
|
|
26
|
+
/* Land wants to be clearly land without competing with the pins on it —
|
|
27
|
+
--rule was so faint the continents were guesswork. */
|
|
28
|
+
.cmap-land{fill:var(--ink-faint); fill-opacity:.32; stroke:none}
|
|
29
|
+
/* Exactly the ground the map sits on, so a lake is a hole rather than a shape.
|
|
30
|
+
Kept in step with the svg's own background by hand — color-mix cannot be
|
|
31
|
+
referred to twice without repeating it. */
|
|
32
|
+
.cmap-lakes{fill:color-mix(in srgb, var(--sea) 6%, var(--ground)); stroke:none}
|
|
33
|
+
.cmap-trail{
|
|
34
|
+
fill:none; stroke:light-dark(oklch(.55 .16 var(--h)), oklch(.74 .15 var(--h))); stroke-width:calc(1.6px * var(--k,1));
|
|
35
|
+
stroke-dasharray:calc(4px * var(--k,1)) calc(4px * var(--k,1)); stroke-linecap:round; opacity:.75;
|
|
36
|
+
}
|
|
37
|
+
/* Radius lives here rather than in the markup so it can grow as the map
|
|
38
|
+
shrinks: at phone width the whole 1000-unit map is ~330px, where a 4.5-unit
|
|
39
|
+
pin is three pixels across and invisible. The `r` attribute stays in the SVG
|
|
40
|
+
as the value a stylesheet-less render would use. */
|
|
41
|
+
.cmap-pin circle{
|
|
42
|
+
r:calc(3px * var(--k,1)); fill:var(--ground); stroke:light-dark(oklch(.55 .16 var(--h)), oklch(.74 .15 var(--h))); stroke-width:calc(2px * var(--k,1));
|
|
43
|
+
transition:r .12s ease;
|
|
44
|
+
}
|
|
45
|
+
.cmap-pin circle.is-shared{r:calc(4.5px * var(--k,1)); fill:light-dark(oklch(.55 .16 var(--h)), oklch(.74 .15 var(--h)))}
|
|
46
|
+
.cmap-pin:hover circle, .cmap-pin:focus-visible circle{r:calc(8px * var(--k,1))}
|
|
47
|
+
@media (max-width:44rem){
|
|
48
|
+
.cmap-pin circle{r:calc(6px * var(--k,1)); stroke-width:calc(3px * var(--k,1))}
|
|
49
|
+
.cmap-pin circle.is-shared{r:calc(8px * var(--k,1))}
|
|
50
|
+
.cmap-trail{stroke-width:calc(3px * var(--k,1)); stroke-dasharray:calc(7px * var(--k,1)) calc(7px * var(--k,1))}
|
|
51
|
+
}
|
|
52
|
+
@media (prefers-reduced-motion:reduce){ .cmap-pin circle{transition:none} }
|
|
53
|
+
|
|
54
|
+
.cmap figcaption{
|
|
55
|
+
margin:0 0 .7rem;
|
|
56
|
+
/* Inset by the map's own corner radius, so the first dot and the last word
|
|
57
|
+
begin where the squircle stops curving rather than floating past it. */
|
|
58
|
+
padding:0 12px; font-size:.8rem; color:var(--ink-mute); line-height:1.5;
|
|
59
|
+
display:flex; flex-wrap:wrap; align-items:center; gap:.35rem .9rem;
|
|
60
|
+
}
|
|
61
|
+
/* The legend shows the two pins rather than describing them; a sentence
|
|
62
|
+
explaining a shape the reader can simply be shown is a sentence too many. */
|
|
63
|
+
.cmap-key{display:inline-flex; align-items:center; gap:.35rem; white-space:nowrap}
|
|
64
|
+
.cmap-key svg{width:.6875rem; height:.6875rem; background:none; border-radius:0; flex:none}
|
|
65
|
+
.cmap-key circle{stroke:var(--ink-mute); stroke-width:2; fill:var(--ground)}
|
|
66
|
+
.cmap-key--shared circle{fill:var(--ink-mute)}
|
|
67
|
+
.cmap-key--trail svg{width:1.375rem}
|
|
68
|
+
/* Opposite the legend on the same line: it names the drawing, the keys name
|
|
69
|
+
what is drawn on it. */
|
|
70
|
+
.cmap-proj{margin-left:auto; white-space:nowrap;
|
|
71
|
+
color:var(--ink-faint);
|
|
72
|
+
text-decoration:underline; text-decoration-color:var(--rule-strong);
|
|
73
|
+
text-underline-offset:.18em}
|
|
74
|
+
.cmap-proj:hover, .cmap-proj:focus-visible{color:var(--accent); text-decoration-color:currentColor}
|
|
75
|
+
.cmap-key--trail line{stroke:var(--ink-mute); stroke-width:2; stroke-dasharray:4 4}
|
|
76
|
+
|
|
77
|
+
/* Everything recedes but the person under the cursor. Two rules, keyed on an
|
|
78
|
+
attribute the script puts on the figure — a pin and its name live in
|
|
79
|
+
different subtrees, so there is nothing a pure-CSS sibling selector could
|
|
80
|
+
reach across. Without the script nothing fades, which is the honest default. */
|
|
81
|
+
/* The hovered person's pins grow a little. The script dims the others by
|
|
82
|
+
writing an inline opacity, so "has no inline opacity" is exactly "is the one
|
|
83
|
+
being pointed at" — expressed here rather than as a second class to keep in
|
|
84
|
+
step. */
|
|
85
|
+
.cmap[data-hover] .cmap-pin:not([style*="opacity"]) circle{r:calc(7px * var(--k,1))}
|
|
86
|
+
.cmap-pin, .cmap-trail, .cmap-list > li{transition:opacity .15s ease}
|
|
87
|
+
.cmap-list > li{outline-offset:3px}
|
|
88
|
+
@media (prefers-reduced-motion:reduce){
|
|
89
|
+
.cmap-pin, .cmap-trail, .cmap-list > li{transition:none}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.cmap-head{margin-top:2rem}
|
|
93
|
+
/* A rule between the research page's sections, not above the first. */
|
|
94
|
+
.cmap-head ~ .cmap-head{margin-top:2.6rem; padding-top:2rem; border-top:1px solid var(--rule)}
|
|
95
|
+
|
|
96
|
+
.cmap-list{list-style:none; margin:1.6rem 0 0; padding:0;
|
|
97
|
+
display:grid; grid-template-columns:repeat(auto-fill,minmax(19rem,1fr)); gap:1.2rem}
|
|
98
|
+
/* `display` on the element beats the user agent's [hidden]{display:none}, so a
|
|
99
|
+
grid that sets one is never hidden by the attribute alone. Both of these are
|
|
100
|
+
toggled with `hidden`, so both have to say it themselves. */
|
|
101
|
+
.cmap-list[hidden], .cmap-list ol[hidden]{display:none}
|
|
102
|
+
.cmap-list > li{
|
|
103
|
+
border-left:3px solid light-dark(oklch(.55 .16 var(--h)), oklch(.74 .15 var(--h))); padding-left:.7rem;
|
|
104
|
+
}
|
|
105
|
+
/* Arrived at from a byline on /publications/; mappick.js clears it once the
|
|
106
|
+
map is used any other way. */
|
|
107
|
+
.cmap-list > li.is-arrived, .cmap-list > li:target{
|
|
108
|
+
outline:1.5px solid oklch(.6 .16 var(--h) / .7); outline-offset:.4rem; border-radius:.2rem;
|
|
109
|
+
}
|
|
110
|
+
.cmap-list h3{margin:0 0 .35rem; font-size:.92rem}
|
|
111
|
+
.cmap-list h3 a{color:inherit}
|
|
112
|
+
.cmap-list h3 a:hover{color:var(--accent); text-decoration:none}
|
|
113
|
+
/* No script: a pin or a name links to its record, and the record opens. With
|
|
114
|
+
the script, mappick.js decides what shows and the hash is never set by a pin. */
|
|
115
|
+
.cmap-list > li:target > ol[hidden]{display:grid}
|
|
116
|
+
.cmap-list > li:target > .cmap-note[hidden]{display:block}
|
|
117
|
+
.cmap-list > li{scroll-margin-top:calc(var(--nav-h) + 1rem)}
|
|
118
|
+
.cmap-list ol{list-style:none; margin:0; padding:0; display:grid; gap:.5rem}
|
|
119
|
+
.cmap-list ol li{font-size:.8rem; line-height:1.45}
|
|
120
|
+
.cmap-when{font-family:var(--mono); font-size:.7rem; color:var(--ink-faint); margin-right:.4rem}
|
|
121
|
+
.cmap-where{color:var(--ink-2)}
|
|
122
|
+
.cmap-role{display:block; color:var(--ink-mute); font-size:.76rem}
|
|
123
|
+
/* A post is the journey, left; the papers it produced sit opposite, right. */
|
|
124
|
+
.cmap-list ol > li{display:flex; flex-wrap:wrap; justify-content:space-between; gap:.2rem 1.2rem}
|
|
125
|
+
.cmap-post{flex:1 1 12rem}
|
|
126
|
+
.cmap-papers{list-style:none; margin:.2rem 0 0 auto; padding:0; flex:0 1 20rem; text-align:right}
|
|
127
|
+
.cmap-papers li{font-size:.76rem; margin-top:.15rem}
|
|
128
|
+
/* Each paper as an inline-card, as /research/ names one in a sentence — but
|
|
129
|
+
by its full title, so it wraps, each line a chip of its own. */
|
|
130
|
+
.cmap-papers .inline-card{white-space:normal; -webkit-box-decoration-break:clone; box-decoration-break:clone; line-height:1.7}
|
|
131
|
+
.cmap-sep{color:var(--ink-faint)}
|
|
132
|
+
.cmap-papers .inline-card a{color:inherit}
|
|
133
|
+
.cmap-papers .inline-card a:hover{color:var(--accent)}
|
|
134
|
+
.cmap-doi{font-size:.9em; color:var(--ink-mute)}
|
|
135
|
+
.cmap-assist{color:var(--ink-mute)}
|
|
136
|
+
.cmap-note{margin:.4rem 0 0; font-size:.76rem; color:var(--ink-mute)}
|
|
137
|
+
|
|
138
|
+
/* The map's person picker. */
|
|
139
|
+
/* The picker and its label are revealed by mappick.js: without it they pick nothing. */
|
|
140
|
+
.cmap-pick [hidden]{display:none}
|
|
141
|
+
.cmap-pick{display:flex; flex-wrap:wrap; align-items:center; gap:.5rem; margin:.8rem 0 0}
|
|
142
|
+
.cmap-pick label{
|
|
143
|
+
font-family:var(--mono); font-size:.68rem; letter-spacing:.06em;
|
|
144
|
+
text-transform:uppercase; color:var(--ink-faint);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* With one person selected the list is a single record rather than a grid of
|
|
148
|
+
thirty-four, so it should not be laid out as columns. */
|
|
149
|
+
.cmap[data-only]:not([data-only=""]) .cmap-list{grid-template-columns:1fr}
|
|
150
|
+
|
|
151
|
+
/* Sits on each person's record rather than under the map, because that is
|
|
152
|
+
where the claim is actually made: this is their career as ORCID has it, not
|
|
153
|
+
as it was. Orange rather than the site's blue — it is a caveat, not a link. */
|
|
154
|
+
/* The WIP tag's shape and weight, in orange rather than the site's blue —
|
|
155
|
+
same kind of label, different kind of thing to say. */
|
|
156
|
+
.cmap-partial{
|
|
157
|
+
margin-left:auto; white-space:nowrap;
|
|
158
|
+
font-family:var(--mono); font-size:.62rem; letter-spacing:.08em; text-transform:uppercase;
|
|
159
|
+
color:light-dark(#a35c05, #f5cb9a); background:light-dark(#fff8ee, #43301a); border:1px solid light-dark(#f6dcb4, #7a5626);
|
|
160
|
+
border-radius:.25rem; padding:.1rem .38rem;
|
|
161
|
+
}
|
|
162
|
+
/* ORCID's own green, because it is their mark and a reader recognises it
|
|
163
|
+
faster than any label would explain it. */
|
|
164
|
+
.cmap-orcid{float:right; margin:0 0 .2rem .5rem; line-height:0; color:#A6CE39}
|
|
165
|
+
.cmap-orcid .ico{width:.9375rem; height:.9375rem; fill:currentColor; display:block}
|
|
166
|
+
.cmap-orcid:hover, .cmap-orcid:focus-visible{color:var(--accent)}
|
|
167
|
+
|
|
168
|
+
/* ── Zoom ────────────────────────────────────────────────────────────────────
|
|
169
|
+
mapzoom.js moves the svg's viewBox and sets --k to 1/zoom, which every pin and
|
|
170
|
+
line above multiplies by so they stay the size they were. */
|
|
171
|
+
.cmap-stage{position:relative}
|
|
172
|
+
.cmap-zoom{position:absolute; top:.6rem; right:.6rem; display:flex; flex-direction:column; gap:.25rem}
|
|
173
|
+
.cmap-zoom[hidden]{display:none}
|
|
174
|
+
.cmap-zoom button{
|
|
175
|
+
width:1.9rem; height:1.9rem; padding:0; line-height:1; font:inherit; font-size:1.05rem;
|
|
176
|
+
color:var(--ink-2); background:var(--ground); border:1px solid var(--rule-strong); border-radius:.4rem; cursor:pointer;
|
|
177
|
+
}
|
|
178
|
+
.cmap-zoom button:hover:not(:disabled), .cmap-zoom button:focus-visible{color:var(--accent); border-color:currentColor}
|
|
179
|
+
.cmap-zoom button:disabled{color:var(--ink-faint); cursor:default; opacity:.55}
|
|
180
|
+
/* Once there is somewhere to pan to, a drag pans: the page must not scroll under
|
|
181
|
+
a finger doing it. */
|
|
182
|
+
.cmap-trail[data-offview]{display:none}
|
|
183
|
+
.cmap-stage[data-zoom] svg{cursor:grab; touch-action:none}
|
|
184
|
+
|
|
185
|
+
/* A pin is pressable, so it should look it. */
|
|
186
|
+
.cmap-pin, .cmap-trail{cursor:pointer}
|
|
187
|
+
|
|
188
|
+
/* ── The conference map ──────────────────────────────────────────────────────
|
|
189
|
+
Shares every class with the collaborator map above; what follows is only the
|
|
190
|
+
handful of things a map of talks needs and a map of people does not. */
|
|
191
|
+
|
|
192
|
+
/* A conference pin's radius carries its count, so it comes from the model as
|
|
193
|
+
--r rather than from a fixed rule. Written back through CSS because the
|
|
194
|
+
collaborator map's `r` above would otherwise flatten every pin to one size —
|
|
195
|
+
the `r` attribute in the markup is still what a stylesheet-less render uses.
|
|
196
|
+
Part-transparent so overlapping cities read as overlapping rather than as one
|
|
197
|
+
blob, which is what Cambridge and New York do at this scale. */
|
|
198
|
+
.cmap--conf .cmap-pin circle{r:calc(var(--r) * var(--k,1)); fill:oklch(.55 .16 var(--h) / .72)}
|
|
199
|
+
.cmap--conf .cmap-pin:hover circle,
|
|
200
|
+
.cmap--conf .cmap-pin:focus-visible circle,
|
|
201
|
+
.cmap--conf[data-hover] .cmap-pin:not([style*="opacity"]) circle{
|
|
202
|
+
r:calc(var(--r) * 1.45 * var(--k,1)); fill-opacity:1
|
|
203
|
+
}
|
|
204
|
+
@media (max-width:44rem){
|
|
205
|
+
/* The phone rule above doubles a three-unit pin; the same generosity here,
|
|
206
|
+
applied to a size that already varies. */
|
|
207
|
+
.cmap--conf .cmap-pin circle{r:calc(var(--r) * 1.7 * var(--k,1)); stroke-width:calc(3px * var(--k,1))}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Filled and unstroked, because a conference pin is: a 2px stroke on a circle
|
|
211
|
+
this small is most of the circle, and the two keys stopped being comparable. */
|
|
212
|
+
.cmap-key--conf circle{fill:var(--ink-mute); stroke:none}
|
|
213
|
+
|
|
214
|
+
.cmap-count{font-family:var(--mono); font-size:.7rem; color:var(--ink-faint); font-weight:400}
|
|
215
|
+
.cmap-kind{display:inline-block; margin-left:.4rem; font-size:.68rem; color:var(--ink-mute);
|
|
216
|
+
text-transform:lowercase; font-variant:small-caps; letter-spacing:.02em}
|
|
217
|
+
|
|
218
|
+
/* The talks the map cannot show. Collapsed, because they are a footnote to it —
|
|
219
|
+
but present, because dropping them would make the map read as complete. */
|
|
220
|
+
.cmap-off{margin:1.2rem 0 0; display:grid; gap:.5rem}
|
|
221
|
+
.cmap-off details{border-left:2px solid var(--ink-faint); padding:0 0 0 .7rem}
|
|
222
|
+
.cmap-off summary{font-size:.8rem; color:var(--ink-2); cursor:pointer}
|
|
223
|
+
.cmap-off summary:hover, .cmap-off summary:focus-visible{color:var(--accent)}
|
|
224
|
+
.cmap-off ol{list-style:none; margin:.5rem 0 0; padding:0; display:grid; gap:.5rem}
|
|
225
|
+
.cmap-off ol li{font-size:.8rem; line-height:1.45}
|
|
226
|
+
.cmap-off .cmap-note{margin:.5rem 0 0}
|
|
227
|
+
.cmap-off code{font-size:.9em}
|
|
228
|
+
.cmap-mentee{margin:.5rem 0 0; font-size:.8rem; line-height:1.45}
|
|
229
|
+
.cmap-mentee[hidden]{display:none}
|
|
230
|
+
.cmap-mentee p{margin:0 0 .3rem}
|
|
231
|
+
.cmap-mentee ul{margin:0 0 .3rem; padding-left:1.1rem}
|
package/src/mappick.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The picking, hovering and dimming shared by the maps on this site.
|
|
2
|
+
//
|
|
3
|
+
// Progressive enhancement, and only that: with no JavaScript every pin, every
|
|
4
|
+
// trajectory and every name is already on the page, which is the honest default
|
|
5
|
+
// anyway. What this adds is the ability to pull one entry out of thirty.
|
|
6
|
+
//
|
|
7
|
+
// It toggles `hidden` rather than a class so the effect is in the DOM rather
|
|
8
|
+
// than only in the paint — a screen reader is told the same thing the eye is.
|
|
9
|
+
//
|
|
10
|
+
// Written against the markup both maps share — a `.cmap` figure containing
|
|
11
|
+
// `.cmap-pin`/`.cmap-trail` elements, a `.cmap-who` select, and a `.cmap-list`
|
|
12
|
+
// whose items carry the same `data-c` — rather than against either one, so the
|
|
13
|
+
// conference map got this behaviour without a second copy of it.
|
|
14
|
+
|
|
15
|
+
function wire(fig) {
|
|
16
|
+
const pick = fig.querySelector('.cmap-who');
|
|
17
|
+
if (!pick) return;
|
|
18
|
+
pick.hidden = false;
|
|
19
|
+
pick.labels[0].hidden = false;
|
|
20
|
+
const all = (sel) => [...fig.querySelectorAll(sel)];
|
|
21
|
+
const list = fig.querySelector('.cmap-list');
|
|
22
|
+
|
|
23
|
+
const show = (only) => {
|
|
24
|
+
const mine = (el) =>
|
|
25
|
+
only === '' || (only === 'mentees' ? 'mentee' in el.dataset : el.dataset.c === only);
|
|
26
|
+
for (const el of all('.cmap-pin, .cmap-trail')) el.style.display = mine(el) ? '' : 'none';
|
|
27
|
+
// Showing everything means the map, and only the map: a roster of thirty
|
|
28
|
+
// entries under it is the wall of text the picker exists to avoid.
|
|
29
|
+
if (list) {
|
|
30
|
+
list.hidden = only === '';
|
|
31
|
+
for (const li of all('.cmap-list > li')) {
|
|
32
|
+
li.hidden = !mine(li);
|
|
33
|
+
// The detail belongs to whoever was asked for.
|
|
34
|
+
const detail = li.querySelector('ol');
|
|
35
|
+
const note = li.querySelector('.cmap-note');
|
|
36
|
+
// Only the one asked for; the others are hidden anyway, and content
|
|
37
|
+
// inside a hidden element should not be pretending to be visible.
|
|
38
|
+
const open = only !== '' && mine(li);
|
|
39
|
+
if (detail) detail.hidden = !open;
|
|
40
|
+
if (note) note.hidden = !open;
|
|
41
|
+
const mentee = li.querySelector('.cmap-mentee');
|
|
42
|
+
if (mentee) mentee.hidden = !open;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
fig.dataset.only = only;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Hover and keyboard linking between the two halves. Set on the elements
|
|
49
|
+
// rather than expressed as a rule keyed off the figure: CSS cannot say "this
|
|
50
|
+
// element's data-c matches its ancestor's data-hover" without one rule per
|
|
51
|
+
// entry, and the version that dimmed everything and lit the match back up did
|
|
52
|
+
// not survive contact with a real browser. This does one thing and can be
|
|
53
|
+
// checked by reading it back.
|
|
54
|
+
const dimmable = () => all('.cmap-pin, .cmap-trail, .cmap-list > li');
|
|
55
|
+
const mark = (el) => {
|
|
56
|
+
const owner = el?.closest?.('[data-c]');
|
|
57
|
+
const only = owner?.dataset.c;
|
|
58
|
+
for (const e of dimmable()) {
|
|
59
|
+
// Trails carry their own resting opacity, so restoring means clearing the
|
|
60
|
+
// override rather than writing 1 over the top of it.
|
|
61
|
+
e.style.opacity = only === undefined || e.dataset.c === only ? '' : '.12';
|
|
62
|
+
}
|
|
63
|
+
if (only === undefined) delete fig.dataset.hover;
|
|
64
|
+
else fig.dataset.hover = only;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// The highlight marks whoever a byline sent the reader to; any other use of
|
|
68
|
+
// the map is the reader's own choice, and clears it.
|
|
69
|
+
const unmark = () => { for (const li of all('.cmap-list > li.is-arrived')) li.classList.remove('is-arrived'); };
|
|
70
|
+
pick.addEventListener('change', () => { unmark(); show(pick.value); });
|
|
71
|
+
|
|
72
|
+
// A byline links to /research/?collab=<orcid or slug>#collaborators: the fragment
|
|
73
|
+
// brings the map into view, and this picks that person on it.
|
|
74
|
+
const asked = new URLSearchParams(location.search).get('collab');
|
|
75
|
+
const li = asked && [...(list?.children ?? [])].find((el) => el.dataset.key === asked || el.dataset.slug === asked);
|
|
76
|
+
pick.value = li ? li.dataset.c : '';
|
|
77
|
+
show(pick.value);
|
|
78
|
+
li?.classList.add('is-arrived');
|
|
79
|
+
|
|
80
|
+
fig.addEventListener('pointerover', (e) => mark(e.target));
|
|
81
|
+
fig.addEventListener('pointerleave', () => mark(null));
|
|
82
|
+
fig.addEventListener('focusin', (e) => mark(e.target));
|
|
83
|
+
fig.addEventListener('focusout', () => mark(null));
|
|
84
|
+
|
|
85
|
+
// Clicking a pin or a trail picks it, which is what pointing at something on
|
|
86
|
+
// a map and pressing it ought to do. Hover deliberately does not: sweeping
|
|
87
|
+
// the cursor across eighty-five pins would rebuild the list under the reader
|
|
88
|
+
// several times a second. Hover previews by dimming the others; the click
|
|
89
|
+
// commits. Clicking the one already picked puts everyone back, so a click
|
|
90
|
+
// undoes itself without a trip to the select.
|
|
91
|
+
fig.addEventListener('click', (e) => {
|
|
92
|
+
const owner = e.target.closest?.('.cmap-pin, .cmap-trail');
|
|
93
|
+
if (!owner) return;
|
|
94
|
+
// The pin is a link to the record, for a reader with no script; here the
|
|
95
|
+
// click picks it instead, and the page should not jump.
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
unmark();
|
|
98
|
+
pick.value = pick.value === owner.dataset.c ? '' : owner.dataset.c;
|
|
99
|
+
show(pick.value);
|
|
100
|
+
// Selecting hides everything else, so the thing being pointed at is no
|
|
101
|
+
// longer under the cursor and the dimming has nothing left to say.
|
|
102
|
+
mark(null);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Wire every map on the page, once each. Both components import this, and
|
|
107
|
+
* Astro serves one copy of the module however many of them there are. */
|
|
108
|
+
export function wireAll() {
|
|
109
|
+
for (const fig of document.querySelectorAll('.cmap')) {
|
|
110
|
+
if (fig.dataset.wired) continue;
|
|
111
|
+
fig.dataset.wired = '1';
|
|
112
|
+
wire(fig);
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/mapzoom.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Zoom and pan for the world maps, by moving the SVG's viewBox.
|
|
2
|
+
//
|
|
3
|
+
// Progressive enhancement, like mappick.js: the controls are `hidden` in the
|
|
4
|
+
// markup and shown here, so with no script there is nothing that does nothing.
|
|
5
|
+
// The viewBox keeps the map's aspect ratio, so the figure never changes height
|
|
6
|
+
// and the page below it never jumps.
|
|
7
|
+
|
|
8
|
+
export const MAX_ZOOM = 8;
|
|
9
|
+
const STEP = 2;
|
|
10
|
+
|
|
11
|
+
/** The viewBox for zoom `z` centred on (cx, cy), kept inside the map. */
|
|
12
|
+
export function viewFor(W, H, z, cx, cy) {
|
|
13
|
+
const w = W / z;
|
|
14
|
+
const h = H / z;
|
|
15
|
+
return {
|
|
16
|
+
x: Math.min(Math.max(cx - w / 2, 0), W - w),
|
|
17
|
+
y: Math.min(Math.max(cy - h / 2, 0), H - h),
|
|
18
|
+
w,
|
|
19
|
+
h,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Is the point inside the view? */
|
|
24
|
+
export const inView = (view, x, y) =>
|
|
25
|
+
x >= view.x && x <= view.x + view.w && y >= view.y && y <= view.y + view.h;
|
|
26
|
+
|
|
27
|
+
/** Zoom to `z`, holding the point under the cursor (fractions fx, fy of the
|
|
28
|
+
* view) where it is: the centre of a button zoom is (.5, .5). */
|
|
29
|
+
export function zoomAbout(W, H, view, z, fx = 0.5, fy = 0.5) {
|
|
30
|
+
const next = Math.min(Math.max(z, 1), MAX_ZOOM);
|
|
31
|
+
const w = W / next;
|
|
32
|
+
const h = H / next;
|
|
33
|
+
return viewFor(W, H, next, view.x + fx * view.w - fx * w + w / 2, view.y + fy * view.h - fy * h + h / 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function wire(fig) {
|
|
37
|
+
const stage = fig.querySelector('.cmap-stage');
|
|
38
|
+
const svg = stage?.querySelector('svg');
|
|
39
|
+
const bar = stage?.querySelector('.cmap-zoom');
|
|
40
|
+
if (!svg || !bar) return;
|
|
41
|
+
const [, , W, H] = svg.getAttribute('viewBox').split(' ').map(Number);
|
|
42
|
+
let view = { x: 0, y: 0, w: W, h: H };
|
|
43
|
+
const zoomOf = () => W / view.w;
|
|
44
|
+
|
|
45
|
+
// Where each person's pins are, so a trail can be dropped when none of them is
|
|
46
|
+
// on screen: a line running across the view from somewhere else says nothing.
|
|
47
|
+
const pinsOf = new Map();
|
|
48
|
+
for (const c of stage.querySelectorAll('.cmap-pin')) {
|
|
49
|
+
const dot = c.querySelector('circle');
|
|
50
|
+
if (!pinsOf.has(c.dataset.c)) pinsOf.set(c.dataset.c, []);
|
|
51
|
+
pinsOf.get(c.dataset.c).push([Number(dot.getAttribute('cx')), Number(dot.getAttribute('cy'))]);
|
|
52
|
+
}
|
|
53
|
+
const trails = [...stage.querySelectorAll('.cmap-trail')];
|
|
54
|
+
|
|
55
|
+
const apply = () => {
|
|
56
|
+
svg.setAttribute('viewBox', `${view.x.toFixed(2)} ${view.y.toFixed(2)} ${view.w.toFixed(2)} ${view.h.toFixed(2)}`);
|
|
57
|
+
const z = zoomOf();
|
|
58
|
+
// Pins and lines are drawn in map units, so they grow with the zoom unless
|
|
59
|
+
// the stylesheet scales them back: it multiplies every size by --k.
|
|
60
|
+
svg.style.setProperty('--k', String(1 / z));
|
|
61
|
+
stage.dataset.zoom = z > 1 ? String(z) : '';
|
|
62
|
+
for (const t of trails) {
|
|
63
|
+
t.toggleAttribute('data-offview', z > 1 && !pinsOf.get(t.dataset.c)?.some(([x, y]) => inView(view, x, y)));
|
|
64
|
+
}
|
|
65
|
+
bar.querySelector('[data-zoom="in"]').disabled = z >= MAX_ZOOM;
|
|
66
|
+
bar.querySelector('[data-zoom="out"]').disabled = z <= 1;
|
|
67
|
+
bar.querySelector('[data-zoom="reset"]').disabled = z <= 1;
|
|
68
|
+
};
|
|
69
|
+
const to = (z, fx, fy) => { view = zoomAbout(W, H, view, z, fx, fy); apply(); };
|
|
70
|
+
|
|
71
|
+
bar.hidden = false;
|
|
72
|
+
bar.addEventListener('click', (e) => {
|
|
73
|
+
const how = e.target.closest('[data-zoom]')?.dataset.zoom;
|
|
74
|
+
if (how === 'in') to(zoomOf() * STEP);
|
|
75
|
+
else if (how === 'out') to(zoomOf() / STEP);
|
|
76
|
+
else if (how === 'reset') to(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Ctrl or ⌘ plus wheel — which is also what a trackpad pinch sends — zooms at
|
|
80
|
+
// the cursor. A bare wheel is left alone: it is the page scrolling past.
|
|
81
|
+
svg.addEventListener('wheel', (e) => {
|
|
82
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
const r = svg.getBoundingClientRect();
|
|
85
|
+
to(zoomOf() * Math.exp(-e.deltaY * 0.01), (e.clientX - r.left) / r.width, (e.clientY - r.top) / r.height);
|
|
86
|
+
}, { passive: false });
|
|
87
|
+
|
|
88
|
+
// Drag to pan, once there is somewhere to pan to. A drag that moved is not a
|
|
89
|
+
// click: without this, letting go over a pin would pick it.
|
|
90
|
+
let drag = null;
|
|
91
|
+
let moved = false;
|
|
92
|
+
svg.addEventListener('pointerdown', (e) => {
|
|
93
|
+
if (zoomOf() > 1 && e.button === 0) { drag = { x: e.clientX, y: e.clientY, view: { ...view } }; moved = false; }
|
|
94
|
+
});
|
|
95
|
+
svg.addEventListener('pointermove', (e) => {
|
|
96
|
+
if (!drag) return;
|
|
97
|
+
const r = svg.getBoundingClientRect();
|
|
98
|
+
const dx = e.clientX - drag.x;
|
|
99
|
+
const dy = e.clientY - drag.y;
|
|
100
|
+
if (!moved && Math.hypot(dx, dy) < 4) return;
|
|
101
|
+
if (!moved) svg.setPointerCapture(e.pointerId);
|
|
102
|
+
moved = true;
|
|
103
|
+
view = viewFor(W, H, zoomOf(), drag.view.x + drag.view.w / 2 - (dx / r.width) * view.w,
|
|
104
|
+
drag.view.y + drag.view.h / 2 - (dy / r.height) * view.h);
|
|
105
|
+
apply();
|
|
106
|
+
});
|
|
107
|
+
const end = () => { drag = null; };
|
|
108
|
+
svg.addEventListener('pointerup', end);
|
|
109
|
+
svg.addEventListener('pointercancel', end);
|
|
110
|
+
fig.addEventListener('click', (e) => { if (moved) { e.stopPropagation(); e.preventDefault(); moved = false; } }, true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function wireZoom() {
|
|
114
|
+
for (const fig of document.querySelectorAll('.cmap')) {
|
|
115
|
+
if (!fig.dataset.zoomWired) { fig.dataset.zoomWired = '1'; wire(fig); }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Generated by scripts/build-worldmap.mjs from Natural Earth 110m land and lakes (public domain), projected to Equal Earth. Do not edit by hand.",
|
|
3
|
+
"projection": "Equal Earth (Šavrič, Patterson & Jenny 2018)",
|
|
4
|
+
"width": 1000,
|
|
5
|
+
"height": 487,
|
|
6
|
+
"d": "M397.9 481.4L397.6 482.3L386.6 481.6L397.9 481.4ZM421.4 479.1L425.7 481.4L407.6 482L415.3 479.1L421.4 479.1ZM279.7 472.6L284.1 472.5L282.7 473.5L277.5 472.8L279.7 472.6ZM317.2 469.9L323.4 470.9L312.7 470.6L310.9 469.9L311.7 469.6L317.2 469.9ZM372.3 468.2L373.3 470.3L369.5 470.9L363.6 470.7L361 469.5L365.8 468.6L364.1 465.5L366 464.3L372.3 468.2ZM382.4 454.3L376.4 455.7L376.1 457.2L378.1 458.7L375.3 459.4L373.5 462.4L384.4 467.8L389.6 472.9L385 475.3L375.5 477.3L363.9 477.4L371.6 478.9L364.7 479.5L371.1 481.6L402.3 484.2L451.2 481.7L448.8 480.5L438.6 480.7L437.8 479.5L468.7 475.1L471.7 474.2L472.2 473.5L470.2 473.2L470.7 472.5L480.8 468.7L486.3 469.5L487.2 468.1L499.6 469.4L514.6 466.2L518 466.4L520.2 468L525.3 466.4L528.4 467.2L536.3 466.2L542.2 467.7L550.8 467.3L560.5 465.8L564.9 463.5L573 466L607.5 457.9L610.9 458.3L614 461.1L618.3 462.4L624.1 461.3L632.7 462.4L632.4 464.9L627.4 467L627.1 467.7L629.3 467.7L625.6 469.8L628.6 470.5L639.4 466.2L647.2 465.4L660.7 460.9L668.6 460.8L672.7 458.8L673 460.4L674.2 460.8L685.6 461.3L693.6 461L703.2 457.4L706.8 460.3L723.7 458.1L725.7 459.8L732.5 461L741.1 459.4L751.3 459.9L764.5 458.8L767.6 456.8L766.5 460L767.6 460.4L783.4 460.3L782.7 462.3L785.4 463.3L790.9 464.3L795.3 463.6L797.5 465.2L801.3 465.7L801 466.4L802.7 467.5L812.6 468L817 469.5L807.2 472.8L789.5 476.7L786.6 478.8L789.1 479.9L779.1 480.4L772 482.3L783.4 484.6L799.8 485.2L796.2 486.7L203.8 486.7L200.2 485.2L201 484.8L262 485.4L242.9 484.5L241.7 483.3L233.3 482.4L244.2 482.7L249.6 481.7L231.8 480.3L221.4 477.6L235 478.3L242 477L239.1 475.5L256.2 473.8L282.1 474.2L293.3 472.9L298.3 474.5L320.3 475.4L309.8 471.1L325.1 472.8L335.9 472.3L336.2 471L345.1 472.5L352.4 473.1L353.5 472L362.1 473.3L374.1 471.8L376.1 470.1L370.4 465.9L370.6 463.6L368.6 461.1L368.9 460.2L374.3 455.4L383 452.2L384.5 452.8L382.4 454.3ZM349.8 428.2L356.8 430.6L356.5 431.9L352.9 431.1L351.3 433L348.7 432.7L340.7 430L333 425.4L342.6 428.9L342.9 425.7L344.4 424.5L346.2 424.8L349.8 428.2ZM367.1 420.5L369.4 421.8L369.1 422.8L366.4 423.6L365 422.6L363.6 423.9L361.9 422.6L363.9 420.9L366.1 421.6L367.1 420.5ZM661.4 416.4L657.8 416.6L659.7 413.2L662.8 414.5L661.4 416.4ZM856.2 389L857.7 390.1L863.1 389.3L856.6 396.7L856.4 395.8L853 398L851.3 397.8L853.7 390.2L854.8 388.7L856.2 389ZM923.5 389.4L922.9 390.7L925.8 389.4L924.1 392.1L915.9 397.2L915.4 398.7L910.4 399.9L903.5 405.1L898.3 407.3L893.3 406L897.4 402.6L914.5 394.5L921.1 389.5L924.1 388L923.5 389.4ZM939.8 373.8L939 377.3L940.7 375.1L941.2 376L939.6 378.5L940.8 379.5L945.7 378.9L940.4 383.7L938.6 383.7L926.8 391.9L926.5 390.6L930.2 387.9L930.8 386.2L929.3 384.9L933 382.5L936.9 377.9L938.7 368.4L939.8 368.1L939.9 370L941.2 370.9L939.8 373.8ZM947.8 325.2L946.4 326.1L944 323.5L942.3 317.8L944.5 319.1L947.8 325.2ZM984.7 307.7L985.3 308.8L984.1 310.7L981 310.7L982.7 307.9L984.7 307.7ZM988 305.8L986 306.5L986.1 305.2L990.6 303.1L990.1 304.9L988 305.8ZM957.1 304.5L956 305L956 302.4L957.1 304.5ZM956.7 298.9L956.3 301.9L955.1 301.6L955.7 297.8L956.7 298.9ZM637.2 293.8L637.4 301.7L636.9 302.8L636.1 300.7L635.5 301.8L635.4 306L626.7 331L625 335.1L620.2 337.5L616.8 335.3L615.9 324.8L619.7 317.7L619.4 308L621.1 303.6L626.3 302L630.5 297.7L631.2 294.2L632.3 294.7L635.2 288.2L637.2 293.8ZM893.3 294.6L893.6 297.5L895.7 296.1L897.2 299.1L896.1 313.6L901 318.8L900.8 325.9L902.1 325.1L902.6 326.6L903.5 326.1L902.5 329.9L905.1 336.3L904.5 339.1L902.5 343.3L902.3 346.3L894.2 358.5L879.6 372.2L875.1 378L870 379.3L862.4 383.3L860.2 381.3L861.7 379.6L856.2 382.6L850.4 380L850.1 378L851.6 373.8L851.1 372.4L848.9 372L851.5 367.9L848.9 370.2L846.3 370.8L852 365.4L853.1 362.9L844.9 369.6L843.7 368.2L844.7 366.4L843.7 362.7L844.6 361.9L838.8 358.1L824.4 360.5L818.2 363.1L815.3 366.2L805.5 366.5L799 370.2L795.6 370L792.8 367.3L797.8 360.5L799.6 351L799.4 339.3L800 340.8L800.2 337.5L801.5 339.9L801.5 333.2L806.2 323.7L805.7 326.5L807.5 324L814.2 319.9L816.1 320.1L821.7 317.2L826.3 316.3L831.4 310.9L832.4 307.4L835 304.3L835.5 307.5L836.8 306.7L836.2 305L837.5 303.2L838.5 304L839.4 301.2L844 296.3L845.1 296.8L848.1 294.8L850.8 298.7L854.2 299L854 297L858.7 290.1L860.6 288.8L864.3 288.5L864.6 286.6L862.8 285.4L864.4 284.9L867.2 287.3L871.7 289L875.3 287.6L876.2 289.4L872.8 293L870.2 299.1L880.6 309.1L882.8 307.8L887.2 299.3L887.8 294.4L891.3 284.5L892.6 283.2L893.1 285L893.3 294.6ZM946.7 282.5L947.3 283.7L945.3 283.7L944.7 281.4L946.7 282.5ZM832.8 281.6L828.3 279L830.9 278.3L833.1 280.6L832.8 281.6ZM943.6 280.2L940.9 279.9L940.9 277.9L943.6 280.2ZM946.1 279.2L945.6 279.9L943.8 274.4L944.8 274.4L946.1 279.2ZM843.1 281.2L840.3 281.6L842.2 278L845.6 275.7L851.9 274.7L845.2 278.4L843.1 281.2ZM825.9 273.6L826.8 274.6L828.6 274.3L829.1 275.9L822.4 277.1L823.5 274.9L825.1 274.9L825.9 273.6ZM839.8 273.6L839.1 275.7L834.9 276.7L831.2 276.3L831.4 274.9L833.7 274.1L835.3 275.2L839.8 273.6ZM935.9 270.8L934.2 270.2L933.5 268L935.9 270.8ZM800.7 268.7L806 269.1L806.7 267.5L811.7 269.3L812.5 271.7L816.6 272.4L819.8 274.6L816.5 276L813.6 274.5L799.5 272.4L794.5 270.8L794.2 269.2L791.7 269L793.8 265.4L797.2 265.6L800.4 267.4L800.7 268.7ZM873.2 266.6L871.5 269.1L872.8 263.7L873.2 266.6ZM931.5 268.8L930.7 269.2L929.7 267.8L928.8 262.2L932 267.8L931.5 268.8ZM921.3 263.8L916.1 267L911 264.8L911.3 263.7L915.3 263.9L916.3 262.1L916.4 264L918 263.8L920.6 261.2L920.4 258.9L922.6 259.5L921.3 263.8ZM853.2 256.3L852.1 257.5L850.2 256.9L849.7 255.2L852.5 255.1L853.2 256.3ZM862.2 254.9L863 257.8L860.8 256.3L855 256.1L855.7 254L862.2 254.9ZM924.8 260.2L923.8 261.2L922.9 257.5L918.3 253.6L919.1 252.7L922.6 255.5L924.8 260.2ZM872.6 247.7L873.2 253.7L876 256L878.4 252L881.7 249.7L884.2 249.7L901.2 257.8L904.4 261.6L904.6 263.8L909 266.1L909.5 268.1L906.9 268.5L907.2 271L909.4 273.4L910.7 277.4L912.3 277.2L911.9 278.9L913.9 279.5L913 280.2L915.7 281.8L915.2 282.8L907.8 281.2L903.8 273.5L900.4 271.9L896.1 274.2L896 276.9L893.7 278.2L889.4 277.4L887.3 274.4L884.6 273.6L883.8 274.7L880.3 274.8L881.8 271.7L883.7 270.7L882.3 263.5L874.9 260.1L870.9 256.6L868.9 258.7L868.5 255.7L866.4 253.9L871.4 252.6L871.2 251.6L867.2 251.6L866.1 249.4L863.7 248.7L862.5 246.9L867.7 244.7L872.2 246.3L872.6 247.7ZM847.8 238L845.7 241.8L843.6 242.5L840.9 241.7L833.8 242.5L833.4 245.3L835.9 248.6L837.4 246.9L842.6 245.7L842.4 247.4L841.2 246.8L837.4 250.5L839.9 255.3L839.3 256.6L841.6 260.9L841.4 263.3L839.8 264.4L838.9 263.1L840.4 260.1L837.6 261.5L837 260.5L837.4 259L835.5 256.8L835.9 253.2L834 254.3L833.8 264L832 264.6L830.9 263.5L831.6 256.4L830.5 256.4L829.7 253.8L833.4 241.2L835.8 238.5L841.4 240.1L844.6 239.9L847.3 237.2L847.8 238ZM857.4 239.1L857.3 242.4L855.9 242L855.5 244.3L856.6 246.3L855.8 246.7L853.9 239.6L854.4 236.6L855.2 235.2L855.5 237.3L857.1 237.6L857.4 239.1ZM793.2 265.2L790.1 265.3L784.6 259.1L775.7 242.7L773.8 236.5L764.5 224.8L764.1 222.9L770.2 223.7L779.5 235.5L782.3 235.6L788.4 243L787.3 246L789.9 247.4L791.2 252.1L793.3 252.4L794.5 254.8L793.2 265.2ZM827.4 236.5L830.5 240L827.2 240.4L826.4 246.4L823.7 248.9L822.3 258.4L821.9 257L818.7 258.7L817.7 256.4L814.4 255L811 256.4L810.1 254.6L806 254.3L805.7 249.3L803 245.1L802.9 238.3L804.5 235.9L806.6 237.1L808.7 236.4L809.2 233.3L813.7 231.8L823.1 217.5L824.2 217.5L826.1 221L830.4 223.1L830.3 224.6L828.4 224.8L829 226.6L827 227.9L825.6 231.3L827.8 234.8L827.4 236.5ZM849.2 211.9L850.2 216.5L849.5 219.9L848.2 216.1L847.1 218L848.2 220.7L847.5 222.5L844.1 220.3L843.1 217.6L843.8 215.9L841.8 214.1L838 217.6L837.4 216.5L838.2 213.3L841.1 210.9L842.3 212.6L844.3 211.6L844.5 209.9L846.5 209.8L846 206.9L848.4 208.7L849.2 211.9ZM725 220.2L722.6 221L721.1 218.1L720.3 212.7L721.1 206.7L723.2 208.8L726.3 215.2L726.1 219.1L725 220.2ZM332 205.6L329.2 205.7L330.1 203.2L332.3 202.9L332 205.6ZM841.8 205L839.6 209.7L837.6 207.1L838.6 202.8L840.1 202.5L840 205L841.5 201.5L841.8 205ZM827.1 208.6L823.8 212.1L828.9 201L829.8 204L827.1 208.6ZM835.1 199L838.7 200.2L838.9 201.7L836.2 204.4L835.1 199ZM844.9 198L846.3 202.2L844 201.2L845.3 204.7L844.1 205.5L842 200.5L843.6 200.8L843.4 199.4L841.2 196.6L843.9 196.6L844.9 198ZM833.4 194.7L833.2 197.9L829.8 193.2L832.2 193.3L833.4 194.7ZM828.6 174.7L830.6 175.8L831.2 174.8L833.1 179.9L833.1 182.9L831.7 184.1L833.1 190L835.8 190L839.6 192L840.7 196.6L836.9 192.9L836.5 194.2L834.3 192L831.9 192.6L830.4 191.8L830.9 189.3L830 188.5L829.9 189.8L828.1 187.7L826.5 182.6L827.9 183.8L827 174.7L828.6 174.7ZM322.2 175.7L321.4 176.7L317.7 176.8L318.2 174.7L322.2 175.7ZM291.3 177.1L290.4 177.7L287.6 175.8L289.3 174.7L291.6 175.1L293.2 177L291.3 177.1ZM304.1 169.8L311.1 170.6L311.4 171.9L312.9 171.8L312.7 172.9L315 174.4L313.8 175.8L310.5 175L308.8 175.9L308.5 175L306.1 178L305.5 176.4L303.8 175.8L299.5 176.5L298.2 175.3L298.6 174.2L303 175L304.1 174.1L303.4 171.2L301.8 170.6L304.1 169.8ZM798.8 174.1L796.8 175.9L794.3 174.7L793.5 171.6L794.5 170L798.8 169L799.7 170.4L798.8 174.1ZM79.3 172.6L78.2 172.7L79.9 168.3L81.8 171.1L79.3 172.6ZM79.8 167L78.8 167.2L78.5 166L79.7 166L79.8 167ZM76.5 164.5L75.2 164.6L75.1 163.6L76 163.1L76.5 164.5ZM72.8 162.2L71.7 161.9L73.1 161.3L72.8 162.2ZM287 159.3L287.8 160.7L290.3 160.3L294.3 165L296.7 165.6L296.3 166.7L300.1 168.3L299.6 169.1L290.2 169.8L292.3 167.8L289.7 166.6L288.7 163.5L280.8 161.4L280 160.7L281.2 159.8L278.6 159.6L274.6 162.4L272.1 162.5L276 159.3L280.3 157.8L287 159.3ZM293.4 155.8L291.7 152.9L292.8 150.6L293.5 150.8L293.4 155.8ZM824 159.3L823.7 162.2L820.3 156.5L821.9 150.3L823.5 151.4L824 159.3ZM294.8 145.8L291.8 146.3L291.9 145L293.3 144.8L295 144.9L294.8 145.8ZM297 145.7L296 148.3L295.3 144.2L297 145.7ZM842.8 119.6L843.7 120.8L843.4 122.8L841.8 121.7L840.9 122.5L841.2 124.5L839 123.6L838.6 119.9L840.2 120.3L840.5 118.9L842.8 119.6ZM587.3 114.5L585.8 115.9L586.2 116.8L583.8 118.2L581.7 116.4L587.3 114.5ZM559.8 114.4L561.3 115.5L566.5 115.7L566.3 116.7L562.7 117L559.5 115.8L559.8 114.4ZM538.6 106L537.9 111.3L531 108.1L531.3 106.4L538.6 106ZM522.5 96.4L524.1 98.6L523.9 102.9L521.8 103.8L520.9 102.9L520 97.2L522.5 96.4ZM853.1 109.6L855.2 116.3L852.9 117.9L848.5 118.1L847 121.9L844.6 120.6L843.1 118.1L834 120.5L837.8 123L838.9 128.8L837.9 130.3L836.1 128.9L835.2 125.8L830.9 122.5L832.8 121.5L835.3 115.3L839.8 114.3L842.9 115L842.1 109.1L844.7 110.7L847 106.1L842.8 96.4L845 95.8L849.4 100.3L851.1 102.9L850.9 106.2L853.1 109.6ZM523.2 93.4L522.5 95.8L520.7 93L522.7 90.7L523.2 93.4ZM844.6 87L846.8 87.7L847.5 86.4L850.7 89.9L847.8 90.7L848 93.9L842.6 91.7L843.8 95.2L841.1 95.2L838.5 92.1L838 89.6L840.4 89.5L836.6 82.7L844.6 87ZM350.1 79.7L353.9 80L351.4 81.5L348.8 80.2L349.9 78.2L350.1 79.7ZM357.4 72.1L352 69.8L355.6 70.3L357.4 72.1ZM213.6 73.8L212.1 74.2L209.4 72.9L208.5 70L206.6 69.4L207 67.9L207.7 67.2L212.5 68.6L213.6 73.8ZM372.1 67.4L369.7 70L371.5 69L372.8 69.6L371.7 70.6L376.7 71.6L375.3 73.8L377.1 73.3L377 76.7L375.2 79.4L372.7 78.9L373.5 76.1L369.9 78.7L368.6 78.6L370.7 77.2L368.7 76.4L361.6 76.5L363.4 74.6L362.7 73.8L369.3 67.3L373.7 64.7L374.7 64.8L372.1 67.4ZM206.2 58L208.5 57.7L204.9 60.9L204.8 63.2L204 63.2L203.8 59.7L206.2 58ZM827.1 67.3L834.2 72.4L829.9 71.5L832.2 75.7L838.8 81L835.5 79.1L835.9 81.5L823.2 66.7L815.7 60L816.5 58.7L814.9 57.1L827.1 67.3ZM484.7 62.9L480.7 64.6L477.5 64.2L479.5 61.2L478.5 58.4L483.4 55L485.2 54.9L487.5 56.5L484.7 62.9ZM527.7 53.7L526.6 55.9L523.8 53.2L526.9 52.3L527.7 53.7ZM170.7 49.7L167.4 50.7L168.2 48.8L174.1 47.6L170.7 49.7ZM493.6 45.8L491.3 48.5L495.8 48.2L493.2 52.7L495.5 52.9L497.5 56.4L499.1 56.8L501.1 61L503.8 61.6L502.4 64.2L503.3 65.7L501.3 67.2L493.3 67.4L488 69.5L486.8 69L492.3 65.3L488.7 64.8L488.1 63.7L490.5 62.8L489.3 61.3L489.8 59.5L493.1 59.7L493.5 58.1L492 56.4L489.3 55.9L489 53.2L487.8 54.5L486.7 50.6L489.4 45.8L493.6 45.8ZM153.2 42.5L151.4 42.9L150.3 41.8L154.3 41.6L153.2 42.5ZM337.7 37.1L335.3 38.1L336.7 36.5L337.7 37.1ZM333.3 35.8L328.5 37L330.9 35.3L333.3 35.8ZM154.2 33.3L158.6 34.4L155.8 35.2L152.9 34.4L154.2 33.3ZM331.9 29.1L331.5 30.1L332.8 29.7L336.8 31.8L336.1 32.8L338.6 33.4L336.3 34.1L333.6 33.6L333.2 32.6L326.5 35L326.9 33.6L323.9 33.8L330.6 28.9L331.9 29.1ZM471.6 27.4L470.9 28.8L473 30.3L462.3 34L454.3 32.9L456.4 31.9L452.3 30.8L456 29.7L452 29.2L453.6 27.8L456.7 27.5L459.4 29L462.6 27.8L465 28.4L471.6 27.4ZM352.6 26L350.3 26.1L352.3 23.9L355.4 24.2L352.6 26ZM157.9 27.1L160.5 26.1L165.2 26.4L165.7 28.4L158.6 29.6L153.5 32.2L149.9 30.7L151 29.8L146.9 29.7L149 28.1L142.1 30.6L156.9 22.3L160.3 25.8L157.9 27.1ZM318 22L316.1 22.7L310.7 21.4L315.1 20L318 22ZM836.3 18.8L833 18.3L833.9 17.5L836.3 18.8ZM166.3 18.6L163.7 18.8L166.1 17.5L169.7 18L166.3 18.6ZM328.4 21.3L326.5 23.3L330.5 21.7L331.6 23L329.7 24.5L330.4 25.8L338.6 20.5L343.7 20.9L345.4 21.9L342.4 23.9L342.9 25L341.8 26L336.8 27.5L332.3 27.2L326 31.1L317 33.7L315.7 35.2L313.1 35.5L304.6 40.1L299.8 45L302.6 45.4L301.3 49.8L304.7 49.2L313.9 54.5L319.7 54.9L317 60.1L316.8 63.2L318.7 65.9L323.7 62.1L325 57.7L324.3 56.2L328.5 55L334.4 51.2L335.1 47.2L333.7 45.3L337.9 42.7L340.3 36.7L342.1 36.1L349.3 36.4L352.3 39.5L355.9 39.7L353.5 44.9L355.2 45.3L355.9 46.8L365.3 41.5L367.7 50.1L366 51.7L369.5 54.8L373.7 56.3L373.7 58.7L375.7 60.1L374.7 63.3L362.7 68.7L348.1 68.8L333 78.9L340.6 74.4L346.4 72L350 71.7L351.5 73.1L348.5 75.1L347.9 80.6L350.4 82.2L354.5 81.7L358 78.3L357.4 80.5L358.6 81.6L342.8 89L341 88.7L341.9 86.1L346.9 83.5L340.3 84L340.4 85L331.5 88.5L328.2 92.8L328.4 94.5L329.4 94.5L329.6 93.4L330.1 94.1L329.5 95L319.6 97.3L323.9 97.3L318.8 97.8L314.4 103.7L313.5 101.9L313.4 105.5L309.9 109.4L311.2 107L310.4 105.7L311 103L309.9 106.5L308.5 106L309.8 107.1L308.6 114.9L306.3 117.4L298 121.8L290.1 128.9L288.8 133.7L289.2 144.7L286.9 150.6L284.8 150.7L284 148.3L282.7 141.2L283.7 137L281.6 133.5L278.5 135.1L275.9 132.4L268.6 132.7L267.4 133.2L267.5 136.3L264.8 136.9L263 136.8L261.6 134.9L255.8 134.8L247.1 139.7L244.1 143L238 162.5L240.4 173.6L243.1 174.5L244 176.1L254.6 171.9L257.2 165.7L266.3 163.7L266.3 166.2L263.4 170.6L261.9 175.6L260.8 174.8L259.4 181.9L257.5 184.3L259.6 185L268.3 183.9L272.2 186.6L272.4 190.1L269.3 201.9L273 209.8L276.4 210.3L280.4 207.5L287.7 211.1L291.2 208.1L292 203.7L293.8 202L297.9 201.5L302.9 197L304.5 198.2L303.7 200.3L302 200.8L302.6 204.4L301.2 206.5L302 209.5L303.2 209.2L304.1 206.6L303.4 202.4L307 200.9L306.7 199.2L307.8 198L308.6 200.6L310.5 200.7L312.1 204L317.5 203.6L321.1 205.7L322.8 203.7L329.5 203.4L327.1 204.5L327.9 206.2L330.1 206.5L332.1 208.3L332.4 211.3L333.8 211.2L336.6 213.5L338.2 217.8L341.7 221L345 221.8L347.5 220.8L350.5 221.8L353.4 223.1L356.7 227.8L357.6 227.6L359.7 236.2L361.2 236.9L361.3 239.4L359.2 242.5L360 243.7L364.9 244.2L365.1 248L367.2 245.5L375.3 249.2L376.7 251.4L376.2 253.4L379.4 252.3L384.9 254.3L389 254.1L396.8 261.4L402.3 263.8L403.9 270.8L403 277L393.9 292L393.4 309.7L392.6 316L390.3 320.7L390.2 324.4L388.2 326L387.8 328.1L380.9 329.5L373.6 334.9L371.8 338.5L372.2 348.3L368.9 356.3L365.7 360.6L363.2 367.9L360.7 369.8L357.4 369.5L353 368.2L351.1 366.3L351.3 368L355.2 370.9L355.4 373.2L357.3 374.7L356.2 380.5L353 382.3L345.4 382.6L347.7 388.6L344.2 390.2L341.4 389L340.8 389.9L342.1 393.1L344.1 394L345.1 393L346.3 394.7L343.2 397.6L344 402.4L341.7 402.4L340.4 404L340.6 406.3L346.3 409.1L346.5 411.8L344.5 413.5L344.4 416.9L342.5 419.4L346.9 424L343.9 423.9L341.6 425.6L342.5 428.2L341.6 428.3L331.5 423.8L324.9 413.4L324.6 410.5L326 408.2L322.1 407.3L323.2 404.6L321.9 399.5L325 400.6L323.7 394.1L321.8 393.2L322.5 397.2L320.8 396.7L318.9 384.1L315.7 377.2L316.7 377L316.5 361.2L313.3 348.9L313.9 344.7L311.7 322.4L309.3 311.4L292.1 297.9L279.3 270.2L274.9 266.3L275.7 264.6L274.2 261.1L278.5 253.3L277.9 251.7L276.9 253.4L275.2 251.8L275.2 247.3L276.2 246.8L277.5 240.5L281 238.2L282.3 233.5L283.6 233.3L286 229L285 228.1L285.8 221.5L283.2 213.3L283.9 212.3L281.5 209.8L280.3 210L277.5 213.1L278.7 215.2L276.2 216.4L275.8 214.1L274.5 214.6L274.1 213.1L271.1 212.4L270.9 213.2L269.2 211.8L269.1 209.6L265.7 205.7L265.1 207.7L263.8 206.3L264 202L259.4 195.3L260.4 195L260.1 193.8L257.3 194.3L250.1 191.5L245.3 185.3L242 183.2L236.6 185.2L225.9 179.6L223.5 176.9L219.5 175.5L215.4 169.5L215.2 167.7L217.3 164.2L216.9 161.1L212.6 150.8L210.8 149.3L211.7 146.3L207 137.5L207.6 129.8L206.3 128.4L204.3 127.6L202.3 133.3L205.8 145.5L205.7 153.9L207 154L207.8 157.8L205.9 159.1L205.8 157L202.1 152.3L203.3 147.8L198.9 143.8L198.1 141.8L199.4 141.7L201.5 138.8L199.3 135.4L199.7 123.4L198 120L193.6 118.1L194.3 107.5L193.4 103.7L194.3 99.2L198.9 91.4L206.1 82.8L210.1 74.8L213.5 75.2L212.6 78L216.3 72.4L213.1 68.2L210 67L212.7 62.7L210.9 61.5L212.5 59.3L211.7 57.3L213 55.9L211.7 54L213.4 51.7L212.8 49.5L214.2 47.1L209 46.8L207.3 46.1L206 43.5L201.9 42.1L198.7 42.3L194.9 40.1L191.9 40.7L190.2 42.4L179.9 44.4L188.8 39.2L175.6 43.9L175.6 45.2L171.3 47L155.4 52.7L136.4 57L162 48.5L167.8 45L162 46.3L162.2 45L160.5 45.9L161.3 44.6L156.6 45.7L160 43.2L159.9 42.3L156.5 42.8L155.7 41.1L157.7 39.7L157.6 38.6L160.4 37.3L166.5 34.8L176.2 33.3L176.9 31.9L179.7 31L170.2 31.8L168.1 31.3L168.2 29.1L178.5 27.2L180.1 27.2L178.1 28.2L182.4 28.1L182.9 26.8L181.7 24.1L180.1 23.5L195.9 19.7L209 17.8L211.2 18.2L211.2 19L229.8 20L239.7 22.4L255.8 19.9L256.1 20.7L259.7 19.4L261.6 21.3L265.8 20L264.2 21.4L270.5 20.7L280.2 22.4L281.6 23.4L277.7 24.4L288.3 24.2L289 25.5L292.2 24.4L291.2 23.6L293.2 22.9L297.3 22.6L299.1 24.2L303.9 24.9L310 24.7L313.1 23L315.4 23.7L313.5 25.7L320.6 22.1L318.3 20.1L324.1 16.8L327.3 17.9L327.8 19.9L325.3 20.9L328.4 21.3ZM291.7 14.8L289.8 15.6L294.5 15L296 15.9L299.1 15L300.1 15.6L299.6 17.3L301.4 16.6L302.1 14.8L305.5 14.8L306.8 15.5L305.1 18.5L309.7 20.3L305.3 21.2L305.7 22L304.3 22.7L298.5 21.9L283.1 23.1L283.1 22.2L279.2 21.9L278.7 20.4L288.9 19.6L279 19.3L284.1 17.9L278.6 17.4L284 15.5L291.7 14.8ZM309.9 14.3L307 15.4L305.5 14.2L309.9 14.3ZM360.7 14.8L354.4 15.4L352.7 14.4L358.3 13.9L360.7 14.8ZM342.1 14.7L342.5 15.8L350.7 13.7L352.3 15.4L351 16.6L357.4 15.4L362.6 17.1L362.3 17.9L366 17.4L372.1 20.1L372.7 21.9L368.5 22.8L374.9 24.5L376.5 26.4L379.4 26.6L372.9 30.6L369.3 27.6L366.6 27.8L365.5 29.1L369.3 31.9L369.3 34.2L367.6 35.9L361.5 33.4L364.2 37.6L355.7 35.3L354 34.2L355.1 33.5L350.7 31.3L344.3 32.3L343.1 31.5L345.7 29.9L353.7 29.6L359.2 24.8L353.4 22.4L355 21.9L351.4 20L346.3 20.8L333.6 19.5L332.6 18.9L335.4 18.1L332.7 18L333.9 16.3L339.4 14.1L344.5 13.7L342.1 14.7ZM318.2 13.6L323.4 13.7L320.8 15L322.8 15.7L320.8 17.3L315.4 17.8L311.8 15.8L315.9 15.5L315.2 14.4L318.2 13.6ZM761.8 14.6L754.6 14.4L757.3 13.6L761.8 14.6ZM329.3 15.3L324 16.6L324.4 15.1L327 13.6L336.1 13.6L329.3 15.3ZM276.2 17.7L270.2 18.6L270.2 17.8L267.2 16.9L275.1 13.9L274.7 12.9L287.8 13.1L290 14.2L276.2 17.7ZM769.7 11.7L765.6 12.1L760.9 11.1L769.7 11.7ZM332.3 11.8L326.5 11.9L331.2 10.8L332.3 11.8ZM758.4 11L758.9 12.1L749.8 12.4L744.7 11.4L744.1 10.4L758.4 11ZM326.4 9.3L327 10L324.2 11.8L317.4 11L318.6 9.9L326.4 9.3ZM308.4 10.1L312.1 10.4L309.6 11.8L297.9 12.7L295.5 12.2L300.1 11.6L289.6 11.5L296.2 9.7L305.6 11.1L304.8 9.7L308.4 10.1ZM607.6 19L600.4 18.9L595.8 17.6L595 16.6L598.8 13.9L597 13.7L600.5 12.4L599.6 11.7L608.3 10L621.5 9.6L605.4 12.9L601.9 16L603.2 17.5L607.6 19ZM333.7 8.8L338.7 9.3L339.7 9.7L338.7 10.2L341.2 10.9L357.1 11.3L352.4 12.7L338.5 12.5L334.2 12L335 10.5L333.9 9.9L328.9 9.3L333.7 8.8ZM296.9 8.1L293.3 9.6L282.3 10.2L296.9 8.1ZM688.1 9L702.8 10.6L703.3 11.3L697.5 13.1L706.7 14.4L709.5 13.7L725.2 15L723.5 13.8L730.7 14L745.4 18.8L744.5 17L759.6 17.6L755.8 16L757 15.2L775.4 16.3L785.8 18.7L797 18.7L805.2 21.4L817.8 21.1L824.1 22.8L825.4 22.2L821 20.1L843.1 22.3L857.9 30.6L857 31.6L854.1 31.4L866.4 36.7L861.8 36.2L857.5 38.3L856 41.5L856.9 42.6L851.4 40.9L848.7 42.8L846.5 41.9L846 43L842.7 42.6L844.9 46.8L849.5 48.4L854.1 52.2L852.2 52.3L854 54.6L856.3 55.7L854.1 57.1L856.9 60.3L854.3 61L856.7 63.8L856.3 66.5L840 54.3L836.6 50.6L835.2 47.8L837.8 47.2L837.6 41.5L838.6 39.5L835.4 36.1L833.2 36.3L834.8 38.3L833.3 41L827.5 38L823.3 38.8L823.5 42.9L827.2 44.5L820.4 45.4L818.3 43.6L814.5 43.2L813.4 44.4L800.4 44.7L797.3 56.1L801.1 56.4L806.6 58.7L806.7 57.4L809.3 57.5L815.5 60.6L824.9 74L825.9 80.4L824.7 89.4L822.8 91.3L818.7 89.8L816 94L817.5 97.4L814.5 101L815.1 102.8L818.7 104.8L824.9 110.8L827.1 116.5L821.6 118.8L816.6 111L818.2 110.4L814.9 107.6L813.3 107L812.8 107.9L810.6 106.4L809.4 101.7L806.1 100.5L800.1 103.8L800.5 102.3L799.3 101.1L800 98.9L797.7 97.2L794.7 100.6L794.4 102.7L792.1 102.8L791.6 104.4L797.3 108.6L799.8 109.6L801.3 107.2L805.9 108.6L807.2 110.3L804.2 111.2L802.1 117.1L805.8 118.9L814.2 128L815.3 130.5L814.1 131.5L817.5 134.4L818.6 140L817.3 140.3L815.3 153L809.9 159.3L807 159.7L805.8 161.3L804.5 160.1L803.5 161.9L797.6 164.3L797.6 168.1L796.2 168.3L795.1 164.3L791.2 163.1L787.2 166.8L785.8 170.2L785.8 172.7L797.3 186.5L799.7 193.4L800.3 199.9L798.4 202.3L790.5 211.2L789.3 208.9L789.8 206.3L787.5 204.2L785.1 203.7L781.9 197.9L779.2 196.2L776.8 196.3L776.8 193.4L774.4 193.4L773.9 208.9L775.7 209L777.9 215.6L785.4 222.7L789.5 238.5L787.5 238.8L781.5 233L777.8 223.5L777.2 219.2L772.2 212L772 214.2L771.7 200.7L770.2 194.5L764.3 180.5L760.2 184.9L756.8 183.8L756.9 179.2L755.7 175.8L752.9 170.3L748.7 166.9L744.4 159.3L741.9 159.2L742.1 162.7L738.6 161.9L738.5 163.2L733.5 163.9L734.2 166.6L733.2 168.8L729.8 171.2L723.5 180.2L723.8 181.9L719 184.3L720.1 204.7L718.7 204.9L717.7 207.7L718.6 208.9L716.2 210L714.4 213.6L711.5 210.1L703.6 189L700.5 183.9L695.1 164.4L691.5 166.6L689.6 166.1L685.3 161.8L686.4 160.5L685.4 159.1L679.6 155.1L675.8 149.9L663.1 151.1L651.8 148.7L650 144.4L648.6 143.8L644.3 146.1L640.9 145L635.1 141.3L630.1 133.3L628.8 133.9L627 132.7L626.2 134.1L624.7 133.9L626.6 138.9L628.1 141.9L632.2 145.4L632.5 148L634.9 152.2L634.8 147.8L636.4 148.5L636.5 152.7L637.9 154.9L643.7 154.5L648.7 146.4L649.6 151.6L651.2 154.1L656.6 156.5L660.2 161L657.6 167.8L655.9 168.4L656.1 173.1L653.3 174.5L652.7 177L650.1 177.9L650.2 179.4L642.7 182.5L642.4 185.4L635.5 188.6L633.3 191.2L625.1 193.9L623.5 196L619.4 196.3L616.4 186.8L616.1 181.1L610.6 171.2L605.2 164.6L604.5 160L602.6 156L599.7 153.9L592.1 140.6L590.8 140.6L590.9 135.6L589 142L584.3 134.3L590.1 147.3L595.1 155.2L594.9 158.1L598.8 162.1L601.5 174.3L604.2 176.6L607.1 184.2L618.3 196L619 198.7L617.5 199.6L621.6 204.4L622.9 204.4L640.5 198.5L640.6 203.7L636.9 217.9L632.4 227.6L629.3 232.7L619.8 242.3L615.5 249.7L611.8 253L608.7 260.8L607.4 267.6L609.2 268.9L608.3 275.1L611.5 283.5L611.5 298L607.4 305.5L601.6 308.6L593.9 316.6L593.5 319.2L595.3 324.9L594.4 332.2L586.2 337.9L586.9 339.7L584.1 348.6L572.4 362.4L565.7 366.4L557.6 366.1L549.8 369.4L546.8 367.1L546 361.9L547 358.6L540 342.7L538.2 325L532 310.4L532.1 302.1L534.3 293.8L537.5 288.2L537.7 283.4L535.5 277.6L536.6 275.3L533 262.2L524.4 247.5L527.2 231.9L526.1 229.4L524.8 228.8L523.6 225.5L516.4 227.4L512 219.9L505.2 220.4L494.6 225.7L487.1 224L479.1 227.1L475 225.3L465.6 216.2L459.1 202.8L454.3 198L454.2 192.7L451.8 188.6L455.1 183.4L456.2 176.2L456.1 169L454.1 165.7L454.5 162.5L457.4 155.9L459.9 153.1L461.9 146.9L466.9 140.7L469.4 140.3L475.1 134.1L474.6 129.8L476.1 125L482.4 119.8L485 114.2L486.9 114.2L488.4 115.6L494.5 116.2L503.7 111.4L513.4 111L515.7 109.7L521.1 110.3L523.8 108.9L525.6 109.3L525.6 111L527.8 110.4L526.7 112L527.6 114.4L527.4 117.3L525.8 119L526.4 120.9L527.7 120.9L529.4 123.1L539.2 126L540.6 129.1L549.5 132.9L551.9 130.4L551.1 127.8L551.8 126.1L555.2 124.1L558.7 124.8L559.8 126.3L564.2 127.3L564.9 128.4L568.3 128.4L574.8 130.8L579.9 128.5L581.8 128.9L582.7 130.6L583.2 129.5L587.4 130.5L589.1 128.5L591.4 117.9L591.2 114L590.1 112.5L590.8 111.2L587.1 110.8L585.7 112.7L581.9 113L579.6 111.3L576.9 111.2L576.5 112.5L574.8 112.9L572.2 111.2L569.4 111.2L565.5 106.1L566.4 103.6L564.6 102L567 98.9L570.7 98.8L571.4 96.3L576.2 96.8L581.4 93.8L585.5 93.7L593.8 97.2L598.8 97L601.3 95.3L601.4 94L600.3 91.8L587.2 83.7L588.8 83.2L590.2 80.6L588.6 79.4L591.6 77.5L582.5 80.5L583 82.4L586.7 83L586.4 84.1L581 86.4L579.6 85.8L579.8 84.3L577.1 83.4L579.5 81.8L578.7 81.1L574.8 80.4L574.5 79.2L572.4 79.6L570.5 84.3L568.7 84.7L568.6 88.5L567 92L568.5 95L570.8 96.1L570.4 96.9L567.6 97L564.8 99.8L563.8 97.6L561 97.2L558.1 98L560 99.9L558.9 100.4L556 98.7L555.6 99.4L557.8 102.9L556.9 103.6L559.8 106.1L560 107.9L557.6 107L558.5 108.7L557 109.1L558.2 112L556.6 112L554.4 110.6L552.5 105.8L547.7 99.5L547.6 94.7L538.5 89.1L535.5 84.2L533.9 83.7L533.3 85.1L533 82.6L531.1 82.2L529.3 83.3L530.2 87.3L536.8 94L538.7 94L538.7 95.3L545.4 99.7L545.1 100.9L541.4 98.8L540.5 100.9L542.4 102.1L542.3 103.8L541.2 104L540.1 106.8L539.1 107.1L539.9 103.6L537.9 100.1L529.5 94.8L525.4 90.9L524.5 87.8L521.3 86.4L515.7 90.3L511 89.4L507.5 90.4L507.4 94.2L505.1 96.3L502 97L500.3 99.9L499.3 102.5L500.3 104.4L498.3 108L496.4 108.6L494.6 111.2L489 111.2L486.4 113.6L485.2 113.3L483.7 110.3L477.7 110.5L478 105.9L476.4 104.4L478.5 97.8L477.3 90.6L480.8 88.3L495.4 89.4L496.7 87.5L497.2 81.3L493.1 76.6L489.5 75.5L489.4 73.3L496.3 73.4L495.6 70.1L497.7 71.3L503.1 69.1L503.7 66.7L508.7 64.7L510.5 60.6L515.8 58.9L518.1 59.4L519.5 58L517.7 53.9L518.4 49.7L522.6 48.1L522.1 50.3L523.6 51.4L521.1 54.1L521.9 56.4L524.2 57.1L524.2 58L527.6 56.8L531.3 58.7L538.7 55.7L543.4 56.9L543.7 55.7L546.6 54.8L545.5 50.6L546.3 48.9L548.2 48L550.2 50L552 49.9L551.9 46.4L549.7 45.8L549.2 44.3L558.9 43.6L560.9 42.2L558.4 41.1L547.9 42.7L544.3 40.5L544.3 38.2L542.9 36L543.6 34.7L550.4 30.3L547 28.4L543.8 29L542.2 30.5L542.7 31.9L536.3 35.7L535.3 39L539.3 42.1L537.8 44.9L535.7 45.5L534.5 52.4L531.8 52.1L530.8 54.2L528.3 54.4L521.8 43.6L517.8 46.6L515 47.2L512 45.9L510.2 37.5L521 31.7L528.5 24.6L536.2 20.6L543.3 19.9L545.8 18.4L552.4 18.1L558.7 19.4L556.5 19.9L558.9 21.1L560.6 20.5L577.6 24.3L580.2 26.7L578.5 27.8L575.5 28.4L564.8 27L568.6 28.6L569.9 31.9L574.5 33.1L572.8 31L573.8 30.2L579.1 31.6L580.6 31.1L578.6 29.5L582.4 27.4L586.4 28.2L586.9 26.8L583.2 23.1L588.8 23.7L590.5 24.8L588.1 25.1L588.7 26.2L590.5 27L593.3 26.5L593.2 25.2L602.5 22.5L604 22.6L602.8 23.8L612.2 22.5L615.1 23.6L616.4 22.3L613.7 21.2L614.3 20.6L631.8 24L632.3 23L626.9 21.3L624.4 18.4L627.7 14.9L634.1 16.3L633.4 17.7L636.6 19.6L638.2 22.2L641.2 23.4L639.7 27.7L642.2 28L644.2 26.7L644.9 24.7L642.9 23.5L642.8 22.2L639.3 21L639.3 19.1L635.7 17.6L638.1 16.5L636.6 15.2L639.3 16.2L639.9 17.8L642.2 18.2L640.3 16.9L642.8 16.2L650.8 17.1L646.1 13.9L657.1 13.4L654.9 12.6L655.9 11.6L678 9.7L678.8 8.6L682.3 8.1L688.1 9ZM619.9 96.1L623.9 99.4L621.9 99.7L621.2 104.1L622.9 108.2L627.5 110.5L631.2 111.1L634.9 110.2L633.5 103.7L631.3 102.6L631.4 100.3L629.7 100.2L629.6 97.4L632.1 98.2L633.9 97.2L630.5 93.5L628.7 94.3L629.1 96.6L626.9 91.3L623.8 90.3L620.1 85.7L622.5 85.9L622 83.7L626 83.6L624.6 78.8L620 78.2L615.7 80.2L615.1 82L613 82.5L611.4 85.7L619.9 96.1ZM543.1 7.9L539.4 8.4L536.2 7.4L543.1 7.9ZM333.1 7.6L329 7.1L332.5 6.8L334.1 7.2L333.1 7.6ZM326.2 7.3L317.4 7.2L319.5 6.9L318.2 6.1L326.2 7.3ZM682.6 7.3L673.4 7.8L675.9 6.1L682.6 7.3ZM531.4 5.7L537.2 6.5L533 7L530.1 9.2L528 9.3L518 5.8L531.4 5.7ZM543.5 5L547 5.3L539.7 6L529.7 5.1L543.5 5ZM672.8 6.6L655.9 5L663 4.1L672 5.6L672.8 6.6ZM350.4 5.7L342.1 7.4L338.7 7.3L337.3 6.8L339.6 6.1L334.4 5.2L343 4.1L350.4 5.7ZM385 2.6L395.6 3.2L367.5 6.1L369.7 6.3L368.3 6.5L369.2 7L360.1 8.7L362.8 9.3L357.3 10.1L341.9 9.7L346 8.7L346.1 7.8L351.4 8.3L347.2 7.2L353.4 6.1L351.3 5.1L360.2 4.9L350.4 4.9L345.1 3.6L385 2.6ZM454.6 2.3L464.9 2.9L446.9 3.5L479.3 4.1L465.7 5.2L469.6 5.3L465.9 6.8L465.6 8.1L467.5 9L461.8 9.5L464.8 10.2L465 11.4L463 11.6L465.1 12.9L461 13L463 13.6L462.2 14.2L457.1 14.5L458.9 16.4L454.4 16.1L458.9 17.6L459.3 19.1L455.9 19.4L452.6 17.7L452.9 18.9L450.4 19.9L457.9 20.1L446.8 23.3L438.9 24L433.2 26.9L421.2 29.6L416.8 34L412.8 35.8L412.9 37.7L409.3 42.1L406.3 42.2L404 40.2L399.9 40.2L395.9 33.7L396.3 30.2L394.6 28.2L396.1 26.6L395.2 25.9L398.4 23.5L401.6 22.8L404 20.4L398.5 21.7L396.5 21L398.4 18.8L403.7 19.2L396.6 17.3L399.6 15.7L397 12.2L391.4 10.2L378.5 10.3L374.5 9L383 8.5L372.3 7.6L386.8 6L383.5 5.3L393.9 3.7L424.6 3.8L421.3 3L427.2 2.5L454.6 2.3Z",
|
|
7
|
+
"lakes": "M738.5 61.4L738.2 61L738.9 60.3L739.6 58.5L739.3 55.3L738.5 53.9L738.7 53.4L739.7 53.4L742.7 59.1L741.9 58.7L741.7 59.6L742.6 60.3L742 60.5L742.1 61.4L741 62.5L739.7 62.8L738.1 65L736 65.1L734 64.4L734.3 64.1L736.8 63.7L738.5 61.4ZM280.7 58.3L283.7 57.1L282.7 59.7L281.7 65.4L280.7 67.4L279.2 68.1L279.9 65.1L279.1 63.1L278.7 60.2L280.7 58.3ZM264.1 37.5L263.9 35.6L266.3 36.4L267.6 37.3L276.5 34.9L278.1 35.5L272.6 36.6L264.8 39.7L258.4 40.2L255.7 39L256.5 38.5L258.8 39.1L264.1 37.5ZM309.5 89.9L307.8 89.7L309.9 88.3L314 87.4L314.8 88L316.7 86.9L317.1 87.1L316.8 88.8L314.6 89.9L310.9 89.5L309.5 89.9ZM298.1 93.6L299.3 93.8L303 91.9L305 92.1L307.2 91.2L309.4 90.8L306.5 93L301.1 95.2L299.1 95.6L296.7 94.8L298.1 93.6ZM291.4 75.3L296.2 72.5L299.6 73.2L299.9 73.6L299.8 75.3L302.2 75.7L301.4 76.6L301.9 77.5L300.9 78.6L301.7 79L300.7 80L300 79.9L300 79.1L295.1 80L294 78.8L292.9 78.5L294.5 76.9L287.3 79.6L286.4 78.6L283.7 79.2L283.9 78.8L288.5 76.3L291.4 75.3ZM594 242.9L594.8 244.6L594.6 247.3L592.3 250.7L593.4 252L591.8 252.9L591.5 252.5L589.9 252.7L588.6 253.5L587.9 252.1L588.4 249.5L588.4 245.8L589.6 243.6L591.4 243L592.6 242.1L594 242.9ZM561.7 39.3L563.4 38L567.3 39.6L568.5 40.7L568.3 41.1L567.9 41L568 41.8L566.2 41.7L566 42.5L565.1 42.5L563.3 40.2L561.7 39.3ZM685.7 79.1L686.4 79.4L685.9 80.2L685 80.5L678 79.8L677.5 79.3L677.6 79.9L676.5 80.1L676.6 81L675.5 81.4L676.4 84.5L674.1 82.6L673.8 82L674.3 81.3L673.9 80.8L674.7 80.7L674.6 80.1L676 78.9L683.1 79.4L684.4 79.9L684.4 79.4L685.7 79.1ZM585.1 275.4L584.2 275.1L584.5 273.7L583.7 272.7L583.4 270.6L581.8 268.6L580.9 265.9L581.4 264.4L580.7 262.3L581.3 256.3L582.3 259.9L582.1 261.7L582.6 262.2L582.5 263.8L583 265.3L582.3 266.7L584.5 269.2L584.7 271.5L586.2 276L585.7 276.2L585.1 275.4ZM596.5 296.5L596.4 297L595.5 295.5L595 296.4L594.6 295.7L594.7 294.3L594.1 293.2L594.2 291.6L593.5 288.9L594.4 286.8L594.4 282.3L593.5 279.9L593.8 278.8L595.2 280.8L595.3 284.7L596.1 286.1L595.3 289.7L595.5 294.4L596 294.5L596.5 296.5ZM529.5 44.3L529.4 45.1L528.2 45.8L526.5 46.1L526.5 45.1L528.6 44L529.5 44.3ZM287.4 145L286.8 144.9L287.1 144.1L287.6 144.2L287.4 145ZM266.4 201.8L265.2 201.7L264 200.4L263.9 199L264.8 198.8L266.1 200.4L266.4 201.8ZM602.1 199.2L601.7 198.5L602.3 197.8L603.1 198L603.1 199.3L602.7 199.7L602.1 199.2ZM310.8 303.3L309.1 301.9L309.2 300.5L310 300.7L312 302.5L312.7 304.1L312.4 304.7L311.6 304.3L310.8 303.3ZM278.2 60.5L276.2 60.1L276.3 59.4L278.5 57.4L279 57.5L278 58.3L278.7 58.3L277.3 59.6L278.5 60.1L278.2 60.5ZM573 36.8L575.2 39.2L574.8 39.8L573.3 40L572.2 39.6L573.6 39.6L570.1 36.9L570.9 36.9L570.7 36.4L571.8 37.1L572.5 36.8L570.4 35.7L571.8 35.9L573 36.8ZM226.2 95.9L225.3 97.5L224.6 96.6L225 95L225.6 96L226.2 95.7L226.2 95.9ZM268.9 27.9L267.1 28.9L263.9 29L263 29.8L258.5 30.8L260.5 29.6L256.4 30.5L255.5 30.2L259.1 28.6L254.4 28.3L267.4 26.3L264.8 27.4L270 27.2L268.9 27.9ZM268.3 44.7L264.2 45.9L264.5 45.4L270.7 43.4L275.5 44L268.3 44.7ZM282.6 47.3L283 47.7L280.9 49.4L276.3 51.7L277.1 50.8L277.1 51.1L278.9 50.1L279.5 48.8L282.6 47.3ZM309.4 82.6L309.9 85L308.7 86L306.9 85.6L306.6 83.7L306 85.6L304.1 87.3L303 89.7L301 90.7L301.8 87.6L301 87.3L298.5 88.7L298.7 87.9L300.6 86.6L301.9 83.8L300.7 82.6L299 82L299.6 81.6L301.9 81.3L301.3 80.1L301.9 79.7L302.1 80.4L309 81.7L309.4 82.6ZM297.9 81.3L299.6 81.6L296.3 85L296.5 84L295.2 84.7L292.8 87.3L292.1 88.6L292 90.6L291 92.6L288.6 94.6L287 95L286.9 93.1L288.1 90L291 86L293.3 83.6L290.3 85.6L290.1 85.3L294 82.2L297.9 81.3Z"
|
|
8
|
+
}
|
package/src/worldmap.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The world, and the geometry every map on this site shares.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from collabmap.js when a second map arrived (the conference map,
|
|
4
|
+
// #22): the projection is a published formula with a test asserting it matches
|
|
5
|
+
// the paper, and a second copy of it is a second thing that can drift out of
|
|
6
|
+
// agreement with the first.
|
|
7
|
+
//
|
|
8
|
+
// Inline SVG on a committed Equal Earth outline — no tiles, so no third party
|
|
9
|
+
// sees a visitor's viewport, and the whole thing works offline.
|
|
10
|
+
|
|
11
|
+
import world from './world-equal-earth.json';
|
|
12
|
+
|
|
13
|
+
export const map = {
|
|
14
|
+
width: world.width, height: world.height,
|
|
15
|
+
land: world.d, lakes: world.lakes,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const A1 = 1.340264, A2 = -0.081106, A3 = 0.000893, A4 = 0.003796;
|
|
19
|
+
const M = Math.sqrt(3) / 2;
|
|
20
|
+
|
|
21
|
+
/** Equal Earth (Šavrič, Patterson & Jenny 2018). Equal-area, so no country is
|
|
22
|
+
* drawn bigger than it is — which is the point of a map about where things are. */
|
|
23
|
+
export function project(lon, lat) {
|
|
24
|
+
const l = (lon * Math.PI) / 180;
|
|
25
|
+
const p = (lat * Math.PI) / 180;
|
|
26
|
+
const th = Math.asin(M * Math.sin(p));
|
|
27
|
+
const th2 = th * th, th6 = th2 * th2 * th2;
|
|
28
|
+
const x = (2 * Math.sqrt(3) * l * Math.cos(th))
|
|
29
|
+
/ (3 * (9 * A4 * th6 * th2 + 7 * A3 * th6 + 3 * A2 * th2 + A1));
|
|
30
|
+
const y = A4 * th6 * th2 * th + A3 * th6 * th + A2 * th2 * th + A1 * th;
|
|
31
|
+
return [x, y];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const [MAXX] = project(180, 0);
|
|
35
|
+
const [, MAXY] = project(0, 90);
|
|
36
|
+
const SCALE = world.width / (2 * MAXX);
|
|
37
|
+
|
|
38
|
+
/** lon/lat -> the same user units the committed world path is drawn in. */
|
|
39
|
+
export function toXY(lon, lat) {
|
|
40
|
+
const [x, y] = project(lon, lat);
|
|
41
|
+
return [(x + MAXX) * SCALE, (MAXY - y) * SCALE];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Kilometres per map unit: the map's height spans 180 degrees of latitude, and
|
|
45
|
+
* a degree of latitude is 111 km wherever you stand. */
|
|
46
|
+
export const KM_PER_UNIT = 111 / (map.height / 180);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How far a pin may be moved from the place it stands for.
|
|
50
|
+
*
|
|
51
|
+
* Ten miles — city level. Written as a distance rather than a number of units
|
|
52
|
+
* because the tolerance is a claim about the world, not about the drawing: at
|
|
53
|
+
* this scale one unit is about 41 km, so three units was seventy-six miles and
|
|
54
|
+
* put Case Western outside Cleveland.
|
|
55
|
+
*
|
|
56
|
+
* A pin is three units across, which is far wider than this, so stacked pins
|
|
57
|
+
* now overlap almost exactly. That is the honest picture — the picker below the
|
|
58
|
+
* map is what separates a crowd, not a lie about where things were.
|
|
59
|
+
*/
|
|
60
|
+
// Nine and a half, not ten: coordinates are written to two decimals in the SVG,
|
|
61
|
+
// which is a fifth of a mile of rounding on top of whatever the model decided.
|
|
62
|
+
// The promise is about the pin on the screen, so the rounding comes out of the
|
|
63
|
+
// budget rather than out of the promise.
|
|
64
|
+
export const MAX_DRIFT_MILES = 9.5;
|
|
65
|
+
export const MAX_PIN_DRIFT = (MAX_DRIFT_MILES * 1.609) / KM_PER_UNIT;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Separate pins that share a coordinate, and no more than that.
|
|
69
|
+
*
|
|
70
|
+
* An earlier version clustered by proximity and then relaxed everything apart
|
|
71
|
+
* until nothing touched. It produced a tidy map and a false one: pins drifted a
|
|
72
|
+
* median of ten units and a maximum of twenty-five, which is nine degrees of
|
|
73
|
+
* longitude — Columbia University came out somewhere in Ohio, and Case Western
|
|
74
|
+
* left Cleveland. Overlapping pins are honest; misplaced ones are not, and a
|
|
75
|
+
* map's first duty is to put things where they are.
|
|
76
|
+
*
|
|
77
|
+
* So only exact co-location is fixed, by fanning the stack into a small rosette
|
|
78
|
+
* inside MAX_PIN_DRIFT. Institutions that are genuinely near each other —
|
|
79
|
+
* Harvard and MIT, Princeton and the IAS — are drawn near each other, because
|
|
80
|
+
* they are. The picker is what separates a crowd now.
|
|
81
|
+
*/
|
|
82
|
+
export function spread(pins) {
|
|
83
|
+
const at = new Map();
|
|
84
|
+
for (const pin of pins) {
|
|
85
|
+
const key = `${pin.x.toFixed(2)},${pin.y.toFixed(2)}`;
|
|
86
|
+
if (!at.has(key)) at.set(key, []);
|
|
87
|
+
at.get(key).push(pin);
|
|
88
|
+
}
|
|
89
|
+
for (const group of at.values()) {
|
|
90
|
+
if (group.length === 1) continue;
|
|
91
|
+
// Every member the same distance out, so the stack reads as a rosette
|
|
92
|
+
// rather than one pin with satellites.
|
|
93
|
+
const r = Math.min(MAX_PIN_DRIFT, 1.2 + group.length * 0.35);
|
|
94
|
+
group.forEach((pin, i) => {
|
|
95
|
+
const a = (2 * Math.PI * i) / group.length - Math.PI / 2;
|
|
96
|
+
pin.x += r * Math.cos(a);
|
|
97
|
+
pin.y += r * Math.sin(a);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Hues spread by the golden angle, so neighbours in a list are far apart on
|
|
103
|
+
* the wheel however many entries there turn out to be. A number, not a colour:
|
|
104
|
+
* the stylesheet decides how light to make it, because a hue that reads on
|
|
105
|
+
* paper-white disappears on near-black. */
|
|
106
|
+
export const hueFor = (i) => Math.round((i * 137.508) % 360);
|