subgraph module
Use subgraph to construct and inspect opaque zero-copy selections of half-edges. Pass those selections to graph queries, drawing, and layout functions that accept a subgraph argument.
Concepts
Subgraphs
A subgraph is a dictionary containing half-edge membership, a topology signature, subgraph-wide data, and per-half-edge hedge-data. Annotations remain Typst values: content and callbacks are not serialized through Wasm. Independent selections can annotate the same hedge without changing its graph data or each other.
subgraph.select(g, nodes: (), edges: (), source: (), sink: (), hedges: ())selects the union of the supplied references. Node and edge references are Typst labels or numeric IDs, supplied in arrays.nodesselects incident hedges,edgesselects both available halves, andsource/sinkselects the named edges’ structural halves, independently of drawing orientation.hedgesselects exact numeric hedge IDs. Unknown references and missing requested halves are errors; an isolated node contributes no hedges.subgraph.label(g, label)constructs a subgraph from a base62 label.subgraph.bits(g, bits)constructs a subgraph from a boolean hedge array.subgraph.compass(g, compass)selects half edges with a DOT compass point.subgraph.hedges(sg),subgraph.contains(sg, hedge), andsubgraph.contains-edge(sg, edge)inspect membership. The last checks whether either half of an edge record is selected.subgraph.complement(g, sg)selects all other hedges. It preserves subgraph-wide data, but the newly selected hedges have no annotations.
subgraph.with-data(g, sg, data: value, hedge: updater) returns an annotated selection. data: auto preserves subgraph-wide data and hedge: none preserves hedge annotations. A non-function hedge value replaces every selected hedge’s annotation. A callback receives its original endpoint record plus edge, edge-name, and flow; data holds the existing selection annotation and graph-data holds the graph’s half-edge data. Its return value replaces the annotation, even when it is none. To store a callback as an annotation, return it from the updater rather than passing it as the updater itself. subgraph.hedge-data(sg, h) reads one selected hedge’s annotation.
Graph-aware consumers check a signature of IDs, names, and source/sink incidence, not graph identity or placement. Selections survive layout and drawing/data changes, but incompatible topology or names are rejected. Re-select on a cut view rather than reusing its master’s selections.
Breaking change: raw subgraph bytes are no longer accepted by public APIs. Use the constructors above, or the wrapped results of graph.cycles and graph.forests, and pass the whole object, not its internal bytes field. subgraph.to-label(sg) exports membership only: rebuilding with subgraph.label(g, label) binds it to that graph but does not restore annotations or the old topology signature. Keep the object to preserve Typst sidecar data; a base62 label is not a complete serialization of an annotated selection.
Passing subgraph: sg to draw highlights half-edges, while layout restricts placement or optimization. Neither opens edges; use graph.cut for that.
Directed Cut Views
graph.cut(g, left: left, right: right, boundary: patch) opens one weighted directed cut and leaves g unchanged. The two selections are its drawing sides, not separate cuts or a partition of the vertices. They must be disjoint and contain exactly opposite halves of every selected paired edge in g. Already dangling edges cannot be selected for opening.
Think of the drawing boundaries as L and R, with a positive seam passage from R to L. Selecting an edge’s source on R and sink on L follows that positive direction; swapping them reverses it. This is independent of superficial drawing orientation, including reversed fermion arrows. Side names do not place endpoints automatically: boundary supplies the view’s geometry.
An annotation (winding: n) on either selected half requests a positive integer number of same-direction passages; it defaults to one. If both halves specify winding, they must agree. Each selected edge becomes a source stub, a sink stub, and n - 1 middle segments. Thus winding two produces three segments without separate named seams or a caller-supplied ordering of cut objects. Winding is not a net count of cancelling back-and-forth crossings.
This small example uses the manual’s existing imports. It stores content on the original edge and endpoints, annotates the cut sides, and opens one edge with winding two. The boundary callback places both ordinary free endpoints and the generated middle endpoints using the same record interface:
#let master = graph.build({
node(<a>, pos: graph.pos(x: 0, y: 0))
node(<b>, pos: graph.pos(x: 0, y: -2))
edge(
<link>,
source(<a>, caption: [output]),
sink(<b>, caption: [input]),
label: [connection],
)
})
#let left = subgraph.with-data(
master, subgraph.select(master, sink: (<link>,)),
data: [left boundary],
hedge: h => (winding: 2, caption: h.graph-data.caption),
)
#let right = subgraph.with-data(
master, subgraph.select(master, source: (<link>,)),
data: [right boundary],
hedge: h => (caption: h.graph-data.caption),
)
#let view = graph.cut(master, left: left, right: right, boundary: record => {
let b = record.boundary
(pos: graph.pos(
x: if b.side == "left" { -3 } else { 3 },
y: -2 * b.crossing,
))
})
#let view = layout(view, layout-algo: "tree")
#assert(graph.edges(master).len() == 1)
#assert(graph.edges(view).len() == 3)
#assert(graph.edge-data(view, <link.1>).label == [connection])
#assert(graph.boundaries(view).len() == 4)
#draw(view, edge-label: edge => edge.label)Fragments are named <link.0> through <link.n> in underlying source-to-sink order, irrespective of L/R or drawing arrows. Unnamed edges use the native prefix __linnest_cut_edge_ID; generated name collisions are errors. Prefer explicit edge names when patching a view. Node, edge, and hedge origin refer to the immediate input graph, including on repeated cuts. Edge origins contain the input edge ID and name, plus segment and winding; uncut edges have segment: none and winding: 0. Node and hedge origins are input IDs; newly generated nodes have origin: none.
Physical graph, node, edge, and half-edge Typst data are remapped, including content and callbacks. Middle source hedges inherit the original sink hedge’s data, and middle sink hedges inherit the original source’s data, following the boundary side they continue. The cut resets fragment geometry for re-layout, rather than copying an old midpoint pin to new endpoints. Apply view-specific bends, placement, labels, and crossing-under targets with graph.map; a reference to a split edge needs the appropriate fragment name.
The boundary callback runs only on newly created endpoints, receiving a node or dangling-edge record and returning ordinary graph.map-style placement/data patches. Its record.boundary has output edge and optional node IDs, the original side’s hedge, side ("left" or "right"), zero-based crossing along underlying flow, data from that hedge’s annotation, cut-data from that side’s subgraph-wide data, and the fragment’s origin at creation. For example, the code above preserves each endpoint caption in boundary.data.caption.
On subsequent cuts, boundary.origin and boundary.hedge still refer to the input to the cut that created the endpoint; side, crossing, data, and cut-data are retained. Only boundary.edge and boundary.node remap to current anchors. Inherited boundaries retain their placements and annotations without invoking the callback again.
graph.boundaries(view) returns these descriptors plus current pos, not coordinates cached at cut time. A dangling endpoint uses its edge position and has node: none; a middle endpoint uses its auxiliary node position. Query after layout, or inside draw-after, to group solved endpoints into background boxes with existing CeTZ primitives. Auxiliary boundary nodes are automatically zero-sized and unpainted, including their labels and custom node-drawing callbacks; they remain addressable anchors for incident edges.
subgraph
- label()
- bits()
- compass()
- to-label()
- hedges()
- contains()
- select()
- with-data()
- hedge-data()
- complement()
- contains-edge()
- node-depths()
label
Construct a subgraph object from a base62 label.
#let g = graph.build({
graph.node(<a>)
graph.node(<b>)
graph.edge(graph.source(<a>, compass: "e"), graph.sink(<b>))
})
#let east = subgraph.compass(g, "e")
#let same = subgraph.label(g, subgraph.to-label(east))
#subgraph.hedges(same).len()Parameters
graph:
dictionary ,label:
string ) ->
dictionary graph
dictionary
Graph object whose half-edge set the label refers to.
label
string
Base62 subgraph label returned by to-label or produced by Linnest.
bits
Construct a subgraph object from a boolean hedge array.
#let g = graph.build({
graph.node(<a>)
graph.node(<b>)
graph.edge(graph.source(<a>), graph.sink(<b>))
})
#let first = subgraph.bits(g, (true, false))
#subgraph.contains(first, 0)Parameters
graph:
dictionary ,bits:
array ) ->
dictionary graph
dictionary
Graph object whose half-edge order defines the bit array.
bits
array
Boolean array selecting half edges by graph half-edge index.
compass
Construct a subgraph object from a DOT compass point such as "n" or "s".
#let g = graph.build({
graph.node(<a>)
graph.node(<b>)
graph.edge(graph.source(<a>, compass: "e"), graph.sink(<b>))
})
#subgraph.hedges(subgraph.compass(g, "e")).len()Parameters
graph:
dictionary ,compass:
string ) ->
dictionary graph
dictionary
Graph object to filter by half-edge compass statement.
compass
string
DOT compass point such as "n", "s", "e", or "w".
to-label
Convert a subgraph object to its base62 label.
The label encodes only membership, not topology or annotations.
#let g = graph.build({
graph.node(<a>)
graph.node(<b>)
graph.edge(graph.source(<a>), graph.sink(<b>))
})
#subgraph.to-label(subgraph.bits(g, (true, false)))Parameters
dictionary ) -> string subgraph
dictionary
Subgraph object returned by this module or by graph.cycles/graph.forests.
hedges
Return the hedge indices included in a subgraph object.
#let g = graph.build({
graph.node(<a>)
graph.node(<b>)
graph.edge(graph.source(<a>), graph.sink(<b>))
})
#subgraph.hedges(subgraph.bits(g, (true, false)))Parameters
dictionary ) -> array subgraph
dictionary
Subgraph object to inspect.
contains
Test whether a subgraph object includes a hedge.
#let g = graph.build({
graph.node(<a>)
graph.node(<b>)
graph.edge(graph.source(<a>), graph.sink(<b>))
})
#subgraph.contains(subgraph.bits(g, (true, false)), 0)Parameters
subgraph:
dictionary ,hedge:
int ) ->
bool subgraph
dictionary
Subgraph object to inspect.
hedge
int
Half-edge index to test.
select
Select the union of node incidences, whole edges, edge halves, and exact hedges.
Node and edge references are Typst label names or numeric IDs. source and sink refer to the structural halves, independently of drawing orientation. Missing references or absent requested halves are errors. An isolated node contributes no hedges: subgraphs represent half-edge sets, not node sets.
#let g = graph.build({
graph.node(<a>)
graph.edge(<D1>, graph.source(<a>))
graph.edge(<D6>, graph.sink(<a>))
})
#let left = subgraph.select(g, source: (<D1>,), sink: (<D6>,))
#subgraph.hedges(left)Parameters
g:
dictionary ,nodes:
array ,edges:
array ,source:
array ,sink:
array ,hedges:
array ) ->
dictionary g
dictionary
Graph whose records define names and IDs.
nodes
array
Select all half edges incident to these nodes.
Default: ()
edges
array
Select both available halves of these edges.
Default: ()
source
array
Select only the source half of these edges.
Default: ()
sink
array
Select only the sink half of these edges.
Default: ()
hedges
array
Select these exact numeric half-edge IDs.
Default: ()
with-data
Attach arbitrary Typst data without serializing it through Wasm.
data: auto preserves subgraph-wide data; hedge: none preserves annotations. A hedge callback receives the original endpoint record plus numeric edge, label-valued edge-name (or none), and flow ("source" or "sink"). Its data is the existing subgraph annotation; graph-data holds the original half-edge data. The return value replaces the annotation, including none. Other non-function hedge values replace every selected hedge’s annotation. Only selected hedges retain annotations; callback values are never evaluated unless supplied as the hedge updater itself.
Parameters
g:
dictionary ,s:
dictionary ,data:
any ,hedge:
any ) ->
dictionary g
dictionary
Compatible graph supplying endpoint records and graph data.
s
dictionary
Subgraph to annotate.
data
any
Replacement subgraph-wide data, or auto to preserve it.
Default: auto
hedge
any
Half-edge updater, constant annotation, or none to preserve annotations.
Default: none
hedge-data
Return a selected hedge’s annotation, or none when it has no annotation. Unselected and out-of-range half-edge IDs are errors.
Parameters
s:
dictionary ,h:
int ) ->
any s
dictionary
Subgraph whose annotation is read.
h
int
Selected numeric half-edge ID.
complement
Return the complement of a subgraph’s selected half edges.
Subgraph-wide data is preserved; newly selected hedges have no annotations.
Parameters
graph:
dictionary ,selected:
dictionary ) ->
dictionary graph
dictionary
Graph object whose half-edge order defines the result.
selected
dictionary
Subgraph object to invert.
contains-edge
Test whether either half-edge of an edge record is selected.
Parameters
subgraph:
dictionary ,edge:
dictionary ) ->
bool subgraph
dictionary
Subgraph object to inspect.
edge
dictionary
Edge record from graph.edges(...) or draw callback data.
node-depths
Compute breadth-first node depths inside a selected edge set.
Parameters
graph:
dictionary ,selected:
dictionary ,root:
int ) ->
dictionary graph
dictionary
Graph object to inspect.
selected
dictionary
Subgraph whose selected half edges define the traversal edges.
root
int
Root node index.
Default: 0