Zero-dependency graph layout engine for Zig. Visualize DAGs, dependency trees, and flow graphs in terminals, SVG, or JSON.
| Terminal (Unicode) | SVG (Debug) | SVG (Splines) | SVG (Labels on Path) |
|
|
|
|
- Zero dependencies — Pure Zig, no libc required
- Two layout engines — Sugiyama (hierarchical DAGs) and Fruchterman-Reingold (force-directed)
- Subgraphs (clusters) — Hierarchical grouping with visual boundaries, nested subgraphs
- Cycle breaking — Automatic back-edge detection for cyclic graphs (DFS-based)
- Directed & undirected edges —
addDiEdge/addUnDiEdgewith per-edge arrow control - Three renderers — Unicode (terminal), SVG (with splines), JSON (for tooling)
- Edge labels — Annotate edges with text, rendered in all output formats
- Pluggable algorithms — Bring your own crossing reduction, positioning, routing
- Embedded-first — Explicit allocators, ~40KB WASM target
Run this command to add zigraph to your project:
zig fetch --save git+https://github.com/AshutoshMahala/zigraphThen in build.zig:
const zigraph = b.dependency("zigraph", .{});
exe.root_module.addImport("zigraph", zigraph.module("zigraph"));const zigraph = @import("zigraph");
// Render with ANSI colors (optional)
const output = try zigraph.terminal.renderWithConfig(&ir, allocator, .{
.edge_palette = &zigraph.color.ansi_dark,
.show_dummy_nodes = false,
});
defer allocator.free(output);
std.debug.print("{s}\n", .{output});// Default: edges colored via Radix UI palette, smooth splines on by default
const svg = try zigraph.svg.render(&ir, allocator, .{});
defer allocator.free(svg);
// Monochrome instead of palette cycling
const svg_mono = try zigraph.svg.render(&ir, allocator, .{
.edge_style_fn = &zigraph.svg.monoEdgeStyle,
});
defer allocator.free(svg_mono);Per-edge color, markers, and CSS are all controlled by
edge_style_fn. See SVG Customization Guide.
// Export layout data for external tools
const json = try zigraph.json.render(&ir, allocator);
defer allocator.free(json);See JSON_SCHEMA.md for data format details.
const std = @import("std");
const zigraph = @import("zigraph");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Build graph
var graph = zigraph.Graph.init(allocator);
defer graph.deinit();
try graph.addNode(1, "Parse");
try graph.addNode(2, "Compile");
try graph.addNode(3, "Link");
try graph.addDiEdge(1, 2); // directed edge (arrow)
try graph.addDiEdgeLabeled(2, 3, "link"); // directed + labeled
// try graph.addUnDiEdge(1, 3); // undirected (no arrow)
// Layout using a preset (recommended)
const output = try zigraph.render(&graph, allocator, zigraph.presets.sugiyama.standard());
defer allocator.free(output);
std.debug.print("{s}\n", .{output});
}Output:
[Parse]
│
↓
[Compile]
│
↓
[Link]
Annotate edges with descriptive text:
try graph.addDiEdgeLabeled(1, 2, "requires");
try graph.addDiEdgeLabeled(2, 3, "queries");
try graph.addDiEdge(1, 3); // unlabeled edgeLabels appear in all renderers — terminal, SVG, and JSON.
zigraph automatically handles cyclic graphs via DFS-based back-edge detection. Enable it with .cycle_breaking = .depth_first:
var graph = zigraph.Graph.init(allocator);
defer graph.deinit();
try graph.addNode(1, "Input");
try graph.addNode(2, "Process");
try graph.addNode(3, "Output");
try graph.addEdge(1, 2);
try graph.addEdge(2, 3);
try graph.addEdgeLabeled(3, 1, "feedback"); // Creates a cycle!
const output = try zigraph.render(&graph, allocator, .{
.cycle_breaking = .depth_first,
});
defer allocator.free(output);
std.debug.print("{s}\n", .{output});Back edges are virtually reversed — the original graph is not mutated. Reversed edges are visually distinct:
- Unicode: dashed arrows (
⇡) with side routing - SVG: dashed lines with bezier curves (two-node cycles arc to avoid overlap)
- Self-loops:
↺symbol in Unicode, arc above node in SVG
Supported patterns: feedback loops, mutual dependencies (A ↔ B), self-loops (A → A), and complex multi-cycle graphs.
Group nodes into visual clusters with hierarchical nesting:
var graph = zigraph.Graph.init(allocator);
defer graph.deinit();
try graph.addNode(0, "Gateway");
try graph.addNode(1, "Auth");
try graph.addNode(2, "DB");
try graph.addDiEdge(0, 1);
try graph.addDiEdge(1, 2);
// Create a subgraph and assign nodes
const backend = try graph.addSubgraph("backend");
try graph.putNodes(&.{ 1, 2 }).inside(backend);
const output = try zigraph.render(&graph, allocator, zigraph.presets.sugiyama.standard());
defer allocator.free(output);
std.debug.print("{s}\n", .{output});Output:
[Gateway]
│
╔══╧═╤════╗
║ backend ║
║ ↓ ║
║ [Auth] ║
║ │ ║
║ ↓ ║
║ [DB] ║
║ ║
╚═════════╝
const services = try graph.addSubgraph("services");
const auth = try graph.addSubgraph("auth");
try graph.putNodes(&.{ api_id, auth_id, token_id, db_id }).inside(services);
try graph.putNodes(&.{ auth_id, token_id }).inside(auth);
try graph.putSubgraphs(&.{auth}).inside(services); // nest auth inside services| Renderer | Subgraph Style |
|---|---|
| Unicode | Double-line box (╔═╗║╚╝) with label; edges cross borders using mixed junction chars (╫╪╤╧) |
| SVG | Dashed rounded rectangle with configurable fill/stroke/opacity |
| JSON | subgraphs array with id, label, parent_id, bounding box (x, y, width, height) |
Both Sugiyama and FDG layouts are subgraph-aware:
- Sugiyama: Contiguous level enforcement, block-based crossing reduction, overlap repair, subgraph padding, bounding box computation
- FDG: Cohesion force pulls subgraph members toward group centroid; inter-cluster separation force (
cluster_separation) pushes sibling clusters apart
zigraph supports directed, undirected, and mixed graphs:
try graph.addDiEdge(1, 2); // directed: renders with arrow (→)
try graph.addUnDiEdge(2, 3); // undirected: renders without arrow (—)
try graph.addDiEdgeLabeled(1, 3, "dep"); // directed + labeled
try graph.addUnDiEdgeLabeled(3, 4, "link"); // undirected + labeled
// Legacy aliases still work:
try graph.addEdge(1, 2); // same as addDiEdge
try graph.addEdgeLabeled(1, 2, "x"); // same as addDiEdgeLabeledPresets provide curated configurations for common use cases:
const zigraph = @import("zigraph");
// Sugiyama (hierarchical DAG layout)
const ir = try zigraph.layout(&graph, allocator, zigraph.presets.sugiyama.standard());
const ir_fast = try zigraph.layout(&graph, allocator, zigraph.presets.sugiyama.fast());
const ir_quality = try zigraph.layout(&graph, allocator, zigraph.presets.sugiyama.quality());
// Force-directed (any graph type)
const ir_fdg = try zigraph.layout(&graph, allocator, zigraph.presets.fdg_presets.standard());
const ir_fdg_fast = try zigraph.layout(&graph, allocator, zigraph.presets.fdg_presets.fast());| Preset | Use Case | Speed |
|---|---|---|
sugiyama.standard() |
DAGs, balanced quality/speed | ★★★ |
sugiyama.fast() |
Large DAGs, speed priority | ★★★★ |
sugiyama.quality() |
Small DAGs, best visuals | ★★ |
fdg_presets.standard() |
General graphs < 500 nodes | ★★★ |
fdg_presets.fast() |
Large graphs 500-10000 nodes | ★★★★ |
For non-hierarchical or general graphs, use the Fruchterman-Reingold algorithm:
const zigraph = @import("zigraph");
// Use preset (recommended)
var ir = try zigraph.layout(&graph, allocator, zigraph.presets.fdg_presets.standard());
var ir_fast = try zigraph.layout(&graph, allocator, zigraph.presets.fdg_presets.fast());
// Or manual config for fine control
var ir_custom = try zigraph.layout(&graph, allocator, .{
.algorithm = .{ .fruchterman_reingold = .{} },
});
defer ir_custom.deinit();| Nodes | FR Standard | FR-Fast (Barnes-Hut) | Speedup |
|---|---|---|---|
| 500 | 11 ms | 5 ms | 2.2× |
| 1000 | 42 ms | 11 ms | 3.8× |
| 5000 | 1040 ms | 28 ms | 37.6× |
// Default: labels centered at edge midpoint
const svg = try zigraph.svg.render(&ir, allocator, .{});
// Text-on-path: labels follow the edge curve
const svg_path = try zigraph.svg.render(&ir, allocator, .{
.labels_on_path = true, // uses SVG <textPath>
});SVG labels are automatically oriented left-to-right (never upside-down) and centered on the geometric midpoint of each edge.
const output = try zigraph.render(&graph, allocator, .{});var ir = try zigraph.layout(&graph, allocator, .{ .routing = .spline });
defer ir.deinit();
// Default config — Radix palette colors, smooth splines, arrow markers
const svg = try zigraph.svg.render(&ir, allocator, .{
.labels_on_path = true, // Labels follow edge curves
.show_control_points = true, // Debug splines
});
// Custom palette via edge_style_fn (replaces the old `edge_palette` field)
fn neonStyle(ctx: zigraph.svg.EdgeStyleContext) zigraph.svg.EdgeStyle {
const palette = [_][]const u8{ "#ff00ff", "#00ffff", "#ffff00" };
return .{
.stroke = palette[ctx.edge_index % palette.len],
.marker_end = if (ctx.directed) .arrow else .none,
};
}
const svg_neon = try zigraph.svg.render(&ir, allocator, .{ .edge_style_fn = &neonStyle });const json = try zigraph.exportJson(&graph, allocator, .{});See JSON_SCHEMA.md for the output format, or view assets/hero.json for an example.
For fine-grained control, configure manually (or start with a preset and override):
const output = try zigraph.render(&graph, allocator, .{
// Layering
.layering = .longest_path, // default: simple, fast
// .layering = .network_simplex, // optimal: minimizes total edge span
// .layering = .network_simplex_fast, // bounded iterations, good for large graphs
// Positioning
.positioning = .compact, // default: left-to-right packing (no collisions)
// .positioning = .barycentric, // single-pass barycentric (graph-aware)
// .positioning = .brandes_kopf, // multi-pass parent/child centering (best quality)
// Crossing reduction
.crossing_reducers = &zigraph.crossing.balanced, // default
// .crossing_reducers = &zigraph.crossing.fast, // speed
// .crossing_reducers = &zigraph.crossing.quality, // best
// Edge routing
.routing = .direct, // or .spline
// Spacing
.node_spacing = 3,
.level_spacing = 2,
// Performance
.skip_validation = false,
});Compose your own pipeline:
.crossing_reducers = &[_]zigraph.crossing.Reducer{
zigraph.crossing.medianReducer(4),
zigraph.crossing.adjacentExchangeReducer(2),
zigraph.crossing.medianReducer(2), // polish
},Or bring your own algorithm:
fn myReducer(self: *const zigraph.crossing.Reducer, levels: *VirtualLevels, g: *const Graph, alloc: Allocator) !void {
// Custom crossing reduction logic
}
.crossing_reducers = &[_]zigraph.crossing.Reducer{
zigraph.crossing.medianReducer(2),
.{ .runFn = myReducer, .passes = 5 },
},Benchmarks on Apple M2 (zig build run-benchmark):
| Nodes | Edges | Layout | Render | Total |
|---|---|---|---|---|
| 100 | 200 | 1.0 ms | 0.03 ms | 1.0 ms |
| 1,000 | 2,000 | 57 ms | 0.1 ms | 57 ms |
| 10,000 | 20,000 | 4.5 s | 1.4 ms | 4.5 s |
| Preset | Time | Description |
|---|---|---|
none |
0.03 ms | No reduction |
fast |
0.04 ms | median(2) |
balanced |
0.6 ms | median(4) + exchange(2) |
quality |
0.6 ms | median(8) + exchange(4) + median(2) |
- Layout: O(passes × (V + E)) dominated by crossing reduction (V=nodes, E=edges)
- Render: O(W × H) where W×H is output dimensions
- <100 nodes: Use
crossing.qualityfor best results - 100-1000 nodes: Use
crossing.balanced(default) - >1000 nodes: Use
crossing.fastorskip_validation = true - Wide layers (>20 nodes): Adjacent exchange auto-skips for performance
Graph API → Layout Engine → LayoutIR → Renderer → Output
│ │
┌─────────┴──────────┐ ┌──────────┼──────────┐
Sugiyama FDG SVG Unicode JSON
(hierarchical) (force-dir) (styled) (ANSI) (data)
- Sugiyama pipeline: Layering → Crossing reduction → Positioning → Routing (with optional subgraph-aware stages)
- Force-directed: Fruchterman-Reingold with optional Barnes-Hut quadtree (O(V log V)), Q16.16 fixed-point arithmetic
- LayoutIR: Stable contract between layout and rendering — nodes, edges, subgraphs, bounding boxes
- 3 renderers: SVG (full style API with 4 function hooks,
<defs>injection, CSS/JS), Unicode (box-drawing + ANSI color), JSON (schema v1.2) - Color module: 4 color spaces (sRGB, Oklab, HSL, linear), 6 scientific colormaps, 10 palettes, SVG gradient generators
See ARCHITECTURE.md for the full design: pipeline diagrams, module map, file tree, error handling, and testing strategy.
- CLI tools — Error chain visualization, build graphs
- Compilers — AST/IR visualization
- Documentation — Embedded diagrams
- Embedded systems — Diagnostics on microcontrollers
- WASM dashboards — Browser-based visualization
The SVG renderer supports full style customization via function pointers. Each gallery example demonstrates a specific feature — run all at once with zig build run-svg-gallery, or individually.
See SVG Customization Guide for the complete API reference.
| 01 — Basic | 02 — Presets (Diamond) | 02 — Presets (Ellipse) |
|---|---|---|
| Zero-config rendering | Built-in shape presets | One-liner shape swaps |
| 03 — Flowchart | 04 — Clusters | 05 — Dark Theme |
|---|---|---|
| Conditional shapes + colored labels | Depth-aware subgraph styling | All 4 style fns + global CSS |
| 06 — Interactive | 07 — Heatmap |
|---|---|
| CSS hover + JS click events | FEA stress viz with color spill |
Run individual examples
zig build run-svg_01_basic # Basic
zig build run-svg_02_presets # Shape presets
zig build run-svg_03_flowchart # Flowchart
zig build run-svg_04_clusters # Clusters
zig build run-svg_05_dark_theme # Dark theme
zig build run-svg_06_interactive # Interactive
zig build run-svg_07_heatmap # HeatmapThe terminal renderer supports ANSI 256 / truecolor output with box-drawing characters, gradient edges, and custom node shapes. Each example demonstrates a specific feature.
See Terminal Customization Guide for the complete API reference.
| 01 — Node Control | 02 — Edge Styles | 03 — Color System |
|---|---|---|
![]() |
![]() |
![]() |
| Borders, colors, gradients | Line weights + marker shapes | Color modes, palettes, gradients |
| 04 — Edge Labels | 05 — Subgraph Styles | 06 — Output Formats |
|---|---|---|
![]() |
![]() |
![]() |
| Label placement + colored labels | Nested borders, depth-cycled colors | ASCII charset, HTML <pre> output |
| 07 — Record Nodes | 08 — DB Diagram | 10 — Interactive TUI |
|---|---|---|
![]() |
![]() |
![]() |
| ER-style multi-row record boxes | CRM schema with PK/FK color coding | Click-to-select with hit-testing |
| 11 — Text Attributes |
|---|
![]() |
| Bold, dim, italic, underline |
Run individual examples
zig build run-terminal-node-control # Node borders + colors
zig build run-terminal-edge-styles # Edge weights + markers
zig build run-terminal-color-system # Color modes + gradients
zig build run-terminal-edge-labels # Label placement
zig build run-terminal-subgraph-styles # Subgraph borders + nesting
zig build run-output-formats # ASCII + HTML output
zig build run-terminal-record-nodes # Record-style nodes
zig build run-terminal-db-diagram # ER diagram
zig build run-streaming # Streaming render
zig build run-tui # Interactive TUI
zig build run-terminal-text-attrs # Text attributeszig build run-basic # Basic usage
zig build run-hero # README hero diagram
zig build run-presets # Presets demo (all presets side-by-side)
zig build run-config # Configuration options demo
zig build run-positioning # Positioning algorithms comparison
zig build run-svg # SVG with splines
zig build run-labels # Edge labels demo (exports SVG)
zig build run-cycle # Cycle breaking demo (feedback loops, self-loops)
zig build run-subgraph # Subgraph demo (clusters, nesting, SVG/JSON export)
zig build run-ns-compare # Compare layering algorithms
zig build run-json # JSON export
zig build run-fdg # Force-directed layout (terminal + SVG)
zig build run-fdg-bench # FDG performance benchmarks
zig build run-stress # Stress test suite
zig build run-benchmark # Sugiyama benchmarks
zig build run-svg-gallery # All SVG gallery examples at onceLicensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.










