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(caption: none, replicas: (), events: (:), messages: (), orientation: horizontal, overlays: none, col-gap: none, row-gap: none, text-size: 0.62em, dot: 0.095, message-stroke: 0.9pt + luma(110))
Arguments:
  • orientation – The direction and sense of logical time. Defaults to horizontal.

  • caption – If provided, the result is a figure; without one it is the bare drawing, to place inside a figure of your own. Attach a <label> after the call and the reference resolves to the figure, numbering alongside the document’s others.

  • replicas – the lanes order: top to bottom on a horizontal diagram, left to right on a vertical one. Each entry is an id string, a replica – which also carries that lane’s point defaults – or a bare dictionary of the same fields.

  • events – each replica id mapped 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.

  • messages

    arbitrary arrows the diagram is to draw between two of its own points, as message items.

    Every other arrow the diagram works out for itself, from the send, recv and sync points on the lanes.

  • overlays – your own CeTZ, drawn into the diagram at a layer of your choosing. See Overlays.

The rest are the drawing’s measurements. Lengths without a unit are canvas centimeters: the canvas is laid out at length: 1cm, so 2.0 is two centimeters before the document scales anything.

The two gaps default to none, which means the value that suits this orientation rather than no gap at all. What a gap has to make room for is text, and text runs across the page whichever way the diagram does – so the wider default belongs to whichever axis is lying horizontally, and turning a diagram on its side turns the two over with it.

Orientation

col-gap

row-gap

horizontal, rightwards, leftwards

2.0

1.5

vertical, downwards, upwards

1.5

2.4

Arguments:
  • col-gap – the distance between two columns of logical time, and so how far apart the solver’s columns land. Along the page on a horizontal diagram, down it on a vertical one. A gap span given as a ratio is taken against it, as is the mark-displacement that nudges a point off its column – so widening the diagram widens those to match. label-displacement is the other one: a ratio there is taken against the label’s own extent, the label being what it moves.

  • row-gap – the distance between two lanes. Down the page on a horizontal diagram, across it on a vertical one – which is why its default is the larger of the two there: a label sitting beside a lane runs toward the next one.

  • text-size – the text size the diagram is drawn at, and what every em inside it resolves against. A label-size: 0.8em on a point is therefore eight tenths of this diagram’s em, not of the surrounding document’s, so a diagram keeps its proportions wherever it is placed.

  • dot – the radius of the mark on a local event. A recv and a sync are drawn at the same radius, a send at seven tenths of it, and each mark’s backdrop reaches a little past it. The dot inside a sync's ring is not measured against this one: it is a fraction of the thickness of the timeline it sits on.

  • message-stroke – the stroke every message and sync arrow is drawn with. Its paint also colors the arrowheads and any label carried by an arrow, so one value dresses the whole of the messaging. col-gap and row-gap are the two to reach for when a diagram reads too cramped or too sparse; dot and text-size are for when it is going somewhere much larger or much smaller than a page.

orientation (argument)

Which way logical time runs. It takes one of the six values below.

horizontal (value)

Alias of rightwards, and the default of lamport-diagram.

vertical (value)

Alias of downwards

rightwards (value)
leftwards (value)

The timelines run horizontally to the right or left.

downwards (value)
upwards (value)

The timelines run vertically.

Note

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

replica(name, ..args)

Define a lane, and the defaults the points on it fall back on.

Arguments:
  • label – what the diagram prints for the lane – content or a plain string, so the name of a lane can carry an icon or any other markup. Defaults to name.

  • color – the lane’s color. Defaults to the next entry of default-palette, cycled over replicas in order.

  • defaults

    a dictionary of defaults for the points on this lane, each still overridable point by point. Its keys are the point arguments they stand for – label-position, label-size, label-displacement and first-label-displacement – and nothing else, which is what keeps them clear of label. The lane’s label is the name the diagram prints for the lane; defaults.label-size is the text size of the points’ labels, not of that name.

    first-label-displacement is label-displacement for the lane’s opening point, the one that would otherwise crowd the replica name. Left alone it is the orientation’s own: 20% on a horizontal diagram, where the name sits immediately left of that first label, and 0% on a vertical one, where the name is before the lane in time and the labels are beside it, so there is nothing to move out of.

    Important

    label-position reaches this lane’s local events only. A send, a recv and a sync each have an arrow to stay clear of, and their label takes the side that clears it; their own label-position overrides that. The other three defaults reach all four kinds of point.

label, color and the label-position default 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. Content in a trailing block is the label, which is what lets a lane carry markup. defaults must be named.

event(..args)

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

All four kinds of point – event, send, recv and sync – take the arguments below and read them the same way. What tells them apart is the arrow: an event carries none, so it is the one that takes no message name and no message-label.

Arguments:
  • label-positionabove or below the timeline, or left or right on a vertical diagram; see orientation.

  • label-size – the label’s text size.

  • label-displacement – slides the label along the timeline, out of being centred on its own dot. A ratio is taken against the label’s own width, so +50% leaves the label’s left edge over the dot and -50% its right edge, while a length is an exact offset and 0 (or 0%) centres it. On a vertical diagram the ratio is taken against the label’s height instead, that being what runs along the timeline there.

  • mark-displacement

    slides the mark itself off the column the layout solved it into, in either direction. It is the one argument that moves the point rather than its label. A ratio is taken against col-gap, a length is an exact offset, and 0 (the default on every kind but recv) leaves the mark on its column.

    The nudge is a drawing offset the column solver knows nothing about. It reserves no room, so it never moves what follows on its lane, and 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 on that side, so a nudge large enough to push a bodiless mark left of where its lane starts will leave it overhanging the replica name.

  • label-width – wraps the label to a fixed width instead of letting it run along the timeline on one line, which is what keeps a long label from crowding its neighbors. Named only: a bare length is read as a label-displacement, that being the far commoner one to reach for. The box is centred on the mark like any other label, and its contents are left to you – wrap the body in align(center, ..) if centred lines read better than the ragged right edge.

  • label-padding – how far the label’s backdrop reaches past the label’s own box, which is what breaks an arrow crossing the lane so it does not crowd the glyphs. auto (the default) matches the reach of the disc under a mark, so a label and a dot break an arrow by the same amount; a length sets an exact reach, and none drops the backdrop, letting whatever is behind show through.

  • label-backdrop – what that backdrop is painted with. auto (the default) is white, which is what breaks whatever runs behind the label; a paint is used as given, so a label sitting in a wash an overlay laid down can be given that same wash and read as part of it rather than as a hole punched in it; and none leaves the backdrop unpainted, which is label-padding: none with the label’s box kept. A translucent paint hides no more than it says, so an arrow behind a washed label still shows through – and a translucent paint over a wash of its own color compounds with it into a slightly darker patch.

  • id – what an overlay names this point by, in place of the index it sits at on its lane. On a send, a recv or a sync it defaults to the message name.

label-position, label-displacement and the body may also be given positionally, in any order: they are told apart by type, so event(below, +50%)[AddFile1] and event(+50%, below, "AddFile1") are the same event, and so are send("push", below)[pushed] and send("push", "pushed", below). 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(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.

Both take every argument an event takes, and one more:

Arguments:
  • message-label – the text drawn on the message arrow itself, which keeps its own styling. Either end of the message may carry it, and the first (following the replicas list) one given wins. The point’s own label is its body, as it is on every other kind of point.

  • label-position – Overrides the position of the label. By default, the label is placed in the opposite side of the arrow.

  • mark-displacement

    Just like event's with different defaults:

    • on a recv it is 1cm – how far past its send in time the point lands whenever nothing on its own replica pushes it further, and enough to lean the arrow forward. recv(.., mark-displacement: 0) leaves it on its column, drawing an arrow straight across the lanes when the receiving replica has nothing else competing for that column.

    • on a send it is 0 – a send sits on its own column unless you say otherwise, since it is the receive that leans a message forward. Reach for it to tilt an arrow away from whatever a straight run across the lanes would otherwise cross, or to separate two sends the solver put in one column.

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 each end as a hollow mark with a dot of ink inside it, narrower than the timeline it sits on: neither side of an exchange is the sender, so neither is drawn smaller the way a send is, and the inner dot is what tells a sync’s end from a recv. 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.

It takes every argument an event takes, and message-label for the arrow itself, exactly as a send does – either end may carry it, and the first (following the replicas list) one given wins. mark-displacement nudges this end off the shared column, which tilts the arrow away from whatever a straight run across the lanes would otherwise cross; 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", message-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]),
  ),
)
message(from, to, ..args)

An arrow between two points the diagram already holds, and the note that goes beside it. Both ends go positionally, each a (replica, id-or-index) pair – the way a point is named everywhere else – and the body is the note:

  messages: message(("server", "http-request"), ("node", -1), luma(150))[
    server already\
    committed its DB
  ],
)

A message is drawn and nothing more. Both of its ends are points the diagram has already placed, so it moves no column and says nothing about order. It is for an arrow the diagram has no other way to hold – the reply a sync stands for, say.

Arguments:
  • color – tints the arrow, its head and the note together. Left alone they take the paint of message-stroke, which is what makes an arrow given here look like one the diagram worked out for itself. It may be given positionally.

  • position – which side of the middle of the arrow the note sits on: above, below, left or right. Left alone the note sits where the orientation puts it – after the middle on a horizontal diagram, under it on a vertical one – and this is what moves a note off whatever it landed on.

  • stroke – replaces the arrow’s stroke outright, for an arrow that is to read as an aside rather than as one of the diagram’s own. A dashed or dotted one says this arrow is a remark as plainly as anything can.

  • size – the note’s text size, 0.8em of the diagram’s own by default, since a note annotates a diagram rather than belonging to it. none takes the size the diagram is drawn at.

  • padding – how far the note keeps off the middle of the arrow, in canvas centimeters – 0.2 by default – or a length.

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(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 neighboring marks.

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

above (value)
below (value)
left (value)
right (value)

above and below 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 (value)

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

overlays (argument)

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. It has a page of its own: Overlays.