Graph Visualization
The graph visualization renders dependency graphs as interactive SVG diagrams. It uses the Dagre library for automatic layout of directed acyclic graphs (DAGs), positioning nodes to minimize edge crossings and optimize readability. Nodes display effective states with color-coding (green for healthy, yellow for stale, red for failed), and arrows illustrate state propagation from dependencies to dependents.
This visualization integrates with the core model by incorporating computed effective states from dependencies. It supports full namespace graphs, filtered views, and focused subgraphs (e.g., upstream or downstream from a specific node).
Graph Data Structure
Graph data follows a standardized JSON format, fetched via API endpoints:
interface GraphNode {
id: string;
state: string;
effective_state: string;
label: string | null;
reason: string | null;
}
interface GraphEdge {
from: string; // Dependent node
to: string; // Dependency node
}
interface GraphData {
namespace: string;
nodes: GraphNode[];
edges: GraphEdge[];
}
- Nodes include raw
state(local) and computedeffective_state(propagated worst-case from dependencies; see Graph Computation). - Edges represent "depends on" relationships:
from(dependent) points toto(dependency). This matches database storage in theedgestable but is reversed during layout for visualization.
State colors use fixed palettes:
const STATE_COLORS: Record<
string,
{ fill: string; stroke: string; text: string }
> = {
green: { fill: "#64d177", stroke: "#4caf50", text: "#1b5e20" },
yellow: { fill: "#ff9800", stroke: "#f57c00", text: "#4e2c00" },
red: { fill: "#f44336", stroke: "#d32f2f", text: "#fff" },
};
Rendering Process
Rendering occurs in src/graph/svg.ts via the renderSvg function, which takes GraphData and outputs a self-contained string:
- Node Measurement: Approximates dimensions using monospace-like character widths (
CHAR_WIDTH = 7.5for 13px sans-serif font). Lines include:
- Node
id(always, bold). label(if present).effective: ${effective_state}(if differs fromstate).
Minimum width is 80px; padding is 16x10px; line height is 16px.
function measureNode(node: GraphNode): {
width: number;
height: number;
lines: string[];
} {
const lines: string[] = [node.id];
if (node.label) lines.push(node.label);
if (node.state !== node.effective_state)
lines.push(`effective: ${node.effective_state}`);
// Compute max width/height...
}
- Dagre Layout:
- Creates a graph with
rankdir: "BT"(bottom-to-top, roots at bottom). - Sets node sizes from measurements.
- Adds edges reversed:
g.setEdge(edge.to, edge.from)so arrows point from dependencies to dependents (state propagation direction). - Config:
nodesep: 40,ranksep: 60, margins 20px.
- SVG Generation:
- Defines arrow marker in
. - Draws edges as stroked paths (
stroke="#666",marker-end="url(#arrow)"). - Draws nodes as rounded rects (
rx=6) filled/stroked by effective state color. - Centers multi-line text (
text-anchor="middle"), bold/large for ID.
export function renderSvg(graph: GraphData): string {
// ... Dagre setup and layout ...
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${svgHeight}" viewBox="0 0 ${svgWidth} ${svgHeight}">`,
`<style>svg { background: #fff; }</style>`,
arrowMarker(),
...edgeSvgs,
...nodeSvgs,
`</svg>`,
].join("\n");
}
The result is a scalable vector graphic suitable for embedding in web pages or exporting.
API Endpoints
Graph data powers the visualization via endpoints in src/routes/graph.ts:
GET /graph/{namespace}: Full graph (optionally?state=green|yellow|redto filter nodes/edges, or?format=yamlfor export).GET /graph/{namespace}/{node}: Subgraph (node + upstream dependencies + downstream dependents).GET /graph/{namespace}/{node}/upstream: Node + dependencies.GET /graph/{namespace}/{node}/downstream: Node + dependents.PUT /graph/{namespace}: Import YAML spec (?prune=trueremoves absent nodes); links to Cli.
Subgraphs use BFS traversal via getUpstreamNodes/getDownstreamNodes (in src/graph/effective.ts) to include relevant nodes/edges.
Effective states are computed on-the-fly during graph building (via Graph Computation's computeEffectiveState, propagating "worst" state: red > yellow > green).
CLI Integration
The dep graph {namespace} command (in src/cli/commands/graph.ts) fetches GraphData and renders a text-based tree:
- Identifies roots (nodes with no
depends_on). - Prints tree with children as dependents, using ANSI colors for states.
- Avoids cycles/duplicates via
printedSet.
This complements SVG for terminal use but is not part of the core visualization.
Design Decisions
- Arrow Direction: Data edges follow "depends on" (dependent → dependency), but visualization reverses them to visualize propagation (dependency → dependent), aligning with mental model of "state flows downstream."
- Effective State Focus: Colors/labels prioritize
effective_state(computed) over rawstate, reflecting real deployability. - Text Approximation: Fixed
CHAR_WIDTHavoids expensive canvas measurement, trading precision for performance/simplicity. - Layout Choices: Bottom-to-top ranks place roots (independent components) at the bottom, with dependents above; generous separation prevents crowding.
- No Cycles: Enforced by project invariants; attempts trigger 409 errors.
- Self-Contained SVG: Includes styles, defs, and viewBox for easy embedding; white background by default.
This system enables quick visual assessment of dependency health across complex graphs.
Recent changes
- Created: Covers Dagre-powered interactive SVG DAGs with state colors and JSON data