Drawing functions
draw, edge-halves, and to-cetz-edge-halves are top-level functions exported by lib.typ. They turn an already laid-out graph into CeTZ content and expose the edge geometry used by custom renderers.
Concepts
Drawing
Draw styling is Typst-native. Pass dictionaries or callbacks to draw; edge callbacks receive the merged scope, edge statements, source/sink half-edge records, and the edge index:
#let edge-label(edge) = text(fill: rgb("#" + edge.color))[#edge.display-label]
#let source-style(edge) = (stroke: red + 0.5pt)
#let sink-style(edge) = (stroke: blue + 0.5pt)
#draw(layout(g), edge-label: edge-label, source-style: source-style, sink-style: sink-style)Edge labels are drawn with CeTZ content at the layout label position. Use edge-label-style: (anchor: "south"), or an edge-data callback returning a style dictionary, to choose which point of the label is anchored there.
Marks can follow graph orientation as a first-class draw option. Put the same mark layer on both halves and set mark-orientation: "edge"; default-oriented edges mark the source half, reversed edges mark the sink half with the marker flipped, and undirected edges suppress the mark.
#let oriented-arrow = (
stroke: black + 0.7pt,
mark: (end: (symbol: ">", fill: black, anchor: "center", shorten-to: auto), scale: 0.75),
mark-position: "center-if-dangling",
mark-orientation: "edge",
)
#draw(layout(g), source-style: oriented-arrow, sink-style: oriented-arrow)Reference
- edge-halves()
- to-cetz-edge-halves()
- draw()
edge-halves
Split a laid-out graph edge into source and sink half-edge paths.
The returned dictionary has source, sink, and curve. The split point is the edge layout point, so the two half-edges join smoothly there.
Parameters
edge
dictionary
Edge record returned by graph.edges(layout(g)).
nodes
array
Node records returned by graph.nodes(layout(g)).
omega
float
Hobby curl used for the source-to-edge and edge-to-sink curves.
Default: 1.0
source-outset
int or float
Arc-length trim applied at the source node side.
Default: 0
sink-outset
int or float
Arc-length trim applied at the sink node side.
Default: 0
accuracy
float
Arc-length accuracy used while trimming.
Default: 0.001
to-cetz-edge-halves
Draw the two halves of a laid-out graph edge through CeTZ.
source-style applies from the source node to the edge layout point, and sink-style applies from the edge layout point to the sink node.
Parameters
edge
dictionary
Edge record returned by graph.edges(layout(g)).
nodes
array
Node records returned by graph.nodes(layout(g)).
unit
int or float or length or ratio
Coordinate length for one graph-layout unit. Numbers are interpreted as em.
Default: 1
omega
float
Hobby curl used for the source-to-edge and edge-to-sink curves.
Default: 1.0
source-outset
int or float
Arc-length trim applied at the source node side.
Default: 0
sink-outset
int or float
Arc-length trim applied at the sink node side.
Default: 0
accuracy
float
Arc-length accuracy used while trimming.
Default: 0.001
source-style
dictionary
CeTZ style for the source half edge.
Default: (:)
sink-style
dictionary
CeTZ style for the sink half edge.
Default: (:)
draw
Draw a graph object with CeTZ.
The graph must already have positions, either from layout or from explicit pos values passed to graph node or edge items. Paired edges without an explicit edge position use the midpoint of their endpoint nodes. Node and edge Typst style dictionaries or callbacks are evaluated and forwarded to CeTZ.
#let positioned = graph.build({
graph.node(<left>, label: [left], pos: graph.pos(x: 0, y: 0))
graph.node(<right>, label: [right], pos: graph.pos(ref: <left>, dx: 2.5, dy: 0))
graph.edge(graph.source(<left>), graph.sink(<right>))
graph.edge(graph.source(<right>), <right-out>, pos: graph.pos(ref: <right>, dx: 0.9, dy: 0.7))
},
name: "positioned",
)
#draw(positioned, source-style: (stroke: black + 0.7pt), sink-style: (stroke: black + 0.7pt))
#let parallel-base-edge = 0
#let source-patterns = (
"coil",
"zigzag",
"coil",
"wave",
"zigzag",
"coil",
"wave",
"zigzag",
)
#let sink-patterns = (
"coil",
"zigzag",
"coil",
"zigzag",
"coil",
"wave",
"coil",
"wave",
)
#let parallel-layer(edge, mark: none) = if edge.eid == parallel-base-edge {
(
offset: -0.5,
length: 1.5,
ratio: 0.5,
resolve-length: "min",
stroke: (paint: rgb("#2f6f4e"), thickness: 1.1pt, cap: "round"),
mark: mark,
)
} else { none }
#let stack(base, layer) = if layer == none { base } else { (base, layer) }
#let source-stroke(edge) = if edge.eid == parallel-base-edge {
(paint: gray, thickness: 0.75pt, cap: "round")
} else {
(paint: red, thickness: 1.2pt, cap: "round")
}
#let sink-stroke(edge) = if edge.eid == parallel-base-edge {
(paint: gray, thickness: 0.75pt, cap: "round")
} else {
(paint: blue, thickness: 1.2pt, cap: "round", dash: "dotted")
}
#let source-style(edge) = {
let base = (
stroke: source-stroke(edge),
pattern: source-patterns.at(edge.eid),
pattern-amplitude: 0.18,
pattern-wavelength: 0.55,
pattern-coil-longitudinal-scale: 1.6,
)
stack(base, parallel-layer(edge))
}
#let sink-style(edge) = {
let base = (
stroke: sink-stroke(edge),
pattern: sink-patterns.at(edge.eid),
pattern-amplitude: 0.18,
pattern-wavelength: 0.55,
pattern-coil-longitudinal-scale: 1.6,
)
stack(base, parallel-layer(edge, mark: (end: (symbol: "straight"), scale: 0.75)))
}
#let g = graph.build({
graph.node(<a>, label: [a hi])
graph.node(<c>)
graph.node(<d>)
graph.node(<e>)
graph.edge(graph.source(<a>), <ac>, graph.sink(<c>), pos: graph.pos(x: 0, y: 0.75, mode: "pin"))
graph.edge(graph.source(<c>), <ca>, graph.sink(<a>, compass: "e"))
graph.edge(graph.source(<c>), <cd>, graph.sink(<d>, compass: "e"))
graph.edge(graph.source(<e>), <ed>, graph.sink(<d>, compass: "e"))
graph.edge(graph.source(<e>), <ea>, graph.sink(<a>, compass: "e"))
graph.edge(graph.source(<d>), <d-out>)
graph.edge(graph.source(<e>, compass: "e"), <e-out>)
graph.edge(graph.source(<a>), <a-out>)
},
name: "demo",
)
#let layed-out = layout(g)
#let east = subgraph.compass(layed-out, "e")
#draw(layed-out, subgraph: east, source-style: source-style, sink-style: sink-style)
#let p = graph.build({
graph.node(<pa>, label: [a], pos: graph.pos(y: 0, mode: "pin"))
graph.node(<pc>, label: [c], pos: graph.pos(y: 0, mode: "pin"))
graph.node(<pc1>, label: [c], pos: graph.pos(y: 0, mode: "pin"))
graph.node(<pc2>, label: [c], pos: graph.pos(y: 0, mode: "pin"))
graph.edge(graph.source(<pa>), <e0>, graph.sink(<pc>))
graph.edge(graph.source(<pc2>), <e1>, graph.sink(<pc1>))
},
name: "parallel demo",
)
#let parallel-edge-style(edge) = (
offset: -0.5,
length: 1.4,
ratio: 0.5,
resolve-length: "min",
stroke: (paint: rgb("#2f6f4e"), thickness: 1.1pt, cap: "round"),
)
#let focused-base-style(edge) = (
stroke: (paint: gray, thickness: 0.7pt, cap: "round"),
pattern: "coil",
pattern-amplitude: 0.14,
pattern-wavelength: 0.45,
pattern-coil-longitudinal-scale: 1.5,
)
#let focused-source-style(edge) = (focused-base-style(edge), parallel-edge-style(edge))
#let focused-sink-style(edge) = (focused-base-style(edge), parallel-edge-style(edge) + (
mark: (end: ">"),
))
#draw(layout(p, g-center: 0.004, label-steps: 0,), unit: 1.25, source-style: focused-source-style, sink-style: focused-sink-style)
#let g = graph.build({
graph.node(<a>, pos: graph.pos(x: 0, y: 0, mode: "pin"))
graph.node(<c>, pos: graph.pos(x: 3, y: 0, mode: "pin"))
graph.node(<d>, pos: graph.pos(x: 3, y: -1, mode: "pin"))
graph.edge(graph.source(<a>), <ac>, graph.sink(<c>), orientation: "default")
graph.edge(graph.source(<a>), <ad>, graph.sink(<d>), orientation: "reversed")
},
name: "oriented marks",
)
#let arrow = (
end: (symbol: ">", fill: black, anchor: "center", shorten-to: auto),
scale: 0.75,
)
#let oriented-arrow = (
stroke: black + 0.7pt,
mark: arrow,
mark-position: "center-if-dangling",
mark-orientation: "edge",
)
#draw(layout(g), unit: 1.4, source-style: oriented-arrow, sink-style: oriented-arrow)
Parameters
graph
bytes
Graph object with positions from layout or explicit graph API pos fields.
scope
dictionary
Additional values merged into node and edge callback dictionaries.
Default: (:)
unit
auto or int or float or length or ratio
Coordinate length for one graph-layout unit. auto uses any unit stored by graph.style, falling back to 1em. Numbers are interpreted as em.
Default: auto
title
none or auto or content or string
Optional title displayed above the diagram. Use auto for the graph name.
Default: none
subgraph
none or bytes or array
Optional subgraph or array of subgraphs whose half-edges are shaded. Array entries may be raw subgraphs or records like (subgraph: sg, edge-style: (stroke: red + 2pt)).
Default: none
debug
bool or int
Debug level. 1 enables CeTZ canvas debug; 2 also marks edge positions.
Default: false
node-radius
auto or int or float or array
Default CeTZ node radius. Use auto to fit the node label.
Default: auto
node-min-radius
int or float
Minimum radius used when node-radius is auto.
Default: 0.16
node-label-padding
int or float
Extra canvas-unit padding around labels when node-radius is auto.
Default: 0.08
node-fill
any
Default CeTZ node fill.
Default: white
node-stroke
any
Default CeTZ node stroke.
Default: black
node-outset
auto or int or float
Edge clearance from node centers. auto uses each node circle radius. Increase this when labels extend beyond the circle.
Default: auto
node-label-style
dictionary
Default node-label style forwarded to cetz.draw.content.
Default: (:)
node-style
dictionary or function or none
Node style dictionary or callback. A callback receives node data.
Default: (:)
node-label
auto or content or string or function or none
Node label content or callback. auto uses the node name.
Default: auto
draw-node
auto or function
CeTZ-compatible node drawing callback. auto draws the default circular node and label. A callback receives (node, box) and should return CeTZ draw elements; box contains name, center, width, height, unit, label, label-style, style, radius, and node.
Default: auto
edge-stroke
any
Default CeTZ edge stroke.
Default: 0.1em
edge-offset
int or float
Default normal offset for edge paths. Applied to the base edge geometry before patterns; node outsets then trim the shifted path.
Default: 0
edge-length
none or int or float
Maximum visible arc length for centered parallel edge paths. none keeps the full shifted path.
Default: none
edge-ratio
none or int or float
Maximum visible fraction of the base edge length for centered parallel edge paths. Combined with edge-length according to edge-resolve-length.
Default: none
edge-resolve-length
string or function
Resolve edge-length and edge-ratio. Accepted string values are "min"/"shorter", "max"/"longer", "length"/"fixed", "ratio"/"relative", or "none"/"full". A function receives (base-length, length, ratio).
Default: "min"
edge-accuracy
float
Arc-length accuracy for fitted parallel edge paths.
Default: 0.001
edge-optimize
bool
Let Kurbo optimize the fitted parallel path.
Default: true
source-style
dictionary or array or function or none
Source half-edge style dictionary, array of layer dictionaries, or callback. mark-position: "center-if-dangling" keeps an end marker at the paired-edge split point while centering it on dangling half edges. mark-orientation: "edge" makes a mark follow edge.orientation instead of raw path direction; reversed edges move the mark to the sink half and flip it, while undirected edges suppress it. source-anchor may be a CeTZ anchor name such as "north" or "south" to route this endpoint from a measured node-box anchor. By default, anchored paired edges use two smooth cubic halves through the edge layout point while preserving the endpoint anchor tangents. Set route: "hobby-through" to instead build a single Hobby spline through the source anchor, source guide, edge point, sink guide, and sink anchor, then split source/sink styling at the edge point. route: "direct" keeps the same anchored cubic routing but suppresses the default edge-position Hobby route. route-points: "through" also threads any layout-provided half-edge route points through that same Hobby path. route: "straight-through" draws the two straight force springs from source to edge position and from edge position to sink.
Default: (:)
sink-style
dictionary or array or function or none
Sink half-edge style dictionary, array of layer dictionaries, or callback. A callback receives edge data. mark-position: "center-if-dangling" has the same dangling-edge behavior as for source-style, and mark-orientation: "edge" participates in the same orientation-aware mark placement. sink-anchor may be a CeTZ anchor name such as "north" or "south" to route this endpoint into a measured node-box anchor. route: "direct" has the same meaning as in source-style.
Default: (:)
edge-label
content or string or function or none
Edge label content or callback. A callback receives edge data.
Default: none
edge-label-style
dictionary or function or none
Edge-label style forwarded to cetz.draw.content; use anchor to choose which point of the label is placed at the layout label position. A callback receives edge data.
Default: (:)
edge-omega
float
Hobby curl used at the endpoints of paired edge curves.
Default: 1.0
edge-trim-accuracy
float
Optional style key for anchored source/sink routes. Set anchor-control-distance in source-style or sink-style to override the automatic guide distance used by cubic anchored routes. -> auto | int | float Arc-length accuracy for trimming edge curves at node outsets.
Default: 0.001
padding
none or int or float or array or dictionary
CeTZ canvas padding.
Default: 0.4
debug-edge-radius
int or float
Radius for edge-position markers shown at debug >= 2.
Default: 0.08
debug-edge-fill
any
Fill for edge-position markers shown at debug >= 2.
Default: rgb("#ff9f1c")
debug-edge-stroke
any
Stroke for edge-position markers shown at debug >= 2.
Default: rgb("#d72638") + 0.35pt
debug-edge-label-fill
any
Label fill for edge-position markers shown at debug >= 2.
Default: rgb("#7a1020")
subgraph-edge-style
dictionary
Default CeTZ style used to shade half-edges included in subgraph. Individual subgraph records may override this with edge-style.
Default: (stroke: rgb("#ffd166") + 4.5pt)
subgraph-edge-underlay
bool
Draw subgraph shading below the normal half-edge style.
Default: true