Quick Overview

Import fletcher with:

#import "@preview/fletcher:0.6.0" as fletcher: diagram, node, edge

Diagrams contain node()s and edge()s. Nodes contain content and can have various shapes and styles, while edges snap to nodes and can be given marks and labels. Nodes and edges can be placed in a diagram() or directly into a CeTZ canvas. Objects in a diagram() are arranged on a flexible coordinate grid.

#diagram({
  node((0,0), $G$)
  edge("->", $f$) // start/end at prev/next node
  node((1,0), $im(f)$)
  edge((0,0), "->>", $pi$) // start given; end is next node
  edge("<--hook'", $tilde(f)$)
  node((0,1), $G slash ker(f)$)
})

Styles can be controlled with the named arguments of node() and edge(). Default styles can be set by passing the same arguments prefixed with “node-” or “edge-” to diagram().

#diagram(
  spacing: (1cm, 4mm), // column and row gutter
  node-inset: 8pt, // padding around node content
  node-fill: teal.mix(fuchsia),
  edge-stroke: 2pt + gray,
  node((0,0))[A], edge(), node((1,0))[B],
  node((2,1))[C], node((2,-1), fill: orange)[D],
  edge((1,0), (2,1), bend: 45deg),
  edge((1,0), "=>", (2,-1), stroke: 1pt + black),
)

You can also place nodes and edges directly into cetz.canvas() and set node and edge styles with cetz.draw.set-style(). Below, we use fletcher’s marks to draw an edge in a CeTZ-based figure.

#import fletcher.cetz // import this way to ensure compatibility
#cetz.canvas(length: 5mm, {
  import cetz.draw: *
  set-style(fill: teal, edge: (stroke: 1pt + teal))
  circle((5,0), radius: (1.2,0.8), name: "egg")
  line((0,1), (1,0), (0,-1), (-1,0), close: true, name: "jewel")
  edge(<jewel>, "stealth--stealth", <egg>, bend: 45deg)
})

Flexible coordinate grids

Diagrams are laid out on a flexible coorginate grid, visible when the debug option of diagram() is on. The rows and columns in this coordinate system grow to accommodate the sizes of nodes, useful for tabular layouts:

#diagram(
  debug: "grid", // show helper annotations
  spacing: 5mm,  // gutter between cells
  node((0,0), stroke: yellow, [Wide node]),
  node((1,0), fill: green, [A\ tall\ node], name: "tall"),
  node((0,1), fill: red, emph[top left], align: top + left),
  node((1,1), fill: blue, text(white, $ a/b $)),
  cetz.draw.polygon("tall.north-east", 6, radius: 4pt)
)

Unlike a table, coordinates can be fractional; the center of a node placed at 0.25 is 25% between the adjacent columns or rows. Notice how the column sizes respond to the green node:

When inside a flexigrid, nodes can also span multiple columns or rows:

#diagram(
  spacing: 4pt,
  node-stroke: 1pt,
  node-fill: white,
  node-corner-radius: 2pt,
  node((0,0), [Sight], name: <sight>),
  node((1,0), [Sound]),
  node((2,0), [Smell], name: <smell>),
  node((0,1), [Senses], colspan: 3),
  node((0,0), rowspan: 2, colspan: 3,
    fill: yellow, extrude: 4pt, layer: -1, []),
)

Under the hood, a diagram() is just a flexigrid() inside a CeTZ canvas. You can also use the flexigrid coordinate system inside a normal CeTZ canvas (see CeTZ Integration for details.)

Coordinate expressions

Dimensionless coordinates like (2, 3) in a node() or edge() refer to columns/rows in the grid. You can also use physical lengths like (20mm, 5mm) or any CeTZ coordinate expression, including anchors.

#diagram(
  node((1, 0), name: "O"), // elastic coordinate
  for i in range(12) {
    let θ = i/12*360deg
    edge(<O>, "->", auto)
    node((rel: (θ, 10mm), to: "O"), $ #i $, inset: 4pt)
  }
)