six-degrees 0.4.10 → 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/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +9 -9
- package/.next/standalone/.next/routes-manifest.json +6 -0
- 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 +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- 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/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- 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 +4 -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 +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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 +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
- 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 +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0bat8nx._.js → [root-of-the-server]__05_3-22._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__13dx4sd._.js} +7 -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]__1tir0vc._.js +8 -0
- 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/_0ea9qwy._.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/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- 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_linkedin-export_12imqxg.js +3 -0
- 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_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 -1
- 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 +3 -0
- 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/{static/chunks/1xptlvtcz6654.css → standalone/.next/static/chunks/1x_ysly42-teq.css} +1 -1
- 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 +235 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/ingest/route.js +40 -1
- package/.next/standalone/app/api/scraper/route.js +12 -2
- package/.next/standalone/app/api/settings/sector-preview/route.js +2 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- 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 +72 -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/DataSection.js +12 -3
- 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/globals.css +11 -0
- 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/Crm.js +683 -0
- package/.next/standalone/app/social/SocialHub.js +374 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +10 -351
- package/.next/standalone/lib/chain-layout.js +105 -35
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- 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-export.js +267 -46
- 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/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- 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 +177 -35
- 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/{standalone/.next/static/chunks/1xptlvtcz6654.css → static/chunks/1x_ysly42-teq.css} +1 -1
- 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 +235 -0
- package/lib/chain-layout.js +105 -35
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- 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-export.js +267 -46
- 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/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/lib/tier-grid.js +99 -0
- package/lib/title-ranking.js +83 -0
- package/package.json +1 -1
- package/scripts/scrape.py +177 -35
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- 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_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.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/_0c_y09g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.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/_1errbuf._.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/07dyt47mp4s9-.js +0 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/standalone/.next/static/chunks/160wjb73ddwqd.js +0 -6
- 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/206rm6j42gl_a.js +0 -1
- package/.next/standalone/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/standalone/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/standalone/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- package/.next/standalone/app/components/OrbitGraph.js +0 -737
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
- package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
- package/.next/static/chunks/160wjb73ddwqd.js +0 -6
- package/.next/static/chunks/1c_n0973t07l0.js +0 -2
- package/.next/static/chunks/1e1n0kkz-psdb.js +0 -1
- package/.next/static/chunks/206rm6j42gl_a.js +0 -1
- package/.next/static/chunks/236f0jtp68j_h.js +0 -1
- package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
- package/.next/static/chunks/2kfhc58df2xyo.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39hr8d-b2ibtw.js +0 -31
- package/.next/static/chunks/3zw8bv7-mc4n6.js +0 -33
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_buildManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → I-cj79yE2IZ8taX2dZGp3}/_ssgManifest.js +0 -0
|
@@ -19,21 +19,24 @@
|
|
|
19
19
|
// circle fills in as the scanner saves (app/page.js, NetworkRefresh). A dot
|
|
20
20
|
// ready for a scan goes to the Scan page with them picked instead: their
|
|
21
21
|
// circle is empty until it's scanned.
|
|
22
|
-
// -
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
22
|
+
// - Chains are drawn inside an opened circle (Blake, 2026-10-02: "once they
|
|
23
|
+
// are clicked on and can view the cluster i want any scanned d2,3,4 or
|
|
24
|
+
// whatever to be show inside with lines out of the d1 leading to the d2s").
|
|
25
|
+
// Anyone you met through this circle whose own circle has been scanned sits
|
|
26
|
+
// outside the tier bands with their cluster round them and a line from the
|
|
27
|
+
// person in the middle; anyone met through *their* circle sits a step beyond,
|
|
28
|
+
// and so on (lib/chain-layout.js chainTree). They aren't bridges on the
|
|
29
|
+
// overview: that shows the people you connected with yourself, and says how
|
|
30
|
+
// many chains lead on from each.
|
|
28
31
|
|
|
29
32
|
import { useState, useRef, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
30
33
|
import Link from 'next/link';
|
|
31
34
|
import { useRouter } from 'next/navigation';
|
|
32
|
-
import { circleIndex } from '../../lib/circle';
|
|
35
|
+
import { circleIndex, MAX_DEGREE } from '../../lib/circle';
|
|
33
36
|
import { localPhoto } from '../../lib/photos';
|
|
34
37
|
import { reachIndex, reachState, circleState, readyByCircle, circleScanCost, scanBars } from '../../lib/reach';
|
|
35
|
-
import {
|
|
36
|
-
import { ringLayout, dotRadius, previewBand, tierBandLayout
|
|
38
|
+
import { reachSegments, RING } from '../../lib/dot-rings';
|
|
39
|
+
import { ringLayout, chainTree, dotRadius, previewBand, tierBandLayout } from '../../lib/chain-layout';
|
|
37
40
|
import { redundancy } from '../../lib/brokerage';
|
|
38
41
|
import { keyFor, score } from '../../lib/separation';
|
|
39
42
|
import { hasRequest } from '../../lib/requests-client';
|
|
@@ -89,6 +92,30 @@ function membersOf(person, index) {
|
|
|
89
92
|
return [...take(index.introduced.get(person.id)), ...take(index.circles.get(person.id))];
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The chains that lead on from `person`: everyone you met through their circle
|
|
97
|
+
* whose own circle has been scanned, then the same from each of those, biggest
|
|
98
|
+
* circle first. `parent` is an index into the list, or -1 for `person`; `step`
|
|
99
|
+
* is how many links out. Nobody already on the trail, nobody twice.
|
|
100
|
+
*/
|
|
101
|
+
function chainsFrom(person, index, skip = [], maxStep = MAX_DEGREE) {
|
|
102
|
+
const out = [];
|
|
103
|
+
const seen = new Set([person.id, ...skip]);
|
|
104
|
+
const size = (row) => (index.circles.get(row.id)?.length || 0) + (index.introduced.get(row.id)?.length || 0);
|
|
105
|
+
const walk = (from, parent, step) => {
|
|
106
|
+
if (step > maxStep) return;
|
|
107
|
+
const next = (index.introduced.get(from.id) || []).filter((row) => size(row) > 0 && !seen.has(row.id))
|
|
108
|
+
.sort((a, b) => size(b) - size(a) || String(a.name || '').localeCompare(String(b.name || '')));
|
|
109
|
+
next.forEach((row) => seen.add(row.id));
|
|
110
|
+
for (const row of next) {
|
|
111
|
+
out.push({ row, parent, step });
|
|
112
|
+
walk(row, out.length - 1, step + 1);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
walk(person, -1, 1);
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
/** The trail to open for a link: the circle they came from, then them. */
|
|
93
120
|
function trailTo(id, rows) {
|
|
94
121
|
if (!id) return [];
|
|
@@ -98,10 +125,11 @@ function trailTo(id, rows) {
|
|
|
98
125
|
return from != null && rows.some((r) => r.id === from) ? [from, id] : [id];
|
|
99
126
|
}
|
|
100
127
|
|
|
101
|
-
export default function ChainView({ connections, degree2 = [], onSelect, userName, fullDegree1, fullDegree2, scanNotes, canScan = true, chainOpen = null, onChainOpened }) {
|
|
128
|
+
export default function ChainView({ connections, degree2 = [], onSelect, userName, fullDegree1, fullDegree2, scanNotes, canScan = true, chainOpen = null, onChainOpened, onCircle }) {
|
|
102
129
|
const containerRef = useRef(null);
|
|
103
130
|
const [hovered, setHovered] = useState(null);
|
|
104
|
-
|
|
131
|
+
// The overview's zoom: null until it's changed, which means "fit" (homeZoom below).
|
|
132
|
+
const [zoomSet, setZoom] = useState(null);
|
|
105
133
|
const [dims, setDims] = useState({ w: 800, h: 600 });
|
|
106
134
|
// Every row, whatever the tier filter: a circle opened from a bridge the
|
|
107
135
|
// filter shows is drawn whole, and so is anyone's circle opened from it.
|
|
@@ -162,6 +190,10 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
162
190
|
trail.push(row);
|
|
163
191
|
}
|
|
164
192
|
const focused = trail.length > 0;
|
|
193
|
+
// Whose circle is open, for the right panel (page.js → Sidebar → CirclePanel).
|
|
194
|
+
const inCircle = trail.length ? trail[trail.length - 1] : null;
|
|
195
|
+
useEffect(() => { onCircle?.(inCircle); }, [inCircle, onCircle]);
|
|
196
|
+
useEffect(() => () => onCircle?.(null), [onCircle]);
|
|
165
197
|
|
|
166
198
|
// Esc goes back one circle, unless it's closing something else (the card's
|
|
167
199
|
// large map) or leaving a text box.
|
|
@@ -191,14 +223,39 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
191
223
|
const cy = dims.h / 2;
|
|
192
224
|
const maxR = Math.min(cx, cy) - 30;
|
|
193
225
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
226
|
+
// Someone met through another bridge's circle isn't a bridge of their own
|
|
227
|
+
// here: they're a link in that bridge's chain, shown inside its circle.
|
|
228
|
+
const withCircle = new Set(bridges.map((b) => b.id));
|
|
229
|
+
const chained = new Map();
|
|
230
|
+
for (const b of bridges) {
|
|
231
|
+
if (withCircle.has(b.unlocked_from_bridge_id)) chained.set(b.unlocked_from_bridge_id, (chained.get(b.unlocked_from_bridge_id) || 0) + 1);
|
|
232
|
+
}
|
|
233
|
+
// (A loop in who-introduced-whom shouldn't happen; if one does, they all stay here.)
|
|
234
|
+
const bridgeById = new Map(bridges.map((b) => [b.id, b]));
|
|
235
|
+
const linked = (b) => {
|
|
236
|
+
const seen = new Set([b.id]);
|
|
237
|
+
for (let from = b.unlocked_from_bridge_id; withCircle.has(from); from = bridgeById.get(from).unlocked_from_bridge_id) {
|
|
238
|
+
if (seen.has(from)) return false;
|
|
239
|
+
seen.add(from);
|
|
240
|
+
}
|
|
241
|
+
return withCircle.has(b.unlocked_from_bridge_id);
|
|
242
|
+
};
|
|
243
|
+
const roots = bridges.filter((b) => !linked(b));
|
|
244
|
+
// Bridges round you: one ring while they fit, more once there are many, each
|
|
245
|
+
// ring far enough from the last for the names under it.
|
|
246
|
+
const bridgeLayout = ringLayout(roots.length, {
|
|
247
|
+
inner: maxR * 0.55, innerMin: maxR * 0.4, outer: maxR * 0.62, spacing: 46, minSpacing: 20, ringGap: 62,
|
|
197
248
|
});
|
|
198
|
-
const bridgePos =
|
|
249
|
+
const bridgePos = roots.map((b, i) => {
|
|
199
250
|
const p = bridgeLayout.points[i];
|
|
200
|
-
return { ...b, x: cx + p.x, y: cy + p.y, angle: p.angle, clusterSize: (bridgeMap[b.id] || []).length };
|
|
251
|
+
return { ...b, x: cx + p.x, y: cy + p.y, angle: p.angle, clusterSize: (bridgeMap[b.id] || []).length, chains: chained.get(b.id) || 0 };
|
|
201
252
|
});
|
|
253
|
+
// More than one ring: the counts under each name wait for a hover.
|
|
254
|
+
const crowded = bridgeLayout.rings.length > 1;
|
|
255
|
+
// Where a hovered circle is previewed, and the zoom at which that still fits the window.
|
|
256
|
+
const band = previewBand(maxR, bridgeLayout.rings);
|
|
257
|
+
const homeZoom = Math.max(0.6, Math.min(1.3, Math.min(cx, cy) / (band.outer + 8)));
|
|
258
|
+
const zoom = zoomSet ?? homeZoom;
|
|
202
259
|
|
|
203
260
|
// Touch rotary dial on the overview: drag a finger round the circle to pick a bridge.
|
|
204
261
|
const bridgeAnglesRef = useRef([]);
|
|
@@ -257,7 +314,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
257
314
|
key={trail.map((r) => r.id).join('>')}
|
|
258
315
|
trail={trail} index={index} reach={reach} dims={dims} requests={requests}
|
|
259
316
|
scanningId={scanningId} still={still} canScan={canScan}
|
|
260
|
-
onOpen={(
|
|
317
|
+
onOpen={(ids) => setPath(trail.map((r) => r.id).concat(ids))}
|
|
261
318
|
onBack={(depth) => setPath(trail.slice(0, depth).map((r) => r.id))}
|
|
262
319
|
onSelect={onSelect}
|
|
263
320
|
/>
|
|
@@ -275,13 +332,15 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
275
332
|
<div ref={containerRef} style={{ flex: 1, background: '#0a0a1a', position: 'relative', overflow: 'hidden' }}>
|
|
276
333
|
<svg width={dims.w} height={dims.h}>
|
|
277
334
|
<g transform={`translate(${cx * (1 - zoom)}, ${cy * (1 - zoom)}) scale(${zoom})`}>
|
|
278
|
-
{/*
|
|
279
|
-
|
|
335
|
+
{/* A guide line for every ring of bridges */}
|
|
336
|
+
{bridgeLayout.rings.map((ring) => (
|
|
337
|
+
<circle key={'ring-' + ring.radius} cx={cx} cy={cy} r={ring.radius} fill="none" stroke={`${DEGREE_COLORS[1]}10`} strokeWidth={1} />
|
|
338
|
+
))}
|
|
280
339
|
|
|
281
340
|
{/* Their circle, previewed on hover, in a wedge that grows rows as it fills */}
|
|
282
341
|
{hovBridge && (
|
|
283
342
|
<CirclePreview bridge={hovBridge} members={membersOf(hovBridge, index)} reach={reach}
|
|
284
|
-
cx={cx} cy={cy} maxR={maxR} still={still} band={
|
|
343
|
+
cx={cx} cy={cy} maxR={maxR} still={still} band={band} />
|
|
285
344
|
)}
|
|
286
345
|
|
|
287
346
|
{/* Bridge nodes — hover to preview their circle, click to open it */}
|
|
@@ -296,24 +355,21 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
296
355
|
onMouseEnter={() => setHovered(b.id)}
|
|
297
356
|
onMouseLeave={() => setHovered(null)}
|
|
298
357
|
style={{ cursor: 'pointer' }}>
|
|
358
|
+
<title>{bridgeTitle(b, ready, scanBars(b, reach))}</title>
|
|
299
359
|
{/* Glow */}
|
|
300
|
-
<circle cx={b.x} cy={b.y} r={isHov ?
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
<title>{bars === 5 ? 'Their whole list is scanned' : `About ${bars * 20}% of their list is scanned`}</title>
|
|
314
|
-
</g>
|
|
315
|
-
);
|
|
316
|
-
})()}
|
|
360
|
+
<circle cx={b.x} cy={b.y} r={isHov ? 21 : 18} fill={`${TIER_COLORS[b.tier]}${isHov ? '14' : '08'}`} />
|
|
361
|
+
{/* The ring, tight on the dot: one bar for each person you added through
|
|
362
|
+
this circle. Orange: scanned since, with a cluster of their own.
|
|
363
|
+
Green: ready for a scan. Nobody yet: one faint line. */}
|
|
364
|
+
<g transform={`translate(${b.x} ${b.y})`} pointerEvents="none">
|
|
365
|
+
{b.chains + ready === 0 && (
|
|
366
|
+
<circle r={(isHov ? 13 : 11) + 3.6} fill="none" stroke={RING.empty} strokeWidth={1} />
|
|
367
|
+
)}
|
|
368
|
+
{reachSegments((isHov ? 13 : 11) + 3.6, { formed: b.chains, ready }).map((seg, i) => (
|
|
369
|
+
<path key={i} d={seg.d} fill="none" strokeLinecap="round" strokeWidth={2}
|
|
370
|
+
stroke={seg.kind === 'ready' ? GREEN : DEGREE_COLORS[2]} />
|
|
371
|
+
))}
|
|
372
|
+
</g>
|
|
317
373
|
{/* Node */}
|
|
318
374
|
<circle cx={b.x} cy={b.y} r={isHov ? 13 : 11}
|
|
319
375
|
fill={localPhoto(b.profile_image_url) ? '#1a1a2e' : TIER_COLORS[b.tier]}
|
|
@@ -331,24 +387,22 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
331
387
|
<text x={b.x} y={b.y + 4} textAnchor="middle" fill={b.tier === 'S' ? '#000' : '#fff'}
|
|
332
388
|
fontSize={11} fontWeight={700}>{b.name?.charAt(0)}</text>
|
|
333
389
|
)}
|
|
334
|
-
{/*
|
|
390
|
+
{/* How many in their circle are waiting for a scan: a small outlined number, top right */}
|
|
335
391
|
{ready > 0 && (
|
|
336
|
-
<g>
|
|
337
|
-
<
|
|
338
|
-
<
|
|
339
|
-
fill="rgba(0,255,136,0.16)" stroke={GREEN} strokeWidth={0.6} />
|
|
340
|
-
<text x={b.x + 7 + (ready > 9 ? 20 : 17)} y={b.y - 13} textAnchor="middle" fill={GREEN} fontSize={7} fontWeight={800}>
|
|
341
|
-
{ready} ready
|
|
342
|
-
</text>
|
|
392
|
+
<g pointerEvents="none">
|
|
393
|
+
<circle cx={b.x + 14} cy={b.y - 14} r={6} fill="#0a0a1a" stroke={GREEN} strokeWidth={1.2} />
|
|
394
|
+
<text x={b.x + 14} y={b.y - 11.4} textAnchor="middle" fill={GREEN} fontSize={ready > 9 ? 6 : 7} fontWeight={800}>{ready > 99 ? '99+' : ready}</text>
|
|
343
395
|
</g>
|
|
344
396
|
)}
|
|
345
397
|
{/* Name + count */}
|
|
346
398
|
<text x={b.x} y={b.y + (isHov ? 24 : 22)} textAnchor="middle" fill="#fff" fontSize={9} fontWeight={600}>
|
|
347
399
|
{b.name?.split(' ')[0]}
|
|
348
400
|
</text>
|
|
349
|
-
|
|
350
|
-
{b.
|
|
351
|
-
|
|
401
|
+
{(!crowded || isHov) && (
|
|
402
|
+
<text x={b.x} y={b.y + (isHov ? 34 : 32)} textAnchor="middle" fill={isHov ? '#bbb' : '#888'} fontSize={7}>
|
|
403
|
+
{b.clusterSize} · {sCount > 0 ? sCount + 'S ' : ''}{aCount > 0 ? aCount + 'A' : ''}
|
|
404
|
+
</text>
|
|
405
|
+
)}
|
|
352
406
|
</g>
|
|
353
407
|
);
|
|
354
408
|
})}
|
|
@@ -363,9 +417,9 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
363
417
|
|
|
364
418
|
{/* Zoom controls, clear of the Galaxy switch below them */}
|
|
365
419
|
<ZoomButtons
|
|
366
|
-
onIn={() => setZoom(z => Math.min(3, z + 0.3))}
|
|
367
|
-
onReset={() => setZoom(
|
|
368
|
-
onOut={() => setZoom(z => Math.max(0.4, z - 0.3))}
|
|
420
|
+
onIn={() => setZoom(z => Math.min(3, (z ?? homeZoom) + 0.3))}
|
|
421
|
+
onReset={() => setZoom(null)}
|
|
422
|
+
onOut={() => setZoom(z => Math.max(0.4, (z ?? homeZoom) - 0.3))}
|
|
369
423
|
/>
|
|
370
424
|
|
|
371
425
|
{/* Depth tracker */}
|
|
@@ -394,7 +448,7 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
394
448
|
))}
|
|
395
449
|
</div>
|
|
396
450
|
<div style={{ fontSize: 9, color: '#555', marginTop: 8 }}>
|
|
397
|
-
{
|
|
451
|
+
{roots.length} bridges{bridges.length > roots.length ? ` + ${bridges.length - roots.length} along their chains` : ''} · {d2S} S + {d2A} A at 2nd degree
|
|
398
452
|
</div>
|
|
399
453
|
{twoWays > 0 && (
|
|
400
454
|
<div style={{ fontSize: 9, color: '#555', marginTop: 4 }}
|
|
@@ -408,6 +462,15 @@ export default function ChainView({ connections, degree2 = [], onSelect, userNam
|
|
|
408
462
|
);
|
|
409
463
|
}
|
|
410
464
|
|
|
465
|
+
/** What a bridge's ring and number mean, in words, for its tooltip. */
|
|
466
|
+
function bridgeTitle(b, ready, bars) {
|
|
467
|
+
const parts = [];
|
|
468
|
+
if (ready) parts.push(`${ready} in ${firstName(b)}’s circle ${ready === 1 ? 'is' : 'are'} ready for a scan`);
|
|
469
|
+
if (b.chains) parts.push(`${b.chains} scanned since, with ${b.chains === 1 ? 'a cluster' : 'clusters'} of their own`);
|
|
470
|
+
if (bars != null) parts.push(bars === 5 ? 'their whole list is scanned' : `about ${bars * 20}% of their list is scanned`);
|
|
471
|
+
return parts.length ? parts.join(' · ') : `${firstName(b)}’s circle`;
|
|
472
|
+
}
|
|
473
|
+
|
|
411
474
|
/** A bridge's circle on hover: the people you reached through it first, then their S, A and B. */
|
|
412
475
|
function CirclePreview({ bridge, members, reach, cx, cy, maxR, still, band }) {
|
|
413
476
|
const shown = members.filter((m) => m.degree === 1 || m.tier === 'S' || m.tier === 'A' || m.tier === 'B');
|
|
@@ -494,8 +557,17 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
494
557
|
const person = trail[trail.length - 1];
|
|
495
558
|
const depth = trail.length; // the person's degree along the chain: a bridge is 1
|
|
496
559
|
const members = useMemo(() => membersOf(person, index), [person, index]);
|
|
497
|
-
|
|
498
|
-
|
|
560
|
+
// The chains that lead on from here. Each link stays a dot in the circle it
|
|
561
|
+
// was met through; the cluster their scan formed sits outside, joined to
|
|
562
|
+
// that dot, so you can see where each path came from.
|
|
563
|
+
const trailKey = trail.map((r) => r.id).join('>');
|
|
564
|
+
const links = useMemo(
|
|
565
|
+
() => chainsFrom(person, index, trailKey.split('>'), Math.max(0, MAX_DEGREE - 1 - depth)),
|
|
566
|
+
[person, index, trailKey, depth],
|
|
567
|
+
);
|
|
568
|
+
const [hover, setHover] = useState(null); // a member's index, 'center', 'L' + a link's index, or null
|
|
569
|
+
// null until it's moved or zoomed: the fit worked out below.
|
|
570
|
+
const [viewSet, setViewSet] = useState(null);
|
|
499
571
|
const drag = useRef(null);
|
|
500
572
|
const svgRef = useRef(null);
|
|
501
573
|
const router = useRouter();
|
|
@@ -503,25 +575,74 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
503
575
|
const cx = dims.w / 2;
|
|
504
576
|
const cy = dims.h / 2;
|
|
505
577
|
const maxR = Math.min(cx, cy) - 30;
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
// Tier bands round them, S nearest, like Network Circle's orbits; with fans
|
|
578
|
+
// Chains fan out to the side on a wide window, below on a tall one; the people
|
|
579
|
+
// you added sit on that side of the circle, so each line is short.
|
|
580
|
+
const toward = dims.w >= dims.h ? 0 : Math.PI / 2;
|
|
581
|
+
const hasChains = links.length > 0;
|
|
582
|
+
// Tier bands round them, S nearest, like Network Circle's orbits; with chains
|
|
512
583
|
// to draw, the bands leave the outside of the circle for them.
|
|
513
584
|
const layout = useMemo(() => tierBandLayout(members, {
|
|
514
|
-
inner: Math.max(62, maxR * 0.3), outer: maxR * (
|
|
585
|
+
inner: Math.max(62, maxR * 0.3), outer: maxR * (hasChains ? 0.72 : 0.94),
|
|
515
586
|
spacing: Math.max(14, Math.min(24, maxR * 0.085)), minSpacing: 4, gap: Math.max(10, maxR * 0.04),
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
587
|
+
start: hasChains ? toward : -Math.PI / 2,
|
|
588
|
+
}), [members, maxR, hasChains, toward]);
|
|
589
|
+
|
|
590
|
+
// Each link's people (anyone met through their circle first, so the next
|
|
591
|
+
// links of the chain are among the dots drawn) and the disc they need. The
|
|
592
|
+
// same size of dot for all of them, so a bigger circle is a bigger disc.
|
|
593
|
+
const sizes = useMemo(() => links.map(({ row }) => {
|
|
594
|
+
const people = membersOf(row, index);
|
|
595
|
+
const l = ringLayout(Math.min(people.length, LINK_DOTS), CLUSTER);
|
|
596
|
+
return { people, radius: (l.rings[l.rings.length - 1]?.radius || LINK_INNER) + 6 };
|
|
597
|
+
}), [links, index]);
|
|
598
|
+
// Where each cluster sits: straight out from its person's dot in the circle;
|
|
599
|
+
// further along a chain, straight out from the cluster before.
|
|
600
|
+
const tree = useMemo(() => {
|
|
601
|
+
if (!links.length) return null;
|
|
602
|
+
const widest = Math.max(...sizes.map((c) => c.radius));
|
|
603
|
+
const at = new Map(members.map((row, i) => [row.id, i]));
|
|
604
|
+
return chainTree(links.map((l) => l.parent), {
|
|
605
|
+
from: layout.edge + layout.spacing + widest + 22, hop: widest * 2 + 46, spacing: widest * 2 + 26, toward,
|
|
606
|
+
angles: links.map((l) => (l.parent < 0 ? layout.points[at.get(l.row.id)]?.angle : undefined)),
|
|
607
|
+
});
|
|
608
|
+
}, [links, sizes, members, layout, toward]);
|
|
609
|
+
// The dots of each cluster, its first ring starting on the side that faces
|
|
610
|
+
// outwards, where the next link of the chain will be.
|
|
611
|
+
const clusters = useMemo(() => sizes.map((c, j) => ({
|
|
612
|
+
...c,
|
|
613
|
+
points: ringLayout(Math.min(c.people.length, LINK_DOTS), { ...CLUSTER, start: tree.points[j].angle }).points,
|
|
614
|
+
})), [sizes, tree]);
|
|
615
|
+
// The dot each path comes from: the link's own dot, in the big circle or in
|
|
616
|
+
// the cluster of whoever you met them through. `back` is who that was.
|
|
617
|
+
const origins = useMemo(() => {
|
|
618
|
+
const at = new Map(members.map((row, i) => [row.id, i]));
|
|
619
|
+
return links.map((link) => {
|
|
620
|
+
if (link.parent < 0) {
|
|
621
|
+
const p = layout.points[at.get(link.row.id)];
|
|
622
|
+
return p ? { x: p.x, y: p.y, r: dotRadius(layout.spacing, link.row.tier) * 1.4, back: { x: 0, y: 0, r: 30 } } : { x: 0, y: 0, r: 30, back: null };
|
|
623
|
+
}
|
|
624
|
+
const hub = tree.points[link.parent];
|
|
625
|
+
const i = clusters[link.parent].people.findIndex((row) => row.id === link.row.id);
|
|
626
|
+
const pt = clusters[link.parent].points[i] || { x: 0, y: 0 };
|
|
627
|
+
return { x: hub.x + pt.x, y: hub.y + pt.y, r: KID_DOT, inCluster: true, back: { x: hub.x, y: hub.y, r: LINK_HUB } };
|
|
628
|
+
});
|
|
629
|
+
}, [links, members, layout, tree, clusters]);
|
|
630
|
+
// Fit the bands and every chain in the window to start with.
|
|
631
|
+
const fit = useMemo(() => {
|
|
632
|
+
if (!tree) return { k: 1, x: 0, y: 0 };
|
|
633
|
+
const box = { l: -layout.edge, r: layout.edge, t: -layout.edge, b: layout.edge };
|
|
634
|
+
tree.points.forEach((p, j) => {
|
|
635
|
+
const r = clusters[j].radius;
|
|
636
|
+
box.l = Math.min(box.l, p.x - r - 30); box.r = Math.max(box.r, p.x + r + 30);
|
|
637
|
+
box.t = Math.min(box.t, p.y - r - 8); box.b = Math.max(box.b, p.y + r + 30);
|
|
638
|
+
});
|
|
639
|
+
// Room at the top for the trail, and a little at every other edge.
|
|
640
|
+
const pad = { side: 70, top: 96, bottom: 30 };
|
|
641
|
+
const k = Math.max(0.35, Math.min(1, (dims.w - pad.side * 2) / (box.r - box.l), (dims.h - pad.top - pad.bottom) / (box.b - box.t)));
|
|
642
|
+
return { k, x: (-(box.l + box.r) / 2) * k, y: (-(box.t + box.b) / 2) * k + (pad.top - pad.bottom) / 2 };
|
|
643
|
+
}, [tree, clusters, layout, dims.w, dims.h]);
|
|
644
|
+
const view = viewSet ?? fit;
|
|
645
|
+
const setView = (next) => setViewSet((v) => (typeof next === 'function' ? next(v ?? fit) : next));
|
|
525
646
|
|
|
526
647
|
// What each dot is: someone you reached (ready, hidden, scanned) or a
|
|
527
648
|
// 2nd-degree row, maybe with a request out; and how big their own circle is.
|
|
@@ -543,11 +664,14 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
543
664
|
const dots = useMemo(() => {
|
|
544
665
|
const few = members.length <= 60;
|
|
545
666
|
const roomy = layout.spacing * k >= 26;
|
|
667
|
+
// The people you added sit side by side, so their names take turns above and below.
|
|
668
|
+
let named = 0;
|
|
546
669
|
return members.map((row, i) => {
|
|
547
670
|
const p = layout.points[i];
|
|
548
671
|
const f = facts[i];
|
|
549
672
|
const r = dotRadius(layout.spacing, row.tier) * (f.reached ? 1.4 : 1);
|
|
550
673
|
const color = f.reached === 'hidden' ? HIDDEN : TIER_COLORS[row.tier] || '#555';
|
|
674
|
+
const above = f.reached ? named++ % 2 === 1 : false;
|
|
551
675
|
return (
|
|
552
676
|
<g key={row.id}>
|
|
553
677
|
{(few || f.reached) && (
|
|
@@ -567,7 +691,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
567
691
|
<circle cx={p.x} cy={p.y} r={r * 2} fill="none" stroke={DEGREE_COLORS[3]} strokeWidth={1 / k} strokeDasharray={`${3 / k} ${2 / k}`} />
|
|
568
692
|
)}
|
|
569
693
|
{(f.reached || (roomy && row.tier === 'S')) && (
|
|
570
|
-
<text x={p.x} y={p.y + r + 10 / k} textAnchor="middle" fontSize={8.5 / k} fontWeight={f.reached ? 700 : 400}
|
|
694
|
+
<text x={p.x} y={above ? p.y - r - 4 / k : p.y + r + 10 / k} textAnchor="middle" fontSize={8.5 / k} fontWeight={f.reached ? 700 : 400}
|
|
571
695
|
fill={f.reached === 'hidden' ? '#999' : f.reached ? GREEN : '#aaa'} pointerEvents="none"
|
|
572
696
|
stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">
|
|
573
697
|
{firstName(row)}
|
|
@@ -586,6 +710,10 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
586
710
|
const wx = (clientX - rect.left - cx - view.x) / k;
|
|
587
711
|
const wy = (clientY - rect.top - cy - view.y) / k;
|
|
588
712
|
if (Math.hypot(wx, wy) <= 30) return 'center';
|
|
713
|
+
if (tree) {
|
|
714
|
+
const j = tree.points.findIndex((p, i) => Math.hypot(p.x - wx, p.y - wy) <= Math.max(16, clusters[i].radius));
|
|
715
|
+
if (j >= 0) return 'L' + j;
|
|
716
|
+
}
|
|
589
717
|
const near = Math.max(layout.spacing * 0.6, 10 / k);
|
|
590
718
|
let best = null;
|
|
591
719
|
let bestD = near;
|
|
@@ -620,6 +748,7 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
620
748
|
if (!d || d.moved) return;
|
|
621
749
|
const hit = hitAt(e.clientX, e.clientY);
|
|
622
750
|
if (hit === 'center') onSelect?.(person);
|
|
751
|
+
else if (typeof hit === 'string') onOpen(chainTo(Number(hit.slice(1))));
|
|
623
752
|
else if (hit != null && goesToScan(hit)) router.push(scanPageFor(members[hit]));
|
|
624
753
|
else if (hit != null) onOpen(members[hit].id);
|
|
625
754
|
};
|
|
@@ -633,6 +762,15 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
633
762
|
zoomBy(Math.exp(-e.deltaY * 0.0015), { x: e.clientX - rect.left - cx, y: e.clientY - rect.top - cy });
|
|
634
763
|
};
|
|
635
764
|
|
|
765
|
+
// From the person in the middle out to link j: the ids to open, nearest first.
|
|
766
|
+
const chainTo = (j) => {
|
|
767
|
+
const ids = [];
|
|
768
|
+
for (let i = j; i >= 0; i = links[i].parent) ids.unshift(links[i].row.id);
|
|
769
|
+
return ids;
|
|
770
|
+
};
|
|
771
|
+
const hovLink = typeof hover === 'string' && hover[0] === 'L' ? Number(hover.slice(1)) : -1;
|
|
772
|
+
const litLinks = hovLink >= 0 ? new Set(chainTo(hovLink)) : null;
|
|
773
|
+
|
|
636
774
|
const ready = facts.filter((f) => f.reached === 'ready').length;
|
|
637
775
|
const hidden = facts.filter((f) => f.reached === 'hidden').length;
|
|
638
776
|
const added = facts.filter((f) => f.reached).length;
|
|
@@ -659,28 +797,70 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
659
797
|
stroke={TIER_COLORS[band.tier] || '#555'} strokeOpacity={0.07}
|
|
660
798
|
strokeWidth={Math.max(1 / k, band.outer - band.inner + layout.spacing)} pointerEvents="none" />
|
|
661
799
|
))}
|
|
662
|
-
{
|
|
663
|
-
{
|
|
664
|
-
|
|
665
|
-
|
|
800
|
+
{dots}
|
|
801
|
+
{/* The chains that lead on from here. Every cluster is the circle of one
|
|
802
|
+
person you added and then scanned; the line to it starts at that
|
|
803
|
+
person's own dot, so you can see where the path came from. */}
|
|
804
|
+
{tree && links.map((link, j) => {
|
|
805
|
+
const p = tree.points[j];
|
|
806
|
+
const c = clusters[j];
|
|
807
|
+
const row = link.row;
|
|
808
|
+
const lit = litLinks?.has(row.id);
|
|
809
|
+
const color = DEGREE_COLORS[depth + link.step] || DEGREE_COLORS[6];
|
|
666
810
|
return (
|
|
667
|
-
<g key={'
|
|
668
|
-
{
|
|
669
|
-
|
|
670
|
-
|
|
811
|
+
<g key={'lk-' + row.id} transform={`translate(${p.x} ${p.y})`} pointerEvents="none"
|
|
812
|
+
opacity={litLinks && !lit ? 0.45 : 1}>
|
|
813
|
+
<circle r={c.radius} fill="#0a0a1a" fillOpacity={0.9} stroke={color} strokeOpacity={lit ? 0.7 : 0.25} strokeWidth={1 / k} />
|
|
814
|
+
{/* Their people, each joined to the middle of the cluster */}
|
|
815
|
+
{clusterPaths(c).map(([tier, { spokes }]) => (
|
|
816
|
+
<path key={'s' + tier} d={spokes} fill="none" stroke={TIER_COLORS[tier] || '#555'}
|
|
817
|
+
strokeWidth={0.7} strokeOpacity={lit ? 0.6 : 0.38} />
|
|
671
818
|
))}
|
|
672
|
-
{
|
|
673
|
-
<
|
|
674
|
-
fill={TIER_COLORS[fan.people[j]?.tier] || '#555'} fillOpacity={0.75} />
|
|
819
|
+
{clusterPaths(c).map(([tier, { dots: d }]) => (
|
|
820
|
+
<path key={tier} d={d} fill={TIER_COLORS[tier] || '#555'} fillOpacity={lit ? 1 : 0.9} />
|
|
675
821
|
))}
|
|
676
|
-
{
|
|
677
|
-
|
|
678
|
-
|
|
822
|
+
<circle r={LINK_HUB} fill={color} stroke={hovLink === j ? '#fff' : '#0a0a1a'} strokeWidth={1.5} />
|
|
823
|
+
{row.id === scanningId && (
|
|
824
|
+
<circle r={c.radius + 4} fill="none" stroke={DEGREE_COLORS[3]} strokeWidth={1 / k} strokeDasharray={`${3 / k} ${2 / k}`} />
|
|
825
|
+
)}
|
|
826
|
+
<text y={c.radius + 12 / k} textAnchor="middle" fill="#fff" fontSize={10 / k} fontWeight={700}
|
|
827
|
+
stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">{firstName(row)}’s circle</text>
|
|
828
|
+
<text y={c.radius + 23 / k} textAnchor="middle" fill={color} fontSize={8 / k}
|
|
829
|
+
stroke="#0a0a1a" strokeWidth={2.4 / k} paintOrder="stroke">
|
|
830
|
+
{c.people.length.toLocaleString('en-US')} · {ordinal(depth + link.step + 1)} degree
|
|
831
|
+
</text>
|
|
832
|
+
</g>
|
|
833
|
+
);
|
|
834
|
+
})}
|
|
835
|
+
{/* The paths, on top of the clusters they cross: from whoever you met
|
|
836
|
+
them through, to their own dot, and on out to the cluster their scan formed. */}
|
|
837
|
+
{tree && links.map((link, j) => {
|
|
838
|
+
const o = origins[j];
|
|
839
|
+
const p = tree.points[j];
|
|
840
|
+
const lit = litLinks?.has(link.row.id);
|
|
841
|
+
const color = DEGREE_COLORS[depth + link.step] || DEGREE_COLORS[6];
|
|
842
|
+
const seg = (a, ra, b, rb, key) => {
|
|
843
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
844
|
+
if (len <= ra + rb) return null;
|
|
845
|
+
const ux = (b.x - a.x) / len;
|
|
846
|
+
const uy = (b.y - a.y) / len;
|
|
847
|
+
return (
|
|
848
|
+
<line key={key} x1={a.x + ux * ra} y1={a.y + uy * ra} x2={b.x - ux * rb} y2={b.y - uy * rb}
|
|
849
|
+
stroke={color} strokeLinecap="round" strokeWidth={(lit ? 2.6 : 1.8) / k}
|
|
850
|
+
strokeOpacity={litLinks && !lit ? 0.3 : lit ? 1 : 0.85} />
|
|
851
|
+
);
|
|
852
|
+
};
|
|
853
|
+
return (
|
|
854
|
+
<g key={'ln-' + link.row.id} pointerEvents="none">
|
|
855
|
+
{o.back && seg(o.back, o.back.r, o, o.r + 1, 'a')}
|
|
856
|
+
{seg(o, o.r + 1, p, LINK_HUB + 1, 'b')}
|
|
857
|
+
{/* Their own dot inside the cluster they were met through */}
|
|
858
|
+
{o.inCluster && (
|
|
859
|
+
<circle cx={o.x} cy={o.y} r={o.r} fill={TIER_COLORS[link.row.tier] || '#555'} stroke={GREEN} strokeWidth={1.4} />
|
|
679
860
|
)}
|
|
680
861
|
</g>
|
|
681
862
|
);
|
|
682
863
|
})}
|
|
683
|
-
{dots}
|
|
684
864
|
{/* The one under the pointer, on top */}
|
|
685
865
|
{hp && (
|
|
686
866
|
<>
|
|
@@ -708,14 +888,23 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
708
888
|
<Tip x={cx + view.x + hp.x * k} y={cy + view.y + hp.y * k} w={dims.w}
|
|
709
889
|
lines={tipFor(hovered, facts[hover], scanningId, depth, goesToScan(hover))} accent={facts[hover].reached ? GREEN : TIER_COLORS[hovered.tier]} />
|
|
710
890
|
)}
|
|
891
|
+
{hovLink >= 0 && (
|
|
892
|
+
<Tip x={cx + view.x + tree.points[hovLink].x * k} y={cy + view.y + (tree.points[hovLink].y - clusters[hovLink].radius) * k} w={dims.w}
|
|
893
|
+
lines={[`${links[hovLink].row.name}’s circle`,
|
|
894
|
+
`${clusters[hovLink].people.length.toLocaleString('en-US')} people · ${ordinal(depth + links[hovLink].step + 1)} degree`,
|
|
895
|
+
`You met ${firstName(links[hovLink].row)} through ${firstName(links[hovLink].parent >= 0 ? links[links[hovLink].parent].row : person)}’s circle, then scanned`,
|
|
896
|
+
'Click to open it']}
|
|
897
|
+
accent={DEGREE_COLORS[depth + links[hovLink].step] || DEGREE_COLORS[6]} />
|
|
898
|
+
)}
|
|
711
899
|
{hover === 'center' && (
|
|
712
900
|
<Tip x={cx + view.x} y={cy + view.y - 28 * k} w={dims.w} lines={[person.name, 'Open their card']} accent={TIER_COLORS[person.tier]} />
|
|
713
901
|
)}
|
|
714
902
|
</svg>
|
|
715
903
|
|
|
716
904
|
{/* The way back: one circle at a time, or straight to any on the trail.
|
|
717
|
-
Clear of the Filters tab at the left edge
|
|
718
|
-
|
|
905
|
+
Clear of the Filters tab at the left edge, and under the notch that
|
|
906
|
+
holds the view buttons at the top of the map. */}
|
|
907
|
+
<div style={{ position: 'absolute', top: 54, left: 64, right: 64, pointerEvents: 'none', display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'flex-start' }}>
|
|
719
908
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', pointerEvents: 'auto' }}>
|
|
720
909
|
<button type="button" onClick={() => onBack(depth - 1)} style={{
|
|
721
910
|
padding: '8px 16px', borderRadius: 8, border: 'none', cursor: 'pointer',
|
|
@@ -778,11 +967,36 @@ function CircleFocus({ trail, index, reach, dims, requests, scanningId, still, c
|
|
|
778
967
|
</div>
|
|
779
968
|
)}
|
|
780
969
|
|
|
781
|
-
<ZoomButtons onIn={() => zoomBy(1.35)} onReset={() =>
|
|
970
|
+
<ZoomButtons onIn={() => zoomBy(1.35)} onReset={() => setViewSet(null)} onOut={() => zoomBy(1 / 1.35)} />
|
|
782
971
|
</>
|
|
783
972
|
);
|
|
784
973
|
}
|
|
785
974
|
|
|
975
|
+
// A link's cluster draws at most this many of their people; the count beside it is the whole circle.
|
|
976
|
+
const LINK_DOTS = 290;
|
|
977
|
+
// The middle of a cluster, where its first ring starts, and a link's own dot inside one.
|
|
978
|
+
const LINK_HUB = 5;
|
|
979
|
+
const LINK_INNER = 15;
|
|
980
|
+
const KID_DOT = 4.6;
|
|
981
|
+
const CLUSTER = { inner: LINK_INNER, outer: Infinity, spacing: 7.5, minSpacing: 7.5 };
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* A cluster as two paths per tier, however many people: the dots, and a line
|
|
985
|
+
* from the middle of the cluster out to each of them.
|
|
986
|
+
*/
|
|
987
|
+
function clusterPaths(cluster) {
|
|
988
|
+
const paths = {};
|
|
989
|
+
cluster.points.forEach((pt, i) => {
|
|
990
|
+
const tier = cluster.people[i]?.tier || 'D';
|
|
991
|
+
const q = tier === 'S' ? 3.1 : tier === 'A' ? 2.8 : 2.5;
|
|
992
|
+
const len = Math.hypot(pt.x, pt.y) || 1;
|
|
993
|
+
const at = (paths[tier] ||= { dots: '', spokes: '' });
|
|
994
|
+
at.dots += `M${(pt.x - q).toFixed(1)},${pt.y.toFixed(1)}a${q},${q} 0 1,0 ${2 * q},0a${q},${q} 0 1,0 ${-2 * q},0`;
|
|
995
|
+
at.spokes += `M${((pt.x / len) * LINK_HUB).toFixed(1)},${((pt.y / len) * LINK_HUB).toFixed(1)}L${pt.x.toFixed(1)},${pt.y.toFixed(1)}`;
|
|
996
|
+
});
|
|
997
|
+
return Object.entries(paths);
|
|
998
|
+
}
|
|
999
|
+
|
|
786
1000
|
/** The lines of a dot's tooltip. `toScan`: a click goes to the Scan page with them picked. */
|
|
787
1001
|
function tipFor(row, f, scanningId, depth, toScan) {
|
|
788
1002
|
const first = firstName(row);
|