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.
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.