Help us improve
Share bugs, ideas, or general feedback.
From cafleet
Creates matplotlib charts with consistent color palettes, saving scripts and PNG outputs to structured subdirectories under a configurable base path.
npx claudepluginhub himkt/cafleet --plugin cafleetHow this skill is triggered — by the user, by Claude, or both
Slash command
/cafleet:create-figureThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Generate matplotlib charts. Scripts, outputs, and data go in separate subdirectories under `figures/`.
Generates charts like bar, line, pie, scatter, heatmaps from data using Matplotlib. Analyzes structure, customizes styles, adds interactivity, exports to PNG, SVG, HTML.
Guides chart selection for data relationships and generates Python visualization code with matplotlib, seaborn using professional styles, palettes, and accessibility principles.
Generates publication-quality matplotlib/seaborn charts and diagrams with colorblind-accessible palettes, despined axes, and rich annotations using specific aesthetics. Use for data visualizations, plots, or diagrams.
Share bugs, ideas, or general feedback.
Generate matplotlib charts. Scripts, outputs, and data go in separate subdirectories under figures/.
The skill writes a self-contained Python script that imports matplotlib. The run command is host-project-specific — refer to your project's .claude/rules/ for the canonical Python invocation. Any environment that provides matplotlib will run the script.
Before writing any script, read the Chart Type Selection and Color Rules sections. All charts in a deck share the same C_BAR / C_BAR_SEC palette regardless of data topic.
CRITICAL — placeholder convention. ${FIGURE_BASE}, ${BASE}, ${SRC_DIR}, ${OUTPUT_DIR}, and ${DATA_DIR} in this document are template placeholders, NOT shell environment variables. You must mentally resolve each one to a concrete absolute path and write that literal path into the script file via the Write tool.
Do NOT run export FIGURE_BASE=..., FIGURE_BASE=... uv run ..., or any other shell variable assignment. Bash calls in Claude Code are ephemeral — values set in one call do not persist to the next. The placeholders are resolved entirely in your head, not in the shell.
Resolve ${BASE} in this order:
/cafleet:research-presentation passes its research folder as the figure base), use that path literally as ${BASE}. Skip base-dir resolution.Skill(cafleet:base-dir) and follow its procedure (no path argument; CWD-based inference applies). Use the resolved ${BASE} verbatim. Figures, scripts, and data land under ${BASE}/figures/{src,output,data} regardless of whether ${BASE} is a git-repo root, /tmp/claude-code, or any other path. If you want figures kept out of a repo tree, pick /tmp/claude-code (or any non-repo path) at base-dir resolution time — create-figure does NOT second-guess base-dir's answer.Derive the subdirectories (each is a literal path string you will embed in the script):
${SRC_DIR} = ${BASE}/figures/src${OUTPUT_DIR} = ${BASE}/figures/output${DATA_DIR} = ${BASE}/figures/dataExample resolution: if base-dir resolved ${BASE} = /home/user/proj (a git-repo root), then ${SRC_DIR} in your head is /home/user/proj/figures/src and figures land under the repo tree — that is the intended behavior. If base-dir resolved ${BASE} = /tmp/claude-code instead, ${SRC_DIR} is /tmp/claude-code/figures/src. Either way, the literal string you write into the Python script is the concrete resolved path.
If the directories do not exist yet, the Write tool auto-creates parent directories when you write the script file — do NOT call mkdir.
All subsequent steps use ${SRC_DIR}, ${OUTPUT_DIR}, and ${DATA_DIR} as literal resolved paths. Figure artifacts always live under ${BASE}/figures/; never directly at ${BASE} and never at /tmp unless ${BASE} itself is /tmp/claude-code.
Font: No setup needed. The theme font Noto Sans is available as a system font. Scripts set plt.rcParams['font.family'] = 'Noto Sans' (see template below).
Use the Write tool to create a .py file in ${SRC_DIR}.
The script must follow this pattern. Replace ${OUTPUT_DIR} and ${DATA_DIR} below with the literal concrete paths you resolved in Step 0 — the Python source you write must contain real path strings, not ${...} syntax:
import pathlib
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# REPLACE these with literal paths from Step 0 resolution.
# e.g. pathlib.Path("/tmp/claude-code/researches/foo/figures/output")
OUTPUT_DIR = pathlib.Path("${OUTPUT_DIR}")
DATA_DIR = pathlib.Path("${DATA_DIR}")
plt.rcParams['font.family'] = 'Noto Sans'
# Data input
data_path = DATA_DIR / "data.csv"
# Create the figure
fig, ax = plt.subplots(constrained_layout=True)
# ... plotting logic ...
# Output — filename matches script name
script_stem = pathlib.Path(__file__).stem
output_path = OUTPUT_DIR / f"{script_stem}.png"
plt.savefig(output_path, dpi=150, bbox_inches="tight", facecolor='white')
plt.close()
print(f"Saved: {output_path}")
Key points:
ax.set_title() — when embedded in slides, the slide heading is the titlematplotlib.use("Agg") before import matplotlib.pyplotchart.py → chart.png)plt.show(), always plt.savefig() then plt.close()dpi=150, bbox_inches="tight", facecolor='white'Run the script with the Python invocation documented in your host project's .claude/rules/. This skill is invocation-agnostic — it only requires that the chosen environment provide matplotlib:
<project-python-runner> ${SRC_DIR}/script_name.py
The host project's rules document the project-specific runner (typical patterns include uv run, python, or a mise task wrapper). The skill itself does not name a runner.
Use the Read tool to load the output PNG from ${OUTPUT_DIR} and show it to the user.
CSV:
import csv
with open(DATA_DIR / "sales.csv") as f:
rows = list(csv.DictReader(f))
JSON:
import json
with open(DATA_DIR / "metrics.json") as f:
data = json.load(f)
Inline data (from web search or user input):
categories = ["Q1", "Q2", "Q3", "Q4"]
values = [120, 185, 240, 310]
Pick the right visualization for the data. Do NOT default to bar charts for everything.
| Data pattern | Chart type | matplotlib |
|---|---|---|
| Category comparison, ranking | Horizontal bar | ax.barh() |
| Time series, trend | Line chart | ax.plot() — format dates with mdates.DateFormatter (see below) |
| Correlation between 2 variables | Scatter plot | ax.scatter() |
| Distribution of one variable | Histogram | ax.hist() |
| Distribution comparison across groups | Box plot or violin plot | ax.boxplot() / ax.violinplot() |
| Matrix, cross-tabulation | Heatmap | ax.imshow() + annotate |
| Part-of-whole composition | Stacked bar | ax.bar(bottom=...) |
| Before/after, paired comparison | Dumbbell chart | ax.hlines() + ax.scatter() |
| Time-based categories (quarters, years) | Vertical bar | ax.bar() |
Horizontal vs vertical bars: Prefer barh when category labels are text. Use vertical bar only for time-based x-axes.
import matplotlib.dates as mdates
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
fig.autofmt_xdate() # auto-rotate date labels
For side-by-side comparison panels, use subplot_mosaic or GridSpec. All panels must share the same Y-axis range (see Prohibited section).
# Named panels — cleaner than index-based access
fig, axes = plt.subplot_mosaic(
[['left', 'right']],
figsize=(12, 5), constrained_layout=True,
)
axes['left'].barh(categories, values_a, color=C_BAR)
axes['right'].barh(categories, values_b, color=C_BAR_SEC)
# Complex layouts with GridSpec
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(12, 8), constrained_layout=True)
gs = GridSpec(2, 3, figure=fig)
ax_wide = fig.add_subplot(gs[0, :]) # top row, full width
ax_left = fig.add_subplot(gs[1, :2]) # bottom row, 2/3 width
ax_right = fig.add_subplot(gs[1, 2]) # bottom row, 1/3 width
1 chart = max 2 colors. This is the single most important design rule. Violating it produces amateurish, noisy charts.
These approximate the Slidev theme's CSS tokens (defined in OKLCH inside the my-slidev skill's theme stylesheet). When updating colors, keep both in sync.
# Primary (use for most bars/lines)
C_BAR = '#3B82F6' # blue-500
# Muted secondary (use for secondary series, negative values, or contrast)
C_BAR_SEC = '#94A3B8' # slate-400
# Accent (use for ONE highlighted item only — never for multiple bars)
C_ACCENT = '#1E40AF' # blue-800 (darker shade of primary)
# Negative accent (use only when one specific item is the "worst")
C_NEGATIVE = '#DC2626' # red-600
# Spine / grid
C_TEXT = '#1E293B'
C_TEXT_SEC = '#64748B'
C_GRID = '#E2E8F0'
| Data pattern | Colors to use |
|---|---|
| Single series, all same type | C_BAR for all |
| Single series, grouping by category | Lightness steps of blue (#1E40AF / #3B82F6 / #93C5FD) |
| Two series (e.g., Verified vs Pro) | C_BAR + C_BAR_SEC |
| Positive vs negative values | C_BAR (positive) + C_BAR_SEC (negative) |
| Highlight one worst item | C_BAR_SEC for all + C_NEGATIVE for worst |
| Highlight one best/top item | C_BAR for all + C_ACCENT (darker) for top |
Use annotations to call out a specific data point instead of (or alongside) color highlighting:
ax.annotate('Peak', xy=(x_peak, y_peak),
xytext=(x_peak + offset, y_peak + offset),
fontsize=10, color=C_TEXT,
arrowprops=dict(arrowstyle='->', color=C_TEXT_SEC, lw=1.5))
Keep annotation text short (1–3 words). Use C_TEXT for text, C_TEXT_SEC for arrows.
C_BAR (blue), not red. An adoption chart uses C_BAR, not green. Data meaning comes from axis labels and slide context. All charts in a deck must look like they belong togetherC_NEGATIVE or C_ACCENT for at most 1 highlighted item; everything else is C_BAR or C_BAR_SECC_NEGATIVE / C_ACCENT as primary color — highlight colors are only for emphasizing a single item, never for all data pointsApply to every figure:
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color(C_GRID)
ax.spines['bottom'].set_color(C_GRID)
ax.tick_params(colors=C_TEXT_SEC)
ax.yaxis.grid(True, alpha=0.3, color='#CBD5E1')
ax.set_axisbelow(True)
Always use facecolor='white' in plt.savefig().
When a legend is needed, place it outside the plot area to avoid obscuring data:
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', frameon=False)
For horizontal legends below the chart (useful in multi-series line plots):
ax.legend(bbox_to_anchor=(0.5, -0.12), loc='upper center', ncol=3, frameon=False)
Use frameon=False to keep the look clean. Limit legend entries to ≤ 5; if more, reconsider the chart design.
plt.show()