Layout functions
layout and layout-sequence are top-level functions exported by lib.typ, not members of a public layout module. Their generated entries come from the shared layout.typ implementation and use the public export names.
Concepts
Placements
graph.pos creates a first-class placement. The default mode: "pin" turns a coordinate into a fixed layout constraint and a drawable position. Use mode: "start" when the coordinate should only seed the layout:
#let g = build({
node(<a>, pos: graph.pos(x: -2, y: 0))
node(<c>, pos: graph.pos(ref: <a>, dx: 4, dy: 0))
edge(source(<a>), <a-c>, sink(<c>), pos: graph.pos(x: 0, y: 1.2))
})graph.group links one coordinate across several nodes or edge control points. A side of "+" keeps the coordinate positive, and "-" keeps it negative. GammaLoop external-edge columns use this to keep incoming and outgoing external legs on opposite sides while pairing rows by a shared y group:
#let edge-items = (
edge(
source(<right-ext>),
sink(<center>),
pos: graph.pos(x: graph.group("right", side: "+"), y: graph.group("edgee0")),
),
edge(
source(<center>),
sink(<left-ext>),
pos: graph.pos(x: graph.group("left", side: "-"), y: graph.group("edgee0")),
),
)Raw DOT input uses the same placement model through the pos attribute. The standard Graphviz subset is preserved: pos="x,y" is a starting coordinate and pos="x,y!" is pinned. Linnest extends the value with explicit id references and axis entries. In axis entries, ! belongs to that axis: numeric entries without ! are starting coordinates, numeric entries with ! are fixed constraints, and grouped entries must use ! because groups are constraints.
digraph {
a [id=0 pos="0,0!"]
b [id=1 pos="ref(node:0)+4,0!"]
a -> b [id=0 pos="ref(node:1)+0,1!"]
b -> c [id=1 pos="ref(edge:0)+1,0!"]
c [id=2 pos="x:2!"]
d [id=3 pos="x:2!,y:1"]
ext -> a [id=2 pos="x:@-left!,y:@edge0!"]
}The DOT grammar is intentionally id-based: ref(node:0) uses a node id and ref(edge:0) uses an edge id. Bare names and implicit edge order are not placement references. The @ syntax denotes grouped coordinate constraints; @+name and @-name keep the grouped coordinate on the positive or negative side respectively.
pos="x,y" // start x and y
pos="x,y!" // pin x and y
pos="x:<coord>" // start numeric x
pos="x:<coord>!" // pin x
pos="y:<coord>!" // pin y
pos="x:<coord>!,y:<coord>" // pin x, start numeric y
pos="x:<coord>,y:<coord>!" // start numeric x, pin yLayout Model
layout starts from a traversal-tree placement, then optimizes the positions of graph nodes and edge control points. The initial tree spacing is
,
with horizontal spacing and vertical spacing . Here is length-scale, is viewport-w, is viewport-h, is tree-dx, and is tree-dy. These fields set the geometry scale for both layout modes.
For deterministic, non-iterative placement, use layout-algo: "tree" or layout-algo: "dot". "tree" places a traversal forest by levels. "dot" uses a directed layered placement for acyclic inputs, falling back to tree placement when the selected edges contain a cycle. Both modes also work on a subgraph:
#let g = parse("digraph partial { a -> b; b -> c; c -> d; d -> a }").at(0)
#let tree = graph.forests(g).at(0)
#let g = layout(g, layout-algo: "tree", subgraph: tree)
#draw(g)Only nodes touched by the selected subgraph are placed. Edge control points are then resolved from the current node positions, so edges outside the selected subgraph are drawn as straight lines unless their own position constraints say otherwise.
layout-algo: "force" and layout-algo: "anneal" also accept subgraph. For these iterative modes, nodes and edge control points outside the selected subgraph stay fixed and act as boundary points while the selected subgraph is optimized.
Set layout-nodes: "fixed" to keep every node at its current pos for this layout pass and move only edge control points. With subgraph, only edges in the selected subgraph are moved; all other edge control points keep their current positions. The fixed-node policy is temporary; the returned graph stores the resulting coordinates as pos, but it does not turn them into persistent pin constraints. This is useful after one node-placement pass when a later pass should route or relax selected edges without disturbing the node layout:
#let g = parse("digraph partial { a [pos=\"0,0\"]; b [pos=\"4,0\"]; c [pos=\"8,0\"]; a -> b; b -> c }").at(0)
#let first = subgraph.bits(g, (true, true, false, false))
#let g = layout(g, layout-algo: "tree", layout-nodes: "fixed", subgraph: first)
#draw(g)The shared spring/charge model uses the following coefficients:
The Typst parameter names are beta for , gamma-ev for , gamma-ee for , g-center for , and gamma-dangling for . The spring stiffness is k-spring, and the softening constant is eps.
In layout-algo: "anneal", linnest minimizes an energy:
.
Here is crossing-penalty and is the number of detected edge crossings. The quadratic center term pulls nodes toward the origin at every radius. temp, step, seed, steps, epochs, cool, accept-floor, step-shrink, and incremental-energy belong to this simulated annealing mode. crossing-penalty is also anneal-only; force mode does not currently add a crossing force.
In layout-algo: "force", linnest applies the direct forces corresponding to the same vertex-vertex, edge-vertex, incidence spring, local edge-edge, dangling-edge, and center terms. step is the integration step, delta clamps per-step movement, steps and epochs set the iteration budget, cool shrinks the step after each epoch, and early-tol stops when movement is small. z-spring and z-spring-growth are force-only helpers: the integrator gives points temporary z coordinates to break overlaps and pulls them back toward the 2D plane.
directional-force is applied in both modes as an extra bias derived from pin/port direction constraints.
After either graph layout mode, labels are relaxed separately. If is the label target distance and is the label repulsion strength, then and . The Typst names are label-length-scale for and label-charge for . label-spring is the spring constant pulling each label toward its target. label-layout: "normal" uses a perpendicular offset target. With label-layout: "dangling-tangent", paired edges still use that perpendicular target, but dangling half-edge labels are offset along the edge direction away from the attached node. With label-layout: "fixed-length", the label remains at distance from the edge point and only rotates around it under repulsive forces. label-steps, label-step, label-early-tol, and label-max-delta-scale control the label relaxation iteration.
Qualified aliases
The exported layouts module exposes the same implementations for callers that prefer qualified access; these aliases link back to the canonical top-level entries below.
layouts.layout
Use layout for the shared signature and parameter reference.
layouts.sequence
Use layout-sequence for the shared multi-pass reference.
Reference
- layout()
- layout-sequence()
layout
Apply the linnest layout pass to a graph object.
This is intentionally a second step: construct or parse a graph first, then call layout. Set layout-algo to "force" for deterministic force integration, "anneal" for simulated annealing, "tree" for a traversal tree placement, "dot" for a Graphviz-like layered placement, or "stable-layered" for a stable railroad-inspired layered placement.
#let g = graph.parse("digraph partial { a -> b;a -> b;a:s -> b:s; b:s -> c:s; c:s -> d:s; d:s -> a:s }").at(0)
#let south = subgraph.compass(g,"s")
#let gf = layout(layout(g, layout-algo: "force"), layout-algo: "tree", layout-nodes: "fixed",subgraph:south)
#let ga = layout(g, layout-algo: "force")
#let gt = layout(layout(g, layout-algo: "tree",layout-roots: (2)), layout-algo: "anneal", layout-nodes: "fixed",gamma-ee:0.1,gamma-ev:.75,beta:5,length-scale:0.4)
#grid(columns: 3, gutter: 2cm, draw(gf), draw(ga), draw(gt))
#let g = graph.parse("digraph partial { a -> b; b -> c; c -> d; d -> a }").at(0)
#let tree = graph.forests(g).at(0)
#let g = layout(g, layout-algo: "tree", subgraph: tree)
#graph.edges(g).map(edge => edge.pos)Parameters
graph
dictionary
Graph object returned by graph.build or graph.parse.
subgraph
none or bytes
Optional subgraph object to lay out. With "tree", other edges are drawn from the resulting node positions. With "dot" and "stable-layered", the subgraph determines rank constraints, while all paired edges between included nodes get dummy routing vertices and edge positions. With "force" and "anneal", nodes and edges outside the subgraph are fixed boundary points during optimization.
Default: none
viewport-w
float
Width of the layout viewport used to derive the natural spring length. Applies to both "force" and "anneal".
Default: 10.0
viewport-h
float
Height of the layout viewport used to derive the natural spring length. Applies to both "force" and "anneal".
Default: 10.0
tree-dx
float
Horizontal spacing multiplier for traversal-tree and layered placement. For "force" and "anneal", this scales the initial placement.
Default: 0.9
tree-dy
float
Vertical spacing multiplier for traversal-tree and layered placement. For "force" and "anneal", this scales the initial placement.
Default: 1.2
steps
int
Iterations per epoch. In "force" mode this is the number of force integration steps; in "anneal" mode this is the number of proposals per temperature epoch.
Default: int(sys.inputs.at("steps", default: "30"))
seed
int
Seed for deterministic initialization, force-mode jitter, and annealing proposals. Applies to both modes.
Default: int(sys.inputs.at("seed", default: "2"))
step
float
Initial movement scale. "force" multiplies computed forces by this value; "anneal" uses it as a proposal step size in natural spring-length units.
Default: 0.81
step-shrink
float
Anneal-only step shrink factor, applied when an epoch’s acceptance ratio falls below accept-floor.
Default: 0.21
cool
float
Cooling factor applied once per epoch. "anneal" cools temp; "force" shrinks step.
Default: 0.85
accept-floor
float
Anneal-only acceptance-ratio threshold below which step is shrunk by step-shrink.
Default: 0.15
early-tol
float
Force-only early stop threshold for maximum movement in one step, as a multiple of the natural spring length. The annealing schedule stores this value but does not currently use it for stopping.
Default: 1e-6
temp
float
Anneal-only initial temperature used in the Metropolis acceptance test, scaled by natural spring length squared.
Default: 0.3
delta
float
Force-mode maximum movement clamp per point and per step, as a multiple of the natural spring length.
Default: 0.4
beta
float
Base repulsion strength for vertex-vertex interactions. Also scales gamma-ev, gamma-ee, gamma-dangling, and g-center. Applies to both modes through the shared spring energy.
Default: 50.0
k-spring
float
Spring stiffness for node-to-edge incidence lengths. Applies to both modes.
Default: 11.0
g-center
float
Centering strength relative to beta. Applies to both modes.
Default: 0.002
epochs
int
Number of epochs. Both modes run up to steps iterations inside each epoch.
Default: 30
crossing-penalty
float
Anneal-only fixed energy penalty per detected edge crossing. The direct force integrator does not currently add a crossing force.
Default: 30.0
gamma-dangling
float
Repulsion for dangling half edges, relative to beta. Applies to both modes through the shared spring energy.
Default: 5.0
gamma-ee
float
Local edge-edge repulsion, relative to beta. Applies to both modes.
Default: 0.1
directional-force
float
Bias that pushes points in directions implied by pin/port constraints. Force mode treats this as a force and scales it by natural spring length; anneal mode treats it as a dimensionless multiplier on proposal steps. Applies to both modes.
Default: 5.0
label-length-scale
float
Edge-label target offset as a multiple of the graph spring length. Label layout runs after both graph layout modes.
Default: 0.6
label-spring
float
Spring strength pulling each label toward its target offset in the spring-based label layouts.
Default: 23.0
label-charge
float
Repulsion strength between labels and graph points, scaled by spring length squared. Label layout runs after both modes.
Default: 3.0
label-steps
int
Maximum number of post-layout label relaxation steps. Set to 0 to skip label placement. Applies after both modes.
Default: 20
label-layout
string
Edge-label relaxation model. "normal" uses a perpendicular offset, "dangling-tangent" uses the edge direction for dangling half-edge labels and a perpendicular offset for paired edges, and "fixed-length" keeps each label at a fixed distance from its edge point and only lets that segment rotate.
Default: "normal"
label-step
float
Label relaxation step size. Applies after both modes.
Default: 0.15
label-early-tol
float
Label relaxation early stop threshold. Applies after both modes.
Default: 1e-3
label-max-delta-scale
float
Label movement clamp as a multiple of spring length. Applies after both modes.
Default: 0.5
gamma-ev
float
Edge-vertex repulsion, relative to beta. Applies to both modes.
Default: 0.01
eps
float
Softening epsilon used in inverse-square force/energy terms. Applies to both modes.
Default: 1e-4
incremental-energy
bool
Whether annealing updates cached energy by local deltas. This is anneal-only; force mode computes direct forces instead of energies.
Default: true
layout-algo
string
Layout algorithm. Use "force" for direct force integration, "anneal" for simulated annealing against the spring energy, "tree" for a traversal-tree placement, "dot" for a Graphviz-like layered placement, or "stable-layered" for a stable railroad-inspired layered placement.
Default: "force"
layout-nodes
string
Node movement policy. "layout" lets the layout algorithm move nodes. "fixed" keeps every node at its current position for this layout pass and only moves edge control points. With subgraph, only edges in the subgraph are moved; other edge control points stay at their current positions.
Default: "layout"
layout-direction
string
Direction for traversal-tree and layered rank placement. "down" places increasing ranks downward; "right" swaps the layout axes so increasing ranks go left-to-right and measured node/label widths reserve rank-axis space.
Default: "down"
rank-align
string
Alignment of real nodes inside a rank along the rank axis. "center" keeps node centers aligned, while "start" / "left" and "end" / "right" align the corresponding measured node-box side.
Default: "center"
layout-roots
array
Ordered node indices used as preferred roots for "tree", "dot", and "stable-layered". Roots outside the selected node set are ignored. Remaining components are laid out afterward in graph order.
Default: ()
rank-same
array
Subgraphs whose incident nodes should share a dot/stable-layered rank. These are layout hints supplied by Typst rather than parsed graph structure.
Default: ()
route-edge-weight
float
Dot/stable-layered also honors a node statement layout-rank as an exact non-negative integer rank. Nodes with the same layout-rank are placed on the same horizontal layer, and larger ranks are placed lower. Relative layout weight for paired edges outside the dot/stable-layered rank subgraph. Lower values make these edges guide routing without dominating the rank tree.
Default: 0.15
route-exit-weight
float
Extra horizontal straightening weight for the first or last segment of an edge with source-route-exit or sink-route-exit set to a vertical side.
Default: 4.0
route-label-width-scale
float
Multiplier for measured edge-label width when sizing non-rank dummy routing vertices in dot/stable-layered layout.
Default: 1.0
route-label-width-cap
float
Maximum non-rank dummy label width as a multiple of tree-dx. Set to 0 or a negative value to disable the cap.
Default: 2.0
z-spring
float
Force-only spring pulling temporary z coordinates back toward the layout plane. Higher values keep the visible 2D forces from being hidden by the temporary 3D symmetry-breaking offsets.
Default: 2.0
z-spring-growth
float
Force-only per-epoch multiplier for z-spring.
Default: 1.0
length-scale
float
Natural spring-length multiplier. This scales the graph’s preferred edge length and dimensional force/energy terms so changing only this value mostly zooms the result instead of retuning the force ratios. Applies to both modes.
Default: 0.35
layout-sequence
Apply multiple layout passes in order.
Each pass is a dictionary of named arguments accepted by layout, excluding the graph itself.
Parameters
graph
dictionary
Graph object returned by graph.build or graph.parse.
passes
array
Array of layout option dictionaries.