NetworkGraph turns a JSON file of people and relationships into an interactive, force-directed map of who knows who — then runs a real shortest-path search to trace the warmest chain of introductions between any two people. It's a zero-build, fully client-side D3 app: a personal-CRM front end, an org and investor map, and a relationship mind-map for anyone who works off their network and needs to see it — founders planning warm intros, BD and sales teams building account maps, recruiters hunting well-connected hubs, investors charting co-investors, and researchers untangling associates.
Self-hosted and static. Your people, your data, your infrastructure — nothing leaves the browser.
- A warm-intro finder. Map your network, switch on path-finding mode, click yourself and a target, and get the named chain of introductions between you, with the exact degrees of separation spelled out — who do I know who knows them.
- An account & stakeholder map. Cluster every contact at a target company with one toggle and plan outreach around your strongest existing ties.
- An investor / portfolio graph. Chart who's connected to whom and which firms cluster together to surface co-investors and warm paths into a deal.
- A talent or community map. Filter members by industry and connection strength to find the well-connected hubs and the underused links.
- An investigation board. Map a subject's web of associates and organisations, then shortest-path two people to surface the non-obvious link between them.
- A lightweight CRM front end. Drop it in as the visual layer over your own relationship data — swap in your JSON today, wire enrichment later.
- A teaching tool. Demonstrate degrees of separation, two-hop influence reach, and clustering with an interactive, brandable, near-dependency-free visual.
NetworkGraph is a real interactive graph engine, not a canned demo. Every behaviour below is wired and live.
- Concentric degree-ring force layout. A D3
forceSimulationruns five forces simultaneously — link, charge (forceManyBody), center,forceCollide, and aforceRadial. The radial force mathematically pins each node to an orbital ring chosen by its hand-authoreddegreefield (ring radii[0, 350, 550, 750, 950, 1150]), so closeness-to-root is literally encoded as distance-to-centre. Exactly one node must bedegree: 0— the pulsing root at the centre. Nodes are spread evenly around each ring with angular jitter, and a ring auto-expands its radius when it holds more nodes than its circumference can fit at the minimum spacing. Four dashed grey guide circles (r = 400 / 650 / 900 / 1200) mark the degree boundaries. - Per-node radial-gradient bubbles. Every node gets its own SVG
radialGradient— a white highlight fading into the node's colour — plus a matching outer stroke and a drop shadow. Colour resolves viagetNodeColor(degree, industry): root uses the accent, degrees 1–2 use the industry'sdegreeCloseband, 3 usesdegreeMid, 4 usesdegreeFar, and 5+ fall back to light grey. Any node can hard-override its colour with acolorfield. - Edges that attach to the bubble surface. Link endpoints are computed to land on each node's circle perimeter rather than its centre (direction-vector math in
_edgePointusing the node radius), so lines touch the bubble edge cleanly — even mid-drag. Weak links are dashed (5,3); all links are round-capped.
- Connection path finder (genuine BFS shortest path). Toggle Find Connection Path, click two distinct nodes, and NetworkGraph runs an actual breadth-first unweighted shortest-path search over the edge list. It highlights the chain with an animated red pulsing stroke, marks the path nodes, and prints the named chain with arrows plus "N degree(s) of separation" (singular/plural aware). The no-path case shows an explicit message. Clicking the same node twice is ignored; a third click starts a fresh pair.
- Influence sphere (two-hop neighbourhood). Click any node in influence mode to dim everything except that node's two-hop neighbourhood — everyone reachable within two introductions, computed by BFS to depth 2. Non-internal edges drop to
0.05opacity and a dashed red 200px-radius sphere is drawn around the node. - Group-by-company clustering. Toggle dashed gold outline circles around every set of nodes sharing the same
companyvalue (singletons are skipped). Each cluster circle centres on its members' centroid with a radius of the farthest member's distance + 50px. - Industry filter (dims nodes and edges). A dropdown populated at runtime from the
INDUSTRIEStaxonomy inconfig.js. Selecting an industry dims non-matching nodes to0.2and dims edges to0.05unless both endpoints belong to that industry. All Industries resets. - Tie-strength filter. A dropdown — All / Strong Only / Medium+ (medium + strong) — that adjusts edge opacity (matching edges
0.8, others0.05). Strength also drives line colour, width, opacity, dash pattern, and the link-force rest distance (strong 120 / medium 160 / weak 200), so stronger ties pull people closer. - Live incremental search. Typing dims non-matching nodes in real time, matching across name, company, and title (substring, case-insensitive). Clearing the box restores full opacity. On mobile, a toast also shows the live match count once the query passes two characters.
- Drag, zoom, pan. Every node is draggable — D3 drag pins
fx/fyduring the drag, re-heats the simulation (alphaTarget 0.3), and releases on end. The background supportsd3.zoompan and scale (extent 0.5–2). A header range slider drives zoom programmatically and stays bidirectionally in sync with wheel/trackpad zoom. The zoom filter explicitly excludes node-groups, so dragging a node never fights the canvas pan. - Hover highlighting. Hovering a node adds a glow, brightens its connected links to full opacity, dims every other link to
0.1, and thickens connected links by strength (strong 6 / medium 4 / weak 2). Mouseout restores the defaults. - Info panel & live stats bar. Clicking a node slides in a panel with name, title, company, location, an ordinal degree label (Root node / 1st degree / 2nd degree …), and influence as X/10. A stats overlay tracks Total Nodes, Direct Links (edges touching the root), Network Reach (max degree, shown as N°), Industries (distinct count), and Max Influence — all recomputed live, including after adding a node. On desktop the panel auto-closes when you click empty canvas.
- Add Node (live, in-memory edit). The + Add Node button really mutates the graph: it picks a random name/title/company from fixed pools, a random industry and random degree 3–4, computes a fresh
idofmax(existing)+1, wires the node to one random existing node with a weak acquaintance edge, creates its gradient def, re-renders, re-heats the simulation (alpha 0.4), and updates the stats. It's a demo of live editability — added nodes are not persisted and vanish on reload.
- Dark / light theme with persistence. The toggle flips
data-themeonbodyacross a full CSS-variable palette (background, text, borders, glass), swaps the button label/emoji, recolours the degree guide circles and node labels for dark mode, and persists the choice tolocalStorage(restored on boot). - Mobile-responsive tabbed bottom sheet. Under 768px the desktop panels hide and a fixed bottom sheet appears with five tabs — Legend / Stats / Tools / Search / Info — using separate mobile DOM ids, a connection-path sub-panel, smaller label fonts, truncated titles, touch-action tuning, and toast notifications.
- Welcome / help / tips modals. Boot shows a welcome modal titled with the root node's name plus usage instructions; How to Use swaps to a mobile-aware tips view, Help re-opens it, and Explore closes it. A data-load failure surfaces a red error modal telling you to start a local server instead of leaving a blank screen.
- Graceful data-load error handling. The
fetchofdata/data.jsonis wrapped — a non-OK response or parse failure logs to the console and shows the start a local server modal rather than failing silently.
- Config-driven branding & physics.
config.jscentralises theTHEME(accent hex + rgb), theINDUSTRIEStaxonomy with per-degree colour bands,CONNECTION_COLORS/WIDTHS/OPACITY, the fullPHYSICStuning (ring radii, min distances, link distance/strength, charge base/size/distance factors, collision and radial strength),DEFAULT_NODE_SIZEper degree, and the D3 CDN URL/version.THEMEvalues are injected into CSS custom properties at runtime — JS takes precedence over the HTML defaults, so a re-skin is a few hex edits with no code changes. - Documented schema + sample data. The app boots on a bundled, fully fictional sample of 64 nodes / 104 edges spanning six degree levels (0–5), four industries (tech / marketing / design / finance), and edge types colleague / investor / professional / acquaintance — backed by a full schema reference in
data/schema.md. - Pluggable enrichment interface. Every node passes through an async
enrich(node)hook at load (aPromise.allover all nodes) before rendering. It ships as a documented no-op that returns the node unchanged, with an in-file skeleton showing how to fetch from anAPI_BASEand mergeavatarUrl/bio/profileUrl. Extra fields flow straight through to the click and hover callbacks.
- D3 v7 — force-directed SVG layout and rendering (link, charge, center, collide, radial).
- Vanilla JavaScript (ES modules) — no framework, no build step, no bundler.
- HTML + CSS custom properties — single page, theme-driven styling.
- Zero backend — fully client-side and static. JSON data file + documented schema.
- Optional
.env-driven enrichment adapter interface — no server included.
D3 loads from a Cloudflare CDN by default; self-hosting is a one-line change in config.js (D3_CDN_URL). There is no package.json — no build, test, or lint tooling, just static files.
ES modules + fetch mean NetworkGraph needs a local HTTP server — it will not run from a file:// double-click.
# 1. Clone
git clone <your-repo-url> network-graph
cd network-graph
# 2. (Optional) set up env for an enrichment adapter you'll build later
cp .env.example .env
# 3. Serve over HTTP — any static server works
python3 -m http.server 8000
# or: npx serve .
# 4. Open it
open http://localhost:8000No npm install — there are no runtime dependencies beyond D3 from the CDN. The app boots on the bundled sample network; to see your network, replace data/data.json (schema in data/schema.md).
Everything visual and physical lives in config.js:
| Setting | Controls |
|---|---|
THEME |
Accent colours (accentPrimary, accentSecondary, accentRgb) — injected into CSS variables at runtime. |
INDUSTRIES |
Industry taxonomy + per-degree colour bands; keys must match the industry field in your data and drive the industry filter. |
CONNECTION_COLORS / CONNECTION_WIDTHS / CONNECTION_OPACITY |
Edge styling per tie strength (strong / medium / weak). |
PHYSICS |
Ring radii, node spacing, link distance/strength, charge, collision, and radial pull. |
DEFAULT_NODE_SIZE |
Fallback bubble size per degree when data omits size. |
D3_CDN_URL / D3_VERSION |
D3 source — point at a vendored build to go fully offline. |
Enrichment adapter. The enrich(node) hook augments each node with external data (avatar, bio, profile URL, tags) at load. It ships as a no-op that returns the node unchanged. To wire a real source, implement against the skeleton in src/enrichment/provider.js. .env.example lists placeholder keys (LinkedIn, Crunchbase, Clearbit, GitHub, PeopleDataLabs, FullContact, or your own backend) — placeholders only; no adapter is written for any of them. For a static deploy, proxy enrichment through a backend you control so keys never reach the browser.
- Swap the data. Replace
data/data.jsonwith your own people and relationships (schema indata/schema.md). Each node needs adegree, and exactly one node must bedegree: 0— the centre of your map. Degrees are hand-authored, not derived from edges; the path-finder and influence-sphere features read the edge list independently of the degree field. - Re-brand. Set
THEMEto your accent colours and editINDUSTRIESto match your sectors. - Re-tune the layout. Adjust
PHYSICSring radii and spacing for denser or sparser networks. - Go offline (optional). Vendor
d3.min.jslocally and pointD3_CDN_URLat it. - Add live data (optional). Implement an enrichment adapter against your own backend.
Straight talk so you know exactly what you're getting:
- Sample data is fictional.
data/data.jsonships 64 made-up people (e.g. Alice Nguyen / TechFlow).influenceis just a number you supply — there is no computed centrality or analytics. - Enrichment is a no-op stub.
src/enrichment/provider.jsreturns the node unchanged. LinkedIn / Crunchbase / Clearbit / GitHub / PeopleDataLabs / FullContact are not integrated — the.envkeys are placeholders for an adapter you write; no adapter exists for any of them. - No backend, no persistence. Purely client-side and static — no database, auth, accounts, import/export UI, or saved edits. Nodes added via the demo button vanish on reload; only the theme choice persists (in
localStorage). Node positions and added nodes do not. - Add Node is a demo. Random name/title/company from small hardcoded pools, random degree 3–4, wired to one random node — not a real add-contact form.
avatarUrlis reserved. It exists in the schema and there's a.person-avatarCSS class, but no avatar image is rendered in JS yet.- Needs a local server. ES modules +
fetchmean nofile://double-click. - Needs internet for D3 by default. D3 loads from a Cloudflare CDN unless you self-host it. Note: the
integrityhash on the D3<script>tag is malformed (non-standard length), which can block the CDN load in strict browsers — remove or correct it if D3 fails to load. - Two known minor bugs. (1)
showInfluenceSpherenever sets thedata-node-idattribute the tick handler reads, so the influence sphere stays at the click-time position instead of following its node as the simulation moves. (2) On boot,app.jsrestores a saved theme to the DOM butgraph.js'sisDarkflag startsfalse, so if the saved theme was dark, the toggle button label and the first click are one step out of phase.
MIT © 2026
