# dartwork-mpl — full agent reference (llms-full.txt)

This file is auto-generated at build time from the canonical sources
listed below. Do **not** hand-edit; edit the upstream files and rebuild
the docs (`uv run sphinx-build -b html docs docs/_build/html`).

Companions:

- `CLAUDE.md` / `AGENTS.md` — 30-second onboarding for agents.
- `llms.txt` — link index following the llmstxt.org spec.


---
## Quickstart
# Quick Start

A minimal end-to-end workflow: apply a style, create a figure, and
export it. Skim this in five minutes — you'll already know enough to
ship a publication-grade plot.

:::{tip}
A **live ruler** below this section maps `dm.fs(n)` / `dm.fw(n)` / `dm.lw(n)`
to actual point sizes and stroke widths under each preset — drag the sliders
to read off the resolved values without leaving the docs.

If the slider doesn't load (JavaScript disabled, terminal browsers,
or offline LLM contexts), the [static reference table](#fs-fallback-table)
further down lists the same base values per preset.
:::

## At-a-glance ROI

| What used to hurt                   | dartwork-mpl                                    |
| ----------------------------------- | ----------------------------------------------- |
| Hand-tuning `figsize` and `dpi`     | `plt.subplots(figsize=dm.figsize("15cm", "wide"))`  |
| `tight_layout` clipping labels      | `dm.simple_layout(fig)` — deterministic, content-aware |
| Reaching for hex codes              | `color="dc.teal3"` (the curated `dc.*` system), plus `"oc.*"`, `"tw.*"`, `"md.*"`, `"ad.*"`, `"cu.*"`, `"pr.*"` for third-party systems |
| Saving in 3 formats                 | `dm.save_formats(fig, "out", formats=("png", "svg", "pdf"))` |
| Catching margin / overflow problems | `dm.validate_with_fixes(fig)`                   |

Here's a typical matplotlib figure, then the same figure with dartwork-mpl:

::::{tab-set}

:::{tab-item} ✨ With dartwork-mpl

<!-- snippet: no-run — save_and_show writes files + opens a preview -->
```python
import matplotlib.pyplot as plt
import dartwork_mpl as dm
import numpy as np

dm.style.use("scientific")  # curated fonts, colors, line weights

# width = physical figure width, aspect = height/width ratio
fig, ax = plt.subplots(figsize=dm.figsize("15cm", "wide"))

x = np.linspace(0, 10, 200)
ax.plot(x, np.sin(x), color="dc.teal3", label="signal", lw=dm.lw(1.5))
ax.set_xticks(np.arange(0, 11, 2))
ax.set_xlabel("Time [s]")
ax.set_ylabel("Amplitude")
ax.legend()

dm.simple_layout(fig)           # content-aware margins
dm.save_and_show(fig, "first")  # save + inline preview
```

:::

:::{tab-item} 🔧 Vanilla matplotlib

```python
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(5.91, 3.94), dpi=300)

x = np.linspace(0, 10, 200)
ax.plot(x, np.sin(x), color="#1c7ed6", label="signal", lw=1.5)
ax.set_xticks(np.arange(0, 11, 2))
ax.set_xlabel("Time [s]", fontsize=7.5)
ax.set_ylabel("Amplitude", fontsize=7.5)
ax.legend(fontsize=6.5)

fig.tight_layout()
fig.savefig("output.png", dpi=300, bbox_inches="tight")
plt.show()
```

:::

::::

:::{figure} images/quickstart_first_figure.svg
:alt: Scientific-style line chart created with dartwork-mpl
:width: 100%

The same chart rendered with `dm.style.use("scientific")` and
`plt.subplots(figsize=dm.figsize(…, …))` — professional typography,
optimized margins, and named colors.
:::

**Drag the slider to compare — same data, different styling:**

```{raw} html
:file: images/compare_slider.html
```

Same data, same plotting logic — the difference is one `dm.style.use()`
call, the `width` / `aspect` arguments on `dm.figsize`, named colors,
and `simple_layout`.

**What each dartwork-mpl call does:**

| Call                                          | Purpose                                                                            |
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
| `dm.style.use("scientific")`                  | Sets palette, fonts, line weights — see [Styles](styles.md)                        |
| `dm.figsize("15cm", "wide")`                  | Physical width plus an aspect token, returned as the inches tuple `figsize=` expects. |
| `dm.fs(0)`                                    | Returns the base font size of the active preset (`fs(2)` = base + 2 pt, and so on) |
| `dm.simple_layout(fig)`                       | Deterministic content-aware margins (replaces `tight_layout`)                      |
| `dm.save_and_show(fig, "first")`              | Saves multi-format and previews inline in the notebook                             |

(fs-fallback-table)=
### Static reference: `dm.fs(n)` resolved per preset

This is the **JS-free equivalent of the live ruler at the top of the
page** — same numbers, same purpose. Useful when JavaScript is
disabled (AI agents, terminal browsers) or when copying numbers into a
spreadsheet. Each row is the **base font size** that ships with the
preset's `font-*.mplstyle`; `dm.fs(n)` returns ``base + n`` (in
points), `dm.fw(n)` and `dm.lw(n)` apply analogous offsets to font
weight and stroke width.

| Preset         | Base `font.size` (pt) | Typical `dm.fs(2)` (pt) |
| -------------- | --------------------: | ----------------------: |
| `scientific`   | 7.5                   | 9.5                     |
| `report`       | 8.0                   | 10.0                    |
| `web`          | 11.0                  | 13.0                    |
| `presentation` | 10.5                  | 12.5                    |
| `poster`       | 12.0                  | 14.0                    |
| `minimal`      | 7.5                   | 9.5                     |

Source of truth: `src/dartwork_mpl/asset/mplstyle/font-*.mplstyle`. If
those values change, regenerate this table.

## Creating Figures with `dm.figsize`

`dm.figsize(width, aspect)` is the sanctioned way to size a figure. It
returns the inch tuple matplotlib's `figsize=` argument expects, so you
keep the native `plt.subplots` / `plt.figure` constructors and never
hand-roll inches yourself.

```python
# Physical width plus an aspect token (default aspect="standard" = 3/4)
fig, ax = plt.subplots(figsize=dm.figsize("13cm"))
fig, ax = plt.subplots(figsize=dm.figsize("15cm", "wide"))
fig, ax = plt.subplots(figsize=dm.figsize(dm.cm(11.3), "square"))

# Apply a style preset separately
dm.style.use("scientific")
fig, ax = plt.subplots(figsize=dm.figsize("13cm", "wide"))

# Stacked styles
dm.style.stack(["font-report", "theme-dark"])
fig, axes = plt.subplots(2, 2, figsize=dm.figsize("17cm", "standard"))

# Academic-column shortcuts
fig, ax = plt.subplots(figsize=dm.figsize(dm.col1, "golden"))  # 9 cm
fig, ax = plt.subplots(figsize=dm.figsize(dm.col2, "cinema"))  # 17 cm
```

**Width** accepts:

- A unit-suffixed string: `"13cm"`, `"9.5cm"`, `"6.7in"`, `"170mm"`,
  `"24pt"`
- A `Length` value: `dm.cm(11.3)`, `dm.inch(4.6)`, `dm.mm(170)`,
  `dm.pt(24)`, or `dm.length("13cm")` (string parser)
- The sugar constants `dm.col1` (9 cm) and `dm.col2` (17 cm)

Bare `int` / `float` are rejected — the unit must always be explicit.

**The second argument** picks the figure's height in one of four
equivalent forms; use whichever reads naturally for the call site:

| Form                      | Example                              | Notes                                            |
| :------------------------ | :----------------------------------- | :----------------------------------------------- |
| Aspect token              | `dm.figsize("13cm", "wide")`         | One of `square / portrait / tall / standard / golden / wide / a4 / slide / cinema / panoramic`. |
| Numeric ratio             | `dm.figsize("13cm", 0.6)`            | Positive `int` / `float` interpreted as `height / width`. |
| Unit-suffix string height | `dm.figsize("13cm", "8cm")`          | Width and height units may differ.               |
| `Length` value            | `dm.figsize("13cm", dm.cm(8))`       | Useful for `dm.col1` / `dm.col2` heights.        |

Bare numeric strings (`"0.5"`) and unknown aspect tokens raise
`ValueError` with a "did-you-mean" hint, so ambiguous inputs fail
loudly.

:::{note}
The 0.4-era constructors `dm.subplots` and `dm.figure` (and their
`figsize=` / `dpi=` arguments) were removed. Calls now raise
`AttributeError` / `TypeError` with a message naming the modern
`plt.subplots(figsize=dm.figsize(...))` form. See
[`asset/prompt/01-policy.md`](https://github.com/dartworklabs/dartwork-mpl/blob/main/src/dartwork_mpl/asset/prompt/01-policy.md)
for the full lint catalog.
:::

**Multi-panel figures:**

```python
fig, axes = plt.subplots(
    2, 2,
    figsize=dm.figsize("17cm", "standard"),
    width_ratios=[2, 1],
    height_ratios=[1, 2],
)

for ax in axes.flat:
    ax.plot(np.random.randn(100))

dm.simple_layout(fig)
```

## Adding color

dartwork-mpl ships its own curated palette — `dc.*` ("dartwork color"),
a 24-palette system across 11 families (8 shades each) — and registers six
third-party design systems alongside it. Use any of them anywhere matplotlib
accepts a color string:

```python
# Curated dartwork palette (recommended starting point)
ax.plot(x, y, color="dc.teal3")                   # cool blue
ax.fill_between(x, y1, y2, color="dc.earth1")     # warm accent
ax.bar(categories, values, color="dc.vivid1")      # bold call-out

# Third-party systems also available for cross-team consistency
ax.plot(x, y2, color="oc.blue5")                   # OpenColor
ax.plot(x, y1, color="tw.emerald500")              # Tailwind
```

**Discover what's available without leaving Python:**

```python
import dartwork_mpl as dm

dm.list_palettes()[:5]      # → ['ad.blue', 'ad.cyan', 'ad.geekblue', ...]
dm.show_palette("dc.teal")  # renders the 8-shade swatch row in Jupyter
dm.plot_colors(ncols=4)     # full library overview, one figure per system
```

See [Colors and Colormaps](colors.md) for the full palette reference,
or open the [interactive palette explorer](../color_system/colors.md)
to click-and-copy color names from your browser.

## Multi-panel layout

```python
import dartwork_mpl as dm
import numpy as np

dm.style.use("presentation")

x = np.linspace(0, 10, 100)
fig = plt.figure(figsize=dm.figsize("15cm", "wide"))
gs = fig.add_gridspec(1, 2, wspace=0.3)
ax1 = fig.add_subplot(gs[0])
ax2 = fig.add_subplot(gs[1])

ax1.plot(x, np.sin(x), color="dc.vivid1")
ax2.plot(x, np.cos(x), color="dc.teal3")

dm.label_axes([ax1, ax2])  # adds (a), (b) panel labels
dm.simple_layout(fig, gs=gs)
```

:::{figure} images/quickstart_multi_panel.svg
:alt: Two-panel layout with label_axes showing sin and cos
:width: 100%
:::

## Saving in multiple formats

```python
dm.save_formats(
    fig,
    "output/my_figure",
    formats=("png", "svg", "pdf"),
    dpi=300,
    validate=True,  # auto-check for overflow, overlap, etc.
)
```

## Catch problems before you export

`validate=True` above runs the same checks as the standalone
`validate_with_fixes` helper, which can also patch the easy issues
in-place:

```python
warnings, fixes = dm.validate_with_fixes(fig)
for w in warnings:         # each is a VisualWarning with fix suggestions
    print(w)
# e.g. MARGIN_ASYMMETRY → fix: dm.simple_layout(fig)
```

Use it in CI to fail a build when a figure breaks; use it locally to
get a one-line health check before you `save_formats`.

## Try the interactive UI

If you'd rather see the effect of every parameter before committing
it to code, dartwork-mpl ships a local web app that wires sliders to
your render function and exports the resulting Python script:

```bash
# 1. scaffold a starter viewer (first time only)
pip install "dartwork-mpl[ui]"
dartwork-mpl-ui init ./my-viewer --example simple

# 2. run it — opens the viewer in your default browser
cd my-viewer && python viewer.py
```

→ [Interactive UI guide](interactive.md)

## Next steps

::::{grid} 1 1 2 3
:gutter: 2

:::{grid-item-card} 🎨 Styles and Presets
Choose the right preset for your use case — papers, reports, slides, posters.

→ [Browse presets](styles.md)
:::

:::{grid-item-card} 🌈 Colors and Colormaps
Explore 900+ named palettes and perceptual OKLCH interpolation.

→ [See palettes](colors.md)
:::

:::{grid-item-card} 📐 Layout and Typography
Panel labels, arrows, font scaling, and margin optimization.

→ [Learn layout](layout.md)
:::

:::{grid-item-card} 💾 Save and Validation
Multi-format export + automatic visual quality checks.

→ [Export guide](save_export.md)
:::

:::{grid-item-card} 🛠️ Interactive UI
Tune fonts, line weights, margins with sliders. Export the exact
script that reproduces what you see.

→ [Interactive UI](interactive.md)
:::

:::{grid-item-card} 🔬 Diagnostics & Templates
`dm.plot_colors()` / `plot_colormaps()` / `plot_fonts()` for asset
audit, plus ready-to-use plot templates like `plot_diverging_bar`.

→ [Extras guide](extras.md)
:::

::::

:::{note}
**Upgrading from a previous PyPI release?** The
[Migration Guide](../migration.md) lists every renamed / removed name
since v0.4.0 with a side-by-side `Before → After` table — most call
sites are one-liners.
:::


---
## Migration guide
---
orphan: true
---

# Migration Guide

This guide collects every rename / deprecation that has shipped on PyPI
(v0.4.0 onwards), **newest release first**. Every removed symbol now
raises `AttributeError` / `ModuleNotFoundError` / `TypeError` (or, for
palette tokens, the usual "not a valid color" error) with a message
naming its replacement.

> **New to dartwork-mpl?** You don't need this page. Head to the
> [Quick Start](usage_guide/quickstart.md) — it uses the current API
> end-to-end.

## At a glance

The moves you're most likely to hit, regardless of the version you're
coming from:

| Old surface                           | New surface                                          |
| ------------------------------------- | ---------------------------------------------------- |
| `dm.subplots(width=..., aspect=...)`  | `plt.subplots(figsize=dm.figsize(...))`              |
| `dm.figure(width=..., aspect=...)`    | `plt.figure(figsize=dm.figsize(...))`                |
| `plt.tight_layout()`                  | `dm.simple_layout(fig)`                              |
| `dm.auto_layout(fig)`                 | `dm.simple_layout(fig, margin=...)`                  |
| `dm.named("oc.red5")`                 | `dm.color("oc.red5")` (also accepts hex / `rgb(...)` / `oklch(...)` / `oklab(...)`) |
| `from dartwork_mpl.color import ...`  | `from dartwork_mpl.colors import ...` (submodule renamed) |
| `dartwork_mpl.asset_viz`              | `dartwork_mpl.diagnostics`                           |
| `dartwork_mpl.helpers.formatting`     | `dartwork_mpl.helpers.labels`                        |
| `dm.install_llm_txt()`                | `dm.get_agent_doc(name)` / `dm.agent_doc_path(name)` (or MCP `dartwork-mpl://guide/*`) |
| `dc.ocean*`, `dc.spectrum*`, `dc.focus*`, … | see [dc.* palette migration](#dc-palette-migration-cumulative) |

The per-release sections below carry the full detail and runnable
snippets.

## v5 — generative color system (Unreleased)

A 91-parameter generative palette (16 families × 10 perceptually-equalized
steps, `dc.{family}{step}`) replaces the hand-curated v4 catalog as the
default. Design spec:
`docs/superpowers/specs/2026-07-03-color-system-v5-design.md`.

1. **v4 `dc.*` tokens are frozen, not removed.** Every legacy-only token
   (`dc.vivid*`, `dc.pastel*`, `dc.0`–`dc.7`, …) keeps resolving to its
   pre-v5 hex value — existing scripts render unchanged. Accessing one
   emits a one-time `DeprecationWarning` per token; removal is planned
   no sooner than two minor releases out.
2. **Opt in to the v5 remap for the three colliding families** with
   `dm.set_palette_version(5)`. `dc.teal*`, `dc.indigo*`, and `dc.gray*`
   (steps 0–7) are the only names that exist in both catalogs; calling
   this remaps those specific tokens (and their `dm.*` alias) to their
   v5 hex in place. The default (`dm.set_palette_version(4)`, implicit)
   keeps them frozen at the legacy value. Note: the legacy ramps run
   dark→light; the v5 ramps run light→dark, so step indices invert
   meaning under version 5 (e.g. `dc.gray0` goes from near-black to
   near-white).
3. **Colormaps are matplotlib-native — no bespoke accessor.** Use
   `cmap="dc.aurora"` in any plotting call, or `plt.colormaps["dc.aurora"]`
   / `mpl.colormaps["dc.aurora"]` to fetch the `Colormap` object directly.
4. **BREAKING: `dc.aurora` / `dc.teal_rose` renamed to
   `dc.legacy_aurora` / `dc.legacy_teal_rose`.** Both names are ceded to
   the new v5 colormap catalog, which ships a differently-tuned
   `dc.aurora`. A script that passes `cmap="dc.aurora"` will silently
   start rendering the v5 colormap after upgrading — swap to
   `cmap="dc.legacy_aurora"` (or `dc.legacy_teal_rose`) to keep the
   pre-v5 rendering.
5. **`get_palette("teal" | "indigo" | "gray")` length depends on the
   active palette version.** These three curated names collide with a
   v5 family, so under the default version (4) they stay capped at the
   legacy 8-step ramp; call `dm.set_palette_version(5)` first to get the
   full, coherent 10-step v5 ramp. Every other family always returns 10
   steps regardless of version.
6. **The `DeprecationWarning` only fires through `dm.color()` /
   `Color()`.** Native matplotlib color resolution — e.g.
   `plt.plot(color="dc.vivid3")` — reads the named-color mapping
   directly and does not go through dartwork-mpl's warning hook, so it
   will not warn even though the token is frozen-legacy.
7. **`dc.ref` is a new token, not an alias for `dc.gray6`.** The
   locale-aware semantic tokens (`dc.pos` / `dc.neg` / `dc.ref` /
   `dc.hl`) are new in v5; `dc.ref` happens to equal the v5 `gray6` hex,
   which is not the same value as the frozen-legacy `dc.gray6` under the
   default palette version. Use `dc.ref` (not `dc.gray6`) for the
   reference-line role so it stays consistent whichever palette version
   is active.

### Automated migration

A codemod rewrites the removed v0.5.5 palette-token names (see the table
below) to their current equivalents — never at runtime, always as a visible
diff you review before applying:

```bash
python -m dartwork_mpl.colors._migrate path/to/script.py      # dry-run diff
python -m dartwork_mpl.colors._migrate --apply path/to/*.py   # write
python -m dartwork_mpl.colors._migrate --no-advisory script.py # omit the ΔE advisory table
```

Each rewrite is tagged *colours preserved* (a pure rename) or *review* (a
merge into a different palette). The run also prints an advisory table of the
per-token CIEDE2000 ΔE by which `teal` / `indigo` / `gray` shift under
`set_palette_version(5)`, so opting into the v5 values is an informed choice.

The codemod *rewrites* prefixed tokens — a shade colour like `dc.ocean2` and
quoted `dc.` / `dm.` names like `set_cycle("dc.spectrum")`. Bare names *without*
a `dc.` / `dm.` prefix (e.g. `set_cycle("spectrum")`, `get_palette("ocean")`,
`cycle="pop"`) are now *reported* — flagged with `file:line` and the suggested
current name when they appear in a palette-API call — but are still **not**
auto-rewritten (a bare string has no namespace to prove intent), so review and
update those by hand.

Exit codes are CI-friendly for prefixed rewrites: dry-run exits `1` when
prefixed rewrites are pending, and exits `0` for `--apply` runs or clean files.
Files with only bare-name reports also exit `0`, so do not use the exit code
alone as a gate for manual bare-name cleanup.

## v0.5.5 — categorical palette overhaul

A judge-panel redesign reworked the 24 curated `dc.*` palettes into a
more coherent, better-covered set. Every palette is still CIELAB-generated
on an even-L\* ladder and CVD + B&W verified — but several were renamed,
merged, or dropped. The token mapping:

| Old token       | New token          | Change |
| --------------- | ------------------ | ------ |
| `dc.spectrum*`  | `dc.vivid*`        | rename |
| `dc.coolwarm*`  | `dc.cool_warm*`    | rename (uniform diverging underscore) |
| `dc.bold*`      | `dc.vivid*`        | merged into `vivid` |
| `dc.corporate*` | `dc.trustworthy*`  | merged into `trustworthy` |
| `dc.warm_cool*` | `dc.blue_orange*`  | removed (weakest under CVD) — use `blue_orange` or `teal_coral` |

`pastel` / `dusty` were also re-designed into an intentional high-key /
low-key pair (shared hue plan, different L\* band).

**New palettes** you can now reach for: `neon` (max-chroma electric),
`ember` (warm-vibrant), and `purple_green` (tritan-robust diverging).

## v0.5.4 — palette cleanup, module & MCP renames

### Legacy `dc.*` aliases removed

The 7 original back-compat aliases (`Vivid`, `Sunset`, `Ocean`, `Pop`,
`Cyber`, `Autumn`, `Nordic`) no longer resolve — `dc.ocean2`, `dc.nordic1`,
… now raise the usual "not a valid color" error. Their shade index is
preserved when you switch to the curated replacement:

| Old token     | New token (0.5.4)  |
| ------------- | ------------------ |
| `dc.vivid*`   | `dc.bold*`         |
| `dc.sunset*`  | `dc.earth*`        |
| `dc.ocean*`   | `dc.teal*`         |
| `dc.pop*`     | `dc.spectrum*`     |
| `dc.cyber*`   | `dc.jewel*`        |
| `dc.autumn*`  | `dc.dusty*`        |
| `dc.nordic*`  | `dc.teal_indigo*`  |

> **Coming from 0.5.4 or earlier?** `bold` and `spectrum` were themselves
> renamed again in 0.5.5. The
> [dc.\* palette migration](#dc-palette-migration-cumulative) table folds
> both hops into one lookup.

### Curated palettes now use snake_case

| Old token          | New token         |
| ------------------ | ----------------- |
| `dc.teal_seq*`     | `dc.teal*`        |
| `dc.focus*`        | `dc.teal_accent*` |
| `dc.focus_warm*`   | `dc.coral_accent*`|
| `dc.muted*`        | `dc.pastel*`      |
| `dc.teal_amber_div*` | `dc.teal_amber*`|

### Module & entry-point removals

| Removed | Replacement |
| --- | --- |
| `dm.auto_layout(fig)` | `dm.simple_layout(fig, margin=...)` — the legacy `padding` inches arg maps to `margin`; `max_iter` / `tolerance` were obsolete. Now raises a migration-hint `AttributeError`. |
| `dartwork_mpl.helpers.formatting` | `dartwork_mpl.helpers.labels` (`ModuleNotFoundError`) |
| `dartwork_mpl.asset_viz` | `dartwork_mpl.diagnostics` (`ModuleNotFoundError`) |
| MCP `dartwork-mpl://guide/general-guide` | `dartwork-mpl://guide/agent-entry` |
| MCP `dartwork-mpl://guide/layout-guide` | `dartwork-mpl://guide/policy` |
| `ui._config.append_history` / `load_history` | `save_preset` / `load_presets` |

## v0.5.0 — installer removal & lighter core

**The `install_llm_txt` installer was removed.** It copied the bundled
prompt corpus into IDE-specific folders; the corpus is now read at runtime
instead, so there is nothing to install.

| Removed | Replacement |
|---|---|
| `dm.install_llm_txt(...)` | `dm.get_agent_doc(name)` / `dm.agent_doc_path(name)` — `name` ∈ `AGENTS`, `CLAUDE`, `llms`, `llms-full` — or the MCP `dartwork-mpl://guide/*` resources |
| `dm.uninstall_llm_txt(...)` | nothing — the corpus is read at runtime, so there is no install to undo |
| `dm.INSTALL_TARGETS` | nothing — install targets no longer exist |

**`ipython` is no longer a core dependency.** Only `dm.show()` (inline SVG
display in Jupyter) needs it, so it moved to the `notebook` optional
extra. `dm.show()` raises a clear `ImportError` naming the extra when
IPython is absent; every other entry point is unaffected.

| Old install | New install |
|---|---|
| `pip install dartwork-mpl` (pulled in ~30 MB of IPython deps) | `pip install "dartwork-mpl[notebook]"` if you use `dm.show()` |

## v0.4.1 — public API audit (wrapper removals)

The 0.4.1 audit retired the thin matplotlib wrappers whose only
contribution was default kwargs. Where the curated values were worth
keeping they now live as snippets in
[`usage_guide/recipes.md`](usage_guide/recipes.md).

### `dm.subplots` / `dm.figure` removal

The wrappers around the matplotlib figure constructors are gone. Use
`plt.subplots` / `plt.figure` directly and pass
`figsize=dm.figsize(width, aspect)`:

```python
# Before
fig, ax = dm.subplots(width="15cm", aspect="wide")

# Now
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=dm.figsize("15cm", "wide"))
```

If you also passed `style=`, call `dm.style.use(...)` first:

```python
# Before
fig, ax = dm.subplots(width="15cm", aspect="wide", style="scientific")

# Now
dm.style.use("scientific")
fig, ax = plt.subplots(figsize=dm.figsize("15cm", "wide"))
```

`dm.figsize(width, aspect)` accepts a unit-string width (`"15cm"`,
`"5in"`, `"170mm"`, `"24pt"`) or a `Length` value (`dm.cm(15)`,
`dm.col1`, `dm.col2`), and four equivalent aspect forms — token
(`"wide"`), numeric ratio (`0.6`), unit-string height (`"12cm"`), or
`Length` height (`dm.cm(12)`). Bare `int` / `float` widths are rejected.

### `dm.style_spines` / `dm.add_grid` / `dm.minimal_axes` removal

All three were 1–3 line matplotlib calls; the curated kwargs now live in
[`usage_guide/recipes.md`](usage_guide/recipes.md):

| Removed | Inline replacement |
| --- | --- |
| `dm.style_spines(ax, color=c, linewidth=w, which=ws)` | `for s in ws: ax.spines[s].set_color(c); ax.spines[s].set_linewidth(w)` |
| `dm.add_grid(ax)` | `ax.grid(True, color="oc.gray3", alpha=0.3, linewidth=0.5); ax.set_axisbelow(True)` |
| `dm.minimal_axes(ax)` | see [Minimal axes recipe](usage_guide/recipes.md#minimal-axes-tufte-style) |

### `dm.auto_select_colors` → `dm.make_palette`

The rename cleaned up the argument names; the body is unchanged:

```python
# Before
colors = dm.auto_select_colors(n_series=5, color_type="sequential", highlight_index=2)

# Now
colors = dm.make_palette(5, kind="sequential", highlight=2)
```

### Formatter & spine wrappers → direct matplotlib

Five wrappers from audit round 3:

```python
# Before
dm.format_axis_percent(ax)
dm.format_axis_labels(ax, fmt="{:,.0f}")
dm.add_frame(fig)
dm.add_value_labels(ax, bars)
dm.set_xmargin(ax, 0.05); dm.set_ymargin(ax, 0.05)

# Now — direct matplotlib calls, sometimes one-liners
from matplotlib import ticker
ax.yaxis.set_major_formatter(ticker.PercentFormatter(xmax=1.0))
ax.yaxis.set_major_formatter(ticker.StrMethodFormatter("{x:,.0f}"))
fig.patches.append(plt.Rectangle((0, 0), 1, 1, fill=False, transform=fig.transFigure))
for bar in bars: ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
                          f"{bar.get_height():.0f}", ha="center", va="bottom")
ax.set_xmargin(0.05); ax.set_ymargin(0.05)
```

Eight more from audit round 2 — each is a single matplotlib call now:

```python
# Before / Now
dm.hide_spines(ax, ["top", "right"])            # → for s in ("top", "right"): ax.spines[s].set_visible(False)
dm.hide_all_spines(ax)                           # → for s in ax.spines: ax.spines[s].set_visible(False)
dm.show_only_spines(ax, ["left", "bottom"])      # → for s in ax.spines: ax.spines[s].set_visible(s in ("left", "bottom"))
dm.remove_grid(ax)                               # → ax.grid(False)
dm.format_axis_thousands(ax, axis="y")           # → see snippet below
dm.save_figure(fig, "out.png")                   # → fig.savefig("out.png")
dm.create_figure_with_style(width=..., style=...)  # → dm.style.use(style); plt.subplots(figsize=dm.figsize(...))
dm.templates.diverging_bar.get_source_code()     # → inspect.getsource(dm.templates.diverging_bar)
```

```python
# Thousand separator on y-axis:
from matplotlib import ticker
formatter = ticker.FuncFormatter(lambda x, p: f"{x:,.0f}")   # sep=","
# Non-comma separator:
# formatter = ticker.FuncFormatter(lambda x, p: f"{x:,.0f}".replace(",", sep))
ax.yaxis.set_major_formatter(formatter)   # or ax.xaxis for axis="x"
```

### `dm.Inches` → `dm.Length`

The in-flight `Inches(float)` marker was replaced by `dm.Length`, an
opaque wrapper with per-unit property views (`length.cm`, `length.mm`,
`length.inch`, `length.pt`). It is deliberately **not** a `float`
subclass — passing it to a pt-based (`fontsize=`) or px-based API would
silently misinterpret the value. `dm.Inches` is no longer importable.

## dc.* palette migration (cumulative)

If you have `dc.*` tokens from any release before 0.5.5, this table folds
every rename hop (0.5.4 legacy-alias removal, 0.5.4 snake_case, 0.5.5
overhaul) into a single lookup. Shade indices are preserved
(`dc.ocean2` → `dc.teal2`).

| Legacy token (0.5.3 or earlier) | Current token (0.5.5) |
| ------------------------------- | --------------------- |
| `dc.Vivid*` / `dc.vivid*` (alias) | `dc.vivid*` |
| `dc.Sunset*` / `dc.sunset*`     | `dc.earth*`           |
| `dc.Ocean*` / `dc.ocean*`       | `dc.teal*`            |
| `dc.Pop*` / `dc.pop*`           | `dc.vivid*`           |
| `dc.Cyber*` / `dc.cyber*`       | `dc.jewel*`           |
| `dc.Autumn*` / `dc.autumn*`     | `dc.dusty*`           |
| `dc.Nordic*` / `dc.nordic*`     | `dc.teal_indigo*`     |
| `dc.spectrum*`                  | `dc.vivid*`           |
| `dc.bold*`                      | `dc.vivid*`           |
| `dc.coolwarm*`                  | `dc.cool_warm*`       |
| `dc.corporate*`                 | `dc.trustworthy*`     |
| `dc.warm_cool*`                 | `dc.blue_orange*`     |
| `dc.teal_seq*`                  | `dc.teal*`            |
| `dc.focus*`                     | `dc.teal_accent*`     |
| `dc.focus_warm*`                | `dc.coral_accent*`    |
| `dc.muted*`                     | `dc.pastel*`          |
| `dc.teal_amber_div*`            | `dc.teal_amber*`      |

> These map each old token to its **closest current palette** — the
> colours themselves were re-generated in the overhaul, so a migrated
> token is a starting point, not a byte-identical swap. Browse the live
> set on the [categorical palettes](color_system/categorical-palettes.md)
> page.

## color parsing (`dm.named` → `dm.color`)

`dm.named` is gone. The replacement is `dm.color`, a single string-parser
entry point that mirrors `dm.length`:

```python
# Before
red = dm.named("oc.red5")

# Now
red = dm.color("oc.red5")               # palette token
red = dm.color("#ff0000")               # hex
red = dm.color("rgb(255, 0, 0)")        # rgb function string
red = dm.color("oklch(0.7 0.2 30)")     # oklch function string
red = dm.color("oklab(0.7 0.1 0.1)")    # oklab function string
```

The submodule was also renamed `color` → `colors`. Anywhere you used
`from dartwork_mpl.color import ...` swap to:

```python
# Before
from dartwork_mpl.color import Color

# Now
from dartwork_mpl.colors import Color
```


---
## Anti-pattern catalog (SSOT, YAML)
# dartwork-mpl anti-pattern catalog (SSOT for lint engine).
#
# Each rule has:
#   id          unique kebab-case identifier
#   severity    critical | warning | info
#   detector
#     kind      regex | substring
#     pattern   regex pattern (Python re, MULTILINE) for kind=regex
#     literal   literal substring (case-sensitive) for kind=substring
#   message     short user-facing text shown in the lint report
#   why         (optional) longer rationale; shown only on demand
#   fix_suggestion  (optional) one-line replacement or pointer
#
# Rule IDs are part of the public API: tools, docs, and tests refer
# to them. Do not rename without bumping the dartwork-mpl version.

version: 1
rules:
  - id: figsize-direct
    severity: critical
    detector:
      kind: regex
      pattern: '\bfigsize\s*=\s*\('
    message: |
      Raw `figsize=(w, h)` tuples bypass dartwork-mpl's physical-width
      contract. Use `figsize=dm.figsize("13cm", "wide")` so the width
      stays in cm/in/mm and the aspect intent is named.
    why: |
      Width and aspect are decided separately so chart-wide width
      consistency can be enforced and so the height follows from the
      content's aspect intent.
    fix_suggestion: 'figsize=dm.figsize("13cm", "standard")'

  - id: tight-layout
    severity: critical
    detector:
      kind: regex
      pattern: '\btight_layout\s*\('
    message: |
      `tight_layout()` collides with dartwork-mpl's spine and legend
      handling. Use `dm.simple_layout(fig)` instead.
    fix_suggestion: 'dm.simple_layout(fig)'

  - id: multirow-subplots-no-gridspec-kw
    severity: info
    detector:
      kind: regex
      pattern: '\bplt\.subplots\(\s*[2-9]\b(?![\s\S]{0,200}?(?:gridspec_kw|sharex|sharey))'
    message: |
      Vertically stacked `plt.subplots(>=2, ...)` was created without
      `gridspec_kw={"hspace": ..., "wspace": ...}`. The matplotlib
      default `hspace` (0.2) frequently causes the upper subplot's
      xlabel / xtick labels to overlap the lower subplot's title once
      `dm.simple_layout(fig)` snaps the outer margins flush. Pass an
      explicit `gridspec_kw` (e.g. `hspace=0.55`, `wspace=0.3`) when
      every subplot has both a title and an xlabel, then verify with
      `dm.validate_figure(fig)` — the `CROSS_AXES_OVERLAP` check will
      flag any residual collisions.
    why: |
      `dm.simple_layout` only adjusts the figure's outer margins; it
      intentionally preserves the GridSpec's `hspace` / `wspace` so
      authors can keep their own intent. That makes the default
      `hspace=0.2` the hidden source of title↔xlabel collisions in
      multi-panel gallery and report figures. The detector scans the
      200 characters following `plt.subplots(<n>` for `gridspec_kw` /
      `sharex` / `sharey` so it catches line-wrapped calls (where the
      kwargs sit on a later line), not just single-line ones. This is
      a best-effort static hint — the authoritative collision check is
      the runtime `CROSS_AXES_OVERLAP` validator.
    fix_suggestion: 'gridspec_kw={"hspace": 0.55, "wspace": 0.3}'

  - id: zero-resize-mention
    severity: warning
    detector:
      kind: substring
      literal: 'Zero-Resize'
    message: |
      "Zero-Resize Policy" was retired in 0.4.0. dartwork-mpl now uses
      free-form width input plus a lint consistency guard.

  - id: plt-style-use
    severity: warning
    detector:
      kind: regex
      pattern: '\bplt\.style\.use\s*\('
    message: |
      Use `dm.style.use(...)` (or `dm.style.stack([...])` for a stack)
      instead of `plt.style.use(...)`.
    fix_suggestion: 'dm.style.use("scientific")'

  - id: plt-show-only
    severity: info
    detector:
      kind: regex
      pattern: '\bplt\.show\s*\(\)'
    message: |
      Prefer `dm.save_and_show(fig, "name")` or `dm.save_formats(fig,
      "name")` so the figure ships with its rendered artifact.

  - id: dm-subplots-removed
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.(?:subplots|figure)\s*\('
    message: |
      `dm.subplots` and `dm.figure` were REMOVED — accessing them now
      raises `AttributeError`. Use `plt.subplots(figsize=dm.figsize(...))`
      (or `plt.figure(figsize=dm.figsize(...))` for custom GridSpec).
      Apply style separately via `dm.style.use(...)`.
    why: |
      Both functions were thin wrappers around `plt.subplots` / `plt.figure`
      that bundled style mutation, polymorphic `width=`, and a deny-list
      of legacy kwargs. Removing them keeps the matplotlib API native and
      reduces dartwork-mpl's surface to a single sizing helper.
    fix_suggestion: 'plt.subplots(figsize=dm.figsize("13cm", "standard"))'

  - id: cm2in-figsize
    severity: critical
    detector:
      kind: regex
      pattern: 'figsize\s*=\s*\([^)]*cm2in'
    message: |
      `figsize=(dm.cm2in(...), dm.cm2in(...))` is the legacy 0.3
      pattern: `dm.cm2in` itself raises `AttributeError`. Use
      `figsize=dm.figsize("<n>cm", "<aspect>")` instead.
    fix_suggestion: 'figsize=dm.figsize("9cm", "standard")'

  - id: dm-spines-removed
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.(?:style_spines|add_grid|minimal_axes)\s*\('
    message: |
      `dm.style_spines`, `dm.add_grid`, and `dm.minimal_axes` were
      REMOVED in 0.4.1 (#156). They were thin matplotlib wrappers
      whose only contribution was curated default kwargs. Inline the
      raw matplotlib calls instead, or copy a snippet from
      `docs/usage_guide/recipes.md` (publication grid, minimal axes,
      thin gray spines).
    fix_suggestion: 'see docs/usage_guide/recipes.md'

  - id: dm-auto-select-colors-renamed
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.(?:helpers\.colors\.)?auto_select_colors\s*\('
    message: |
      `auto_select_colors` was renamed to `make_palette` in 0.4.1
      (#156) for vocabulary alignment with `make_offset` /
      `list_palettes`. Argument cleanup at the same time:
      `n_series` → `n`, `color_type` → `kind`,
      `highlight_index` → `highlight`.
    fix_suggestion: 'dm.make_palette(n, kind="categorical")'

  - id: dm-auto-layout-removed
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.auto_layout\s*\('
    message: |
      `dm.auto_layout` was REMOVED in 0.5.4 — accessing it now raises
      `AttributeError`. Use `dm.simple_layout(fig, margin=...)`: the
      legacy `padding` inches argument maps to `margin`;
      `max_iter`/`tolerance` are obsolete (simple_layout is
      direct-calc).
    fix_suggestion: 'dm.simple_layout(fig)'

  - id: dm-named-renamed
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.named\s*\('
    message: |
      `dm.named` was REMOVED in 0.4.1 — the replacement is
      `dm.color(...)`, a single string-parser entry point that accepts
      token names, hex, and `rgb()`/`oklch()`/`oklab()` functional
      strings.
    fix_suggestion: 'dm.color("oc.red5")'

  - id: dm-removed-namespaces
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.(?:agent_utils|xplot)\b'
    message: |
      `dm.agent_utils` and `dm.xplot` were REMOVED in 0.4. The
      agent_utils surfaces moved to `dm.lint` / `dm.validate_figure` /
      `dm.helpers`; the xplot templates now live in `dm.templates`
      (e.g. `dm.templates.plot_diverging_bar`). See docs/migration.md.
    fix_suggestion: 'see docs/migration.md'

  - id: dm-install-llm-txt-removed
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.(?:install_llm_txt|uninstall_llm_txt|INSTALL_TARGETS)\b'
    message: |
      The `install_llm_txt` installer family
      (`install_llm_txt`/`uninstall_llm_txt`/`INSTALL_TARGETS`) was
      REMOVED in 0.5. The agent-doc corpus is read at runtime — use
      `dm.get_agent_doc(name)` / `dm.agent_doc_path(name)`, or the MCP
      `dartwork-mpl://guide/*` resources.
    fix_suggestion: 'dm.get_agent_doc("llms")'

  - id: deprecated-width-token
    severity: critical
    detector:
      kind: regex
      pattern: '\bdm\.(SW|MW|TW|DW|FS_[A-Z_]+|WIDTHS)\b'
    message: |
      `dm.SW/MW/TW/DW/FS_*/WIDTHS` were REMOVED in 0.4.0 — accessing
      them now raises `AttributeError`. Use
      `figsize=dm.figsize("<n>cm", "<aspect>")`, or `dm.col1`/`dm.col2`
      for academic columns.

  - id: raw-width-number
    severity: critical
    detector:
      kind: regex
      # `dm.figsize(13, ...)` or `dm.figsize(13.5)` — bare numbers carry
      # no unit and dartwork-mpl rejects them at runtime. Catch the
      # call site with a clearer message than the raised TypeError.
      pattern: '\bdm\.figsize\s*\(\s*-?\d+(?:\.\d+)?\s*[,)]'
    message: |
      `dm.figsize(<bare number>, ...)` is rejected — bare numbers carry
      no unit. Pass a unit string (`"13cm"`, `"5in"`, `"170mm"`,
      `"24pt"`) or a Length value (`dm.cm(13)`, `dm.col1`, `dm.col2`).
    fix_suggestion: 'dm.figsize("13cm", "standard")'

  - id: dpi-arg
    severity: critical
    detector:
      kind: regex
      # Allow one level of nested parens (e.g. ``figsize=(8, 6)``)
      # before the ``dpi=`` token so that
      # ``plt.figure(figsize=(8,6), dpi=200)`` is caught. The previous
      # ``[^)]*`` form bailed at the first ``)`` and silently missed
      # this very common spelling. ``savefig(dpi=...)`` is intentionally
      # outside this rule's scope — see ``savefig-direct``.
      pattern: '\bplt\.(?:subplots|figure)\s*\((?:[^()]|\([^()]*\))*\bdpi\s*='
    message: |
      `dpi=` should not be set per-figure. The active dartwork-mpl
      style controls dpi (display vs savefig). Direct
      ``savefig(dpi=...)`` is flagged separately by ``savefig-direct``.

  - id: raw-hex-color
    severity: info
    detector:
      kind: regex
      pattern: '\b(?:color|c)\s*=\s*["'']#[0-9a-fA-F]{3,8}["'']'
    message: |
      Raw hex colors bypass dartwork-mpl's color system. Prefer named
      tokens passed as strings, e.g. `color="oc.blue6"` (Open Color),
      `color="tw.sky500"` (Tailwind), or `color="dc.0"` (dartwork core
      categorical). These register as matplotlib named colors; wrap in
      `dm.color("oc.blue6")` when you need a Color object.
    why: |
      Named palettes give consistent perception across reports and
      let style presets remap colors centrally.
    fix_suggestion: 'color="oc.blue6"'

  - id: fontsize-literal
    severity: warning
    detector:
      kind: regex
      pattern: '\bfontsize\s*=\s*\d+(?:\.\d+)?'
    message: |
      `fontsize=<n>` hardcodes a pt size and bypasses the active
      style preset. Use `dm.fs(n)` for an offset from the style's
      base size, or rely on the style defaults.
    fix_suggestion: 'fontsize=dm.fs(0)'

  - id: linewidth-literal
    severity: warning
    detector:
      kind: regex
      # Match only literals whose integer part is >= 1. Sub-1 values
      # (e.g. ``linewidth=0.3`` for hairline edges) and the no-border
      # idiom ``linewidth=0`` are common, intentional decoration and
      # don't fight the active style — bundled templates rely on them.
      pattern: '\b(?:linewidth|lw)\s*=\s*[1-9]\d*(?:\.\d+)?'
    message: |
      `linewidth=<n>` (>= 1) hardcodes a line width. Use `dm.lw(n)`
      for a relative offset from the active style. (`linewidth=0`,
      used to disable a border, and sub-1 hairline widths like
      `linewidth=0.3` are allowed.)
    fix_suggestion: 'linewidth=dm.lw(0)'

  - id: savefig-direct
    severity: warning
    detector:
      kind: regex
      pattern: '\b(?:fig|plt)\.savefig\s*\('
    message: |
      Direct `savefig(...)` bypasses the dartwork-mpl save preset
      (multi-format, dpi, metadata). Use
      `dm.save_formats(fig, "name")` for scripts or
      `dm.save_and_show(fig, "name")` for notebooks.
    fix_suggestion: 'dm.save_formats(fig, "name")'

  - id: jet-cmap
    severity: warning
    detector:
      kind: regex
      pattern: 'cmap\s*=\s*["''](?:jet|hsv|gist_rainbow|gist_ncar|nipy_spectral|rainbow)["'']'
    message: |
      Rainbow colormaps (`jet`, `hsv`, `gist_rainbow`, `gist_ncar`,
      `nipy_spectral`, `rainbow`) misrepresent ordinal data. Use a
      perceptually uniform colormap (`viridis`, `cividis`, `inferno`,
      `magma`, `plasma`) or one of the dartwork-registered colormaps
      (`dm.list_colormaps()`).
    fix_suggestion: 'cmap="viridis"'

  - id: oversize-width
    severity: warning
    detector:
      kind: regex
      # Match a > 17 cm literal in either of the two canonical width
      # contexts: a `width=` kwarg (legacy / non-figsize callers) or
      # the first positional of `dm.figsize("...cm", ...)`. The aspect
      # argument (second positional) is filtered out because it sits
      # behind a `,` rather than directly after `(`.
      pattern: '(?:width\s*=\s*|\bdm\.figsize\s*\(\s*)["''](?:1[89]|[2-9]\d|\d{3,}|17\.\d*[1-9]\d*)(?:\.\d+)?cm["'']'
    message: |
      Width exceeds 17 cm (`dm.col2`). Most page layouts break above
      this threshold and produce ragged multi-figure reports. Reduce
      to 17 cm or split into a multi-row layout.
    why: |
      Snap widths to the 0.5 cm grid and keep ≤ 5 distinct widths
      per project for cross-figure consistency.
    fix_suggestion: 'dm.figsize("17cm", "standard")'


---
## AI plot templates index
AI Plot Templates
-----------------

Eighteen ready-to-use plot templates curated for AI coding assistants. Each
template is a small, self-contained script written with the
``figsize=dm.figsize(...)`` API and the curated ``dc.*`` palette. They are
bundled in the wheel under ``dartwork_mpl/asset/prompt/05-templates/`` and
exposed through both the prompt utilities and the
:doc:`MCP server </integrations/mcp_server>`.

How to access these templates
=============================

.. code-block:: python

   import dartwork_mpl as dm

   # Read a template script as text
   bar_src = dm.get_prompt("05-templates/bar")

   # Or list everything available under prompts/
   print(dm.list_prompts())

Or via MCP from Claude / Cursor / Continue:

.. code-block:: text

   dartwork-mpl://templates/{plot_type}

where ``{plot_type}`` is one of ``bar``, ``bar_horizontal``,
``bar_grouped``, ``boxplot``, ``contour``, ``heatmap``, ``histogram``,
``line``, ``pie``, ``plot_3d``, ``polar``, ``scatter``, ``small_multiples``,
``stacked_bar``, ``tornado``, ``twin_axis``, ``violin``, or ``waterfall``.

The gallery entries below render each template so you can pick the right
shape at a glance, then copy the source from the page (or fetch it via
``dm.get_prompt``) into your project.
