tufte-mpl
Tufte house style for matplotlib figures, Quarto HTML documents, and leadership PDFs. One palette, one stylesheet, one set of helpers.
Why this exists
The style had spread by copying. A census on 2026-08-13 found:
| Artifact | Copies | Distinct versions |
|---|---|---|
| matplotlib helper | 7 | 3, plus one independent reimplementation |
tufte.css |
15 | 4 |
| PDF scaffold | 2 | 1, drifted |
Copying cost three things:
The palette forked. One lineage drew ink at #1a1a1a and the accent at
#8c2d04; the other used #111111 and #a00000. Figures from the two
lineages did not match, though the call sites were identical.
A fixed bug kept shipping. Five stylesheets predate the .accent class.
In those directories [text]{.accent} renders as body text, so every inline
defect mark disappears silently. Copies receive no upgrades.
The stable layer drifted anyway. The PDF template marks 184 lines KEEP
AS-IS. Its one derivative changed 22 of them: padding, leading, VALIGN, table
font size. Code that must not change belongs in a library.
Install
uv add tufte-mpl # figures and the Quarto stylesheet
uv add "tufte-mpl[pdf]" # adds the leadership-PDF layer (reportlab)
Pin a filed report or an archived analysis, so it renders the same way next
year: uv add "tufte-mpl==0.1.0". For development against a checkout, use
uv add --editable path/to/tufte-mpl.
Rendering Quarto documents with Python chunks also needs matplotlib,
ipykernel, nbformat, nbclient and pyyaml in the interpreter you point
QUARTO_PYTHON at. All four of the last are required: Quarto's own jupyter.py
imports yaml, so ipykernel alone fails with a bare ModuleNotFoundError.
Figures
import matplotlib.pyplot as plt
import tufte_mpl
tufte_mpl.setup()
fig, ax = plt.subplots(figsize=tufte_mpl.figsize())
ax.plot(x, y, color=tufte_mpl.INK)
tufte_mpl.range_frame(ax, x=x, y=y) # spines span the data, not the box
tufte_mpl.label_at_end(ax, x[-1], y[-1], "series") # direct label, no legend
tufte_mpl.note(ax, (8, 12), (5, 12), "the anomaly") # accent, with a leader
setup() applies the rcParams globally. rcparams() returns them as a dict if
you would rather inspect or override them first.
Quarto documents
tufte init . # writes tufte.css and a starter .qmd
quarto render doc.qmd --to html && open doc.html
Quarto reads its stylesheet from disk, so tufte init still copies the CSS. It
copies from one versioned source, and it refuses to overwrite a file you have
edited unless you pass --force. Rerun it to learn whether a directory has
drifted.
Python chunks need an interpreter carrying the render dependencies listed under Install:
QUARTO_PYTHON=.venv/bin/python quarto render doc.qmd --to html
Leadership PDFs
tufte report build_report.py
uv run build_report.py
The scaffold holds only what changes per report: compute(), the chart_*()
functions, and the story. Page geometry, paragraph styles, FigureBlock,
callout(), data_table(), and the footer come from tufte_mpl.pdf.
reportlab ships no Palatino, so PDFs set in Times while HTML figures set in Palatino. The palette is shared; the typeface is not.
The palette
| Name | Value | Use | Contrast on paper |
|---|---|---|---|
INK |
#111111 |
primary series, body text | 18.8:1 |
MUTED |
#6b6b63 |
context series, direct labels | 5.4:1 |
FAINT |
#93938d |
axis ticks and other marks a reader must resolve | 3.1:1 |
RULE |
#b7b7af |
hairline separators, decorative only | 2.0:1 |
ACCENT |
#a00000 |
the anomaly under discussion, never decoration | 8.4:1 |
PAPER |
#fffff8 |
the page |
Two greys replace the single #d8d8cf the house style used before. That value
measured 1.4:1 and lost axis ticks on a dim screen.
tests/test_palette.py asserts these properties rather than trusting them. It
simulates protanopia, deuteranopia, and tritanopia, and it requires the accent
to stay at least ΔE 10 from every grey under each. The suite carries a positive
control, because the first control written for it never fired: no grey collapses
onto a red for any dichromat, so a red-against-grey test can never fail. A dark
red against a dark olive does fail, which proves the measurement works.
Grayscale plus one accent is safe by construction. A reader with colour-vision deficiency is never asked to separate two hues, only a hue from a grey.
Archiving a directory
An analysis filed for the record should render the same way next year. Pin the version:
dependencies = ["tufte-mpl==0.1.0"]
Or cut the dependency entirely:
tufte vendor . # writes a self-contained tufte_mpl/ package
Tests
uv run pytest # everything, ~5s
uv run pytest -m "not integration" # skips the Quarto renders, for CI without it
Nothing at an external boundary is mocked. The reportlab tests build real PDFs and read the bytes back with pypdf; the Quarto tests run the real binary and assert on the rendered HTML. A fake would satisfy either contract while the real engine refused.
Three assertions carry a control, because each would otherwise pass while measuring nothing:
test_the_extractor_reads_distinct_text_per_pagefails if pypdf ever returns the whole document for every page, which would make the text assertions vacuous.test_bracket_footnotes_do_land_at_the_endis the control for thefootnotes-end-of-document == 0check. It also pins the gotcha: undertheme: none,[^1]renders as an end-of-page list rather than a sidenote. When it starts failing, Quarto has fixed that and the guidance can relax.test_the_metric_can_failis the control for the colour-vision thresholds.
Two mutations were run by hand to confirm the suite bites. Drawing the figure
caption at x=0 instead of in the margin fails the margin test; deleting the
.accent rule from the stylesheet fails the accent test, which is the exact bug
still live in five copied stylesheets.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tufte_mpl-0.1.0.tar.gz.
File metadata
- Download URL: tufte_mpl-0.1.0.tar.gz
- Upload date:
- Size: 16.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc7939214c85adf290a6157cc911623a378e452acfccf686b93cf89262f387cb
|
|
| MD5 |
428d1eb11439f4c7c8ee91e976db002d
|
|
| BLAKE2b-256 |
25251dd98b0bc3031d5f255188875293b80d967e917a4f73bad3369e3b142b05
|
Provenance
The following attestation bundles were made for tufte_mpl-0.1.0.tar.gz:
Publisher:
publish.yml on GarrettMooney/tufte-mpl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tufte_mpl-0.1.0.tar.gz -
Subject digest:
bc7939214c85adf290a6157cc911623a378e452acfccf686b93cf89262f387cb - Sigstore transparency entry: 2467102641
- Sigstore integration time:
-
Permalink:
GarrettMooney/tufte-mpl@da6522fc27c0a2a0c9ba6452f587f0e67ee72f35 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GarrettMooney
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@da6522fc27c0a2a0c9ba6452f587f0e67ee72f35 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tufte_mpl-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tufte_mpl-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9962b9d7da20d870590de1553ebe1982ef8a6f3f9e1b42a78b8a80748a0bd304
|
|
| MD5 |
3b9e3ce84ae81dd7415d1a19c01453e6
|
|
| BLAKE2b-256 |
53359efbd274bd805b160a51d3dd6cb1ea7e568ca2c3c499944a7ae203ce5cea
|
Provenance
The following attestation bundles were made for tufte_mpl-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on GarrettMooney/tufte-mpl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tufte_mpl-0.1.0-py3-none-any.whl -
Subject digest:
9962b9d7da20d870590de1553ebe1982ef8a6f3f9e1b42a78b8a80748a0bd304 - Sigstore transparency entry: 2467102748
- Sigstore integration time:
-
Permalink:
GarrettMooney/tufte-mpl@da6522fc27c0a2a0c9ba6452f587f0e67ee72f35 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GarrettMooney
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@da6522fc27c0a2a0c9ba6452f587f0e67ee72f35 -
Trigger Event:
push
-
Statement type: