fletcher
(noun) a maker of arrows
A Typst package for diagrams with lots of arrows, built on top of CeTZ.
Commutative diagrams, flow charts, state machines, block diagrams…
github.com/Jollywatt/typst-fletcher
Version 0.5.1
Guide
Usage examples ........................................................ 3
Diagrams ................................................................... 4
Elastic coordinates .............................................. 4
Absolute coordinates ......................................... 4
All sorts of coordinates ..................................... 4
Nodes .......................................................................... 5
Node shapes ......................................................... 5
Node groups ......................................................... 6
Edges .......................................................................... 6
Specifying edge vertices .................................... 6
Connecting to adjacent nodes ..................... 6
Relative coordinate shorthands .................. 7
Named or labelled coordinates .................... 7
Edge types ............................................................ 7
Tweaking where edges connect ...................... 7
Marks and arrows .................................................... 8
Custom marks ...................................................... 9
Mark objects .................................................... 9
Special mark properties .............................. 10
Detailed example .......................................... 11
Custom mark shorthands ............................... 11
CeTZ integration ................................................... 12
Bézier edges ....................................................... 12
Touying integration .............................................. 13
Reference
Main functions ....................................................... 14
diagram() ........................................................... 14
node() ................................................................. 18
edge() ................................................................. 22
Behind the scenes .................................................. 29
marks.typ ........................................................... 29
shapes.typ ......................................................... 33
coords.typ ......................................................... 39
diagram.typ ...................................................... 41
node.typ ............................................................. 43
edge.typ ............................................................. 44
draw.typ ............................................................. 45
utils.typ ........................................................... 49
1
Assoc
Assoc
Assoc
Id
Id Id
Id
Div
DivDiv
Inv
Inv AssocAssoc
magma
semigroup unital magma quasigroup
monoid inverse semigroup loop
group
 

󰍖
󰍖
󰍏
󰍖
󰍖

󰧢
󰧢
󰧢
󰍭
󰍓
󰍭
󰍓

󰍭

󰍭
󰘢
󰍭
󰘣
󰍓
󰬷

󰬷
󰬷

󰬷
󰬷
󰬷
󰬷
󰬷
󰬷
󰍓
󰬷
input
memory unit (MU)
arithmetic & logic
unit (ALU)
control unit (CU)
output
2
Usage examples
Avoid importing everything with * as many internal functions are also exported.
#import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge
// You can specify nodes in math-mode, separated by `&`:
#diagram($
G edge(f, ->) edge("d", pi, ->>) & im(f) \
G slash ker(f) edge("ur", tilde(f), "hook-->")
$)


// Or you can use code-mode, with variables, loops, etc:
#diagram(spacing: 2cm, {
let (A, B) = ((0,0), (1,0))
node(A, $cal(A)$)
node(B, $cal(B)$)
edge(A, B, $F$, "->", bend: +35deg)
edge(A, B, $G$, "->", bend: -35deg)
let h = 0.2
edge((.5,-h), (.5,+h), $alpha$, "=>")
})
#diagram(
spacing: (10mm, 5mm), // wide columns, narrow rows
node-stroke: 1pt, // outline node shapes
edge-stroke: 1pt, // make lines thicker
mark-scale: 60%, // make arrowheads smaller
edge((-2,0), "r,u,r", "-|>", $f$, label-side: left),
edge((-2,0), "r,d,r", "..|>", $g$),
node((0,-1), $F(s)$),
node((0,+1), $G(s)$),
node(enclose: ((0,-1), (0,+1)), stroke: teal, inset: 10pt,
snap: false), // prevent edges snapping to this node
edge((0,+1), (1,0), "..|>", corner: left),
edge((0,-1), (1,0), "-|>", corner: right),
node((1,0), text(white, $ plus.circle $), inset: 2pt, fill: black),
edge("-|>"),
)


An equation $f: A -> B$ and \
an inline diagram #diagram($A edge(->, text(#0.8em, f)) & B$).
An equation and
an inline diagram
.
#import fletcher.shapes: diamond
#diagram(
node-stroke: black + 0.5pt,
node-fill: gradient.radial(white, blue, center: (40%, 20%),
radius: 150%),
spacing: (10mm, 5mm),
node((0,0), [1], name: <1>, extrude: (0, -4)), // double stroke
node((1,0), [2], name: <2>, shape: diamond),
node((2,-1), [3a], name: <3a>),
node((2,+1), [3b], name: <3b>),
edge(<1>, <2>, [go], "->"),
edge(<2>, <3a>, "->", bend: -15deg),
edge(<2>, <3b>, "->", bend: +15deg),
edge(<3b>, <3b>, "->", bend: -130deg, label: [loop!]),
)
go
loop!
1 2
3a
3b
3
Diagrams
Diagrams created with diagram() are a collection of nodes and edges rendered on a CeTZ canvas.
Elastic coordinates
Diagrams are laid out on a flexible coordinate grid, visible when the debug option of diagram() is on.
When a node is placed, the rows and columns grow to accommodate the node’s size, like a table.
By default, coordinates  have going and going . is can be changed with the axes option
of diagram(). e cell-size option is the minimum row and column width, and spacing is the guer
between rows and columns.
#let c = (orange, red, green, blue).map(x => x.lighten(50%))
#diagram(
debug: 1,
spacing: 10pt,
node-corner-radius: 3pt,
node((0,0), [a], fill: c.at(0), width: 10mm, height: 10mm),
node((1,0), [b], fill: c.at(1), width: 5mm, height: 5mm),
node((1,1), [c], fill: c.at(2), width: 20mm, height: 5mm),
node((0,2), [d], fill: c.at(3), width: 5mm, height: 10mm),
)
a b
c
d
0 1
2
1
0
So far, this is just like a table — however, elastic coordinates can be fractional. Notice how the column
sizes change as the green node is gradually moved between columns:
a b

d
0 1
2
1
0
a b

d
0 1
2
1
0
a b

d
0 1
2
1
0
a b

d
0 1
2
1
0
a b

d
0 1
2
1
0
Absolute coordinates
As well as elastic or  coordinates, which are row/column numbers, you can also use absolute or 
coordinates, which are physical lengths. is lets you break away from flexible grid layouts.
Elastic coordinates, e.g., (2, 1) Physical coordinates, e.g., (10mm, 5mm)
Dimensionless, dependent on row/column sizes. Lengths, independent of row/column sizes.
Placed objects can influence diagram layout. Placed objects never affect diagram layout.
Absolute coordinates aren’t very useful on their own, but they may be used in combination with elastic
coordinates, particularly with (rel: (x, y), to: (u, v)).
#diagram(
node((0, 0), name: <origin>),
for θ in range(16).map(i => i/16*360deg) {
node((rel: (θ, 10mm), to: <origin>), $ * $, inset: 1pt)
edge(<origin>, "-")
}
)
All sorts of coordinates
You can also use any CeTZ-style coordinate expression, mixing and matching elastic and physical
coordinates, e.g., relative (rel: (1, 2)), polar (45deg, 1cm), interpolating (<P>, 80%, <Q>), per-
pendicular (<X>, "|-", <Y>), and so on. However, support for CeTZ-style anchors is incomplete.
4
Nodes
node(coord, label, ..options)
Nodes are content centered at a particular coordinate. ey can be circular, rectangular, or any custom
shape. Nodes automatically fit to the size of their label (with an inset), but can also be given an exact
width, height, or radius, as well as a stroke and fill. For example:
#diagram(
debug: true, // show a coordinate grid
spacing: (5pt, 4em), // small column gaps, large row spacing
node((0,0), $f$),
node((1,0), $f$, stroke: 1pt),
node((2,0), $f$, stroke: blue, shape: rect),
node((3,0), $f$, stroke: 1pt, radius: 6mm, extrude: (0, 3)),
{
let b = blue.lighten(70%)
node((0,1), `xyz`, fill: b, )
let dash = (paint: blue, dash: "dashed")
node((1,1), `xyz`, stroke: dash, inset: 1em)
node((2,1), `xyz`, fill: b, stroke: blue, extrude: (0, -2))
node((3,1), `xyz`, fill: b, height: 5em, corner-radius: 5pt)
}
)
xyz xyz xyz xyz
0 1 2 3
1
0
Node shapes
By default, nodes are circular or rectangular depending on the aspect ratio of their label. e shape op-
tion accepts rect, circle, various shapes provided in the fletcher.shapes submodule, or a function.
#import fletcher.shapes: pill, parallelogram, diamond, hexagon
#diagram(
node-fill: gradient.radial(white, blue, radius: 200%),
node-stroke: blue,
(
node((0,0), [Blue Pill], shape: pill),
node((1,0), [_Slant_], shape: parallelogram.with(angle: 20deg)),
node((0,1), [Choice], shape: diamond),
node((1,1), [Stop], shape: hexagon, extrude: (-3, 0), inset: 10pt),
).intersperse(edge("o--|>")).join()
)
Blue Pill
Slant
Choice Stop
Custom node shapes may be implemented with CeTZ via the shape option of node(), but it is up to
the user to support outline extrusion for custom shapes. e predefined shapes are:
rect circle ellipse pill
parallelogram trapezium diamond
triangle
house
chevron hexagon octagon
Shapes respect the stroke, fill, width, height, and extrude options of edge().
5
Node groups
Nodes are usually centered at a particular coordinate, but they can also enclose multiple centers.
When the enclose option of node() is given, the node automatically resizes.
#diagram(
node-stroke: 0.6pt,
node($Sigma$, enclose: ((1,1), (1,2)), // a node spanning multiple centers
inset: 10pt, stroke: teal, fill: teal.lighten(90%), name: <bar>),
node((2,1), [X]),
node((2,2), [Y]),
edge((1,1), "r", "->", snap-to: (<bar>, auto)),
edge((1,2), "r", "->", snap-to: (<bar>, auto)),
)
X
Y
You can also enclose other nodes by coordinate or name to create node groups:
#diagram(
node-stroke: 0.6pt,
node-fill: white,
node((0,1), [X]),
edge("->-", bend: 40deg),
node((1,0), [Y], name: <y>),
node($Sigma$, enclose: ((0,1), <y>),
stroke: teal, fill: teal.lighten(90%),
snap: -1, // prioritise other nodes when auto-snapping
name: <group>),
edge(<group>, <z>, "->"),
node((2.5,0.5), [Z], name: <z>),
)
X
Y
Z
Edges
edge(..vertices, marks, label, ..options)
An edge connects two coordinates. If there is a node at the endpoint, the edge snaps to the nodes’
bounding shape (aer applying the node’s outset). An edge can have a label, can bend into an arc,
and can have various arrow marks.
#diagram(spacing: (12mm, 6mm), {
let (a, b, c, abc) = ((-1,0), (0,1), (1,0), (0,-1))
node(abc, $A times B times C$)
node(a, $A$)
node(b, $B$)
node(c, $C$)
edge(a, b, bend: -18deg, "dashed")
edge(c, b, bend: +18deg, "<-<<")
edge(a, abc, $a$)
edge(b, abc, "<=>")
edge(c, abc, $c$)
node((.6,3), [_just a thought..._])
edge(b, "..|>", corner: right)
})
just a thought…
Specifying edge vertices
e first few arguments given to edge() specify its vertices, of which there can be two or more.
Connecting to adjacent nodes
If an edge’s first or last vertex is auto, the coordinate of the previous or next node is used, respectively,
according to the order that nodes and edges are passed to diagram(). A single vertex, as in edge(to),
is interpreted as edge(auto, to). Given no vertices, an edge connects the nearest nodes on either side.
6
#diagram(
node((0,0), [London]),
edge("..|>", bend: 20deg),
edge("<|--", bend: -20deg),
node((1,1), [Paris]),
)
London
Paris
Implicit coordinates can be handy for diagrams in math-mode:
#diagram($ L edge("->", bend: #30deg) & P $)
Relative coordinate shorthands
Like node positions, edge vertices may be specified by CeTZ-style coordinate expressions, combining
elastic and physical coordinates.
Additionally, you may specify relative shorthand strings such as "u" for up or "sw" for south west. Any
combination of top/up/north, boomp/down/south, le/west, and right/east are allowed. Together
with implicit coordinates, this allows you to do things like:
#diagram($ A edge("rr", ->, #[jump!], bend: #30deg) & B & C $)

Named or labelled coordinates
Another way coordinates can be expressed is through node names. Nodes can be given a name, which
is a label (not a string) identifying that node. A label as an edge vertex is interpreted as the position of
the node with that label.
#diagram(
node((0,0), $frak(A)$, name: <A>),
node((1,0.5), $frak(B)$, name: <B>),
edge(<A>, <B>, "-->")
)
Node names are labels (instead of strings like in CeTZ) to disambiguate them from other positional
string arguments. Since they are not inserted into the document, they do not interfere with other labels.
Edge types
ere are three types of edges: "line", "arc", and "poly". All edges have at least two vertices, but
"poly" edges can have more. If unspecified, kind is chosen based on bend and the number of vertices.
#diagram(
edge((0,0), (1,1), "->", `line`),
edge((2,0), (3,1), "->", bend: -30deg, `arc`),
edge((4,0), (4,1), (5,1), (6,0), "->", `poly`),
)
line
arc
poly
All vertices except the first can be relative coordinate shorthands (see above), so that in the example
above, the "poly" edge could also be wrien in these equivalent ways:
edge((4,0), (rel: (0,1)), (rel: (1,0)), (rel: (1,-1)), "->", `poly`)
edge((4,0), "d", "r", "ur", "->", `poly`) // using relative coordinate names
edge((4,0), "d,r,ur", "->", `poly`) // shorthand
Only the first and last vertices of an edge automatically snap to nodes.
Tweaking where edges connect
A node’s outset controls how close edges connect to the node’s boundary. To adjust where along the
boundary the edge connects, you can adjust the edge’s end coordinates by a fractional amount.
7
#diagram(
node-stroke: (thickness: .5pt, dash: "dashed"),
node((0,0), [no outset], outset: 0pt),
node((0,1), [big outset], outset: 10pt),
edge((0,0), (0,1)),
edge((-0.1,0), (-0.4,1), "-o", "wave"), // shifted with fractional coordinates
edge((0,0), (0,1), "=>", shift: 15pt), // shifted by a length
)
no outset
big outset
Alternatively, the shift option of edge() lets you shi edges sideways by an absolute length:
#diagram($A edge(->, shift: #3pt) edge(<-, shift: #(-3pt)) & B$)
By default, edges which are incident at an angle are automatically adjusted slightly, especially if the
node is wide or tall. Aesthetically, things can look more comfortable if edges don’t all connect to the
node’s exact center, but instead spread out a bit. Notice the (subtle) difference the figures below.
Figure1: With focus (default)
Figure2: Without defocus
e strength of this adjustment is controlled by the defocus option of node() or the node-defocus
option of diagram().
Marks and arrows
Arrow marks can be specified like edge(a, b, "-->") or with the marks option of edge(). Some
mathematical arrow heads are supported, which match , , , , , and in the default font.
"->" "=>" "==>" "|->" "->>" "hook->"
A few other marks are provided, and all marks can be placed anywhere along the edge.
">>-->" "||-/-|>" "o..O" "hook'-x-}>" "-*-harpoon"
All the built-in marks (see Table 1) are defined in the state variable fletcher.MARKS, which you
may access with context fletcher.MARKS.get(). You add or tweak mark styles by modifying
fletcher.MARKS, as described in Mark objects.
8
head doublehead triplehead harpoon straight solid
stealth latex cone circle square diamond
bar cross hook hooks > <
>> << >>> <<< |> <|
}> <{ | || ||| /
\ x X o O *
@ [] <> crowfoot n n!
n? 1 1! 1?
Table1: Default marks by name. Properties such to size, angle, spacing, or fill can be adjusted.
Custom marks
While shorthands like "|=>" exist for specifying marks and stroke styles, finer control is possible.
Marks can be specified by passing an array of mark objects to the marks option of edge(). For example:
#diagram(
edge-stroke: 1.5pt,
spacing: 25mm,
edge((0,1), (-0.1,0), bend: -8deg, marks: (
(inherit: ">>", size: 6, delta: 70deg, sharpness: 65deg),
(inherit: "head", rev: true, pos: 0.8, sharpness: 0deg, size: 17),
(inherit: "bar", size: 1, pos: 0.3),
(inherit: "solid", size: 12, rev: true, stealth: 0.1, fill: red.mix(purple)),
), stroke: green.darken(50%)),
)
In fact, shorthands like "|=>" are expanded with interpret-marks-arg() into a form more like the
example above. More precisely, edge(from, to, "|=>") is equivalent to:
context edge(from, to, ..fletcher.interpret-marks-arg("|=>"))
If you want to explore the internals of mark objects, you might find it handy to inspect the output of
context fletcher.interpret-marks-arg(..) with various mark shorthands as input.
Mark objects
A mark object is a dictionary with, at the very least, a draw entry containing the CeTZ objects to be
drawn. ese CeTZ objects are translated and scaled to fit the edge; the mark’s center should be at the
origin, and the stroke’s thickness is defined as the unit length. For example, here is a basic circle mark:
9
#import cetz.draw
#let my-mark = (
draw: draw.circle((0,0), radius: 2, fill: none)
)
#diagram(
edge((0,0), (1,0), stroke: 1pt, marks: (my-mark, my-mark), bend: 30deg),
edge((0,1), (1,1), stroke: 3pt + orange, marks: (none, my-mark)),
)
A mark object can contain arbitrary parameters, which may depend on parameters defined earlier by
being wrien as a function of the mark object. For example, the mark above could also be wrien as:
#let my-mark = (
size: 2,
draw: mark => draw.circle((0,0), radius: mark.size, fill: none)
)
is form makes it easier to change the size without modifying the draw function, for example:
#diagram(edge(stroke: 3pt, marks: (my-mark + (size: 4), my-mark)))
Lastly, mark objects may inherit properties from other marks in fletcher.MARKS by containing an
inherit entry, for example:
#let my-mark = (
inherit: "stealth", // base mark on `fletcher.MARKS.stealth`
fill: red,
stroke: none,
extrude: (0, -3),
)
#diagram(edge("rr", stroke: 2pt, marks: (my-mark, my-mark + (fill: blue))))
Internally, marks are passed to resolve-mark(), which ensures all entries are evaluated to final values.
Special mark properties
A mark object may contain any properties, but some have special functions.
Name Description Default
inherit
e name of a mark in fletcher.MARKS to inherit properties from. is
can be used to make mark aliases, for instance, "<" is defined as
(inherit: "head", rev: true).
draw
As described above, this contains the final CeTZ objects to be drawn.
Objects should be centered at  and be scaled so that one unit is the
stroke thickness. e default stroke and fill is inherited from the edge’s
style.
pos
Location of the mark along the edge, from 0 (start) to 1 (end).
auto
fill
stroke
e default fill and stroke styles for CeTZ objects returned by draw. If
none, polygons will not be filled/stroked by default, and if auto, the style
is inherited from the edge’s stroke style.
auto
rev
Whether to reverse the mark so it points backwards.
false
flip
Whether to reflect the mark across the edge; the difference between
and , for example. A suffix ' in the name, such as "hook'", results in
a flip.
false
scale
Overall scaling factor. See also the mark-scale option of edge().
100%
10
Name Description Default
extrude
Whether to duplicate the mark and draw it offset at each extrude
position. For example, (inherit: "head", extrude: (-5, 0, 5)) looks
like .
(0,)
tip-origin
tail-origin
ese two properties control the coordinate of the point of the mark,
relative to . If the mark is acting as a tip ( or ) then tip-
origin applies, and tail-origin applies when the mark is a tail ( or
). See mark-debug().
0
tip-end
tail-end
ese control the coordinate at which the edge’s stroke terminates,
relative to . See mark-debug().
0
cap-offset
A function (mark, y) => x returning the coordinate at which the
edge’s stroke terminates relative to tip-end or tail-end, as a function of
the coordinate. is is relevant for extruded edges. See cap-offset().
e last few properties control the fine behaviours of how marks connect to the target point and to the
edge’s stroke. Briefly, a mark has four possibly-distinct center points. It is easier to show than to tell:
tip-end
tail-end
tip-origin
tail-origin
See mark-debug() and cap-offset() for details.
Detailed example
As a complete example, here is the implementation of a straight arrowhead in src/default-marks.typ:
#import cetz.draw
#let straight = (
size: 8,
sharpness: 20deg,
tip-origin: mark => 0.5/calc.sin(mark.sharpness),
tail-origin: mark => -mark.size*calc.cos(mark.sharpness),
fill: none,
draw: mark => {
draw.line(
(180deg + mark.sharpness, mark.size), // polar cetz coordinate
(0, 0),
(180deg - mark.sharpness, mark.size),
)
},
cap-offset: (mark, y) => calc.tan(mark.sharpness + 90deg)*calc.abs(y),
)
#set align(center)
#fletcher.mark-debug(straight)
#fletcher.mark-demo(straight)
tip-end
tail-end
tip-origin
tail-origin
Custom mark shorthands
While you can pass custom mark objects directly to the marks option of edge(), this can get annoying
if you use the same mark oen. In these cases, you can define your own mark shorthands.
Mark shorthands such as "hook->" search the state variable fletcher.MARKS for defined mark names.
#context fletcher.MARKS.get().at(">")
(inherit: "head", rev: false)
11
With a bit of care, you can modify the MARKS state like so:
Original marks:
#diagram(spacing: 2cm, edge("<->", stroke: 1pt))
#fletcher.MARKS.update(m => m + (
"<": (inherit: "stealth", rev: true),
">": (inherit: "stealth", rev: false),
"multi": (
inherit: "straight",
draw: mark => fletcher.cetz.draw.line(
(0, +mark.size*calc.sin(mark.sharpness)),
(-mark.size*calc.cos(mark.sharpness), 0),
(0, -mark.size*calc.sin(mark.sharpness)),
),
),
))
Updated marks:
#diagram(spacing: 2cm, edge("multi->-multi", stroke: 1pt + eastern))
Original marks:
Updated marks:
Here, we redefined which mark style the "<" and ">" shorthands refer to, and added an entirely new
mark style with the shorthand "multi".
Finally, I will restore the default state so as not to affect the rest of this manual:
#fletcher.MARKS.update(fletcher.DEFAULT_MARKS) // restore to built-in mark styles
CeTZ integration
Fletcher’s drawing capabilities are deliberately restricted to a few simple building blocks. However, an
escape hatch is provided with the render option of diagram() so you can intercept diagram data and
draw things using CeTZ directly.
Bézier edges
Here is an example of how you might hack together a Bézier edge using the same functions that
fletcher uses internally to anchor edges to nodes:
12
#diagram(
node((0,1), $A$, stroke: 1pt, shape: fletcher.shapes.diamond),
node((2,0), [Bézier], fill: purple.lighten(80%)),
render: (grid, nodes, edges, options) => {
// cetz is also exported as fletcher.cetz
cetz.canvas({
// this is the default code to render the diagram
fletcher.draw-diagram(grid, nodes, edges, debug: options.debug)
// retrieve node data by coordinates
let n1 = fletcher.find-node-at(nodes, (0,1))
let n2 = fletcher.find-node-at(nodes, (2,0))
let out-angle = 45deg
let in-angle = -110deg
fletcher.get-node-anchor(n1, out-angle, p1 => {
fletcher.get-node-anchor(n2, in-angle, p2 => {
// make some control points
let c1 = (to: p1, rel: (out-angle, 10mm))
let c2 = (to: p2, rel: (in-angle, 20mm))
cetz.draw.bezier(
p1, p2, c1, c2,
mark: (end: ">") // cetz-style mark
)
})
})
})
}
)
Bézier
Touying integration
You can create incrementally-revealed diagrams in Touying presentation slides by defining the follow-
ing touying-reducer:
#import "@preview/touying:0.2.1": *
#let diagram = touying-reducer.with(reduce: fletcher.diagram, cover: fletcher.hide)
#let (init, slide) = utils.methods(s)
#show: init
#slide[
Slide with animated figure:
#diagram(
node-stroke: .1em,
node-fill: gradient.radial(blue.lighten(80%), blue,
center: (30%, 20%), radius: 80%),
spacing: 4em,
edge((-1,0), "r", "-|>", `open(path)`, label-pos: 0, label-side: center),
node((0,0), `reading`, radius: 2em),
pause,
edge((0,0), (0,0), `read()`, "--|>", bend: 130deg),
edge(`read()`, "-|>"),
node((1,0), `eof`, radius: 2em),
pause,
edge(`close()`, "-|>"),
node((2,0), `closed`, radius: 2em, extrude: (-2.5, 0)),
edge((0,0), (2,0), `close()`, "-|>", bend: -40deg),
)
]
13
Reference
Main functions
diagram()
Draw a diagram containing node()s and edge()s.
diagram(
..args: array,
debug: bool 1 2 3,
axes: pair of directions,
spacing: length pair of lengths,
cell-size: length pair of lengths,
edge-stroke: stroke,
node-stroke: stroke none,
edge-corner-radius: length none,
node-corner-radius: length none,
node-inset: length pair of lengths,
node-outset: length pair of lengths,
node-shape: rect circle function,
node-fill: paint,
node-defocus: number,
label-sep: length,
label-size: length,
label-wrapper: function,
mark-scale: percent,
crossing-fill: paint,
crossing-thickness: number,
render: function,
)
..args array
Content to draw in the diagram, including nodes and edges.
e results of node() and edge() can be joined, meaning you can specify them as separate argu-
ments, or in a block:
#diagram(
// one object per argument
node((0, 0), $A$),
node((1, 0), $B$),
{
// multiple objects in a block
// can use scripting, loops, etc
node((2, 0), $C$)
node((3, 0), $D$)
},
for x in range(4) { node((x, 1) [#x]) },
)
Nodes and edges can also be specified in math-mode.
#diagram($
A & B \ // two nodes at (0,0) and (1,0)
C edge(->) & D \ // an edge from (0,1) to (1,1)
14
node(sqrt(pi), stroke: #1pt) // a node with options
$)
debug bool or 1 or 2 or 3 default false
Level of detail for drawing debug information. Level 1 or true shows a coordinate grid; higher
levels show bounding boxes and anchors, etc.
axes pair of directions default (ltr, ttb)
e orientation of the diagram’s axes.
is defines the elastic coordinate system used by nodes and edges. To make the coordinate
increase up the page, use (ltr, btt). For the matrix convention (row, column), use (ttb, ltr).
 

axes: (ltr, ttb)
0 1
1
0
 

axes: (ltr, btt)
0 1
0
1

 
axes: (ttb, ltr)
0 1
1
0
spacing length or pair of lengths default 3em
Gaps between rows and columns. Ensures that nodes at adjacent grid points are at least this far
apart (measured as the space between their bounding boxes).
Separate horizontal/vertical guers can be specified with (x, y). A single length d is short for
(d, d).
cell-size length or pair of lengths default 0pt
Minimum size of all rows and columns. A single length d is short for (d, d).
edge-stroke stroke default 0.048em
Default value of the stroke option of edge(). By default, this is chosen to match the thickness of
mathematical arrows such as in the current font size.
e default stroke is folded with the stroke specified for the edge. For example, if edge-stroke is
1pt and the stroke option of edge() is red, then the resulting stroke is 1pt + red.
node-stroke stroke or none default none
Default value of the stroke option of node().
e default stroke is folded with the stroke specified for the node. For example, if node-stroke is
1pt and the stroke option of node() is red, then the resulting stroke is 1pt + red.
15
edge-corner-radius length or none default 2.5pt
Default value of the corner-radius option of edge().
node-corner-radius length or none default none
Default value of the corner-radius option of node().
node-inset length or pair of lengths default 6pt
Default value of the inset option of node().
node-outset length or pair of lengths default 0pt
Default value of the outset option of node().
node-shape rect or circle or function default auto
Default value of the shape option of node().
node-fill paint default none
Default value of the fill option of node().
node-defocus number default 0.2
Default value of the defocus option of node().
label-sep length default 0.4em
Default value of the label-sep option of edge().
label-size length default 1em
Default value of the label-size option of edge().
label-wrapper function
Default value of the label-wrapper option of edge().
Default: edge => box(
[#edge.label],
inset: .2em,
radius: .2em,
fill: edge.label-fill,
)
16
mark-scale percent default 100%
Default value of the mark-scale option of edge().
crossing-fill paint default white
Color to use behind connectors or labels to give the illusion of crossing over other objects. See the
crossing-fill option of edge().
crossing-thickness number default 5
Default thickness of the occlusion made by crossing connectors. See crossing-thickness.
render function
Aer the node sizes and grid layout have been determined, the render function is called with the
following arguments:
grid: a dictionary of the row and column widths and positions;
nodes: an array of nodes (dictionaries) with computed aributes (including size and physical
coordinates);
edges: an array of connectors (dictionaries) in the diagram; and
options: other diagram aributes.
is callback is exposed so you can access the above data and draw things directly with CeTZ.
Default: (grid, nodes, edges, options) => {
cetz.canvas(draw-diagram(grid, nodes, edges, debug: options.debug))
}
17
node()
Draw a labelled node in a diagram which can connect to edges.
node(
..args: any,
pos: coordinate,
name: label none,
label: content,
inset: length,
outset: length,
fill: paint,
stroke: stroke,
extrude: array,
width: length auto,
height: length auto,
radius,
enclose: array,
corner-radius: length,
shape: rect circle function,
defocus: number,
snap: number false,
layer: number,
post: function,
)
..args any
e first positional argument is pos and the second, if given, is label.
pos coordinate default auto
Position of the node, or its center coordinate. is may be an elastic (row/column) coordinate like
(2, 1), or a CeTZ-style coordinate expression like (rel: (30deg, 1cm), to: (2, 1)).
See the options of diagram() to control the physical scale of elastic coordinates.
name label or none default none
An optional name to give the node.
Names can sometimes be used in place of coordinates. For example:
fletcher.diagram(
node((0,0), $A$, name: <A>),
node((1,0.6), $B$, name: <B>),
edge(<A>, <B>, "->"),
node((rel: (1, 0), to: <B>), $C$)
)
18
label content default none
Content to display inside the node.
If a node is larger than its label, you can wrap the label in align() to control the label alignment
within the node.
diagram(
node((0,0), align(bottom + left)[¡Hola!],
width: 3cm, height: 2cm, fill: yellow),
)
¡Hola!
inset length default auto
Padding between the node’s content and its outline.
In debug mode, the inset is visualised by a thin green outline.
diagram(
debug: 3,
node-stroke: 1pt,
node((0,0), [Hello,]),
edge(),
node((1,0), [World!], inset: 10pt),
)
Hello, World!
0 1
0
Defaults to the node-inset option of diagram().
outset length default auto
Margin between the node’s bounds to the anchor points for connecting edges.
is does not affect node layout, only how closely edges connect to the node.
In debug mode, the outset is visualised by a thin green outline.
diagram(
debug: 3,
node-stroke: 1pt,
node((0,0), [Hello,]),
edge(),
node((1,0), [World!], outset: 10pt),
)
Hello, World!
0 1
0
Defaults to the node-outset option of diagram().
fill paint default auto
Fill style of the node. e fill is drawn within the node outline as defined by the first extrude value.
Defaults to the node-fill option of diagram().
19
stroke stroke default auto
Stroke style for the node outline.
Defaults to the node-stroke option of diagram().
extrude array default (0,)
Draw strokes around the node at the given offsets to obtain a multi-stroke effect. Offsets may be
numbers (specifying multiples of the stroke’s thickness) or lengths.
e node’s fill is drawn within the boundary defined by the first offset in the array.
(0,) (0, 2) (2, 0) (0, -2.5, 2mm)
See also the extrude option of edge().
width length or auto default auto
Width of the node. If auto, the node’s width is the width of the node label, plus twice the inset.
If the width is not auto, you can use align to control the placement of the node’s label.
height length or auto default auto
Height of the node. If auto, the node’s height is the height of the node label, plus twice the inset.
If the height is not auto, you can use align to control the placement of the node’s label.
enclose array default ()
Positions or names of other nodes to enclose by enlarging this node.
If given, causes the node to resize so that its bounding rectangle surrounds the given nodes. e
center pos does not affect the node’s position if enclose is given, but still affects connecting edges.
diagram(
node-stroke: 1pt,
node((0,0), [ABC], name: <A>),
node((1,1), [XYZ], name: <Z>),
node(
text(teal)[Node group], stroke: teal,
enclose: (<A>, <Z>), name: <group>),
edge(<group>, (3,0.5), stroke: teal),
)
Node group
ABC
XYZ
corner-radius length default auto
Radius of rounded corners, if supported by the node shape.
Defaults to the node-corner-radius option of diagram().
20
shape rect or circle or function default auto
Shape of the node’s outline. If auto, one of rect or circle is chosen depending on the aspect ratio
of the node’s label.
Other shapes are defined in the fletcher.shapes submodule, including rect, circle, ellipse,
pill, parallelogram, trapezium, diamond, triangle, house, chevron, hexagon, and octagon.
Custom shapes should be specified as a function (node, extrude, ..parameters) => (..) which
returns cetz objects.
e node argument is a dictionary containing the node’s aributes, including its dimensions
(node.size), and other options (such as node.corner-radius).
e extrude argument is a length which the shape outline should be extruded outwards by. is
serves two functions: to support automatic edge anchoring with a non-zero node outset, and
to create multi-stroke effects using the extrude node option.
See the
src/shapes.typ
source file for example shape implementations.
Defaults to the node-shape option of diagram().
defocus number default auto
Strength of the “defocus” adjustment for connectors incident with this node.
is affects how connectors aach to non-square nodes. If 0, the adjustment is disabled and con-
nectors are always directed at the node’s exact center.
defocus: 0.2 defocus: 0 defocus: 1
Defaults to the node-defocus option of diagram().
snap number or false default 0
e snapping priority for edges connecting to this node. A higher priority means edges will au-
tomatically snap to this node over other overlapping nodes. If false, edges only snap to this node
if manually set with the snap-to option of edge().
Seing a lower value is useful if the node encloses other nodes that you want to snap to first.
layer number default auto
Layer on which to draw the node.
Objects on a higher layer are drawn on top of objects on a lower layer. Objects on the same layer
are drawn in the order they are passed to diagram().
Defaults to layer 0 unless the node encloses points, in which case layer defaults to -1.
21
post function default x => x
Callback function to intercept cetz objects before they are drawn to the canvas.
is can be used to hide elements without affecting layout (for use with Touying, for example).
e hide() function also helps for this purpose.
edge()
Draw a connecting line or arc in an arrow diagram.
edge(
..args: any,
vertices: array,
label: content,
label-side: left right center,
label-pos: number,
label-sep: length,
label-angle: angle left right top bottom auto,
label-anchor: anchor,
label-fill: bool paint,
label-size: auto length,
label-wrapper: auto function,
stroke: stroke,
dash: string,
decorations: none string function,
extrude: array,
shift: length number pair,
kind: string,
bend: angle,
corner: none left right,
corner-radius: length none,
marks: array,
mark-scale: percent,
crossing: bool,
crossing-thickness: number,
crossing-fill: paint,
snap-to: pair,
layer: number,
post: function,
)
..args any
An edge’s positional arguments may specify:
the edge’s vertices, each specified with a CeTZ-style coordinate
the label content
arrow marks, like "=>" or "<<-|-o"
other style flags, like "double" or "wave"
Vertex coordinates must come first, and are optional:
edge(from, to, ..) // explicit start and end nodes
edge(to, ..) == edge(auto, to, ..) // start snaps to previous node
edge(..) == edge(auto, auto, ..) // snaps to previous and next nodes
22
edge(from, v1, v2, ..vs, to, ..) // a multi-segmented edge
edge(from, "->", to) // for two vertices, the marks style can come in between
All vertices except the start point can be shorthand relative coordinate string containing the char-
acters lrudtbnesw or commas.
If given as positional arguments, an edge’s marks and label are disambiguated by guessing based
on the types. For example, the following are equivalent:
edge((0,0), (1,0), $f$, "->")
edge((0,0), (1,0), "->", $f$)
edge((0,0), (1,0), $f$, marks: "->")
edge((0,0), (1,0), "->", label: $f$)
edge((0,0), (1,0), label: $f$, marks: "->")
Additionally, some common options are given flags that may be given as string positional argu-
ments. ese are "dashed", "dotted", "double", "triple", "crossing", "wave", "zigzag", and
"coil". For example, the following are equivalent:
edge((0,0), (1,0), $f$, "wave", "crossing")
edge((0,0), (1,0), $f$, decorations: "wave", crossing: true)
vertices array default ()
Array of (at least two) coordinates for the edge.
Vertices can also be specified as leading positional arguments, but if so, the vertices option must
be empty. If the number of vertices is greater than two, kind defaults to "poly".
label content default none
Content for the edge label. See the label-pos and label-side options to control the position (and
label-sep and label-anchor for finer control).
label-side left or right or center default auto
Which side of the edge to place the label on, viewed as you walk along it from base to tip.
If center, then the label is placed directly on the edge and label-fill defaults to true. When
auto, a value of left or right is automatically chosen so that the label is:
roughly above the connector, in the case of straight lines; or
on the outside of the curve, in the case of arcs.
label-pos number default 0.5
Position of the label along the connector, from the start to end (from 0 to 1).
0 0.25 0.5 0.75 1
label-sep length
23
Separation between the connector and the label anchor.
With the default anchor (automatically set to "south" in this case):
-5pt
0pt
0.4em
0.8em
0 1 2 3 4 5 6 7
0
With label-anchor set to "center":
-5pt
0pt
0.4em
0.8em
0 1 2 3 4 5 6 7
0
Set debug to 2 or higher to see label anchors and outlines as seen here.
Default: the label-sep option of diagram()
label-angle angle or left or right or top or bottom or auto default 0deg
Angle to rotate the label (counterclockwise).
If a direction is given, the label is rotated so that the edge travels in that direction relative to the
label. If auto, the best of right or left is chosen.
0deg
90deg
auto
right
top
left
label-anchor anchor default auto
e CeTZ-style anchor point of the label to use for placement (e.g., "north-east" or "center").
If auto, the best anchor is chosen based on label-side, label-angle, and the edge’s direction.
label-fill bool or paint default auto
e background fill for the label. If true, defaults to the value of crossing-fill. If false or none,
no fill is used. If auto, then defaults to true if the label is covering the edge (label-side: center).
label-size auto or length
e default text size to apply to edge labels.
Default: the label-size option of diagram()
24
label-wrapper auto or function
Callback function accepting a node dictionary and returning the label content. is is used to add
a label background (see crossing-fill), and can be used to adjust the label’s padding, outline,
and so on.
diagram(edge($f$, label-wrapper: e =>
circle(e.label, fill: e.label-fill)))
Default: the label-wrapper option of diagram()
stroke stroke default auto
Stroke style of the edge. Arrows/marks scale with the stroke thickness (and with mark-scale).
dash string default none
e stroke’s dash style. is is also set by some mark styles. For example, seing marks: "<..>"
applies dash: "dotted".
decorations none or string or function default none
Apply a CeTZ path decoration to the stroke. Preset options are "wave", "zigzag", and
"coil" (which may also be passed as convenience positional arguments), but a decoration func-
tion may also be specified.
diagram(
$
A edge("wave") &
B edge("zigzag") &
C edge("coil") & D \
alpha &&& omega
$,
edge((0,1), (3,1), "<->", decorations:
cetz.decorations.wave
.with(amplitude: .4)
)
)
extrude array default (0,)
Draw a separate stroke for each extrusion offset to obtain a multi-stroke effect. Offsets may be
numbers (specifying multiples of the stroke’s thickness) or lengths.
(0,) (-1.5, 1.5) (-2, 0, 2) (-0.5em,) (0, 5pt)
Notice how the ends of the line need to shi a lile depending on the mark. is offset is computed
with cap-offset().
25
See also the extrude option of node().
shift length or number or pair default 0pt
Amount to shi the edge sideways by, perpendicular to its direction. A pair (from, to) controls
the shis at each end of the edge independently, and a single shi s is short for (s, s). Shis can
absolute lengths (e.g., 5pt) or coordinate differences (e.g., 0.1).
3pt
-3pt
If an edge has many vertices, the shis only affect the first and last segments of the edge.
diagram(
node-fill: luma(70%),
node((0,0), [Hello]),
edge("u,r,d", "->"),
edge("u,r,d", "-->", shift: 8pt),
node((1,0), [World]),
)
Hello World
kind string default auto
e kind of the edge, one of "line", "arc", or "poly". is is chosen automatically based on the
presence of other options (bend implies "arc", corner or additional vertices implies "poly").
bend angle default 0deg
Edge curvature. If 0deg, the connector is a straight line; positive angles bend clockwise.
-100deg
-50deg
0deg
50deg
100deg
corner none or left or right default none
Whether to create a right-angled corner, turning left or right. (Bending right means the corner
sticks out to the le, and vice versa.)
right
left
from
to
26
corner-radius length or none
Radius of rounded corners for edges with multiple segments. Note that none is distinct from 0pt.
none
0pt
5pt
is length specifies the corner radius for right-angled bends. e actual radius is smaller for acute
angles and larger for obtuse angles to balance things visually. (Trust me, it looks naff otherwise!)
Default: the edge-corner-radius option of diagram()
marks array default ()
e marks (arrowheads) to draw along an edge’s stroke. is may be:
A shorthand string such as "->" or "hook'-/->>". Specifically, shorthand strings are of the form
󰇨

󰈗
or
󰇨

󰈗

󰈑
, etc, where
󰍯
fletcher.MARKS
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
󰚜
head
stealth
bar
>>
}>
\
@
n?
doublehead
latex
cross
<<
<{
x
[]
1
triplehead
cone
hook
>>>
|
X
<>
1!
harpoon
circle
hooks
<<<
||
o
crowfoot
1?
straight
square
>
|>
|||
O
n
solid
diamond
<
<|
/
*
n!
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
󰚝
is a mark name and
fletcher.LINE_ALIASES -===--..~
is the line style.
An array of marks, where each mark is specified by name of as a mark object (dictionary of
parameters with a draw entry).
Shorthands are expanded into other arguments. For example, edge(p1, p2, "=>") is short for
edge(p1, p2, marks: (none, "head"), "double"), or more precisely, the result of edge(p1,
p2, ..fletcher.interpret-marks-arg("=>")).
Result Value of marks
"->"
">>-->"
"<=>"
"==>"
"->>-"
"x-/-@"
"|..|"
27
"hook->>"
"hook'->>"
"||-*-harpoon'"
("X", (inherit: "head", size: 15, sharpness: 40deg))
((inherit: "circle", pos: 0.5, fill: auto),)
mark-scale percent default 100%
Scale factor for marks or arrowheads, relative to the stroke thickness. See also the mark-scale
option of diagram().
100% 150% 200%
Note that the default arrowheads scale automatically with double and triple strokes:
-> => ==>
crossing bool default false
If true, draws a backdrop of color crossing-fill to give the illusion of lines crossing each other.
You can also pass "crossing" as a positional argument as a shorthand for crossing: true.
crossing-thickness number
ickness of the “crossing” background stroke (applicable if crossing is true) in multiples of the
normal stroke’s thickness.
11 22 44 88
Default: the crossing-thickness option of diagram()
28
crossing-fill paint
Color to use behind connectors or labels to give the illusion of crossing over other objects.
Default: the crossing-fill option of diagram()
snap-to pair default (auto, auto)
e nodes the start and end of an edge should snap to. Each node can be a position or node name,
or none to disable snapping. See also the snap option of node().
By default, an edge’s first and last vertices snap to nearby nodes. is option can be used in case
automatic snapping fails (if there are many nodes close together, for example.)
layer number default 0
Layer on which to draw the edge.
Objects on a higher layer are drawn on top of objects on a lower layer. Objects on the same layer
are drawn in the order they are passed to diagram().
post function default x => x
Callback function to intercept cetz objects before they are drawn to the canvas.
is can be used to hide elements without affecting layout (for use with Touying, for example).
e hide() function also helps for this purpose.
Behind the scenes
marks.typ
e default marks are defined in the fletcher.MARKS dictionary with keys: head, doublehead,
triplehead, harpoon, straight, solid, stealth, latex, cone, circle, square, diamond, bar, cross,
hook, hooks, >, <, >>, <<, >>>, <<<, |>, <|, }>, <{, |, ||, |||, /, \, x, X, o, O, *, @, [], <>, crowfoot, n, n!,
n?, 1, 1!, and 1?.
cap-offset()
resolve-mark()
draw-mark()
mark-debug()
29
cap-offset()
For a given mark, determine where that the stroke should terminate at, relative to the mark’s origin
point, as a function of the shi.
Imagine the tip-origin of the mark is at  . A stroke along the line  coming from
 terminates at , where  is the result of this function. Units are in multiples of
stroke thickness.
is is used to correctly implement multi-stroke marks, e.g., . e function mark-debug() can
help visualise a mark’s cap offset.
fletcher.mark-debug("O")
tip-end
tail-end
tip-origin
tail-origin
e dashed green line shows the stroke tip end as a function of , and the dashed red line shows where
the stroke ends if the mark is acting as a tail.
cap-offset(mark, shift)
resolve-mark()
Resolve a mark dictionary by applying inheritance, adding any required entries, and evaluating any
closure entries.
context fletcher.resolve-mark((
a: 1,
b: 2,
c: mark => mark.a + mark.b,
))
(
a: 1,
b: 2,
c: 3,
rev: false,
flip: false,
scale: 100%,
extrude: (0,),
tip-end: 0,
tail-end: 0,
tip-origin: 0,
tail-origin: 0,
)
resolve-mark(mark, defaults)
30
draw-mark()
Draw a mark at a given position and angle
draw-mark(
mark: dictionary,
stroke: stroke,
origin: point,
angle: angle,
debug: bool,
)
mark dictionary
Mark object to draw. Must contain a draw entry.
stroke stroke default 1pt
Stroke style for the mark. e stroke’s paint is used as the default fill style.
origin point default (0,0)
Coordinate of the mark’s origin (as defined by tip-origin or tail-origin).
angle angle default 0deg
Angle of the mark, 0deg being , counterclockwise.
debug bool default false
Whether to draw the origin points.
31
mark-debug()
Visualise a mark’s anatomy.
context {
let mark = fletcher.MARKS.get().stealth
// make a wide stealth arrow
mark += (angle: 45deg)
fletcher.mark-debug(mark)
}
tip-end
tail-end
tip-origin
tail-origin
Green/le stroke: the edge’s stroke when the mark is at the tip.
Red/right stroke: edge’s stroke if the mark is at the start acting as a tail.
Blue-white dot: the origin point

in the mark’s coordinate frame.
tip-origin: the -coordinate of the point of the mark’s tip.
tail-origin: the -coordinate of the mark’s tip when it is acting as a reversed tail mark.
tip-end: e -coordinate of the end point of the edge’s stroke (green stroke).
tail-end: e -coordinate of the end point of the edge’s stroke when acting as a tail mark (red
stroke).
Dashed green/red lines: e stroke end points as a function of . is is controlled by the special
cap-offset mark property and is used for multi-stroke effects like . See cap-offset().
is is mainly useful for designing your own marks.
mark-debug(
mark: string dictionary,
stroke: stroke,
show-labels: bool,
show-offsets: bool,
offset-range: number,
)
mark string or dictionary
e mark name or dictionary.
stroke stroke default 5pt
e stroke style, whose paint and thickness applies both to the stroke and the mark itself.
show-labels bool default true
Whether to label the tip/tail origin/end points.
show-offsets bool default true
Whether to visualise the cap-offset() values.
32
offset-range number default 6
e span above and below the stroke line to plot the cap offsets, in multiples of the stroke’s thick-
ness.
shapes.typ
To use built-in shapes in a diagram, import them with:
#import fletcher: shapes
#diagram(node([Hello], stroke: 1pt, shape: shapes.hexagon))
or:
#import fletcher.shapes: hexagon
#diagram(node([Hello], stroke: 1pt, shape: hexagon))
To set a shape parameter, use shape.with(..), for example hexagon.with(angle: 45deg). Shapes
respect the stroke, fill, width, height, and extrude options of edge().
rect()
circle()
ellipse()
pill()
parallelogram()
trapezium()
diamond()
triangle()
house()
chevron()
hexagon()
octagon()
rect()
e standard rectangle node shape.
A string "rect" or the element function rect given to the shape option of node() are interpreted as
this shape.
rect
rect(node, extrude)
33
circle()
e standard circle node shape.
A string "circle" or the element function circle given to the shape option of node() are interpreted
as this shape.
circle
circle(node, extrude)
ellipse()
An elliptical node shape.
ellipse
ellipse(
node,
extrude,
scale: number,
)
scale number default 1
Scale factor for ellipse radii.
pill()
A capsule node shape.
pill
pill(node, extrude)
parallelogram()
A slanted rectangle node shape.
parallelogram
parallelogram(
node,
extrude,
flip,
angle: angle,
fit: number,
)
34
angle angle default 20deg
Angle of the slant, 0deg is a rectangle. Don’t set to 90deg unless you want your document to be
larger than the solar system.
fit number default 0.8
Adjusts how comfortably the parallelogram fits the label’s bounding box.
fit: 0 fit: 0.5 fit: 1
trapezium()
An isosceles trapezium node shape.
trapezium
trapezium(
node,
extrude,
dir: top bottom left right,
angle: angle,
fit: number,
)
dir top or bottom or left or right default top
e side the shorter parallel edge is on.
angle angle default 20deg
Angle of the slant, 0deg is a rectangle. Don’t set to 90deg unless you want your document to be
larger than the solar system.
fit number default 0.8
Adjusts how comfortably the trapezium fits the label’s bounding box.
fit: 0 fit: 0.5 fit: 1
35
diamond()
A rhombus node shape.
diamond
diamond(
node,
extrude,
fit: number,
)
fit number default 0.5
Adjusts how comfortably the diamond fits the label’s bounding box.
fit: 0
fit: 0.5
fit: 1
triangle()
An isosceles triangle node shape.
One of angle or aspect may be given, but not both. e triangle’s base coincides with the label’s base
and widens to enclose the label; see hps://www.desmos.com/calculator/i4i9svunj4.
triangle
triangle(
node,
extrude,
dir: top bottom left right,
angle: angle auto,
aspect: number auto,
fit: number,
)
dir top or bottom or left or right default top
Direction the triangle points.
angle angle or auto default auto
Angle of the triangle opposite the base.
aspect number or auto default auto
Aspect ratio of triangle, or the ratio of its base to its height.
36
fit number default 0.8
Adjusts how comfortably the triangle fits the label’s bounding box.
fit: 0
fit: 0.5
fit: 1
house()
A pentagonal house-like node shape.
house
house(
node,
extrude,
dir: top bottom left right,
angle: angle,
)
dir top or bottom or left or right default top
Direction of the roof of the house.
angle angle default 10deg
e slant of the roof. A plain rectangle is 0deg, and 90deg is a sky scraper stretching past Pluto.
chevron()
A chevron node shape.
chevron
chevron(
node,
extrude,
dir: top bottom left right,
angle: angle,
fit: number,
)
dir top or bottom or left or right default right
Direction the chevron points.
angle angle default 30deg
e slant of the arrow. A plain rectangle is 0deg.
37
fit number default 0.8
Adjusts how comfortably the chevron fits the label’s bounding box.
fit: 0 fit: 0.5 fit: 1
hexagon()
An (irregular) hexagon node shape.
hexagon
hexagon(
node,
extrude,
angle: angle,
fit: number,
)
angle angle default 30deg
Half the exterior angle, 0deg being a rectangle.
fit number default 0.8
Adjusts how comfortably the hexagon fits the label’s bounding box.
fit: 0 fit: 0.5 fit: 1
octagon()
A truncated rectangle node shape.
octagon
octagon(
node,
extrude,
truncate: number length,
)
truncate number or length default 0.5
Size of the truncated corners. A number is interpreted as a multiple of the smaller of the node’s
width or height.
38
coords.typ
uv-to-xy()
xy-to-uv()
duv-to-dxy()
dxy-to-duv()
vector-polar-with-xy-or-uv-length()
resolve()
uv-to-xy()
Convert from elastic to absolute coordinates,
 
.
Elastic coordinates are specific to the diagram and adapt to row/column sizes; absolute coordinates are
the final, physical lengths which are passed to cetz.
uv-to-xy(grid: dictionary, uv: array)
grid dictionary
Representation of the grid layout, including:
origin
centers
spacing
flip
e grid is passed to the render option of diagram().
uv array
Elastic coordinate, (float, float).
xy-to-uv()
Convert from absolute to elastic coordinates,  .
Inverse of uv-to-xy().
xy-to-uv(grid, xy)
duv-to-dxy()
Jacobian of the coordinate map uv-to-xy().
Used to convert a “nudge” in  coordinates to a “nudge” in  coordinates. is is needed because
 coordinates are non-linear (they’re elastic). Uses a balanced finite differences approximation.
duv-to-dxy(
grid: dictionary,
uv: array,
duv: array,
)
39
grid dictionary
Representation of the grid layout. e grid is passed to the render option of diagram().
uv array
e point (float, float) in the -manifold where the shi tangent vector is rooted.
duv array
e shi tangent vector (float, float) in  coordinates.
dxy-to-duv()
Jacobian of the coordinate map xy-to-uv().
dxy-to-duv(
grid,
xy,
dxy,
)
vector-polar-with-xy-or-uv-length()
Return a vector rooted at a  coordinate with a given angle in -space but with a length specified
in either -space or -space.
vector-polar-with-xy-or-uv-length(
grid,
xy,
target-length,
θ,
)
resolve()
Resolve CeTZ-style coordinate expressions to absolute vectors.
is is an drop-in replacement of cetz.coordinate.resolve() but extended to handle fletcher’s elas-
tic  coordinates alongside CeTZ’ physical  coordinates. e target coordinate system must be
specified in the context object ctx.
Resolving  coordinates to or from  coordinates requires the diagram’s grid, which defines the
non-linear maps uv-to-xy() and xy-to-uv(). e grid may be supplied in the context object ctx.
If grid is not supplied, coordinate resolution may fail, in which case the vector (NaN, NaN) is
returned.
resolve(
ctx: dictionary,
..coordinates: coordinate,
update,
)
40
ctx dictionary
CeTZ canvas context object, additionally containing:
target-system: the target coordinate system to resolve to, one of "uv" or "xyz".
grid (optional): the diagram’s grid specification, defining the coordinate maps  . If not
given, coordinates requiring this map resolve to (NaN, NaN).
..coordinates coordinate
CeTZ-style coordinate expression(s), e.g., (1, 2), (45deg, 2cm), or (rel: (+1, 0), to: "name").
diagram.typ
interpret-axes()
expand-fractional-rects()
compute-cell-sizes()
compute-cell-centers()
compute-grid()
interpret-axes()
Interpret the axes option of diagram().
Returns a dictionary with:
x: Whether is reversed
y: Whether is reversed
xy: Whether the axes are swapped
interpret-axes(axes: array) -> dictionary
axes array
Pair of directions specifying the interpretation of  coordinates. For example, (ltr, ttb)
means goes and goes .
expand-fractional-rects()
Convert an array of rects (center: (x, y), size: (w, h)) with fractional positions into rects with
integral positions.
If a rect is centered at a factional position floor(x) < x < ceil(x), it will be replaced by two new
rects centered at floor(x) and ceil(x). e total width of the original rect is split across the two new
rects according two which one is closer. (E.g., if the original rect is at x = 0.25, the new rect at x = 0
has 75% the original width and the rect at x = 1 has 25%.) e same spliing procedure is done for y
positions and heights.
is is the algorithm used to determine grid layout in diagrams.
expand-fractional-rects(rects: array) -> array
41
rects array
An array of rects of the form (center: (x, y), size: (width, height)). e coordinates x
and y may be floats.
compute-cell-sizes()
Determine the number and sizes of grid cells needed for a diagram with the given nodes and edges.
Returns a dictionary with:
origin: (u-min, v-min) Coordinate at the grid corner where elastic/uv coordinates are minimised.
cell-sizes: (x-sizes, y-sizes) Lengths and widths of each row and column.
compute-cell-sizes(
grid: dictionary,
verts,
rects,
)
grid dictionary
Representation of the grid layout, including:
flip
compute-cell-centers()
Determine the centers of grid cells from their sizes and spacing between them.
Returns the a dictionary with:
centers: (x-centers, y-centers) Positions of each row and column, measured from the corner
of the bounding box.
bounding-size: (x-size, y-size) Dimensions of the bounding box.
compute-cell-centers(grid: dictionary) -> dictionary
grid dictionary
Representation of the grid layout, including:
cell-sizes: (x-sizes, y-sizes) Lengths and widths of each row and column.
spacing: (x-spacing, y-spacing) Gap to leave between cells.
42
compute-grid()
Determine the number, sizes and relative positions of rows and columns in the diagram’s coordinate
grid.
Rows and columns are sized to fit nodes. Coordinates are not required to start at the origin, (0,0).
compute-grid(
rects,
verts,
options,
)
node.typ
measure-node-size()
resolve-node-enclosures()
resolve-node-coordinates()
measure-node-size()
Measure node labels with the style context and resolve node shapes.
Widths and heights that are auto are determined by measuring the size of the node’s label.
measure-node-size(node, styles)
resolve-node-enclosures()
Process the enclose options of an array of nodes.
resolve-node-enclosures(nodes, ctx)
resolve-node-coordinates()
Resolve node positions to a target coordinate system in sequence.
CeTZ-style coordinate expressions work, with the previous coordinate () referring to the resolved
position of the previous node.
e resolved coordinates are added to each node’s pos dictionary.
resolve-node-coordinates(nodes: array, ctx: dictionary) -> array
nodes array
Array of nodes, each a dictionary containing a pos entry, which should be a CeTZ-compatible
coordinate expression.
ctx dictionary default (:)
CeTZ-style context to be passed to resolve(ctx, ..). is must contain target-system, and
optionally grid.
43
edge.typ
interpret-marks-arg()
interpret-edge-args()
apply-edge-shift()
interpret-marks-arg()
Parse and interpret the marks argument provided to edge(). Returns a dictionary of processed edge()
arguments.
interpret-marks-arg(arg: string array) -> dictiony
arg string or array
Can be a string, (e.g. "->", "<=>"), etc, or an array of marks. A mark can be a string (e.g., ">" or
"head", "x" or "cross") or a dictionary containing the keys:
kind (required) the mark name, e.g. "solid" or "bar"
pos the position along the edge to place the mark, from 0 to 1
rev whether to reverse the direction
parameters specific to the kind of mark, e.g., size or sharpness
interpret-edge-args()
Interpret the positional arguments given to an edge()
Tries to intelligently distinguish the from, to, marks, and label arguments based on the argument
types.
Generally, the following combinations are allowed:
edge(..<coords>, ..<marklabel>, ..<options>)
<coords> = () or (to) or (from, to) or (from, ..vertices, to)
<marklabel> = (marks, label) or (label, marks) or (marks) or (label) or ()
<options> = any number of options specified as strings
interpret-edge-args(args, options)
apply-edge-shift()
Apply the shift option of edge() by translating edge vertices.
apply-edge-shift(grid: dictionary, edge: dictionary)
grid dictionary
Representation of the grid layout. is is needed to support shis specified as coordinate lengths.
edge dictionary
e edge with a shift entry.
44
draw.typ
place-edge-label-on-curve()
draw-edge-line()
draw-edge-arc()
draw-edge-polyline()
find-farthest-intersection()
get-node-anchor()
defocus-adjustment()
draw-debug-axes()
hide()
place-edge-label-on-curve()
Draw an edge label at point along a curve.
Label is drawn near the point curve(edge.label-pos), respecting the label options of edge() such as
label-side and label-angle.
place-edge-label-on-curve(
edge: dictionary,
curve: function,
debug,
)
edge dictionary
Edge object. Must include:
label-pos
label-sep
label-side
label-anchor
label-angle
label-wrapper
curve function
Parametric curve
󰈗
describing the shape of the edge in  coordinates.
draw-edge-line()
Draw a straight edge.
draw-edge-line(edge: dictionary, debug: int)
45
edge dictionary
e edge object, a dictionary, containing:
vertices: an array of two points, the line’s start and end points.
extrude: An array of extrusion lengths to apply a multi-stroke effect with.
stroke: e stroke style.
marks: An array of marks to draw along the edge.
label: Content for label.
label-side, label-pos, label-sep, and label-anchor.
debug int default 0
Level of debug details to draw.
draw-edge-arc()
Draw a bent edge.
draw-edge-arc(edge: dictionary, debug: int)
edge dictionary
e edge object, a dictionary, containing:
vertices: an array of two points, the arc’s start and end points.
bend: e angle of the arc.
extrude: An array of extrusion lengths to apply a multi-stroke effect with.
stroke: e stroke style.
marks: An array of marks to draw along the edge.
label: Content for label.
label-side, label-pos, label-sep, and label-anchor.
debug int default 0
Level of debug details to draw.
draw-edge-polyline()
Draw a multi-segment edge
draw-edge-polyline(edge: dictionary, debug: int)
46
edge dictionary
e edge object, a dictionary, containing:
vertices: an array of at least two points to draw segments between.
corner-radius: Radius of curvature between segments.
extrude: An array of extrusion lengths to apply a multi-stroke effect with.
stroke: e stroke style.
marks: An array of marks to draw along the edge.
label: Content for label.
label-side, label-pos, label-sep, and label-anchor.
debug int default 0
Level of debug details to draw.
find-farthest-intersection()
Of all the intersection points within a set of CeTZ objects, find the one which is farthest from a target
point and pass it to a callback.
If no intersection points are found, use the target point itself.
find-farthest-intersection(
objects: cetz array none,
target: point,
callback,
)
objects cetz array or none
Objects to search within for intersections. If none, callback is immediately called with target.
target point
Target point to sort intersections by proximity with, and to use as a fallback if no intersections
are found.
get-node-anchor()
Get the anchor point around a node outline at a certain angle.
get-node-anchor(
node,
θ,
callback,
)
47
defocus-adjustment()
Return the anchor point for an edge connecting to a node with the “defocus” adjustment.
Basically, for very long/wide nodes, don’t make edges coming in from all angles go to the exact node
center, but “spread them out” a bit.
See hps://www.desmos.com/calculator/irt0mvixky.
defocus-adjustment(node, θ)
draw-debug-axes()
Draw diagram coordinate axes.
draw-debug-axes(grid: dictionary, debug)
grid dictionary
Dictionary specifying the diagram’s grid, containing:
origin: (u-min, v-min), the minimum values of elastic coordinates,
flip: (x, y, xy), the axes orientation (see interpret-axes()),
centers: (x-centers, y-centers), the physical offsets of each row and each column,
cell-sizes: (x-sizes, y-sizes), the physical sizes of each row and each column.
hide()
Make diagram contents invisible, with or without affecting layout. Works by wrapping final drawing
objects in cetz.draw.hide.
rect(diagram({
fletcher.hide({
node((0,0), [Can't see me])
edge("->")
})
node((1,1), [Can see me])
}))
Can see me
hide(objects: content array, bounds: bool)
objects content or array
Diagram objects to hide.
bounds bool default true
If false, layout is as if the objects were never there; if true, the layout treats the objects is present
but invisible.
48
utils.typ
interp()
interp-inv()
get-arc-connecting-points()
is-space()
interp()
Linearly interpolate an array with linear behaviour outside bounds
interp(
values: array,
index: int float,
spacing: length,
)
values array
Array of lengths defining interpolation function.
index int or float
Index-coordinate to sample.
spacing length default 0pt
Gradient for linear extrapolation beyond array bounds.
interp-inv()
Inverse of interp().
interp-inv(
values: array,
value,
spacing: length,
)
values array
Array of lengths defining interpolation function.
value: Value to find the interpolated index of.
spacing length default 0pt
Gradient for linear extrapolation beyond array bounds.
49
get-arc-connecting-points()
Determine arc between two points with a given bend angle
e bend angle is the angle between chord of the arc (line connecting the points) and the tangent to
the arc and the first point.
Returns a dictionary containing:
center: the center of the arc’s curvature
radius
start: the start angle of the arc
stop: the end angle of the arc
get-arc-connecting-points(
from: point,
to: point,
angle: angle,
) ->
dictionary
from point
2D vector of initial point.
to point
2D vector of final point.
angle angle
e bend angle between chord of the arc (line connecting the points) and the tangent to the arc
and the first point.
0deg 45deg -90deg
is-space()
Return true if a content element is a space or sequence of spaces
is-space(el)
50