Skip to content

Repository files navigation

zigraph

License

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)

Features

  • 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 edgesaddDiEdge / addUnDiEdge with 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

Installation

Run this command to add zigraph to your project:

zig fetch --save git+https://github.com/AshutoshMahala/zigraph

Then in build.zig:

const zigraph = b.dependency("zigraph", .{});
exe.root_module.addImport("zigraph", zigraph.module("zigraph"));

API Usage

1. Terminal Renderer (Box-Drawing)

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});

2. SVG Renderer (Web/Vector)

// 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.

3. JSON Renderer (Integration)

// 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.

Quick Start

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]

Edge Labels

Annotate edges with descriptive text:

try graph.addDiEdgeLabeled(1, 2, "requires");
try graph.addDiEdgeLabeled(2, 3, "queries");
try graph.addDiEdge(1, 3);  // unlabeled edge

Labels appear in all renderers — terminal, SVG, and JSON.

Cycle Breaking

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.

Subgraphs (Clusters)

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]   ║
 ║         ║
 ╚═════════╝

Nested Subgraphs

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 Support

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

Directed & Undirected Edges

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 addDiEdgeLabeled

Presets (Recommended)

Presets 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 ★★★★

Force-Directed Layout (FDG)

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();

FDG Perf (Apple M2)

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×

SVG Label Modes

// 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.

Renderers

Unicode (Terminal)

const output = try zigraph.render(&graph, allocator, .{});

SVG

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 });

JSON

const json = try zigraph.exportJson(&graph, allocator, .{});

See JSON_SCHEMA.md for the output format, or view assets/hero.json for an example.

Configuration

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,
});

Custom Crossing Reduction

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 },
},

Performance

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

Crossing Reduction Comparison (100 nodes)

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)

Complexity

  • Layout: O(passes × (V + E)) dominated by crossing reduction (V=nodes, E=edges)
  • Render: O(W × H) where W×H is output dimensions

Recommendations

  • <100 nodes: Use crossing.quality for best results
  • 100-1000 nodes: Use crossing.balanced (default)
  • >1000 nodes: Use crossing.fast or skip_validation = true
  • Wide layers (>20 nodes): Adjacent exchange auto-skips for performance

Architecture

  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.

Use Cases

  • CLI tools — Error chain visualization, build graphs
  • Compilers — AST/IR visualization
  • Documentation — Embedded diagrams
  • Embedded systems — Diagnostics on microcontrollers
  • WASM dashboards — Browser-based visualization

SVG Gallery

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)
Basic Diamond 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
Interactive
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       # Heatmap

Terminal Gallery

The 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 attributes

Examples

zig 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 once

License

Licensed under either of:

at your option.


Created by Ash • Inspired by ascii-dag (Rust)

About

Zero-dependency graph layout engine for Zig. Visualize DAGs in the terminal (Unicode/ANSI), SVG, or JSON.

Resources

Stars

17 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages