On this page

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.label

The 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>).label

source(..) 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. Its eval-graph-fields, eval-node-fields, eval-edge-fields, eval-source-fields, and eval-sink-fields arguments are convenience arguments for graph.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. node and edge accept a callback or a dictionary keyed by current record names. Each dictionary value is a patch, a callback receiving the full record, or none. Edge patches may set spring-length directly; 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>) and graph.edge-data(graph, <name>) return one named node or edge data.
  • graph.update-node-data(graph, <name>, data) and graph.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(..) and sink(..) 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

from-spec(input) -> 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

build(
..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).name

Default: none

data

any

Native Typst graph data.

#let g = build(data: (a:(b:(1, ))), {
})
#info(g).data

Default: 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-statements

Default: (:)

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.data

Graph-level handling:

  • The DOT graph name becomes graph.info(g).name. A graph-level name attribute can name an anonymous graph; a named graph/digraph header takes precedence.
#let src = ```dot
digraph header_wins { graph [name="ignored"]; a }
```
#info(parse(src.text).first()).name
  • Top-level key=value statements and graph [key=value] attributes become graph.info(g).global-statements, except for the consumed name. They do not configure layout, even when a key looks like a layout option; pass layout options directly to layout.
#let src = ```dot
digraph { steps=1; graph [subtitle="nested"]; a }
```
#let statements = info(parse(src.text).first()).global-statements
#statements.steps
#statements.subtitle
  • node [key=value] and edge [key=value] become default-node-statements and default-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.particle

Node handling:

  • The DOT node id becomes the node name returned by nodes. If the DOT node id is numeric, it is consumed as the node index instead and the public name is none.
#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=invis marks the DOT node as a dangling external endpoint. It is not returned by nodes. Its remaining attributes are copied onto the external edge that touches it, except shape and label; style and id are 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 style is consumed for every node; only style=invis has 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().statements
  • pos is parsed as node placement. Simple numeric values are exposed as node.pos; extended placement values are used by layout. pin is an explicit layout constraint and is not exposed as a public statement. The pos-x-set and pos-y-set record fields say which axes were explicitly supplied, including for partial placements whose resolved pos contains both coordinates. dot preserves 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))
  • shift is parsed as a drawing shift and also remains available as a statement. eval is 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.statements

Edge handling:

  • id=<n> is consumed as the edge index. It is not preserved as a statement; drawing callbacks expose the resulting stable edge index as eid. 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, and dir=none become 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="..." and sink="..." are consumed as endpoint statement values and removed from edge statements. On an external edge, use the attribute matching the real endpoint side in the DOT edge: ext -> a uses sink=..., while a -> ext uses source=....
#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))
  • pos is parsed as the edge control-point placement. pin is an explicit edge layout constraint. shift, label-pos, label-angle, and bend are parsed into edge geometry fields; except for pin, they also remain available as statements. The pos-x-set and pos-y-set record fields identify explicitly supplied axes, including partial placements; dot preserves 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 shape or label.
#let src = ```dot
digraph { ext [style=invis, column=left, shape=none, label=skip]; ext -> a [id=0]; }
```
#edges(parse(src.text).first()).first().statements

Port and half-edge handling:

  • DOT ports of the form node:port and node:port:compass are preserved on source/sink endpoint records. Numeric ports also assign explicit half-edge ids. Non-numeric ports are exposed only as port-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 include n, 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 join with key: "id"; query and eval records expose the resulting half-edge index as hedge.
#let src = ```dot
digraph { a:1 -> b:0 [id=0]; }
```
#let e = edges(parse(src.text).first()).first()
#e.source.hedge
#e.sink.hedge

Placement 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!" and pos="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> and y:<coord> entries. ! applies to the individual axis, for example pos="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 @-name in axis form and must be pinned with !, for example pos="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().pos
  • pin accepts numeric point constraints, x:<coord>, y:<coord>, and grouped constraints such as x:@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

parse(
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.label

Default: ()

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.label

Default: ()

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.statement

Default: ()

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.statement

Default: ()

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.title

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: (:)

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

node(
..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

source(
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

sink(
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

edge(
..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

group(
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

pin(value: 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

start(value: 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

pos(
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>).weight

Parameters

map(
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

style(
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

eval-fields(
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).name

Parameters

info(graph: 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

dot(graph: 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

nodes(
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

edges(
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>).label

Parameters

node-data(
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>).label

Parameters

edge-data(
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.label

Parameters

update-node-data(
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.label

Parameters

update-edge-data(
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,
cut-data, origin)
, where 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

cut(
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

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

boundaries(graph_: 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

join(
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

join-edges(
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

cycles(graph: 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

forests(graph: dictionary ) -> array
graph

dictionary

Graph object to analyze.