# ADR 038: Content Knowledge Graph Visualization

- HTML version: https://robbiepalmer.me/projects/personal-knowledge-graph/adrs/038-content-graph
- Project: Personal Knowledge Graph (https://robbiepalmer.me/projects/personal-knowledge-graph.md)
- Status: Accepted
- Date: 2026-02-07
- Initiatives: Semi-autonomous Software Development (https://robbiepalmer.me/initiatives/semi-autonomous-software-development.md)

# Context

The site's content is structured as a knowledge graph with \~150 nodes
(projects, blog posts, ADRs, technologies, roles, tags) and hundreds of edges representing relationships like
`USES_TECHNOLOGY`, `PART_OF_PROJECT`, `CREATED_AT_ROLE`, and `HAS_TAG`.
This graph already powers backlinks, connected filters, and content discovery throughout the site.

However, there is currently no way to visually see the graph itself. A visualization would:

* Reveal the structure and interconnectedness of the content at a glance
* Enable exploration by clicking through nodes to discover related content
* Demonstrate the depth of the knowledge graph to visitors
* Help identify clusters, central nodes, and isolated content during authoring

The main libraries for interactive graph visualization in the browser are:

* **D3.js** (`d3-force`): The foundational force simulation library. Maximum flexibility but low-level. It requires manual rendering, zoom/pan, and React integration. Fights React's declarative model over DOM ownership.
* **react-force-graph**: React wrapper around `force-graph` (which uses `d3-force` internally). Provides 2D (Canvas) and 3D (WebGL/Three.js) variants with zoom, pan, drag, hover, and click built in. However, incompatible with React 19 due to a [breaking change in ref handling](https://github.com/facebook/react/issues/31613).
* **Sigma.js** (`@react-sigma/core`): WebGL-based, purpose-built for graph visualization. Uses `graphology` for the graph data model and provides React hooks for events, layout, and rendering.
* **Cytoscape.js** (`react-cytoscapejs`): Mature graph library with many layout algorithms (hierarchical, circular, force). More complex API, better suited for analytical/research use cases.

# Original decision

Use **[Sigma.js](https://www.sigmajs.org)** with `@react-sigma/core` for an interactive knowledge graph visualization.

This extracts the site's real `ContentGraph` data at build time, serializes it as `{nodes, edges}`,
and renders it client-side using a **deterministic, seed-based layout**.
The implementation:

* **Color-codes nodes by type**: projects (blue), blogs (orange), roles (purple), ADRs (grey), technologies (green), tags (yellow)
* **Sizes nodes by connection count**: more-connected nodes appear larger
* **Supports filtering**: toggle node types on/off to reduce visual density
* **Links to content**: clicking a node opens its page and highlights connections
* **Shows labels on zoom**: node names appear when zoomed in, avoiding clutter at overview level
* **Stable Layout**: Uses a synchronous force-directed algorithm (ForceAtlas2) to calculate positions once, ensuring the graph looks identical on every load avoiding jerking on filtering/interactivity etc.
* **Loads lazily**: uses `next/dynamic` with `ssr: false` since the WebGL API requires the browser

## Demo

*(Interactive KnowledgeGraph component — view it on the HTML page)*

# Consequences

### Pros

* **React 19 compatible**: Sigma.js with `@react-sigma/core` works with React 19's ref handling.
* **WebGL rendering**: Performant for \~150 nodes with hardware-accelerated rendering.
* **graphology ecosystem**: The `graphology` data model provides a clean API for graph manipulation.
* **SSG-compatible**: Data extraction happens at build time in a server component.
* **Filterable**: Node type toggles let users focus on specific content types.
* **Deterministic**: The layout is calculated with a fixed seed, ensuring the graph always looks the same to every visitor.

### Cons

* **No SSR**: The WebGL-based renderer requires the browser. The graph shows a loading placeholder during hydration.
* **Bundle size**: sigma + graphology + layout worker add client-side JavaScript.
* **Fixed Layout**: Users cannot drag nodes to rearrange them manually, which is a trade-off for guaranteed layout stability without "jitter".
* **Information density**: With all node types visible, the graph can feel busy.
* **Accessibility**: WebGL-based rendering is not accessible to screen readers.
* **Mobile experience**: Interaction is optimized for mouse/trackpad (hover for details). Touch works for pan/zoom but lacks hover states.

# Superseding decision

The fixed Sigma layout made filtering predictable, but rebuilding the Graphology instance after every filter change discarded the previous layout. Synchronous ForceAtlas2 work also blocked the main thread, and rendered node radii could still overlap.

Replace Sigma.js and Graphology with **[cosmos.gl](https://github.com/cosmosgl/graph)**. Keep every node at a stable numeric index and hide filtered nodes with absent positions. A deterministic radial layout keeps the graph inside its fitted viewport without rebuilding its identity. Each filter state gets a new deterministic layout based on its visible nodes, with a finite transition to the new positions. Dragging a node directly displaces nearby connected nodes with a depth-based falloff. Local collision handling pushes overlapping nodes aside without starting a global force simulation.

The replacement adds:

* A stable layout that remains inside the fitted viewport
* Anchored drag propagation with local collision handling, while filter changes animate once to a newly centred layout
* Node sizing based on global connection count
* Search, neighbourhood highlighting, direct page links, and a reset control
* A passive mobile homepage preview that does not capture scrolling
* A full-screen explorer on every viewport, with pan, zoom, filters, search, and node details
* Reduced-motion support, capped mobile pixel density, and an HTML graph index

The HTML index is always available rather than appearing only when WebGL fails. It provides keyboard and screen-reader access to the same filtered nodes and their destination pages.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
