Edges

edge(..vertices, marks, labels, ..)

Use the edge() function inside a diagram(), cetz.canvas() or flexigrid() to draw lines or paths with various edge effects, including:

You can specify an edge with a sequence of vertex coordinates, similar to cetz.draw.line(..). There are a few built-in edge kinds which accept extra named arguments (such as bend: 30deg for arc edges or corner: "|-" for right-angled corners).

Edge effects can also be applied to any CeTZ path by wrapping it in the edge() function.

Specifying vertices

Vertices should be the first arguments, unless there are two vertices, in which case you may put the marks argument in between:

edge((0,0), (1,0), (2,1), "->")
edge(<from>, "->", <to>)

Alternatively, an array of vertices may be supplied to the edge.vertices argument.

Automatic vertices

To refer to the previous or next node in a diagram, you can use auto, as in:

edge(<from>, <to>)
edge(<from>, auto)  // to next node
edge(auto, <to>)    // from previous node
edge(auto, auto)    // connects surrounding nodes

In unambiguous situations, auto vertices can be omitted altogether:

edge(<coord>) == edge(<coord>, auto)
edge("->", <coord>) == edge(auto, "->", <coord>)

For example, this diagram uses implicit automatic coordinates:

#diagram({
  edge((-1,0), "~>")               // to next node (A)
  node((0,0), [A])
  edge("<..>")                     // connects A to B
  edge("->", (.5,1), bend: -30deg) // from previous node (A)
  node((1,0), [B])
})

Relative coordinates

You may use strings such as "u" (up) or "sw" (south west) as shorthands for relative vertex coordinates (rel: (du, dv)). The first letters of top/up/north, bottom/down/south, left/west, and right/east are allowed.

Commas can be used to separate multiple coordinates, so "r,d" is understood as "r", "d" which is (rel: (1, 0)), (rel: (0, 1)), assuming flexigrid.axes is set so (𝑢,𝑣) goes (→,↓).

#diagram(
  spacing: 5mm,
  node((0,0), $A$),
  edge("u,rr,d", "-->"),
  node((1,0), stroke: 1pt)[Box],
  node((2,0), $B$),
)

In example above, the edge implicitly begins from the previous node (relative coordinates cannot be used as the first coordinate).

Edge labels

Edges can have any number of labels attached to them at specific positions. Any markup or math content passed to edge() after vertices is interpreted a label.

#diagram(
  debug: "edge.label",
  node((0,0), [A]),
  edge("->", [Label], bend: 60deg),
  edge("->", label: $f$, bend: -60deg),
  node((1,0), [B]),
)

The following edge options set properties of the edge’s label(s):

To specify multiple labels, pass an array of dictionaries to edge.label, where each contains properties without the label- prefix:

label: (
  (body: [First label], pos: .., side: ..),
  (body: [Second label], angle: .., sep: ..),
)

In the example below, the vertical edges have two labels each:

#diagram(
  spacing: 15mm,
  node((0,0), $V$),   edge("->", $f$),        node((1,0), $W$),
  node((0,1), $K^n$), edge("->", $tilde(f)$), node((1,1), $K^m$),

  edge((0,0), (0,1), "->", label: (
    (body: $ tilde $, angle: auto),
    (body: $kappa_X$, side: right),
  ), label-sep: 3pt), // label-sep applies to both labels

  edge((1,0), (1,1), "->", label: (
    (body: $ tilde $, angle: auto),
    (body: $kappa_Y$, side: right),
  ), label-sep: 3pt),
)

Kinds of edges

To make it easy to achieve common edge shapes, like arcs, loops or right-angled corners, edges can have different kinds, depending on the combination of named arguments present.

Edge KindArguments
arcbend
bezier-cubicfrom, to
bezier-fromfrom
bezier-toto
bezier-throughthrough
looploop, size, fit
bowbow, size, angle, fit
cornercorner
custompath

Polyline

By default, edges are displayed as straight paths between two or more vertices. As with all edge kinds, they can have rounded corners.

Arc

edge(.., bend: angle | length)

A perfect arc can be made with the bend option, which can be an angle (measuring the initial angle of the arc relative to a straight edge) or a length, specifying the height of the arc.

Loop

edge(pt, loop: angle | direction, size: length, fit: number)

Loop edges are perfect arcs with only one vertex. The required option loop specifies the general direction of the loop. The diameter can be adjusted with size, and the distance from the vertex to the center of curvature can be adjusted with fit.

The fit parameter moves the loop inward or outward from the node. The default value fit: 1 ensures the loop meets circular nodes at right angles. When fit: 0, the loop touches the node’s center.

Bow

edge(pt, bow: angle | direction, size: length, angle: angle, fit: number)

Bows have one vertex, like loops. The required option bow specifies the general direction of the edge. The size and angle options specify the length and wideness of the bow, which is formed as a cubic Bézier curve with its control points arranged in an isosceles triangle.

Like for loop edges, the fit option fine-tunes how far in or out the bow is from the node.

Bézier

edge(.., from: angle | (angle, length), to: angle | (angle, length))

Quadratic or cubic Bézier curves can be specified by giving an angle or polar coordinate such as (90deg, 5mm) to one of from, to or both.

You can also specify a Bézier curve through another point with edge(.., through: <coord>).

Corner

edge(.., corner: "|-" | "-|" | "-|-" | "|-|")

Edges with one or two right-angled corners can be specified with corner, which is a string of "-" and "|" specifying the order of horizontal or vertical segments.

Custom

edge(.., path: vertices => ..)

The custom edge kind accepts a path option, which must be a function accepting an array of edge vertices and returning a single CeTZ path. Note that you can also wrap CeTZ paths in edges directly.

For example, below we make a custom path which draws the edge as a Hobby curve through its vertices.

#diagram(
  $
    A & B & C \
    D & E & F \
    G & H & I
  $,
  edge((0,0), "rr,d,ll,dr", "-->",
    path: verts => cetz.draw.hobby(..verts)),
)

Wrapping CeTZ paths

When integrating with CeTZ, you can wrap a CeTZ element in edge() to apply any of fletcher’s edge effects to it. When used in this mode, edges cannot have vertices. To support edge vertices, consider using the custom edge kind.

For example, below we draw a composite CeTZ path using lines and a cubic Bézier segment and apply fletcher’s marks, multistroke effects, label placement and snapping.

#cetz.canvas({
  import cetz.draw: *
  circle((0,0), radius: .4, name: "orb")

  // cetz path to be used as edge
  let path = merge-path({
    line((0,0), (0,1))
    bezier((0,1), (2,0), (1,1), (1,0))
    line((2,0), (2,1))
  })

  // add edge effects to cetz path
  edge(path, "<=>", snap-to: ("orb", none), label: (
    (body: $L$, pos: 0.5),
    (body: $R$, pos: 2.5, side: right),
    (body: `mid`, pos: 1.5, side: center, angle: auto),
  ), label-sep: 10pt)
})

Path anchors

Edges support path anchors, like most CeTZ elements. This allows you to refer to points along an edge. In particular, the label position is a path anchor, and if the edge has a edge.name, you can place other elements using the coordinates <name.anchor>, "name.anchor" or (name: "name", anchor: "anchor").

Path anchorDescription
25%Fraction of total length along path
"start", "mid", "end"Aliases for 0%, 50% and 100%
10pt, -2emCertain length from start or end of path
0.5, 2.5, -1Segment indices referring to or between vertices of the path

The default path anchor is the midpoint, so in the example below <f> refers to (name: "f", anchor: 50%).

#diagram(
  spacing: 15mm,
  node((0,0), $X$),
  edge("->", bend: +60deg, $f$, name: <f>),
  edge("->", bend: -60deg, $g$, name: <g>),
  node((1,0), $Y$),

  edge(<f>, "=>", <g>, $alpha$, shorten: 3pt),
)

Fletcher defines segment indices in addition to CeTZ’s usual path anchors, to make it easier to place labels along complex paths. A segment index is a number which interpolates between the edge’s vertices. For example, 0 is the start, 1 is the first vertex, and 2.5 is halfway along the third segment of the edge. Negative indices refer to vertices in reverse order.

#diagram(
  spacing: 15mm,
  import cetz.draw: *,
  set-style(circle: (radius: 4pt)),
  edge("r,t,rd,r", ">>->", name: "foo", stroke: 1pt,
    [3#super[rd]], label-pos: 2),     // label at 3rd vertex
  circle("foo.0.5", stroke: red),     // 50% along first segment
  circle("foo.1", stroke: green),     // second vertex
  circle("foo.-0", stroke: blue),     // last vertex
  circle("foo.2.5", stroke: fuchsia), // 50% along third segment
)

Importantly, node positions cannot depend on edge anchors. This is because nodes are processed before edges. To place annotations on edges, you can use labels or draw directly with CeTZ, like in the example above.