six-degrees 0.4.11 → 0.5.0
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 +9 -9
- 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/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
- 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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js +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 +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 +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 +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/crm/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +3 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +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 +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 +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/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +14 -14
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +14 -14
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- 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 +14 -14
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +14 -14
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +14 -14
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +14 -14
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +14 -14
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +14 -14
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- 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 +14 -14
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
- 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 +13 -13
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__13dx4sd._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15wc9e-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20rwfdw._.js +8 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_0851sk0._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1q7p2ro._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_1r6l86l._.js +8 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +8 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_04s8mzr.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_053z7as.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0b_-2te.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dhbxdm.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0dxlk31.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0f8b-qc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0pzie7_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r76fq_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ut7pjp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_15zyqaz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_18zpjtq.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1a8xkiz.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1e_55xo.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1oxfd1_.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1s0lod0.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01kacen._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_03l7vge._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1ap1tu_._.js → _0gtv9jb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0jdkqgi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xi-fbs._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0xmwmfv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-b9hj7._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/_112ewem._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_16ab63q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fxm0-s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1ra1d3r._.js +9 -0
- package/.next/standalone/.next/server/chunks/ssr/app_141w189._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +22 -10
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +6 -5
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +19 -3
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +1 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_13rapwj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +1 -1
- 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/03bziijw5srol.js +1 -0
- package/.next/standalone/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/standalone/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/standalone/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/standalone/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/standalone/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/standalone/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/standalone/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/standalone/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/standalone/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/standalone/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/standalone/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/standalone/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/standalone/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/standalone/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/standalone/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/.next/standalone/CHANGELOG.md +187 -0
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +7 -1
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/components/AppTabs.js +59 -0
- package/.next/standalone/app/components/ChainView.js +303 -89
- package/.next/standalone/app/components/CirclePanel.js +162 -0
- package/.next/standalone/app/components/CompanyScores.js +2 -2
- package/.next/standalone/app/components/FieldStep.js +4 -4
- package/.next/standalone/app/components/FilterPanel.js +121 -183
- package/.next/standalone/app/components/ForceGraph.js +41 -3
- package/.next/standalone/app/components/GalaxyLab.js +26 -31
- package/.next/standalone/app/components/LegacyScoresCard.js +2 -2
- package/.next/standalone/app/components/MapControls.js +154 -0
- package/.next/standalone/app/components/NoteDetail.js +118 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +147 -110
- package/.next/standalone/app/components/PeopleMap.js +308 -0
- package/.next/standalone/app/components/ScanRadar.js +129 -0
- package/.next/standalone/app/components/ScanStatusBar.js +71 -27
- package/.next/standalone/app/components/SeparationView.js +626 -118
- package/.next/standalone/app/components/Sidebar.js +28 -2
- package/.next/standalone/app/components/settings/NetworkHealthSection.js +1 -1
- package/.next/standalone/app/components/settings/SectorSection.js +2 -2
- package/.next/standalone/app/components/settings/TierSection.js +1 -1
- package/.next/standalone/app/components/settings/TitleSection.js +149 -0
- package/.next/standalone/app/components/views.js +15 -14
- package/.next/standalone/app/page.js +89 -156
- package/.next/standalone/app/paths/page.js +39 -27
- package/.next/standalone/app/profile/page.js +33 -5
- package/.next/standalone/app/queue/page.js +56 -34
- package/.next/standalone/app/scores/page.js +16 -66
- package/.next/standalone/app/settings/page.js +27 -13
- package/.next/standalone/app/setup/page.js +298 -205
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/page.js +10 -362
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/degrees.js +0 -6
- package/.next/standalone/lib/dot-rings.js +25 -0
- package/.next/standalone/lib/galaxy-lab.js +65 -6
- package/.next/standalone/lib/ingest.js +2 -0
- package/.next/standalone/lib/island.js +40 -0
- package/.next/standalone/lib/linkedin-limits.js +6 -2
- package/.next/standalone/lib/map-heat.js +20 -0
- package/.next/standalone/lib/network-health.js +1 -1
- package/.next/standalone/lib/notifications.js +121 -0
- package/.next/standalone/lib/people-map.js +82 -0
- package/.next/standalone/lib/rarity.js +45 -3
- package/.next/standalone/lib/reach.js +7 -6
- package/.next/standalone/lib/rpc.js +15 -4
- package/.next/standalone/lib/scan-pace.js +46 -0
- package/.next/standalone/lib/scoring.js +39 -8
- package/.next/standalone/lib/scraper-client.js +19 -6
- package/.next/standalone/lib/sector-focus.js +4 -4
- package/.next/standalone/lib/separation.js +137 -10
- package/.next/standalone/lib/settings-effects.js +20 -11
- package/.next/standalone/lib/settings.js +3 -0
- package/.next/standalone/lib/tier-grid.js +99 -0
- package/.next/standalone/lib/title-ranking.js +83 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +51 -13
- package/.next/static/chunks/03bziijw5srol.js +1 -0
- package/.next/static/chunks/0bgq8wmopl_ig.js +7 -0
- package/.next/static/chunks/0ezus6p2eb-vg.js +1 -0
- package/.next/static/chunks/0g_fy2034bp0x.js +34 -0
- package/.next/static/chunks/0q5dru4q6gxl5.js +22 -0
- package/.next/static/chunks/0w-byiwb2zax8.js +2 -0
- package/.next/static/chunks/1g7jcovdejc9-.js +1 -0
- package/.next/static/chunks/1i51yylxdzht1.js +1 -0
- package/.next/static/chunks/1zf8swaasjf1e.js +43 -0
- package/.next/static/chunks/2rqtpccruavf2.js +1 -0
- package/.next/static/chunks/2wgz9ietkqqe6.js +1 -0
- package/.next/static/chunks/30x3oq_st7a5r.js +1 -0
- package/.next/static/chunks/355ryio1b7sm1.js +1 -0
- package/.next/static/chunks/3ap5hgma7n0o7.js +1 -0
- package/.next/static/chunks/3fci6qi39-1n5.js +1 -0
- package/.next/static/chunks/{24tvdun1cb3g1.js → 3sowbvbaj1s6n.js} +1 -1
- package/CHANGELOG.md +187 -0
- package/lib/chain-layout.js +105 -35
- package/lib/degrees.js +0 -6
- package/lib/dot-rings.js +25 -0
- package/lib/galaxy-lab.js +65 -6
- package/lib/ingest.js +2 -0
- package/lib/island.js +40 -0
- package/lib/linkedin-limits.js +6 -2
- package/lib/map-heat.js +20 -0
- package/lib/network-health.js +1 -1
- package/lib/notifications.js +121 -0
- package/lib/people-map.js +82 -0
- package/lib/rarity.js +45 -3
- package/lib/reach.js +7 -6
- package/lib/rpc.js +15 -4
- package/lib/scan-pace.js +46 -0
- package/lib/scoring.js +39 -8
- package/lib/scraper-client.js +19 -6
- package/lib/sector-focus.js +4 -4
- package/lib/separation.js +137 -10
- package/lib/settings-effects.js +20 -11
- package/lib/settings.js +3 -0
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +51 -13
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +0 -8
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j4-lc3._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0l8cxmo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0xkur4p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_14iv_zy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_15x6q32._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_212xw8q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_19c2d9c._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +0 -3
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +0 -1
- package/.next/standalone/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/standalone/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +0 -1
- package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/15sjx07xhpn6p.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1a74dky22dxd6.js +0 -1
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/297e1z34yi--z.js +0 -1
- package/.next/static/chunks/2_ak6qjxb6oa5.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3ui4d-uqkqj5d.js +0 -2
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{jqE2D3zWqSGz5su948Azf → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
|
@@ -1,737 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Orbit — the galaxy from the v2 build, ported.
|
|
5
|
-
*
|
|
6
|
-
* Different from the original Galaxy view in three ways that matter:
|
|
7
|
-
* - Tiers are *orbits*. Higher leverage sits closer to you, so distance from
|
|
8
|
-
* the centre means something rather than being wherever the force settled.
|
|
9
|
-
* - Second-degree people render as small dots fanned in an arc just outside
|
|
10
|
-
* the connection who leads to them, so a bridge with a big circle behind it
|
|
11
|
-
* reads as a cluster at a glance.
|
|
12
|
-
* - Drag a 1st-degree node and it stays where you drop it if it is a bridge;
|
|
13
|
-
* leaves re-settle. Double-click releases it.
|
|
14
|
-
*
|
|
15
|
-
* Imperative D3 over a ref'd <svg>: React renders the shell, d3-selection owns
|
|
16
|
-
* everything inside it, so the simulation mutates positions without React churn.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
20
|
-
import useRequests from './useRequests';
|
|
21
|
-
import { hasRequest } from '../../lib/requests-client';
|
|
22
|
-
import {
|
|
23
|
-
drag,
|
|
24
|
-
forceCollide,
|
|
25
|
-
forceLink,
|
|
26
|
-
forceManyBody,
|
|
27
|
-
forceRadial,
|
|
28
|
-
forceSimulation,
|
|
29
|
-
select,
|
|
30
|
-
zoom,
|
|
31
|
-
zoomIdentity,
|
|
32
|
-
zoomTransform,
|
|
33
|
-
} from 'd3';
|
|
34
|
-
import { TIER_COLORS, TIER_ORDER, TIER_RING_RADIUS, initialsFor } from '../../lib/tiers';
|
|
35
|
-
import { localPhoto } from '../../lib/photos';
|
|
36
|
-
|
|
37
|
-
const GOLDEN_ANGLE = 2.399963;
|
|
38
|
-
const LABEL_ZOOM_THRESHOLD = 1.1;
|
|
39
|
-
const USER_RADIUS = 26;
|
|
40
|
-
const HUB_FALLBACK = 14; // hubs to show when no circle has been mapped yet
|
|
41
|
-
const STANDARD_RADIUS = 4.5; // context nodes: one circle each, nothing more
|
|
42
|
-
|
|
43
|
-
function safeId(id) {
|
|
44
|
-
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Every string here came from a scraped web page. TRAPS §11.
|
|
48
|
-
function escapeHtml(s) {
|
|
49
|
-
return String(s ?? '').replace(/[&<>"']/g, (c) =>
|
|
50
|
-
c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : '''
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function useIsMobile() {
|
|
55
|
-
const [mobile, setMobile] = useState(false);
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
const check = () => setMobile(window.innerWidth < 768);
|
|
58
|
-
check();
|
|
59
|
-
window.addEventListener('resize', check);
|
|
60
|
-
return () => window.removeEventListener('resize', check);
|
|
61
|
-
}, []);
|
|
62
|
-
return mobile;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const OrbitGraph = forwardRef(function OrbitGraph(
|
|
66
|
-
{ connections = [], degree2 = [], onSelect, userName = 'You', userImage = null, selectedId = null, mode = 'network' },
|
|
67
|
-
ref
|
|
68
|
-
) {
|
|
69
|
-
const containerRef = useRef(null);
|
|
70
|
-
const svgRef = useRef(null);
|
|
71
|
-
const tooltipRef = useRef(null);
|
|
72
|
-
const onSelectRef = useRef(onSelect);
|
|
73
|
-
const selectedIdRef = useRef(selectedId);
|
|
74
|
-
const zoomKRef = useRef(1);
|
|
75
|
-
const savedTransformRef = useRef(null);
|
|
76
|
-
const applySelectionRef = useRef(() => {});
|
|
77
|
-
const applyRequestsRef = useRef(() => {});
|
|
78
|
-
const focusNodeRef = useRef(() => {});
|
|
79
|
-
const [size, setSize] = useState({ w: 0, h: 0 });
|
|
80
|
-
const isMobile = useIsMobile();
|
|
81
|
-
|
|
82
|
-
// The parent recomputes these arrays on every render, so depending on their
|
|
83
|
-
// identity rebuilds the whole scene constantly — and a rebuild that happens
|
|
84
|
-
// before the simulation's first frame means it never ticks at all. With a
|
|
85
|
-
// small network that settled in the gaps and looked fine; at 749 nodes and
|
|
86
|
-
// 598 links every node stayed at 0,0. Depend on what is actually in them.
|
|
87
|
-
const signature = useMemo(
|
|
88
|
-
() => `${connections.length}:${degree2.length}:` +
|
|
89
|
-
`${connections.map((c) => c.id).join(',')}|${degree2.map((c) => c.id).join(',')}`,
|
|
90
|
-
[connections, degree2]
|
|
91
|
-
);
|
|
92
|
-
const dataRef = useRef({ connections, degree2 });
|
|
93
|
-
|
|
94
|
-
useImperativeHandle(ref, () => ({ focusNode: (id) => focusNodeRef.current(id) }), []);
|
|
95
|
-
|
|
96
|
-
useEffect(() => { onSelectRef.current = onSelect; }, [onSelect]);
|
|
97
|
-
|
|
98
|
-
// Declared before the scene effect so it has already run when that rebuilds.
|
|
99
|
-
useEffect(() => { dataRef.current = { connections, degree2 }; }, [connections, degree2]);
|
|
100
|
-
|
|
101
|
-
// Measure synchronously first — first paint must not wait on ResizeObserver.
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
const el = containerRef.current;
|
|
104
|
-
if (!el) return undefined;
|
|
105
|
-
const measure = () => {
|
|
106
|
-
const rect = el.getBoundingClientRect();
|
|
107
|
-
setSize((prev) => {
|
|
108
|
-
const w = Math.round(rect.width);
|
|
109
|
-
const h = Math.round(rect.height);
|
|
110
|
-
return prev.w === w && prev.h === h ? prev : { w, h };
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
|
-
measure();
|
|
114
|
-
const ro = new ResizeObserver(measure);
|
|
115
|
-
ro.observe(el);
|
|
116
|
-
window.addEventListener('resize', measure);
|
|
117
|
-
return () => {
|
|
118
|
-
ro.disconnect();
|
|
119
|
-
window.removeEventListener('resize', measure);
|
|
120
|
-
};
|
|
121
|
-
}, []);
|
|
122
|
-
|
|
123
|
-
// Selection restyles existing nodes; it never rebuilds the simulation.
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
selectedIdRef.current = selectedId;
|
|
126
|
-
applySelectionRef.current(selectedId);
|
|
127
|
-
}, [selectedId]);
|
|
128
|
-
|
|
129
|
-
// So do requests you've sent (lib/requests-client.js): a request sent from
|
|
130
|
-
// any view rings that person's dots here at once.
|
|
131
|
-
const requests = useRequests();
|
|
132
|
-
const requestsRef = useRef(requests);
|
|
133
|
-
useEffect(() => {
|
|
134
|
-
requestsRef.current = requests;
|
|
135
|
-
applyRequestsRef.current(requests);
|
|
136
|
-
}, [requests]);
|
|
137
|
-
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
const svgEl = svgRef.current;
|
|
140
|
-
if (!svgEl || size.w < 40 || size.h < 40) return undefined;
|
|
141
|
-
|
|
142
|
-
const width = size.w;
|
|
143
|
-
const height = size.h;
|
|
144
|
-
|
|
145
|
-
// ---- nodes & links ---------------------------------------------------
|
|
146
|
-
const { connections: liveConnections, degree2: liveDegree2 } = dataRef.current;
|
|
147
|
-
// In Degrees the subject is the circles, so only the people you've mapped
|
|
148
|
-
// are drawn, each with room for everyone behind them.
|
|
149
|
-
const degreesMode = mode === 'degrees';
|
|
150
|
-
const mappedIds = new Set(liveDegree2.map((c) => c.source_connection_id).filter(Boolean));
|
|
151
|
-
const visibleD1 = degreesMode ? liveConnections.filter((c) => mappedIds.has(c.id)) : liveConnections;
|
|
152
|
-
const d1ById = new Map(visibleD1.map((c) => [c.id, c]));
|
|
153
|
-
const visibleD2 = liveDegree2.filter(
|
|
154
|
-
(c) => c.source_connection_id && d1ById.has(c.source_connection_id)
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
// Only draw rings for tiers actually on screen — the filter lives upstream.
|
|
158
|
-
const activeTiers = new Set(visibleD1.map((c) => c.tier).filter(Boolean));
|
|
159
|
-
|
|
160
|
-
// Three weights, not one.
|
|
161
|
-
//
|
|
162
|
-
// Drawing all 754 connections at full weight — glow, avatar, clip path,
|
|
163
|
-
// label — is both the lag and the mess: nothing stands out because
|
|
164
|
-
// everything is equally loud. The people whose circle you have opened are
|
|
165
|
-
// the subject; everyone else is context. A hub is someone with a mapped
|
|
166
|
-
// circle, falling back to the strongest people when nothing is mapped yet,
|
|
167
|
-
// so the view is never empty of structure on a fresh network.
|
|
168
|
-
const bridgeIds = new Set(liveDegree2.map((c) => c.source_connection_id).filter(Boolean));
|
|
169
|
-
const hubIds = bridgeIds.size
|
|
170
|
-
? new Set(visibleD1.filter((c) => bridgeIds.has(c.id)).map((c) => c.id))
|
|
171
|
-
: new Set(
|
|
172
|
-
[...visibleD1]
|
|
173
|
-
.sort((a, b) => (Number(b.power_score) || 0) - (Number(a.power_score) || 0))
|
|
174
|
-
.slice(0, HUB_FALLBACK)
|
|
175
|
-
.map((c) => c.id)
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
const userNode = {
|
|
179
|
-
id: '__user__', kind: 'user', tier: null, r: USER_RADIUS,
|
|
180
|
-
targetRadius: 0, connection: null, x: 0, y: 0, fx: 0, fy: 0,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const tierIndex = { S: 0, A: 0, B: 0, C: 0, D: 0 };
|
|
184
|
-
const d1Nodes = [];
|
|
185
|
-
const d1NodeById = new Map();
|
|
186
|
-
const d1AngleById = new Map();
|
|
187
|
-
for (const c of visibleD1) {
|
|
188
|
-
const tier = c.tier || 'D';
|
|
189
|
-
const angle = (tierIndex[tier] = (tierIndex[tier] || 0) + 1, (tierIndex[tier] - 1) * GOLDEN_ANGLE);
|
|
190
|
-
const ring = TIER_RING_RADIUS[tier] || TIER_RING_RADIUS.D;
|
|
191
|
-
const isHub = hubIds.has(c.id);
|
|
192
|
-
const n = {
|
|
193
|
-
id: c.id, kind: 'd1', hub: isHub, tier,
|
|
194
|
-
r: isHub ? 7 + Number(c.power_score || 0) * 1.1 : STANDARD_RADIUS,
|
|
195
|
-
targetRadius: ring, connection: c,
|
|
196
|
-
x: Math.cos(angle) * ring, y: Math.sin(angle) * ring,
|
|
197
|
-
};
|
|
198
|
-
d1Nodes.push(n);
|
|
199
|
-
d1NodeById.set(c.id, n);
|
|
200
|
-
d1AngleById.set(c.id, angle);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Group each bridge's circle so it can be fanned beside them.
|
|
204
|
-
const clusters = new Map();
|
|
205
|
-
for (const c of visibleD2) {
|
|
206
|
-
const key = c.source_connection_id;
|
|
207
|
-
const list = clusters.get(key);
|
|
208
|
-
if (list) list.push(c);
|
|
209
|
-
else clusters.set(key, [c]);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const d2Nodes = [];
|
|
213
|
-
const links = [];
|
|
214
|
-
let sceneRadius = TIER_RING_RADIUS.B + 70;
|
|
215
|
-
if (degreesMode) {
|
|
216
|
-
// Someone you added through a circle, whose own circle is scanned, sits in
|
|
217
|
-
// the fan of the circle they came from, and their people fan out behind
|
|
218
|
-
// them a band further out, and theirs behind them, to 6th degree (Blake,
|
|
219
|
-
// 2026-09-29: Orbit stopped at each connection's 2nd degree).
|
|
220
|
-
const nestedIn = new Map();
|
|
221
|
-
const nested = new Set();
|
|
222
|
-
for (const d of d1Nodes) {
|
|
223
|
-
const from = d.connection?.unlocked_from_bridge_id;
|
|
224
|
-
if (from == null || from === d.id || !d1NodeById.has(from)) continue;
|
|
225
|
-
nested.add(d.id);
|
|
226
|
-
const list = nestedIn.get(from);
|
|
227
|
-
if (list) list.push(d); else nestedIn.set(from, [d]);
|
|
228
|
-
}
|
|
229
|
-
// Everyone reachable from someone on the ring is drawn in a fan; anyone
|
|
230
|
-
// else (a loop of introductions) goes on the ring.
|
|
231
|
-
const onRing = d1Nodes.filter((d) => !nested.has(d.id));
|
|
232
|
-
const inFans = new Set();
|
|
233
|
-
const walk = (d) => { for (const p of nestedIn.get(d.id) || []) if (!inFans.has(p.id)) { inFans.add(p.id); walk(p); } };
|
|
234
|
-
onRing.forEach(walk);
|
|
235
|
-
for (const d of d1Nodes) if (nested.has(d.id) && !inFans.has(d.id)) onRing.push(d);
|
|
236
|
-
const behind = new Map();
|
|
237
|
-
const reachOf = (d, stack = new Set()) => {
|
|
238
|
-
if (behind.has(d.id)) return behind.get(d.id);
|
|
239
|
-
if (stack.has(d.id)) return 0;
|
|
240
|
-
stack.add(d.id);
|
|
241
|
-
let count = clusters.get(d.id)?.length || 0;
|
|
242
|
-
for (const p of nestedIn.get(d.id) || []) count += 1 + reachOf(p, stack);
|
|
243
|
-
stack.delete(d.id);
|
|
244
|
-
behind.set(d.id, count);
|
|
245
|
-
return count;
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
// One ring of the people you've mapped, biggest reach first, each given
|
|
249
|
-
// a wedge; their circle fans outward in arcs, the most powerful closest.
|
|
250
|
-
const ordered = [...onRing].sort((a, b) => reachOf(b) - reachOf(a));
|
|
251
|
-
const n = Math.max(1, ordered.length);
|
|
252
|
-
const R = Math.max(140, n * 9);
|
|
253
|
-
// Room in proportion to how many are behind each person (square-rooted,
|
|
254
|
-
// so one huge circle can't take the whole ring): big circles spread into
|
|
255
|
-
// fans instead of shooting out as thin spikes.
|
|
256
|
-
const weight = (d) => Math.sqrt(reachOf(d) + 4);
|
|
257
|
-
const total = ordered.reduce((sum, d) => sum + weight(d), 0) || 1;
|
|
258
|
-
|
|
259
|
-
// `owner`'s people in rows across `wedge`, from rStart out; someone they
|
|
260
|
-
// added gets their own narrower fan behind them. Returns the outer edge.
|
|
261
|
-
const placeFan = (owner, angle, wedge, rStart, depth) => {
|
|
262
|
-
const people = [
|
|
263
|
-
...(clusters.get(owner.id) || []).map((c) => ({ c })),
|
|
264
|
-
...(nestedIn.get(owner.id) || []).filter((p) => inFans.has(p.id)).map((p) => ({ added: p, c: p.connection })),
|
|
265
|
-
].sort((p, q) => (Number(q.c?.power_score) || 0) - (Number(p.c?.power_score) || 0));
|
|
266
|
-
const far = depth > 2;
|
|
267
|
-
const step = far ? 8 : 9;
|
|
268
|
-
const subs = [];
|
|
269
|
-
let placed = 0;
|
|
270
|
-
let r = rStart;
|
|
271
|
-
while (placed < people.length) {
|
|
272
|
-
const fit = Math.max(1, Math.floor((r * wedge) / step));
|
|
273
|
-
const row = people.slice(placed, placed + fit);
|
|
274
|
-
row.forEach((p, j) => {
|
|
275
|
-
const a = angle + (row.length === 1 ? 0 : (j / (row.length - 1) - 0.5) * wedge);
|
|
276
|
-
const x = Math.cos(a) * r;
|
|
277
|
-
const y = Math.sin(a) * r;
|
|
278
|
-
if (p.added) {
|
|
279
|
-
const d = p.added;
|
|
280
|
-
d.x = d.fx = x; d.y = d.fy = y;
|
|
281
|
-
d.hub = false; d.added = true;
|
|
282
|
-
d.r = 5 + Math.min(5, reachOf(d) / 60);
|
|
283
|
-
subs.push({ d, a });
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const node = {
|
|
287
|
-
id: p.c.id, kind: 'd2', depth, tier: p.c.tier || 'D',
|
|
288
|
-
r: (far ? 1.3 : 1.8) + Math.min(10, Number(p.c.power_score) || 0) * (far ? 0.24 : 0.32),
|
|
289
|
-
connection: p.c, x, y,
|
|
290
|
-
};
|
|
291
|
-
node.fx = node.x; node.fy = node.y;
|
|
292
|
-
d2Nodes.push(node);
|
|
293
|
-
if (owner.added) links.push({ source: owner, target: node, faint: true });
|
|
294
|
-
});
|
|
295
|
-
placed += row.length;
|
|
296
|
-
r += step;
|
|
297
|
-
}
|
|
298
|
-
// Their people go behind this fan, centred on it (not on wherever the
|
|
299
|
-
// person they came through sits), side by side in the order those people
|
|
300
|
-
// sit, so no fan spills into the next one's space; a line runs back to
|
|
301
|
-
// each. Wider than the wedge, they close up to fit it.
|
|
302
|
-
let edge = r;
|
|
303
|
-
const next = subs.filter(({ d }) => depth < 6 && reachOf(d)).sort((x, y) => x.a - y.a);
|
|
304
|
-
if (next.length) {
|
|
305
|
-
const from = r + 24;
|
|
306
|
-
const gap = 10 / from;
|
|
307
|
-
const want = next.map(({ d }) => Math.max(0.12, (Math.min(reachOf(d), 60) * 8) / from));
|
|
308
|
-
const need = want.reduce((sum, w) => sum + w, 0) + gap * (next.length - 1);
|
|
309
|
-
const scale = need > wedge ? wedge / need : 1;
|
|
310
|
-
let at = angle - (need * scale) / 2;
|
|
311
|
-
next.forEach(({ d }, k) => {
|
|
312
|
-
const w = want[k] * scale;
|
|
313
|
-
edge = Math.max(edge, placeFan(d, at + w / 2, w, from, depth + 1));
|
|
314
|
-
at += w + gap * scale;
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
return edge;
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
let cursor = -Math.PI / 2;
|
|
321
|
-
ordered.forEach((b) => {
|
|
322
|
-
const share = (weight(b) / total) * Math.PI * 2;
|
|
323
|
-
const angle = cursor + share / 2;
|
|
324
|
-
cursor += share;
|
|
325
|
-
const wedge = Math.min(share * 0.88, 1.6);
|
|
326
|
-
b.x = b.fx = Math.cos(angle) * R;
|
|
327
|
-
b.y = b.fy = Math.sin(angle) * R;
|
|
328
|
-
b.hub = true;
|
|
329
|
-
b.r = 9 + Math.min(10, (clusters.get(b.id)?.length || 0) / 40);
|
|
330
|
-
sceneRadius = Math.max(sceneRadius, placeFan(b, angle, wedge, R + b.r + 16, 2) + 10);
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
for (const [bridgeId, cluster] of degreesMode ? [] : clusters) {
|
|
334
|
-
const bridgeNode = d1NodeById.get(bridgeId);
|
|
335
|
-
if (!bridgeNode) continue;
|
|
336
|
-
const baseAngle = d1AngleById.get(bridgeId) ?? 0;
|
|
337
|
-
const ring = bridgeNode.targetRadius;
|
|
338
|
-
const fanStep = Math.min(0.15, 1.5 / Math.max(1, cluster.length));
|
|
339
|
-
cluster.forEach((c, j) => {
|
|
340
|
-
const angle = baseAngle + (j - (cluster.length - 1) / 2) * fanStep;
|
|
341
|
-
const radius = ring + 46 + (j % 3) * 13;
|
|
342
|
-
const n = {
|
|
343
|
-
id: c.id, kind: 'd2', tier: c.tier || 'D', r: 3.5,
|
|
344
|
-
targetRadius: ring + 60, connection: c,
|
|
345
|
-
x: Math.cos(angle) * radius, y: Math.sin(angle) * radius,
|
|
346
|
-
};
|
|
347
|
-
d2Nodes.push(n);
|
|
348
|
-
links.push({ source: n, target: bridgeNode });
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const simNodes = [userNode, ...d1Nodes, ...d2Nodes];
|
|
353
|
-
// Paint order: faint D2 first, then D1, the user on top.
|
|
354
|
-
const renderNodes = [...d2Nodes, ...d1Nodes, userNode];
|
|
355
|
-
|
|
356
|
-
// ---- static scene ----------------------------------------------------
|
|
357
|
-
const svg = select(svgEl);
|
|
358
|
-
svg.selectAll('*').remove();
|
|
359
|
-
|
|
360
|
-
const defs = svg.append('defs');
|
|
361
|
-
const grad = defs.append('radialGradient').attr('id', 'og-user-grad');
|
|
362
|
-
grad.append('stop').attr('offset', '0%').attr('stop-color', '#f8fafc');
|
|
363
|
-
grad.append('stop').attr('offset', '55%').attr('stop-color', '#c7d2fe');
|
|
364
|
-
grad.append('stop').attr('offset', '100%').attr('stop-color', '#818cf8');
|
|
365
|
-
defs.append('filter').attr('id', 'og-blur')
|
|
366
|
-
.attr('x', '-75%').attr('y', '-75%').attr('width', '250%').attr('height', '250%')
|
|
367
|
-
.append('feGaussianBlur').attr('stdDeviation', 6);
|
|
368
|
-
|
|
369
|
-
const viewport = svg.append('g').attr('class', 'viewport');
|
|
370
|
-
const ringsLayer = viewport.append('g');
|
|
371
|
-
const spokesLayer = viewport.append('g');
|
|
372
|
-
const linksLayer = viewport.append('g');
|
|
373
|
-
const nodesLayer = viewport.append('g');
|
|
374
|
-
|
|
375
|
-
const hubNodes = d1Nodes.filter((d) => d.hub);
|
|
376
|
-
const spoke = spokesLayer.selectAll('line').data(hubNodes, (d) => d.id).join('line')
|
|
377
|
-
.attr('stroke', (d) => TIER_COLORS[d.tier] || TIER_COLORS.D)
|
|
378
|
-
.attr('stroke-opacity', 0.09)
|
|
379
|
-
.attr('stroke-width', 0.5);
|
|
380
|
-
|
|
381
|
-
if (degreesMode && d1Nodes.length) {
|
|
382
|
-
const R = Math.hypot(d1Nodes[0].x, d1Nodes[0].y);
|
|
383
|
-
ringsLayer.append('circle').attr('r', R).attr('fill', 'none')
|
|
384
|
-
.attr('stroke', '#FF6B35').attr('stroke-opacity', 0.18).attr('stroke-width', 1)
|
|
385
|
-
.attr('stroke-dasharray', '3 7');
|
|
386
|
-
}
|
|
387
|
-
for (const tier of degreesMode ? [] : TIER_ORDER) {
|
|
388
|
-
if (!activeTiers.has(tier)) continue;
|
|
389
|
-
ringsLayer.append('circle')
|
|
390
|
-
.attr('r', TIER_RING_RADIUS[tier])
|
|
391
|
-
.attr('fill', 'none')
|
|
392
|
-
.attr('stroke', TIER_COLORS[tier])
|
|
393
|
-
.attr('stroke-opacity', 0.14)
|
|
394
|
-
.attr('stroke-width', 1)
|
|
395
|
-
.attr('stroke-dasharray', '3 7');
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const link = linksLayer.selectAll('line').data(links).join('line')
|
|
399
|
-
.attr('stroke', (l) => (l.faint ? 'rgba(0,255,136,0.12)' : 'rgba(255,255,255,0.08)'))
|
|
400
|
-
.attr('stroke-width', (l) => (l.faint ? 0.5 : 1));
|
|
401
|
-
|
|
402
|
-
const node = nodesLayer.selectAll('g').data(renderNodes, (d) => d.id).join('g')
|
|
403
|
-
.attr('cursor', 'pointer');
|
|
404
|
-
|
|
405
|
-
node.each(function build(d) {
|
|
406
|
-
const g = select(this);
|
|
407
|
-
|
|
408
|
-
if (d.kind === 'd2') {
|
|
409
|
-
g.attr('opacity', degreesMode ? (d.depth > 2 ? 0.6 : 0.85) : 0.55);
|
|
410
|
-
g.append('circle').attr('class', 'og-base').attr('r', d.r)
|
|
411
|
-
.attr('fill', TIER_COLORS[d.tier] || TIER_COLORS.D);
|
|
412
|
-
return; // selection ring is created on demand, see applySelection
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const isUser = d.kind === 'user';
|
|
416
|
-
const conn = d.connection;
|
|
417
|
-
const color = isUser ? '#818cf8' : (TIER_COLORS[d.tier] || TIER_COLORS.D);
|
|
418
|
-
|
|
419
|
-
// Context: one circle, no avatar, no initials, no label. Hovering or
|
|
420
|
-
// selecting promotes it — the tooltip carries the detail, so nothing is
|
|
421
|
-
// hidden, it is just not all shouted at once.
|
|
422
|
-
if (!isUser && !d.hub) {
|
|
423
|
-
g.attr('opacity', d.added ? 1 : 0.75);
|
|
424
|
-
// Someone you added through a circle: green-ringed, with their own fan behind them.
|
|
425
|
-
g.append('circle').attr('class', 'og-base').attr('r', d.r).attr('fill', color)
|
|
426
|
-
.attr('stroke', d.added ? '#00ff88' : 'none').attr('stroke-width', d.added ? 1.5 : 0);
|
|
427
|
-
return; // selection ring is created on demand, see applySelection
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (isUser || d.tier === 'S' || d.tier === 'A') {
|
|
431
|
-
g.append('circle').attr('r', d.r * 1.8).attr('fill', color)
|
|
432
|
-
.attr('opacity', 0.25).attr('filter', 'url(#og-blur)');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (conn && conn.is_catalyst) {
|
|
436
|
-
g.append('circle').attr('r', d.r + 3).attr('fill', 'none')
|
|
437
|
-
.attr('stroke', TIER_COLORS.S).attr('stroke-width', 1).attr('stroke-opacity', 0.9);
|
|
438
|
-
g.append('text').text('⚡').attr('x', d.r * 0.85).attr('y', -d.r * 0.7)
|
|
439
|
-
.attr('font-size', 9).attr('text-anchor', 'middle').style('pointer-events', 'none');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Initials always render; an avatar draws on top and removes itself on
|
|
443
|
-
// error, so a dead image never leaves an empty circle.
|
|
444
|
-
g.append('circle').attr('class', 'og-base').attr('r', d.r)
|
|
445
|
-
.attr('fill', isUser ? 'url(#og-user-grad)' : color);
|
|
446
|
-
|
|
447
|
-
const name = isUser ? userName : (conn?.name ?? '');
|
|
448
|
-
g.append('text').attr('class', 'og-initials').text(initialsFor(name))
|
|
449
|
-
.attr('text-anchor', 'middle').attr('dy', '0.35em')
|
|
450
|
-
.attr('font-size', d.r * 0.8).attr('font-weight', 700)
|
|
451
|
-
.attr('fill', '#0b0b16').style('pointer-events', 'none');
|
|
452
|
-
|
|
453
|
-
// Only a photo saved on this computer (lib/photos.js).
|
|
454
|
-
const imageUrl = localPhoto(isUser ? userImage : conn?.profile_image_url);
|
|
455
|
-
if (imageUrl) {
|
|
456
|
-
const clipId = `og-clip-${safeId(d.id)}`;
|
|
457
|
-
defs.append('clipPath').attr('id', clipId).append('circle').attr('r', d.r);
|
|
458
|
-
g.append('image')
|
|
459
|
-
.attr('href', imageUrl)
|
|
460
|
-
.attr('x', -d.r).attr('y', -d.r)
|
|
461
|
-
.attr('width', d.r * 2).attr('height', d.r * 2)
|
|
462
|
-
.attr('clip-path', `url(#${clipId})`)
|
|
463
|
-
.attr('preserveAspectRatio', 'xMidYMid slice')
|
|
464
|
-
.on('error', function onErr() { select(this).remove(); });
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
g.append('circle').attr('class', 'og-sel').attr('r', d.r + 5)
|
|
468
|
-
.attr('fill', 'none').attr('stroke', '#ffffff').attr('stroke-width', 1.5)
|
|
469
|
-
.style('opacity', 0);
|
|
470
|
-
|
|
471
|
-
if (isUser) {
|
|
472
|
-
g.append('text').text(userName).attr('y', d.r + 18)
|
|
473
|
-
.attr('text-anchor', 'middle').attr('font-size', 12).attr('font-weight', 600)
|
|
474
|
-
.attr('fill', '#e2e8f0').attr('paint-order', 'stroke')
|
|
475
|
-
.attr('stroke', 'rgba(5,5,16,0.85)').attr('stroke-width', 3)
|
|
476
|
-
.style('pointer-events', 'none');
|
|
477
|
-
} else {
|
|
478
|
-
g.append('text').attr('class', 'og-label').text(conn?.name ?? '')
|
|
479
|
-
.attr('y', d.r + 13).attr('text-anchor', 'middle').attr('font-size', 10)
|
|
480
|
-
.attr('fill', '#cbd5e1').attr('paint-order', 'stroke')
|
|
481
|
-
.attr('stroke', 'rgba(5,5,16,0.8)').attr('stroke-width', 3)
|
|
482
|
-
.style('pointer-events', 'none').style('opacity', 0)
|
|
483
|
-
.style('transition', 'opacity 160ms ease');
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
// ---- labels ----------------------------------------------------------
|
|
488
|
-
const labels = node.select('text.og-label');
|
|
489
|
-
let hoveredId = null;
|
|
490
|
-
// S-tier and catalysts stay labelled so the map is scannable zoomed out.
|
|
491
|
-
// Hubs carry their name; context nodes have no label element at all, which
|
|
492
|
-
// is most of what was removed.
|
|
493
|
-
const alwaysLabeled = (d) => d.hub === true;
|
|
494
|
-
const refreshLabels = () => {
|
|
495
|
-
labels.style('opacity', (d) =>
|
|
496
|
-
alwaysLabeled(d) ||
|
|
497
|
-
zoomKRef.current > LABEL_ZOOM_THRESHOLD ||
|
|
498
|
-
d.id === hoveredId ||
|
|
499
|
-
d.id === selectedIdRef.current ? 1 : 0
|
|
500
|
-
);
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
// ---- interaction -----------------------------------------------------
|
|
504
|
-
node.on('click', (event, d) => {
|
|
505
|
-
if (event.defaultPrevented) return;
|
|
506
|
-
event.stopPropagation();
|
|
507
|
-
onSelectRef.current?.(d.connection);
|
|
508
|
-
});
|
|
509
|
-
svg.on('click', () => onSelectRef.current?.(null));
|
|
510
|
-
|
|
511
|
-
const tip = tooltipRef.current;
|
|
512
|
-
const moveTip = (event) => {
|
|
513
|
-
const el = containerRef.current;
|
|
514
|
-
if (!tip || !el) return;
|
|
515
|
-
const rect = el.getBoundingClientRect();
|
|
516
|
-
tip.style.left = `${Math.min(event.clientX - rect.left + 14, rect.width - 190)}px`;
|
|
517
|
-
tip.style.top = `${Math.max(event.clientY - rect.top - 8, 8)}px`;
|
|
518
|
-
};
|
|
519
|
-
const showTip = (event, d) => {
|
|
520
|
-
if (!tip || !d.connection) return;
|
|
521
|
-
const c = d.connection;
|
|
522
|
-
const meta = [c.role || c.headline, c.company].filter(Boolean).map(String);
|
|
523
|
-
tip.innerHTML =
|
|
524
|
-
`<div style="font-weight:600;color:#f1f5f9">${escapeHtml(c.name)}</div>` +
|
|
525
|
-
(meta.length ? `<div style="color:#94a3b8;margin-top:1px">${escapeHtml(meta.join(' · '))}</div>` : '') +
|
|
526
|
-
`<div style="margin-top:3px;color:${TIER_COLORS[c.tier] || TIER_COLORS.D}">` +
|
|
527
|
-
`${escapeHtml(c.tier || '?')} · power ${Number(c.power_score || 0).toFixed(1)}` +
|
|
528
|
-
`${c.is_catalyst ? ' · ⚡ catalyst' : ''}</div>`;
|
|
529
|
-
tip.style.opacity = '1';
|
|
530
|
-
moveTip(event);
|
|
531
|
-
};
|
|
532
|
-
const hideTip = () => { if (tip) tip.style.opacity = '0'; };
|
|
533
|
-
|
|
534
|
-
if (!isMobile) {
|
|
535
|
-
node
|
|
536
|
-
.on('mouseenter', (event, d) => { hoveredId = d.id; refreshLabels(); showTip(event, d); })
|
|
537
|
-
.on('mousemove', (event) => moveTip(event))
|
|
538
|
-
.on('mouseleave', () => { hoveredId = null; refreshLabels(); hideTip(); });
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// ---- positions -------------------------------------------------------
|
|
542
|
-
const ticked = () => {
|
|
543
|
-
link
|
|
544
|
-
.attr('x1', (l) => l.source.x ?? 0).attr('y1', (l) => l.source.y ?? 0)
|
|
545
|
-
.attr('x2', (l) => l.target.x ?? 0).attr('y2', (l) => l.target.y ?? 0);
|
|
546
|
-
spoke
|
|
547
|
-
.attr('x1', 0).attr('y1', 0)
|
|
548
|
-
.attr('x2', (d) => d.x ?? 0).attr('y2', (d) => d.y ?? 0);
|
|
549
|
-
node.attr('transform', (d) => `translate(${d.x ?? 0},${d.y ?? 0})`);
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
let sim = null;
|
|
553
|
-
if (isMobile || degreesMode) {
|
|
554
|
-
ticked(); // static layouts: golden-angle on phones, wedges in Degrees
|
|
555
|
-
} else {
|
|
556
|
-
const simulation = forceSimulation(simNodes)
|
|
557
|
-
// A 2nd-degree person belongs to their bridge, not to a ring. Giving
|
|
558
|
-
// them their own radial target fought the link holding them to that
|
|
559
|
-
// bridge, and the result was dots strewn across everyone else's space.
|
|
560
|
-
// Zero here lets the link force gather each circle around its own hub.
|
|
561
|
-
.force('radial', forceRadial((d) => d.targetRadius, 0, 0)
|
|
562
|
-
.strength((d) => (d.kind === 'd1' ? 0.9 : 0)))
|
|
563
|
-
.force('collide', forceCollide((d) => d.r + 2.5))
|
|
564
|
-
.force('charge', forceManyBody().strength(-18))
|
|
565
|
-
.on('tick', ticked);
|
|
566
|
-
if (links.length > 0) {
|
|
567
|
-
simulation.force('link', forceLink(links).distance(26).strength(0.7));
|
|
568
|
-
}
|
|
569
|
-
sim = simulation;
|
|
570
|
-
|
|
571
|
-
// Place everyone at their computed starting positions right away. The
|
|
572
|
-
// simulation ticks on requestAnimationFrame, which does not run in a
|
|
573
|
-
// background or hidden tab — without this the whole graph sits stacked at
|
|
574
|
-
// the origin until the tab is looked at.
|
|
575
|
-
ticked();
|
|
576
|
-
|
|
577
|
-
const dragBehavior = drag()
|
|
578
|
-
.on('start', (event, d) => {
|
|
579
|
-
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
580
|
-
d.fx = d.x; d.fy = d.y;
|
|
581
|
-
})
|
|
582
|
-
.on('drag', (event, d) => { d.fx = event.x; d.fy = event.y; })
|
|
583
|
-
.on('end', (event, d) => {
|
|
584
|
-
if (!event.active) simulation.alphaTarget(0);
|
|
585
|
-
// Bridges stay where you drop them; leaves re-settle.
|
|
586
|
-
const isBridge = d.connection?.circle_power !== null && d.connection?.circle_power !== undefined;
|
|
587
|
-
if (!isBridge) { d.fx = null; d.fy = null; }
|
|
588
|
-
});
|
|
589
|
-
node.filter((d) => d.kind === 'd1').call(dragBehavior);
|
|
590
|
-
node.on('dblclick', (event, d) => {
|
|
591
|
-
if (d.kind !== 'd1') return;
|
|
592
|
-
event.stopPropagation();
|
|
593
|
-
d.fx = null; d.fy = null;
|
|
594
|
-
simulation.alphaTarget(0.2).restart();
|
|
595
|
-
window.setTimeout(() => simulation.alphaTarget(0), 400);
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// ---- zoom & pan ------------------------------------------------------
|
|
600
|
-
const zoomBehavior = zoom().scaleExtent([0.15, 3]).on('zoom', (event) => {
|
|
601
|
-
viewport.attr('transform', event.transform.toString());
|
|
602
|
-
// Persist only user-driven camera moves; saving the programmatic initial
|
|
603
|
-
// transform would pin the view to stale dimensions across resizes.
|
|
604
|
-
if (event.sourceEvent) savedTransformRef.current = event.transform;
|
|
605
|
-
if (event.transform.k !== zoomKRef.current) {
|
|
606
|
-
zoomKRef.current = event.transform.k;
|
|
607
|
-
refreshLabels();
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
svg.call(zoomBehavior).on('dblclick.zoom', null);
|
|
611
|
-
|
|
612
|
-
const fitK = Math.max(0.15, Math.min(3, Math.min(width, height) / (sceneRadius * 2)));
|
|
613
|
-
const initialTransform =
|
|
614
|
-
savedTransformRef.current ?? zoomIdentity.translate(width / 2, height / 2).scale(fitK);
|
|
615
|
-
svg.call(zoomBehavior.transform, initialTransform);
|
|
616
|
-
|
|
617
|
-
// ---- selection -------------------------------------------------------
|
|
618
|
-
applySelectionRef.current = (id) => {
|
|
619
|
-
// Hubs keep a permanent ring; everyone else gets one only while selected.
|
|
620
|
-
// Creating 1,400 invisible rings up front was most of the element count.
|
|
621
|
-
nodesLayer.selectAll('circle.og-sel-temp').remove();
|
|
622
|
-
node.select('circle.og-sel').style('opacity', (d) => (d.id === id ? 1 : 0));
|
|
623
|
-
node.select('circle.og-base').attr('r', (d) => (d.id === id && d.kind !== 'user' ? d.r + 1.5 : d.r));
|
|
624
|
-
if (id) {
|
|
625
|
-
node.filter((d) => d.id === id && !d.hub && d.kind !== 'user')
|
|
626
|
-
.append('circle')
|
|
627
|
-
.attr('class', 'og-sel-temp')
|
|
628
|
-
.attr('r', (d) => d.r + 3)
|
|
629
|
-
.attr('fill', 'none')
|
|
630
|
-
.attr('stroke', '#ffffff')
|
|
631
|
-
.attr('stroke-width', 1.2);
|
|
632
|
-
}
|
|
633
|
-
refreshLabels();
|
|
634
|
-
};
|
|
635
|
-
applySelectionRef.current(selectedIdRef.current);
|
|
636
|
-
|
|
637
|
-
// ---- requests you've sent ---------------------------------------------
|
|
638
|
-
// A dashed gold ring on every 2nd-degree dot you've asked, in step with
|
|
639
|
-
// the shared list, without rebuilding the scene.
|
|
640
|
-
applyRequestsRef.current = (state) => {
|
|
641
|
-
nodesLayer.selectAll('circle.og-asked').remove();
|
|
642
|
-
node.filter((d) => d.kind === 'd2' && hasRequest(d.connection, state))
|
|
643
|
-
.append('circle')
|
|
644
|
-
.attr('class', 'og-asked')
|
|
645
|
-
.attr('r', (d) => d.r + 2.2)
|
|
646
|
-
.attr('fill', 'none')
|
|
647
|
-
.attr('stroke', '#FFD700')
|
|
648
|
-
.attr('stroke-width', 1)
|
|
649
|
-
.attr('stroke-dasharray', '2 1.6')
|
|
650
|
-
.style('pointer-events', 'none');
|
|
651
|
-
};
|
|
652
|
-
applyRequestsRef.current(requestsRef.current);
|
|
653
|
-
|
|
654
|
-
// ---- fly-to ----------------------------------------------------------
|
|
655
|
-
const nodeById = new Map(renderNodes.map((n) => [n.id, n]));
|
|
656
|
-
focusNodeRef.current = (id) => {
|
|
657
|
-
const target = nodeById.get(id);
|
|
658
|
-
if (!target) return;
|
|
659
|
-
const tx = target.x ?? 0;
|
|
660
|
-
const ty = target.y ?? 0;
|
|
661
|
-
const start = zoomTransform(svgEl);
|
|
662
|
-
const end = zoomIdentity.translate(width / 2, height / 2).scale(2.5).translate(-tx, -ty);
|
|
663
|
-
const t0 = performance.now();
|
|
664
|
-
const flyDur = 650;
|
|
665
|
-
const fly = (t) => {
|
|
666
|
-
const p = Math.min(1, (t - t0) / flyDur);
|
|
667
|
-
const e = 1 - Math.pow(1 - p, 3);
|
|
668
|
-
zoomBehavior.transform(svg, zoomIdentity
|
|
669
|
-
.translate(start.x + (end.x - start.x) * e, start.y + (end.y - start.y) * e)
|
|
670
|
-
.scale(start.k + (end.k - start.k) * e));
|
|
671
|
-
if (p < 1) requestAnimationFrame(fly);
|
|
672
|
-
};
|
|
673
|
-
requestAnimationFrame(fly);
|
|
674
|
-
// rAF is throttled in background tabs; make sure the camera still lands.
|
|
675
|
-
window.setTimeout(() => zoomBehavior.transform(svg, end), flyDur + 80);
|
|
676
|
-
|
|
677
|
-
onSelectRef.current?.(target.connection);
|
|
678
|
-
selectedIdRef.current = id;
|
|
679
|
-
applySelectionRef.current(id);
|
|
680
|
-
|
|
681
|
-
const pulse = nodesLayer.append('circle')
|
|
682
|
-
.attr('cx', tx).attr('cy', ty).attr('r', target.r + 2)
|
|
683
|
-
.attr('fill', 'none').attr('stroke', '#ffffff').attr('stroke-width', 2)
|
|
684
|
-
.attr('opacity', 0.9);
|
|
685
|
-
const p0 = performance.now();
|
|
686
|
-
const pulseDur = 700;
|
|
687
|
-
const puls = (t) => {
|
|
688
|
-
const p = Math.min(1, (t - p0) / pulseDur);
|
|
689
|
-
pulse.attr('r', target.r + 2 + 26 * p).attr('opacity', 0.9 * (1 - p));
|
|
690
|
-
if (p < 1) requestAnimationFrame(puls);
|
|
691
|
-
else pulse.remove();
|
|
692
|
-
};
|
|
693
|
-
requestAnimationFrame(puls);
|
|
694
|
-
window.setTimeout(() => pulse.remove(), pulseDur + 100);
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
return () => {
|
|
698
|
-
sim?.stop();
|
|
699
|
-
applySelectionRef.current = () => {};
|
|
700
|
-
applyRequestsRef.current = () => {};
|
|
701
|
-
focusNodeRef.current = () => {};
|
|
702
|
-
svg.on('.zoom', null);
|
|
703
|
-
svg.on('click', null);
|
|
704
|
-
svg.selectAll('*').remove();
|
|
705
|
-
};
|
|
706
|
-
}, [signature, userName, userImage, isMobile, size, mode]);
|
|
707
|
-
|
|
708
|
-
return (
|
|
709
|
-
<div
|
|
710
|
-
ref={containerRef}
|
|
711
|
-
// The view area is a flex column, so a percentage height resolves
|
|
712
|
-
// against nothing and collapses to zero. Flex into the space instead,
|
|
713
|
-
// the way the other views do.
|
|
714
|
-
style={{ position: 'relative', flex: 1, minHeight: 0, width: '100%' }}
|
|
715
|
-
>
|
|
716
|
-
<svg
|
|
717
|
-
ref={svgRef}
|
|
718
|
-
style={{ display: 'block', height: '100%', width: '100%', touchAction: 'none', userSelect: 'none' }}
|
|
719
|
-
role="img"
|
|
720
|
-
aria-label="Your network as an orbital galaxy — closer orbits are higher-leverage tiers"
|
|
721
|
-
/>
|
|
722
|
-
<div
|
|
723
|
-
ref={tooltipRef}
|
|
724
|
-
aria-hidden
|
|
725
|
-
style={{
|
|
726
|
-
position: 'absolute', zIndex: 20, maxWidth: 190, pointerEvents: 'none',
|
|
727
|
-
padding: '6px 10px', fontSize: 11.5, lineHeight: 1.45, borderRadius: 8,
|
|
728
|
-
background: 'rgba(10,10,26,0.92)', border: '1px solid rgba(255,255,255,0.12)',
|
|
729
|
-
backdropFilter: 'blur(8px)', opacity: 0, transition: 'opacity 150ms ease',
|
|
730
|
-
left: 0, top: 0,
|
|
731
|
-
}}
|
|
732
|
-
/>
|
|
733
|
-
</div>
|
|
734
|
-
);
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
export default OrbitGraph;
|