Knowledge graph
A force-directed relationship graph in the spirit of Obsidian's graph view. Nodes are circles sized by how connected they are, links are thin SVG lines, and a small physics simulation lays the web out — linked nodes pull together, every node pushes the rest away, and a gentle gravity keeps the cloud centred. The layout is seeded and settled synchronously, so the first server paint is stable and hydration never mismatches. From there the viewport is live: drag the background to pan, wheel or the control cluster to zoom, fit to frame everything, and drag a node to pull it around. Hovering or selecting a node lifts it and its neighbours out of the crowd while the rest dim back, so a dense web stays legible. Every node is a real button — Tab to it, Enter to select, arrow keys to nudge — and carries a text label, so colour is never the only signal. The live version is on the Knowledge graph showcase.
const nodes = [ { id: 'home', label: 'Home', group: 'Maps' }, { id: 'graph-view', label: 'Graph view', group: 'Projects' }, { id: 'flab-ui', label: 'Flab UI', group: 'Projects' }, { id: 'force', label: 'Force layout', group: 'Research' }, { id: 'graph-theory', label: 'Graph theory', group: 'Research' }, { id: 'essay', label: 'On graphs', group: 'Writing' },];
const links = [ { source: 'home', target: 'graph-view' }, { source: 'home', target: 'flab-ui' }, { source: 'flab-ui', target: 'graph-view' }, { source: 'graph-view', target: 'force' }, { source: 'graph-view', target: 'graph-theory' }, { source: 'graph-view', target: 'essay' }, { source: 'force', target: 'graph-theory' },];
// Drag the background to pan, wheel to zoom, drag a node to move it, click to select.// The graph is monochrome by default; pass colorByGroup to tint nodes by folder.<KnowledgeGraph nodes={nodes} links={links} height={360} colorByGroup ariaLabel="Vault knowledge graph" className="rounded-xl border border-hairline"/>Highlighting a search
Pass a set of node ids to highlightIds and the graph lights each one and its direct neighbours while the rest dim back — the links touching a highlighted node turn accent. It is a persistent emphasis, independent of selection, so it pairs neatly with a search box: filter your nodes to the matches and hand their ids to the graph. Hovering a node still takes over while the pointer is on it.
function SearchableGraph() { const [query, setQuery] = useState('graph');
const highlightIds = useMemo( () => nodes .filter((n) => n.label.toLowerCase().includes(query.trim().toLowerCase())) .map((n) => n.id), [query], );
return ( <> <SearchInput value={query} onChange={(e) => setQuery(e.target.value)} /> <KnowledgeGraph nodes={nodes} links={links} highlightIds={highlightIds} /> </> );}Props
| Prop | Type | Default | Notes |
|---|---|---|---|
nodes | KnowledgeGraphNode[] | — | The notes/entities on the canvas. |
links | KnowledgeGraphLink[] | — | Undirected connections between nodes, by id. |
height | number | string | 460 | Viewport height; the canvas pans and zooms within it. |
ariaLabel | string | 'Knowledge graph' | Accessible name for the whole graph. |
nodeRadius | number | 6 | Base node radius in px; scaled up by link count or value. |
linkDistance | number | 60 | Target distance between two linked nodes, in px. |
charge | number | -240 | Repulsion strength between nodes; more negative pushes harder. |
showLabels | boolean | true | Show a text label beside each node. |
colorByGroup | boolean | false | Tint nodes by their group. Off by default: the graph stays monochrome and colour comes only from an explicit node tone or from selection/hover emphasis. |
labelZoomThreshold | number | 0.7 | Below this zoom, labels hide except for the emphasised node, so a zoomed-out overview stays clean. Set to 0 to always show labels. |
controls | boolean | true | Show the built-in fit / zoom control cluster. |
fitOnMount | boolean | true | Frame every node inside the viewport once, on mount. |
minZoom | number | 0.2 | Lower zoom bound. |
maxZoom | number | 3 | Upper zoom bound. |
zoomStep | number | 0.2 | Per-step zoom factor for the wheel and buttons. |
seed | number | 1 | Seed for the deterministic initial layout. |
highlightIds | string[] | — | Node ids to highlight (e.g. a search result): each and its direct neighbours stay lit while the rest dim. Independent of selection. |
selectedId | string | null | — | Controlled selection. Omit for uncontrolled. |
defaultSelectedId | string | null | — | Uncontrolled initial selection. |
onSelectChange | (id: string | null) => void | — | Fires when selection toggles. |
toneForGroup | (group: string) => BadgeTone | — | Map a group name to a tone; else a stable tone is assigned per group. |
KnowledgeGraphNode
| Prop | Type | Default | Notes |
|---|---|---|---|
id | string | — | Stable node id, referenced by links. |
label | string | — | Display label; falls back to the id. Doubles as the accessible name. |
group | string | — | Grouping key; drives the node colour and its legend swatch. |
tone | BadgeTone | — | Explicit tone; overrides the tone derived from group. |
value | number | — | Manual size weight; when omitted, the node is sized by its link count. |
x / y | number | — | Optional fixed initial position in world px; else it is seeded. |
selected | boolean | false | Selects this node on first render (uncontrolled). |
KnowledgeGraphLink
| Prop | Type | Default | Notes |
|---|---|---|---|
source | string | — | Source node id. |
target | string | — | Target node id. |
strength | number | 0.7 | Optional weight, 0–1, scaling how strongly the link pulls. |
KnowledgeGraphLegend
| Prop | Type | Default | Notes |
|---|---|---|---|
items | KnowledgeGraphLegendItem[] | — | Each pairs a group colour (tone) with its meaning in text. |