Matplotlib Charts skill for Codex
Figures that read clearly at printed size. Codex sizes each figure for where it will appear, applies a shared style with colour-blind-safe colours and embedded fonts, and uses the object-oriented matplotlib API with seaborn for grouped statistics. Titles state the finding, lines get direct labels, and every figure comes from a headless script that Codex reruns and inspects before handing over.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- matplotlib-charts Installs to ~/.agents/skills/matplotlib-charts/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install matplotlib-charts The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project matplotlib-charts Installs to .agents/skills/matplotlib-charts/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/matplotlib-charts.zipunzip -o matplotlib-charts.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) matplotlib-charts PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $matplotlib-charts, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Produces publication-quality static charts with matplotlib and seaborn using the object-oriented API - constrained layout, exact figure sizes for papers and slides, colour-blind-safe palettes, readable ticks and date axes, direct labels instead of legends, small multiples, and vector export with embedded fonts. Use when a chart is for a paper, report, slide or README, when default matplotlib output looks cramped or unreadable, when building a repeatable house style, or when generating figures headlessly in scripts and CI.
Use it when
- Charts are headed for a paper, report, slide deck or README
- Default matplotlib output is cramped, unreadable or inconsistent
- A team needs one reusable plotting style
- Figures must be generated headlessly in scripts or CI
Use something else when
- You need interactive, browser-based charts: use d3-visualizations
- You are choosing chart types and encodings at a design level: use data-visualization-design
How to check the result
Run the figure script headless, open every output at its target size, and check titles, units, label overlap, legibility and greyscale readability.
The mistake it prevents
Creating a figure at the default size and scaling the image down later, which shrinks tick labels to unreadable sizes.
A finished chart makes one point that a reader gets in five seconds: the title states the finding, axes have units, text is legible at the size it will be printed, colours survive colour-blindness and greyscale, and the file is produced by a script that reruns without a display.
Before plotting
- Read
AGENTS.mdfor where figures live (figures/,reports/img/) and any existing style file (*.mplstyle, aplotting.pyhelper). Reuse the house style. - Ask, or decide and state: the destination (paper column, slide, web), its physical width, and the single message of the chart. Size and font follow from the destination.
- Shape data to tidy form first (one row per observation) so seaborn and loops over groups stay simple.
Run headless: import matplotlib; matplotlib.use("Agg") before pyplot in scripts, or
MPLBACKEND=Agg. Codex cannot see a figure until it is saved; save to PNG and open the
file to check it.
Size by destination
| Destination | figsize (inches) | Base font | Format |
|---|---|---|---|
| Single journal column | 3.5 x 2.6 | 8 pt | PDF or SVG, fonts embedded |
| Full-width journal figure | 7.0 x 3.5 | 8-9 pt | |
| 16:9 slide, full | 10 x 5.6 | 14-16 pt | PNG at 200 dpi or SVG |
| Web / README | 8 x 4.5 | 11-12 pt | PNG at 150-200 dpi, or SVG |
Set the size to the real output size and never rescale the image afterwards; rescaling is what produces unreadable 4 pt tick labels.
House style
# plotstyle.py
import matplotlib as mpl
def apply(base: float = 10) -> None:
mpl.rcParams.update({
"figure.dpi": 100,
"savefig.dpi": 200,
"figure.constrained_layout.use": True,
"font.size": base,
"axes.titlesize": base * 1.15,
"axes.titleweight": "bold",
"axes.titlelocation": "left",
"axes.labelsize": base,
"xtick.labelsize": base * 0.9,
"ytick.labelsize": base * 0.9,
"legend.fontsize": base * 0.9,
"legend.frameon": False,
"axes.spines.top": False,
"axes.spines.right": False,
"axes.grid": True,
"axes.grid.axis": "y",
"grid.alpha": 0.3,
"axes.prop_cycle": mpl.cycler(color=[
"#0072B2", "#E69F00", "#009E73", "#D55E00", "#CC79A7", "#56B4E9", "#F0E442", "#000000",
]), # Okabe-Ito, colour-blind safe
"pdf.fonttype": 42, # embed TrueType so text stays editable and journals accept it
"ps.fonttype": 42,
"svg.fonttype": "none",
})
Or ship it as house.mplstyle and call plt.style.use("path/to/house.mplstyle").
Built-in alternatives: plt.style.use("tableau-colorblind10").
Object-oriented pattern
Always use fig, ax = plt.subplots(...) and methods on ax; never mix with
plt.xlabel style state calls in library code.
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.ticker import PercentFormatter, FuncFormatter
import pandas as pd
import plotstyle
plotstyle.apply(base=11)
def revenue_chart(df: pd.DataFrame, out: str) -> None:
fig, ax = plt.subplots(figsize=(8, 4.5))
for i, (region, g) in enumerate(df.groupby("region")):
ax.plot(g["week"], g["revenue"], lw=2)
last = g.iloc[-1]
ax.annotate(region, (last["week"], last["revenue"]), xytext=(6, 0),
textcoords="offset points", va="center", color=f"C{i}") # direct label
ax.set_title("EU overtook US revenue in August")
ax.set_ylabel("Weekly revenue (EUR)")
ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{v / 1e3:,.0f}k"))
loc = mdates.AutoDateLocator()
ax.xaxis.set_major_locator(loc)
ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(loc))
ax.set_ylim(bottom=0)
fig.text(0.01, 0.01, "Source: marts.fct_orders, 2025-01 to 2025-09", fontsize=8, alpha=0.7)
fig.savefig(out, bbox_inches="tight")
plt.close(fig)
plt.close(fig) in loops prevents memory growth and the “more than 20 figures” warning.
Choosing the chart
| Message | Chart | Rules |
|---|---|---|
| Change over time | Line | Up to ~5 lines; more becomes small multiples |
| Compare categories | Horizontal bar, sorted | Bars start at zero; label values with ax.bar_label |
| Distribution | Histogram, ECDF, box or violin with points | Show n; log x for skewed data |
| Relationship | Scatter (alpha for density, or hexbin) | Add a fitted line only if you report it |
| Part of whole | Stacked bar or 100% bar | Pie only for 2-3 parts |
| Many groups | Small multiples, shared axes | sharex=True, sharey=True |
Seaborn
Seaborn handles grouped statistics; finish with matplotlib calls on the returned axes.
import seaborn as sns
fig, ax = plt.subplots(figsize=(6, 4))
sns.barplot(data=df, x="conversion", y="channel", errorbar=("ci", 95),
order=df.groupby("channel")["conversion"].mean().sort_values().index, ax=ax)
ax.xaxis.set_major_formatter(PercentFormatter(1.0))
ax.set(xlabel="Conversion rate", ylabel="", title="Email converts best, with a wide interval")
g = sns.relplot(data=df, x="week", y="revenue", col="region", col_wrap=3, kind="line",
height=2.4, aspect=1.4, facet_kws={"sharey": True})
g.set_titles("{col_name}")
g.figure.savefig("figures/revenue_by_region.png", bbox_inches="tight")
- Seaborn 0.12+ uses
errorbar=("sd",("ci", 95),("pi", 50),None); the oldci=argument is deprecated. - Figure-level functions (
relplot,catplot,displot) create their own figure; do not passax=. Useheightandaspectto size them. - Always state what error bars mean in the caption.
Colour
- Categorical: Okabe-Ito or
tableau-colorblind10; at most 6-8 categories, otherwise highlight one or two and grey the rest ("#B0B0B0"). - Sequential:
viridis,cividis. Diverging around a meaningful midpoint:RdBu_rwithTwoSlopeNorm(vcenter=0). Neverjetorrainbow. - Encode the same category with the same colour across every figure in a report; build a
dict
{"EU": "#0072B2", ...}and passcolor=palette[name].
Gotchas
tight_layoutand constrained layout conflict; use constrained layout only.- Dates: pandas datetimes plot fine, but string dates sort alphabetically. Convert first.
- Bar charts with a truncated y-axis exaggerate differences; lines may truncate.
- Overplotting: 100k scatter points hide density. Use
alpha=0.1,hexbinor sample, and say which. savefigaftershow()in a script writes a blank image on some backends; save first.- Unicode minus and fonts: missing glyph warnings mean the font lacks a character; pick a font that has it rather than silencing the warning.
- Rasterise only dense layers in vector output (
ax.scatter(..., rasterized=True)) to keep PDFs small while text stays vector.
Verification
MPLBACKEND=Agg python scripts/make_figures.py
python -c "from PIL import Image; im = Image.open('figures/revenue.png'); print(im.size)"
Open each output image and check: title states a finding, axis labels with units, no overlapping tick labels, legend or direct labels not covering data, text legible at the target size, and the figure looks right in greyscale. If any check fails, adjust size or font settings in the style, not by editing one figure by hand.
Delivery template
**Figures:** figures/revenue_trend.pdf (7.0 x 3.5 in), figures/conversion_by_channel.png (8 x 4.5 in, 200 dpi)
**Script:** scripts/make_figures.py (reruns headless, ~4 s)
**Style:** plotstyle.py (Okabe-Ito, fonts embedded, constrained layout)
**Data:** marts.fct_orders as of 2025-10-01
**Notes:** error bars are 95% bootstrap CIs; EU and US colours fixed across all figures What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Matplotlib Charts"
short_description: "Figures that read clearly at printed size"
default_prompt: "$matplotlib-charts Make these three charts publication-ready for a two-column paper" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.