# 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.
:::

## At-a-glance ROI

| What used to hurt                   | dartwork-mpl                                    |
| ----------------------------------- | ----------------------------------------------- |
| Hand-tuning `figsize` and `dpi`     | `plt.subplots(figsize=dm.figsize("13cm", "standard"))`  |
| `tight_layout` clipping labels      | `dm.simple_layout(fig)` — deterministic, content-aware |
| Reaching for hex codes              | `color="dc.ocean3"` (curated 8-mood palette), 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

```python
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.ocean3", 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("13cm", "standard")`              | 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                             |

### Static reference: `dm.fs(n)` resolved per preset

Plain-text fallback for the live ruler — 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 / standard / golden / wide / cinema`. |
| 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"),
8 mood families × 6 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.ocean3")                   # cool blue
ax.fill_between(x, y1, y2, color="dc.sunset1")     # 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="dc.ocean2")                   # OpenColor
ax.plot(x, y3, color="tw.emerald500")              # Tailwind
```

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

```python
import dartwork_mpl as dm

dm.list_palettes()[:5]      # → ['dc.autumn', 'dc.cyber', 'dc.forest', ...]
dm.show_palette("dc.ocean") # renders the 6-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.ocean3")

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
result = dm.validate_with_fixes(fig)
print(result.report())     # human-readable summary of warnings
# margin_asymmetry → auto-fixed via dm.simple_layout()
# pie_label_offset → auto-adjusted pctdistance
```

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-ui init ./my-viewer --example simple

# 2. run it — opens http://127.0.0.1:8501
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)
:::

::::


---
## Migration (0.3 → 0.4)
# Migration Guide

This guide covers the rename / deprecation events that have shipped
since v0.1, ordered newest first. The `install_llm_txt` /
`uninstall_llm_txt` installer was **removed in 0.5**; the 0.4-era
figure constructors (`dm.subplots`, `dm.figure`), the `agent_utils`
and `xplot` module aliases, the 0.3 width tokens, `FS_*` figsize
tuples, `cm2in`, the `dartwork_mpl.constant` module, and the
`figsize=` / `dpi=` arguments they used to accept were all **removed**
across 0.4.x. Accessing any of them now raises `AttributeError` /
`ModuleNotFoundError` / `TypeError` with a message naming the
replacement. The `dartwork_mpl.asset_viz` and
`dartwork_mpl.helpers.formatting` submodule aliases still import with a
`DeprecationWarning` and are scheduled for removal in **v1.0**.

## At a glance

| Old surface                           | New surface                                          |
| ------------------------------------- | ---------------------------------------------------- |
| `dm.install_llm_txt()` / `uninstall_llm_txt()` | `dm.get_agent_doc(name)` / `dm.agent_doc_path(name)` (or MCP `dartwork-mpl://guide/*`) |
| `dm.subplots(width=..., aspect=...)`  | `plt.subplots(figsize=dm.figsize(...))`              |
| `dm.figure(width=..., aspect=...)`    | `plt.figure(figsize=dm.figsize(...))`                |
| `dm.subplots(..., style=...)`         | call `dm.style.use(...)` first, then `plt.subplots`  |
| raw `figsize=(w, h)` tuple            | `figsize=dm.figsize("<n>cm", "<aspect>")`            |
| bare-number width (e.g. `width=13`)   | unit string (`"13cm"`) or `dm.cm(13)` / `dm.col1`    |
| `dm.SW`, `dm.MW`, `dm.TW`, `dm.DW`    | `dm.col1` / `dm.col2` / `dm.cm(...)`                 |
| `dm.WIDTHS`                           | iterate explicit widths inline                       |
| `dm.FS_SINGLE` / `FS_DOUBLE` / etc.   | `figsize=dm.figsize("<n>cm", "<aspect>")`            |
| `dm.cm2in(...)`                       | `dm.cm(...)` (returns `Length`)                      |
| `dpi=` argument                       | active style preset                                  |
| `dartwork_mpl.constant` module        | `dartwork_mpl.units` + width / aspect API            |
| `plt.tight_layout()`                  | `dm.simple_layout(fig)`                              |
| `dm.auto_layout(fig)`                 | `dm.simple_layout(fig)` (auto_layout is a deprecation alias) |
| `dm.simple_layout(fig, margins=(...), bbox=..., bound_margin=..., gtol=..., importance_weights=...)` | `dm.simple_layout(fig, margin="2%", ml=..., mr=..., mt=..., mb=...)` (new keyword API) |
| `dartwork_mpl.agent_utils`            | `dartwork_mpl.helpers`                               |
| `dartwork_mpl.xplot`                  | `dartwork_mpl.templates`                             |
| `dartwork_mpl.helpers.formatting`     | `dartwork_mpl.helpers.labels`                        |
| `dartwork_mpl.asset_viz`              | `dartwork_mpl.diagnostics`                           |
| `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(n_series=N, color_type=K, highlight_index=I)` | `dm.make_palette(N, kind=K, highlight=I)` |
| `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) |

## `dm.named` removal & `color` → `colors` submodule rename

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

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

# Now
dm.color("oc.red5")              # palette name
dm.color("#ff0000")              # hex
dm.color("rgb(1, 0, 0)")         # functional rgb
dm.color("oklch(0.7, 0.15, 30)") # functional oklch
dm.color("oklab(0.5, 0.05, 0.05)")
dm.color(some_color_instance)    # pass-through
```

The specialized constructors `dm.hex`, `dm.rgb`, `dm.oklch`,
`dm.oklab` are unchanged — use them when you already have the
components, just as `dm.cm(13)` coexists with `dm.length("13cm")`.

The `dartwork_mpl.color` submodule was renamed to
`dartwork_mpl.colors` so the `color` name is free for the new
function. There is no compatibility shim — update direct imports:

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

# Now
from dartwork_mpl.colors import Color
```

Most users never touch the submodule directly (`dm.Color`,
`dm.color`, etc. all continue to work via the top-level package).

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

Both `dm.subplots` and `dm.figure` were removed. The package no
longer wraps `plt.subplots` / `plt.figure`; instead it ships a
single sizing helper that can be passed directly to matplotlib's
own `figsize=` argument:

```python
# Before
import dartwork_mpl as dm

fig, ax = dm.subplots(width="13cm", aspect="standard", style="scientific")

# Now
import matplotlib.pyplot as plt

import dartwork_mpl as dm

dm.style.use("scientific")
fig, ax = plt.subplots(figsize=dm.figsize("13cm", "standard"))
```

Other `dm.subplots` keyword arguments (`sharex`, `sharey`,
`width_ratios`, `height_ratios`, `gridspec_kw`, `subplot_kw`,
`squeeze`, `nrows`, `ncols`) move to `plt.subplots` unchanged. The
`style=` kwarg is gone — call `dm.style.use(...)` (or
`dm.style.stack([...])`) before constructing the figure.

`dm.figsize(width, aspect)` rejects bare `int` / `float` widths
explicitly — pass a unit string (`"13cm"`, `"5in"`, `"170mm"`,
`"24pt"`) or a `Length` value (`dm.cm(13)`, `dm.col1`, `dm.col2`)
so the unit is always visible at the call site. The
`raw-width-number` lint rule catches stragglers.

The two-pass migrator at `dm.lint.migrate_legacy_code` inserts a
`# TODO(dm-migrate):` hint above each `dm.subplots` / `dm.figure`
call so an agent can rewrite them in one sweep.

## v0.4.x → v0.5.0

**The `install_llm_txt` installer was removed (#170).** It copied the
bundled prompt corpus into IDE-specific folders; the corpus is now read
at runtime instead, so there is nothing to install. `dm.install_llm_txt`
/ `dm.uninstall_llm_txt` / `dm.INSTALL_TARGETS` raise `AttributeError`.

| 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 (#248).** 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.0 → v0.4.1 — API audit round 3 (#141)

5 wrapper functions removed using the *knowledge-encapsulation* criterion:
AI agents can reproduce these in one shot without spec.

| Removed | Replacement |
|---|---|
| `dm.format_axis_percent(ax, axis, decimals)` | `ax.yaxis.set_major_formatter(ticker.PercentFormatter(1.0, decimals=decimals))` |
| `dm.format_axis_labels(ax, x_label, x_unit, ...)` | `ax.set_xlabel(f"{x_label} ({x_unit})")` (compose inline) |
| `dm.add_frame(ax, color, linewidth)` | `for s in ax.spines.values(): s.set_visible(True); s.set_color(color); s.set_linewidth(linewidth)` |
| `dm.add_value_labels(ax, x, y, ...)` | inline text loop: `for xi, yi in zip(x, y): ax.text(xi, yi+offset, f"{yi:.1f}", ...)` |
| `dm.set_xmargin(ax, margin, left, right)` | `ax.margins(x=margin)` plus optional `ax.set_xlim((left, right))` |
| `dm.set_ymargin(ax, margin, bottom, top)` | same shape, y axis: `ax.margins(y=margin)` |

## v0.4.0 → v0.4.1 — API audit round 2 (#141)

8 thin-wrapper utility functions were removed. Each can be replaced by
one or two plain matplotlib calls.

| Removed | Replacement |
|---|---|
| `dm.hide_spines(ax, which)` | `for s in (which or ["top","right"]): ax.spines[s].set_visible(False)` |
| `dm.hide_all_spines(ax)` | `for s in ax.spines.values(): s.set_visible(False)` |
| `dm.show_only_spines(ax, which)` | `for s in ["top","right","bottom","left"]: ax.spines[s].set_visible(s in which)` |
| `dm.remove_grid(ax)` | `ax.grid(False)` |
| `dm.format_axis_thousands(ax)` | `ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, p: f"{x:,.0f}"))` |
| `dm.save_figure(fig, path, ...)` | `Path(path).parent.mkdir(parents=True, exist_ok=True); dm.save_formats(fig, str(path), ...)` |
| `dm.create_figure_with_style(style)` | `dm.style.use(style); fig = plt.figure(figsize=(cm(17), cm(17)*0.6), dpi=200)` |
| `dm.templates.diverging_bar.get_source_code()` | `pathlib.Path(__file__).read_text()` |

For `format_axis_thousands`, add the following at your callsite:

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

## v0.3.x → v0.4.0

> [!NOTE]
> dartwork-mpl has since evolved further: `dm.subplots` and
> `dm.figure` themselves were removed. The snippets below describe
> the *0.4-era* idiom; for the modern call form, see the top section
> of this guide.

0.4 reshapes the figure-creation surface around two ideas:

1. **Width is free-form.** Pass any unit-suffixed string
   (`"13cm"`, `"6.7in"`, `"170mm"`), a helper call (`dm.cm(13)`,
   `dm.inch(6.7)`, `dm.mm(170)`), or the academic-column sugar
   `dm.col1` / `dm.col2`. The fixed 4-tier `SW`/`MW`/`TW`/`DW`
   constants are deprecated.
2. **Aspect is a height/width ratio**, separated from width. Six
   named tokens cover the common cases; pass a positive float for
   anything else.
3. **The 0.3 surface is gone, not deprecated.** The 0.4.0 cut
   pulled the planned 0.5.0 removals forward, so the table above
   marks every 0.3 width / FS / `cm2in` / `figsize=` / `dpi=` entry
   as already removed. There is no `DeprecationWarning` grace period
   — old call sites raise immediately. Migrate them all in one pass.

A new `dm.lint` module checks code against a 15-rule anti-pattern
catalog so editor tooling, MCP clients, and CI all share the same
ground truth.

### Width tokens → `width="..."`

```python
# DEPRECATED — fires `width-token` lint warning
fig, ax = plt.subplots(figsize=(dm.SW, dm.SW * 0.75))

# 0.4 — width string, aspect token
fig, ax = dm.subplots(width="9cm", aspect="standard")

# 0.4 — academic column sugar
fig, ax = dm.subplots(width=dm.col1, aspect="standard")
```

Same idea for the rest of the 4-tier ladder:

| 0.3                       | 0.4 (preferred)                 |
| ------------------------- | ------------------------------- |
| `dm.SW` (≈ 9 cm)          | `width="9cm"` or `dm.col1`      |
| `dm.MW` (≈ 12 cm)         | `width="12cm"`                  |
| `dm.TW` (≈ 14.5 cm)       | `width="14.5cm"` (or `"15cm"`)  |
| `dm.DW` (≈ 17 cm)         | `width="17cm"` or `dm.col2`     |
| `dm.WIDTHS` (tuple)       | iterate explicit widths inline  |

### `figsize=` → `width=` + `aspect=`

`figsize=` is the most common 0.3 idiom and is the single biggest
source of mismatched figures across a multi-figure report. The
`figsize-direct` lint rule flags any remaining usage:

```python
# DEPRECATED — fires `figsize-direct` lint critical
fig, ax = plt.subplots(figsize=(dm.cm2in(13), dm.cm2in(10)))

# 0.4
fig, ax = dm.subplots(width="13cm", aspect=10 / 13)
```

`FS_*` tuples follow the same path:

```python
# DEPRECATED — fires `width-token` lint warning
fig, ax = plt.subplots(figsize=dm.FS_SINGLE)

# 0.4
fig, ax = dm.subplots(width="9cm", aspect="standard")
```

### Aspect tokens

Aspect is **height ÷ width**. Six named tokens are recognised:

| Token        | Ratio (h/w) | Typical use                     |
| ------------ | ----------- | ------------------------------- |
| `"square"`   | `1.0`       | scatter, correlation matrices   |
| `"portrait"` | `5 / 4`     | tall multi-row dashboards       |
| `"standard"` | `3 / 4`     | default: time series, bar/line  |
| `"golden"`   | `1 / 1.618` | classic publication figures     |
| `"wide"`     | `2 / 3`     | landscape charts, talks         |
| `"cinema"`   | `1 / 2`     | very wide trend strips          |

Or pass a positive float directly:

```python
fig, ax = dm.subplots(width="13cm", aspect=0.6)
```

`validate_figure` warns on extreme aspects (< 0.3 or > 4.0).

### `tight_layout` → `simple_layout`

```python
# DEPRECATED — fires `tight-layout` lint critical
plt.tight_layout()
fig.tight_layout()

# 0.5+
dm.simple_layout(fig)                        # snap content flush to figure edges
dm.simple_layout(fig, margin="2%")           # uniform 2% buffer
dm.simple_layout(fig, margin=dm.mm(2))       # uniform 2 mm buffer
dm.simple_layout(fig, ml=dm.cm(1), mr="3%")  # per-side, mixed units
```

`simple_layout` measures every visible artist on every axes
(texts, title, axis labels, view-limited tick labels, axis offset
text, legend) and arithmetically places the GridSpec so the
content union sits at the requested distance from each figure
edge. The result is deterministic — same figure produces the same
GridSpec, no scipy involved. It survives long labels, multi-line
titles, twinx() right-spine cases, and log-scale fixed-points
that `tight_layout` mangles.

The historical `dm.auto_layout(fig)` still works as a
:class:`DeprecationWarning`-emitting alias; new code should call
`simple_layout` directly.

The previous `simple_layout` keyword arguments (`margins=(...)`,
`bbox=...`, `bound_margin=...`, `gtol=...`, `importance_weights=...`)
have been removed in favour of the unified `margin` /
`ml`/`mr`/`mt`/`mb` API. See the at-a-glance table at the top of
this guide.

### `dm.cm2in` → `dm.cm`

`cm2in(x)` converted cm to a plain `float`. `dm.cm(x)` returns a
:class:`~dartwork_mpl.units.Length` value — a Color-pattern wrapper
with multi-unit views (`.cm` / `.mm` / `.inch` / `.pt`) and
arithmetic that preserves the tag, so `dm.cm(9) * 2` stays a
`Length` and round-trips through `parse_width` cleanly.
`dm.inch(x)`, `dm.mm(x)`, `dm.pt(x)` are the analogous helpers for
the other units; `dm.length("13cm")` parses a unit string.

```python
# DEPRECATED — emits DeprecationWarning
value = dm.cm2in(9)

# 0.4+
length = dm.cm(9)               # Length(9.0000cm)
length = dm.inch(3.5)           # Length(3.5000in)
length = dm.mm(170)             # Length(17.0000cm)
length = dm.pt(24)              # Length(0.8467cm)

# Multi-unit views (Color-style):
length.cm     # 9.0
length.inch   # 3.5433...
length.pt     # 255.118...
```

> **0.4 in-flight rename.** An earlier 0.4 draft used
> `Inches(float)` as a phantom-type marker. It was renamed to
> :class:`Length` (a wrapper, no longer a `float` subclass) before
> any 0.4 release shipped, to align with the `Color` class and to
> expose per-unit views. Top-level constructors `dm.cm` / `dm.inch`
> / `dm.mm` keep the same signatures; they just return `Length`
> now. `dm.Inches` is no longer importable.

### Module renames carried forward

The earlier rename pairs (`agent_utils → helpers`,
`xplot → templates`, `helpers.formatting → helpers.labels`,
`asset_viz → diagnostics`) are still deprecated and continue to
work in 0.4 with a `DeprecationWarning`. See the v0.1.x → v0.2.0
and v0.3.x sections below for the canonical replacements.

### "Zero-Resize Policy" wording is retired

Pre-0.4 docs framed figure sizing as a "Zero-Resize Policy"
enforced by the style preset. dartwork-mpl now uses **free width
input plus a lint consistency guard** (the `oversize-width` and
`raw-width-number` rules). Any project rule that still cites the
"Zero-Resize Policy" should be rewritten in terms of
`plt.subplots(figsize=dm.figsize(...))` + `dm.lint`.

### New: `dm.lint`

`dm.lint.lint(code)` runs the 15-rule anti-pattern catalog over a
Python source string. It is the same engine the MCP
`lint_dartwork_mpl_code` tool and `dartwork-mpl lint` CLI use, so
your editor, your CI, and your AI assistant all see the same
violations.

```python
import dartwork_mpl as dm

source = """
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6.7, 4.0))
plt.tight_layout()
"""

issues = dm.lint.lint(source)
for issue in issues:
    print(issue.rule_id, issue.severity, issue.message)
print(dm.lint.format_report(issues))
```

The full rule list (`figsize-direct`, `tight-layout`,
`width-token`, `oversize-width`, `fontsize-literal`,
`linewidth-literal`, `raw-hex-color`, `jet-cmap`, …) lives in
`asset/prompt/02-anti-patterns.yaml` and is also reachable as the
MCP resource `dartwork-mpl://guide/anti-patterns`.

## v0.1.x → v0.2.0

### `agent_utils` → `helpers`

Renamed to make clear these are general-purpose utilities, not
AI-agent-specific.

```python
# Old (deprecated — emits DeprecationWarning)
from dartwork_mpl import agent_utils
from dartwork_mpl.agent_utils.colors import auto_select_colors

# New (recommended)
from dartwork_mpl import helpers
from dartwork_mpl.helpers.colors import auto_select_colors
```

### `xplot` → `templates`

Renamed to better describe its purpose: a small, curated set of
ready-to-use plot templates.

```python
# Old (deprecated)
from dartwork_mpl.xplot import plot_diverging_bar
import dartwork_mpl.xplot as xp

# New
from dartwork_mpl.templates import plot_diverging_bar
import dartwork_mpl.templates as tpl

# Or — preferred — top-level
import dartwork_mpl as dm
dm.plot_diverging_bar(...)
```

## v0.3.x

### `helpers.formatting` → `helpers.labels`

The `formatting` submodule of `helpers` was renamed to `labels` to
remove a long-standing name clash with the top-level
`dartwork_mpl.formatting` module (which houses the `format_axis_*`
tick formatters). The contents (`format_axis_labels`, `optimize_legend`,
`add_value_labels`) are unchanged.

```python
# Old (deprecated)
from dartwork_mpl.helpers.formatting import format_axis_labels

# New
from dartwork_mpl.helpers.labels import format_axis_labels
# Or via the namespace:
import dartwork_mpl as dm
dm.helpers.labels.format_axis_labels(...)
```

### `asset_viz` → `diagnostics`

The four asset-inspection helpers (`classify_colormap`,
`plot_colormaps`, `plot_colors`, `plot_fonts`) moved to a single
top-level `dartwork_mpl.diagnostics` module. The old `asset_viz`
subpackage is now a thin shim that re-exports the same four names.
Behaviour is unchanged.

```python
# Old (deprecated)
from dartwork_mpl.asset_viz import plot_colors, plot_fonts

# New (canonical)
from dartwork_mpl.diagnostics import plot_colors, plot_fonts

# Best — top-level (was already supported, also unchanged)
import dartwork_mpl as dm
dm.plot_colors()
dm.plot_fonts()
```

## Worth adopting

These additions don't *require* migration but pay off if you bump
into them:

### `dm.simple_layout(fig, margin=...)`

`simple_layout` accepts a `margin` keyword (and per-side
`ml`/`mr`/`mt`/`mb` overrides) so you can declare the inset
buffer in the same call:

```python
dm.simple_layout(fig)                        # flush to figure edges
dm.simple_layout(fig, margin="2%")           # uniform 2% buffer
```

### `dm.validate_with_fixes(fig)`

A lightweight quality check that returns *and* fixes common figure
issues (margin asymmetry, pie label cutoff, etc.):

```python
result = dm.validate_with_fixes(fig)
print(result.report())
```

## Silencing legacy warnings

If you can't migrate everything immediately:

```python
import warnings
warnings.filterwarnings(
    "ignore",
    category=DeprecationWarning,
    module="dartwork_mpl",
)
```

For test runs, instead surface them so you don't lose track:

```bash
python -W default::DeprecationWarning -m pytest
```

## One-shot migration script

For larger codebases, run this once to rewrite every deprecated
import in-place. It covers the v0.2.0 / v0.3.x renames; the 0.3→0.4
width token migration is intentionally left for a manual review
because the right replacement (`dm.col1` vs `width="9cm"` vs an
explicit `dm.subplots` rewrite) depends on the surrounding code:

```python
import re
from pathlib import Path

REPLACEMENTS = [
    # Module-level renames
    (r"\bdartwork_mpl\.agent_utils\b", "dartwork_mpl.helpers"),
    (r"\bdartwork_mpl\.xplot\b", "dartwork_mpl.templates"),
    (r"\bdartwork_mpl\.asset_viz\b", "dartwork_mpl.diagnostics"),
    # Submodule rename
    (
        r"\bdartwork_mpl\.helpers\.formatting\b",
        "dartwork_mpl.helpers.labels",
    ),
]


def migrate(directory: str) -> None:
    """Rewrite deprecated imports under *directory* in-place."""
    for py in Path(directory).rglob("*.py"):
        original = py.read_text()
        updated = original
        for pattern, replacement in REPLACEMENTS:
            updated = re.sub(pattern, replacement, updated)
        if updated != original:
            py.write_text(updated)
            print(f"updated: {py}")


if __name__ == "__main__":
    import sys

    migrate(sys.argv[1] if len(sys.argv) > 1 else ".")
```

Save as `migrate_dartwork.py` and run `python migrate_dartwork.py` in
your project root. Diff the result with version control, run your
test suite, and commit.

For the 0.4 width / aspect rewrite, run `dm.lint.lint(source)` on
each file and walk the issues — every flagged line points at the
specific replacement.

## Sanity-check before upgrading to v0.5 / v1.0

Once 0.5 lands, the 0.3 width tokens (`SW`, `MW`, `TW`, `DW`,
`WIDTHS`, `FS_*`, `cm2in`) will be removed. v1.0 drops the older
`agent_utils` / `xplot` / `helpers.formatting` / `asset_viz`
shims as well. You can audit your project for them with:

```bash
grep -rE "(dartwork_mpl\.agent_utils|dartwork_mpl\.xplot|dartwork_mpl\.helpers\.formatting|dartwork_mpl\.asset_viz)" \
     --include='*.py' .

grep -rE "\bdm\.(SW|MW|TW|DW|WIDTHS|FS_[A-Z]+|cm2in)\b" \
     --include='*.py' .
```

Anything that comes back is something the next breaking release
will remove.

## Best practices going forward

1. **Use top-level imports when available.** `dm.simple_layout(fig)`
   is shorter and survives any internal reorganization that keeps the
   public API stable:

   ```python
   import dartwork_mpl as dm
   dm.simple_layout(fig)
   ```

2. **Pin a range, not an exact version.** Patch and minor releases
   shouldn't break you; majors will:

   ```toml
   # pyproject.toml
   dependencies = ["dartwork-mpl>=0.4,<1.0"]
   ```

3. **Surface deprecation warnings in CI** — add
   `-W default::DeprecationWarning` to the pytest invocation in your
   CI workflow so you find legacy usage before the next breaking
   release forces you to.

4. **Run `dm.lint` in pre-commit.** A 15-rule scan catches the
   common 0.3-isms (figsize, tight_layout, hex colors, jet cmap)
   before they hit review.

## Getting help

- Detailed version notes:
  [CHANGELOG](https://github.com/dartworklabs/dartwork-mpl/blob/main/CHANGELOG.md)
- Current API reference: [API documentation](api/index.rst)
- Open an issue: [GitHub Issues](https://github.com/dartworklabs/dartwork-mpl/issues)


---
## 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: 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 0.4
``width=...``/``aspect=...`` API and Open Color 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.
