What is this?

An interactive graph that displays all content of this portfolio as interconnected nodes — inspired by the graph view in Obsidian and a collaboration with the Wossidlo Archive. Blog posts and projects become dots, shared tags and explicit links become edges. Instead of a linear list, a visual map of relationships emerges.

The graph is not a static image. The nodes move physically: dragging a node affects its neighbors, the neighbors’ neighbors, and so on — with decreasing force. After a few frames, everything comes to rest.

Why a Custom Physics Engine?

Cytoscape.js includes force-directed layouts (cose, cose-bilkent), but they run once and then position statically. I wanted nodes that react to interaction — not just during initial loading, but continuously.

The solution is a custom requestAnimationFrame loop with three forces:

  • Spring forces between connected nodes — pull them toward an ideal distance
  • Repulsion between all nodes — prevents overlap
  • Gravity toward the center — keeps the graph centered

The damping (0.35) ensures that motion decays quickly. A micro-velocity cutoff at 0.01 pixels per frame prevents permanent jittering — a problem that was only solved after several iterations.

Lazy Loading

Cytoscape.js is ~144 KB (gzip). It must not slow down the initial page load. The solution is a two-stage pattern:

  • graph-shell.ts (~15 lines, always loaded): Responds to Escape and close buttons, imports the main logic only on demand
  • knowledge-graph.ts (lazy via import()): Only loaded on the first click of the graph button — including Cytoscape

The graph button in the toolbar triggers import("./knowledge-graph").then(m => m.openGraph()). On the first call, the library and data are loaded in parallel; on subsequent opens, everything is cached.

Data at Build Time

The graph data does not come from a runtime API. A static endpoint (/api/graph-data.json) is generated at build time from the content collections:

  • Nodes: Every blog post and project becomes a node with title, description, tags, URL, and date
  • Tag edges: For every pair of content items sharing at least one tag, an edge is automatically created
  • relatedTo edges: Explicit links from the frontmatter (relatedTo: ["projects/once"]) create additional edges

The result is a JSON file that is simply fetched via fetch() at runtime — no server process, no database access.

Initial Positioning

The nodes do not start randomly but on a Fermat spiral with the golden angle. A seed-based jitter provides organic variation without the distribution looking different every time. The spiral distributes nodes evenly across the area — the physics simulation then pushes them into their equilibrium positions.

Interaction

  • Click on a node opens a sidebar with title, type, description, tags, and date. The rest of the graph dims; only neighboring nodes remain visible.
  • Double-click navigates directly to the page.
  • Drag moves the node and its neighbors physically.
  • Tag filter via the filter button (funnel icon next to the toolbar): matching nodes are highlighted, the rest is dimmed. Multiple tags can be selected simultaneously.

Theme Reactivity

The graph reacts live to theme and color changes. A MutationObserver on <html> watches for changes to data-theme and style (accent color). On every change, the Cytoscape stylesheet is rebuilt — node colors, edge colors, and text colors adapt immediately.

What I Learned

  • Force-directed layouts and physics simulations are different things. A layout computes positions once. A simulation runs continuously and reacts to interaction. For a lively feel, you need the latter.
  • Micro-velocity cutoffs are essential. Without them, spring forces and repulsion create permanent tiny movements that never come to rest. A threshold of 0.01 px/frame solves the problem elegantly.
  • Lazy loading with the shell pattern keeps the initial bundle small without compromising UX. The lightweight shell responds immediately to close/escape, while the heavy logic loads asynchronously.
  • Fermat spirals are excellent for the initial distribution of graph nodes. They avoid the “perfect circle” look of a concentric layout and the randomness of a random distribution.

Glossary

Static Site Generation (SSG)
A build process where all HTML pages are generated at build time — not on each request. The web server delivers pre-built files without a database or server-side logic. Result: fast load times, minimal attack surface, and simple hosting.