Skip to main content

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.1". 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.1"]

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_page fails if pypdf ever returns the whole document for every page, which would make the text assertions vacuous.
  • test_bracket_footnotes_do_land_at_the_end is the control for the footnotes-end-of-document == 0 check. It also pins the gotcha: under theme: 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_fail is 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

tufte_mpl-0.1.1.tar.gz (16.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tufte_mpl-0.1.1-py3-none-any.whl (19.5 kB view details)

Uploaded Python 3

File details

Details for the file tufte_mpl-0.1.1.tar.gz.

File metadata

  • Download URL: tufte_mpl-0.1.1.tar.gz
  • Upload date:
  • Size: 16.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tufte_mpl-0.1.1.tar.gz
Algorithm Hash digest
SHA256 2f4a58f16f47f2f953912a18220947d0a6086d50c510ff8a444b1fdd801bba44
MD5 77aa85ba84e10b4c703335c5a17f0466
BLAKE2b-256 c14b94f5c87a544ef58a6662cda998f99817660fcc052bd987b2226792eb8066

See more details on using hashes here.

Provenance

The following attestation bundles were made for tufte_mpl-0.1.1.tar.gz:

Publisher: publish.yml on GarrettMooney/tufte-mpl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tufte_mpl-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: tufte_mpl-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 19.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tufte_mpl-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 80ae1351bc0dcd13ba77d23241adc2bc1b9aa2683d408b2d13c78fa47461693f
MD5 a9a6565c682098838d6324bd94346188
BLAKE2b-256 851b04073d6f3208746a8320b401c76e59112516f20b98fc480e635c186fe6e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for tufte_mpl-0.1.1-py3-none-any.whl:

Publisher: publish.yml on GarrettMooney/tufte-mpl

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page