On this page

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, for x/y, a drawable position. Use mode: "start" when the coordinate should only seed the layout, or use graph.pin(value) / graph.start(value) to choose a mode for one axis:

#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))
})

Auxiliary z is force-layout depth in the same numeric layout units as x/y, not a Typst length or a rendered coordinate. Both graph.build and graph.map accept graph.pos(z: graph.pin(2)) for a hard raw-depth pin and graph.pos(z: graph.start(2)) for a movable initial depth. A bare graph.pos(z: 2) follows mode, defaulting to "pin":

#let g = graph.map(g,
  node: node => (pos: graph.pos(z: graph.pin(2))),
  edge: edge => (pos: graph.pos(z: graph.start(-1))),
)

A z-only placement leaves XY coordinates, constraints, and partial-position flags alone, including automatic edge midpoint seeds. Omitting z preserves existing depth metadata during unrelated XY patches. ref, dx, and dy affect XY only; z groups, relative depth references, dz, and non-finite z values are not supported.

Node and edge statements expose "pos-z" as finite numeric text and "pos-z-mode" as "pin" or "start". These describe the supplied raw pin or seed; output pos remains a two-coordinate x/y record. The force solver multiplies raw depth by a global scale that flattens to zero: a hard raw pin remains fixed even when its effective depth is zero. Other layout backends do not use z. Auxiliary depth does not change draw order or provide an over/under rendering layer.

graph.group links one XY coordinate across several nodes or edge control points. A side of “+” keeps the coordinate positive, and “-” keeps it negative. Use start to seed the shared coordinate without fixing it. Matching node and edge groups are one degree of freedom during force and annealing layouts, so repulsion and springs act on their combined force rather than being reconciled afterward. If several members supply starts for one group, their mean initializes the shared coordinate. 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: "+", start: 4), 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 y

Layout Model

layout starts from a traversal-tree placement, then optimizes the positions of graph nodes and edge control points. The initial tree spacing is

𝐿=𝜆𝑊𝐻max(𝑛,1)

,

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. Isolated subgraph layout uses the number of selected incident nodes for 𝑛; other layouts use the full graph node count.

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. The default solver.subgraph-mode: "fixed-boundary" keeps nodes and edge control points outside the selection fixed while retaining their spring, repulsion, centroid, and crossing interactions. Set solver: (subgraph-mode: "isolated") to omit unselected half edges, nodes, control points, and labels from those interactions. Selected halves of otherwise paired edges become dangling boundaries of the isolated solver domain. Complement coordinates and labels remain unchanged, and an empty selection is a no-op. An isolated selection cannot split a grouped coordinate; expand the selection or remove that group.

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)

Related controls can be supplied as shallow semantic records. These records only name graph-layout concepts; they do not select a drawing domain or style:

#let common = layouts.options(
  spring: (strength: 8, length: 0.5),
  repulsion: (edge-node: 0.05, dangling: 2),
  constraints: (side-strength: 5),
  labels: (steps: 0),
  solver: (algorithm: "force", steps: 50),
)
#let spacious = layouts.options(
  base: common,
  spring: (length: 0.7),
  repulsion: (dangling-centroid: 3),
)
#let g = layout(g, ..spacious)

layouts.options performs a sparse update: the second spring record above changes length without losing the inherited strength. Grouped fields override their corresponding flat arguments. Flat arguments remain useful for numerical experiments and controls that do not have a semantic grouping.

The shared spring/charge model uses the following coefficients:

𝑐vv=𝛽𝐿3𝑐ev=𝛽𝛾ev𝐿3𝑐ee=𝛽𝛾ee𝐿3𝑐center=𝛽𝑔center𝑐dangling=𝛽𝛾dangling𝐿3𝑐dangling-centroid=𝛽𝛾dangling-centroid𝐿3

The Typst parameter names are beta for 𝛽, gamma-ev for 𝛾ev, gamma-ee for 𝛾ee, g-center for 𝑔center, gamma-dangling for 𝛾dangling, and gamma-dangling-centroid for 𝛾dangling-centroid. The spring stiffness 𝑘 is k-spring, and the softening constant 𝜀 is eps.

In layout-algo: "anneal", linnest minimizes an energy:

𝐸=∑𝑖<𝑗12𝑐vv𝑑(𝑣𝑖,𝑣𝑗)+𝜀+∑𝑖,𝑒𝑐ev𝑑(𝑣𝑖,𝑒)+𝜀+∑(𝑣,𝑒) incident12𝑘(ℓ𝑒−𝑑(𝑣,𝑒))2+∑local edge pairs12𝑐ee𝑑(𝑒𝑖,𝑒𝑗)+𝜀+∑dangling pairs12𝑐dangling𝑑(𝑒𝑖,𝑒𝑗)+𝜀+∑dangling𝑒𝑐dangling-centroid𝑑(𝑒,𝑣)+𝜀+∑𝑖12𝑐center𝑑(𝑣𝑖,0)2+𝑝cross𝑁cross

.

Here 𝑣 is the mean node position. The centroid term pushes every dangling endpoint away from that mean; its equal-and-opposite reaction is shared over the nodes so it introduces no net force. 𝑝cross is crossing-penalty and 𝑁cross 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, dangling-centroid, and center terms. step is the integration step, delta clamps per-step movement, steps and epochs set the iteration budget, and cool shrinks the step after each epoch. early-tol can stop the run when movement is small only after the effective depth scale reaches zero; small movement or a cooled-to-zero step cannot skip flattening.

The force-only depth-scale (default 1.0) and flattening-end (default 0.5) control auxiliary depth. depth-scale must be finite and non-negative; flattening-end must be finite and in 0..=1. They can be supplied as flat arguments or in solver: (depth-scale: 1.0, flattening-end: 0.5); grouped values override flat ones. The old depth-spring controls are not aliases.

Effective depth is scale * raw-z. The integrator starts with auxiliary raw depths to break overlaps and smoothly reduces scale from depth-scale to zero over the initial flattening-end fraction of the total steps × epochs iteration budget. For normalized progress u from 0 to 1 over that interval, the smoothstep scale is depth-scale * (1 - u)^2 * (1 + 2 * u). The remaining iterations relax projected overlaps with effective depth exactly zero, so repulsion cannot be satisfied by invisible separation. flattening-end: 0 or depth-scale: 0 starts in 2D; flattening-end: 1 still evaluates the final iteration on the exact plane.

There is a single original iteration budget and cooling schedule: flattening neither adds a second phase nor restarts step. Unpinned raw depths stay bounded by the initial spread and supplied depth magnitudes; hard raw-depth pins remain fixed even after their effective depth has flattened to zero. XY pins, shared coordinates, and fixed subgraph boundaries apply throughout; label layout runs only after this single graph pass. Output positions remain 2D, and auxiliary z does not alter draw order. Flattening is exact, while force convergence remains limited by the iteration budget and movement tolerance.

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 𝑞𝑙=𝛽𝑙𝐿2. 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", relaxation keeps the label at distance 𝐿𝑙 from the edge point and only rotates it under repulsive forces. label-steps, label-step, label-early-tol, and label-max-delta-scale control the label relaxation iteration.

graph.style measures node and label sizes before layout; those sizes enter repulsion during label relaxation. For labels with measured width and height, a subsequent box-collision pass starts from the relaxed positions. It keeps each position when its box clears node boxes and previously placed label boxes, otherwise searching nearby candidates. This correction may change the distance from the edge point, including in "fixed-length" mode.

Repulsion considers edge layout points, and box correction considers nodes and labels; neither guarantees clearance from complete edge curves or drawing layers such as momentum arrows. Attached path labels may also be positioned again during drawing, as described above.

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

  • options()
  • layout()
  • layout-sequence()

options

Construct reusable semantic layout options or sparsely update existing ones.

Each option group is a plain dictionary. Supplying base preserves every field not mentioned by a patch, including fields inside a patched group. Pass the result to layout with argument spreading.

#let common = options(
  spring: (strength: 8, length: 0.5),
  repulsion: (edge-node: 0.05, dangling: 2),
  solver: (algorithm: "force", steps: 50),
)
#let spacious = options(
  base: common,
  spring: (length: 0.7),
  repulsion: (dangling-centroid: 3),
)
#let g = layout(g, ..spacious)

Parameters

options(
..patches: arguments ,
base: dictionary
) -> dictionary
..patches

arguments

Semantic option-group patches.

base

dictionary

Existing option dictionary to update.

Default: (:)

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.

Force layout uses one steps × epochs iteration budget and one cooling schedule. Effective auxiliary depth is scale * raw-z: scale follows a smoothstep from depth-scale to zero over the initial flattening-end fraction of that budget. Remaining iterations are planar, without restarting step or cooling. early-tol can stop the run only after scale reaches zero. Unpinned raw depths remain bounded; hard raw-depth pins are preserved even when their effective depth is zero. Output positions remain 2D, and auxiliary depth does not change draw order; label layout runs after the single graph pass.

#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

layout(
graph: dictionary ,
spring: none dictionary ,
repulsion: none dictionary ,
constraints: none dictionary ,
labels: none dictionary ,
solver: none dictionary ,
subgraph: none dictionary ,
viewport-w: float ,
viewport-h: float ,
tree-dx: float ,
tree-dy: float ,
steps: int ,
seed: int ,
step: float ,
step-shrink: float ,
cool: float ,
accept-floor: float ,
early-tol: float ,
temp: float ,
delta: float ,
beta: float ,
k-spring: float ,
g-center: float ,
epochs: int ,
crossing-penalty: float ,
gamma-dangling: float ,
gamma-dangling-centroid: float ,
gamma-ee: float ,
directional-force: float ,
label-length-scale: float ,
label-spring: float ,
label-charge: float ,
label-steps: int ,
label-layout: string ,
label-step: float ,
label-early-tol: float ,
label-max-delta-scale: float ,
gamma-ev: float ,
eps: float ,
incremental-energy: bool ,
layout-algo: string ,
layout-nodes: string ,
layout-direction: string ,
rank-align: string ,
layout-roots: array ,
rank-same: array ,
route-edge-weight: float ,
route-exit-weight: float ,
route-label-width-scale: float ,
route-label-width-cap: float ,
depth-scale: float ,
flattening-end: float ,
length-scale: float
) -> dictionary
graph

dictionary

Graph object returned by graph.build or graph.parse.

spring

none or dictionary

Semantic incidence-spring options. Supported fields are strength and length. These override the corresponding flat parameters below.

Default: none

repulsion

none or dictionary

Semantic repulsion options. Supported fields are strength, centering, edge-node, edge-edge, dangling, dangling-centroid, and softening. These override the corresponding flat parameters below.

Default: none

constraints

none or dictionary

Semantic constraint options. Supported fields are side-strength, node-movement, direction, rank-alignment, roots, and same-rank. These override the corresponding flat parameters below.

Default: none

labels

none or dictionary

Semantic label-layout options. Supported fields are distance, spring, repulsion, steps, model, step, tolerance, and max-movement. These override the corresponding flat parameters below.

Default: none

solver

none or dictionary

Semantic solver options. Supported fields are algorithm, steps, epochs, seed, step, step-shrink, cooling, acceptance-floor, tolerance, temperature, max-movement, incremental-energy, subgraph-mode, crossing-penalty, depth-scale, and flattening-end. These override the corresponding flat parameters below.

Default: none

subgraph

none or dictionary

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", solver.subgraph-mode: "fixed-boundary" keeps outside points fixed but interacting; "isolated" excludes them from the solver and label-layout interactions. The selection must have compatible topology, and isolated selections cannot cross grouped coordinates.

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 within the single depth-flattening schedule; in "anneal" mode this is the number of proposals per temperature epoch.

Default: 30

seed

int

Seed for deterministic initialization, force-mode jitter, and annealing proposals. Applies to both modes.

Default: 2

step

float

Initial movement scale. "force" multiplies computed forces by this value, with no restart when depth reaches zero; "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, eligible only after effective depth scale reaches zero. 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, gamma-dangling-centroid, 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. Force mode shares this budget and its cooling schedule across depth flattening and the remaining planar relaxation.

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-dangling-centroid

float

Repulsion of every dangling endpoint from the current node centroid, relative to beta. The equal-and-opposite reaction is shared over the nodes, avoiding translational drift. Applies to both modes.

Default: 0.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

Subgraph objects whose incident nodes should share a dot/stable-layered rank; node-index arrays and callbacks receiving the graph are also accepted. 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

depth-scale

float

Force-only initial multiplier of raw auxiliary depth, finite and >= 0. Effective z is this smoothly decreasing scale times raw z, not a spring pulling raw coordinates toward the plane. A value of 0 starts in 2D; flattening preserves hard raw-depth pins while removing their invisible separation from the visible 2D force calculation.

Default: 1.0

flattening-end

float

Force-only fraction of the total steps × epochs iteration budget over which the depth scale reaches zero by smoothstep, finite and in 0..=1. Zero starts in 2D; 1 still evaluates the final iteration on the exact plane. Neither value adds a phase or restarts the cooling schedule.

Default: 0.5

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

layout-sequence(
graph: dictionary ,
passes: array
) -> dictionary
graph

dictionary

Graph object returned by graph.build or graph.parse.

passes

array

Array of layout option dictionaries.