Documentation source
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:
```typescript
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:
```typescript
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