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.

  • Data & Analytics
  • $matplotlib-charts
  • 191 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- matplotlib-charts

Installs to ~/.agents/skills/matplotlib-charts/ for every project on this machine.

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

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.

~/.agents/skills/matplotlib-charts/SKILL.md

--- name: matplotlib-charts description: Produces publication-quality static charts with matplotlib and seaborn using the object-oriented API - constrained layout, exact figure sizes for… license: MIT ---

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

  1. Read AGENTS.md for where figures live (figures/, reports/img/) and any existing style file (*.mplstyle, a plotting.py helper). Reuse the house style.
  2. 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.
  3. 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

Destinationfigsize (inches)Base fontFormat
Single journal column3.5 x 2.68 ptPDF or SVG, fonts embedded
Full-width journal figure7.0 x 3.58-9 ptPDF
16:9 slide, full10 x 5.614-16 ptPNG at 200 dpi or SVG
Web / README8 x 4.511-12 ptPNG 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

MessageChartRules
Change over timeLineUp to ~5 lines; more becomes small multiples
Compare categoriesHorizontal bar, sortedBars start at zero; label values with ax.bar_label
DistributionHistogram, ECDF, box or violin with pointsShow n; log x for skewed data
RelationshipScatter (alpha for density, or hexbin)Add a fitted line only if you report it
Part of wholeStacked bar or 100% barPie only for 2-3 parts
Many groupsSmall multiples, shared axessharex=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 old ci= argument is deprecated.
  • Figure-level functions (relplot, catplot, displot) create their own figure; do not pass ax=. Use height and aspect to 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_r with TwoSlopeNorm(vcenter=0). Never jet or rainbow.
  • Encode the same category with the same colour across every figure in a report; build a dict {"EU": "#0072B2", ...} and pass color=palette[name].

Gotchas

  • tight_layout and 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, hexbin or sample, and say which.
  • savefig after show() 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.yamlgenerated from this page

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.

esc