Skip to content

Drawing Figures in Code

Every figure on this site comes from a short Python script or a WaveDrom file. Nothing is drawn by hand, so fixing a figure means fixing its source and rendering it again.

Three kinds of source

  • Block diagrams and flowcharts are Python generators. Each one places its blocks on a grid and writes a draw.io file, which draw.io exports to SVG from the command line.
  • Timing diagrams are WaveDrom files, rendered with wavedrom-cli.
  • Charts are matplotlib, saved as SVG with the text left as text.

One script, render.py, renders every source for an article in one go.

On a grid

The timing path figure in Part 2 is a good one to look at.

Launch FF
launch edge
Logic + routing
Capture FF
capture edge
clock-to-Q
arrives at D
data path
Clock source
launch clock path
capture clock path
The capture flip-flop and the data path carry the accent, and the clock tree stays plain.

The generator opens with the one claim the figure makes: every timing path is a launch flip-flop, a data path and a capture flip-flop, with both flip-flops clocked from one source over two different clock paths. Then it computes every coordinate from a handful of sizes:

FF_W, FF_H, Y = 124, 72, 40
MW, MH = 150, 56
GUT = 128  # gutter: room for a wire label
LX = 20
MX = LX + FF_W + GUT
CX = MX + MW + GUT
mid = Y + FF_H / 2

f.block(LX, Y, FF_W, FF_H, "Launch FF", sub="launch edge")
f.block(MX, Y + (FF_H - MH) / 2, MW, MH, "Logic + routing")
f.block(CX, Y, FF_W, FF_H, "Capture FF", sub="capture edge", kind="accent")

f.wire([(LX + FF_W, mid), (MX, mid)], kind="accent", label="clock-to-Q")
f.wire([(MX + MW, mid), (CX, mid)], kind="accent", label="arrives at D")

Change FF_W, and the logic block, the capture flip-flop and both wires move with it. A wire that carries a label gets a gutter of at least 120 px, here GUT = 128.

Colors the page fills in

A generator never writes a real color. It paints with twelve placeholders, and a second script, themesvg.py, swaps each one for a CSS variable that the site’s stylesheet sets once for light mode and once for dark. Three of the twelve:

Placeholder Paints Light, dark
#010101 text #111417
#ededee
#fe0202 the accent block #e3f3ea
#10291d
#fe0303 the data path #00804a
#4ee6a0

WaveDrom paints with its own fixed colors, so those get mapped onto the same variables. A WaveDrom color with no mapping stops the render with an error.

Inline in the page

At build time the SVG goes straight into the page’s HTML. Its text is set in Inter and Fira Code like the words around it, and its colors come from the page. Switch the theme in the header and the figure above switches with it.

Each figure’s IDs get their own prefix, so several figures can share a page. And since WaveDrom sizes its name column for Helvetica, themesvg.py measures each name in Fira Code and widens the column.

Checked at the size you read it

preview.py drops each figure into an article column 685 px wide for a laptop and 358 px wide for a phone, with the site’s own stylesheets and fonts, and screenshots it in light and dark. Anything in those screenshots that collides, clips or reads too small goes back to the generator.

Figures aim for 560 to 700 px wide. In the 685 px column that’s close to 1:1, and their 11 to 13 px text stays readable. On a phone, a wide figure keeps a minimum width and scrolls sideways like a code block, instead of shrinking its labels to 5 px.

The rules

  • One claim per figure, written in the generator’s docstring before a single block goes down.
  • Names, values and directions come from the article’s text.
  • The alt text lives in the source, and the render stops if it’s missing.
  • Sentence case, no all-caps labels.