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.

  • namedata-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.

~/.claude/skills/data-visualization/SKILL.md

--- name: data-visualization description: Produces honest, readable charts in matplotlib, seaborn, Plotly, and Vega-Lite/Altair — chart choice from the question being answered,… license: MIT ---

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

QuestionChartAvoid
Compare categoriesHorizontal bar, sorted by valuePie, unsorted bars
Change over timeLine (continuous); bar for few discrete periodsArea charts for multiple overlapping series
Part of whole (2–4 parts)Stacked bar or single 100% barPie with many slices, donut with tiny slices
Part of whole over timeStacked area (few parts) or small multiples of linesMany stacked layers (only the bottom is readable)
DistributionHistogram, box/violin, ECDF for comparisonsBar of means without spread
Relationship of two measuresScatter (with trend line if justified)Line chart connecting unordered points
Many series over timeSmall multiples with shared axes; highlight oneSpaghetti chart with 12 colors
Ranking change between two pointsSlope chart or dumbbellGrouped bars
GeographyChoropleth of rates (per capita), not totalsRaw counts on maps (they map population)
Exact values matterA tableA 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

OutputDefault
Static figures for reports/papersmatplotlib (+ seaborn for statistical plots)
Interactive HTML, notebooks, dashboards in PythonPlotly (plotly.express)
Declarative specs, web embedding, grammar of graphicsVega-Lite (Altair in Python)
Existing app chartsThe 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

DataPalette
Categories (≤ 7)Okabe-Ito or Tableau 10; beyond 7, group into “Other” or use small multiples
Ordered magnitudeSequential single-hue or perceptually uniform (viridis, cividis)
Signed deviation around a meaningful midpointDiverging (e.g. blue–white–orange) centered on zero/target
Highlight one seriesOne 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

DestinationFormat
Web pages, docs that scaleSVG (static) or HTML (interactive)
Slides, chat, emailPNG at 2x (dpi 200)
Print / papersPDF 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 figsize for the target.
  • Plotly static export needs kaleido; without it write_image fails.
  • 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.

esc