Data Visualization skill for Claude Code
Charts that make one point, honestly. Claude starts from the takeaway sentence and picks the chart form for that question, then builds it with zero-based bar axes, small multiples instead of dual axes, a highlighted series against gray, direct labels and a title that states the finding. Before export it checks values against the data, grayscale and colorblind readability, and writes alt text.
- name
data-visualization - size179 lines · ≈2,100 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Produces honest, readable charts in matplotlib, seaborn, Plotly, and Vega-Lite/Altair — chart choice from the question being answered, zero-based bar axes, log scales and small multiples instead of dual axes, colorblind-safe palettes with sufficient contrast, takeaway titles, direct labels, annotations, and alt text. Use when plotting data in Python or JavaScript, when choosing a chart type, when a chart looks misleading or cluttered, or when preparing figures for reports, slides, or dashboards. Try asking
- Make a chart showing monthly revenue by region for the board deck
- This dual-axis chart is confusing. Redesign it
- Which chart should I use to compare conversion rates across 12 countries?
Use it when
- You are plotting data for a report, slide, notebook, or web page
- You are unsure which chart type fits the question
- A chart looks cluttered, misleading, or hard to read
- Figures must be accessible to colorblind readers
Skip it when
- You are designing a full dashboard layout and its components — use dashboard-ui instead
- You need to define a brand color palette from scratch — use color-systems instead
How to check the result
Plotted values match a printed summary of the data, bars start at zero, the chart reads correctly in grayscale and a colorblind simulator, and alt text states the takeaway.
The common mistake it prevents
Truncating the y-axis of a bar chart so a 3% difference looks like a doubling.
A chart is done when a reader gets the intended takeaway in five seconds, the visual encoding cannot mislead (axes, areas, and colors are proportional and consistent), it works for colorblind readers and in grayscale, and every number shown matches the data.
Ground rules
- Start from the question and the one sentence the chart must support; pick the chart for that, not for variety.
- Bars encode length, so bar axes start at zero. Line charts may zoom, but label it.
- No dual y-axes, no 3D, no pie charts with more than four slices, no rainbow colormaps.
- Color carries meaning or nothing: highlight the series that matters, gray the rest.
- Title states the finding (“EMEA revenue grew 18% while APAC stayed flat”); subtitle gives units, period, and filters; footer gives source and date.
- Label directly on the chart where possible instead of a distant legend.
- Every chart ships with alt text describing the takeaway and the data shape.
Workflow
- [ ] 1. Write the question and the takeaway sentence
- [ ] 2. Prepare tidy data (one row per observation) and check the numbers
- [ ] 3. Choose the chart form (table below)
- [ ] 4. Build with the project's library; apply the style defaults
- [ ] 5. Annotate: takeaway title, direct labels, reference lines, units
- [ ] 6. Check: honesty, accessibility, size, numbers vs. data
- [ ] 7. Export in the right format with alt text and caption
3. Choose the form
| Question | Chart | Avoid |
|---|---|---|
| Compare categories | Horizontal bar, sorted by value | Pie, unsorted bars |
| Change over time | Line (continuous); bar for few discrete periods | Area charts for multiple overlapping series |
| Part of whole (2–4 parts) | Stacked bar or single 100% bar | Pie with many slices, donut with tiny slices |
| Part of whole over time | Stacked area (few parts) or small multiples of lines | Many stacked layers (only the bottom is readable) |
| Distribution | Histogram, box/violin, ECDF for comparisons | Bar of means without spread |
| Relationship of two measures | Scatter (with trend line if justified) | Line chart connecting unordered points |
| Many series over time | Small multiples with shared axes; highlight one | Spaghetti chart with 12 colors |
| Ranking change between two points | Slope chart or dumbbell | Grouped bars |
| Geography | Choropleth of rates (per capita), not totals | Raw counts on maps (they map population) |
| Exact values matter | A table | A chart forcing readers to estimate |
Two measures with different units: two aligned charts sharing the x-axis, or index both to 100 at a start date. Not a dual axis.
4. Library choice
| Output | Default |
|---|---|
| Static figures for reports/papers | matplotlib (+ seaborn for statistical plots) |
| Interactive HTML, notebooks, dashboards in Python | Plotly (plotly.express) |
| Declarative specs, web embedding, grammar of graphics | Vega-Lite (Altair in Python) |
| Existing app charts | The project’s library (Recharts, ECharts, Observable Plot, etc.) |
matplotlib defaults that fix most ugliness:
import matplotlib.pyplot as plt
OKABE_ITO = ["#0072B2", "#E69F00", "#009E73", "#D55E00", "#CC79A7", "#56B4E9", "#F0E442", "#000000"]
plt.rcParams.update({
"axes.prop_cycle": plt.cycler(color=OKABE_ITO),
"axes.spines.top": False, "axes.spines.right": False,
"axes.titlelocation": "left", "axes.titleweight": "bold",
"font.size": 11, "figure.dpi": 110,
})
fig, ax = plt.subplots(figsize=(8, 4.5), layout="constrained")
for region, g in df.groupby("region"):
highlight = region == "EMEA"
ax.plot(g["month"], g["revenue"] / 1e6,
color=OKABE_ITO[0] if highlight else "#B0B0B0",
linewidth=2.5 if highlight else 1.2, zorder=3 if highlight else 2)
last = g.iloc[-1]
ax.annotate(region, (last["month"], last["revenue"] / 1e6), xytext=(5, 0),
textcoords="offset points", va="center",
color=OKABE_ITO[0] if highlight else "#707070")
ax.set_title("EMEA revenue grew 18% since January; other regions flat")
ax.set_ylabel("Revenue (USD millions)")
ax.set_ylim(bottom=0)
ax.grid(axis="y", alpha=0.3)
fig.text(0.01, -0.02, "Source: orders warehouse, net of refunds. Data through 2026-09-30.", fontsize=9, color="#555")
fig.savefig("out/revenue_by_region.png", dpi=200, bbox_inches="tight")
fig.savefig("out/revenue_by_region.svg", bbox_inches="tight")
Plotly:
import plotly.express as px
fig = px.bar(df_sorted, x="revenue", y="region", orientation="h", text_auto=".2s",
color_discrete_sequence=["#0072B2"],
title="EMEA leads Q3 revenue<br><sup>USD, net of refunds, Jul–Sep 2026</sup>")
fig.update_layout(template="simple_white", yaxis={"categoryorder": "total ascending"},
xaxis_title="Revenue (USD)", yaxis_title=None, margin={"l": 10, "r": 10})
fig.write_html("out/q3_revenue.html", include_plotlyjs="cdn")
fig.write_image("out/q3_revenue.png", scale=2) # requires the kaleido package
Vega-Lite spec (portable JSON):
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {"text": "Weekly signups doubled after the pricing change", "subtitle": "Signups per week, 2026"},
"data": {"url": "signups.csv"},
"mark": {"type": "line", "point": true},
"encoding": {
"x": {"field": "week", "type": "temporal", "title": null},
"y": {"field": "signups", "type": "quantitative", "title": "Signups", "scale": {"zero": true}}
}
}
5. Color
| Data | Palette |
|---|---|
| Categories (≤ 7) | Okabe-Ito or Tableau 10; beyond 7, group into “Other” or use small multiples |
| Ordered magnitude | Sequential single-hue or perceptually uniform (viridis, cividis) |
| Signed deviation around a meaningful midpoint | Diverging (e.g. blue–white–orange) centered on zero/target |
| Highlight one series | One saturated color + gray for everything else |
Graphical elements need at least 3:1 contrast against the background (WCAG 1.4.11); text needs 4.5:1. Do not encode meaning by color alone: add labels, markers, or line styles. Keep a category’s color identical across every chart in the same report.
6. Check before shipping
- Numbers on the chart match a printed summary of the plotted data
- Bar/area axes start at zero; any truncated line axis is labeled and justified
- Same scale across small multiples (or explicitly marked as independent)
- Log scale labeled (“log scale”) and used only when values span orders of magnitude
- Readable in grayscale and with a colorblind simulator (Coblis, Chrome DevTools “Emulate vision deficiencies”)
- Text readable at the final display size (slide, phone, print); no overlapping labels
- Units, period, filters, source, and data date present
- Incomplete periods (current month) marked or excluded
7. Export
| Destination | Format |
|---|---|
| Web pages, docs that scale | SVG (static) or HTML (interactive) |
| Slides, chat, email | PNG at 2x (dpi 200) |
| Print / papers | PDF or SVG (vector) |
Alt text template: “Line chart of monthly revenue by region, January to September 2026. EMEA rises from 1.2M to 1.9M USD; APAC, LATAM, and NA stay between 0.6M and 0.9M.”
Gotchas
- Pandas plotting on a datetime index with missing months draws a straight line across the gap; reindex to a full date range first.
- Area and bubble sizes must scale by area, not radius (
s=in matplotlib is area). - Averages hide distributions; show spread (box, interval, or points) when comparing groups.
- Cumulative charts always go up and make everything look like growth; label clearly.
- Default matplotlib figure sizes produce tiny text on slides; set
figsizefor the target. - Plotly static export needs
kaleido; without itwrite_imagefails. - Percent change on small bases (“+300%”) misleads; show absolute values alongside.
Output
**Chart:** out/revenue_by_region.svg (+ .png at 2x)
**Takeaway (title):** EMEA revenue grew 18% since January; other regions flat
**Form:** line chart, EMEA highlighted, others gray, direct labels, y from zero
**Checks:** values match summary table; colorblind and grayscale OK; readable at 960 px
**Alt text:** Line chart of monthly revenue by region, Jan–Sep 2026. EMEA rises from 1.2M to 1.9M USD; other regions stay between 0.6M and 0.9M. About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.