psi-slides · ::: draw
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":::
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, 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.
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.
::: draw {unit=170x56}box cl "Client" at 0,0box sv "Server" right of cl gap 4.25:::
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.
::: draw {unit=170x56}box cl "Client" at 0,0box sv "Server" right of cl gap 4.25edge cl -> sv:::
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.
::: 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:::
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.
::: 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:::
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.
::: 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 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.
::: 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}:::
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.
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.
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.
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.
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 wordgap 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 positionbox 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.
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.
# is a commentAnywhere 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
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 wrote | the opening picture | during the lecture | in print |
|---|---|---|---|
show / hide | hidden from the start if the first thing any beat says about it is show; visible otherwise | cumulative – every beat up to here has happened | the last beat |
an edge, a container, a leader | as visible as what it joins, holds or points at | re-derived at every beat | the last beat |
move, label | as written on the element’s own line | cumulative | the last beat |
| fill, ink, stroke and the rest of the look | as written on the element’s own line | cumulative | the last beat |
.emph .dim .ghost | as written on the element’s own line | cumulative | the opening picture, not the last beat |
the drawable, its type size, its label anchor, .smooth, .front | as written | cannot be changed by a beat | as 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.
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.
::: 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.
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.
The four-beat order set out lower down the page: the stage, the normal case, the disturbance, the consequence. Step through it.
::: 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.
One beat, one move statement. Everything else in the figure follows on its own.
::: 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
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.
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.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.
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.
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 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.
| group | classes | where it acts | in a step? | what it sets |
|---|---|---|---|---|
| fill | .tone-1 .tone-2 .tone-3 .tone-4 .clear .paper | box dot text container edge | yes | What 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 .muted | box dot text container brace edge | yes | Which 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 .dotted | box dot container brace edge | yes | A 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 container | yes | A 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 box | no | The 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 .large | box dot text container brace edge | no | The label one step down or up from the figure’s normal size. |
| family | .mono .serif .hand | box dot text container brace edge | yes | Typeface 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 .shrink | box text | no | What 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-heads | edge | yes | Which 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 .elbow | edge | only .elbow | The 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 .right | box dot text | no | Push the label to one side of the space its element gives it. |
| label down | .top .bottom | box dot text | no | The 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 .ghost | every element | yes | How 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 | .front | edge | no | An 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 | .turn | box dot text container brace edge | yes | The 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 | .bold | box dot text container brace edge | yes | The 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).
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.
wbox sw "Switch" at 0,0 w 1.2hbox fw "Firewall" at 0,0 h 1.5rdot j at sw.right,sw.cy r 0.12same asbox gw "GW" right of sw same as swA 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.
offsetbox b "B" right of a offset 0,-0.3flushbox b "B" below a flush leftAfter 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.
alignalign y middle a, b, cspreadspread x a, b, c, dWritten 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.
->edge cl -> sv<-edge cl <- sv--edge cl -- sv<->edge cl <-> svFour 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.
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.
aspectplot p "" "" at 0,0 w 1.9 aspect 1:1w 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.
defaultdefault box {.tone-2} w 1.1default 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
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.
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.
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 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.
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}
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.
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"
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.
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 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.
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}
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.
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 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.
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 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.
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}
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.
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}
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.
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
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.
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 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.
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
.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.
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
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.
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.
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.
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}
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}
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.
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}
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}
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.
box sw "Switch" at 0,0 {.tone-1}text n "learns MAC addresses" right of sw gap 3.45edge n -> sw
box sw "Switch" at 0,0 {.tone-1}text n "learns MAC addresses" right of sw gap 3.45 -- sw {.small .muted}
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.
| class | means |
|---|---|
| .tone-1 | infrastructure: switch, router, resolver, firewall |
| .tone-2 | a legitimate host or participant |
| .tone-3 | payload: packets, protocol fields, records |
| .tone-4 | the one thing this beat is about – a solid fill, so use it sparingly |
| .accent | the attacker and the attack traffic |
| .dim | still 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}. |
| .muted | supporting 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.
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.
step attack show e, e-web, reply1, reply2, note-spoof
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.
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.
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
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.
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}
.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.
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}
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}
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.
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
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.
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.
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"
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.
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
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.
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.
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.
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}
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.
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.
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}
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}
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.
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.
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}
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.
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.
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}
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
The beats are the order the argument is made in. This order works almost every time.
Everything that is just true: the topology, the axes, the participants. One beat, no emphasis.
What happens when nothing is wrong. This is what the attack will be measured against.
The attacker, the failure, the exception. This is the beat that earns .accent and emph.
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
node lint.js <source.md> reports nothing. A warning you have decided to live with still counts as unclean.node build.js <source.md> prints no [diagram] warning, or the source carries a comment saying why the remaining one is deliberate..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.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.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.horizontal. Four phrases under four upright columns are four phrases nobody in the room reads.A key: nothing disappears, nothing becomes unreadable. The two single-colour terminal themes are the hardest to pass.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.same as, and every size difference left standing is a claim about importance the figure means to make..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.space 0, and no bar chart is.what it cannot do
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.
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.
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.
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.
A label breaks where you write \n, and nowhere else.
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.
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.
.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
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.
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.
::: 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:::
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.
::: 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}:::
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.
::: 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}:::
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.
::: 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}:::
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.
::: 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.
Passkey registration: four lifelines, nine messages and two notes, in four beats. Step through it.
::: 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.
a gallery
Network security, mostly: how a packet is addressed, a spoofed reply to a name lookup, a handshake used as a weapon, certificate chains, firewall rules, an intrusion detector and the curve that describes it. Three of these are one drawing that develops rather than three separate drawings – the same names in the same places, at different beats.
Each is shown at its final beat, which is what a printed copy gets. Open a card to read the source it was built from, exactly as it stands in the lecture file, comments and working notes included.
::: draw {unit=150x54}default box {.tone-3 .sharp} w 0.88 h 0.85box fh "Frame\nHeader" at 0,0box dh "Datagram\nHeader" right of fh gap 0 same as fhbox sh "Segment\nHeader" right of dh gap 0 same as fhbox pl "Payload" right of sh gap 0 same as fh {.paper}# The three labels climb to the right, so their leaders stay vertical and none# lies over the next. They point at the top edge rather than at the box: a# leader to the centre would end in the middle of the box's own label.text lmac "Ethernet source\nand destination\naddresses*" above fh gap 0.5 -- fh.cx,fh.top {.muted @l1}text lip "IP source\nand destination\naddresses" above dh gap 1.7 -- dh.cx,dh.top {.muted @l2}text lprt "source and\ndestination ports\n(if TCP/UDP is used)" above sh gap 2.9 -- sh.cx,sh.top {.muted @l3}text foot "*also called MAC addresses (media access control)" below fh gap 0.85 flush left {.left .muted}step ethernet show @l1 emph fhstep ip show @l2 emph dhstep ports show @l3 emph sh:::
::: draw {unit=190x54}# Diese Topologie tragen vier Figuren gemeinsam (A-08, A-12, A-13, A-14):# the same names in the same places, so the run reads as one drawing that# develops.box sw "Switch &\nRouter" at 0,0 w 0.9 h 0.9 {.tone-1}box b "B" above sw gap 1.6 offset 0.24,0 w 0.2 h 0.7 {.tone-2}box a "A" above sw gap 0.55 offset -0.32,0 same as b {.dim}edge awire a.bottom -- a.cx,sw.top {.muted}edge bwire b.bottom -- b.cx,sw.top {.muted}box rt "Router" at 2.30,0 w 0.72 h 0.9 {.tone-1 @net}box res "" above rt gap 1.6 w 0.2 h 0.7 {.tone-1 @net}box web "" below rt gap 1.75 same as res {.tone-2 @net}align y middle b, resedge trunk sw -- rt {.muted}edge rwire res.bottom -- rt.top {.muted}edge uplink rt.bottom -- web.top {.muted}text brk "//" between rt,web pad 0.12 {.paper .muted @net}text bmac "eb:99:f1:f3:1f:f2\n10.1.1.5" above b gap 0.3 {.mono .muted}text gwl "default gateway\n10.1.1.1\n00:ac:c1:11:15:11" below sw gap 0.3 {.muted}text resl "41.1.2.1\nDNS Resolver of ISP" above res gap 0.28 {.muted @net}text webl "Webserver\nexample.com\n80.5.5.3" below web gap 0.28 {.muted @net}# The zone boundary is a separation rather than a relation: a headless dotted# edge between two coordinates, touching no element.text zoneh "Home Network\n(10.1.1.1–254)" above bmac gap 0.5 offset 0.55,0 {.muted .serif}text zonei "Internet" above resl gap 0.5 offset -0.5,0 {.muted .serif}align y middle zoneh, zoneiedge rt.left-0.52,zoneh.top-0.2 -- rt.left-0.52,webl.bottom+0.2 {.dotted .muted}step internet show @netstep knows emph res, swstep wants emph web dim res, sw:::
::: draw {unit=190x54}# The same topology as #ns-a08, #ns-a12 and #ns-a13, the same names and# places. All that is new is who the attacker is: A steps back and the box at# the bottom left of the internet gets a name.box sw "Switch &\nRouter" at 0,0 w 0.9 h 0.9 {.tone-1}box b "B" above sw gap 1.6 offset 0.24,0 w 0.2 h 0.7 {.tone-2}box a "A" above sw gap 0.55 offset -0.32,0 same as b {.dim}edge awire a.bottom -- a.cx,sw.top {.muted}edge bwire b.bottom -- b.cx,sw.top {.muted}box rt "Router" at 2.30,0 w 0.72 h 0.9 {.tone-1}box res "" above rt gap 1.6 w 0.2 h 0.7 {.tone-1}box web "" below rt gap 1.75 same as res {.tone-2}align y middle b, resedge trunk sw -- rt {.muted}edge rwire res.bottom -- rt.top {.muted}edge uplink rt.bottom -- web.top {.muted}text brk "//" between rt,web pad 0.12 {.paper .muted}box awb "" left of web gap 5.45 same as web {.accent}box e "" left of awb gap 2.35 same as web {.dim}edge e -- awb {.muted}edge awb -- web {.muted}text bmac "eb:99:f1:f3:1f:f2\n10.1.1.5" above b gap 0.3 {.mono .muted}text gwl "default gateway\n10.1.1.1\n00:ac:c1:11:15:11" below sw gap 0.3 {.muted}text resl "DNS Resolver\nof ISP" above res gap 0.28 {.muted}text webl "Webserver\nexample.com\n80.5.5.3" below web gap 0.28 {.muted}text awbl "Attacker's Webserver\n“example.com”\n66.9.9.6" below awb gap 0.28 {.muted}edge forged b.cx,sw.top -> b.bottom {.accent}text forgedl "“example.com\nis 66.9.9.6”" above sw gap 0.45 offset 0.24,0 pad 0.14 {.paper .accent .mono @spoof}edge poison res.cx,rt.top -> res.bottom {.accent}text poisonl "“example.com\nis 66.9.9.6”" between rt,res pad 0.14 {.paper .accent .mono @cache}edge rt.left-0.52,resl.top-0.3 -- e.cx-0.45,gwl.bottom+0.28 via rt.left-0.52,gwl.bottom+0.28 {.dotted .muted}step offpath label e "E" style e {.accent .paper} emph estep race hide bwire show forged, @spoof dim estep cache hide rwire show poison, @cache emph poison:::
::: draw {unit=96x74}# Again with no world map, for the same reasons as the figure before. The# dashed boxes with the question mark are the "faked sources": what is not# genuine about them is the outline. Text verbatim from the original; only the# line breaks in the box on the right are re-set - the original breaks# "proto-cols" mid-word there, because its frame ran out.default box {.accent} w 0.44 h 0.34box vic "Victim" at 0,0 w 1.05 h 0.62 {.tone-4 !accent}box q1 "?" at -2.6,-1.5 {.dashed @fake}box q2 "?" at -1.0,-2.2 {.dashed @fake}box q3 "?" at 1.1,-2.0 {.dashed @fake}box q4 "?" at 2.7,-1.1 {.dashed @fake}box q5 "?" at 2.9,0.9 {.dashed @fake}box q6 "?" at 1.0,2.1 {.dashed @fake}box q7 "?" at -1.2,2.0 {.dashed @fake}box q8 "?" at -2.9,0.75 {.dashed @fake}edge q1 -> vic.left:0.25 {.accent @spoofed}edge q2 -> vic.top:0.35 {.accent @spoofed}edge q3 -> vic.top:0.65 {.accent @spoofed}edge q4 -> vic.right:0.25 {.accent @spoofed}edge q5 -> vic.right:0.6 {.accent @spoofed}edge q6 -> vic.bottom:0.65 {.accent @spoofed}edge q7 -> vic.bottom:0.35 {.accent @spoofed}edge q8 -> vic.left:0.7 {.accent @spoofed}text fs "Faked sources" right of q5 gap 0.6 -- q5 {@fake}box atk "Attacker" at -4.3,2.6 w 0.95 h 0.55 {@real}edge real-traffic atk -> vic.bl {.accent .thick @real}# The label sits on the line rather than beside it, and .paper knocks the line# out for it - otherwise line and word read as one pattern.text tr "Traffic of the DoS attack" between atk,vic pad 0.14 {.paper .accent @real}text loc "Attacker's real location\nis unknown (IP spoofing)." below atk gap 0.4 {.muted @real}box why "Faking Src IP possible for\nconnectionless protocols,\ne.g., ICMP (“ping”) or\nprotocols using UDP (DNS, NTP)" at 5.05,-1.7 w 2.8 h 1.2 {.clear @fake}text note1 "Victim (and ISPs) cannot filter the DoS\ntraffic if it resembles legitimate traffic." at -4.6,4.3 {.left .muted @conc}text note2 "To mitigate IP Spoofing many (all) ISPs would\nhave to perform *Ingress/Egress Filtering*.\nDifficult due to negative externality." at -0.4,4.2 {.left .muted @conc}text bcp "cf. BCP 38: Ingress Filtering" at 5.1,5.3 {.small .muted @conc}step attacker show @realstep spoofed show @fake, @spoofed emph vicstep mitigation show @conc dim @spoofed:::
::: draw {unit=112x58}# The original's typo stays: the code on the left says# print('Connected by', add) rather than addr.# The slide's block arrows are .chevron (pointing right, client -> server) and# .chevron point left (server -> client); the italic variables are written *c*# and *s*, and the compiler sets them in the accent.default box {.tone-3} w 2.35 h 0.5text cl "Client" at -1.175,-0.75 {.left}text sv "Server" at 1.175,-0.75 {.right}box syn "SYN seq=*c*" at 0,0 {.chevron}box sa "SYN+ACK seq=*s* ack=*c*+1" below syn gap 0.22 point left {.chevron @two}box ack "ACK seq=*c*+1 ack=*s*+1" below sa gap 0.22 {.chevron @three}box data "DATA" below ack gap 0.62 {.chevron .tone-4 @data}text state "Server stores *state* (e.g., seq,\nIPs, ports) in memory to match\nclient's ACK previous packets." below data gap 0.62 {@why}step reply show @twostep established show @threestep data show @data emph datastep state show @why dim data:::
::: draw {unit=120x46}default box {.tone-3} w 2.3text cl "Client" at 0,0 {.large .muted}text c1 "Generate DH key pair (c, C)" right of cl gap 1.3 {.left}box ch "ClientHello\n– Supported ciphersuites\n– Public key C" below c1 gap 0.5 flush left {@hello}box a1 "" right of ch gap 1.05 flush top w 0.8 h 0.42 {.chevron @hello}text s1 "Generate DH key pair (s, S)\nCompute secret = DH(s, C)\nDerive keys = KDF(secret)" right of a1 gap 1.05 flush top {.left @hello}text sv "Server" above s1 gap 0.5 {.large .muted}align y middle cl, svbox sh "ServerHello\n– Selected ciphersuite\n– Public key S" below s1 gap 0.5 flush left {@srv}box cert "Certificate(s)" below sh gap 0 flush left {@srv}box sig "Signature over ClientHello,\nServerHello, and Certificate" below cert gap 0 flush left {@srv}box mac "MAC over ClientHello,\nServerHello, Certificate,\nand Signature" below sig gap 0 flush left {@srv}box a2 "" left of cert gap 1.05 flush top w 0.8 h 0.42 point left {.chevron @srv}text vf "Verify certificate\nVerify signature\nCompute secret = DH(c, S)\nDerive keys = KDF(secret)\nVerify MAC" left of a2 gap 1.05 flush top {.left @done}step hello show @hellostep flight show @srvstep verify show @done emph vf:::
::: draw {unit=124x50}default box {.tone-3} w 1.55box os "Browser/OS" at 0,0 {.tone-1}text st "Store with trusted\ncertificates" right of os gap 0.85 {.left .muted}box r0 "Certificate\nof a Root CA" below os gap 0.5 flush left offset 0.55,0box r1 "Certificate of an\nintermediate CA" below r0 gap 0.5 flush left offset 0.55,0box r2 "Cert. of another\nintermediate CA" below r1 gap 0.5 flush left offset 0.55,0box r3 "Certificate\nof server" below r2 gap 0.5 flush left offset 0.55,0 {.tone-4}# The staircase: vertically out of the bottom edge, down the channel left of# the next box, then horizontally onto its left edge. The start point is a# coordinate rather than an anchor, so the descent and the waypoint carry the# same x and the vertical is vertical.edge os.left+0.35,os.bottom -> r0.left via os.left+0.35,r0.cyedge r0.left+0.35,r0.bottom -> r1.left via r0.left+0.35,r1.cyedge r1.left+0.35,r1.bottom -> r2.left via r1.left+0.35,r2.cyedge r2.left+0.35,r2.bottom -> r3.left via r2.left+0.35,r3.cytext n0 "Has" at os.left+0.2,r0.cy {.right}text n1 "Signs" at r0.left+0.2,r1.cy {.right}text n2 "Signs" at r1.left+0.2,r2.cy {.right}text n3 "Signs" at r2.left+0.2,r3.cy {.right}box d0 "DigiCert High Assurance EV Root CA" at 5.7,2.30 w 2.6 h 0.42 {@real}box d1 "DigiCert SHA2 High Assurance Server CA" below d0 gap 0.5 flush left offset 0.3,0 same as d0 {@real}box d2 "github.com" below d1 gap 0.5 flush left offset 0.3,0 same as d0 {.tone-4 @real}edge d0.left+0.2,d0.bottom -> d1.left via d0.left+0.2,d1.cy {.muted @real}edge d1.left+0.2,d1.bottom -> d2.left via d1.left+0.2,d2.cy {.muted @real}step anchor show r0step chain show r1, r2, r3step real show @real emph d2:::
::: draw {unit=150x30}default text {.small}text l1 "Extension\nCritical\nUsage" at 0,0 {.right .muted}text v1 "Key Usage ( 2.5.29.15 )\nYES\nDigital Signature, Key Encipherment" right of l1 gap 1.5 flush top {.left}text l2 "Extension\nCritical" below l1 gap 0.4 flush right {.right .muted}text v2 "Basic Constraints ( 2.5.29.19 )\nYES" right of l2 gap 1.5 flush top {.left}text l2b "Certificate Authority" below l2 gap 0 flush right {.right .muted}text v2b "NO" right of l2b gap 1.5 flush top {.left}text l3 "Extension\nCritical\nPurpose #1\nPurpose #2" below l2b gap 0.4 flush right {.right .muted}text v3 "Extended Key Usage ( 2.5.29.37 )\nNO\nServer Authentication ( 1.3.6.1.5.5.7.3.1 )\nClient Authentication ( 1.3.6.1.5.5.7.3.2 )" right of l3 gap 1.5 flush top {.left}# The fourth, empty row is deliberate: the value beside it runs to four lines,# and without it the next group measures its gap from a label ending three# lines higher - the grouping gap would disappear.text l4 "Extension\nCritical\nKey ID\n " below l3 gap 0.4 flush right {.right .muted}text v4 "Subject Key Identifier ( 2.5.29.14 )\nNO\n63 02 D2 5D 02 5F F7 8D D5 5A 12 9E 76 11 36 96\n86 2C 8A 48" right of l4 gap 1.5 flush top {.left}text l5 "Extension\nCritical\nKey ID\n " below l4 gap 0.4 flush right {.right .muted}text v5 "Authority Key Identifier ( 2.5.29.35 )\nNO\n51 68 FF 90 AF 02 07 75 3C CC D9 65 64 62 A2\n12 B8 59 72 3B" right of l5 gap 1.5 flush top {.left}text l6 "Extension\nCritical" below l5 gap 0.4 flush right {.right .muted}text v6 "Subject Alternative Name ( 2.5.29.17 )\nNO" right of l6 gap 1.5 flush top {.left}text l6b "DNS Name\nDNS Name" below l6 gap 0 flush right {.right .muted}text v6b "github.com\nwww.github.com" right of l6b gap 1.5 flush top {.left}text l7 "Extension\nCritical" below l6b gap 0.4 flush right {.right .muted}text v7 "Certificate Policies ( 2.5.29.32 )\nNO" right of l7 gap 1.5 flush top {.left}box ca "no signing of other keys!" right of v2b gap 5.5 flush top h 0.85 point left {.chevron .tone-4 @ca}box dom "domain(s)" right of v6b gap 5.5 flush top h 0.85 point left {.chevron .tone-4 @dom}step no-ca show @ca emph l2b, v2bstep domains dim l2b, v2b show @dom emph l6b, v6b:::
::: draw {unit=132x78}# Every label verbatim from the original - including "publically reachable",# which is spelled exactly that way in the original and stays. The line break# in "demilitarized zone (DMZ)" is only a break in the original (hyphenated# there), not different wording.## Three shapes the language does not have, and their stand-ins, the same on# all three slides: the cylinder (web/database/file server) is a .round box in# the storage tone, the cloud is a .round box "Internet", and the monitor# symbols are labelled boxes under the group caption "desktops".default text {.muted}box net "Internet" at 0,0 w 0.8 {.round .dim}box fw1 "FIREWALL" at 1.35,0 w 0.3 h 1.6 {.turn .tone-1}box fw2 "FIREWALL" at 3.35,0 w 0.3 h 1.6 {.turn .tone-1}box sw "SWITCH" at 4.85,0 w 0.95 {.tone-1}box web "web\nserver" at 2.35,1.95 w 0.82 {.round .tone-3}box db "database\nserver" at 4.25,1.95 w 0.92 {.round .tone-3}box fs "file\nserver" at 5.45,1.95 w 0.82 {.round .tone-3}box d1 "desktop" at 4.25,-1.95 w 0.82 {.tone-2}box d2 "desktop" at 5.45,-1.95 same as d1 {.tone-2}text dl "desktops" between d1,d2 offset 0,-0.6# The bus: every edge runs vertically out of its box, horizontally along the# channel and vertically into the switch - no diagonals, and that the last# pieces lie on each other is exactly the original's T.edge net -- fw1edge fw1 -- fw2edge fw2 -- swedge web.top -- web.cx,0edge d1.bottom -- sw.top via d1.cx,-1.0 sw.cx,-1.0edge d2.bottom -- sw.top via d2.cx,-1.0 sw.cx,-1.0edge db.top -- sw.bottom via db.cx,1.0 sw.cx,1.0edge fs.top -- sw.bottom via fs.cx,1.0 sw.cx,1.0text pub "publically\nreachable" at 1.66,-2.15 {.left @zone}text dmz "demilitarized\nzone (DMZ)" at 1.66,-1.35 {.left @zone}text tru "trusted hosts" at 4.85,2.75 {@zone}container perim "" over fw1,fw2,sw,web,db,fs,d1,d2,dl,pub,dmz,tru {.muted}# The dashed zone separator is a headless edge between two coordinates. It# runs through fw2 - boxes are drawn after edges and cover it there, exactly# as in the original.edge zsep fw2.cx,perim.top -- fw2.cx,perim.bottom {.dashed .muted @zone}step zones show @zonestep exposed style web {.tone-4} emph web, fw1step trusted dim web, fw1 emph fw2:::
::: draw {unit=150x62}default box {.tone-1}# The backbone: uplink, firewall, switch. The original's pale blue full-bleed# background is dropped; what is accented is the sensors.dot ext "" at 0,0 r 0.07 {.muted}box fw "FIREWALL" right of ext gap 3.85 w 0.34 h 1.55 {.turn}box sw "SWITCH" right of fw gap 3.85 w 0.85 h 0.44edge w1 ext -- fw.leftedge w2 fw.right -- sw.left# The sensors sit on the wire rather than beside it.box n1 "NIDS" between ext,fw w 0.7 h 0.44 {.hex .tone-4 @nids}box n2 "NIDS" between fw,sw same as n1 {.hex .tone-4 @nids}# Workstations above the switch, on one shared strand. Labelled as in# #ns-b04 rather than left empty: a box with no word reads as a mistake, and# the two figures show the same network.box d2 "desktop" above sw gap 1.25 w 0.6 h 0.44 {.tone-2 @hosts}box d1 "desktop" left of d2 gap 1.45 same as d2 {.tone-2 @hosts}text dlab "desktops" left of d1 gap 1.2 -- d1 {.muted @hosts}edge k2 d2.bottom -- sw.top {@hosts}edge k1 d1.bottom -- sw.cx,d1.bottom+0.42 via d1.cx,d1.bottom+0.42 {@hosts}# Two servers. The original's cylinder is a .round box in the server tone.box web "Web server" below n1 gap 1.3 w 1.0 h 0.5 {.round .tone-3 @hosts}box db "DB server" below sw gap 1.3 offset -1.9,0 same as web {.round .tone-3 @hosts}align y middle web, dbedge wl web.top -- web.cx,ext.cy {@hosts}edge dl db.right -- sw.bottom via sw.cx,db.cy {@hosts}box n3 "NIDS" right of db gap 1.2 same as n1 {.hex .tone-4 @nids}box h1 "HIDS" above d1 gap 0.5 same as n1 {.hex .tone-4 @hids}box h2 "HIDS" above d2 gap 0.5 same as n1 {.hex .tone-4 @hids}box hw "HIDS" below web gap 0.45 same as n1 {.hex .tone-4 @hids}box hd "HIDS" below db gap 0.45 same as n1 {.hex .tone-4 @hids}step hosts show @hostsstep nids show @nidsstep hids show @hids:::
::: draw {unit=150x58}# The string under the columns is verbatim from the original, letter-spaced:# "t / p r e n . ; l m o b". The second string splits on spaces, so one label# per column. The values are chosen so that the bins on #ns-b57 - the same# packet, the same columns - come to exactly the counts 43 / 36 / 21 taken# verbatim there.bars obs "20,12,11,10,9,9,8,8,7,6,5,4" "t / p r e n . ; l m o b" at 0,0 w 3.1 h 0.85 {.tone-3 .bare}text hcmp "Comparison with normal behavior" below obs gap 0.62 flush left {.left}# The frame is normalised on both axes: the character's rank across, its# relative frequency down. The slide labels only the horizontal, and with the# string itself, so that stands as the axis title.plot cmp ".ie0lo1/a35M6rckn()tW…" below hcmp gap 0.5 flush left w 3.1 h 1.5 x 0,1 y 0,1 tick 0.5# Normal behaviour runs as a smooth curve through its waypoints.edge normal cmp@0.01,cmp@0.6 -- cmp@1,cmp@0.03 via cmp@0.1,cmp@0.4 cmp@0.25,cmp@0.26 cmp@0.45,cmp@0.15 cmp@0.7,cmp@0.07 {.smooth .thick}# The suspicious packet's needles: a second run of columns standing on the# plot's baseline and filling its width. 24 rather than the original's ~40# needles - the point is the contrast, not the count.# .tone-4 is the only full fill there is, and it mixes from --emph: a needle# 6 px wide with a bare stroke would be empty inside.bars sus "20,14,55,10,8,45,12,88,9,62,7,6,10,5,18,4,6,5,3,4,3,2,3,2" at cmp@0.5,cmp@0.425 w 3.1 h 1.275 space 0.085 {.tone-4 .bare @sus}text ls "/" above sus-2 gap 0.06 {@sus}text lr "r" above sus-5 gap 0.06 {@sus}text lt "t" above sus-7 gap 0.06 {@sus}text lp "p" above sus-9 gap 0.06 {@sus}# Both labels lie over gridlines and so get a ground of their own, which# knocks the line out behind them.text nnorm "normal behavior" at cmp@0.66,cmp@0.42 pad 0.12 {.paper}text nsus "suspicious packet" at cmp@0.62,cmp@0.86 pad 0.12 {.paper .hand @sus}text ask "Suitable distance metric?" below cmp gap 0.92 flush left {.hand @ask}step normal show normal, nnormstep suspicious show @sus emph @susstep metric show @ask dim normal:::
::: draw {unit=150x56}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.5 h 0.9 {.tone-3 .bare}text cap "Char. freq. distribution for\nnormal payloads" above f gap 0.16 flush right {.right}brace b1 over f-0,f-1,f-2 side bottom "Bin 1" pad 0.45 {.muted @bins}brace b2 over f-3,f-4,f-5,f-6 side bottom "Bin 2" pad 0.45 {.muted @bins}brace b3 over f-7,f-8,f-9 side bottom "Bin 3" pad 0.45 {.muted @bins}text bd "…" at f.right+0.3,f.bottom+0.78 {.muted @bins}# The weights stand under the bin names rather than under the brace: a brace# measures itself without its label, and one line higher the number would lie# on the row of characters.text w1 "0.20" at b1.cx,f.bottom+1.12 {@weights}text w2 "0.18" at b2.cx,f.bottom+1.12 {@weights}text w3 "0.12" at b3.cx,f.bottom+1.12 {@weights}text ws "Σ=1.0" at f.right+0.3,f.bottom+1.12 {@weights}step bins show @binsstep weights show @weights emph @weights:::
::: draw {unit=62x62}# The ten labelled packets lie on an axis: the anomaly score the sensor# computes across (the chi-squared of the slide before), the true class down.# Only that lets the threshold separate anything at all - in the version# before, the markers stood side by side as a block and the rule ran past# them rather than through them.# The unit is square, so that a packet box comes out square.text ds "Labeled dataset (e.g., by DARPA/Lincoln Labs)" at 0,0 {.left}text latt "attack traffic" below ds gap 1.0 flush left {.left}text lben "benign traffic" below latt gap 0.5 flush left {.left}# Grid slot to grid slot is 0.74 and a box measures 0.42, so the space# between is three quarters of a box wide and the threshold fits visibly# between them without touching one. The order is the sort by anomaly score -# attacks lie higher on average but overlap, and that overlap is the subject.# Grid slots: benign 0,1,2,3,5,7 - attack 4,6,8,9.box b1 "" right of lben gap 0.55 w 0.42 h 0.42 {.tone-2 .sharp}box b2 "" right of b1 gap 0.3 same as b1 {.tone-2 .sharp}box b3 "" right of b2 gap 0.3 same as b1 {.tone-2 .sharp}box b4 "" right of b3 gap 0.3 same as b1 {.tone-2 .sharp}box b5 "" right of b4 gap 1.05 same as b1 {.tone-2 .sharp}box b6 "" right of b5 gap 1.05 same as b1 {.tone-2 .sharp}box a1 "" at b1.cx+2.96,latt.cy same as b1 {.accent .sharp}box a2 "" right of a1 gap 1.05 same as b1 {.accent .sharp}box a3 "" right of a2 gap 1.05 same as b1 {.accent .sharp}box a4 "" right of a3 gap 0.3 same as b1 {.accent .sharp}edge axis b1.left-0.5,b1.bottom+0.7 -> a4.right+0.6,b1.bottom+0.7 {.muted}text axn "anomaly score" at b1.cx+3.33,b1.bottom+1.12 {.muted}text lno "no alert" at b1.cx+0.37,b1.bottom+0.34 {.muted}text lal "alert" at a4.cx-0.37,b1.bottom+0.34 {.muted}# The threshold itself is the label, and the line hangs off it: a step moves# "t", the layout is worked out again, and the rule follows. The old version's# double-headed arrow, which was there to say the rule can move, is therefore# unnecessary - now it moves.text tlbl "t" at a1.cx+0.37,a1.top-0.45 pad 0.12 {.paper .hand @thr}edge thr tlbl.cx,tlbl.bottom -- tlbl.cx,b1.bottom+0.7 {.thick @thr}# The 2x2 matrix. The attack row carries the accent, the normal row the tone# for legitimate traffic - the same assignment as everywhere else.# The columns stand as the axis above them does: no alarm left of t, alarm# right of it. So FN sits left of TP and not the other way round.box fn "FN" at ds.left+1.2,b1.bottom+3.05 w 1.3 h 0.9 {.accent}box tp "TP" right of fn gap 0 same as fn {.accent}box tn "TN" below fn gap 0 same as fn {.tone-2}box fp "FP" right of tn gap 0 same as fn {.tone-2}# The number is in the cell's own label rather than a row of small markers# beside it: it changes in every beat, and a "label" step swaps in a variant# typeset at build time for it. Counting four squares from the back row of a# lecture hall takes longer than reading.text cno "no alert" above fn gap 0.28text cal "alert" above tp gap 0.28text head "REACTION OF IDS" above cno gap 0.3 flush left {.bold .left}text rowa "attack" left of fn gap 0.25 {.turn}text rown "normal" left of tn gap 0.25 {.turn}text rates "TP rate: 0.75 / FP rate: 0.33" at tp.right+2.1,fn.bottom-0.25 {.bold @thr}# The takeaway belongs to the last beat and therefore to the handout too: the# relationship the next slide draws as a ROC curve.text tnote "moving t moves both rates" at rates.cx,rates.bottom+0.6 {.hand}step threshold show @thr emph thr label fn "FN\n1" label tp "TP\n3" label tn "TN\n4" label fp "FP\n2"step stricter move tlbl by 2.22,0 label fn "FN\n2" label tp "TP\n2" label tn "TN\n6" label fp "FP\n0" label rates "TP rate: 0.50 / FP rate: 0.00"step lenient move tlbl by -4.44,0 label fn "FN\n0" label tp "TP\n4" label tn "TN\n2" label fp "FP\n4" label rates "TP rate: 1.00 / FP rate: 0.67"step tradeoff move tlbl by 2.22,0 show tnote label fn "FN\n1" label tp "TP\n3" label tn "TN\n4" label fp "FP\n2" label rates "TP rate: 0.75 / FP rate: 0.33":::
::: draw {unit=104x104}# "False Postive Rate" is the original slide's typo and stays.# The unit is square, so that the ROC frame comes out square.plot roc "False Postive Rate" "True Positive Rate" at 0,0 w 2.6 h 2.45 x 0,1 y 0,1 tick 0.2edge curve roc@0.02,roc@0.03 -- roc@0.98,roc@1 via roc@0.06,roc@0.5 roc@0.2,roc@0.8 roc@0.49,roc@0.95 roc@0.75,roc@0.98 {.smooth .thick}# The labels sit under their point rather than to its right: the curve is to# the right, and a .paper ground would otherwise knock it out.dot pstrict "" at roc@0.06,roc@0.5 r 0.055 {.tone-4 @strict}text lstrict "Strict threshold" at roc@0.3,roc@0.43 pad 0.12 {.bold .paper @strict}dot pmod "" at roc@0.2,roc@0.8 r 0.055 {.tone-4 @moderate}text lmod "Moderate threshold" at roc@0.5,roc@0.71 pad 0.12 {.bold .paper @moderate}dot plen "" at roc@0.49,roc@0.95 r 0.055 {.tone-4 @lenient}text llen "Lenient threshold" at roc@0.76,roc@0.86 pad 0.12 {.bold .paper @lenient}step curve show curvestep strict show @strictstep moderate show @moderatestep lenient show @lenient:::
::: draw {unit=70x70}# 96 faces out of four grids rather than 96 lines: the rule is one line per# grid, the exception a grid of its own. The asset is embedded once, however# often it appears.grid sickp image face-bad 7x1 at 0,0 cell 0.3 space 0.08 {@tp}grid sickn image face-ok 1x1 right of sickp gap 0.1 cell 0.3 space 0.08 {@fn}grid well image face-ok 7x11 below sickp gap 0.32 flush left cell 0.3 space 0.08 {@wellneg}grid fpos image face-bad 1x11 right of well gap 0.1 flush top cell 0.3 space 0.08 {@fpos}container zsick "" over sickp,sickn pad 0.09 {.accent .sharp}container zwell "" over well,fpos pad 0.09 {.sharp}text lsick "sick" left of sickp gap 0.3 {.right}text lwell "healthy" left of well gap 0.3 {.right}text rates "TP rate: 87.5% FP rate: 12.5%\nactually sick: 8.3%" above sickp gap 0.35 flush left {.left .muted}# The legend stands halfway down the board rather than at the top: otherwise# the bottom right half of the figure would stay empty.image legb face-bad right of fpos gap 1.1 offset 0,-0.3 w 0.3text tlegb "test positive (you are worried)" right of legb gap 0.2 {.left}image lego face-ok below legb gap 0.35 same as legbtext tlego "test negative (you feel safe)" right of lego gap 0.2 {.left}text ask "Should you be?" below tlego gap 0.55 flush left {.hand @ask}# The beats build the argument up rather than dimming it away. The first used# to be called "positives" and left the one missed sick case standing bright,# and the second was called "alarms" and cleared the sick row entirely - so at# the end only the *false* alarms were lit, the exact opposite of the name.step detected emph @tp dim @fn, @wellneg, @fposstep flagged emph @fpos dim @tpstep alarms emph @tp, @fpos dim @fn, @wellneg label rates "TP rate: 87.5% FP rate: 12.5%\nactually sick: 8.3%\n18 alarms, 7 of them real: 39%"step worried show @ask:::