graph module
Use graph for graph construction, DOT parsing, inspection, data transforms, joins, and graph algorithms. Import the module for qualified calls, or import selected functions from it when a short construction DSL is clearer.
Concepts
Graph Specs
The Typst construction API is half-edge first: create node items, create source and sink half-edge endpoints, then pass edge items to graph.build. graph.build accepts both comma-separated items and ordinary Typst code blocks. Typst labels such as <a>, <h1>, and <e1> are API names; node and edge names are emitted back from nodes(g) and edges(g) as Typst labels. They are resolved before the wire format is sent to the Rust plugin. Numeric id arguments choose graph indexes or ordering: on nodes, id fixes the resulting node index and must be unique and in bounds. On nodes, edges, sources, and sinks, extra named arguments are captured as opaque Typst data fields: edge(source(<a>, style: (stroke: red + 0.7pt)), sink(<b>), kind: "link") stores (style: ..) in the source data and (kind: "link") in the edge data. These user data values are not sent through the Rust plugin boundary. Typst sends an internal opaque payload with correlation data, asks Rust to resolve the graph indexes, then stores user data in arrays at those resolved graph, node, edge, and half-edge ids. graph.info, graph.nodes, and graph.edges merge those native values into the returned records. A captured label data field on nodes and edges is display content used by the default drawing style; use statements: (label: "...") when a flat metadata label string is needed. Statements are flat metadata used by DOT; they cannot nest. Values are scalar strings/numbers/booleans. Use data fields for structured Typst data or content.
graph.build and graph.parse also accept default-node-data, default-edge-data, default-source-data, and default-sink-data. These defaults are merged into the corresponding data; captured data fields on nodes, edges, sources, and sinks override the defaults. Drawing callbacks decide how those fields affect the result. For example, endpoint styles can read data.style from the source and sink half-edge records:
#let g = build({
node(<a>, label: [a])
node(<b>, label: [b])
edge(
source(<a>, style: (stroke: red + 0.7pt)),
<e>,
sink(<b>, style: (stroke: blue + 0.7pt)),
label: [connection],
kind: "dependency",
)
},
default-edge-data: (kind: "propagator"),
)
#let endpoint-style(edge, side) = {
let endpoint = edge.at(side + "-half-edge", default: none)
if endpoint == none { return (:) }
endpoint.data.at("style", default: (:))
}
#draw(
layout(g),
source-style: edge => endpoint-style(edge, "source"),
sink-style: edge => endpoint-style(edge, "sink"),
edge-label: edge => edge.at("label", default: none),
)When parsing DOT, the same native data arrays can be filled from selected string fields. Rust parses DOT into topology and statement metadata; Typst applies defaults and evaluates selected fields afterward. The selected fields are evaluated with the record’s merged fields dictionary in scope. Since node names are labels, use #str(name) when a name should become visible text:
#let g = parse(
"digraph g { a [label=\"A\"]; a -> b [label=\"$p$\", source=\"out\", sink=\"in\"] }",
eval-node-fields: ("label",),
eval-edge-fields: ("label",),
eval-source-fields: ("statement",),
eval-sink-fields: ("statement",),
).first()
#nodes(g).first().data.label
#edges(g).first().data.labelThe same transform can run after construction. This is useful for global edge statements that should apply to every edge while still seeing local edge fields:
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), sink(<b>), statements: (mom: "p"))
}, default-edge-statements: (display-label: "$#mom$"))
#let g = graph.eval-fields(g, eval-edge-fields: ("display-label",))
#edges(g).first().data.at("display-label")#let g = build({
node(<a>)
node(<c>)
edge(source(<a>), <e1>, sink(<c>))
})Named nodes and edges can be updated after construction without scanning in the caller. The update replaces the native data, or it can be a callback receiving (data, record):
#let g = build({
node(<a>)
node(<c>)
edge(source(<a>), <e1>, sink(<c>))
})
#let g = update-node-data(g, <a>, (label: [A]))
#let g = update-edge-data(g, <e1>, (data, edge) => (
label: [$p$],
source: edge.source.node,
))
#node-data(g, <a>).label
#edge-data(g, <e1>).labelsource(..) and sink(..) accept a node reference plus optional name, id, statement, and compass. name is a Typst label name for the half-edge; id is numeric:
edge(source(<a>, name: <h1>, id: 0), <e1>, sink(<c>, id: 2), label: [a-c])One half-edge creates an external edge. The side is determined by the constructor, so there is no public flow argument:
edge(<incoming>, sink(<a>))
edge(source(<c>), <outgoing>)graph.edge(..., spring-length: 1.5) multiplies that edge’s existing preferred spring length by 1.5 in force/anneal layout. The factor must be positive, finite, and dimensionless; it applies to each node-to-control-point spring, including the existing doubled rest length for dangling edges. The argument defaults to none, preserving local or default spring-length statements; a missing statement means 1. An explicit argument overrides those statements. This is not a guaranteed rendered length, a rendering option, or a change to global layout spring.length; tree/dot/stable-layered layout is unaffected.
graph.build does not interpolate statement strings on the Rust side. Use graph.eval-fields or graph.map when a default statement should turn into Typst content or structured data. The evaluation scope includes the record’s merged fields, so default edge statements can still refer to local edge fields:
#let g = build({
node(<a>)
node(<c>)
edge(
source(<a>),
<a-c>,
sink(<c>),
label: [a-c],
statements: (color: "0055ff", label: "a-c"),
)
},
default-edge-statements: (
color: "000000",
display-label: "$#label$",
),
)
#let g = graph.eval-fields(g, eval-edge-fields: ("display-label",))Graph object API
Graph objects are Typst dictionaries wrapping archived Rust graph bytes plus native Typst data arrays for graph, node, edge, source, and sink data. The Rust graph may also carry an internal opaque payload, but that payload is used only by the Typst wrapper and is not exposed in public records. Build or parse graph objects with graph, transform graph objects with layout, and pass objects back to graph or subgraph for inspection. Subgraphs are also dictionaries: they combine an archived half-edge selection with a topology signature and Typst-only annotations, rather than exposing raw bytes as public inputs.
graph.parse(input)parses one or more DOT digraphs and returns an array of graph objects. Itseval-graph-fields,eval-node-fields,eval-edge-fields,eval-source-fields, andeval-sink-fieldsarguments are convenience arguments forgraph.eval-fields.graph.build(..)constructs one graph object from a stream of node and edge items.graph.map(graph, ..)maps graph, node, edge, source, and sink records to new native data or structural patches without changing topology.nodeandedgeaccept a callback or a dictionary keyed by current record names. Each dictionary value is a patch, a callback receiving the full record, ornone. Edge patches may setspring-lengthdirectly; it is validated and stored as the layout statement. Unlisted records remain unchanged; unknown names are errors.graph.cut(graph, left: left, right: right, boundary: patch)opens a weighted directed cut into a new graph, preserving Typst data and recording origins.graph.boundaries(view)queries its current boundary endpoints.graph.node-data(graph, <name>)andgraph.edge-data(graph, <name>)return one named node or edge data.graph.update-node-data(graph, <name>, data)andgraph.update-edge-data(graph, <name>, data)update one named node or edge data. Direct replacements and callbacks run in Typst with(data, record).graph.eval-fields(graph, ..)evaluates selected record fields into data entries. It works on parsed and built graph objects.node(..)returns a node item.source(..)andsink(..)return half-edge endpoints.edge(..)returns an edge item built from source/sink endpoints and an optional edge name.layout(graph, ..)runs layout as an explicit second step. Its settings are named parameters so calls stay descriptive and Tidy can document each field.draw(graph, ..)draws a laid-out graph object with CeTZ.dot(graph)returns a DOT string for inspection or export.
Graph Queries
graph.info(g) returns graph metadata. nodes(g) returns node records, and edges(g) returns edge records. Node and edge record name values are Typst labels when present. Pass subgraph: sg to filter nodes or edges by a compatible subgraph object. Nodes must be incident to a selected half-edge; an edge is included if either half is selected, but its record keeps both available endpoints. These queries do not extract or open topology.
After graph.cut, node, edge, and half-edge records carry origin relative to the immediate input graph. Boundary nodes and dangling cut edges also carry boundary. Use graph.boundaries(view) for a uniform endpoint query, including current positions after placement or layout. Auxiliary nodes remain visible in graph.nodes(view); filter with node.boundary == none to exclude them.
graph.join(left, right, key: "statement") joins dangling half edges with opposite flows. The supported keys are "statement", "compass", "port-label", and "id"; unmatched half edges remain in partial joins. Matching identities must be unique within each input. Node and edge labels that occur in both inputs are rejected. Graph, node, edge, and endpoint data is preserved: right-only dictionary fields are retained and conflicting fields use the left value. The left graph supplies the name and layout configuration. Use graph.join-edges when the matching key is an edge-level statement rather than half-edge metadata.
graph.cycles(g) returns subgraph objects for a cycle basis. graph.forests(g) returns subgraph objects for spanning forests. Both return arrays of the same topology-bound dictionaries accepted by subgraph, graph queries, layout, and draw, not arrays of raw selection bytes.
graph
- from-spec()
- build()
- parse()
- node()
- source()
- sink()
- edge()
- group()
- pin()
- start()
- pos()
- map()
- style()
- eval-fields()
- info()
- dot()
- nodes()
- edges()
- node-data()
- edge-data()
- update-node-data()
- update-edge-data()
- cut()
- boundaries()
- join()
- join-edges()
- cycles()
- forests()
from-spec
Construct a graph from a versioned Linnest graph-spec CBOR document.
This is the native renderer boundary. Unlike parse, it does not parse DOT.
Parameters
dictionary build
Build one graph object from a stream of node and edge items.
Use node, source, sink, and edge to create graph items. Positional items may be passed as comma-separated arguments or yielded from a Typst code block.
Parameters
..items:
array ,name:
none string ,data:
any ,statements:
dictionary ,default-node-statements:
dictionary ,default-edge-statements:
dictionary ,default-node-data:
any ,default-edge-data:
any ,default-source-data:
any ,default-sink-data:
any ) ->
dictionary ..items
array
Node and edge items returned by node and edge. The best way to use these is to use a code scope, so that the edges and nodes append each other:
#let g = build({
node(<a>, label: [a])
node(<b>, label: [b])
node(<c>, label: [c])
edge(source(<a>), sink(<b>))
edge(source(<b>), sink(<c>))
edge(source(<a>), sink(<c>))
})name
none or string
Graph name.
#let g = build(name: "My Graph", {
})
#info(g).nameDefault: none
data
any
Native Typst graph data.
#let g = build(data: (a:(b:(1, ))), {
})
#info(g).dataDefault: none
statements
dictionary
Flat graph statements, that get turned into a string to string dictionary in rust. Used by DOT parsing; values cannot nest.
#let g = build(statements: (a:1), {
})
#info(g).global-statementsDefault: (:)
default-node-statements
dictionary
Flat default node statements. Used by DOT; values cannot nest.
Default: (:)
default-edge-statements
dictionary
Flat default edge statements. Used by DOT parsing; values cannot nest. These are applied/delegated to all edges.
#let g = build(default-edge-statements: (a:1), {
node(<a>)
edge(source(<a>))
edge(sink(<a>))
})
#edges(g).map(e=>e.statements)Default: (:)
default-node-data
any
Default data merged into every node data. Captured node data fields override it.
#let g = build(default-node-data: (a:1), {
node(<a>)
node(<b>,a:2)
node(<c>,b:(a:1))
})
#nodes(g).map(n=>n.data)Default: none
default-edge-data
any
Default data merged into every edge data. Captured edge data fields override it.
#let g = build(default-edge-data: (a:1,b:[$m_mu$]), {
node(<a>,id:0)
edge(source(<a>),id:1)
edge(sink(<a>),<e>,id:0,b:[#set text(font:"Reforma")
This is a test: ])
})
#edges(g).map(e=>e.data.b).join()Default: none
default-source-data
any
Default data merged into every source half-edge data. Captured source data fields override it.
#let g = build(default-source-data: (a:1), {
node(<a>)
edge(source(<a>))
edge(sink(<a>))
})
#edges(g).map(e=>if e.source != none {e.source.data} else {none})Default: none
default-sink-data
any
Default data merged into every sink half-edge data. Captured sink data fields override it.
#let g = build(default-sink-data: (a:1), {
node(<a>)
edge(source(<a>))
edge(sink(<a>))
})
#edges(g).map(e=>if e.sink != none {e.sink.data} else {none})Default: none
parse
Parse one or more DOT graphs into graph objects.
Default data are applied before eval-* fields are evaluated, so default data strings can refer to parsed record fields such as #str(name). Parsed fields take precedence over default data fields, so DOT label="..." overrides default-node-data: (label: ...).
Linnest uses dot-parser for DOT syntax and then gives special meaning to a small set of attributes. Every other attribute is preserved as a flat string statement. Preserved statements are visible through info, nodes, edges, map, and eval-fields, but they do not become native Typst data unless a matching eval-* argument is passed.
#let src = ```dot
digraph { a [label="A"]; }
```
#let n = nodes(parse(src.text).first()).first()
#n.statements.label
#n.dataGraph-level handling:
- The DOT graph name becomes
graph.info(g).name. A graph-levelnameattribute can name an anonymous graph; a namedgraph/digraphheader takes precedence.
#let src = ```dot
digraph header_wins { graph [name="ignored"]; a }
```
#info(parse(src.text).first()).name- Top-level
key=valuestatements andgraph [key=value]attributes becomegraph.info(g).global-statements, except for the consumedname. They do not configure layout, even when a key looks like a layout option; pass layout options directly tolayout.
#let src = ```dot
digraph { steps=1; graph [subtitle="nested"]; a }
```
#let statements = info(parse(src.text).first()).global-statements
#statements.steps
#statements.subtitlenode [key=value]andedge [key=value]becomedefault-node-statementsanddefault-edge-statements. They are merged into each parsed node or edge before local attributes, so local attributes override defaults.
#let src = ```dot
digraph { node [color=gray]; edge [particle=g]; a [color=red]; a -> b }
```
#let g = parse(src.text).first()
#nodes(g).first().statements.color
#edges(g).first().statements.particleNode handling:
- The DOT node id becomes the node
namereturned bynodes. If the DOT node id is numeric, it is consumed as the node index instead and the publicnameisnone.
#let src = ```dot
digraph { alpha; 1; }
```
#nodes(parse(src.text).first()).map(n => n.name)id=<n>is also consumed as an explicit node index. Explicit node indexes must be unique and in bounds after parsing.
#let src = ```dot
digraph { a [id=1]; b [id=0]; }
```
#nodes(parse(src.text).first()).map(n => n.name)style=invismarks the DOT node as a dangling external endpoint. It is not returned bynodes. Its remaining attributes are copied onto the external edge that touches it, exceptshapeandlabel;styleandidare consumed.
#let src = ```dot
digraph { ext [style=invis, column=left, label=skip]; ext -> a [id=0]; }
```
#let g = parse(src.text).first()
#nodes(g).map(n => n.name)
#edges(g).first().statements.column
#edges(g).first().statements.at("label", default: none)- Node
styleis consumed for every node; onlystyle=invishas special behavior. Use another attribute name for drawing style metadata that must survive parsing.
#let src = ```dot
digraph { a [style=filled, "draw-style"=filled]; }
```
#nodes(parse(src.text).first()).first().statementsposis parsed as node placement. Simple numeric values are exposed asnode.pos; extended placement values are used bylayout.pinis an explicit layout constraint and is not exposed as a public statement. Thepos-x-setandpos-y-setrecord fields say which axes were explicitly supplied, including for partial placements whose resolvedposcontains both coordinates.dotpreserves them as canonical Linnest DOT extension attributes.
#let src = ```dot
digraph { a [id=0, pos="0,0!"]; b [id=1, pos="ref(node:0)+2,0!"]; }
```
#let parsed-nodes = nodes(parse(src.text).first())
#parsed-nodes.map(n => n.pos)
#parsed-nodes.map(n => n.statements.at("pin", default: none))shiftis parsed as a drawing shift and also remains available as a statement.evalis retained for legacy round-tripping. Other node attributes are preserved as node statements.
#let src = ```dot
digraph { a [shift="0.1,0", eval=legacy, label=A]; }
```
#let n = nodes(parse(src.text).first()).first()
#n.shift
#n.statementsEdge handling:
id=<n>is consumed as the edge index. It is not preserved as a statement; drawing callbacks expose the resulting stable edge index aseid. Explicit edge indexes must be unique and in bounds. Without explicit ids, edge order is parser-internal, not DOT input order.
#let src = ```dot
digraph { a -> b [id=1, label=later]; b -> c [id=0, label=first]; }
```
#let parsed-edges = edges(parse(src.text).first())
#parsed-edges.map(e => e.edge)
#parsed-edges.map(e => e.statements.label)
#parsed-edges.map(e => e.statements.at("id", default: none))dir=forward,dir=back, anddir=nonebecome edge orientations"default","reversed", and"undirected". If omitted, directed DOT edges are"default"and undirected DOT edges are"undirected".
#let src = ```dot
digraph { a -> b [id=0]; b -> c [id=1, dir=back]; c -> d [id=2, dir=none]; }
```
#edges(parse(src.text).first()).map(e => e.orientation)source="..."andsink="..."are consumed as endpointstatementvalues and removed from edge statements. On an external edge, use the attribute matching the real endpoint side in the DOT edge:ext -> ausessink=..., whilea -> extusessource=....
#let src = ```dot
digraph { ext [style=invis]; ext -> a [id=0, sink=in]; a -> ext [id=1, source=out]; }
```
#let endpoint-statement(endpoint) = if endpoint == none { none } else { endpoint.at("statement", default: none) }
#let parsed-edges = edges(parse(src.text).first())
#parsed-edges.map(e => endpoint-statement(e.source))
#parsed-edges.map(e => endpoint-statement(e.sink))
#parsed-edges.map(e => e.statements.at("source", default: none))posis parsed as the edge control-point placement.pinis an explicit edge layout constraint.shift,label-pos,label-angle, andbendare parsed into edge geometry fields; except forpin, they also remain available as statements. Thepos-x-setandpos-y-setrecord fields identify explicitly supplied axes, including partial placements;dotpreserves them as canonical Linnest DOT extension attributes. Other edge attributes are preserved as edge statements.
#let src = ```dot
digraph { a -> b [id=0, pos="0,1!", pin="x:@edge", shift="0.1,0", "label-pos"="0,1.2", "label-angle"="0.3rad", bend="0.4rad", particle=g]; }
```
#let e = edges(parse(src.text).first()).first()
#e.pos
#e.shift
#e.label-pos
#e.label-angle
#e.bend
#e.statements.particle
#e.statements.at("pin", default: none)- Attributes copied from an invisible endpoint node are merged into the external edge statements unless their keys are
shapeorlabel.
#let src = ```dot
digraph { ext [style=invis, column=left, shape=none, label=skip]; ext -> a [id=0]; }
```
#edges(parse(src.text).first()).first().statementsPort and half-edge handling:
- DOT ports of the form
node:portandnode:port:compassare preserved on source/sink endpoint records. Numeric ports also assign explicit half-edge ids. Non-numeric ports are exposed only asport-label.
#let src = ```dot
digraph { a:left:e -> b:0:w [id=0]; }
```
#let e = edges(parse(src.text).first()).first()
#e.source.port-label
#e.sink.hedge- Compass values are exposed as
compass; valid DOT compass names includen,ne,e,se,s,sw,w,nw,c, and_.
#let src = ```dot
digraph { a:n -> b:sw [id=0]; }
```
#let e = edges(parse(src.text).first()).first()
#e.source.compass
#e.sink.compass- Explicit half-edge ids from numeric ports must be unique and in bounds. They are retained for half-edge ordering and for
joinwithkey: "id"; query and eval records expose the resulting half-edge index ashedge.
#let src = ```dot
digraph { a:1 -> b:0 [id=0]; }
```
#let e = edges(parse(src.text).first()).first()
#e.source.hedge
#e.sink.hedgePlacement fields:
pos="x,y"gives a starting coordinate;pos="x,y!"pins both axes.
#let src = ```dot
digraph { a [id=0, pos="0,0"]; b [id=1, pos="2,0!"]; a -> b; }
```
#let parsed = parse(src.text).first()
#nodes(parsed).map(n => n.pos)
#nodes(layout(parsed, steps: 10, seed: 1)).map(n => n.pos)pos="ref(node:<id>)+dx,dy!"andpos="ref(edge:<id>)+dx,dy!"reference explicit DOT node or edge ids, not names and not implicit parse order.
#let src = ```dot
digraph { a [id=0, pos="1,1!"]; b [id=1, pos="ref(node:0)+2,0!"]; a -> b [id=0, pos="ref(node:1)+0,1!"]; }
```
#let g = parse(src.text).first()
#nodes(g).at(1).pos
#edges(g).first().pos- Axis form accepts
x:<coord>andy:<coord>entries.!applies to the individual axis, for examplepos="x:2!,y:0".
#let src = ```dot
digraph { a [id=0, pos="x:2!,y:0"]; }
```
#nodes(parse(src.text).first()).first().pos- Group coordinates use
@name,@+name, or@-namein axis form and must be pinned with!, for examplepos="x:@-left!,y:@row!".
#let src = ```dot
digraph { ext [style=invis]; ext -> a [id=0, pos="x:@-left!,y:@row!"]; }
```
#edges(layout(parse(src.text).first(), layout-algo: "tree")).first().pospinaccepts numeric point constraints,x:<coord>,y:<coord>, and grouped constraints such asx:@left,x:@+right, or@row.
#let src = ```dot
digraph { a -> b [id=0, pin="x:@+right"]; }
```
#edges(layout(parse(src.text).first(), layout-algo: "tree")).first().statements.at("pin", default: none)Parameters
input:
string bytes ,default-node-data:
any ,eval-node-fields:
string array ,default-edge-data:
any ,eval-edge-fields:
string array ,default-source-data:
any ,eval-source-fields:
string array ,default-sink-data:
any ,eval-sink-fields:
string array ,eval-graph-fields:
string array ,eval-mode:
string ,scope:
dictionary ) ->
array input
string or bytes
DOT source text containing one or more graph or digraph definitions.
default-node-data
any
Default data merged into every node data. Captured node data fields override it, but bare dot statements don’t set data fields. To turn statements into data fields, use eval-node-fields.
#let a = ```dot
digraph {
a -> b -> c -> d -> a
a -> a
b -> d
c [particle="q"]
}
```
#let g = parse(a.text,default-node-data:(particle:"g")).at(0)
#nodes(g).map(n=>n.data.particle)Default: none
eval-node-fields
string or array
Node fields to evaluate into node data. The eval scope includes node statements plus node, name, and pos.
#let src = ```dot
digraph { a [label="#str(name)"]; }
```
#nodes(parse(src.text, eval-node-fields: "label").first()).first().data.labelDefault: ()
default-edge-data
any
Default data merged into every edge data. Captured edge data fields override it, but bare dot statements don’t set data fields. To turn statements into data fields, use eval-edge-fields.
Default: none
eval-edge-fields
string or array
Edge statement fields to evaluate into graph.edges(g).at(i).data. The eval scope includes edge statements plus edge, orientation, endpoint records, placement fields, and existing edge data. Drawing callbacks later expose the edge index as eid.
#let src = ```dot
digraph { a -> b [id=0, label="#orientation"]; }
```
#edges(parse(src.text, eval-edge-fields: "label").first()).first().data.labelDefault: ()
default-source-data
any
Default data merged into every source half-edge data. Captured source data fields override it.
Default: none
eval-source-fields
string or array
Source half-edge fields to evaluate into edge.source.data. The eval scope includes source endpoint fields statement, port-label, compass, node, and hedge, plus the surrounding edge fields.
#let src = ```dot
digraph { a:left:e -> b [id=0, source="#port-label"]; }
```
#edges(parse(src.text, eval-source-fields: "statement").first()).first().source.data.statementDefault: ()
default-sink-data
any
Default data merged into every sink half-edge data. Captured sink data fields override it.
Default: none
eval-sink-fields
string or array
Sink half-edge fields to evaluate into edge.sink.data. The eval scope includes sink endpoint fields statement, port-label, compass, node, and hedge, plus the surrounding edge fields.
#let src = ```dot
digraph { a -> b:0:w [id=0, sink="#str(hedge)"]; }
```
#edges(parse(src.text, eval-sink-fields: "statement").first()).first().sink.data.statementDefault: ()
eval-graph-fields
string or array
Graph statement fields to evaluate into graph.info(g).data. The eval scope includes graph statements and direct graph record fields.
#let src = ```dot
digraph {
title = "Strong graph";
}
```
#info(parse(src.text, eval-graph-fields: "title").first()).data.titleDefault: ()
eval-mode
string
Typst eval mode used for string field values.
Default: "markup"
scope
dictionary
Additional Typst names available while evaluating field values.
Default: (:)
node
Create a graph node item for build.
A Typst label is the node name used by source, sink, and pos. The optional numeric id fixes the resulting graph node index. Extra named arguments are captured as node data fields, so node(<a>, label: [A], color: red) stores (label: [A], color: red). The default draw style uses data.label as the visible node label when present.
Parameters
..args:
label ,name:
none label ,id:
none int ,pos:
none dictionary ,shift:
none string array dictionary ,statements:
dictionary ) ->
array ..args
label
Optional positional node name. Must be a Typst label when provided; extra named arguments become data fields.
name
none or label
Typst node name for references.
Default: none
id
none or int
Numeric graph node index. Must be unique and in bounds when provided.
Default: none
pos
none or dictionary
Node placement.
Default: none
shift
none or string or array or dictionary
Drawing shift stored as a statement.
Default: none
statements
dictionary
Additional flat node statements. Used by DOT; values cannot nest.
Default: (:)
source
Create a source half-edge endpoint.
node may be a node name like <a> or a numeric node index. name gives the half-edge a Typst name; id is a numeric half-edge order/index override. Extra named arguments are captured as source data fields.
Parameters
node:
label int ,..args:
any ,name:
none label ,id:
none int ,statement:
none string ,compass:
none string ) ->
dictionary node
label or int
Referenced node, either by Typst label name or numeric node index.
..args
any
Extra named arguments become source data fields.
name
none or label
Optional half-edge name used for later references.
Default: none
id
none or int
Optional numeric half-edge index/order override.
Default: none
statement
none or string
DOT-ish flat statement used for matching/joining dangling half edges.
Default: none
compass
none or string
DOT compass point such as "n", "s", "e", or "w".
Default: none
sink
Create a sink half-edge endpoint.
node may be a node name like <a> or a numeric node index. name gives the half-edge a Typst name; id is a numeric half-edge order/index override. Extra named arguments are captured as sink data fields.
Parameters
node:
label int ,..args:
any ,name:
none label ,id:
none int ,statement:
none string ,compass:
none string ) ->
dictionary node
label or int
Referenced node, either by Typst label name or numeric node index.
..args
any
Extra named arguments become sink data fields.
name
none or label
Optional half-edge name used for later references.
Default: none
id
none or int
Optional numeric half-edge index/order override.
Default: none
statement
none or string
DOT-ish flat statement used for matching/joining dangling half edges.
Default: none
compass
none or string
DOT compass point such as "n", "s", "e", or "w".
Default: none
edge
Create a graph edge item for build.
Positional arguments may contain one source, one sink, and optionally one Typst label used as the edge name. The numeric id chooses the edge order. Extra named arguments are captured as edge data fields, so edge(source(<a>), sink(<b>), weight: 3) stores (weight: 3). style is first-class drawing metadata: auto asks draw for its configured/default style, a dictionary or callback patches that style, and none hides the painted edge. The default draw style uses data.label as the visible edge label when present.
Parameters
..args:
any ,name:
none label ,id:
none int ,orientation:
string ,pos:
none dictionary ,shift:
none string array dictionary ,label-pos:
none string array dictionary ,label-angle:
none int float string ,bend:
none int float string ,spring-length:
none int float ,style:
auto dictionary array function none ,statements:
dictionary ) ->
array ..args
any
Source/sink half-edges and optional edge name; extra named arguments become data fields.
name
none or label
Typst edge name.
Default: none
id
none or int
Numeric edge order/index override.
Default: none
orientation
string
Edge orientation: "default", "reversed", or "undirected".
Default: "default"
pos
none or dictionary
Edge placement.
Default: none
shift
none or string or array or dictionary
Drawing shift stored as a statement.
Default: none
label-pos
none or string or array or dictionary
Edge label position stored as a statement.
Default: none
label-angle
none or int or float or string
Edge label angle stored as a statement.
Default: none
bend
none or int or float or string
Edge bend stored as a statement.
Default: none
spring-length
none or int or float
Positive finite dimensionless multiplier of each incidence’s preferred spring length in force/anneal layout, including dangling-edge doubling. none preserves statements/defaults; a missing statement means 1. Does not set rendered length or change global spring.length.
Default: none
style
auto or dictionary or array or function or none
Static logical-edge style, callback, auto fallback, or none to hide.
Default: auto
statements
dictionary
Additional flat edge statements. Used by DOT; values cannot nest.
Default: (:)
group
Create a grouped placement coordinate.
side: "+" keeps the solved coordinate non-negative and side: "-" keeps it non-positive. start supplies an initial value without fixing the coordinate; multiple starts for one group are averaged. Groups are layout constraints and therefore require pin placement, which is the pos default. Node and edge coordinates with the same group name and side share one axis coordinate throughout layout.
#group("right", side: "+", start: 4)Parameters
name:
string int bool ,side:
none string ,start:
none int float ) ->
dictionary name
string or int or bool
Group identifier shared by positions constrained to the same coordinate.
side
none or string
Optional sign constraint: "+", "-", "positive", or "negative".
Default: none
start
none or int or float
Optional initial coordinate; unlike pin, this remains movable.
Default: none
pin
Mark one coordinate as a layout constraint.
Parameters
int float dictionary ) -> dictionary value
int or float or dictionary
Coordinate value to constrain.
start
Mark one coordinate as an initial layout value only.
Parameters
int float ) -> dictionary value
int or float
Coordinate value to use as the layout seed.
pos
Create a first-class graph placement.
The default mode: "pin" turns numeric coordinates into layout constraints and also makes the XY coordinates immediately drawable without a layout pass. Use start(value) for an individual coordinate that should only seed the layout, or pin(value) to pin an individual numeric coordinate when mode: "start" is used. Grouped coordinates are always layout constraints for their axis.
z is auxiliary depth for force-based layout only, in the same numeric layout units as x and y (not Typst lengths). Use pos(z: pin(2)) for a hard raw-depth pin or pos(z: start(2)) for a movable initial depth. A bare pos(z: 2) follows mode, defaulting to "pin". The global depth scale and flattening schedule may reduce effective depth to zero without changing a hard raw-depth pin. This is not a rendered third coordinate: node/edge output pos and drawing remain 2D. Statements retain the configured raw pin/seed, not the solved free depth, as "pos-z" (finite numeric text) and "pos-z-mode" ("pin" or "start"). Other layout backends do not use z.
Both build and map accept z placements on nodes and edge control points. A z-only placement leaves XY placement and constraints alone, including automatic edge midpoint seeds. Omitting z preserves existing depth metadata. Z groups, relative depth references, dz, and non-finite values are not supported; ref, dx, and dy continue to affect XY only.
#pos(x: group("right", side: "+"), y: start(10), z: pin(2))Parameters
x:
none int float dictionary ,y:
none int float dictionary ,z:
none int float dictionary ,ref:
none label int ,dx:
none int float ,dy:
none int float ,mode:
string ) ->
dictionary x
none or int or float or dictionary
Absolute or grouped x coordinate.
Default: none
y
none or int or float or dictionary
Absolute or grouped y coordinate.
Default: none
z
none or int or float or dictionary
Auxiliary force-layout depth, numeric or wrapped in pin / start.
Default: none
ref
none or label or int
Node reference for relative XY placement, by name or numeric index.
Default: none
dx
none or int or float
Relative x offset from ref.
Default: none
dy
none or int or float
Relative y offset from ref.
Default: none
mode
string
Placement mode: "pin" constrains layout, "start" only seeds it.
Default: "pin"
map
Map graph records with native-data and structural patches. Position patches merge supplied axes: omitted coordinates retain their existing values, pin/start modes and coordinate groups.
node and edge accept either a callback for every record or a name-keyed dictionary. Dictionary keys are exact string names: a matches <a>, and "D1.0" matches the <D1.0> fragment produced by cut. Each entry is a constant patch dictionary, a callback, or none. Unlisted records and unnamed records are skipped by name-keyed dictionaries. Every supplied name must exist, even when its entry is none. Unknown names and invalid mapper, entry, or callback return types are errors. There are no numeric-ID keys, array selectors, wildcard/default selectors, or recursive merges. graph, source, and sink still accept only a callback or none.
The callbacks receive the same full decoded records in either form, plus a fields dictionary merging native dictionary data, statements, and direct record fields, with later sources taking precedence. A callback returns none to leave the record unchanged, (data: value) to set new native data, or structural fields such as pos, shift, spring-length, and statements to patch data seen by later layout calls. spring-length is validated as a positive finite multiplier and stored in the edge statements used by the layout engine. Structural patches are applied separately from native Typst data, so content and stored functions in the existing data value are preserved. Unchanged fields in a returned full record are not reapplied; this preserves partial XY placements when patching z or data. Source and sink callbacks may likewise patch statement, port-label, and compass before subgraph and layout operations run.
Constant patches and callback results use identical semantics. Node structural keys are pos, shift, and statements; edges also accept label-pos, label-angle, bend, and spring-length. With no data key, other keys shallow-merge into existing native dictionary data (or replace non-dictionary data with that patch). An explicit data key instead replaces native data, ignoring other non-structural keys; data: none leaves native data unchanged. Native Typst content and stored functions are preserved, not serialized or invoked. A mapper or entry of none, an empty name map, an empty patch, or a callback returning none leaves the corresponding records unchanged.
#let g = build({ node(<a>) })
#let g = map(g, node: node => (data: (label: [A])))
#nodes(g).first().data.label#let g = build({
node(<a>); node(<b>)
edge(<D1>, source(<a>), sink(<b>), weight: 2)
})
#let g = map(g,
node: (a: (label: [A]), b: none),
edge: (D1: e => (weight: e.fields.weight + 1)),
)
#edge-data(g, <D1>).weightParameters
graph_:
dictionary ,graph:
none function ,node:
none function dictionary ,edge:
none function dictionary ,source:
none function ,sink:
none function ) ->
dictionary graph_
dictionary
Graph object to transform.
graph
none or function
Callback for graph metadata records.
Default: none
node
none or function or dictionary
Callback for all node records, or name-keyed patches/callbacks/no-ops.
Default: none
edge
none or function or dictionary
Callback for all edge records, or name-keyed patches/callbacks/no-ops.
Default: none
source
none or function
Callback for source half-edge records.
Default: none
sink
none or function
Callback for sink half-edge records.
Default: none
style
Attach layout-relevant drawing style to a graph.
Node style is measured immediately and stored as layout-width / layout-height statements for later layout calls. Edge labels are measured as label-width / label-height statements for label placement. draw uses the stored node, edge, endpoint, and label styles by default.
Parameters
graph_:
dictionary ,scope:
dictionary ,unit:
int float length ratio ,node-label:
auto content string function none ,node-label-style:
dictionary function ,node-style:
dictionary function none ,edge-style:
dictionary array function none ,source-style:
dictionary array function none ,sink-style:
dictionary array function none ,edge-label:
content string function none ,edge-label-style:
dictionary function ) ->
dictionary graph_
dictionary
Graph object to style.
scope
dictionary
Extra scope visible to label/style callbacks.
Default: (:)
unit
int or float or length or ratio
Coordinate length for one graph-layout unit. Numbers are interpreted as em.
Default: 1
node-label
auto or content or string or function or none
Node label content or callback. auto uses node data/statement label or node name.
Default: auto
node-label-style
dictionary or function
CeTZ content style for node labels. Its padding contributes to measured node size.
Default: (:)
node-style
dictionary or function or none
CeTZ node shape style or callback. Explicit numeric radii contribute to measured node size.
Default: (:)
edge-style
dictionary or array or function or none
Logical-edge style dictionary, layers, or callback used as drawing defaults. Explicit draw and record styles override these defaults. none hides edges.
Default: (:)
source-style
dictionary or array or function or none
Source endpoint defaults, composed before explicit draw and record styles.
Default: (:)
sink-style
dictionary or array or function or none
Sink endpoint defaults, composed before explicit draw and record styles.
Default: (:)
edge-label
content or string or function or none
Edge label content or callback. none leaves edge labels unstyled and unmeasured.
Default: none
edge-label-style
dictionary or function
CeTZ content style for edge labels. Its padding contributes to measured edge-label size.
Default: (:)
eval-fields
Evaluate selected fields into native data entries.
Each selected field is read from the record’s merged fields dictionary, evaluated in a scope containing those fields, and written to data.<field>.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), sink(<b>), statements: (mom: "p"))
}, default-edge-statements: (display-label: "$#mom$"))
#let g = eval-fields(g, eval-edge-fields: ("display-label",))
#edges(g).first().data.at("display-label")Parameters
graph_:
dictionary ,eval-graph-fields:
string array ,eval-node-fields:
string array ,eval-edge-fields:
string array ,eval-source-fields:
string array ,eval-sink-fields:
string array ,eval-mode:
string ,scope:
dictionary ) ->
dictionary graph_
dictionary
Graph object whose selected statement fields should be evaluated.
eval-graph-fields
string or array
Graph statement fields to evaluate into graph.info(g).data.
Default: ()
eval-node-fields
string or array
Node statement fields to evaluate into node data.
Default: ()
eval-edge-fields
string or array
Edge statement fields to evaluate into edge data.
Default: ()
eval-source-fields
string or array
Source half-edge fields to evaluate into source data.
Default: ()
eval-sink-fields
string or array
Sink half-edge fields to evaluate into sink data.
Default: ()
eval-mode
string
Typst eval mode used for string field values.
Default: "markup"
scope
dictionary
Additional Typst names available while evaluating field values.
Default: (:)
info
Return graph metadata.
The result has name, global-statements, default-edge-statements, and default-node-statements.
#let g = build({ node(<a>) }, name: "demo")
#info(g).nameParameters
dictionary ) -> dictionary graph
dictionary
Graph object returned by build, parse, layout, or another graph API.
dot
Serialize a graph object to DOT.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), sink(<b>))
}, name: "demo")
#dot(g).contains("digraph demo")Parameters
dictionary ) -> string graph
dictionary
Graph object to serialize.
nodes
Return node records, optionally filtered by a subgraph object.
Node name values are Typst labels when present.
#let g = build({
node(<a>)
node(<b>)
})
#nodes(g).map(node => str(node.name)).join(", ")Parameters
graph:
dictionary ,subgraph:
none dictionary ) ->
array graph
dictionary
Graph object to inspect.
subgraph
none or dictionary
Optional subgraph filter; only nodes incident to selected half edges are returned.
Default: none
edges
Return edge records, optionally filtered by a subgraph object.
Edge name values are Typst labels when present.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>, compass: "e"), sink(<b>))
})
#edges(g).len()Parameters
graph:
dictionary ,subgraph:
none dictionary ) ->
array graph
dictionary
Graph object to inspect.
subgraph
none or dictionary
Optional subgraph filter; selected edges retain their full endpoint records.
Default: none
node-data
Return one named node’s native data.
name is a Typst label such as <a> or the corresponding string name.
#let g = build({ node(<a>, label: [A]) })
#node-data(g, <a>).labelParameters
graph_:
dictionary ,name:
label string ) ->
any graph_
dictionary
Graph object to inspect.
name
label or string
Node name as a Typst label or its string form.
edge-data
Return one named edge’s native data.
name is a Typst label such as <e> or the corresponding string name.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), <e>, sink(<b>), label: [$p$])
})
#edge-data(g, <e>).labelParameters
graph_:
dictionary ,name:
label string ) ->
any graph_
dictionary
Graph object to inspect.
name
label or string
Edge name as a Typst label or its string form.
update-node-data
Update one named node’s native data.
update may be a replacement data value or a function (data, node) => new-data.
#let g = build({ node(<a>) })
#let g = update-node-data(g, <a>, (label: [A]))
#nodes(g).first().data.labelParameters
graph_:
dictionary ,name:
label string ,update:
any function ) ->
dictionary graph_
dictionary
Graph object to update.
name
label or string
Node name as a Typst label or its string form.
update
any or function
Replacement data or (data, node) => new-data callback.
update-edge-data
Update one named edge’s native data.
update may be a replacement data value or a function (data, edge) => new-data.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), <e>, sink(<b>))
})
#let g = update-edge-data(g, <e>, (label: [$p$]))
#edges(g).first().data.labelParameters
graph_:
dictionary ,name:
label string ,update:
any function ) ->
dictionary graph_
dictionary
Graph object to update.
name
label or string
Edge name as a Typst label or its string form.
update
any or function
Replacement data or (data, edge) => new-data callback.
cut
Open paired edges along a weighted directed half-edge cut.
left and right are disjoint subgraphs containing opposite halves of each cut edge. Their names identify boundary sides, not underlying source/sink flow or superficial drawing orientation. A half-edge annotation (winding: n) requests n same-direction seam passages; absent winding defaults to one. If both sides specify a winding they must agree. Net counts of cancelling recrossings are not a supported description of an opening.
The input is unchanged. Every cut edge yields source and sink stubs plus n - 1 middle segments on auxiliary, unpainted boundary nodes. Fragment names are <original.0> through <original.n> in underlying source-to-sink order. Unnamed edges use __linnest_cut_edge_ID as the prefix; name collisions are errors. Cut geometry is reset for re-layout, while physical data and orientation are retained. Arbitrary Typst data, including content and callbacks, is remapped without serialization. Generated middle endpoint hedges inherit the data of the opposite original hedge, corresponding to the joined boundary side.
Output node/edge/half-edge records have origin relative to the input graph. Edge origins contain (edge, name, segment, winding); uncut segments are none. Generated nodes have no original node. Boundary nodes and dangling cut edges also have boundary: (edge, node, hedge, side, crossing, data,, where
cut-data, origin)data is the side’s hedge annotation and cut-data its subgraph-wide data. crossing counts from zero along underlying flow. Boundary hedge and origin describe the input to the cut that created that boundary, even after subsequent cuts; edge and node always locate its current anchor. This differs from each record’s immediate-input origin. The optional callback patches only endpoints created by this operation, exactly like map; earlier boundary placements and annotations are retained.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), <e>, sink(<b>), label: [$k$])
})
#let left = subgraph.select(g, sink: (<e>,))
#let left = subgraph.with-data(g, left, hedge: (winding: 2))
#let right = subgraph.select(g, source: (<e>,))
#let opened = cut(g, left: left, right: right)
#edges(opened).len()Parameters
graph_:
dictionary ,left:
dictionary ,right:
dictionary ,boundary:
none dictionary function ) ->
dictionary graph_
dictionary
Graph to open, without modifying it.
left
dictionary
Selected left half-edges, optionally annotated with winding.
Default: none
right
dictionary
Involution partners defining the right side.
Default: none
boundary
none or dictionary or function
Placement/data patch for each newly created boundary node or dangling edge.
Default: none
boundaries
Inspect all generated cut endpoints with their current solved positions.
Returns the boundary records described by cut plus pos. For a dangling endpoint, node is none and pos is its edge position; for a middle segment endpoint it is the auxiliary node position. This makes boundary annotations independent of their internal node/edge representation.
Parameters
dictionary ) -> array graph_
dictionary
Graph returned by cut, optionally styled, patched, or laid out.
join
Join two graphs by matching dangling half-edge metadata on key.
Supported key values are "statement", "compass", "port-label", and "id". The join is partial: unmatched dangling half-edges remain. Matching identities must be unique within each flow, and node/edge names must not collide across the inputs. Native Typst data is preserved; when both sides provide a dictionary, right-only fields are retained and left-side fields take precedence.
#let left = build({
node(<a>)
edge(sink(<a>, statement: "j"))
})
#let right = build({
node(<b>)
edge(source(<b>, statement: "j"))
})
#edges(join(left, right, key: "statement")).len()Parameters
left:
dictionary ,right:
dictionary ,key:
string ) ->
dictionary left
dictionary
Left graph object.
right
dictionary
Right graph object.
key
string
Dangling half-edge match key: "statement", "compass", "port-label", or "id".
Default: "statement"
join-edges
Join two graphs by matching dangling edge records on an edge statement key.
This is distinct from graph.join: it matches edge-level statements rather than half-edge metadata. The same partial-join, collision, and data-merge rules apply.
Parameters
left:
dictionary ,right:
dictionary ,key:
string ) ->
dictionary left
dictionary
Left graph object.
right
dictionary
Right graph object.
key
string
Nonempty edge statement key used for matching.
Default: "statement"
cycles
Return subgraph objects for the graph’s cycle basis.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), sink(<b>))
})
#cycles(g).len()Parameters
dictionary ) -> array graph
dictionary
Graph object to analyze.
forests
Return subgraph objects for the graph’s spanning forests.
#let g = build({
node(<a>)
node(<b>)
edge(source(<a>), sink(<b>))
})
#forests(g).len()Parameters
dictionary ) -> array graph
dictionary
Graph object to analyze.