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.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_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.0.tar.gz (16.2 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.0-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

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

Hashes for tufte_mpl-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bc7939214c85adf290a6157cc911623a378e452acfccf686b93cf89262f387cb
MD5 428d1eb11439f4c7c8ee91e976db002d
BLAKE2b-256 25251dd98b0bc3031d5f255188875293b80d967e917a4f73bad3369e3b142b05

See more details on using hashes here.

Provenance

The following attestation bundles were made for tufte_mpl-0.1.0.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.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

Hashes for tufte_mpl-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9962b9d7da20d870590de1553ebe1982ef8a6f3f9e1b42a78b8a80748a0bd304
MD5 3b9e3ce84ae81dd7415d1a19c01453e6
BLAKE2b-256 53359efbd274bd805b160a51d3dd6cb1ea7e568ca2c3c499944a7ae203ce5cea

See more details on using hashes here.

Provenance

The following attestation bundles were made for tufte_mpl-0.1.0-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

0.1.1

2 files

This release

0.1.0 This release

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