six-degrees 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +14 -14
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
- 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 +2 -2
- 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.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- 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.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +3 -3
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup/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 +3 -3
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +3 -3
- 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 +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__03ydxxh._.js → [root-of-the-server]__20tmem7._.js} +1 -1
- 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/_1mxn_lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +21 -7
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +6 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/10784r7w7t5gw.js +18 -0
- package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
- package/.next/standalone/.next/static/chunks/3ol1xux1-ofm4.js +6 -0
- package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
- package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/.next/standalone/CHANGELOG.md +59 -0
- package/.next/standalone/app/api/admin-delete/route.js +3 -0
- package/.next/standalone/app/api/ingest/route.js +13 -0
- package/.next/standalone/app/components/ChainView.js +68 -14
- package/.next/standalone/app/components/FilterPanel.js +37 -1
- package/.next/standalone/app/components/ForceGraph.js +44 -5
- package/.next/standalone/app/components/LinkedInLimits.js +20 -3
- package/.next/standalone/app/components/Sidebar.js +37 -0
- package/.next/standalone/app/components/views.js +7 -5
- package/.next/standalone/app/favicon.ico +0 -0
- package/.next/standalone/app/page.js +24 -7
- package/.next/standalone/app/paths/page.js +8 -1
- package/.next/standalone/db/schema.js +14 -0
- package/.next/standalone/lib/brokerage.js +94 -0
- package/.next/standalone/lib/chain-layout.js +89 -0
- package/.next/standalone/lib/circle.js +23 -4
- package/.next/standalone/lib/degrees.js +30 -0
- package/.next/standalone/lib/ingest.js +3 -3
- package/.next/standalone/lib/linkedin-limits.js +9 -4
- package/.next/standalone/lib/search-risk.js +38 -0
- package/.next/standalone/lib/ties.js +23 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +40 -12
- package/.next/static/chunks/10784r7w7t5gw.js +18 -0
- package/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
- package/.next/static/chunks/3ol1xux1-ofm4.js +6 -0
- package/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
- package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/CHANGELOG.md +59 -0
- package/db/schema.js +14 -0
- package/lib/brokerage.js +94 -0
- package/lib/chain-layout.js +89 -0
- package/lib/circle.js +23 -4
- package/lib/degrees.js +30 -0
- package/lib/ingest.js +3 -3
- package/lib/linkedin-limits.js +9 -4
- package/lib/search-risk.js +38 -0
- package/lib/ties.js +23 -0
- package/package.json +1 -1
- package/scripts/scrape.py +40 -12
- package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +0 -18
- package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +0 -2
- package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +0 -2
- package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- package/.next/static/chunks/0w76iraw7ij3k.js +0 -18
- package/.next/static/chunks/135n87ydxjrra.js +0 -2
- package/.next/static/chunks/3qh78g2aqhprd.js +0 -2
- package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
- /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → LlDgA9g8ToBkDLlo9Qtov}/_buildManifest.js +0 -0
- /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → LlDgA9g8ToBkDLlo9Qtov}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → LlDgA9g8ToBkDLlo9Qtov}/_ssgManifest.js +0 -0
package/lib/chain-layout.js
CHANGED
|
@@ -137,3 +137,92 @@ export function dotRadius(spacing, tier) {
|
|
|
137
137
|
const scale = tier === 'S' ? 1 : tier === 'A' ? 0.86 : tier === 'B' ? 0.72 : 0.6;
|
|
138
138
|
return Math.max(1.4, Math.min(7, spacing * 0.42 * scale));
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Where a bridge's circle is previewed on hover: a band beyond every ring of
|
|
143
|
+
* bridges. With many bridges the overview grows extra rings, past where the
|
|
144
|
+
* preview used to start, and it drew over them. `clear` leaves room for a
|
|
145
|
+
* hovered bridge's dot (20) and its name under it.
|
|
146
|
+
*/
|
|
147
|
+
export function previewBand(maxR, bridgeRings = [], clear = 34) {
|
|
148
|
+
const edge = Math.max(0, ...bridgeRings.map((r) => r.radius));
|
|
149
|
+
const inner = Math.max(maxR * 0.66, edge ? edge + clear : 0);
|
|
150
|
+
return { inner, outer: Math.max(maxR * 0.8, inner + maxR * 0.14) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const TIER_BANDS = ['S', 'A', 'B', 'C', 'D'];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* An opened circle's people in tier bands round the person: S nearest, then A,
|
|
157
|
+
* B, C and D, with a gap between bands like Network Circle's tier orbits, so a
|
|
158
|
+
* big circle reads as layers instead of one clump. Points come back in the
|
|
159
|
+
* order of `rows`, each with `slot`: the angle its ring gives each dot. The
|
|
160
|
+
* spacing closes up, down to `minSpacing`, until every band fits inside
|
|
161
|
+
* `outer`; past that the bands carry on outwards and zooming out shows them.
|
|
162
|
+
*
|
|
163
|
+
* @returns {{ spacing: number, edge: number, bands: { tier: string, inner: number, outer: number }[],
|
|
164
|
+
* points: { x: number, y: number, angle: number, slot: number }[] }}
|
|
165
|
+
*/
|
|
166
|
+
export function tierBandLayout(rows, { inner, outer, spacing, minSpacing = spacing / 3, gap = spacing }) {
|
|
167
|
+
const groups = TIER_BANDS.map((tier) => ({ tier, idx: [] }));
|
|
168
|
+
rows.forEach((row, i) => {
|
|
169
|
+
const t = TIER_BANDS.indexOf(row?.tier);
|
|
170
|
+
groups[t < 0 ? 4 : t].idx.push(i); // no tier: with D
|
|
171
|
+
});
|
|
172
|
+
const used = groups.filter((g) => g.idx.length);
|
|
173
|
+
const place = (s) => {
|
|
174
|
+
const points = new Array(rows.length);
|
|
175
|
+
const bands = [];
|
|
176
|
+
let r = inner;
|
|
177
|
+
for (const g of used) {
|
|
178
|
+
const l = ringLayout(g.idx.length, { inner: r, outer: Infinity, spacing: s, minSpacing: s });
|
|
179
|
+
g.idx.forEach((rowIndex, j) => {
|
|
180
|
+
const p = l.points[j];
|
|
181
|
+
points[rowIndex] = { x: p.x, y: p.y, angle: p.angle, slot: TAU / l.rings[p.ring].count };
|
|
182
|
+
});
|
|
183
|
+
const edge = Math.max(r, ...l.rings.map((x) => x.radius));
|
|
184
|
+
bands.push({ tier: g.tier, inner: r, outer: edge });
|
|
185
|
+
r = edge + gap * (s / spacing);
|
|
186
|
+
}
|
|
187
|
+
return { spacing: s, points, bands, edge: bands.length ? bands[bands.length - 1].outer : inner };
|
|
188
|
+
};
|
|
189
|
+
let best = place(spacing);
|
|
190
|
+
if (best.edge <= outer) return best;
|
|
191
|
+
let lo = minSpacing;
|
|
192
|
+
let hi = spacing;
|
|
193
|
+
best = place(lo);
|
|
194
|
+
if (best.edge > outer) return best;
|
|
195
|
+
for (let i = 0; i < 20; i++) {
|
|
196
|
+
const mid = (lo + hi) / 2;
|
|
197
|
+
const tryMid = place(mid);
|
|
198
|
+
if (tryMid.edge <= outer) { lo = mid; best = tryMid; } else hi = mid;
|
|
199
|
+
}
|
|
200
|
+
return best;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The people behind each person in an opened circle: their own circle, drawn
|
|
205
|
+
* as a fan of small dots beyond `from`, centred on that person's angle and no
|
|
206
|
+
* wider than their slot (at most a sixth of the circle), so each fan sits
|
|
207
|
+
* behind its own person. At most `cap` dots a fan and about `total` in all, so
|
|
208
|
+
* a big network stays quick; `more` is how many weren't drawn.
|
|
209
|
+
*
|
|
210
|
+
* @param {{ angle: number, slot: number, count: number }[]} anchors
|
|
211
|
+
* @returns {({ points: { x: number, y: number }[], more: number } | null)[]}
|
|
212
|
+
*/
|
|
213
|
+
export function outerFans(anchors, { from, spacing = 7, cap = 40, total = 1200 } = {}) {
|
|
214
|
+
const fans = anchors.filter((a) => a?.count > 0).length;
|
|
215
|
+
const each = Math.max(6, Math.min(cap, Math.floor(total / Math.max(1, fans))));
|
|
216
|
+
return anchors.map((a) => {
|
|
217
|
+
if (!(a?.count > 0)) return null;
|
|
218
|
+
const n = Math.min(a.count, each);
|
|
219
|
+
// Only as wide as the dots need at `spacing`, never wider than their slot:
|
|
220
|
+
// more rows once it's full, so a small fan stays a tight cluster.
|
|
221
|
+
const room = Math.min(a.slot || TAU, TAU / 6);
|
|
222
|
+
const width = Math.min(room, Math.max((spacing * 3) / from, (n * spacing) / from));
|
|
223
|
+
const l = ringLayout(n, {
|
|
224
|
+
inner: from, outer: Infinity, spacing, minSpacing: spacing, start: a.angle - width / 2, sweep: width,
|
|
225
|
+
});
|
|
226
|
+
return { points: l.points.map(({ x, y }) => ({ x, y })), more: a.count - n };
|
|
227
|
+
});
|
|
228
|
+
}
|
package/lib/circle.js
CHANGED
|
@@ -137,17 +137,34 @@ export function circleLayout(rings = [], size = 240) {
|
|
|
137
137
|
for (let k = 0; k < R; k++) {
|
|
138
138
|
const radius = radiusOf(k);
|
|
139
139
|
const people = rings[k].people;
|
|
140
|
-
|
|
140
|
+
// More than their slice holds without the dots touching: rows, neighbours
|
|
141
|
+
// alternating between them, so a big circle never draws as a solid line.
|
|
142
|
+
// Each keeps its angle behind who they hang off. Rows go out towards the
|
|
143
|
+
// next ring, or for the outermost, in towards the one before (less than
|
|
144
|
+
// half way either side, so two rings' rows never meet).
|
|
145
|
+
const gap = Math.max(2.4, size / 100);
|
|
146
|
+
const inward = k === R - 1 && R > 1;
|
|
147
|
+
const band = (inward ? radius - radiusOf(k - 1) : (k + 1 < R ? radiusOf(k + 1) : half * 0.97) - radius) * 0.45;
|
|
148
|
+
const maxRows = Math.max(1, Math.floor(band / gap) + 1);
|
|
149
|
+
const rowGap = maxRows > 1 ? (inward ? -1 : 1) * Math.min(gap * 1.2, band / (maxRows - 1)) : 0;
|
|
150
|
+
const rowsFor = (count, arc) => clamp(Math.ceil((count * gap) / Math.max(1e-9, radius * arc)), 1, maxRows);
|
|
151
|
+
let rows = 1;
|
|
152
|
+
let room = Infinity; // the least room any one dot gets along its row
|
|
141
153
|
const items = [];
|
|
142
154
|
for (const [parentId, group] of byParent[k]) {
|
|
143
155
|
const [a0, a1] = k === 0 ? [START, START + 2 * Math.PI] : span.get(parentId) || [START, START + 2 * Math.PI];
|
|
144
156
|
const total = group.reduce((s, p) => s + weight.get(p), 0);
|
|
157
|
+
const groupRows = rowsFor(group.length, a1 - a0);
|
|
158
|
+
rows = Math.max(rows, groupRows);
|
|
159
|
+
room = Math.min(room, (radius * (a1 - a0) * groupRows) / Math.max(1, group.length));
|
|
160
|
+
let n = 0;
|
|
145
161
|
let a = a0;
|
|
146
162
|
for (const p of group) {
|
|
147
163
|
const w = ((a1 - a0) * weight.get(p)) / total;
|
|
148
164
|
const angle = a + w / 2;
|
|
149
|
-
const
|
|
150
|
-
const
|
|
165
|
+
const r = radius + (n++ % groupRows) * rowGap;
|
|
166
|
+
const x = half + r * Math.cos(angle);
|
|
167
|
+
const y = half + r * Math.sin(angle);
|
|
151
168
|
items.push({ ...p, angle, x, y });
|
|
152
169
|
if (p.state === 'connected') span.set(p.row.id, [a, a + w]);
|
|
153
170
|
a += w;
|
|
@@ -163,7 +180,9 @@ export function circleLayout(rings = [], size = 240) {
|
|
|
163
180
|
spokes.push({ x1: it.x, y1: it.y, x2: half + nextR * Math.cos(mid), y2: half + nextR * Math.sin(mid) });
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
|
|
183
|
+
// Sized by the most crowded slice, and never wider than the gap between rows.
|
|
184
|
+
const dotR = clamp(Math.min(room * 0.32, rows > 1 ? Math.abs(rowGap) * 0.42 : Infinity), 0.8, size / 60);
|
|
185
|
+
out.push({ degree: rings[k].degree, radius, dotR, rows, items });
|
|
167
186
|
}
|
|
168
187
|
return { size, center: { x: half, y: half }, rings: out, spokes };
|
|
169
188
|
}
|
package/lib/degrees.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Everyone the app knows, by degree, for Network Circle's degree filter.
|
|
2
|
+
//
|
|
3
|
+
// 1st: your connections. 2nd: anyone in a scanned circle, including the people
|
|
4
|
+
// behind someone you added, since you're connected to them now. 3rd: anyone a
|
|
5
|
+
// company scan found who is neither. Each person counts once, at the nearest
|
|
6
|
+
// degree they're found (by profile, lib/separation.js keyFor), whichever
|
|
7
|
+
// circle or scan found them first.
|
|
8
|
+
|
|
9
|
+
import { keyFor } from './separation.js';
|
|
10
|
+
|
|
11
|
+
export function peopleByDegree(degree1 = [], degree2 = [], degree3 = []) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const take = (rows, degree) => {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const row of rows || []) {
|
|
16
|
+
const key = keyFor(row);
|
|
17
|
+
if (seen.has(key)) continue;
|
|
18
|
+
seen.add(key);
|
|
19
|
+
out.push(row.degree === degree ? row : { ...row, degree });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
};
|
|
23
|
+
return { 1: take(degree1, 1), 2: take(degree2, 2), 3: take(degree3, 3) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function tierCountsOf(rows = []) {
|
|
27
|
+
const counts = {};
|
|
28
|
+
for (const row of rows) counts[row.tier] = (counts[row.tier] || 0) + 1;
|
|
29
|
+
return counts;
|
|
30
|
+
}
|
package/lib/ingest.js
CHANGED
|
@@ -31,12 +31,12 @@ export function uniqueByProfile(records) {
|
|
|
31
31
|
*/
|
|
32
32
|
export function splitAlreadyConnected(records, firstDegreeUrls) {
|
|
33
33
|
const keep = [];
|
|
34
|
-
|
|
34
|
+
const connected = []; // their profile URLs: ties between your connections (lib/ties.js)
|
|
35
35
|
for (const r of records) {
|
|
36
|
-
if (firstDegreeUrls.has(r.profile_url))
|
|
36
|
+
if (firstDegreeUrls.has(r.profile_url)) connected.push(r.profile_url);
|
|
37
37
|
else keep.push(r);
|
|
38
38
|
}
|
|
39
|
-
return { keep, alreadyConnected };
|
|
39
|
+
return { keep, alreadyConnected: connected.length, connected };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const MONTHS = ['january', 'february', 'march', 'april', 'may', 'june', 'july',
|
package/lib/linkedin-limits.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// changes here — the budget, and lifting a cooldown.
|
|
6
6
|
//
|
|
7
7
|
// The budget is searches (a day and a month) and profile views (a day). Profile
|
|
8
|
-
// views
|
|
9
|
-
//
|
|
8
|
+
// views got an account restricted (TRAPS §16), so their cap has no "no limit",
|
|
9
|
+
// and the scanner also keeps any two a minute apart. Searches did too, at 373 in
|
|
10
|
+
// 24 hours: the page asks before a risky search budget (lib/search-risk.js).
|
|
10
11
|
//
|
|
11
12
|
// They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
|
|
12
13
|
|
|
@@ -58,14 +59,18 @@ export function nextMonthStartPacific(ms = Date.now()) {
|
|
|
58
59
|
return monthStartPacific(start + 40 * DAY * 1000);
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const profileChoice = (n) => PROFILE_CHOICES.filter((c) => c <= n).pop() ?? PROFILE_CHOICES[0];
|
|
63
|
+
|
|
61
64
|
export function readLimits(dir) {
|
|
62
65
|
const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
|
|
63
66
|
const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
|
|
64
67
|
return {
|
|
65
68
|
daily: num(raw.daily, DEFAULT_LIMITS.daily),
|
|
66
69
|
monthly: num(raw.monthly, DEFAULT_LIMITS.monthly),
|
|
67
|
-
// 0 is "no limit" for searches; for profile views the scanner reads it as the
|
|
68
|
-
|
|
70
|
+
// 0 is "no limit" for searches; for profile views the scanner reads it as the
|
|
71
|
+
// default. A number typed in by hand counts as the largest choice under it,
|
|
72
|
+
// as the scanner reads it (scrape.py search_limits).
|
|
73
|
+
profiles: profileChoice(num(raw.profiles, 0) || DEFAULT_LIMITS.profiles),
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// How much searching is asking for trouble, for the Scan page's budget picker.
|
|
2
|
+
// No Node imports, so the page and the tests share it (lib/linkedin-limits.js
|
|
3
|
+
// reads files and can't load in the browser).
|
|
4
|
+
//
|
|
5
|
+
// The number behind it (TRAPS §16): on 2026-09-28 a real account was restricted
|
|
6
|
+
// after 373 searches in 24 hours, run back to back, with the daily budget raised
|
|
7
|
+
// to 500 and no monthly cap. It opened 7 profiles that day, so searches alone
|
|
8
|
+
// were enough.
|
|
9
|
+
|
|
10
|
+
export const RISKY_DAILY = 100; // above this, the picker asks first
|
|
11
|
+
export const RESTRICTED_AT = 373; // searches in 24 hours when that account was restricted
|
|
12
|
+
export const SAFE_LIMITS = { daily: 50, monthly: 250 };
|
|
13
|
+
|
|
14
|
+
export const riskyDaily = (n) => Number(n) > RISKY_DAILY;
|
|
15
|
+
export const riskyLimits = ({ daily, monthly } = {}) => riskyDaily(daily) || monthly === 0;
|
|
16
|
+
|
|
17
|
+
/** What the picker asks before a change that raises the risk; null when there's nothing to ask. */
|
|
18
|
+
export function limitQuestion(from = {}, to = {}) {
|
|
19
|
+
if (riskyDaily(to.daily) && Number(to.daily) > Number(from.daily)) {
|
|
20
|
+
return `${to.daily} searches a day is more than LinkedIn has put up with. A real account was `
|
|
21
|
+
+ `restricted after ${RESTRICTED_AT} searches in 24 hours, run back to back. The default is `
|
|
22
|
+
+ `${SAFE_LIMITS.daily} a day.\n\nSet ${to.daily} a day anyway?`;
|
|
23
|
+
}
|
|
24
|
+
if (to.monthly === 0 && from.monthly !== 0) {
|
|
25
|
+
return 'With no monthly cap, only the daily budget stops a scan. A real account was restricted '
|
|
26
|
+
+ `after ${RESTRICTED_AT} searches in 24 hours with no monthly cap set.\n\nTurn the monthly cap off anyway?`;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The note under the picker while the budget is risky; null when it isn't. */
|
|
32
|
+
export function limitNote({ daily, monthly } = {}) {
|
|
33
|
+
if (!riskyLimits({ daily, monthly })) return null;
|
|
34
|
+
const parts = [`A real account was restricted after ${RESTRICTED_AT} searches in 24 hours.`];
|
|
35
|
+
if (riskyDaily(daily)) parts.push(`${daily} a day can use up a free account's month in a day or two.`);
|
|
36
|
+
if (monthly === 0) parts.push('With no monthly cap, nothing but the daily budget stops a long run.');
|
|
37
|
+
return parts.join(' ');
|
|
38
|
+
}
|
package/lib/ties.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ties between your own connections: two people you know who know each other.
|
|
2
|
+
//
|
|
3
|
+
// A circle scan reads everyone in someone's list, your own connections among
|
|
4
|
+
// them, and the app sets those aside rather than saving them as 2nd degree
|
|
5
|
+
// (lib/ingest.js splitAlreadyConnected). Each one is a tie between that
|
|
6
|
+
// person and the one whose circle it is. Kept in connection_ties (db/schema.js),
|
|
7
|
+
// for clustering and communities later; nothing reads them yet.
|
|
8
|
+
|
|
9
|
+
/** The pairs to keep: the circle's person with each of your connections in it, each pair once, smaller URL first. */
|
|
10
|
+
export function tiePairs(ownerUrl, urls = []) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const out = [];
|
|
13
|
+
if (!ownerUrl) return out;
|
|
14
|
+
for (const url of urls) {
|
|
15
|
+
if (!url || url === ownerUrl) continue;
|
|
16
|
+
const [a, b] = url < ownerUrl ? [url, ownerUrl] : [ownerUrl, url];
|
|
17
|
+
const key = `${a} ${b}`;
|
|
18
|
+
if (seen.has(key)) continue;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
out.push({ a_url: a, b_url: b });
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blake Burford",
|
package/scripts/scrape.py
CHANGED
|
@@ -1504,16 +1504,29 @@ def take_profile_view(now=None):
|
|
|
1504
1504
|
with _Locked("linkedin-activity"):
|
|
1505
1505
|
now = now if now is not None else time.time()
|
|
1506
1506
|
data = _read_activity()
|
|
1507
|
-
|
|
1508
|
-
if
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1507
|
+
wait = _profile_wait(data, now)
|
|
1508
|
+
if wait == 0:
|
|
1509
|
+
_charge(data, "profiles", 1, now)
|
|
1510
|
+
return wait
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def profile_wait(now=None):
|
|
1514
|
+
"""take_profile_view's answer, writing nothing down: 0, seconds to wait, or None."""
|
|
1515
|
+
with _Locked("linkedin-activity"):
|
|
1516
|
+
now = now if now is not None else time.time()
|
|
1517
|
+
return _profile_wait(_read_activity(), now)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def _profile_wait(data, now):
|
|
1521
|
+
views = [t for t in data["profiles"] if t > now - DAY_SECONDS]
|
|
1522
|
+
if len(views) >= search_limits()["profiles"]:
|
|
1523
|
+
return None
|
|
1524
|
+
# A time ahead of this clock (another computer's, imported) is left
|
|
1525
|
+
# out of the gap: waiting for it could never end.
|
|
1526
|
+
last = max((t for t in views if t <= now), default=None)
|
|
1527
|
+
if last is not None and now - last < PROFILE_GAP:
|
|
1528
|
+
return int(last + PROFILE_GAP - now) + 1
|
|
1529
|
+
return 0
|
|
1517
1530
|
|
|
1518
1531
|
|
|
1519
1532
|
def search_limits():
|
|
@@ -1529,8 +1542,12 @@ def search_limits():
|
|
|
1529
1542
|
return default
|
|
1530
1543
|
return {"daily": num(data.get("daily"), DEFAULT_DAILY_SEARCHES),
|
|
1531
1544
|
"monthly": num(data.get("monthly"), DEFAULT_MONTHLY_SEARCHES),
|
|
1532
|
-
# 0 is no cap for searches, never for profile views.
|
|
1533
|
-
|
|
1545
|
+
# 0 is no cap for searches, never for profile views. A number typed
|
|
1546
|
+
# in by hand counts as the largest choice under it: never past 100,
|
|
1547
|
+
# and the Scan page shows the same one.
|
|
1548
|
+
"profiles": max((c for c in (10, 25, 50, 100)
|
|
1549
|
+
if c <= (num(data.get("profiles"), DEFAULT_DAILY_PROFILES) or DEFAULT_DAILY_PROFILES)),
|
|
1550
|
+
default=10)}
|
|
1534
1551
|
|
|
1535
1552
|
|
|
1536
1553
|
def linkedin_usage(now=None):
|
|
@@ -3475,6 +3492,17 @@ def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
|
|
|
3475
3492
|
raise BudgetReached(0, searches_left()[1])
|
|
3476
3493
|
if profiles_left() <= 0: # a fresh read opens their profile
|
|
3477
3494
|
raise BudgetReached(0, "profiles")
|
|
3495
|
+
# The minute between profile views is waited out before anything is
|
|
3496
|
+
# deleted: a Stop during it would otherwise leave the circle deleted and unread.
|
|
3497
|
+
wait = profile_wait()
|
|
3498
|
+
if wait is None:
|
|
3499
|
+
raise BudgetReached(0, "profiles")
|
|
3500
|
+
if wait:
|
|
3501
|
+
print(f" Waiting {wait}s before re-mapping: at least {PROFILE_GAP}s pass between any "
|
|
3502
|
+
f"two profile views.", flush=True)
|
|
3503
|
+
if not interruptible_sleep(wait, on_tick=lambda left: print(f" {left}s to go", flush=True), step=10):
|
|
3504
|
+
print(" Stopped before anything was deleted.")
|
|
3505
|
+
return
|
|
3478
3506
|
|
|
3479
3507
|
print(f"Deleting old cluster data...")
|
|
3480
3508
|
bridge_id = delete_bridge_cluster(bridge_name)
|