Zigraph: Modular graph layout engine for DAG visualization

I’ve been working on zigraph, a modular DAG layout engine. It started as a port of my rust library ascii-dag, but has evolved into something more ambitious i.e. graph layout layout infrastructure where each component can be used independently

  • Currently supports: Sugiyama layout (Median Heuristic, Barycenter) with Brandes-Kopf positioning
  • output supported are internal IR, terminal, SVG, JSON
  • BYOA (Bring your own Algorithm) - trying to be modular as much as I can

Examples:

usage:

const std = @import("std");
const zigraph = @import("zigraph");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // 1. Build the graph
    var graph = zigraph.Graph.init(allocator);
    defer graph.deinit();

    try graph.addNode(1, "Root");
    try graph.addNode(2, "Task A");
    try graph.addNode(3, "Task B");
    try graph.addNode(4, "Output");

    try graph.addEdge(1, 2);
    try graph.addEdge(1, 3);
    try graph.addEdge(2, 4);
    try graph.addEdge(3, 4);

    // 2. Render to Terminal (with colored edges!)
    const TUI = try zigraph.render(&graph, allocator, .{
        .edge_palette = &zigraph.colors.ansi_dark,
    });
    defer allocator.free(TUI);
    
    std.debug.print("{s}\n", .{TUI});

    // 3. Or export directly to SVG
    // const SVG = try zigraph.svg.render(&graph, allocator, .{ ... });
}

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

This is a v0.1 release, so things might be a bit rough. I’m open to all feedback and PRs to help smooth it out

18 Likes

Update: 0.2.0

  1. Sugiyama: Now supports cycle breaking and displays cycle, opposite direction edge is dotted
  2. Edge labels are now supported
  3. Refined edge routing
  4. Force Directed graph now supported for undirected graph
  5. Added more refined presets for ease of usability.
  6. There is a change in syntax.
    ‘’’
    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
    ‘’’



This is 0.2.0. Next version will remove addEdge() in favour of addDiEdge and add addUnDiEdge for better understanding. FDG uses Q16.16 by default for calculation for determinism but can be changed as per need. FDG may not be as polished as Sugiyama layout. Next 3-4 version will be breaking in terms of DX and IR. I’m trying to front load architectural changes so that later things become stabilized for users.

Next version will be about, renderer modularity and subgraph support. :upside_down_face:

2 Likes

This is a neat project, kudos.

At that point you could parse and render dot, which would be pretty handy, a remarkable number of tools emit it.

I’m sure you’re well aware! Consider this a vote for dot input. “ports” and compass points can feasibly be ignored, or handled later, they’re treated more like requests than commands by graphViz.

2 Likes

Thank you for the compliment! :slightly_smiling_face:
To be honest, I did think about .dot file. I will support it as a secondary file input. .dot file is good and widely adopted but has limitations. I was thinking to create a new DSL for this but after probably around or after 0.6.0.

Because parsing dot is tricky and it forces users to choose a graph type (you can do mixed but it is clanky). I was thinking in terms of supporting graph as mixed by default. I am aware of the pipeline like Sugiyama depends on direction would need special handling. But designing this custom flat DSL is little far.

Out of Ports and Compass point support, I might support port, given my aim is to have custom node shape in svg renderer.

That said I did think about things, but let’s see how execution happens.

And thanks again!

1 Like

That’s very much what I meant, just for the convenience of how many tools are already equipped to create it. The most useful subset for that purpose is mostly what you already have, plus subgraphs.

So a policy of “parse but ignore” would go pretty far, add attributes as they come up, assume that structurally valid .dot is semantically correct, because if it’s from a tool it will be. Doesn’t need all 3,111,696 arrow shapes. :slight_smile:

In any case

Always, I’m sure you’ll take the project in the direction which makes the most sense to you.

1 Like

Why is using Q16.16 necessary for determinism ? f32 does not behave in the same way everywhere ?

1 Like

I’m trying to create reproducible graph layouts across all the environments zig supports. But f32/f64 are deterministic only in same platform, same execution model. they are not deterministic across platforms, since graphs are iterative these small differences accumulate over iterations and can lead to very different outcomes. So I am favoring bit-for-bit determinism here. It does have its own drawbacks.

1 Like