Metadata-Version: 2.4
Name: tufte-mpl
Version: 0.1.0
Summary: Tufte house style for matplotlib figures, Quarto HTML docs, and leadership PDFs
Keywords: matplotlib,tufte,quarto,reportlab,dataviz,accessibility
Author: Garrett Mooney
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Matplotlib
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Dist: matplotlib>=3.8
Requires-Dist: reportlab>=4.0 ; extra == 'pdf'
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/GarrettMooney/tufte-mpl
Project-URL: Source, https://github.com/GarrettMooney/tufte-mpl
Project-URL: Issues, https://github.com/GarrettMooney/tufte-mpl/issues
Provides-Extra: pdf
Description-Content-Type: text/markdown

# 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

```sh
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

```python
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

```sh
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:

```sh
QUARTO_PYTHON=.venv/bin/python quarto render doc.qmd --to html
```

## Leadership PDFs

```sh
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:

```toml
dependencies = ["tufte-mpl==0.1.0"]
```

Or cut the dependency entirely:

```sh
tufte vendor .    # writes a self-contained tufte_mpl/ package
```

## Tests

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