Write, review, refactor, or debug Python code that uses matplotlib (plt.subplots, Axes, Figure, savefig, plt.plot, colormaps, legends, datetime axes) using one canonical, modern idiom set. Use this skill whenever code creates plots or charts, saves figures to files, builds subplot grids, styles axes, fixes "RuntimeWarning: More than 20 figures have been opened," debugs blank or cut-off saved images, hits "no display name and no $DISPLAY" in headless environments, or migrates off deprecated APIs (plt.get_cmap, fig.tight_layout retrofits, the pyplot state machine). Trigger it even when the user just says "plot this data," "make a chart of ...," or shows a stack trace mentioning matplotlib — without saying the word "matplotlib idioms."
How this skill is triggered — by the user, by Claude, or both
Slash command
/matplotlib-consistency:matplotlib-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
matplotlib is stable and extremely well known, yet generated code drifts between two
matplotlib is stable and extremely well known, yet generated code drifts between two
parallel APIs: the implicit pyplot state machine (plt.plot, plt.xlabel, plt.title)
inherited from MATLAB tutorials, and the explicit object-oriented API (fig, ax = plt.subplots() then ax.plot(...)). Mixing them produces code that targets "the current
axes" by accident — wrong subplot, wrong figure, leaked memory in loops. This skill pins
one canonical idiom set — matplotlib 3.x object-oriented style — so every snippet you
produce or review follows the same rules instead of mixing eras.
| Always | Never | Why |
|---|---|---|
fig, ax = plt.subplots() then ax.plot(...) | plt.plot(...), plt.xlabel(...) in durable code | The state machine targets an invisible "current axes"; with multiple figures/subplots it silently draws on the wrong one. |
ax.set_xlabel/set_ylabel/set_title | plt.xlabel/plt.ylabel/plt.title mixed with OO code | Mixing styles in one script means some calls hit ax, others hit whatever is "current." |
fig, axs = plt.subplots(2, 2, layout="constrained") | plt.tight_layout() retrofitted at the end | Constrained layout is computed during draw and handles colorbars/legends; tight_layout is a one-shot retrofit that misses them. |
plt.close(fig) after fig.savefig in loops/servers | letting figures accumulate | pyplot keeps a global registry; unclosed figures leak memory until the "More than 20 figures" warning — or OOM. |
fig.savefig(path, dpi=150, bbox_inches="tight") | plt.savefig() after plt.show() | plt.show() can consume/clear the figure in some backends; saving after it yields blank files. Save first, or skip show() in scripts. |
matplotlib.use("Agg") before import matplotlib.pyplot (or just savefig, no show) | calling plt.show() in headless scripts/CI | No display → backend errors or hangs; Agg renders straight to files. |
matplotlib.colormaps["viridis"] or plt.get_cmap("viridis") with no second arg | plt.cm.get_cmap("viridis", N) / matplotlib.cm.get_cmap | matplotlib.cm.get_cmap was deprecated in 3.7 and removed in 3.9; the registry matplotlib.colormaps[...] is the modern access path (resample via .resampled(N)). |
ax.plot(x, y, label="series") then ax.legend() | ax.legend(["series"]) positional lists | Positional legend lists pair labels to artists by draw order; one added artist silently mislabels everything. |
fig.autofmt_xdate() or mdates locators/formatters | manual set_xticklabels with hand-rotated strings | Hand-set tick labels detach from the data; zooming, resizing, or new data ranges make them silently wrong. |
df.plot(ax=ax) / sns.lineplot(..., ax=ax) | re-implementing grouped/agg plots with loops | pandas and seaborn already render onto a passed ax; hand-rolled loops re-invent and often mis-aggregate. |
ax2 = ax.twinx() for a second y-scale | overlaying two plt.plot calls with rescaled data | Manual rescaling fakes a second axis and makes the right-hand values unreadable/wrong. |
plt.style.use(...) / rcParams once at the top | per-call styling sprinkled everywhere | One theming point keeps figures consistent; scattered kwargs drift. |
House style for a complete figure:
import matplotlib.pyplot as plt
fig, axs = plt.subplots(1, 2, figsize=(10, 4), sharey=True, layout="constrained")
axs[0].plot(dates, revenue, label="revenue")
axs[0].set_title("Revenue")
axs[0].set_xlabel("Date")
axs[0].set_ylabel("EUR")
axs[0].legend()
axs[1].bar(regions, totals, color="tab:blue")
axs[1].set_title("Totals by region")
fig.autofmt_xdate()
fig.savefig("report.png", dpi=150, bbox_inches="tight")
plt.close(fig)
fig2, ax2 = plt.subplots(), a stray plt.title(...)
styles fig2 even if you meant the first figure. Route every call through the
fig/ax objects you hold.show(): in interactive backends the figure may be torn down when the
window closes; the subsequent savefig writes an empty canvas. Always save first.plt.subplots() inside a for loop without plt.close(fig)
retains every figure in pyplot's registry — web servers and batch jobs die slowly. For
fully pyplot-free rendering, build Figure() directly with the Agg canvas.bbox_inches="tight" (or constrained layout) fixes it; don't shrink fonts to
compensate.sharex=True/sharey=True, setting limits or scales on
one axes changes all of them — that is the point, but do it once, deliberately."1", "10", "2" plots
them as categories in data order, not numeric order. Convert to numbers/datetimes first.ax.legend() with no labeled artists emits a warning and draws nothing — label at
plot time, and use label="_nolegend_" to exclude helpers.set_xticks with positions computed once go stale; use
matplotlib.dates.AutoDateLocator/ConciseDateFormatter so ticks track the data.matplotlib.colormaps["viridis"] is read-only and not
resampled; use .resampled(N) for N discrete colors rather than indexing tricks.Target matplotlib 3.x (3.5+). The key breaking line in recent memory is 3.7→3.9:
matplotlib.cm.get_cmap and friends were deprecated in 3.7 and removed in 3.9 — use
matplotlib.colormaps[name]. layout="constrained" (3.5+) supersedes the older
constrained_layout=True kwarg, which still works; both beat tight_layout retrofits.
Seaborn ≥0.12 and pandas .plot both accept ax= — compose, don't re-implement.
matplotlib.use("Agg") before importing pyplot, and never call
plt.show().fig, ax = plt.subplots(..., layout="constrained") and route
every plotting and styling call through fig/ax — no bare plt.* drawing calls.label=), then ax.legend(); set titles/labels via
ax.set_*.fig.savefig(path, dpi=..., bbox_inches="tight"), then plt.close(fig) —
mandatory inside loops and long-running processes.For the fuller migration map (old API → modern API), expanded gotcha explanations, and
more worked examples, read references/matplotlib-patterns.md.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub guidogl/matplotlib-consistency --plugin matplotlib-consistency