Reference

These pages describe the package as it stands on main. A Typst import names an exact version, so what a document sees is whatever it asked for — the changelog is what says which release each of these landed in, and what is still waiting.

lamport-diagram

lamport-diagram(
  caption: none,
  replicas: (),
  events: (:),
  orientation: horizontal,
  col-gap: 2.0,
  row-gap: 1.5,
  text-size: 0.62em,
  dot: 0.095,
  message-stroke: 0.9pt + luma(110),
)

replicas fixes the row order, top to bottom. Each entry is an id string, a replica — which also carries that lane’s event defaults — or a bare dictionary of the same fields.

events maps each replica id to that replica’s local history in order. An entry is bare content or a bare string for a local event, or one of event, send, recv, sync, idle and gap. Every replica must have an entry, and every entry must name a declared replica.

With a caption the result is a figure; without one it is the bare drawing, to place inside a figure of your own. col-gap and row-gap are canvas centimetres, and are the knobs for a diagram that reads too cramped or too sparse.

orientation

horizontal   // = rightwards
vertical     // = downwards
rightwards   leftwards   downwards   upwards

Which way logical time runs. rightwards and leftwards lay the timelines out as rows and stack the replicas downwards; downwards and upwards lay them out as columns and stack the replicas rightwards. horizontal and vertical are the two that need no thinking about, and are rightwards and downwards under a shorter name. These are plain strings, so orientation: "vertical" works without importing anything.

The orientation decides which sides a label may sit on, and which one it sits on by default:

Orientation Sides Default
horizontal, rightwards, leftwards above, below above
vertical, downwards, upwards left, right right

A side the orientation has no room for — above on a vertical diagram, left on a horizontal one — is not an error: it is dropped back to that default and otherwise ignored, so flipping a finished diagram from horizontal to vertical stays one edit rather than a compile error on every lane that named a side. It passes in silence, which is not the ideal: the ideal is a compiler warning, and Typst gives user code no way to raise one. Printing the complaint into the document instead would put it in front of the reader rather than the author, which is worse than saying nothing.

The worked example from the front page, drawn vertically

That is gallery/vertical.typ.

replica

replica(name, ..defaults)

A replica lane, and the defaults the local events on it fall back on. name is the id that the events dictionary keys on.

label, color and position may also be given positionally, in any order: they are told apart by type, so replica("A", below, red) and replica("B", red, below) are the same lane. The rest must be named.

None of these defaults reach a send or recv label: those keep their own arguments, and their side is chosen to stay clear of the message arrow.

event

event(..args)

A local event on a replica’s timeline. Its body is the label — content or a plain string.

The dot itself never moves: it is the event’s place in time, which the layout solves for.

Arguments are told apart by type, so they may come in any order and every one of them is optional: event(below, +50%)[AddFile1] and event(+50%, below, "AddFile1") are the same event. For the common case of a label and nothing else, bare content or a bare string in an events array is shorthand, so [AddFile1], "AddFile1" and event[AddFile1] are the same event too.

send and recv

send(name, ..args)
recv(name, ..args)

The points where the message name leaves one replica and is applied on another. Exactly one send and one recv must exist for each name.

An optional label for the point goes positionally — send("push")[pushed], recv("pull")[now duplicated] — or as body, with size setting its text size and at overriding the side it sits on.

Both take displacement, which nudges the point off the column it is solved into, in either direction. A ratio is taken against the column gap. Only the defaults differ:

send also takes:

The nudge is a drawing offset the column solver knows nothing about, so a negative one wide enough to put a point visually behind its own counterpart does not trip the causal-cycle check. It is equally outside what the drawing sizes itself to, so a displacement large enough to push a bodiless mark left of where its lane starts will leave it overhanging the replica name.

sync

sync(name, ..args)

One end of a two-way exchange. In a single round trip each side gives the other the events it lacks, so both ends come out of the exchange holding the same events — which is not the same as holding the same state, so each end takes its own label. A send/recv pair is the one-way message by comparison.

Exactly two sync points must carry the same name, and they must sit on two different replicas. The pair is drawn as one arrow with a head at each end, and the two ends share a column: neither side can finish the exchange before the other one starts it. A name cannot be both a sync and a send/recv message.

An optional label for the point goes positionally — sync("push")[rolled back] — or as body, with size setting its text size and at forcing the side the label sits on. label instead labels the arrow itself; either end may carry it, and the first one given wins. displacement nudges this end off the shared column, which tilts the arrow away from whatever the vertical line would otherwise run through; it is a drawing offset and says nothing about the order.

#lamport-diagram(
  replicas: ("Client A", replica("Server", below), replica("Client B", below)),
  events: (
    "Client A": ([Edit], sync("first", label: "round trip"), idle(2), sync("third")[has both edits]),
    "Server": (sync("first"), sync("second"), idle(1), sync("third")),
    "Client B": (idle(1), [Edit], sync("second")[has both edits]),
  ),
)

idle

idle
idle(n)

Spacing to convey idle time passing: n columns of ordinary timeline with nothing drawn on them. The specific semantics are for the author to explain. The solver counts them, so the next event on this lane lands n columns later.

Usable bare or called, so idle, idle() and idle(2) are the same thing: two columns is enough for the stretch to read as a pause rather than as the ordinary spacing between two events.

gap is the sibling that shows the stretch, with dots, for time the diagram elides; idle shows nothing, because nothing happened.

gap

gap
gap(size)

Elided time: a stretch of dotted timeline standing for events the diagram does not show, taking one column of its own. The size is how much of that column the dots span — "small", "medium" (the default) or "large", or a length or a ratio of the column gap for an exact span, which past a full column runs into the neighbouring marks.

Usable bare or called, so gap, gap() and gap("medium") are the same thing.

above and below

These are top and bottom under names that read better for a diagram of one horizontal line per replica, and they are those same values, so either spelling works wherever a side is asked for.

left and right are re-exported alongside them, so one import line covers every side a diagram may ask for whichever way it runs. They are the built-in alignments of those names.

default-palette

The lane colours, cycled over replicas in order. Override per replica with replica("A", red).

overlays

Proposed, not implemented. An escape hatch for drawing arbitrary CeTZ into a diagram, in the diagram’s own coordinates, addressing the diagram’s own points, and at a chosen depth — a band behind a stretch of time, a ring around the event that went wrong, a note that breaks around the lanes the way its own arrow does. The design is on its own page, Overlays, and is still open to argument.

Dependencies

Drawing is done with CeTZ 0.5.2. The minimum Typst compiler is 0.14.0.