Knowledge Graph

An undirected-graph layout primitive — circular nodes joined by symmetric links, with deterministic radial auto-layout and click-to-focus neighbourhood highlighting. Designed for concept maps, design-principle webs, knowledge bases, and any picture where the story is the local neighbourhood, not a directed flow.

KnowledgeGraph is a pure layout primitive. Pass flat nodes and edges lists; the component lays them out and lights up whichever node you click. Distinct from DepGraphViz, which renders a directed dependency flow lit by an external highlight path.

HierarchyContrastRhythmBalanceAlignmentScaleColorSpace
Customize
Layout
20
90
180
Color

Installation

npx shadcn@latest add https://craftbits.dev/r/knowledge-graph.json

Usage

import { KnowledgeGraph } from "@craft-bits/core";
 
const nodes = [
  { id: "hierarchy", label: "Hierarchy" },
  { id: "contrast", label: "Contrast" },
  { id: "rhythm", label: "Rhythm" },
];
 
const edges = [
  { source: "hierarchy", target: "contrast" },
  { source: "hierarchy", target: "rhythm" },
];
 
<KnowledgeGraph nodes={nodes} edges={edges} />;

Controlled focus — the parent owns which node is highlighted:

const [focusedId, setFocusedId] = useState<string | null>("hierarchy");
 
<KnowledgeGraph
  nodes={nodes}
  edges={edges}
  focusedId={focusedId}
  onFocusedIdChange={setFocusedId}
/>;

Caller-driven coordinates — every node carries its own x and y, the component just draws:

<KnowledgeGraph
  nodes={[
    { id: "A", label: "A", x: 80, y: 80 },
    { id: "B", label: "B", x: 240, y: 80 },
    { id: "C", label: "C", x: 160, y: 200 },
  ]}
  edges={[
    { source: "A", target: "B" },
    { source: "B", target: "C" },
  ]}
/>;

Category-tinted strokes — map each node.category to a stroke colour:

<KnowledgeGraph
  nodes={nodes}
  edges={edges}
  categoryColor={{
    structure: "var(--cb-accent)",
    motion: "var(--cb-fg-muted)",
  }}
/>;

Understanding the component

  1. Two layout modes. If every node carries explicit x and y, the component trusts the caller's coordinates — useful when the parent has already chosen a hand-tuned scene or is running its own force simulation. Otherwise it falls back to a deterministic radial layout: nodes with degree at least 3 are treated as hubs and placed on an inner ring, every other node sits on the outer ring half a step offset so leaves don't eclipse hubs. The ordering is insertion-stable, so SSR and client agree.
  2. Undirected edges. Edges are shortened symmetrically at both ends via the shared edgeEndpoints helper, so the link stops cleanly at each node's circular boundary. There are no arrowheads — direction is not part of the story.
  3. Click-to-focus the local neighbourhood. Clicking a node toggles it as the focused node; clicking the same node again, or clicking the empty background, clears the focus. While focused, the focused node and its one-hop neighbours render at full opacity with the accent stroke; everything else fades to context. Edges touching the focused node light up; the rest dim. The component supports both controlled (focusedId + onFocusedIdChange) and uncontrolled (defaultFocusedId) usage in the Radix style.
  4. Token-driven styling. Node radius defaults to 20 px; focused fills resolve to var(--cb-accent-muted), focused strokes to var(--cb-accent), and rest fills to var(--cb-bg-elevated). Hex never leaks into the component body — themes drop in via CSS variables. Per-category accent tones are opt-in via the categoryColor prop.
  5. Reduced motion. usePrefersReducedMotion() short-circuits node entry, edge draw, and focus dimming transitions to instant. The graph snaps into place without any path-length animation.

Props

PropTypeDefaultDescription
nodesreadonly KnowledgeGraphNode[]requiredEach node has a stable id, optional label, optional category, and optional x / y coordinates.
edgesreadonly KnowledgeGraphEdge[]requiredUndirected edges with source and target node ids.
focusedIdstring | nullControlled focused node id. Pair with onFocusedIdChange.
defaultFocusedIdstring | nullnullUncontrolled initial focused node.
onFocusedIdChange(id: string | null) => voidFires when the focused node changes.
categoryColorRecord<string, string>Map of node.category to stroke colour. Unmatched categories fall back to var(--cb-accent).
nodeRadiusnumber20Node circle radius in px.
innerRingRadiusnumber90Inner hub ring radius (auto-layout only).
outerRingRadiusnumber180Outer leaf ring radius (auto-layout only).
transitionTransitionSPRINGS.smoothOverride the spring used for enter transitions.
classNamestringMerged onto the outer SVG element.

Accessibility

  • The outer SVG is role="img" with an aria-label summarising node and edge counts (for example, "Knowledge graph: 8 nodes, 9 edges.").
  • Each node renders as role="button" with tabIndex={0}, aria-label matching the node label, and aria-pressed reflecting whether it is the focused node. Enter and Space toggle focus from the keyboard.
  • Highlighted nodes and edges carry both an opacity shift and a scale bump, so the focused neighbourhood survives high-contrast and grayscale rendering. Identity is communicated through the on-circle label, never through colour alone.
  • Motion respects prefers-reduced-motion: node entries, edge draws, focus dimming, and stroke-colour transitions all collapse to instant.

Credits

  • Extracted from: terminal-dreams (src/components/principles/KnowledgeGraph.tsx). The original was scoped to the design-principles index — it pulled the principle list and category palette from project singletons, ran a d3-force simulation in a useEffect, rendered a mobile-only HTML list fallback, and routed every node click through next/link to the principle page. The library extract drops the d3-force dependency in favour of a deterministic radial fall-back (degree-1 leaves on an outer ring, hubs on an inner ring), drops the mobile-list fallback and Next-specific link wiring, generalises the API to flat nodes and edges lists with optional caller-driven coordinates, switches the active-category prop to a Radix-pattern controlled-or-uncontrolled focusedId so the click-to-focus interaction works without any host context, and routes every dimension through SVG_TOKENS so it sits next to DepGraphViz and ForestViz without visual drift.