Skill: Visualization Patterns
SkillDatabases & dataApply whenever you generate any chart, graph, or data visualization — from SQL results, in the chart-maker agent, in decks, or on "make a chart / visualize / plot / dashboard" requests. Enforces Storytelling With Data: gray first, one focus color, action titles, direct labels, no pies, using the helpers in helpers/viz/chart_helpers.py.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Skill: Visualization Patterns skill
What this skill tells your AI
The instructions your AI receives, as published by ai-analyst-lab/ai-analyst in .claude/skills/visualization-patterns/SKILL.md and read by ahel’s review.
Purpose
Ensure every chart Claude Code produces follows high-quality design standards with named themes, consistent styling, and clear data communication.
When to Use
Apply this skill whenever generating a chart, graph, or data visualization.
When someone asks for a pie chart
Do not make the pie. Make the sorted bar (or a single stacked bar for parts of a whole), say in one sentence why (angles and areas are hard to compare; a bar shows the same shares with direct labels), and offer the pie only if they insist after seeing the bar. Producing both "to be safe" is not the standard; the bar is the deliverable.
Default Theme: Minimal
When no theme or palette is set, use the default styling without asking; offer palette options only when the user asks about themes or colors.
The minimal theme is clean, professional, and suitable for most business contexts:
- Warm off-white background (#F7F6F2) for reduced eye strain
- Focus blue accent (#0072B2, an Okabe-Ito color) for the one element the takeaway argues
- Helvetica font family
- Left-aligned titles, minimal gridlines
The swd_style() function automatically loads the minimal theme. Other available themes: nyt, economist, corporate (see Theme Definitions section).
Instructions
STEP 1: Import the SWD helpers
Start every chart with the helpers in helpers/viz/chart_helpers.py; they set the R3 background, gray-plus-accent palette, direct labels and spine cleanup that the checkpoints verify.
from helpers.viz.chart_helpers import (
swd_style, # Apply SWD matplotlib theme
highlight_bar, # Bar chart with one bar highlighted
highlight_line, # Line chart with one series highlighted
action_title, # Action title + subtitle
save_chart # Save with correct DPI and tight layout
)
# Apply SWD style FIRST (loads .mplstyle, returns color palette)
colors = swd_style()
If helpers/viz/chart_helpers.py doesn't exist: Inform the user that chart helpers are missing and you'll need to implement SWD principles manually. Then proceed with manual matplotlib following the SWD principles below.
STEP 2: Choose Your Helper Function
Use the pre-built helpers instead of manual matplotlib code:
| Chart Type | Helper Function | Example Usage |
|---|---|---|
| Bar chart | highlight_bar() | fig, ax = plt.subplots(figsize=(10, 6))highlight_bar(ax, categories=['Desktop', 'Tablet', 'Mobile'], values=[4.6, 4.1, 3.4], highlight='Desktop')action_title(ax, 'Desktop converts best at 4.6%') |
| Line chart | highlight_line() | fig, ax = plt.subplots(figsize=(10, 6))highlight_line(ax, x=months, y_dict={'Revenue': revenue_values}, highlight='Revenue')action_title(ax, 'Revenue grew 43% after pricing launch') |
| Title only | action_title() | action_title(ax, title='Finding here', subtitle='Context: time range, data source, sample size') |
Key points:
- Helpers take a matplotlib
axobject + arrays of data (not DataFrames) - Always create the figure first:
fig, ax = plt.subplots(figsize=(10, 6)) - Helpers automatically apply: gray + accent color, direct labels, SWD styling
- The
highlightparameter specifies which category/series to emphasize
For funnel charts, heatmaps, or custom visualizations: Use manual matplotlib but apply swd_style() first and follow the SWD principles below.
STEP 3: Save Chart to Correct Location
# Final deliverable charts
save_chart(fig, "outputs/conversion_by_device.png")
# Exploratory/intermediate charts
save_chart(fig, "working/exploration_chart.png")
Naming convention: {metric}_{dimension}_{chart_type}.png (e.g., revenue_trends_line.png)
Pre-flight: Load Learnings (Optional)
Check .knowledge/learnings/index.md for relevant entries:
- Read the file. If it doesn't exist or is empty, skip silently.
- Scan for entries under "Chart Style" and "General" headings.
- If entries exist, incorporate them as constraints (e.g., preferred chart types, color overrides).
- Never block execution if learnings are unavailable.
Core Principle: Storytelling with Data (SWD)
Every chart follows the SWD methodology by Cole Nussbaumer Knaflic:
Gray everything first. Color is reserved for the one data point that tells the story.
- Mostly gray. One focus accent, blue (
#0072B2), for the element the takeaway argues; a second accent, orange (#D55E00), only for a genuine two-focal or good-vs-bad case. Both are Okabe-Ito colors, so the pair is colorblind-safe (blue vs orange, never red vs green). Everything else is gray. Use up to 5 Okabe-Ito categoricals ONLY when categories are truly independent; more than that is a signal to rethink the chart, never to add hues. (Amber#D97706is the deck/thumbnail brand color; it is not a chart focus color.) - Titles state the takeaway, not a description. "iOS drove the June ticket spike" not "Tickets by Platform."
- Every visual element must earn its place — if it doesn't help the reader understand the story, remove it.
- Prefer text over charts for single numbers. Prefer horizontal bars over pie charts. Prefer direct labels over legends.
Why use the helpers: They enforce these principles automatically. Manual matplotlib code often forgets to remove borders, uses rainbow colors, or includes legends. The helpers prevent these mistakes.
Declutter Checklist
Before finalizing any chart, verify each item:
- Chart border / box — removed entirely
- Top and right spines — removed (keep only bottom and left)
- Heavy gridlines — removed or very light gray (
#E5E7EB), y-axis only - Data markers — removed from line charts (the line is the data)
- Legend — replaced with direct labels on the data
- Rotated axis text — if labels need rotation, switch to horizontal bars
- Trailing zeros — use
$45not$45.00; use12%not12.0% - 3D effects — never
- Background color — always warm off-white (
#F7F6F2) - Redundant axis labels — if the title says "Revenue ($M)", the y-axis doesn't need "Revenue in Millions of Dollars"
- Excessive tick marks — reduce to 4-6 ticks maximum
- Decimal precision — match the precision to the decision (
12%not12.347%)
Chart Sequencing (Multi-Chart Analyses)
When producing multiple charts for a deep dive or root cause investigation, follow Context → Tension → Resolution:
| Phase | Charts | Purpose | Example |
|---|---|---|---|
| Context | 1-2 | Set the baseline. What does normal look like? | "[Dataset] processes ~4,000 support tickets per month" |
| Tension | 2-3 | Reveal the problem. Progressively zoom in. | "June spiked to 6,200" → "The spike was iOS payment issues" |
| Resolution | 1-2 | Explain why and recommend action. | "iOS v2.3 introduced a bug → fix eliminates ~2,200 tickets/mo" |
- Each chart builds on the previous one
- Never show a chart that makes the audience ask "so what?"
- The number of charts is determined by the storyboard. Each narrative beat that requires a visualization becomes a chart.
- The final chart should make the recommended action obvious
Chart Helper Functions Reference
All chart helpers live in helpers/viz/chart_helpers.py. The style file is helpers/viz/analytics_chart_style.mplstyle. The full style guide with before/after examples is in helpers/viz/chart_style_guide.md.
| Function | Purpose | Key Args |
|---|---|---|
swd_style() | Apply SWD matplotlib style, return color palette | — |
highlight_bar() | Bar chart with one bar highlighted, rest gray | highlight=, horizontal=True, sort=True |
highlight_line() | Line chart with one line colored, rest gray | highlight=, y_dict={} |
action_title() | Bold takeaway title + optional subtitle | title, subtitle= |
annotate_point() | Clean annotation with arrow | x, y, text, offset= |
save_chart() | Tight layout + correct DPI | fig, path, dpi=150 |
stacked_bar() | Stacked / 100% stacked (normalize=True) bar | highlight_layer=, normalize= |
share_bar() | Single horizontal 100% stacked bar (pie replacement) | parts={}, highlight= |
slope_chart() | Two-time-point change across items | start_col, end_col, highlight_label= |
funnel_waterfall() | Funnel drop-off; highlights the biggest drop | highlight_step= |
retention_heatmap() | Cohort retention as a blue-sequential table | keeps numbers in cells |
big_number() | One number as text (the no-chart default for 1-2 numbers) | value, label=, delta= |
bullet() | One metric vs target (gauge replacement) | value, target, ranges= |
end_label() | Direct end-of-line label (replaces a legend entry) | x, y, text, color= |
reference_line() | Goal/threshold line with an inline label | value, label=, orient= |
Theme Definitions
Theme: nyt (New York Times)
NYT_THEME = {
"colors": {
"primary": "#000000",
"secondary": "#666666",
"accent": "#D03A2B",
"palette": ["#D03A2B", "#1A6B54", "#3D6CA3", "#E8912D", "#8B5E3C", "#6B4C9A"],
"background": "#FFFFFF",
"grid": "#E5E5E5",
},
"fonts": {
"title": {"family": "Georgia", "size": 18, "weight": "bold"},
"subtitle": {"family": "Arial", "size": 12, "weight": "normal", "color": "#666666"},
"axis_label": {"family": "Arial", "size": 10},
"annotation": {"family": "Arial", "size": 9, "style": "italic"},
},
"grid": {"show": True, "axis": "y", "style": "--", "alpha": 0.3},
"annotations": {"style": "minimal", "callout_arrows": True},
"title": {"position": "left-aligned", "include_subtitle": True},
}
Theme: economist (The Economist)
ECONOMIST_THEME = {
"colors": {
"primary": "#1F2E3C",
"secondary": "#7C8A96",
"accent": "#E3120B",
"palette": ["#E3120B", "#1F6ED4", "#36B37E", "#F5A623", "#6554C0", "#00B8D9"],
"background": "#D7E4E8",
"grid": "#FFFFFF",
},
"fonts": {
"title": {"family": "Helvetica", "size": 16, "weight": "bold"},
"subtitle": {"family": "Helvetica", "size": 11, "weight": "normal"},
"axis_label": {"family": "Helvetica", "size": 9},
"annotation": {"family": "Helvetica", "size": 8},
},
"grid": {"show": True, "axis": "y", "style": "-", "alpha": 0.5, "color": "#FFFFFF"},
"annotations": {"style": "inline", "red_highlight": True},
"title": {"position": "left-aligned", "red_bar_top": True},
}
Theme: minimal
MINIMAL_THEME = {
"colors": {
"primary": "#1F2937",
"secondary": "#4B5563",
"accent": "#0072B2",
"palette": ["#0072B2", "#D55E00", "#009E73", "#CC79A7", "#404040"],
"background": "#F7F6F2",
"grid": "#F0F0F0",
},
"fonts": {
"title": {"family": "Helvetica", "size": 14, "weight": "bold"},
"subtitle": {"family": "Helvetica", "size": 10, "weight": "normal", "color": "#666666"},
"axis_label": {"family": "Helvetica", "size": 9},
"annotation": {"family": "Helvetica", "size": 8},
},
"grid": {"show": True, "axis": "y", "style": "-", "alpha": 0.15},
"annotations": {"style": "minimal", "direct_labels": True},
"title": {"position": "left-aligned", "include_subtitle": True},
}
Theme: corporate
CORPORATE_THEME = {
"colors": {
"primary": "#1B2A4A",
"secondary": "#5A6B7F",
"accent": "#0066CC",
"palette": ["#0066CC", "#00A651", "#FF6600", "#CC0000", "#9933CC", "#00CCCC"],
"background": "#FFFFFF",
"grid": "#E8E8E8",
},
"fonts": {
"title": {"family": "Arial", "size": 16, "weight": "bold"},
"subtitle": {"family": "Arial", "size": 11, "weight": "normal"},
"axis_label": {"family": "Arial", "size": 10},
"annotation": {"family": "Arial", "size": 9},
},
"grid": {"show": True, "axis": "both", "style": "-", "alpha": 0.2},
"annotations": {"style": "callout", "box_highlight": True},
"title": {"position": "center", "include_subtitle": True},
}
Applying a Theme (matplotlib)
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
def apply_theme(fig, ax, theme):
"""Apply a named theme to a matplotlib figure."""
fig.patch.set_facecolor(theme["colors"]["background"])
ax.set_facecolor(theme["colors"]["background"])
# Title styling
ax.set_title(
ax.get_title(),
fontfamily=theme["fonts"]["title"]["family"],
fontsize=theme["fonts"]["title"]["size"],
fontweight=theme["fonts"]["title"]["weight"],
loc="left" if theme["title"]["position"] == "left-aligned" else "center",
pad=15,
)
# Grid
if theme["grid"]["show"]:
ax.grid(
axis=theme["grid"]["axis"],
linestyle=theme["grid"]["style"],
alpha=theme["grid"]["alpha"],
color=theme["colors"].get("grid", "#E0E0E0"),
)
ax.set_axisbelow(True)
# Clean spines
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_alpha(0.3)
ax.spines["bottom"].set_alpha(0.3)
# Axis labels
ax.xaxis.label.set_fontfamily(theme["fonts"]["axis_label"]["family"])
ax.xaxis.label.set_fontsize(theme["fonts"]["axis_label"]["size"])
ax.yaxis.label.set_fontfamily(theme["fonts"]["axis_label"]["family"])
ax.yaxis.label.set_fontsize(theme["fonts"]["axis_label"]["size"])
plt.tight_layout()
Chart Type Selection
| Data Relationship | Chart Type | When to Use |
|---|---|---|
| Comparison (categories) | Bar chart (vertical) | Comparing ≤12 categories |
| Comparison (many categories) | Bar chart (horizontal) | Comparing >7 categories or long labels |
| Comparison (parts of whole) | Stacked bar | Showing composition across categories |
| Change over time | Line chart | Continuous time series, trends |
| Change over time (few periods) | Bar chart | Discrete periods (quarters, years) |
| Correlation | Scatter plot | Relationship between two continuous variables |
| Distribution | Histogram | Single variable distribution |
| Distribution (compare groups) | Box plot or violin | Distribution comparison across groups |
| Proportion / parts-to-whole | Single 100% stacked bar (share_bar()) | Replaces the pie/donut; direct-labeled shares |
| Flow/Process | Funnel chart | Conversion or drop-off rates |
| Intensity | Heatmap | Two categorical dimensions + one value |
| Cumulative | Area chart | Running totals over time |
| Ranking changes | Bump chart | Rank position changes over time |
| Waterfall | Waterfall chart | Additive/subtractive contributions |
Pick by intent, then encode by length or position, never area or angle. Cleveland and McGill's
graphical-perception ranking (position > length > angle > area) is why bars beat pies and why the
avoid-list below exists. One or two numbers are not a chart: use big_number(). Discouraged charts
(pie, donut, treemap, bubble, dual/secondary y-axis, 3D, radar, truncated bars, >5 series) are gated
behind an explicit user request, never a default.
Builder verdicts (helpers/viz/chart_helpers.py): horizontal bar is the default for long labels
(zero baseline enforced); multi-line-with-one-highlighted (highlight_line) is the default line
behavior (gray context + one accent, cap 4-5 lines); slope_chart is preferred for two-time-point
change; share_bar replaces the pie; retention_heatmap is a blue-sequential table (numbers kept in
cells); stacked bars are for when the TOTAL is the message, with the priority series on the baseline;
stacked/multi-series area is discouraged (redirect to a line or 100% stacked bar).
Annotation Standards
- Always label key data points directly — do not rely on legends for primary story elements
- Use direct labels on bars and line endpoints instead of requiring axis reading
- Annotate inflection points — mark where trends change with a brief note
- Titles are takeaways, not descriptions — "Revenue grew 23% after launch" not "Revenue by Month".
action_title()warns when a title reads as a topic ("... by X") or a question; rewrite it to state the so-what before shipping. - Subtitles provide context — "Monthly revenue, Jan–Dec 2025, in $M"
- Source line at bottom-left in small gray text
- Format numbers for readability — "$1.2M" not "$1,234,567"; "23%" not "0.2345"
- Gray plus at most 2 accents (blue focus, optional orange) — up to 5 Okabe-Ito categoricals only when categories are truly independent; never a rainbow
- Highlight the story — use accent color for the key data point, gray for context
Standard Chart Setup
def create_chart(data, chart_type, theme_name="minimal", title="", subtitle=""):
"""Standard chart creation pattern."""
theme = {"nyt": NYT_THEME, "economist": ECONOMIST_THEME,
"minimal": MINIMAL_THEME, "corporate": CORPORATE_THEME}[theme_name]
fig, ax = plt.subplots(figsize=(10, 6))
fig.patch.set_facecolor(theme["colors"]["background"])
ax.set_facecolor(theme["colors"]["background"])
# Plot data using theme colors
colors = theme["colors"]["palette"]
# Set title as takeaway
ax.set_title(title, fontfamily=theme["fonts"]["title"]["family"],
fontsize=theme["fonts"]["title"]["size"],
fontweight=theme["fonts"]["title"]["weight"],
loc="left", pad=20)
# Subtitle
if subtitle:
ax.text(0, 1.02, subtitle, transform=ax.transAxes,
fontfamily=theme["fonts"]["subtitle"]["family"],
fontsize=theme["fonts"]["subtitle"]["size"],
color=theme["fonts"]["subtitle"].get("color", "#666666"))
apply_theme(fig, ax, theme)
return fig, ax
Examples
Example 1: Bar chart with one highlighted category
fig, ax = plt.subplots(figsize=(10, 6))
colors = swd_style()
highlight_bar(ax, categories=["Mobile", "Desktop", "Tablet"], values=[45, 35, 20], highlight="Mobile")
action_title(ax, "Mobile drives nearly half of all sessions",
subtitle="Share of sessions, Jan–Dec 2025")
save_chart(fig, "outputs/charts/sessions_by_device_bar.png")
Example 2: Line chart with an annotated inflection point
fig, ax = plt.subplots(figsize=(10, 6))
colors = swd_style()
highlight_line(ax, x=months, y_dict={"Revenue": revenue}, highlight="Revenue")
annotate_point(ax, x=launch_month, y=launch_value, text="Feature launch\n+23% MoM")
action_title(ax, "Revenue grew 23% after feature launch",
subtitle="Monthly revenue, Jan–Dec 2025, in $M")
save_chart(fig, "outputs/charts/revenue_trend_line.png")
Example 3: Highlighting one segment
# Use accent for the key finding, gray for everything else
colors = ["#E0E0E0"] * len(categories)
colors[key_index] = theme["colors"]["accent"] # Highlight the story
Anti-Patterns (Banned)
| Anti-Pattern | Why It's Bad | Use Instead |
|---|---|---|
| Pie charts | Humans can't compare angles accurately | Horizontal bar chart |
| Rainbow palettes | No natural ordering, visual noise, not colorblind-safe | Gray + one highlight color (max 2 colors + gray) |
| Spaghetti lines | Too many colored lines, nothing stands out | highlight_line() — gray all, highlight one |
| Dual y-axes | Misleading — any two series can be made to "correlate" | Two separate charts, stacked vertically |
| 3D charts | Distorts proportions, adds no information | Flat 2D versions |
| Descriptive titles | Don't tell the reader what to think | Action titles via action_title() |
| Legend boxes | Force the reader to look away from the data | Direct labels on the data |
| Excessive gridlines | Create visual clutter | Light y-axis gridlines only, or none |
| Truncated y-axes | Exaggerate small differences (for bar charts) | Start at zero for bar charts |
| Cluttered annotations | Annotating every data point defeats the purpose | Annotate only the story |
| Default matplotlib styling | Looks generic, unprofessional | Always apply swd_style() first |
| Rainbow / red-green pairs | Visual noise; red-green is unreadable for ~8% of men | Gray + focus blue + optional orange (Okabe-Ito) |
Review Checklist
Before including any chart in an analysis:
- Title states the takeaway (not a description)
- Only 1-2 colors used (plus gray)
- No chart border, no top/right spines
- Direct labels instead of legend
- Gridlines removed or very light
- Axis labels are clean (no rotation, no trailing zeros)
- Annotations are minimal and support the story
- Chart type matches the data relationship
- A single number isn't charted — it's displayed as text
- The chart would be understood in 5 seconds
- YoY comparisons use lines (not two similar-colored bars)
- Labels don't collide with bars, axes, or other labels
- External context events have prominent bbox annotations
- Multi-panel charts with fig-level titles use direct
savefig()(notsave_chart())
Signals
- GitHub stars
- 297
- Forks
- 137
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
visualization-patterns- Source
- github.com/ai-analyst-lab/ai-analyst