On this page

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