abstracttui-graph 0.5.0

Graph auto-layout for AbstractTUI: layered (sugiyama-lite), force-directed and grid passes over one GraphDesc -> Layout contract, deterministic and honesty-labeled.
Documentation

abstracttui-graph

Graph auto-layout AND rendering for AbstractTUI: an ADR-0004 sibling crate (public core API only, std + abstracttui as the whole dependency posture). Two halves, one crate: the layout engine (GraphDesc -> Layout) and the read-only widget (GraphView) drawing node cards and sub-cell edge strokes over the core canvas layer.

cargo add abstracttui abstracttui-graph

The family guide (pass selection, worked examples, the mermaid consumer) lives in the repo: docs/graphs-and-diagrams.md. API reference: docs.rs/abstracttui-graph.

The one contract: GraphDesc -> Layout

You describe the graph — nodes with cell sizes, edges by id — and a layout pass returns positions, ranks, edge waypoint polylines, a bounding box, and honesty markers. Every pass shares the same input and output types; consumers select the algorithm, never a different data contract:

Pass Shape For
layered(&desc, &LayeredOpts) sugiyama-lite: longest-path ranks, bounded median crossing-reduction sweeps, aligned-median coordinates, waypoints through rank gaps, TD/LR/BT/RL workflows, dependency/build graphs, state machines — DAG-shaped data
force(&desc, &ForceOpts) seeded, alpha-cooled repulsion + springs + optional rank bias; bounded budget, freezes on settle knowledge graphs — cyclic, dense, non-hierarchical data
grid(&desc) near-square row-major placement, always labeled the honest fallback

Honesty markers: cycle-broken edges are marked (EdgeLayout::broken, Layout::broken_edges()), never silently reordered; Layout::fallback names every degradation (node cap exceeded, duplicate ids dropped, unresolvable edges skipped, grid placement). Everything is deterministic (same input, identical Layout — golden-pinned; no transcendental floats, so goldens hold across platforms) and bounded (sweep counts, node cap, iteration budget — documented on the option types).

The force pass is an act, not an animation: run it on demand, cache the Layout, re-render from the cache. Zero idle cost is the caller's story and the engine's rule.

Example

use abstracttui_graph::{layered, Direction, GraphDesc, LayeredOpts};

let desc = GraphDesc::new()
    .node("fetch", 9, 3)   // id, width and height in cells
    .node("build", 9, 3)
    .node("test", 8, 3)
    .node("ship", 8, 3)
    .edge("fetch", "build")
    .edge("build", "test")
    .edge("build", "ship")
    .edge("test", "ship");

let layout = layered(&desc, &LayeredOpts {
    direction: Direction::TopDown,
    ..Default::default()
});

for node in &layout.nodes {
    println!("{} at {:?} (rank {})", node.id, node.rect, node.rank);
}
for edge in &layout.edges {
    // Waypoints run from the source card border to the target card
    // border; draw a polyline or spline through them.
    println!("{} -> {}: {:?}", edge.from, edge.to, edge.waypoints);
}
// The bounding box is the content size a Scroll container advertises.
assert_eq!((layout.bounds.x, layout.bounds.y), (0, 0));
assert!(layout.fallback.is_none(), "clean run");

// Plain-ASCII debugging aid (GraphView is the real renderer):
println!("{}", abstracttui_graph::dump::ascii(&layout));

The widget: GraphView

use abstracttui_graph::{GraphAlgo, GraphDesc, GraphView, LayeredOpts};

// Inside a component: GraphView::new(desc).view(cx) — cards with
// kind-tinted accents + badges, canvas-stroke edges (beziers through
// the layout waypoints, arrowheads, dotted/thick styles, cycle-broken
// edges dotted in the error ink), the fallback label as a notice
// line, pan via Scroll (bounds = content size), click/keyboard
// selection with `on_node_press`, hover tooltips.

One tab stop; arrows pan until a node is selected (Enter selects the first, then arrows walk nodes spatially — aligned-first — Enter presses, Escape returns to pan). Layout is an ACT at view build: rebuild inside a dyn_view over your data to relayout (force re-runs under its fixed seed; incremental reheat of cached positions is outside the widget's scope). Colors are caller-resolved (GraphStyle::from_tokens); a parked view idles at zero (test-pinned). Examples live in the root crate — from the workspace root, cargo run --example workflow (layered pipeline with a retry cycle) and --example network (force-placed concepts).

Status

Shipped and stable: the layout engine (layered(), force(), grid(), the ASCII dump helper) and GraphView with all four directions (BT/RL included), selection, tooltips and pan. The abstracttui-mermaid renderer consumes this crate as its layout authority — flowcharts you write as mermaid text land on these same passes.

License

MIT, same as AbstractTUI.