Skip to content

Graph layout ​

controller.edits.layout(layout) arranges root-level nodes with one of five layouts, as one undoable edit. A Layout names the layout and carries its options; the installed LayoutExtension() performs all five.

dart
final controller = DiagramController(
  configuration: DiagramConfiguration(
    extensions: [EditorExtension(), const LayoutExtension()],
  ),
);
(await controller.edits.layout(
  Layout.layered(direction: LayoutDirection.right, gap: 60, layerGap: 100),
)).requireAllowed();
LayoutOptionsPlaces nodes
Layout.layered()direction (down, up, left, right), gap, layerGapOn layers along the direction with few edge crossings (ELK's layered algorithm). Connected components sit side by side.
Layout.tree()direction (down, up, left, right, radial), gap, levelGapAs a tidy tree: each parent centred over its children, subtrees packed without overlap, each level as deep as its largest node. radial puts the levels on rings, each subtree in a wedge of its parent's in proportion to its leaves.
Layout.force()iterations, gapBy Fruchterman–Reingold forces between node centres, with gravity that keeps disconnected parts close, then removes overlaps.
Layout.circular()radius, gapOn one circle in input order. A null radius is the smallest that keeps gap between neighbours.
Layout.grid()columns, rows, flow, gap, rowGap, columnGap, cellsIn grid cells. Row and column sizes follow their largest node.

Every layout keeps gap between nodes (and layerGap or levelGap between layers or levels), is deterministic, and places nothing on top of another node. Options are validated when the layout is created: a negative or non-finite gap, a non-positive count or radius, or an empty or negative cell throws an ArgumentError.

Trees from any graph ​

A tree layout lays out a breadth-first spanning tree. Its roots are the nodes without incoming edges, in input order; a cycle that no root reaches starts at its first node. Each node sits one level below the node that first reached it, so every tree edge points in the layout's direction, and each tree sits beside the previous one.

Grids ​

flow: GridFlow.rows (the default) fills across each row and wraps after columns cells; flow: GridFlow.columns fills down each column and wraps after rows cells. When the wrapping count is null it is the node count divided by the other count, or the square root of the node count when both are null. Nodes beyond rows × columns add rows (row flow) or columns (column flow).

cells places nodes by id, like CSS grid placement: GridCell(row: 1) puts a node in the first free cell of row 1, GridCell(column: 3) in the first free cell of column 3, and GridCell(row: 1, column: 3) in that cell. The other nodes fill the free cells in flow order. A lane is a row, a stage on a timeline is a column, and gap: 0 packs the cells tightly.

dart
await controller.edits.layout(
  Layout.grid(
    gap: 32,
    cells: {
      'plan': const GridCell(row: 0, column: 0),
      'build': const GridCell(row: 1, column: 1),
      'ship': const GridCell(row: 2, column: 2),
    },
  ),
);

Locked nodes ​

Locked nodes stay where they are and the other nodes are placed around them. The layout moves as a whole so that the first locked node keeps its position; other locked nodes become fixed obstacles that the remaining nodes move past. A force layout treats every locked node as a fixed obstacle in the simulation. Without locked nodes, the laid-out nodes keep the top-left corner of their bounds.

Applying a layout ​

layout applies all positions through one revision-checked applyLayout verb. Undo restores the previous arrangement. Pass motion for a mounted canvas to animate the nodes and their connectors to their new places; the document has the final positions at once:

dart
await controller.edits.layout(Layout.tree(), motion: Motion.smooth);

A document change, active edit, newer layout request, or controller.cancelLayout() prevents a pending result from being applied. Cancellation invalidates the result; it does not interrupt computation. Invalid or incomplete output cannot partially move the graph. The layout is denied with unsupportedOperation when no installed extension performs it.

Layout uses resolved node sizes. Without ownerId, it arranges root nodes; ownerId arranges the direct children of a container in that owner's axes. Explicit nodeIds must be siblings. Moving a child-owning node translates its descendants together and preserves their offsets. Locked nodes remain pinned; internal edges inform placement and boundary bindings remain intact.

dart
await controller.edits.layout(Layout.grid(columns: 3), ownerId: 'swimlane');

The layered layout uses the published pure-Dart elk package. No JavaScript engine, worker script, npm dependency or network service is required. On native Dart platforms, layouts run in an isolate to keep computation off the UI thread. Flutter web executes the same Dart code in its compiled runtime; it yields before starting, but the calculation itself is synchronous, so measure representative graphs before laying out large ones interactively.

Custom algorithms ​

GraphLayoutAlgorithm<L> performs one layout type, such as TreeLayout, and is part of extension_api.dart (package:vyuh_diagram_core/extension_api.dart, re-exported by package:vyuh_diagram_flutter/extension_api.dart). Extend it and implement layout(GraphLayoutRequest request, L layout) to return a GraphLayoutResult with one finite top-left position per requested node, keeping pinned nodes where they are. The request holds the node sizes, current positions, pins and directed edges. Install it with DiagramExtension(id: 'app.layout', graphLayouts: [...]); a diagram installs at most one algorithm per layout type. Synchronous and isolate-backed implementations use the same API.

Try the topology graph and the layout gallery.