Sprinter Docs

Entity Graph Overhaul

Replace the static circular-layout entity graph with an Obsidian-inspired force-directed visualization, reusable as a block and mini-map.

Problem

The current entity graph at /graph uses a static circular layout that places all nodes in a circle regardless of their relationships. It provides no interactive filtering, no search, no hover context, and no way to explore a single entity's neighborhood. The layout does not convey relationship structure -- densely connected entities look the same as isolated ones. The graph is also not reusable outside the full-page view: it cannot be embedded as a block in views or as a mini-map in entity detail sidebars.

Solution

Build a new graph visualization system in features/graph/ using @xyflow/react with d3-force for force-directed layout. Deliver three variants: a full-page interactive graph with search and filters, a block-sized graph for embedding in views and dashboards, and a compact mini-graph for entity detail sidebars showing 1-hop neighborhoods.

Design

Architecture

Create a new features/graph/ module (currently the graph code lives inline in the app route). The module contains types, layout logic, data hooks, and three component variants that compose from a shared canvas.

features/graph/
  types.ts           -- GraphNode, GraphEdge, GraphData
  lib/
    force-layout.ts  -- d3-force simulation runner
    colors.ts        -- entity type color assignment
  hooks/
    use-graph-data.ts  -- React Query fetching with filters
  components/
    graph-node.tsx     -- Custom @xyflow/react node with type color, icon, title
    graph-canvas.tsx   -- Core @xyflow/react + force layout integration
    graph-toolbar.tsx  -- Search + entity type filters + relation type filters
    full-graph.tsx     -- Full-page composition (toolbar + canvas + legend)
    graph-block.tsx    -- Block variant wrapped in Card
    mini-graph.tsx     -- Sidebar compact variant

The force layout uses d3-force to simulate physics: nodes repel each other, edges act as springs, and connected clusters naturally group together. Node sizes scale with connection count. The simulation runs on initial layout and on filter changes, with smooth transitions between states.

Full-Page Graph (/graph)

Interactive features:

  • Search bar: text search with focus/highlight -- matching nodes pulse and the viewport pans to center them
  • Entity type filters: toggle checkboxes to show/hide node types
  • Relationship type filters: toggle which relation types are visible
  • Hover behavior: highlight the hovered node and its direct connections, dim all other nodes and edges
  • Click: navigate to entity detail page
  • Drag: reposition nodes within the simulation
  • Node rendering: custom node component with entity type color accent, icon (from entity type config), and truncated title
  • Connection count sizing: nodes with more connections render larger
  • Legend: color-coded entity type legend at the bottom

Entity Graph Block (entity-graph)

A new block type registered in the block system:

  • Configurable via block config: full graph mode or entity neighborhood mode
  • Self-contained data fetching via React Query
  • Lazy loaded (dynamic import) to avoid loading @xyflow/react and d3-force on pages that do not use the block
  • Default block size: "half" for dashboards, configurable
  • Wrapped in a Card container with optional title

Block config:

interface GraphBlockConfig {
  entityId?: string;      // If set, show 1-hop neighborhood
  entityTypes?: string[]; // Filter by entity type slugs
  relationTypes?: string[]; // Filter by relation types
  limit?: number;         // Max nodes (default: 50)
  interactive?: boolean;  // Enable drag/zoom (default: true)
}

Mini Graph for Right Sidebar

A compact variant for embedding in entity detail page sidebars:

  • Shows the entity's 1-hop neighborhood (direct connections only)
  • Center node highlighted with a distinct ring
  • Click to navigate to connected entities
  • No toolbar, no search, no filters -- minimal chrome
  • Fixed height (200-300px), non-draggable nodes
  • Zoom disabled, auto-fits to container

API

GET /api/graph (refactor existing)

Add query parameters for server-side filtering:

  • entityId -- return entity + 1-hop connections (neighborhood mode)
  • types -- comma-separated entity type slugs to include
  • relationTypes -- comma-separated relation types to include
  • limit -- max number of nodes (default: 100, max: 500)

Response shape:

interface GraphResponse {
  nodes: {
    id: string;
    title: string;
    typeSlug: string;
    typeLabel: string;
    typeIcon: string | null;
    connectionCount: number;
    score: number | null;
  }[];
  edges: {
    id: string;
    source: string;
    target: string;
    relationType: string;
    label: string | null;
  }[];
}

Server-side filtering reduces payload size for large datasets. The current graph loads all entities and relations for the tenant, which does not scale past a few hundred entities.

Block Registration

Add "entity-graph" to the BLOCK_TYPES array in features/blocks/types.ts. Register the component with a lazy dynamic import so that @xyflow/react and d3-force are only loaded when a graph block is actually rendered.

Trade-offs

d3-force vs. built-in @xyflow layouts: @xyflow/react does not include a force-directed layout. d3-force is the standard solution for this and is well-maintained. The tradeoff is an additional dependency (~30KB gzipped), but it produces significantly better layouts than any built-in alternative.

Server-side filtering vs. client-side: Server-side filtering via query params reduces payload size and initial render time. The tradeoff is that filter changes require a new API call instead of instant client-side filtering. For large graphs (100+ nodes), this is the correct tradeoff. For small graphs, the network round-trip is negligible.

New features/graph/ module vs. extending existing code: The current graph code lives inline in the app route with no separation of concerns. A new module is cleaner and enables the block and mini-graph variants without duplication. The tradeoff is more files, but each file is focused and testable.

Lazy loading the block: The graph dependencies (@xyflow/react, d3-force) are large. Lazy loading via dynamic() ensures pages without graph blocks do not pay the bundle cost. The tradeoff is a brief loading state when the block first renders.

Acceptance Criteria

  • Full-page graph at /graph uses force-directed layout with natural clustering
  • Search bar finds and highlights nodes, panning to center them
  • Entity type and relationship type filters toggle node visibility
  • Hover highlights connected subgraph and dims the rest
  • Click navigates to entity detail page
  • Drag repositions nodes within the simulation
  • Node sizes scale with connection count
  • Legend displays all visible entity types with colors
  • Entity-graph block renders in the block system with configurable modes
  • Block is lazy loaded -- graph dependencies not included in main bundle
  • Mini-graph shows 1-hop neighborhood in entity detail sidebar
  • Mini-graph auto-fits to container with no controls
  • Graph API supports entityId, types, relationTypes, and limit parameters
  • Performance: graph renders smoothly with 200+ nodes
  • No regressions in existing /graph route functionality
  • Visual verification of all three variants

Files

New

  • features/graph/types.ts -- GraphNode, GraphEdge, GraphData, GraphBlockConfig interfaces
  • features/graph/lib/force-layout.ts -- d3-force simulation runner (initialize, tick, stabilize)
  • features/graph/lib/colors.ts -- entity type to color mapping
  • features/graph/hooks/use-graph-data.ts -- React Query hook with filter params
  • features/graph/components/graph-node.tsx -- custom @xyflow/react node component
  • features/graph/components/graph-canvas.tsx -- core canvas integrating @xyflow + d3-force
  • features/graph/components/graph-toolbar.tsx -- search + filter controls
  • features/graph/components/full-graph.tsx -- full-page composition
  • features/graph/components/graph-block.tsx -- block variant (Card wrapped, lazy loaded)
  • features/graph/components/mini-graph.tsx -- sidebar compact variant
  • features/graph/index.ts -- barrel export

Modified

  • app/(app)/graph/page.tsx -- use new full-graph component instead of inline implementation
  • app/api/graph/route.ts -- add query params for entityId, types, relationTypes, limit
  • features/blocks/types.ts -- add "entity-graph" to BLOCK_TYPES, add GraphBlockConfig
  • features/blocks/components/block-renderer.tsx -- register entity-graph block with dynamic import

On this page