flowmaid 0.1.1

Mermaid-like flowchart engine in pure std Rust: hand-written parser, Sugiyama-style layout, SVG renderer, and an interactive scene API for drag-and-drop apps. Zero dependencies.
Documentation

flowmaid

A small Mermaid-like flowchart diagram engine written in pure std Rust with zero external dependencies. Takes Mermaid-syntax text and produces SVG.

Usage

cargo build --release

# from a file
./target/release/flowmaid examples/demo.mmd -o demo.svg

# or through a pipe
cat examples/lr.mmd | ./target/release/flowmaid > lr.svg

# during development
cargo run -- examples/demo.mmd -o demo.svg
cargo test

It also works as a library (the crate is lib + bin):

let svg = flowmaid::render_svg("flowchart TD\nA[Start] --> B[Done]")?;

Supported syntax

The header sets the flow direction: flowchart TD (top-down, alias TB), LR (left-right), RL, or BT. The keyword graph is also accepted. Lines starting with %% are comments, and ; separates multiple statements on one line.

Node shapes: A[text] rectangle, A(text) rounded, A([text]) stadium, A{text} diamond, A((text)) circle. Labels may be quoted to protect special characters: A["odd [text]"].

Edges: --> arrow, --- open line, -.-> dotted, ==> thick. Edge labels are written -->|text|. Chains like A --> B --> C are supported, as are cycles (E --> B looping back up) and self-loops (A --> A).

Complete examples live in examples/demo.mmd and examples/lr.mmd.

Architecture

A three-stage pipeline, one module per stage:

  1. parser.rs — hand-written character-cursor parser. Each line is parsed into a chain of nodes and edges, with line-numbered error messages.
  2. layout.rs — a compact Sugiyama-style algorithm: (a) DFS marks back-edges so cycles can't break the layering, (b) longest-path layering assigns nodes to layers, (c) alternating barycenter sweeps reduce edge crossings, (d) coordinates come from per-layer packing followed by alignment towards the mean neighbour position without overlaps. Everything is computed in abstract coordinates (breadth × layer), so all four diagram directions are handled by a single transform at the end.
  3. render.rs — maps abstract coordinates to final x,y according to the direction, then draws bezier curves with arrowheads (SVG markers), clips lines exactly at shape borders (rectangles, circles, and diamonds each have their own intersection formula), and places labels at curve midpoints.

model.rs holds the shared data structures (Graph, Node, Edge, shape and direction enums).

For interactive apps there is the scene module: scene() produces final ready-to-draw geometry (node positions, edge bezier curves), route() re-routes edges for custom node positions such as user drags, and to_svg() exports any arrangement. render() is now just a wrapper over the same pipeline. See examples/drag_sim.rs and the egui demo app.

Performance

A built-in benchmark lives in examples/bench.rs (pure std, deterministic synthetic graphs) — run it with cargo run --release --example bench. Measurements on Linux x86_64, rustc 1.75, release build, best of 3 runs:

nodes edges parse layout render* SVG
49 100 0.04 ms 0.03 ms 0.29 ms 23 KB
200 400 0.16 ms 0.08 ms 1.13 ms 97 KB
1,000 2,010 0.84 ms 0.50 ms 6.16 ms 505 KB
2,500 5,050 1.97 ms 1.30 ms 16.35 ms 1,278 KB
5,000 10,150 4.16 ms 2.75 ms 34.92 ms 2,618 KB

* the render column includes running layout internally.

End-to-end through the CLI for the 5,000-node case — including reading 10,151 input lines and writing a 2.7 MB SVG — takes about 60 ms with ~9 MB peak RAM. The quadratic-trap case (2 layers × 2,500 nodes side by side) finishes in 21 ms, so practical scaling is linear. For realtime use this means: re-rendering from scratch on every keystroke is fine for reasonable diagrams (~0.3 ms), and the 60 fps budget (16 ms) is only reached around 2,500 nodes. The bottleneck is SVG string building, not the algorithms. Numbers depend on hardware — re-measure on your machine with the command above, and always use --release (debug builds are ~10× slower).

Interactivity & desktop apps

Beyond static SVG, the engine exposes an interactive API for GUI apps through the scene module: scene(&graph) returns a Scene — the position, size, and shape of every node plus the bezier curve of every edge in final coordinates — ready to draw with any framework's painter. When a node is dragged, call route(&graph, &positions) to re-route edges for custom positions without re-running layout — so nodes never jump back. to_svg(&scene) exports any state, including after drags. Hit-testing is done by the app from the Scene geometry (per-node position + size + shape are all there).

A complete demo (separate crate; the engine itself stays dependency-free) shows a live text editor with a last good render pattern on the left, a drag & drop canvas with zoom & pan on the right, .mmd file drop, and SVG export — built on eframe/egui. For other frameworks: Tauri/Dioxus can inject the SVG string into a webview; iced has an svg widget; Slint and GTK4 render SVG natively; or draw the Scene directly with each framework's painter like the egui demo does.

Limitations & ideas

Already handled: the canvas is computed from the bounding box of all curve control points so self-loops and back-edges are never clipped; parallel edges (same node pair) separate automatically; long edges that align with a column of nodes bow sideways as a mitigation.

Still open: text width is estimated (~character-class table) since there are no real font metrics, so very long labels or CJK can be off; the long-edge mitigation is only a heuristic — the real solution is virtual nodes per crossed layer; edge labels can collide with nodes on dense diagrams; escaped quotes (\") inside labels aren't supported. Mermaid features that would make good next exercises: subgraph, fan-out A --> B & C, -.- and --text--> lines, cylinder shape [( )], and per-node styling (style/classDef).

License

GPL-3.0-or-later — free for everyone to use; distributed derivatives must remain open source under the same license. Full text in the LICENSE file.