Code Visual is the visual front-end for code-graph-server, an MCP (Model Context Protocol) tool that integrates with VS Code and Claude to analyse a codebase and store its structure — files, modules, functions, classes, imports, exports, and their relationships — as nodes and edges in a Memgraph database.
Code Visual then connects to that same Memgraph instance and renders the stored graph as an interactive force-directed diagram, giving developers a live, navigable map of their codebase.
The app is split into four clear layers: data access → state store → layout engine → render.
Memgraph (Bolt)
│
▼
server/memgraph-proxy.mjs Node.js HTTP proxy — translates POST /query to Bolt driver calls
│
▼
src/lib/memgraphClient.ts Fetch layer — sends Cypher queries, normalises responses, infers types
│
▼
src/state/graphStore.ts Zustand store — single source of truth for all graph + UI state
│
▼
src/lib/layoutEngine.ts d3-force layout — computes (x, y) for every visible node
│
▼
src/App.tsx + GraphCanvas.tsx React render — animates transitions, handles input events
A minimal Node.js HTTP server (no framework). Receives POST /query with { query, params }, forwards via the neo4j-driver Bolt client, and streams back normalised JSON results. Keeps credentials server-side; the browser never sees them.
Thin fetch wrapper around the proxy. Two modes are detected at startup:
- Live mode — proxied HTTP POST to
VITE_MEMGRAPH_URL - Mock mode — deterministic in-memory tree, used when the proxy is unreachable
Key functions:
listProjects()— returns root nodes asProjectSummary[]expandNodePage()— paginated children for a single parent nodeexpandNeighborhoodDepth()— batch-expands to a given depth for background hydrationinferSemanticType()— classifies a node asfunction | class | import | export | variablefrom its labels/relation strings
All TypeScript interfaces that flow through the system:
| Type | Description |
|---|---|
GraphNodeEntity |
Stored graph node (id, label, kind, semanticType, depth, expanded, loading) |
GraphEdgeEntity |
Stored edge (id, source, target, label) |
PositionedNode |
Node annotated with (x, y) after layout |
PositionedEdge |
Edge ready to render |
LayoutFrame |
{ nodes: PositionedNode[], edges: PositionedEdge[] } |
SemanticNodeType |
function | class | import | export | variable |
NodeKind |
layer | module | service | file |
ViewportState |
Pan/zoom { x, y, scale } |
Zustand store that holds the entire application state. Mutations schedule a requestAnimationFrame debounced layout recompute (scheduleFrame) to avoid blocking the main thread on every synchronous state change.
Key state:
nodesById / edgesById / childIdsByParent— normalised graph datamanualPositions— positions overridden by dragframe— last computedLayoutFrameviewport— current pan/zoomconnectionDepth / siblingPageByParent— traversal config
Key mutations:
mergeExpansionPage— adds a paginated batch of children for one parentmergeExpansionBatch— multi-parent batch for background hydrationsetFocusedNode— changes the traversal root and recomputes visible depthssetManualPositionsBatch— updates drag positions without layout rerun
Pure function computeLayoutFrame(input) — produces x/y positions for every visible node. Steps:
- BFS visibility pass — starts at
rootNodeId, respectsconnectionDepthandsiblingPageByParentpagination, producesvisibleNodeIds,depthByNodeId,parentByNodeId. - Radial target angles — weighted subtree angles via
buildTargetAngleById(weighted by subtree size so large subtrees get more arc). - Ring radii — per-depth ring radius computed from node sizes + density (
buildDepthRingRadius). - d3-force simulation —
forceSimulationwithforceCenter,forceManyBody(charge),forceX/Y(ring attraction),forceRadial,forceLink,forceCollide. Ticks run synchronously:min(240, max(120, n × 6)). - Manual position override —
manualPositionsfrom drag replace force-computed positions before output.
Orchestration and interaction layer. Responsibilities:
- Semantic type filtering —
filteredLayoutFrameuseMemo runscomputeLayoutFrameon a pruned graph (nodes/edges/children filtered, subtrees excluded viaexcludeSubtreeDFS). - Layout animation — 280 ms linear interpolation between
renderFrameandfilteredLayoutFrameusingrequestAnimationFrame. - Camera animation — pans viewport to centre the focused node using a separate RAF loop.
- Drag propagation — square-falloff influence BFS that moves nearby nodes when one is dragged (
collectDragPropagationUpdates). - Pointer event routing — distinguishes canvas pan (right-drag), node drag (left-drag), click, double-click.
Receives a LayoutFrame and renders:
<svg class="edges">— one<g>per edge: a glow<path>+ a stroke<path>+ optional<text>label- One
<button class="node ...">per node positioned via CSStransform: translate(x,y) scale(s), classified by kind and semantic type
Node labels are parsed by getStructuredNodeLabel:
- File nodes — extracts basename + extension chip + optional
#anchor - Colon-separated labels (imports, namespaced ids) — split into primary + secondary line
All in-canvas controls rendered as a controlsOverlay prop overlay:
| Component | Purpose |
|---|---|
CanvasControls |
Container; hosts all sub-controls; node-type group is right-aligned |
DepthControl |
+/− connection depth buttons |
MotionControl |
Animation speed slider |
RefreshToggleControl |
Generic labelled toggle switch (reused for auto-refresh and each node-type filter) |
SyncBadgeControl |
Passive "Auto refresh On/Off" label + "Syncing…" badge when active |
ProjectControl |
Project selector dropdown |
user double-clicks node
│
▼
App.tsx onNodeDoubleClick → expandNode(nodeId)
│
├─ graphStore.setNodeExpanded(nodeId, true)
├─ graphStore.setFocusedNode(nodeId)
└─ useGraphController.hydrateNeighborhoodToDepth(nodeId)
│
▼
memgraphClient.expandNeighborhoodDepth({ projectId, startNodeId, depth })
│
▼
graphStore.mergeExpansionBatch({ parents })
│
▼
scheduleFrame() → requestAnimationFrame → computeLayoutFrame → store.frame
│
▼
App.tsx filteredLayoutFrame useMemo invalidates
│
▼
layout animation effect → interpolates renderFrame over 280 ms
inferSemanticType in memgraphClient.ts maps raw Memgraph node labels to UI types:
| Labels / relation / id prefix | SemanticNodeType |
|---|---|
Function, Method |
function |
Class, Interface, Type |
class |
Import, relation IMPORTS, id import: |
import |
Export, relation EXPORTS, id export: |
export |
Variable, Const, Property |
variable |
Nodes without a match have semanticType: undefined and are always visible regardless of filter state.
World coordinates are fixed; the viewport is a CSS transform applied to .graph-layer:
transform: translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.scale})
Node positions are in world space (centre of CANVAS = 900, 600). Manual positions are stored in world space. Hit detection for drag distance uses world-space division by viewport.scale.
| Variable | Consumed by | Default |
|---|---|---|
VITE_MEMGRAPH_URL |
Browser fetch (via Vite) | http://localhost:4000/query |
MEMGRAPH_BOLT_URL |
Node proxy (dotenv) |
bolt://localhost:7687 |
MEMGRAPH_BOLT_USER |
Node proxy | (empty) |
MEMGRAPH_BOLT_PASSWORD |
Node proxy | (empty) |
MEMGRAPH_PROXY_PORT |
Node proxy | 4000 |
| Key | Type | Purpose |
|---|---|---|
code-visual:motionSpeedFactor |
number |
Animation speed multiplier |
code-visual:connectionDepth |
number |
Traversal depth |
code-visual:nodeTypeFilters |
JSON |
Per-type visibility flags |
code-visual:selectedProjectId |
string |
Last selected project |
code-visual:autoRefreshEnabled |
boolean |
Auto-refresh toggle |
See performance-plan.md for a full audit and prioritised optimisation plan. The highest-impact items are:
- Narrow
filteredLayoutFramedeps — exclude viewport from invalidation (Tier 1-C) - Stabilise animation memos — derive topology lookups from layout target, not interpolated frame (Tier 1-A)
- Remove store
frame— eliminate the second redundant force simulation per mutation (Tier 2-D) - Web Worker layout — move d3-force off the main thread (Tier 3-G)
- Canvas edge layer — replace SVG edge DOM with a single
<canvas>draw pass (Tier 3-H)