psi-slides · ::: draw

Figures You Write

A figure in a psi-slides lecture is a few lines of text in the lecture’s own Markdown file, and the build – the one command that turns that file into slides – draws it. The seven lines below are a whole figure, and the drawing is what the build made of them.

A figure is whatever the slide shows in place of a sentence – a photograph, a clip, or a drawing. ::: draw is the one of the three the build makes for you: mostly boxes and arrows, but also bar charts, plots and tables.

Development status: ::: draw is implemented in this repository, but is not part of a tagged psi-slides release yet – how to get it is a clone or the source zip. The graphical editor is experimental and desktop-oriented: it has automated coverage, but has not yet been tested broadly by people.

::: drawbox alice "Alice"box eve "Eve"  right of alice gap 1.7 {.accent}box bob "Bob"  right of eve gap 1.7edge alice -> eve "plaintext"edge eve -> bob "plaintext":::
plaintext plaintext Alice Eve Bob

Read the second line. Eve is put right of Alice – not at a coordinate, but against Alice. Bob is put against Eve. So the row is a chain of relations, and the only numbers in the figure are how much air to leave between two of them.

Nothing says where the arrows run, either. An edge is handed two ends and finds its own route between them. And nothing says where the first box is: the first element in a figure stands at the origin unless it is told otherwise.

That leaves nothing in this file to keep in step by hand. Give Eve a longer name and her box grows to hold it, and both arrows re-aim at the sides that moved. There is a live figure doing exactly that further down this page.

start here

One figure, a line at a time

One figure, built up in six stages. Each stage adds a single idea and shows the whole figure so far: the source, with the new lines marked, and the drawing that source produces. The rest of the page builds on these six.

Two boxes, and a name nobody sees

The first and last lines are the block itself: ::: draw opens one inside a lecture chunk, ::: closes it, and the figure is what stands between them. {unit=170x56} says how many pixels one grid unit is, across and down; leave it out and a default is used. Each of the lines between has the same four parts: what kind of thing this is, what it is called, what it says, and where it goes. Here box is the kind and "Client" is the label. cl is the name, and the name is never drawn – it is there so that later lines can point at this box. at 0,0 is the position, and it is the only position in the whole figure that names a number rather than another element. The first element in a figure may leave the placement out altogether – the one at the top of this page does – and it is written here to have something for the second line to be placed against.

Client Server
::: draw {unit=170x56}box cl "Client" at 0,0box sv "Server" right of cl gap 4.25:::

An edge names its two ends

The new line says which boxes are connected. It does not say where the line runs. The build works that out from where the boxes ended up, and works it out again whenever anything moves.

What it does say is direction: -> puts the arrowhead on the name written second. Three other tokens answer that question differently, and the reference has them.

Client Server
::: draw {unit=170x56}box cl "Client" at 0,0box sv "Server" right of cl gap 4.25edge cl -> sv:::

Positions refer to other positions

Only the first placed element needs at. Every other one names what it hangs off – right of, below, left of, above or between – and a gap, measured in the figure’s own units. That is the whole of where things go, and it is what keeps the figure in step with itself: move the element being hung off, and everything hanging off it follows. Naming an element written further down is allowed, and an arrangement that refers back to itself is refused with the line that caused it.

Client Server Log
::: draw {unit=170x56}box cl "Client" at 0,0box sv "Server" right of cl gap 4.25edge cl -> svbox log "Log" below sv gap 0.9edge sv -> log:::

The tail says how it looks

Anything in braces at the end of a line describes the element rather than placing it. A word beginning with a dot is a class, and a class changes how the element is drawn: .tone-1, .tone-2 and .tone-3 here are fills, and there are four. That is the whole of the tail as this figure uses it. There are around forty classes in all, and the table under the whole vocabulary is every one of them – you need none of the rest to draw this.

Client Server Log
::: draw {unit=170x56}box cl "Client" at 0,0 {.tone-2}box sv "Server" right of cl gap 4.25 {.tone-1}edge cl -> svbox log "Log" below sv gap 0.9 {.tone-3}edge sv -> log:::

Words that are not in a box

A text is a label with no outline around it. The -- sv at the end draws a leader: a thin line from the note to the thing it describes. Use it rather than an arrow for a note: an arrow claims two things are connected, and a reader who sees one goes looking for the connection, where a leader only says this note is about that.

Client Server Log TLS ends here
::: draw {unit=170x56}box cl "Client" at 0,0 {.tone-2}box sv "Server" right of cl gap 4.25 {.tone-1}edge cl -> svbox log "Log" below sv gap 0.9 {.tone-3}edge sv -> logtext n "TLS ends here" right of sv gap 3.65 -- sv {.small .muted}:::

A boundary around part of the figure

A container outlines the members you name and re-fits itself when they move. It also takes its visibility from them, so you never have to reveal it separately.

DMZ Client Server Log TLS ends here
::: draw {unit=170x56}box cl "Client" at 0,0 {.tone-2}box sv "Server" right of cl gap 4.25 {.tone-1}edge cl -> svbox log "Log" below sv gap 0.9 {.tone-3}edge sv -> logtext n "TLS ends here" right of sv gap 3.65 -- sv {.small .muted}container dmz "DMZ" over sv,log pad 0.4 {.dashed .muted}:::

How a line is put together

Every statement that places one element of its own has the shape below – box, dot, text, image – and so does every statement that draws a chart, a table, a swimlane or a protocol. Not every slot is filled every time – most lines use three or four of them – but they always come in this order.

box   sw   "Switch"   right of a gap 1.15   w 1.2   {.tone-1 @net}
___   __   ________   ___________________   _____   ______________
 |    |       |                |              |           |
 |    |       |                |              |           '-- tail: classes and tags
 |    |       |                |              '-- options: size, padding, routing
 |    |       |                '-- placement: where it goes, relative to something else
 |    |       '-- label: what the reader sees. Optional, and "" is a legal empty one
 |    '-- name: how later lines refer to this element. Never drawn
 '-- statement: what kind of thing this is

For a statement that has a placement slot, that slot is compulsory, and the only element allowed to leave it out is the first one, which stands at the origin. Any later one without a placement is an error that names the six words that would fix it: at, above, below, right of, left of, between. The remaining statements have no placement slot at all, because they put what they are about in its stead: an edge writes its two ends there, a container and a brace the members they hold, align and spread an axis and a list, default a kind, and step the name of a beat.

What the shape above does not tell you

Two rulers, both in the figure’s own units

The block opens with ::: draw {unit=170x56}, which sets how many pixels one unit is across and down, and one unit is meant to be about one box wide. Every number after that is in units, so a figure keeps its proportions whatever size it is drawn at – but the two are not one ruler. A number that addresses the grid takes the axis it is written on: w counts unit widths, h unit heights, and so do at, offset and a nudge on a coordinate. A number that states a clearance is square, and its ruler is the row: gap, pad, space and a dot’s r draw the same distance whichever way they point.

Three different marks share one pair of braces

In the tail, .something adds a class, !something takes one off again, and @something is a tag that puts the element in a group. There is no mark for a name: every statement that can be referred to later names itself in its own words, a box and an edge alike.

A coordinate can be another element’s coordinate

Written sw.right, sw.cx, sw.top and so on, optionally with a nudge: sw.right+0.4. A pair separated by a comma is a point: sw.right+0.4,sw.cy. This is what keeps a route attached to what it was drawn against.

A fraction slides an endpoint along a side

eve.right:0.3 is three tenths of the way down Eve’s right-hand side. Two edges between the same pair at :0.3 and :0.7 run parallel instead of on top of each other.

gap and space are not the same word

gap is the distance to the element you are placing against. space is the distance between the bars of a chart, the cells of a grid, or two bands of a protocol.

same as copies the size, not the position

box bob "Bob" right of eve gap 6.35 same as eve places Bob relative to Eve and gives him Eve’s width and height. Leaving it out is how two parallel edges end up slightly skewed, because a fraction of two different heights lands at two different places.

Some lines take lines of their own

Three statements read the lines written under them rather than ending at their own: step, which opens a beat, and table and sequence, which read their rows and their actors. Indent what belongs to them. None of the three is needed for a figure that stands still, and the next section is where beats begin.

A line starting with # is a comment

Anywhere inside the block. Use them for the reasoning behind an arrangement, which the arrangement itself does not record.

That is the everyday vocabulary, and it is enough to write a figure that stands still. To see one, put the block in a ## figure: chunk of a lecture and build it – node build.js <your-lecture>/source.md – then open the audience.html written beside the source. Next is the one thing left that changes the figure over time rather than how it looks: making it move, a beat at a time. After that the page turns to reference – the full class list, then fifteen rules for a figure a room can read at a glance.

steps and tags

How a figure moves from one beat to the next

Take the figure you have just built and give it beats. A beat is one press of the key that advances the slide, in front of the room. A step line names one, the operations under it say what changes, and the figure is then laid out again from the top with those changes in force – which is why an arrow stays attached to a box that walks away: the arrow never held a coordinate, it held “the right-hand edge of eve”, and that is still what it holds after eve has moved. The two figures below are longer than the one at the top of the page, and both do the same three things: bringing elements in on the beat they belong to, taking the room’s attention from one part of the drawing to another, and moving something and letting everything attached to it follow.

Both are live, and they run exactly the code a projected lecture runs. Step forward and back with the arrows, or press play.

Three states are worth holding in mind at once, because a figure with beats is three drawings at the same time: the one the block opens with, the one the room sees at beat k, and the one that comes out of the printer.

what you wrotethe opening pictureduring the lecturein print
show / hidehidden from the start if the first thing any beat says about it is show; visible otherwisecumulative – every beat up to here has happenedthe last beat
an edge, a container, a leaderas visible as what it joins, holds or points atre-derived at every beatthe last beat
move, labelas written on the element’s own linecumulativethe last beat
fill, ink, stroke and the rest of the lookas written on the element’s own linecumulativethe last beat
.emph .dim .ghostas written on the element’s own linecumulativethe opening picture, not the last beat
the drawable, its type size, its label anchor, .smooth, .frontas writtencannot be changed by a beatas written

Two rows in that table need a second reading. An element goes missing from the opening picture when a beat further down says show about it, so there is no second list of what begins hidden. Write hide first if you meant it on screen from the beginning and gone later. And prominence is the one thing print takes from the top of the block rather than the bottom: emphasis written on an element’s own line is part of the drawing and belongs in a handout, while emphasis a step reaches for is an act performed in the room. So box a "A" {.emph} prints emphasised and emph a inside a beat does not, although on screen they are the same act.

One line changes, and four things follow

The same three boxes as at the top of the page, with two beats added: Eve arrives, then Eve is renamed. Step through it and watch what the second beat moves without mentioning.

Eve's reach message Alice Bob EveEve, on path
::: draw {unit=170x56}box  alice  "Alice"box  bob  "Bob"   right of alice gap 10.3box  eve  "Eve"   between alice,bob offset 0,1.5 {.accent}edge direct alice -> bob "message"edge alice -> eve {.accent}edge eve -> bob {.accent}container zone "Eve's reach" over eve pad 0.45 {.dashed .muted}step intercept  show eve  hide directstep relabel  label eve "Eve, on path"  emph eve:::

The block has two halves: above, everything the figure contains; below, what the beats do. The word step is the boundary – the blank line is there for your eye, and the parser does not read it. Writing the cast first and the beats under it is the shape to keep, all the same: it is the order the figure is read in. The first beat is two lines and four changes: Eve appears, and so do her two arrows and the outline around her, because an arrow is only as visible as the things it joins and an outline only as visible as what it holds. No line anywhere says show about the arrows or the outline.

The second beat is one line: label e gives Eve a longer name; her box grows from 54 pixels wide to 116 to hold it; the outline re-fits from 104 to 167; and each arrow re-aims by 31 pixels, one to the left and one to the right. The beat names the label and nothing else. In a drawing program the same edit is one text box and four corrections by hand, repeated for every beat the figure has.

The five families of operation

That is one beat. A step line names it and the operations that make it up follow, indented. Indent them: it is how every figure on this page is written and how a beat is read. Like the blank line above the first step, though, the indentation is for your eye – what ends one beat is the next step, any line that opens with a statement, or the end of the block. They come in five families: visibility (show, hide), movement (move), labelling (label), prominence (emph, dim, ghost) and styling (style). The prominence verbs are the prominence classes, so learning one teaches the other, and a beat returns an element to normal with style e {!dim} rather than with a fourth word.

Four beats, and one line that reveals three things

The four-beat order set out lower down the page: the stage, the normal case, the disturbance, the consequence. Step through it.

A Switch B who has 10.1.1.5? 10.1.1.5 is at bb:bb E 10.1.1.5 is at ee:ee
::: draw {unit=160x54}# The disturbance is one line, `show @attack`. It can be, because an element# starts hidden exactly when the first thing any step says about it is# `show` - so every element is written at the top of the block and simply# appears on the beat that names it.box  a  "A"      at 0,0 {.tone-2}box  sw "Switch" right of a gap 3.55 {.tone-1}box  b  "B"      right of sw gap 3.55 {.tone-2}edge a -- sw {.muted}edge sw -- b {.muted}text q "who has 10.1.1.5?" above sw gap 0.7 pad 0.1 {.small .paper @ask}text r "10.1.1.5 is at bb:bb" below b gap 0.6 pad 0.1 {.small .paper .dim @answer}box  e  "E" below sw gap 1.15 {.accent @attack}edge e.top -- sw.bottom {.accent @attack}text s "10.1.1.5 is at ee:ee" right of e gap 1.8 pad 0.1 -- e {.small .paper .accent @attack}step ask  show @ask  emph astep answer  show @answer  emph b  dim a  # Full prominence for exactly the beats where this is the right answer.  # Print takes prominence from the opening beat, where `r` carries .dim, so  # the handout shows the true reply already superseded - which is the whole  # argument of this figure. The live reading is unchanged: `r` is loud while  # it is correct and quiet again once the spoof has replaced it.  style r {!dim}step spoof  show @attack  emph e  dim bstep redirected  style r {.dim}  emph e, s:::

The third beat is one line, show @attack. It reveals three things – E, the wire to the switch, and the forged answer – because all three carry @attack in their attribute tail. To bring a fourth element into the group you write the tag on that element; there is no line anywhere else in the file to change. The wire carries no show of its own: it arrives with E, because an edge is only ever as visible as the two things it joins. Two of the beats end with a dim, which takes the emphasis off whatever the previous beat emphasised and quiets it.

One element there carries a class that a beat has to take off and put back. The true reply r is written {.dim} on its own line, because the picture a handout should show is the one in which the forged answer has already displaced it. But for the two beats in the middle the true reply is the answer and has to read as one, so the answer beat lifts the class with style r {!dim} and the closing beat writes it back. Neither half of that figure can be had without the other: {.dim} on the line alone gives the right handout and a middle in which the correct answer is already whispering, and no class on the line gives the right middle and a handout where the superseded reply and the forgery are equally loud. This is what the removal mark is for – not for undoing a mistake, but for a class that is true of a drawing and not of every beat of it.

Nothing in that source says an element starts hidden, and one sentence decides which do: an element is hidden at the opening beat exactly when the first thing any step says about it is show. That is what lets the whole figure be written at the top of the block, in whatever order reads best, with each part simply arriving when its beat comes. The rule runs the other way round as well, which is what makes it safe to rely on: an element whose first mention is hide is on screen from the start and goes away later, and one no step mentions at all is on screen throughout.

A box that walks away, and everything that follows it

One beat, one move statement. Everything else in the figure follows on its own.

client side direct Client Laptop Server Proxy
::: draw {unit=160x54}# Nothing here stores a coordinate: the arrows were written as "from cl" and# "to sv" and the outline as "over @client", so both beats move a drawing# rather than a number. The verbs differ in what they can address - "to"# names one position, which a set cannot take without stacking on it; "by"# is a displacement, the same sentence for one element or four. Both client# machines are placed absolutely, or the laptop would take the move twice.box cl  "Client" at 0,0 {.tone-2 @client}box lap "Laptop" at 0,-1.05 same as cl {.tone-2 @client}box sv  "Server" right of cl gap 8.9 {.tone-1}box px  "Proxy"  below cl gap 1.0 offset 1.0,0 {.tone-4}edge direct cl -> sv "direct" {.dashed .muted}edge cl -> px {.muted}edge px -> sv {.muted}container zone "client side" over @client pad 0.36 {.dashed .muted}step interpose  hide direct  move px to between cl,sv  emph pxstep withdraw  move @client by -0.55,0:::

The two arrows were written as “from cl” and “to sv”, and the outline as “over cl,px”. So when the proxy moves onto the path, the arrows follow it and the outline re-fits around a row instead of a column. hide direct takes the bypassed arrow away in the same beat. Without it, that arrow stays on the drawing and its label is left sitting across the proxy that has moved into its path.

move takes to for one element and by for a set, and the two words are not interchangeable. to names a destination – a point, or a relation like between cl,sv – so handing it a tag would give every member of the set the same destination and pile the whole group onto one spot. The build refuses move @tag to … and names the word that works. by is a displacement instead, dx,dy in the figure’s own units, and the same displacement applied to five elements moves all five without changing the arrangement they were in. The second beat above is one line of it.

What print gets is the last beat, carrying the prominence each element opened with – not the union of all the beats. Everything shown and never hidden is in the last beat anyway, so the two readings differ only where the author wrote hide, and there the last beat is the one that means “the finished picture”. The prominence is the one thing taken from the opening beat instead, and that is the whole of the rule: what a step lent an element is a moment in the talk and does not belong in a handout, while a {.dim} written on the element’s own line is a statement about the drawing and does. That is also what a reader with no JavaScript sees: the drawing as written into the file already stands at its last beat.

A figure is written with two frames, and which one you see depends on whether you are reading it on paper or on screen. Paper gets one drawn tight around the finished picture, because a printed figure has exactly one beat and any paper beyond it is blank. A screen cannot use that frame: an element that walks in from outside the picture has to have somewhere to walk in from, and a frame sized to where everything ends up would clip it for the whole of its journey. So the file carries both. The tight frame is the one written into the drawing itself, which is what print uses and what a reader with no JavaScript sees; the wider one is held beside it and swapped in by the page’s own script the moment it loads.

editing by hand

Dragging the figure, without losing what holds it

The live views carry a graphical editor for these blocks, and it edits the source text: it parses the block, answers a gesture by rewriting the smallest span of source it can, and re-runs the same compiler the build runs. Nothing it writes is anything you could not have typed. It is experimental and made for a desktop-sized authoring screen; why a drag is safe here at all is argued on the page next door.

The diagram editor open over a lecture slide: a dark canvas holding a CBC decryption figure with one box selected, the relations that place it written on the canvas beside it, a panel on the right describing the beat that is standing, a rail of the figure’s beats along the bottom, and a strip of the lecture’s other figures under that.
The editor, opened on the CBC figure from lectures/diagrams at its last beat, with the box c1 selected. The words on the canvas beside it – gap 1.15, flush middle, align y middle – are the relations that place it. Click the picture for the pixels that were taken.

Opening it

Click a figure in audience.html or speaker.html and press E, or use the button in the corner of the card the click opens. It opens at the beat that is on screen, and it leaves the slide where it was: its own beat navigation is the editor’s business and writes nothing back to the lecture.

A lecture with a diagram carries the editor by default, in both live views. The frontmatter decides who gets it: editor: speaker keeps it out of the projection, and editor: none ships the lecture without it, which is worth doing for a deck you are handing on: the editor is the larger half of what a figure costs in the built file.

What a gesture writes

A drag rewrites one token and leaves the rest of the line alone. Moving a box that was placed right of a gap 1.1 writes gap 1.6; sliding where an arrow meets a box writes the fraction along that side; moving a waypoint written against another element writes the nudge on it, and the reference it is a nudge from survives. A drag that replaced the relation with a plain coordinate would give you the same picture today and a figure that follows nothing tomorrow.

Three things a gesture does that are worth knowing before you meet them. Holding Ctrl or Cmd suspends snapping, so a position can be set between the guides. Holding Alt takes the element out of an align or spread set instead of moving the whole set with it. And dragging one element over another raises four chips around it – releasing on one rewrites the placement to above, below, left of or right of that element, keeping the gap it already had.

At any beat above the opening one, a drag writes a move into that step rather than changing where the element is placed, and the same holds for the look rows in the panel: a swatch writes a style op into the step you are standing on. Which beat you are on is therefore part of what a gesture means, and the pane at the top of the panel says which one that is and what it already does.

An edit that stops the block compiling is not kept. It is rolled back on the spot and the compiler’s own sentence about it is shown instead.

Where the edit goes

The button in the top right names the tier it will use before you press it. Build the lecture with --watch and it reads Write to source.md: the block goes back into the file through the watch server, the rebuild that follows reloads every open tab, and you land back on the figure you were working on. It is the only tier that closes the loop.

Without a watch server the button reads Copy source and puts the whole ::: draw block on the clipboard to paste over the old one. Open source.md… is the third way, where the browser has the file picker for it: pick the file once and the button writes into it directly. Either way the edit is also kept in the browser, so a reload does not lose it.

The watch server is careful about what it accepts: it binds to 127.0.0.1, and it refuses a patch whose byte range is not a ::: draw block, or where the bytes on disk are no longer the ones that block compiled from – which is what stops a second tab quietly overwriting the first one’s work.

the whole vocabulary, for looking up

The classes, and the words that go with them

The whole list, for looking up. You do not need it to write a figure – the six stages above use only the classes that make a specific visual distinction, and most figures never reach past those. Come back when you want a shape or a weight you have not met.

Most classes belong to a group, one group per thing they set, and only one class from a group may be written on an element: a second from the same group is refused when the figure is built, so two fills can never both take effect. The three below the rule belong to no group, so nothing displaces them – though combining is not always useful, and the row for .turn says where. Every class also states which kinds of element it acts on, and one written where it can do nothing is refused too: a hexagon on an edge, an alignment on a brace. A class is taken off again with ! in place of the dot, which is how an element opts out of something a default line handed it. Anything outside the vocabulary is refused when the figure is built.

groupclasseswhere it actsin a step?what it sets
fill.tone-1 .tone-2 .tone-3 .tone-4 .clear .paperbox dot text container edgeyesWhat the shape is filled with. .paper is the page background, which is what makes a label punch a hole in a line behind it; .clear is no fill at all.
ink.accent .mutedbox dot text container brace edgeyesWhich colour the outline and the words take. .accent is the one strong colour on the page; .muted is lighter and thinner, for supporting apparatus.
stroke pattern.dashed .dottedbox dot container brace edgeyesA broken outline. Conventionally a boundary you can cross, or a thing that is not really there.
stroke weight.thick .bare.thick on box dot container brace edge; .bare on box dot containeryesA heavier outline, or none at all. .bare leaves the fill and the label with no border round them – which an edge and a brace, being a line already, have nothing to take off.
outline.round .sharp .hex .diamond .chevron .wedge .cross.round .sharp on box text container edge; the other five on a boxnoThe shape drawn: a rectangle with rounded or square corners, a hexagon, a diamond, a chevron, a triangular wedge or a cross. They share a group because a hexagon has no corner radius to argue about. A bare .cross comes out square, because a plus with arms of two different lengths is not a plus.
size.small .largebox dot text container brace edgenoThe label one step down or up from the figure’s normal size.
family.mono .serif .handbox dot text container brace edgeyesTypeface for the label: fixed-width for addresses and code, the reading face, or that face in italic for something written in beside the drawing.
fitting.fit .shrinkbox textnoWhat to do when the label does not match the box. .fit scales the type to a width you gave, within about 0.6 to 1.5 times normal; .shrink only ever shrinks. Both need a width.
arrowheads.no-head .one-head .both-headsedgeyesWhich ends carry a head. You do not normally write one: the arrow token between the two ends decides it, and a head class in a tail is refused. They exist for the one thing a token cannot do, which is change part-way through a figure, so a style step is the only place one belongs.
line shape.smooth .elbowedgeonly .elbowThe waypoints you wrote drawn as a curve through them, or a right-angled route the edge works out itself. One or the other. Neither touches the arrowheads: a bent line carries whatever its token asked for.
label across.left .rightbox dot textnoPush the label to one side of the space its element gives it.
label down.top .bottombox dot textnoThe same on the other axis. Which side of the line an edge label sits on is a different question with a word of its own, side, so none of these four is accepted there.
prominence.emph .dim .ghostevery elementyesHow much of the room’s attention an element is asking for, one of the three at a time. .emph is the one a beat is about; .dim is a third of full strength and .ghost roughly half, two depths of the same retreat. All three reach every kind and displace each other, so on a picture, which has no ink to thicken, .emph is what takes a .dim off again. A step reaches for all three by these same names, and {!dim} puts an element back to normal.
The three below belong to no group, so nothing displaces them – though combining is not always useful, and the row for .turn says where.
edge order.frontedgenoAn edge drawn above the boxes instead of behind them. Right for an axis, which is otherwise hidden by the columns it rules; wrong for an arrow, which a box should cover as it arrives.
label direction.turnbox dot text container brace edgeyesThe label reads bottom to top, for a tall element with room for a word only along its long side. It is centred on its origin whichever way it reads, so on a node label .left and .right have nothing left to align and the build warns about the pair. .top and .bottom do still move one.
label weight.boldbox dot text container brace edgeyesThe label in a heavier weight, for the one element a figure is about.

Every class above may be written on the element’s own line. Not every one of them may be changed later by a step. A beat can repaint an element, restroke it, turn its label, reroute an elbow, alter its arrowheads and move it up or down the order of the room’s attention – that is the whole of what a beat is for. It cannot change what is drawn: the drawable, its typeset text and its place in the document are settled once, when the figure is built, and shared by every beat. The classes that decide those are fixed for the whole figure, and writing one inside a step is refused rather than ignored – in either sign, because taking such a class off is as impossible as putting it on. They are, by what each one settles: the drawable kind (.round .sharp .hex .diamond .chevron .wedge .cross), the label anchor (.left .right .top .bottom), the type size (.small .large .fit .shrink), the path kind (.smooth) and the drawing order (.front).

The rest of the tail, and the words beside it

Classes are one half of what a line can carry. The other half is the keyed options – how big a thing is, where exactly it sits, how an edge gets from one end to the other. Each box below is one question and the words that answer it.

Size

  • wbox sw "Switch" at 0,0 w 1.2
  • hbox fw "Firewall" at 0,0 h 1.5
  • rdot j at sw.right,sw.cy r 0.12
  • same asbox gw "GW" right of sw same as sw

A width and a height in the figure’s own units, a radius for a dot, or the size of another element copied wholesale. Leave them out and a box is as big as its label needs.

Nudging a placement

  • offsetbox b "B" right of a offset 0,-0.3
  • flushbox b "B" below a flush left

After right of a gap 1.15 you can add offset 0.2,0 to shift the result, or flush left to keep the new element’s left edge level with the one it was placed against. It takes one word, and the words are the ones the placement leaves free: left, middle or right under an above or a below, top, middle or bottom beside a right of or a left of.

Lining several elements up

  • alignalign y middle a, b, c
  • spreadspread x a, b, c, d

Written on a line of their own, these are statements rather than placements: align y middle a, b, c gives b and c the vertical centre of a, and spread distributes a set evenly. The first element named is the one the others follow. align names its axis first, and the edge words follow the axis: left, middle, right on x; top, middle, bottom on y.

Which end carries the arrowhead

  • ->edge cl -> sv
  • <-edge cl <- sv
  • --edge cl -- sv
  • <->edge cl <-> sv

Four tokens for four answers, and the token is the whole of it. -> puts a head on the name written second and <- one on the name written first; -- puts none, for a link that is not a direction; <-> puts one at each end. Routing changes none of it – edge a -> b via 1,1 {.smooth} arrives at b with one head, exactly as the straight line would. A message inside a sequence takes the same four and means the same by them.

Routing an edge

  • viaedge a -> b via a.right+0.4,a.cy
  • .elbowedge rt -- ca {.elbow}

edge a -> b via a.right+0.4,a.cy sends the line through the points you list before it arrives. A point can be written as another element’s coordinate plus a nudge, so the route survives the figure being rearranged. .elbow is the common case said in one word: a right-angled route with its turn halfway across the gap, which is the two waypoints every tree connector was written with by hand. It takes no options, and writing it alongside via is an error rather than a preference.

Saying what shape a chart is

  • aspectplot p "" "" at 0,0 w 1.9 aspect 1:1

w and h count grid cells, and a grid cell is not square, so they are a poor way to say what proportion something will be on the page. aspect 4:3, aspect 1:1 or one bare number says the proportion the room sees, on a bars or a plot, and the build works the other dimension out. Writing all three is an error.

Setting a base for everything

  • defaultdefault box {.tone-2} w 1.1

default box {.tone-2} w 1.1 at the top of a block applies to every box in it, and an element’s own tail overrides it. The same lines in a draw-defaults: key in the lecture frontmatter set the house style for every figure in the lecture.

beyond the basics

More things you can write

None of these is a new kind of element, so all of them move between beats like the boxes and edges above. Five outlines that are not rectangles, a label that reads bottom to top, a bar chart, the same chart turned on its side, a second series in it, a column singled out from the opening beat, a plot with a curve through it, two ways of saying what shape a chart is, three frames matched to the pixel, one drawing reused many times over, a statement that evens out spacing, the cycle that statement can create, and type that scales to its box.

Each pair below is real: the drawing was produced by the source shown with it.

Outlines that are not rectangles, two of which can be aimed

A protocol message that is an arrow, a sensor that is a hexagon, a decision that is a diamond, a size comparison that is a triangle, a scatter marker. They share one class group with .round and .sharp, and a group holds one class at a time, so two outlines on one box is a line the build refuses.

.diamond eats both axes at once: the widest room a diamond offers is a strip half its width by half its height through the middle, so the box is sized at twice what the same label would need in a rectangle. Give it short labels.

Which way a point aims is set by the point option – up, down, left or right – so the two pointed outlines cover eight orientations between them. Writing point on an outline that has no point is an error.

SYN seq=c SYN+ACK ack=c+1 IDS ?
box a "SYN seq=c"       at 0,0 {.chevron .tone-3}box b "SYN+ACK ack=c+1" below a point left {.chevron}box c "IDS"             right of a gap 0.7 {.hex .tone-1}box d ""                below c {.wedge .tone-4}box e ""                right of d gap 0.7 point up {.wedge}box f ""                right of e gap 0.7 {.cross .accent}box g "?"               right of f gap 0.7 {.diamond .tone-2}

A label that reads bottom to top

A tall narrow element has room for a word only along its long side. A turned label moves between beats exactly as an upright one does.

FIREWALL SWITCH True Positive Rate
box  fw "FIREWALL" at 0,0 h 1.5 {.tone-4 .turn}box  sw "SWITCH"   right of fw gap 2edge fw -> sw.lefttext ax "True Positive Rate" left of fw gap 0.7 {.turn}

A chart that is still boxes

bars turns into ordinary boxes named f-0, f-1, f-2, so a brace spans three of its columns with the syntax it would use on any three boxes.

The baseline carries .front. Drawing order puts edges before boxes, which is right for an arrow – a box should cover the line arriving at it – and wrong for an axis, which would otherwise be hidden behind the columns.

the top three the next five . i e 0 l o 1 / a 3 5 M
bars f "20,19,17,12,11,10,9,9,8,7,6,5" ". i e 0 l o 1 / a 3 5 M" at 0,0 w 2.2 aspect 4:1 {.tone-3 .bare}brace b1 over f-0,f-1,f-2 side bottom "the top three"brace b2 over f-3,f-4,f-5,f-6,f-7 side bottom "the next five"

The same chart, turned on its side

A reader ranks lengths from a shared left edge more reliably than heights from a shared floor, so horizontal on a bars line is usually the better of the two. The categories stack downwards, the strip of category labels moves to the left margin as a column of words with their right edges lined up, and the baseline stands on the left.

It also gives a category called “DNS cache poisoning” somewhere to go, which it has not under an upright column. A quoted list of category names is split on | whenever it holds one, instead of on spaces, so a row label may be a phrase – the same mark that separates a table row and a lanes name. Everything else is unchanged: a second series still groups or stacks, a brace still spans three bars, and the columns are still named after the chart – al-0, al-1, al-2.

SSH brute force Port scan DNS tunnelling attempt SMB exploit attempt TLS certificate mismatch one automated attack accountsfor nearly half the week IDS alerts by signature class, one campus network, one week
bars al "412,268,91,57,24" "SSH brute force | Port scan | DNS tunnelling attempt | SMB exploit attempt | TLS certificate mismatch" at 0,0 horizontal w 3.0 aspect 5:2 emph 0 {.tone-3}text n "one automated attack accounts\nfor nearly half the week" right of al gap 1.45 -- al-0 {.small .muted .left}text ax "IDS alerts by signature class, one campus network, one week" below al gap 0.5 {.small .muted}

A second series in the same chart

A second bars line joins the first chart’s frame instead of drawing one of its own: series of <chart>, and it borrows the frame, the category labels, the baseline and the scale. It brings its own values, its own classes and its own tags: a series carries its own attribute tail, so it can have its own colour and its own name.

Without stacked the series stands beside the ones before it and the cell is shared out between them, so a grouped chart takes exactly the paper a single one did. With stacked it sits on top of the run before it instead, and the scale becomes the tallest stack. A series takes no w, no h, no space, no placement and no category names of its own: all five belong to the chart it joined, and saying otherwise is an error rather than a number the drawing ignores.

info low medium high reviewed by hand scanner false positive
bars man "12,18,9,4" "info low medium high" at 0,0 w 2.9 h 1.3 {.tone-1}bars scan "26,31,14,5" series of man {.tone-3}bars fp "19,22,7,1" series of man stacked {.tone-4}box  s1 "" at man.left+0.14,man.bottom+0.78 w 0.2 h 0.55 {.tone-1 .sharp}text n1 "reviewed by hand" right of s1 gap 0.35 {.small .muted}box  s2 "" right of n1 gap 0.85 same as s1 {.tone-3 .sharp}text n2 "scanner" right of s2 gap 0.35 {.small .muted}box  s3 "" right of n2 gap 0.85 same as s1 {.tone-4 .sharp}text n3 "false positive" right of s3 gap 0.35 {.small .muted}

One column singled out, from the opening beat

emph, dim and ghost on a bars line take a list of column numbers. They are the same three words as the prominence classes and the prominence verbs, and they single a column out in the picture the figure opens with, which is where a chart usually wants one.

Written on the statement the emphasis reaches a printed handout. The same word inside a step is a lecture-time act and the handout does not show it.

80 443 22 53 25 one port carries morethan the other four together blocked connection attempts, by destination port, one week
bars port "12,9,41,7,5" "80 443 22 53 25" at 0,0 w 2.4 h 1.2 emph 2 dim 0,1,3,4 {.tone-3}text n "one port carries more\nthan the other four together" right of port gap 1.6 -- port-2 {.small .muted .left}text ax "blocked connection attempts, by destination port, one week" below port gap 0.55 {.small .muted}

A frame to draw in, and a line through its points

A plot is a frame with two named axes: x 0,1 and y 0,1 are the ranges it covers and tick 0.2 the interval its gridlines and its numbers fall on. After that roc@0.35 names a value in the plot’s own units – 0.35 along its axis, not a distance on the page – and a plot coordinate can be written anywhere a position is allowed. .smooth draws a curve that passes through every waypoint, so switching it on never moves a line off the points you attached it to.

A marker has to be given a marker’s size. A bare dot has a radius of 0.18 grid units, so it is 0.36 across, and an empty .cross box is square at one line of type plus its padding – both right for a junction in a topology, both far too heavy for a point inside a plot frame. The two markers below carry an explicit r and an explicit w and h for that reason.

0 0.2 0.4 0.6 0.8 1 0 0.2 0.4 0.6 0.8 1 False positive rate True positive rate
plot roc "False positive rate" "True positive rate" at 0,0 w 1.9 aspect 1:1 x 0,1 y 0,1 tick 0.2edge roc@0,roc@0 -> roc@1,roc@1 {.muted .dashed}edge roc@0,roc@0 -> roc@1,roc@1 via roc@0.03,roc@0.45 roc@0.1,roc@0.72 roc@0.3,roc@0.9 {.smooth .accent}dot  m1 at roc@0.1,roc@0.72 r 0.1 {.accent}box  m2 "" at roc@0.35,roc@0.95 w 0.09 h 0.26 {.cross .tone-3}

What shape a chart is, said the two ways

What shape a chart is comes from aspect, not from w and h. Those two are counts of grid cells, and a grid cell is not square: written w 1.9 h 1.5 in a block whose unit is 150 by 52, this frame arrived 285 pixels wide and 78 tall, which is nothing like what those two numbers look like. aspect 1:1 says the proportion the room sees and lets the build work the other number out; a ROC curve wants it, because both axes are the same quantity and the diagonal has to read as 45°. Giving w, h and aspect together is an error.

0 0.5 1 0 0.5 1 w 1.9 h 1.5 0 0.5 1 0 0.5 1 w 1.9 aspect 1:1
plot bad "" "" at 0,0 w 1.9 h 1.5 x 0,1 y 0,1 tick 0.5text nb "w 1.9  h 1.5" below bad gap 0.4 {.small .muted}plot good "" "" right of bad gap 2.9 flush bottom w 1.9 aspect 1:1 x 0,1 y 0,1 tick 0.5text ng "w 1.9  aspect 1:1" below good gap 0.4 {.small .muted}

Three frames that match to the pixel

A row of charts meant to be compared takes its size from the first of them. same as <chart> on a plot, or on a bars line that draws a chart of its own, copies the whole frame, so the frames match to the pixel and a reader can lay one over another. Three frames that differ by a hair cannot be read against one another at all.

The chart being copied has to stand above the one copying it, a chart being sized as its own line is read. Written the other way round, the build stops and says which of three things went wrong: the chart named is further down, the name belongs to something that is not a chart, or there is no such name in the block. same as beside w, h or aspect is refused as well, and so is same as on a series of line, which draws in a frame it does not own.

M T W T week 1 M T W T week 2 M T W T week 3
bars w1 "18,24,31,9" "M T W T" at 0,0 w 1.5 aspect 3:2 {.tone-3}text c1 "week 1" below w1 gap 0.35 {.small .muted}bars w2 "22,19,31,14" "M T W T" right of w1 gap 1.75 same as w1 {.tone-3}text c2 "week 2" below w2 gap 0.35 {.small .muted}bars w3 "9,12,7,31" "M T W T" right of w2 gap 1.75 same as w1 {.tone-3}text c3 "week 3" below w3 gap 0.35 {.small .muted}

One drawing, however often it appears

An image statement places a drawing from an SVG file, and a grid repeats it – twenty-four copies here, from one file. However often a drawing appears, the file is embedded once: the last figure in the gallery below draws ninety-six faces out of two files. The exception is a file carrying its own stylesheet, which has to be embedded once per copy, because that stylesheet does not reach into a shared one and the drawing would arrive with no lines.

one file, twenty-four times
grid g image face-ok 8x3 at 0,0 cell 0.26 space 0.09text n "one file, twenty-four times" right of g gap 0.7

Four boxes, evened out by one line

The figure is the same four boxes twice. In the top row they are written at 0, 0.75, 1.15 and 2.7 – three bunched at the left and one stranded out on its own, which is what typing positions in one at a time tends to produce. The bottom row is those same four statements with one line added under them.

spread is the other statement that lines things up. It leaves the first and the last elements exactly where they are and puts equal distances between the centres of everything between them. It acts only on the elements the layout positions – boxes, dots, free texts and images; naming a container, a brace or an edge is an error.

a b c d as written a b c d after spread x
box a1 "a" at 0,0 w 0.7 h 0.5 {.tone-1}box b1 "b" right of a1 gap 0.6 w 0.3 h 0.5 {.tone-2}box c1 "c" right of b1 gap 0.6 w 0.55 h 0.5 {.tone-3}box d1 "d" right of c1 gap 4.35 w 0.4 h 0.5 {.tone-4}text w1 "as written" right of d1 gap 1.15 {.small .muted}box a2 "a" below a1 gap 0.55 same as a1 {.tone-1}box b2 "b" right of a2 gap 0.6 same as b1 {.tone-2}box c2 "c" right of b2 gap 0.6 same as c1 {.tone-3}box d2 "d" below d1 gap 0.55 same as d1 {.tone-4}spread x a2, b2, c2, d2text w2 "after spread x" right of d2 gap 1.15 {.small .muted}

The cycle a spread can create

The last element in a spread cannot be placed against the one before it. spread makes every middle element depend on the first and the last, so writing d right of c and then spreading all four asks c to wait for d and d to wait for c. The build says placement cycle and names the line. Place the far end some other way: below its neighbour in the row above, or at a coordinate.

d right of c spread pins d, so c waits for it c d
box c "c" at 0,0 w 0.75 h 0.6 {.tone-1}box d "d" right of c gap 5.75 same as c {.tone-4}edge c.top -> d.top via c.cx,c.top-0.7 d.cx,d.top-0.7 "d right of c" {.small .muted} side topedge d.bottom -> c.bottom via d.cx,d.bottom+0.7 c.cx,c.bottom+0.7 "spread pins d, so c waits for it" {.small .accent} side bottom

Type that fits, and a line drawn over the top

.fit scales a label up or down until it fills the width the box was given, within about 0.6 to 1.5 times the normal size – the short label below has reached that ceiling. .shrink only ever makes a label smaller, which is what you want when most of them already fit and one is too long. Both need a width to fit into.

An element’s four corners are addressable as a.tl, a.tr, a.bl and a.br, and its centre as a.cx, a.cy. Aim an edge at a corner and it lands on the corner: an endpoint given as the middle of a box is inside it, so the line disappears under the border wherever it happens to cross. Both connectors below end on named corners. <-> is the token that puts an arrowhead at each end, and .front lifts an edge above the boxes.

FIT this one is far too long to fit
box a "FIT" at 0,0 w 1.7 h 0.55 {.fit .tone-2}box b "this one is far too long to fit" below a gap 0.45 same as a {.shrink .tone-2}box c "" right of a gap 2.9 w 0.9 h 1.0 {.tone-1}dot m at c.cx,c.cy r 0.09 {.accent}edge a.tr <-> c.tl {.muted}edge b.br -> c.bl {.muted}edge c.left-0.55,c.cy -- c.right+0.45,c.cy {.front .accent}

the rules

Fifteen rules for a figure a room can read

The build will draw a bad figure as readily as a good one, so the rest is up to you. Each rule below has two or three parts: what to do, what the eye is already doing that makes it work, and – where there is something to see – a wrong and a right version in real source you can copy. The second part draws on two bodies of work. The Gestalt principles are a short list of groupings human vision performs before you have decided to look at anything and that a reader cannot switch off; Matt Carter’s rules for scientific figures are about what a reader does with a picture once they have started on it, and Edward Tufte’s about how much ink it should have taken.

Every drawing below was produced from the source shown with it. The versions marked wrong are wrong on purpose: nothing in the build can detect what they get wrong.

Before the first line: is a figure the right answer?

Write the thing out in words first. If the words come out shorter and clearer than the drawing would be, keep the words and draw nothing.

Colin Ware set a flow chart of a small loop – read a line, put it in capitals, write it out again – beside four lines of pseudo-code saying the same thing. The pseudo-code won by a distance: it took less work to read and less ink to print.

What that leaves for a figure is everything a sentence has to be re-read to hold. Ware’s counter-example is six statements about who reports to whom in an office – Jane is Jim’s boss, Anne works for Jane, Anne is Mary’s boss – which nobody can keep in their head and everybody reads off a tree at a glance. A trust chain, a network segment, a protocol exchange, a table of who answers what: that is structure, and structure is what this language is for. A procedure whose steps are conditions and assignments is prose, or it is code, and a paragraph or a listing will serve it better than any number of diamonds.

If it is, Matt Carter’s eight questions take a minute and settle most of the figure before a line of it is written.

Three and four are emph and dim, and the four-beat order further down the page. Five decides between an arrow, an outline and mere nearness, which is rules 1 to 3. Six and seven are the two that get skipped, and they are the two that bite: if the timing is exact, the left-to-right order is carrying it and nothing may be moved just to tidy the picture up; if the distances are not exact, no gap may be sized as though they were. Eight is rule 4, decided once for a whole lecture rather than once per figure.

1

Group by distance, not by label

Make the gaps inside a group visibly smaller than the gaps between groups. A factor of two is enough; a factor of three is unmistakable.

Proximity is the fastest grouping cue there is, and it fires before anyone reads a word. If your spacing says one thing and your labels say another, the spacing wins and the labels look like a mistake.

wrong
Switch Router Resolver Webserver
box a "Switch"    at 0,0 {.tone-1}box b "Router"    right of a gap 1.45 {.tone-1}box c "Resolver"  right of b gap 1.45 {.tone-1}box d "Webserver" right of c gap 1.45 {.tone-2}
right
Switch Router Resolver Webserver
box a "Switch"    at 0,0 {.tone-1}box b "Router"    right of a gap 0.7 {.tone-1}box c "Resolver"  right of b gap 2.6 {.tone-1}box d "Webserver" right of c gap 0.7 {.tone-2}
2

Enclose what a distance cannot say

Use container for a trust boundary, a network segment, a DMZ, a machine that holds several parts. Do not try to say it with a bigger gap.

Common region beats proximity. An outline drawn around things binds them even when they are far apart, and even when something else sits between them – so a container overrules whatever your spacing happens to say.

wrong
A B Router
box a "A" at 0,0 {.tone-2}box b "B" right of a gap 0.85 {.tone-2}box r "Router" right of b gap 4.6 {.tone-1}
right
Home network A B Router
box a "A" at 0,0 {.tone-2}box b "B" right of a gap 0.85 {.tone-2}box r "Router" right of b gap 4.6 {.tone-1}container home "Home network" over a,b pad 0.4 {.dashed}
3

Keep edges for relations

Draw an edge when two things are actually connected. When you only want to point at something, use a leader: -- target on a text.

Uniform connectedness – two things joined by a line are read as one unit – is the strongest grouping cue of all, stronger than proximity and stronger than colour. An arrow used as a pointing finger claims a relationship that is not there, and the room will look for it.

wrong
Switch learns MAC addresses
box  sw "Switch" at 0,0 {.tone-1}text n  "learns MAC addresses" right of sw gap 3.45edge n -> sw
right
Switch learns MAC addresses
box  sw "Switch" at 0,0 {.tone-1}text n  "learns MAC addresses" right of sw gap 3.45 -- sw {.small .muted}
4

One tone, one role, all the way through

Decide at the start of a lecture what each fill means, write it down, and do not reuse a tone for a second meaning in the same figure.

Similarity groups things that are nowhere near each other. Two boxes in the same tone are claimed to be the same kind of thing, and the claim is made across the whole figure whether you meant it or not. The row below shows the four tones and what each is for.

tone-1 tone-2 tone-3 tone-4 accent dim muted
classmeans
.tone-1infrastructure: switch, router, resolver, firewall
.tone-2a legitimate host or participant
.tone-3payload: packets, protocol fields, records
.tone-4the one thing this beat is about – a solid fill, so use it sparingly
.accentthe attacker and the attack traffic
.dimstill on screen, but not part of what is being said now: full colour at a third of the strength. A step sets it with dim and lifts it again with style x {!dim}.
.mutedsupporting apparatus rather than subject matter: axes, leaders, grid lines, zone outlines. Drawn lighter and thinner, at full strength.

Two tones from the same group on one element contradict each other, and the build refuses the line: one tail cannot give two answers to one question. .tone-4 with .accent is not that kind of contradiction and is still a mistake – .tone-4 is already filled with the accent colour, so accent text on top of it cannot be seen – and because the two are in different groups the build draws it and warns instead of refusing. If the accent is what you want and the tone came down from a default line, {.accent !tone-4} says so.

5

Let a step say what belongs together

Give the elements of one beat a shared tag and reveal the tag, rather than listing their names. The two stepped figures earlier on this page work through theirs.

Things that appear together are read as one thing. Both versions below draw the same beat today, and they stop agreeing the moment the figure grows: a list of names is a second copy of who belongs to the beat, kept nowhere near the elements it names. With a tag, adding a sixth thing to the attack is one word on that element. With a list it is that plus a line further down the file, and a beat that reveals five things where it should reveal six looks exactly as deliberate as one that is right.

wrong
step attack
  show e, e-web, reply1, reply2, note-spoof
right
step attack
  show @attack

Most edges need no show of their own. An edge is only as visible as the two things it joins, a container only as visible as its members, a note with a leader only as visible as what it points at. Reveal the boxes and the lines that connect them appear with them.

The rule is a default, not a law. Naming an edge, a container or a brace in a show or a hide of its own overrides it, in both directions, from that beat onwards. So a line that has to be on the slide before either of its ends, or one that has to come away while both ends stay, costs one line of source.

6

Give a label a ground when it sits on a line

Any text that overlaps a line, a grid or a filled area gets a fill class and some pad. .paper is the one that knocks a hole in what is behind it.

Type printed on top of a line is read as texture rather than as words, and from the back of a room it is lost altogether. This is the figure-and-ground effect, the oldest of the groupings vision performs. A box is filled with the background colour already; a text on its own has no fill until you ask for one, which is what .paper is for.

wrong
Switch Router 10.1.1.0/24
box sw "Switch" at 0,0 {.tone-1}box rt "Router" right of sw gap 6.9 {.tone-1}edge sw -> rttext lbl "10.1.1.0/24" between sw,rt
right
Switch Router 10.1.1.0/24
box sw "Switch" at 0,0 {.tone-1}box rt "Router" right of sw gap 6.9 {.tone-1}edge sw -> rttext lbl "10.1.1.0/24" between sw,rt pad 0.12 {.paper .small}

An edge’s own label follows the same rule and says it in fewer words. A fill class on an edge gives its label a ground, and with no side named the words then sit on the wire and knock it out behind them; with side top, side bottom, side left or side right they clear the line and sit beside it. The label is carried at the middle of the route, so it stays there when the route bends or the boxes move – which a separate text placed between two boxes does not.

A phrase that describes a wire belongs to the wire, so place it against the wire. An edge has a coordinate of its own – w1.cx, w1.cy – and takes above, below, left of and right of like anything else. Pinning the phrase to one of the boxes at either end looks equivalent and is not: the label then holds its distance from the box, and how far it sits from the line changes the moment a fraction or a height does. Nothing warns you. Raise the two boxes in the figure below from h 3.0 to h 4.2 and a label measured from the wire stays 13.5 pixels clear of it, exactly where it was written; one measured from the Client drifts out to 22.3. That is also the reason each of those wires is written edge f1 cl.right:0.14 -> sv.left:0.14, with a name before the first end: an edge has no name until you give it one, and a wire nobody can name is a wire nothing can be placed against.

Pick one of the two per figure and keep to it. On-the-line works for a token that identifies the line – a sequence number, a port, a message type, anything short enough to read as a marker; beside-the-line for a phrase that describes what travels along it. Mix them inside one figure and the reader has to work out which kind each label is before they can read either. A message inside a sequence is the one place the choice is already made: its label is a phrase, it sits beside the line, and it carries a ground, because a lifeline crosses every label in a protocol figure.

both, on purpose
1 2 2 3 Client Server ClientHello ServerHello Certificate ClientKeyExchange
box cl "Client" at 0,0 h 3.0 {.tone-2}box sv "Server" right of cl gap 9.8 h 3.0 {.tone-1}edge f1 cl.right:0.14 -> sv.left:0.14 "1" pad 0.1 {.paper}edge f2a sv.left:0.44 -> cl.right:0.44 "2" pad 0.1 {.paper}edge f2b sv.left:0.62 -> cl.right:0.62 "2" pad 0.1 {.paper}edge f3 cl.right:0.92 -> sv.left:0.92 "3" pad 0.1 {.paper}text m1 "ClientHello"       at f1.cx-0.55,f1.cy-0.26 {.small .muted}text m2 "ServerHello"       at f2a.cx-0.55,f2a.cy-0.26 {.small .muted}text m3 "Certificate"       at f2b.cx-0.55,f2b.cy-0.26 {.small .muted}text m4 "ClientKeyExchange" at f3.cx-0.55,f3.cy-0.26 {.small .muted}
6 ·

Rule 6 continued: four words place a label inside its own box

.left, .right, .top and .bottom mean “as far that way as this element allows” – its inner edge, not its border. Reach for one when the middle of a box is already spoken for: a zone that holds other elements has a caption with nowhere to go but its own top border.

A centred label is right for a box that is only itself, and a zone is not one of those. Look at the wrong version below: the middle of a zone is where its contents are, so the caption is underneath them, with a T showing on one side and an E on the other. That is the visible half. The invisible half is worse – had the caption cleared the boxes by a few pixels it would still have been read as belonging to whichever one it sat nearest, because that is what proximity does, and the word would have been naming the App server rather than the region. Moved to the border it becomes the region’s own name, which is what a caption is, and the middle of the panel goes back to belonging to its contents.

With more than one line they move the block, so .bottom puts the last line on the inner edge. They apply to a box, a dot and a free text, and to nothing else. An edge asks a different question – not where the label sits inside its own space but which side of the line it clears – and answers it with an option of its own, side: side top or side bottom beside a horizontal edge, side left or side right beside a vertical one, with .turn to stand the words on end. Naming the side that runs along the edge cannot move the label, and the build warns rather than refusing, because which pair runs along a line is only decidable once the line has been routed. A container’s caption and a brace’s label are placed by their own statement, so there the four words are refused outright rather than left as classes that quietly do nothing. A brace does take side, and there it means which side of its members the bracket stands on.

wrong
TRUSTED ZONE DB App Auth
box zone "TRUSTED ZONE" at 0,0 w 1.05 h 2.9 {.tone-2}box db  "DB"   at zone.cx,zone.top+1.0 w 0.58 h 0.45 {.paper}box app "App"  below db gap 0.12 same as db {.paper}box aut "Auth" below app gap 0.12 same as db {.paper}
right
TRUSTED ZONE DB App Auth
box zone "TRUSTED ZONE" at 0,0 w 1.05 h 2.9 {.tone-2 .top}box db  "DB"   at zone.cx,zone.top+1.0 w 0.58 h 0.45 {.paper}box app "App"  below db gap 0.12 same as db {.paper}box aut "Auth" below app gap 0.12 same as db {.paper}
7

Run edges on axes, and bend them once

Keep edges horizontal or vertical. When one has to get around something, give it one waypoint – a point you tell the line to pass through – rather than three. Let crossings happen at right angles.

Good continuation – the eye follows a line through a junction and gives up at a corner. Each bend is a place a reader has to re-acquire the line. The build helps: an edge that misses horizontal or vertical, but misses it by less than 4°, is reported as a warning, because a line that nearly lines up is almost never what anyone meant.

wrong
C0 Dec X1 P1
box c0  "C0"  at 0,0 {.tone-3}box dec "Dec" below c0 gap 0.5 {.tone-1}box x1  "X1"  below dec gap 0.5 {.tone-3}box p1  "P1"  right of x1 gap 2.6 {.tone-2}edge x1 -> p1edge c0 -> x1 via c0.cx,c0.bottom+0.25 c0.right+0.4,c0.bottom+0.25 c0.right+0.4,x1.top-0.25 x1.cx,x1.top-0.25
right
C0 Dec X1 P1
box c0  "C0"  at 0,0 {.tone-3}box dec "Dec" below c0 gap 0.5 {.tone-1}box x1  "X1"  below dec gap 0.5 {.tone-3}box p1  "P1"  right of x1 gap 2.6 {.tone-2}edge x1 -> p1edge c0 -> x1 via c0.left-0.4,c0.cy c0.left-0.4,x1.cy

Both versions write every waypoint in terms of other elements, so both survive the figure being rearranged – that is not what separates them. What separates them is four corners against two. The one marked wrong goes down, across, down and back again to reach a box that sits directly below where it started; the one marked right leaves sideways into the empty channel and comes in level, which is the same detour in half the turns. Each corner is a place where the eye has to find the line again, and the first version has four of them doing the work of two.

A route with one turn out and one turn in, halfway across the gap, is common enough to have a word of its own: .elbow on an edge draws it with no waypoints written at all. The build picks the axis the two ends are further apart on, and turns halfway between the two facing sides rather than halfway between the two centres. Two edges leaving one parent for children on the same level then turn on the same line and read as one bracket, which is what the tree under Five figures a lecture keeps asking for is made of. It takes no options, and writing it alongside via is an error.

8

End an edge on an anchor, never inside a box

Name the side the arrow should arrive at – b.left, b.top – whenever the automatic choice is wrong. Separate two edges between the same pair with a fraction along the side.

An arrowhead that crosses a border reads as a mistake in the drawing, and it obscures both the border and itself. The arrow stops at the boundary and the boundary stays unbroken.

wrong
replay forgery Eve Bob
box  eve "Eve" at 0,0 h 1.05 {.accent}box  bob "Bob" right of eve gap 6.35 same as eve {.tone-2}edge eve -> bob "replay"edge eve -> bob "forgery"
right
replay forgery Eve Bob
box  eve "Eve" at 0,0 h 1.05 {.accent}box  bob "Bob" right of eve gap 6.35 same as eve {.tone-2}edge eve.right:0.3 -> bob.left:0.3 "replay"  {.accent}edge eve.right:0.7 -> bob.left:0.7 "forgery" {.accent} side bottom

The wrong half is two arrows drawn along one line, with two labels landing on the same spot. Nothing is hidden: the second arrow is exactly where the first is, so the figure claims one relation where the source says two, and the words are unreadable into the bargain.

The other way to get this wrong is to leave out same as, and it is worth seeing because the failure is small enough to ship. A fraction is measured along each box’s own side, so eve.right:0.3 and bob.left:0.3 are the same fraction of two different heights and land at two different heights. Both lines then run a degree or two off level – a picture that looks almost right, which is worse than one that looks wrong. The build says so: this is one of the two cases the off-axis warning in rule 7 exists for.

wrong, and the build says so
replay forgery Eve Bob
box  eve "Eve" at 0,0 h 1.05 {.accent}box  bob "Bob" right of eve gap 6.35 h 1.85 {.tone-2}edge eve.right:0.3 -> bob.left:0.3 "replay"  {.accent}edge eve.right:0.7 -> bob.left:0.7 "forgery" {.accent} side bottom
9

Draw the fewest things that carry the argument

Cut anything that is not doing work. A server does not need drive bays, a compromised host does not need a biohazard symbol, a person does not need a face unless the figure is about people.

Prägnanz – vision settles on the simplest reading available, so every mark you add is a mark someone has to rule out. Edward Tufte’s name for the same rule is the data-ink ratio: take out the non-data ink and the redundant data ink, within reason, and what is left is the drawing. Before you add an element, ask which beat it belongs to. If the answer is “none”, it is decoration.

10

No shadows, no free colours, no rotation for effect

Stay inside the list of classes the language gives you. There is no way to add to it.

A shadow is a depth cue in a drawing that has no depth: it adds a second edge to every shape and buys nothing. .turn is for a label with no room to be horizontal – a firewall bar, a matrix row, an axis title – and not for effect. A fixed hue imported from a slide deck works on exactly one background.

11

Size is a claim, so make every size deliberate

Give elements of the same kind the same size, with same as. Reach for a bigger box only where the thing inside it really does contain or control the smaller ones.

Relative size reads as importance (Carter). A box half again as wide as its neighbours is taken to be the important one before anyone has worked out why – and the usual reason it is wider is that its label happened to have more letters in it. That is the drawing making an argument you never made.

wrong
Sensor Correlation engine Log
box a "Sensor" at 0,0 {.tone-1}box b "Correlation engine" right of a gap 1.15 {.tone-1}box c "Log" right of b gap 1.15 {.tone-1}
right
Sensor Correlationengine Log
box a "Sensor" at 0,0 w 1.05 h 0.85 {.tone-1}box b "Correlation\nengine" right of a gap 1.15 same as a {.tone-1}box c "Log" right of b gap 1.15 same as a {.tone-1}

The \n is the whole fix for the long label. Nothing breaks a line for you, so where the break falls is a decision, and putting it where the phrase divides reads better than any measure the build could take. The height on the first box is explicit for the same kind of reason: same as copies whatever size it finds, so a one-line box would hand a one-line height to the box that now has two lines in it.

.shrink is the other answer, where the box may not grow. Use it knowing that type size is a size claim too, and that a room reads small type as less important.

12

Lay the figure out the way the room reads it

Start at the top left and run left to right, top to bottom. The first thing in time or in cause goes at the top left, the last at the bottom right.

With nothing shouting for attention, a reader works through a figure the way they work through a page of text (Carter): top left, then a zig-zag rightwards and down. A figure whose flow runs against that gets read twice – once the wrong way, and again once the arrowheads have been noticed.

The exception is a figure whose subject is the return. A request going out and an answer coming back is two directions, and the pair still reads correctly, because the outward leg is the upper one. What does not survive is a single chain laid out backwards. This is also what lets a swimlane carry time with no axis drawn on it, and a flowchart say which branch is the ordinary one without spending a word on saying so: in both, the reading order is the argument.

wrong
Service Filter Request and the drop is logged
box c "Service"  at 0,0 {.tone-2}box b "Filter"   right of c gap 0.75 {.tone-1}box a "Request"  right of b gap 0.75 {.tone-2}edge a -> bedge b -> ctext n "and the drop is logged" below b gap 0.5 {.small .muted}
right
Request Filter Service and the drop is logged
box a "Request"  at 0,0 {.tone-2}box b "Filter"   right of a gap 0.75 {.tone-1}box c "Service"  right of b gap 0.75 {.tone-2}edge a -> bedge b -> ctext n "and the drop is logged" below b gap 0.5 {.small .muted}
13

Bold type marks one element, never a category

Say what kind of thing an element is with a fill, an outline or a shape, and keep .bold for the one element the figure is about.

Bold has no degrees (Carter). Bold on every system box says “look here” about all of them, which is the same as saying it about none. .bold belongs to no class group, so nothing displaces it: a default box {.bold} at the top of a block boldens every box in it, and the build has nothing to refuse. An element that has to be the exception says so in its own tail, {!bold}.

.bold and emph are not interchangeable, and which one you write decides what a handout shows. Where the word is written is what decides: a prominence class on an element’s own line is part of the drawing and reaches the handout, and a prominence a step sets is a lecture-time act and does not. Bold the element the whole figure is about, write {.dim} on the ones that are background for the whole of it, and leave emph and dim inside a step for the element this beat is about.

14

A leader is short, straight, and parallel to its neighbours

Put an outside label beside the thing it names, on the side where there is room, and run the words towards the object: .right on a label sitting to the left of its subject, .left on one sitting to the right.

Carter’s rules for outside labels are all one idea: a leader is plumbing, and it must never be read as content. Keep the line as short as the layout allows, keep several of them parallel, and never let two cross. A crossing is read as a relationship, which is exactly the claim rule 3 says an arrow makes.

wrong
Switch Router learns MACaddresses forwards byprefix
box  sw "Switch" at 0,0 h 1.2 w 1.1 {.tone-1}box  rt "Router" below sw gap 0.75 h 1.2 w 1.1 {.tone-1}text n1 "learns MAC\naddresses" right of rt gap 1.6 -- sw {.small .muted}text n2 "forwards by\nprefix" right of sw gap 1.6 -- rt {.small .muted}
right
Switch Router learns MACaddresses forwards byprefix
box  sw "Switch" at 0,0 h 1.2 w 1.1 {.tone-1}box  rt "Router" below sw gap 0.75 h 1.2 w 1.1 {.tone-1}text n1 "learns MAC\naddresses" left of sw gap 1.6 -- sw {.small .muted .right}text n2 "forwards by\nprefix" right of rt gap 1.6 -- rt {.small .muted .left}

Two of those rules are already the default here. The stub that -- sw grows is thin, grey and headless, so it does not read as an edge in a busy figure, and it takes no waypoints either way, so it is a single straight segment. A head is available on the other token, ->: it buys precision where a note has several candidates near it, at the cost of the distinction the thin grey line was drawing. What is left to you are which side and how short, plus the alignment – .left and .right set a label of more than one line flush against the edge nearest the object.

This is also how a figure that shows a procedure carries its explanation. A figure about structure works with very few words; one that shows a procedure needs sentences, and they belong at the place they describe rather than in the prose underneath. That is rule 1 again: a reader who has to look away and come back rebuilds the picture each time.

15

Take the outline off and the fill has to do its work

If you write .bare, give the shape a fill dark enough to be a shape on its own – .tone-3 rather than .tone-1. And keep the count down: several outline-less shapes in the solid fill read as a figure where everything is emphasised at once.

The outline is what closes the contour, and a closed contour is what the eye takes for an object. Dropping it is a fair way to cut clutter, and it moves the whole job onto the fill, which then has to be darker than it would otherwise have been (Carter). Past three or four such shapes the saving has been spent, and the figure has no quiet ground left to emphasise anything against.

The exception is a region rather than an object. A container written .bare with a pale fill is a ground, and a ground is meant to recede, which is the opposite of what a shape has to do. The rule above is about shapes that have to read as things.

.tone-4 with .small is the specific pair to watch. .tone-4 turns its own label the colour of the page, so that it can be read on a solid fill, and .small is twelve pixels. Reversed type at twelve pixels is legible on the machine that drew the figure and gone from the back of a lecture theatre. A solid-filled box that needs a small label needs a shorter label instead.

wrong
Sensor Engine Store Console
box a "Sensor"  at 0,0 {.bare .tone-1}box b "Engine"  right of a gap 0.5 {.tone-1}box c "Store"   right of b gap 0.5 {.tone-1}box d "Console" right of c gap 0.5 {.tone-1}
right
Sensor Engine Store Console
box a "Sensor"  at 0,0 {.bare .tone-3}box b "Engine"  right of a gap 0.5 {.tone-1}box c "Store"   right of b gap 0.5 {.tone-1}box d "Console" right of c gap 0.5 {.tone-1}

the order, and the walls

A figure with steps is an argument, not a slideshow

The beats are the order the argument is made in. This order works almost every time.

beat 1

The stage

Everything that is just true: the topology, the axes, the participants. One beat, no emphasis.

beat 2

The normal case

What happens when nothing is wrong. This is what the attack will be measured against.

beat 3

The disturbance

The attacker, the failure, the exception. This is the beat that earns .accent and emph.

beat 4

The consequence

The defence, the result, the number. Often dim on what came before, so the last beat is not four things shouting at once.

The last beat has to make sense on paper, standing alone, because that is the beat a printed handout gets. If it does not, you have one beat too few. Stepping backwards works the same way as stepping forwards, because every beat is worked out from scratch, which is what makes hide as usable as show.

before a figure is finished

Checkable mechanically, before a figure is finished

  1. node lint.js <source.md> reports nothing. A warning you have decided to live with still counts as unclean.
  2. node build.js <source.md> prints no [diagram] warning, or the source carries a comment saying why the remaining one is deliberate.
  3. No edge runs at a slight angle. The build says so; do not silence it by nudging – align the elements.
  4. Every label that overlaps a line or a filled shape has a fill class of its own.
  5. Every arrowhead lands on a border, none inside a box, none two-on-a-line.
  6. No tone carries two meanings in one figure, and none contradicts the table in rule 4.
  7. Each beat is one idea. Read the step names out loud in order: they should sound like an argument.
  8. The last beat is a sensible handout picture on its own. Prominence a step set is taken off again for print by the build; a tone a step painted is not, because a tone describes the drawing. So a figure that tints one row, one column or one box at a time ends on a beat that leaves the tint where the handout should have it.
  9. Every edge label in the figure follows one convention: on the line for a token that names the line, beside it for a phrase that describes what runs along the line. Never both in one figure.
  10. Every .diamond holds two or three words. It is sized at twice what a rectangle would need, so a sentence in one is a shape four times the area of its neighbours.
  11. Nothing is built by hand that a statement writes: a grid of labelled cells is table, equal bands with names down the side are lanes, messages between lifelines down the page are sequence, columns whose heights are numbers are bars, and one turn out and one turn in is .elbow.
  12. Every chart says its shape with aspect, not with w and h. A grid cell is not square, so those two do not describe the proportion anyone sees. A row of charts meant to be read against one another takes its frame from the first of them, with same as.
  13. Every bar chart whose categories are phrases, or whose point is a ranking, runs horizontal. Four phrases under four upright columns are four phrases nobody in the room reads.
  14. Opened in the browser and cycled through all seven colour themes with the A key: nothing disappears, nothing becomes unreadable. The two single-colour terminal themes are the hardest to pass.
  15. node test/run.mjs framing passes. It measures the slack on all four sides of every figure, and it is the only check that catches a picture which is correct but not centred in its own frame.
  16. The figure is a figure at all: structure – what holds what, what connects to what, what is the same as what – rather than a procedure that would read better as a paragraph or a listing.
  17. Nothing is a different size by accident. Elements of one kind carry same as, and every size difference left standing is a claim about importance the figure means to make.
  18. The flow runs left to right and top to bottom, unless the subject of the figure is a return leg.
  19. .bold is on at most one element. No leader crosses another, and no .bare box carries a fill paler than .tone-3 – a .bare container is a ground and is exempt.
  20. No plot holds more than three overlapping lines; a fourth is a second frame carrying the same grey baseline. Every histogram is written space 0, and no bar chart is.

what it cannot do

What the language will not do

No automatic routing

Edges are straight segments through the waypoints you write. Nothing steps around a box for you, and nothing fans out parallel edges – that is what the :0.3 / :0.7 fractions are for. .elbow is the one place the build puts a point on the page you did not write, and it is bounded so it cannot become routing: the turn is always halfway across the gap, it looks at nothing else in the figure, and there is no option to move it.

No shadows, no gradients, and a fixed drawing order

A shape is a flat outline with a flat fill. What covers what is decided for you too: containers, images, braces, edges, then boxes, dots and texts. The one way out is .front, which moves an edge in front of the boxes – right for an axis, wrong for an arrow, so you have to ask for it.

No cylinder, no cloud

The outlines are the rectangle, the circle, the hexagon, the diamond, the chevron, the wedge and the cross – and the cross is square. A database is a .round box in the tone you gave storage; a cloud is a box labelled Internet. If a shape genuinely carries meaning of its own, draw it as an SVG file and place it with image.

No free colours

Four tones, plus .accent and .muted. If you need a fifth distinction, use shape (.hex, .diamond, .cross, .chevron, .wedge, the last two aimed with point) or family (.mono, .serif) – and ask first whether the figure is doing too much.

No automatic line breaking

A label breaks where you write \n, and nowhere else.

No constraint solver

Every position is written in terms of another position, and the build resolves those dependencies in one pass. An arrangement that refers back to itself is reported as an error naming the line, rather than drawn as a picture that looks plausible and is wrong.

Text width is estimated

There is no browser at build time, so the estimate is tuned slightly generous – a box a little wider than its text reads as designed, a box narrower reads as broken. Where it matters, give an explicit w.

No arbitrary rotation

.turn turns a label 90°, and point aims a chevron or a wedge at one of the four compass directions. Nothing rotates to an arbitrary angle.

Those are the boundaries. The fair question about what is inside them is whether the list keeps growing under you. There is a written bar for a new statement or option: build it out of what exists first, and only add a word when the hand-built version is the thing that cannot be maintained. table, lanes and sequence are the three most recent words, and each cleared that bar the same way: the language could already draw the picture, and could not let anyone change it afterwards. The section after next has the measurements. A word that only saves typing does not get in, which is why the flowchart and the tree in that same section need no statement of their own.

five shapes of argument

Five visualisations we like to use in our lectures

A flowchart, a swimlane, a tree, a table and a protocol. None of them is a kind of drawing this language knows about – each is boxes, edges and text put in a particular arrangement, and the flowchart and the tree need nothing the page has not already shown. Every one below is real source the build draws, in the number of lines you see.

The swimlane, the table and the protocol have a statement of their own, which writes those same boxes and edges for you. All three were added for one reason: the language could draw the picture by hand, but it could not let you change it afterwards. A swimlane's equal bands meant guessing a width and re-guessing it every time their contents changed. A grid of labelled cells cost twenty-one lines before a single word of the table appeared, each cell placed against the one above or beside it, so inserting a row meant re-aiming three of those references – and nobody catches a missed one by reading the change. A protocol cost every message a y coordinate of its own, so inserting one in the middle meant moving every message under it, renumbering all of them and re-guessing how far each lifeline runs. Measured on the stepped figure at the end of this section: thirteen lines of edits to insert one message, against one line now.

A flowchart

The spine falls out of below … gap, one statement per step, and it needs no coordinates. Decisions are .diamond; the two ends of the process are .round, which is the stadium shape a terminator conventionally takes.

The branch labels are where the care goes. yes and no on an edge take side top, side bottom, side left or side right, which names the side of the line the word sits on – and the side that runs along the line cannot move it, so the build warns rather than letting the word land somewhere you did not mean. The route that skips ahead goes out into the empty channel beside the spine and comes back in level, which is rule 7.

no yes no yes Packet in Known flow? Rule allows? Forward Drop
::: draw {unit=150x52}# The main road runs straight down and reads as the default: every decision# that says no leaves it sideways, so a reader who follows the vertical line# is following the packet that gets through. The fast path is the one branch# that has to rejoin, so it goes out into a channel of its own rather than# crossing the road it will come back to.# Ware would not draw this at all: a procedure of conditions and assignments# reads faster as four lines of pseudo-code. It is here for the shape.box in   "Packet in"   at 0,0 w 1.15 {.round .tone-3}box d1   "Known flow?" below in gap 0.55 {.diamond .tone-1}box d2   "Rule allows?" below d1 gap 0.55 same as d1 {.diamond .tone-1}box acc  "Forward"     below d2 gap 0.55 same as in {.round .tone-2}box drop "Drop"        right of d2 gap 2.9 same as in {.round .accent}edge in -> d1edge d1 -> d2 "no" side rightedge d2 -> acc "yes" side rightedge d2 -> drop "no" side topedge d1.left -> acc.left "yes" via d1.left-0.55,d1.cy d1.left-0.55,acc.cy side left:::

A swimlane

lanes draws the bands and their turned names, and after that every activity is placed one at a time, against a column and a lane: at a2.cx+1.5,swim-1.cy reads as “a step to the right of the last one, in the SOC’s lane”. Nothing is laid out for you; what the statement removes is the part that was never a decision.

A container is the wrong tool here, and it is worth saying why, because it is the obvious first thing to reach for. A container fits its members – so three lanes holding three, five and one element come out as three bands of three different widths, ragged at both ends, which is the opposite of what a swimlane means. Lanes are equal by construction instead. The one number left to choose is how deep a band is, written band 0.95, and it is stated once rather than once per lane.

Before writing one, ask which of two questions the figure answers. Steps parcelled out to the people responsible for them is a swimlane. Messages passing between them is a sequence, the next row down, where the names run across the top and the time runs down the page.

User SOC IT ops Reportssuspect mail Triage Confirmedmalicious Block sender,recall copies Told whatto look for
::: draw {unit=110x64}# The lanes say who and the left-to-right order says when, so neither has to# be written in a box. Every hand-off changes lane, which is why they are# elbows: a straight line between two bands reads as a diagonal across a band# it never enters, and there are four of those here.lanes swim "User | SOC | IT ops" at 0,0 w 7.05 band 0.95 {.muted .dashed}box rep "Reports\nsuspect mail"      at swim.left+0.75,swim-0.cy w 1.2 {.tone-2}box tri "Triage"                     at swim.left+2.05,swim-1.cy w 0.9 {.tone-1}box con "Confirmed\nmalicious"       at swim.left+3.35,swim-1.cy w 1.15 {.tone-4}box blk "Block sender,\nrecall copies" at swim.left+4.9,swim-2.cy w 1.3 {.tone-1}box ntf "Told what\nto look for"      at swim.left+6.3,swim-0.cy w 1.15 {.tone-2}edge rep -> tri {.elbow}edge tri -> conedge con -> blk {.elbow}edge blk -> ntf {.elbow}:::

A protocol

sequence puts the names across the top, hangs a lifeline under each of them, and draws the messages down the page between those lifelines, so the vertical axis is the time. In the swimlane above, the names run down the side instead and time is only the reading direction.

Its lines come in three shapes and no more: actor, a message between two names, and note on a lifeline or between two of them. A message is an edge, so all four arrow tokens – ->, <-, --, <-> – and {.dashed} mean here what they mean anywhere else, and a message from a name to itself loops out of the lifeline and back rather than becoming a fourth shape. The numbers down the left are drawn unless you write unnumbered.

The statement decides the vertical rhythm and nothing else. Each band is as tall as what stands in it – a message its label, a note its text, a self-message its loop – so a note pushes what follows it down instead of cutting into it. space on one entry is the air above that band, which is how a long protocol is broken into phases: it is what sets the encrypted tunnel in the drawing apart from the exchange that opens it. A blank line in the source changes nothing in the drawing, so the lines can be grouped for whoever has to read the source.

CONNECT server:443 TCP handshake note the destination 200 Connection established encrypted tunnel, end to end Client Proxy Server 1 2 3 host, time, byte counts 4 5 the proxy forwards bytesand reads none of them
::: draw {unit=140x44}# Three shapes of line and nothing else: an actor, a message between two# names, and a note standing on a lifeline. What the statement decides is the# vertical rhythm - every band is as tall as what stands in it - so inserting# a message is inserting a line.sequence x at 0,0 space 0.34  actor c "Client"  actor p "Proxy"  actor s "Server"  c -> p "CONNECT server:443"  p -> s "TCP handshake"  p -> p "note the destination" "host, time, byte counts"  c <- p "200 Connection established" {.dashed}  c -- s "encrypted tunnel, end to end" space 0.9  note c,s "the proxy forwards bytes\nand reads none of them" {.tone-2}:::

A tree

Build it leaf-first. Write the leaves in a row, then place each parent with between its own children and an offset upwards – which is what “centred over its reports” means, said without a solver. The whole thing resolves in one pass, because every parent depends on children that were placed before it.

Root-first is the trap. You cannot align a parent onto its children: align hands the first element’s coordinate to all the others, so it would drag the children under the parent instead. And the connectors are -- rather than ->, because a reporting line carries no arrowhead, with .elbow for the bracket – two connectors out of one parent turn at the same rail and read as one.

www.example.org mail.example.org vpn.example.net Issuing CA A Issuing CA B Root CA
::: draw {unit=95x95}# Written from the leaves up, because the leaves are the fixed points - they# are what a browser is actually asked about - and every parent is then the# midpoint of what it signs. Move a leaf and the two ranks above it re-centre# with no other line touched. The connectors carry no arrowheads: a signature# has a direction, but drawing it turns an org chart into a dataflow.box l1 "www.example.org"  at 0,0 w 1.7 h 0.72 {.tone-2}box l2 "mail.example.org" right of l1 gap 0.2 same as l1 {.tone-2}box l3 "vpn.example.net"  right of l2 gap 0.5 same as l1 {.tone-2}box i1 "Issuing CA A" between l1,l2 offset 0,-1.5 same as l1 {.tone-1}box i2 "Issuing CA B" at l3.cx,i1.cy same as l1 {.tone-1}box rt "Root CA"      between i1,i2 offset 0,-1.5 same as l1 {.tone-4}edge i1 -- l1 {.elbow .muted}edge i1 -- l2 {.elbow .muted}edge i2 -- l3 {.muted}edge rt -- i1 {.elbow .muted}edge rt -- i2 {.elbow .muted}:::

A table, a row at a time

The heading is the statement’s own string; the rows are the strings underneath it, one per line, split on a pipe. Step through it.

Layer Forgery Countermeasure Link ARP spoofing Dynamic ARP inspection Network IP source spoofing Ingress filtering (BCP 38) Transport Blind TCP reset Sequence-number checks Application DNS cache poisoning DNSSEC
::: draw {unit=150x52}# A cell carries a tag for its row and a tag for its column, so a beat is one# name rather than three cell names kept in step by hand - and a tag is a tag,# so `show @t-row-2` brings a row in exactly as `style @t-row-2` tints one.# The rows arrive one at a time rather than sitting there being highlighted:# an element starts hidden when the first thing a step says about it is# `show`, and nothing says that about the heading, so the table opens as its# own column headings and fills in under them. The closing beat is what a# printed copy gets, which is why the argument ends on the answers rather# than on whichever row happened to be lit when the lecture stopped.# The last row is written cell by cell: a beat reaches one cell as# readily as a whole row, so t-2-4 arrives on its own beat and is emphasised# there. Column first, row second, and row 0 is the heading.table t "Layer | Forgery | Countermeasure" at 0,0 col 1.15,1.55,1.7 row 0.44 {.clear .bare .left}"Link | ARP spoofing | Dynamic ARP inspection""Network | IP source spoofing | Ingress filtering (BCP 38)""Transport | Blind TCP reset | Sequence-number checks""Application | DNS cache poisoning | DNSSEC"step the-link-layer-can-be-forged  show @t-row-1step so-can-the-network-layer  show @t-row-2step and-the-transport-layer  show @t-row-3step and-the-application-layer  show t-0-4, t-1-4step the-answer-to-that-one-is-a-standard  show t-2-4  emph t-2-4step every-one-of-them-has-an-answer  style @t-col-2 {.tone-2}:::

Every cell is an ordinary box, so a brace spans two rows of one with the syntax it would use on any two boxes, and a step restyles a cell the way it would restyle any box. What the statement adds beyond saving the typing is a tag on every row and a tag on every column, written for you: @t-row-2 names one whole row and @t-col-0 one whole column. That is what makes a row per beat one line of source, rather than a list of three cell names that has to be kept in step with the table above it.

Cells are named t-<column>-<row>, column first and counting from zero, and row 0 is the heading – so @t-row-1 is the first line of data and @t-col-2 is the last column. Column widths are one number each in col; leave it out and w is divided equally. The height is row, and it is the height of one row rather than of the whole table – which is what lets you add a line of data without re-guessing a number. A beat reaches one cell as readily as a whole row, which is why the last row above arrives as t-0-4 and t-1-4 and then, on a beat of its own, t-2-4.

Print gets the last beat, and that rule reaches a stepped table in a way that is easy to miss. A row tinted by a style step is tinted in the handout too, because a tone is a statement about the drawing; a row lit with emph is not, because prominence a beat lent it is a moment in the talk. So a table that tints each row in turn and then stops prints with its last row still tinted, which is a handout reporting the moment the lecture happened to stop. If the finished picture wants no highlight at all, the figure is one beat short – which is why the one above ends on the answers rather than on whichever row was lit last.

A protocol, one phase at a time

Passkey registration: four lifelines, nine messages and two notes, in four beats. Step through it.

on the device, over CTAP clicks Create passkey request registration options registration options CTAP authenticatorMakeCredential prompt: PIN or biometric user verified locally attestation object attestationObject + clientDataJSON verify signature · check origin User Browser Authenticator Relying Party 1 2 3 challenge · rp.id · user.id · algs 4 clientDataHash · rp.id · user · algs CTAP runs over USB, NFC or BLE 5 6 generate key pairbind to SHA-256(rp.id)store privately · emit publicly 7 authData (public key, cred ID) · signature 8 clientDataJSON carries challenge · origin 9 the challenge is what makes it fresh
::: draw {unit=150x40}# The actors are lines of their own because each needs a name later lines can# hold on to and an attribute tail of its own. Everything under them is a# message - an arrow between two names - or a note.sequence wa at 0,0  actor u  "User"  actor br "Browser"  actor au "Authenticator" {.tone-3}  actor rp "Relying Party"  u  -> br "clicks Create passkey"  br -> rp "request registration options"  br <- rp "registration options" "challenge · rp.id · user.id · algs" {.dashed}  br -> au "CTAP authenticatorMakeCredential" "clientDataHash · rp.id · user · algs"  note br,au "CTAP runs over USB, NFC or BLE"  au -> u  "prompt: PIN or biometric"  u  -> au "user verified locally"  note au "generate key pair\nbind to SHA-256(rp.id)\nstore privately · emit publicly"  au -> br "attestation object" "authData (public key, cred ID) · signature" {.dashed}  br -> rp "attestationObject + clientDataJSON" "clientDataJSON carries challenge · origin"  rp -> rp "verify signature · check origin"# Two annotations the statement knows nothing about, hung off generated# names: a brace over three messages and a note beside one of them. A beat# can show or emphasise either one exactly as it would a box.brace ctap over wa-3,wa-4,wa-5 pad 0.3 "on the device, over CTAP" side left {.small .turn}text fresh "the challenge is what makes it fresh" right of wa-2 gap 1.9 {.small .hand} -- wa-2# The beats build the protocol rather than only lighting parts of a finished# one, which is how it is read in front of a room: one exchange, then the next.# Two things arrive on their own and neither is named where it appears. The# brace is never shown by a step - it holds wa-3, wa-4 and wa-5, and a brace is# only as visible as its members, so it comes in with them. The annotation is# the opposite case: it hangs off wa-2 by a leader and would arrive with it, so# an explicit `show` one beat later is what holds it back until the exchange# it comments on has been read.step ask-for-options  show @wa-msg-1, @wa-msg-2  emph @wa-msg-2step the-challenge-is-the-point  show fresh  dim @wa-msg-1, @wa-msg-2step on-the-device  show @wa-msg-3, @wa-msg-4, @wa-msg-5  show wa-note-0, wa-note-1  emph austep back-to-the-relying-party  show @wa-msg-6, @wa-msg-7, @wa-msg-8  dim au  emph @wa-msg-8:::

Each beat is one tag. emph @br-msgs lights every message the browser sends or receives, @au-msgs does the same for the authenticator, and the closing beat quiets all of them so the room sees the whole exchange again. None of it reaches the handout: prominence a step sets is a lecture-time act, so a printed copy shows the finished protocol at full strength whatever the last beat was doing. Nothing here states a size, either. A sequence measures itself from its own type – the heads as wide as the longest name, every band as tall as what stands in it – and w, header and space are overrides for the figure that needs one.

The two annotations are ordinary lines hung off names the statement generated. The brace on the left is brace ctap over wa-3,wa-4,wa-5 pad 0.3 … side left and the aside on the right is text fresh … right of wa-2 gap 1.9 -- wa-2. Both are the ordinary brace and text statements, aimed at a message instead of a box, and both follow the messages they are about when the protocol changes shape, because each is attached to a message and not to a coordinate.

That works because every part the statement draws keeps a name you can write, and those names are fixed. An actor head keeps the name its own line gave it. A lifeline is <actor>-life. Message i, counting from zero, is wa-i; its number is wa-n-i and its smaller second line wa-sub-i; note j is wa-note-j. The sets have tags: @wa-msg-3 is one message together with its number and second line, @wa-msgs is every message, @au-msgs is every message touching the authenticator, and @wa-notes, @wa-actors and @wa-lives are the rest. So when a protocol figure needs something sequence itself cannot draw, you write it in the ordinary vocabulary, aimed at the part of the figure the annotation is about.