six-degrees 0.4.4 → 0.4.6
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.html +1 -1
- package/.next/standalone/.next/server/app/launch.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +17 -4
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +1 -0
- package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +1 -0
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
- package/.next/standalone/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
- package/.next/standalone/.next/static/chunks/38wwtni5e1ec1.js +31 -0
- package/.next/standalone/CHANGELOG.md +29 -0
- package/.next/standalone/app/components/FilterPanel.js +6 -0
- package/.next/standalone/app/components/ForceGraph.js +257 -39
- package/.next/standalone/app/components/GalaxyLab.js +275 -0
- package/.next/standalone/app/social/page.js +4 -16
- package/.next/standalone/lib/galaxy-export.js +152 -0
- package/.next/standalone/lib/galaxy-lab.js +296 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/static/chunks/0y1yms5kez2f3.js +1 -0
- package/.next/static/chunks/14m1zu-8warte.js +1 -0
- package/.next/static/chunks/1e1n0kkz-psdb.js +1 -0
- package/.next/static/chunks/2_zu7rmb8flzl.js +2 -0
- package/.next/static/chunks/38wwtni5e1ec1.js +31 -0
- package/CHANGELOG.md +29 -0
- package/lib/galaxy-export.js +152 -0
- package/lib/galaxy-lab.js +296 -0
- package/package.json +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01f38ty._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08_03wh._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1gpbbw9._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +0 -3
- package/.next/standalone/.next/static/chunks/0l1568qff0efj.js +0 -1
- package/.next/standalone/.next/static/chunks/1ra4cwcp75tfa.js +0 -1
- package/.next/standalone/.next/static/chunks/2hvcevue9aro4.js +0 -1
- package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
- package/.next/standalone/.next/static/chunks/2uo9i03_05rqi.js +0 -18
- package/.next/standalone/.next/static/chunks/3yc3rzp4dxvyo.js +0 -1
- package/.next/static/chunks/0l1568qff0efj.js +0 -1
- package/.next/static/chunks/1ra4cwcp75tfa.js +0 -1
- package/.next/static/chunks/2hvcevue9aro4.js +0 -1
- package/.next/static/chunks/2p5yt1o9tnxjn.js +0 -2
- package/.next/static/chunks/2uo9i03_05rqi.js +0 -18
- package/.next/static/chunks/3yc3rzp4dxvyo.js +0 -1
- /package/.next/standalone/.next/static/{VE8LeJXfD2H0_OgJxuJcm → -QUFjmCQWXW5PCoTA1G12}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{VE8LeJXfD2H0_OgJxuJcm → -QUFjmCQWXW5PCoTA1G12}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{VE8LeJXfD2H0_OgJxuJcm → -QUFjmCQWXW5PCoTA1G12}/_ssgManifest.js +0 -0
- /package/.next/static/{VE8LeJXfD2H0_OgJxuJcm → -QUFjmCQWXW5PCoTA1G12}/_buildManifest.js +0 -0
- /package/.next/static/{VE8LeJXfD2H0_OgJxuJcm → -QUFjmCQWXW5PCoTA1G12}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{VE8LeJXfD2H0_OgJxuJcm → -QUFjmCQWXW5PCoTA1G12}/_ssgManifest.js +0 -0
|
@@ -6,6 +6,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.6] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- **The window's frame matches the app.** The title bar, menus and dialogs are dark by default,
|
|
13
|
+
even when your Mac is set to Light, so the frame no longer shows as a pale bar above the dark
|
|
14
|
+
app.
|
|
15
|
+
|
|
16
|
+
## [0.4.5] - 2026-09-29
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Physics lab for the Galaxy (experimental, in Filters).** Sliders for the forces that lay the
|
|
20
|
+
Galaxy out, in the spirit of Obsidian's graph: *Gravity* (a pull in towards you), *Rings* (how
|
|
21
|
+
hard each tier holds its ring; at 0 the Galaxy finds its own shape), *Push*, *Pull* (how hard
|
|
22
|
+
each person pulls the people who came through them) and *Distance*, plus dot size, line
|
|
23
|
+
thickness, names, and dots sized by power score or by how many hang off them. *Clusters* sets
|
|
24
|
+
it up so each connection bunches their circle round them. Hovering a dot lights up its branch,
|
|
25
|
+
everyone behind it and the chain back to you. **Replay** plays your network growing by the
|
|
26
|
+
date you connected, with a time slider; each circle arrives with its connection. Everything
|
|
27
|
+
moves the Galaxy in place, and *Reset* puts today's layout back. The replay needs "connected
|
|
28
|
+
on" dates, so the sample network has nothing to replay.
|
|
29
|
+
- **More in the physics lab.** *Colour by* tier, degree, company (the eight most common) or
|
|
30
|
+
warmth (from the Social tab), with a legend to match. *Find* lights up everyone whose name,
|
|
31
|
+
company or role matches and dims the rest; Enter flies to the best one and opens their card.
|
|
32
|
+
The replay has a length (5 s to 1 min), *Loop*, your job starts and posts from the Social tab
|
|
33
|
+
marked on its timeline, and the job you were at in its date. *Saved layouts* keep slider
|
|
34
|
+
settings under a name. *Save a picture* makes a PNG of the Galaxy at twice the size, and
|
|
35
|
+
*Record the replay* saves it as a video.
|
|
36
|
+
- **Names on or off** for the Galaxy, in Filters.
|
|
37
|
+
|
|
9
38
|
## [0.4.4] - 2026-09-29
|
|
10
39
|
|
|
11
40
|
### Added
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { viewsForMode } from './views';
|
|
5
|
+
import GalaxyLab, { NamesSwitch } from './GalaxyLab';
|
|
5
6
|
|
|
6
7
|
function useIsMobile() {
|
|
7
8
|
const [m, setM] = useState(false);
|
|
@@ -144,6 +145,8 @@ export default function FilterPanel({ collapsed, onToggle, mode, filter, onFilte
|
|
|
144
145
|
</div>
|
|
145
146
|
)}
|
|
146
147
|
|
|
148
|
+
{!isDegreesMode && visualMode === 'galaxy' && <NamesSwitch />}
|
|
149
|
+
|
|
147
150
|
<div style={{ marginBottom: 18 }}>
|
|
148
151
|
{/* In Degrees these chips filter by the tier of the connection who
|
|
149
152
|
introduces you, not the person reached — Separation has its own
|
|
@@ -220,6 +223,9 @@ export default function FilterPanel({ collapsed, onToggle, mode, filter, onFilte
|
|
|
220
223
|
</div>
|
|
221
224
|
</div>
|
|
222
225
|
|
|
226
|
+
{/* The Galaxy's physics lab (experimental): sliders and a replay, on a computer. */}
|
|
227
|
+
{!isDegreesMode && visualMode === 'galaxy' && !isMobile && <GalaxyLab />}
|
|
228
|
+
|
|
223
229
|
{/* Stats */}
|
|
224
230
|
<div style={{ borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: 12 }}>
|
|
225
231
|
<div style={{ fontSize: 9, fontWeight: 700, color: '#555', letterSpacing: 1, marginBottom: 8, textTransform: 'uppercase' }}>
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
4
4
|
import * as d3 from 'd3';
|
|
5
5
|
import { localPhoto } from '../../lib/photos';
|
|
6
6
|
import { recentre } from '../../lib/galaxy';
|
|
7
7
|
import { reachIndex, readyByCircle, scanBars } from '../../lib/reach';
|
|
8
8
|
import { ringSegments, RING } from '../../lib/dot-rings';
|
|
9
|
+
import { LAB_DEFAULTS, labNow, watchLab, effectiveLab, clockNow, watchClock, setClock, stopReplay, bornTimes, reachCounts, colourScheme, findMatches, loadSocial, chapterAt } from '../../lib/galaxy-lab';
|
|
10
|
+
import { registerGalaxy } from '../../lib/galaxy-export';
|
|
9
11
|
|
|
10
12
|
// Connection fields are attacker-reachable: /api/ingest and /api/update-images
|
|
11
13
|
// accept writes, and a page on any other site can POST to this app on localhost.
|
|
@@ -55,6 +57,13 @@ const RING_CSS = `
|
|
|
55
57
|
50% { opacity: 1; transform: scale(1.12); }
|
|
56
58
|
}
|
|
57
59
|
.galaxy-ring-pulse { animation: galaxy-ring-pulse 1.8s ease-in-out infinite; }
|
|
60
|
+
.lab-focus .gn:not(.lit), .lab-focus .gl:not(.lit) { opacity: 0.06; }
|
|
61
|
+
.lab-focus .gl.lit { stroke-opacity: 0.7; }
|
|
62
|
+
.lab-focus .dot-rings, .lab-focus .catalyst-ring { opacity: 0.15; }
|
|
63
|
+
.lab-find .gn:not(.found), .lab-find .gl:not(.found) { opacity: 0.07; }
|
|
64
|
+
.lab-find .gn.found { stroke: #fff; stroke-width: 2px; }
|
|
65
|
+
@keyframes lab-pop { from { opacity: 0; } }
|
|
66
|
+
@media (prefers-reduced-motion: no-preference) { .lab-pop .gn, .lab-pop .gl { animation: lab-pop 0.5s ease-out; } }
|
|
58
67
|
@media (prefers-reduced-motion: reduce) {
|
|
59
68
|
.galaxy-ring-pulse { animation: none; opacity: 0.8; }
|
|
60
69
|
}`;
|
|
@@ -81,6 +90,36 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
|
|
|
81
90
|
const viewRef = useRef({ transform: null, size: null });
|
|
82
91
|
const sizeRef = useRef(null);
|
|
83
92
|
const [dimensions, setDimensions] = useState(null);
|
|
93
|
+
const stampRef = useRef(null);
|
|
94
|
+
|
|
95
|
+
// The physics lab (lib/galaxy-lab.js): a slider moves the layout in place,
|
|
96
|
+
// and the replay's clock hides whoever wasn't there yet. Neither rebuilds.
|
|
97
|
+
const lab = useSyncExternalStore(watchLab, labNow, () => LAB_DEFAULTS);
|
|
98
|
+
const labRef = useRef(lab);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
labRef.current = lab;
|
|
101
|
+
sceneRef.current?.setLab(effectiveLab(lab));
|
|
102
|
+
const c = clockNow();
|
|
103
|
+
sceneRef.current?.setFind(c.find, c.fly);
|
|
104
|
+
if (lab.on) loadSocial();
|
|
105
|
+
}, [lab]);
|
|
106
|
+
useEffect(() => watchClock(() => {
|
|
107
|
+
const c = clockNow();
|
|
108
|
+
sceneRef.current?.setTime(c.at);
|
|
109
|
+
sceneRef.current?.setFind(c.find, c.fly);
|
|
110
|
+
}), []);
|
|
111
|
+
useEffect(() => () => stopReplay(), []);
|
|
112
|
+
|
|
113
|
+
// The dots' colours (Colour by, in the lab): tier as always, or degree,
|
|
114
|
+
// company or warmth. A new scheme recolours in place.
|
|
115
|
+
const social = useSyncExternalStore(watchClock, () => clockNow().social, () => undefined);
|
|
116
|
+
const colourBy = effectiveLab(lab).colourBy;
|
|
117
|
+
const scheme = useMemo(() => colourScheme(colourBy, connections, tierColors, social), [colourBy, connections, tierColors, social]);
|
|
118
|
+
const schemeRef = useRef(scheme);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
schemeRef.current = scheme;
|
|
121
|
+
sceneRef.current?.setColours(scheme);
|
|
122
|
+
}, [scheme]);
|
|
84
123
|
|
|
85
124
|
// The scene is rebuilt from scratch whenever its inputs change, so only real
|
|
86
125
|
// changes should count. A parent re-rendering hands over a new onSelect every
|
|
@@ -139,9 +178,18 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
|
|
|
139
178
|
const saved = transform ? recentre(transform, setFor, size) : null;
|
|
140
179
|
|
|
141
180
|
const select = (d) => onSelectRef.current?.(d);
|
|
142
|
-
const scene = renderNetworkMode(svg, ringRef.current, size, connections, select, tierColors, focusNodeRef, saved, viewRef, userName, marks);
|
|
181
|
+
const scene = renderNetworkMode(svg, ringRef.current, size, connections, select, tierColors, focusNodeRef, saved, viewRef, userName, marks, effectiveLab(labRef.current), stampRef.current, schemeRef.current);
|
|
143
182
|
sceneRef.current = scene;
|
|
144
183
|
scene.select(selectedIdRef.current);
|
|
184
|
+
setClock({ min: scene.range.min, max: scene.range.max, of: scene.range.of });
|
|
185
|
+
scene.setTime(clockNow().at);
|
|
186
|
+
scene.setFind(clockNow().find, clockNow().fly);
|
|
187
|
+
// Save picture and Record replay (lib/galaxy-export.js) draw from this <svg>.
|
|
188
|
+
const unregister = registerGalaxy({
|
|
189
|
+
svg: svgRef.current,
|
|
190
|
+
stamp: () => (stampRef.current?.style.display === 'block' ? stampRef.current.textContent : null),
|
|
191
|
+
legend: () => schemeRef.current.legend,
|
|
192
|
+
});
|
|
145
193
|
|
|
146
194
|
return () => {
|
|
147
195
|
// Stop the force simulation this render started.
|
|
@@ -151,6 +199,7 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
|
|
|
151
199
|
// the same nodes, so they fought each other and the graph shook.
|
|
152
200
|
// Refreshing made it worse because nothing ever stopped the old ones.
|
|
153
201
|
scene.simulation.stop();
|
|
202
|
+
unregister();
|
|
154
203
|
// And any glide or fly-to still under way, which would otherwise go on
|
|
155
204
|
// moving the next scene's view. viewRef already holds where it was going.
|
|
156
205
|
svg.interrupt();
|
|
@@ -174,11 +223,17 @@ export default function ForceGraph({ connections, onSelect, tierColors, focusNod
|
|
|
174
223
|
}}>
|
|
175
224
|
<div className="galaxy-ring-pulse" style={{ boxSizing: 'border-box', border: '2.5px solid', borderRadius: '50%' }} />
|
|
176
225
|
</div>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
226
|
+
{/* The replay's date, written by the scene as it plays. */}
|
|
227
|
+
<div ref={stampRef} style={{
|
|
228
|
+
position: 'absolute', bottom: 56, left: '50%', transform: 'translateX(-50%)', display: 'none',
|
|
229
|
+
padding: '6px 14px', borderRadius: 16, background: 'rgba(10,15,30,0.8)', border: '1px solid rgba(255,255,255,0.12)',
|
|
230
|
+
color: '#e6edf5', fontSize: 13, fontWeight: 600, pointerEvents: 'none', fontVariantNumeric: 'tabular-nums',
|
|
231
|
+
}} />
|
|
232
|
+
<div style={{ position: 'absolute', bottom: 20, left: 20, right: 20, display: 'flex', flexWrap: 'wrap', gap: '4px 12px', fontSize: 11, color: '#888', pointerEvents: 'none' }}>
|
|
233
|
+
{scheme.legend.map(([label, color]) => (
|
|
234
|
+
<span key={label} style={{ display: 'flex', alignItems: 'center', gap: 4, whiteSpace: 'nowrap' }}>
|
|
180
235
|
<span style={{ width: 8, height: 8, borderRadius: '50%', background: color, display: 'inline-block' }} />
|
|
181
|
-
{
|
|
236
|
+
{label}
|
|
182
237
|
</span>
|
|
183
238
|
))}
|
|
184
239
|
</div>
|
|
@@ -216,7 +271,8 @@ function easeRadius(el, r) {
|
|
|
216
271
|
// Graph coordinates put you at 0,0, whatever the size of the box; the zoom
|
|
217
272
|
// transform places that in the box. So a new size only moves the view, and a
|
|
218
273
|
// rebuild can start from the view as it was.
|
|
219
|
-
function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, focusNodeRef, savedTransform, viewRef, userName, marks = new Map()) {
|
|
274
|
+
function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, focusNodeRef, savedTransform, viewRef, userName, marks = new Map(), lab = LAB_DEFAULTS, stamp = null, scheme = null) {
|
|
275
|
+
let colourOf = scheme?.of ?? ((d) => tierColors[d.tier] || '#666');
|
|
220
276
|
const centerNode = {
|
|
221
277
|
id: CENTER_ID, name: userName || 'You', tier: 'center', degree: 0,
|
|
222
278
|
power_score: 10, fx: 0, fy: 0,
|
|
@@ -285,16 +341,24 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
285
341
|
return SLOTS[slot][t < 0 ? 4 : t];
|
|
286
342
|
};
|
|
287
343
|
|
|
344
|
+
// A phone. Judged by the window, as the page judges it, not by the graph's
|
|
345
|
+
// own box: that narrowed whenever a panel opened on a laptop.
|
|
346
|
+
const isMobileGraph = window.innerWidth < 768;
|
|
347
|
+
|
|
348
|
+
// The physics lab's settings (lib/galaxy-lab.js); today's layout when it's
|
|
349
|
+
// off, and always on a phone, whose layout has no physics.
|
|
350
|
+
let L = isMobileGraph ? { ...LAB_DEFAULTS, labels: lab.labels } : lab;
|
|
351
|
+
const reach = reachCounts(nodes, parentOf);
|
|
352
|
+
|
|
288
353
|
const nodeRadius = (d) => {
|
|
289
354
|
if (d.id === CENTER_ID) return 18;
|
|
355
|
+
// Sized by reach, a dot grows with everyone who hangs off it: a 2nd-degree
|
|
356
|
+
// person with a full circle behind them next to one with nobody.
|
|
357
|
+
if (L.sizeBy === 'reach') return Math.min(28, 2.5 + 1.6 * Math.sqrt(reach.get(d.id) || 0)) * L.dotSize;
|
|
290
358
|
const r = Math.max(3, Math.min(12, (d.power_score || 1) * 1.3));
|
|
291
|
-
return d.degree > 1 ? Math.max(2.5, r * 0.75) : r; // further out, a little smaller
|
|
359
|
+
return (d.degree > 1 ? Math.max(2.5, r * 0.75) : r) * L.dotSize; // further out, a little smaller
|
|
292
360
|
};
|
|
293
361
|
|
|
294
|
-
// A phone. Judged by the window, as the page judges it, not by the graph's
|
|
295
|
-
// own box: that narrowed whenever a panel opened on a laptop.
|
|
296
|
-
const isMobileGraph = window.innerWidth < 768;
|
|
297
|
-
|
|
298
362
|
const g = svg.append('g');
|
|
299
363
|
|
|
300
364
|
// Faint S–D guide rings where each tier settles (the radial force below), so
|
|
@@ -302,9 +366,10 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
302
366
|
// Once the layout settles they move to where each tier's dots actually sit
|
|
303
367
|
// (the median distance out), which the other forces push past that radius.
|
|
304
368
|
const guideRings = new Map();
|
|
369
|
+
let guidesG = null;
|
|
305
370
|
if (!isMobileGraph && layered) {
|
|
306
371
|
// In bands: a faint ring and a label for each step along the chain instead.
|
|
307
|
-
const guides = g.append('g').attr('class', 'degree-guides').style('pointer-events', 'none');
|
|
372
|
+
const guides = guidesG = g.append('g').attr('class', 'degree-guides').style('pointer-events', 'none');
|
|
308
373
|
const names = ['', '1st', '2nd', '3rd', '4th', '5th', '6th'];
|
|
309
374
|
bands.slice(0, SLOTS.length).forEach((b, slot) => {
|
|
310
375
|
const r = SLOTS[slot][2];
|
|
@@ -315,7 +380,7 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
315
380
|
.text(names[b] || `${b}th`);
|
|
316
381
|
});
|
|
317
382
|
} else if (!isMobileGraph) {
|
|
318
|
-
const guides = g.append('g').attr('class', 'tier-guides').style('pointer-events', 'none');
|
|
383
|
+
const guides = guidesG = g.append('g').attr('class', 'tier-guides').style('pointer-events', 'none');
|
|
319
384
|
for (const tier of ['S', 'A', 'B', 'C', 'D']) {
|
|
320
385
|
const r = tierRadius(tier);
|
|
321
386
|
const ring = guides.append('circle').attr('r', r).attr('fill', 'none')
|
|
@@ -371,7 +436,7 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
371
436
|
const r = nodeRadius(ringNode) + 8;
|
|
372
437
|
pulse.style.width = pulse.style.height = `${2 * r + 2.5}px`;
|
|
373
438
|
pulse.style.margin = `${-(r + 1.25)}px`;
|
|
374
|
-
pulse.style.borderColor =
|
|
439
|
+
pulse.style.borderColor = colourOf(ringNode) || '#FFD700';
|
|
375
440
|
placeRing();
|
|
376
441
|
};
|
|
377
442
|
|
|
@@ -421,12 +486,20 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
421
486
|
});
|
|
422
487
|
}
|
|
423
488
|
|
|
489
|
+
// Each force is today's value times its slider in the lab (1 when it's off).
|
|
490
|
+
const linkBase = isMobileGraph ? 0 : layered ? 0.06 : 0.1;
|
|
491
|
+
const radialBase = layered ? 0.45 : 0.3;
|
|
492
|
+
const linkDistance = d => (d.near ? Math.max(40, radiusOf(d.target) - radiusOf(d.source)) : radiusOf(d.target)) * L.distance;
|
|
493
|
+
const charge = d => (d.id === CENTER_ID ? -20 : -1) * L.push;
|
|
424
494
|
const simulation = d3.forceSimulation(nodes)
|
|
425
|
-
.force('link', d3.forceLink(links).id(d => d.id).distance(
|
|
426
|
-
.force('charge', d3.forceManyBody().strength(isMobileGraph ? 0 :
|
|
495
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(linkDistance).strength(Math.min(1, linkBase * L.pull)))
|
|
496
|
+
.force('charge', d3.forceManyBody().strength(isMobileGraph ? 0 : charge))
|
|
427
497
|
.force('center', isMobileGraph ? null : d3.forceCenter(0, 0))
|
|
498
|
+
.force('gravityX', isMobileGraph ? null : d3.forceX(0).strength(0.15 * L.gravity))
|
|
499
|
+
.force('gravityY', isMobileGraph ? null : d3.forceY(0).strength(0.15 * L.gravity))
|
|
428
500
|
.force('collision', isMobileGraph ? null : d3.forceCollide().radius(d => nodeRadius(d) + 2))
|
|
429
|
-
.force('radial', isMobileGraph ? null : d3.forceRadial(d => d.id === CENTER_ID ? 0 : radiusOf(d), 0, 0).strength(
|
|
501
|
+
.force('radial', isMobileGraph ? null : d3.forceRadial(d => d.id === CENTER_ID ? 0 : radiusOf(d), 0, 0).strength(radialBase * L.rings));
|
|
502
|
+
guidesG?.attr('opacity', Math.min(1, L.rings));
|
|
430
503
|
simulation.on('end.guides', () => {
|
|
431
504
|
for (const [tier, { ring, label }] of guideRings) {
|
|
432
505
|
const out = nodes.filter((n) => n.tier === tier && n.id !== CENTER_ID).map((n) => Math.hypot(n.x || 0, n.y || 0)).sort((x, y) => x - y);
|
|
@@ -441,8 +514,9 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
441
514
|
// the colour is one step away. It used to search every node for every link:
|
|
442
515
|
// about 1.8 s at 30,000 people.
|
|
443
516
|
const link = g.append('g').selectAll('line').data(links).join('line')
|
|
444
|
-
.attr('
|
|
445
|
-
.attr('stroke
|
|
517
|
+
.attr('class', 'gl')
|
|
518
|
+
.attr('stroke', d => colourOf(d.target) || '#333')
|
|
519
|
+
.attr('stroke-opacity', 0.15).attr('stroke-width', 0.5 * L.lines);
|
|
446
520
|
|
|
447
521
|
// Catalyst outer glow rings (rendered behind the nodes)
|
|
448
522
|
// A catalyst is the green outline on their dot (design C, with the scan
|
|
@@ -471,10 +545,11 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
471
545
|
const dotRings = g.append('g').attr('class', 'dot-rings').style('pointer-events', 'none')
|
|
472
546
|
.selectAll('g').data(ringed).join('g')
|
|
473
547
|
.attr('transform', d => `translate(${d.x || 0},${d.y || 0})`);
|
|
474
|
-
|
|
548
|
+
function drawDotRing(d) {
|
|
475
549
|
const { bars, ready } = marks.get(d.id);
|
|
476
550
|
const r = nodeRadius(d) + 3.5;
|
|
477
551
|
const el = d3.select(this);
|
|
552
|
+
el.selectAll('*').remove();
|
|
478
553
|
if (bars != null) {
|
|
479
554
|
for (const seg of ringSegments(r)) {
|
|
480
555
|
el.append('path').attr('d', seg.d).attr('fill', 'none').attr('stroke-linecap', 'round')
|
|
@@ -487,11 +562,13 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
487
562
|
el.append('text').attr('x', at).attr('y', at + 2.5).attr('text-anchor', 'middle')
|
|
488
563
|
.attr('font-size', 7).attr('font-weight', 700).attr('fill', '#fff').text(ready > 9 ? '9+' : ready);
|
|
489
564
|
}
|
|
490
|
-
}
|
|
565
|
+
}
|
|
566
|
+
dotRings.each(drawDotRing);
|
|
491
567
|
|
|
492
568
|
const node = g.append('g').selectAll('circle').data(nodes).join('circle')
|
|
569
|
+
.attr('class', 'gn')
|
|
493
570
|
.attr('r', nodeRadius)
|
|
494
|
-
.attr('fill', d => d.id === CENTER_ID ? '#fff' :
|
|
571
|
+
.attr('fill', d => d.id === CENTER_ID ? '#fff' : colourOf(d))
|
|
495
572
|
.attr('fill-opacity', d => (d.degree >= 3 ? 0.55 : d.degree === 2 ? 0.75 : 1))
|
|
496
573
|
.attr('stroke', d => {
|
|
497
574
|
if (d.id === CENTER_ID) return '#FFD700';
|
|
@@ -518,8 +595,26 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
518
595
|
.style('pointer-events', 'none').style('opacity', 0)
|
|
519
596
|
.style('z-index', 1001).style('transition', 'opacity 0.15s');
|
|
520
597
|
|
|
598
|
+
// Their branch: everyone who hangs off them, all the way down, and the chain
|
|
599
|
+
// back to you. Hovering lights it up in the lab, like Obsidian's graph.
|
|
600
|
+
const kids = new Map();
|
|
601
|
+
for (const [child, parent] of parentOf) {
|
|
602
|
+
if (!kids.has(parent)) kids.set(parent, []);
|
|
603
|
+
kids.get(parent).push(child);
|
|
604
|
+
}
|
|
605
|
+
const lightBranch = (d) => {
|
|
606
|
+
const lit = new Set([CENTER_ID, d.id]);
|
|
607
|
+
const stack = [d.id];
|
|
608
|
+
while (stack.length) for (const k of kids.get(stack.pop()) || []) if (!lit.has(k)) { lit.add(k); stack.push(k); }
|
|
609
|
+
for (let p = parentOf.get(d.id), hops = 0; p != null && hops < 7; p = parentOf.get(p), hops++) lit.add(p);
|
|
610
|
+
node.classed('lit', n => lit.has(n.id));
|
|
611
|
+
link.classed('lit', l => lit.has(l.source.id) && lit.has(l.target.id));
|
|
612
|
+
g.classed('lab-focus', true);
|
|
613
|
+
};
|
|
614
|
+
|
|
521
615
|
node.on('mouseover', function (event, d) {
|
|
522
616
|
easeRadius(this, nodeRadius(d) * 1.5);
|
|
617
|
+
if (L.on && L.branch && d.id !== CENTER_ID) lightBranch(d);
|
|
523
618
|
if (d.profile_image_url && d.id !== CENTER_ID) {
|
|
524
619
|
const size = Math.max(48, nodeRadius(d) * 5);
|
|
525
620
|
photoTooltip.style('opacity', 1);
|
|
@@ -547,27 +642,41 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
547
642
|
}
|
|
548
643
|
}).on('mouseout', function (event, d) {
|
|
549
644
|
easeRadius(this, nodeRadius(d));
|
|
645
|
+
g.classed('lab-focus', false);
|
|
550
646
|
tooltip.style('opacity', 0);
|
|
551
647
|
photoTooltip.style('opacity', 0);
|
|
552
648
|
});
|
|
553
649
|
|
|
554
650
|
setupDrag(node, simulation, CENTER_ID);
|
|
555
651
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
.
|
|
652
|
+
// Names for you, your S-tier connections and catalysts; further out (the
|
|
653
|
+
// Degree filter) only on hover, or hundreds of S-tier names pile up. The lab
|
|
654
|
+
// can show all your connections' names, or none.
|
|
655
|
+
// Filter → Names turns them all off; whoever Find matches (40 or fewer) is
|
|
656
|
+
// named either way.
|
|
657
|
+
let findHit = null;
|
|
658
|
+
const labelled = (n) => {
|
|
659
|
+
if (findHit && findHit.size <= 40 && findHit.has(n.id)) return true;
|
|
660
|
+
if (!L.labels) return false;
|
|
661
|
+
return n.id === CENTER_ID || (n.degree === 1 && (L.names === 'all' || n.tier === 'S' || n.is_catalyst));
|
|
662
|
+
};
|
|
663
|
+
const labelsG = g.append('g');
|
|
664
|
+
let labels = labelsG.selectAll('text');
|
|
665
|
+
const drawLabels = () => {
|
|
666
|
+
labels = labelsG.selectAll('text').data(nodes.filter(labelled), d => d.id).join('text')
|
|
667
|
+
.text(d => d.is_catalyst && d.tier !== 'S' ? d.name + ' ⚡' : d.name)
|
|
668
|
+
.attr('font-size', d => d.id === CENTER_ID ? 14 : 10)
|
|
669
|
+
.attr('font-weight', d => d.id === CENTER_ID ? 700 : 500)
|
|
670
|
+
.attr('fill', d => {
|
|
671
|
+
if (d.id === CENTER_ID) return '#fff';
|
|
672
|
+
if (d.is_catalyst) return '#00ff88';
|
|
673
|
+
return colourOf(d);
|
|
674
|
+
})
|
|
675
|
+
.attr('text-anchor', 'middle').attr('dy', d => nodeRadius(d) + 14)
|
|
676
|
+
.attr('x', d => d.x || 0).attr('y', d => d.y || 0)
|
|
677
|
+
.style('pointer-events', 'none');
|
|
678
|
+
};
|
|
679
|
+
drawLabels();
|
|
571
680
|
|
|
572
681
|
const catalystRings = g.selectAll('.catalyst-ring');
|
|
573
682
|
|
|
@@ -580,7 +689,116 @@ function renderNetworkMode(svg, ring, box, connections, onSelect, tierColors, fo
|
|
|
580
689
|
placeRing();
|
|
581
690
|
});
|
|
582
691
|
|
|
583
|
-
|
|
692
|
+
// A slider moved: change the running layout in place and let it settle again.
|
|
693
|
+
const setLab = (next) => {
|
|
694
|
+
if (isMobileGraph) {
|
|
695
|
+
if (next.labels !== L.labels) { L = { ...L, labels: next.labels }; drawLabels(); showBorn(); }
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const prev = L;
|
|
699
|
+
L = next;
|
|
700
|
+
const sized = prev.dotSize !== L.dotSize || prev.sizeBy !== L.sizeBy;
|
|
701
|
+
if (sized) {
|
|
702
|
+
node.attr('r', nodeRadius);
|
|
703
|
+
catalystRings.attr('r', d => nodeRadius(d) + 6);
|
|
704
|
+
dotRings.each(drawDotRing);
|
|
705
|
+
simulation.force('collision').radius(d => nodeRadius(d) + 2);
|
|
706
|
+
if (ringNode) select(ringNode.id);
|
|
707
|
+
}
|
|
708
|
+
if (prev.lines !== L.lines) link.attr('stroke-width', 0.5 * L.lines);
|
|
709
|
+
if (sized || prev.names !== L.names || prev.labels !== L.labels) { drawLabels(); showBorn(); }
|
|
710
|
+
if (!L.on || !L.branch) g.classed('lab-focus', false);
|
|
711
|
+
g.classed('lab-pop', L.on && nodes.length < 5000);
|
|
712
|
+
const forces = ['gravity', 'rings', 'push', 'pull', 'distance'].some(k => prev[k] !== L[k]);
|
|
713
|
+
if (!forces && !sized) return;
|
|
714
|
+
simulation.force('link').distance(linkDistance).strength(Math.min(1, linkBase * L.pull));
|
|
715
|
+
simulation.force('charge').strength(charge);
|
|
716
|
+
simulation.force('gravityX').strength(0.15 * L.gravity);
|
|
717
|
+
simulation.force('gravityY').strength(0.15 * L.gravity);
|
|
718
|
+
simulation.force('radial').strength(radialBase * L.rings);
|
|
719
|
+
guidesG?.attr('opacity', Math.min(1, L.rings));
|
|
720
|
+
simulation.alpha(Math.max(simulation.alpha(), 0.5)).restart();
|
|
721
|
+
};
|
|
722
|
+
g.classed('lab-pop', L.on && nodes.length < 5000);
|
|
723
|
+
|
|
724
|
+
// The replay: whoever wasn't there yet at the clock's time is hidden. Only the
|
|
725
|
+
// dots that change are touched, so a frame costs little at 30,000 people.
|
|
726
|
+
const born = bornTimes(nodes, parentOf);
|
|
727
|
+
const bornById = new Map(nodes.map((n, i) => [n.id, born[i]]));
|
|
728
|
+
const nodeEls = node.nodes();
|
|
729
|
+
const linkEls = link.nodes(); // links[i] is connections[i], which is nodes[i + 1]
|
|
730
|
+
const extraEls = new Map();
|
|
731
|
+
dotRings.each(function (d) { extraEls.set(d.id, [this]); });
|
|
732
|
+
catalystRings.each(function (d) { extraEls.set(d.id, [...(extraEls.get(d.id) || []), this]); });
|
|
733
|
+
const shown = new Uint8Array(nodes.length).fill(1);
|
|
734
|
+
const d1 = nodes.filter(n => n.degree === 1);
|
|
735
|
+
const dated = d1.map(n => bornById.get(n.id)).filter(Number.isFinite);
|
|
736
|
+
const range = { min: dated.length ? dated.reduce((m, t) => Math.min(m, t)) : null, max: dated.length ? dated.reduce((m, t) => Math.max(m, t)) : null, of: d1.length };
|
|
737
|
+
let clockAt = null;
|
|
738
|
+
const showBorn = () => labels.style('display', d => (clockAt == null || bornById.get(d.id) <= clockAt ? null : 'none'));
|
|
739
|
+
const setTime = (at) => {
|
|
740
|
+
if (!L.on) at = null;
|
|
741
|
+
clockAt = at;
|
|
742
|
+
let n1 = 0;
|
|
743
|
+
for (let i = 1; i < nodes.length; i++) {
|
|
744
|
+
const vis = at == null || born[i] <= at ? 1 : 0;
|
|
745
|
+
if (vis && nodes[i].degree === 1) n1++;
|
|
746
|
+
if (vis === shown[i]) continue;
|
|
747
|
+
shown[i] = vis;
|
|
748
|
+
const display = vis ? '' : 'none';
|
|
749
|
+
nodeEls[i].style.display = display;
|
|
750
|
+
if (linkEls[i - 1]) linkEls[i - 1].style.display = display;
|
|
751
|
+
for (const el of extraEls.get(nodes[i].id) || []) el.style.display = display;
|
|
752
|
+
}
|
|
753
|
+
showBorn();
|
|
754
|
+
if (!stamp) return;
|
|
755
|
+
stamp.style.display = at == null ? 'none' : 'block';
|
|
756
|
+
if (at != null) {
|
|
757
|
+
const when = new Date(at).toLocaleDateString(undefined, { month: 'short', year: 'numeric' });
|
|
758
|
+
const job = chapterAt(clockNow().social, at);
|
|
759
|
+
stamp.textContent = `${when} · ${n1.toLocaleString()} of ${range.of.toLocaleString()} connections${job ? ` · at ${job.company}` : ''}`;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
// A new colour scheme (Colour by): recolour the dots, lines and names in place.
|
|
764
|
+
const setColours = (next) => {
|
|
765
|
+
colourOf = next.of;
|
|
766
|
+
node.attr('fill', d => d.id === CENTER_ID ? '#fff' : colourOf(d));
|
|
767
|
+
link.attr('stroke', d => colourOf(d.target) || '#333');
|
|
768
|
+
drawLabels();
|
|
769
|
+
showBorn();
|
|
770
|
+
if (ringNode) select(ringNode.id);
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
// Find: whoever matches stays lit and the rest dim; each Enter (a new `fly`)
|
|
774
|
+
// flies to the best-scored match and opens their card.
|
|
775
|
+
let lastFind = '';
|
|
776
|
+
let lastFly = clockNow().fly;
|
|
777
|
+
const setFind = (query, fly) => {
|
|
778
|
+
const q = L.on ? String(query || '') : '';
|
|
779
|
+
if (q !== lastFind) {
|
|
780
|
+
lastFind = q;
|
|
781
|
+
findHit = findMatches(nodes.filter(n => n.id !== CENTER_ID), q);
|
|
782
|
+
node.classed('found', n => !!findHit?.has(n.id));
|
|
783
|
+
link.classed('found', l => !!findHit?.has(l.target.id));
|
|
784
|
+
g.classed('lab-find', !!findHit);
|
|
785
|
+
drawLabels();
|
|
786
|
+
showBorn();
|
|
787
|
+
setClock({ found: findHit ? findHit.size : 0 });
|
|
788
|
+
}
|
|
789
|
+
if (fly !== lastFly) {
|
|
790
|
+
lastFly = fly;
|
|
791
|
+
const best = findHit && nodes.filter(n => findHit.has(n.id)).sort((a, b) => (b.power_score || 0) - (a.power_score || 0))[0];
|
|
792
|
+
if (best) {
|
|
793
|
+
const k = 2.5;
|
|
794
|
+
moveView(d3.zoomIdentity.translate(box.width / 2 - best.x * k, box.height / 2 - best.y * k).scale(k), 750);
|
|
795
|
+
select(best.id);
|
|
796
|
+
onSelect(best);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
return { simulation, resize, select, setLab, setTime, setColours, setFind, range };
|
|
584
802
|
}
|
|
585
803
|
|
|
586
804
|
function setupDrag(node, simulation, fixedId) {
|