Skill: Visualization Patterns

SkillDatabases & data

Apply 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.

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 TypeHelper FunctionExample Usage
Bar charthighlight_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 charthighlight_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 onlyaction_title()action_title(ax, title='Finding here', subtitle='Context: time range, data source, sample size')

Key points:

  • Helpers take a matplotlib ax object + 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 highlight parameter 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 #D97706 is 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 $45 not $45.00; use 12% not 12.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% not 12.347%)

Chart Sequencing (Multi-Chart Analyses)

When producing multiple charts for a deep dive or root cause investigation, follow Context → Tension → Resolution:

PhaseChartsPurposeExample
Context1-2Set the baseline. What does normal look like?"[Dataset] processes ~4,000 support tickets per month"
Tension2-3Reveal the problem. Progressively zoom in."June spiked to 6,200" → "The spike was iOS payment issues"
Resolution1-2Explain 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.

FunctionPurposeKey Args
swd_style()Apply SWD matplotlib style, return color palette
highlight_bar()Bar chart with one bar highlighted, rest grayhighlight=, horizontal=True, sort=True
highlight_line()Line chart with one line colored, rest grayhighlight=, y_dict={}
action_title()Bold takeaway title + optional subtitletitle, subtitle=
annotate_point()Clean annotation with arrowx, y, text, offset=
save_chart()Tight layout + correct DPIfig, path, dpi=150
stacked_bar()Stacked / 100% stacked (normalize=True) barhighlight_layer=, normalize=
share_bar()Single horizontal 100% stacked bar (pie replacement)parts={}, highlight=
slope_chart()Two-time-point change across itemsstart_col, end_col, highlight_label=
funnel_waterfall()Funnel drop-off; highlights the biggest drophighlight_step=
retention_heatmap()Cohort retention as a blue-sequential tablekeeps 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 labelvalue, 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 RelationshipChart TypeWhen 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 barShowing composition across categories
Change over timeLine chartContinuous time series, trends
Change over time (few periods)Bar chartDiscrete periods (quarters, years)
CorrelationScatter plotRelationship between two continuous variables
DistributionHistogramSingle variable distribution
Distribution (compare groups)Box plot or violinDistribution comparison across groups
Proportion / parts-to-wholeSingle 100% stacked bar (share_bar())Replaces the pie/donut; direct-labeled shares
Flow/ProcessFunnel chartConversion or drop-off rates
IntensityHeatmapTwo categorical dimensions + one value
CumulativeArea chartRunning totals over time
Ranking changesBump chartRank position changes over time
WaterfallWaterfall chartAdditive/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

  1. Always label key data points directly — do not rely on legends for primary story elements
  2. Use direct labels on bars and line endpoints instead of requiring axis reading
  3. Annotate inflection points — mark where trends change with a brief note
  4. 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.
  5. Subtitles provide context — "Monthly revenue, Jan–Dec 2025, in $M"
  6. Source line at bottom-left in small gray text
  7. Format numbers for readability — "$1.2M" not "$1,234,567"; "23%" not "0.2345"
  8. 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
  9. 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-PatternWhy It's BadUse Instead
Pie chartsHumans can't compare angles accuratelyHorizontal bar chart
Rainbow palettesNo natural ordering, visual noise, not colorblind-safeGray + one highlight color (max 2 colors + gray)
Spaghetti linesToo many colored lines, nothing stands outhighlight_line() — gray all, highlight one
Dual y-axesMisleading — any two series can be made to "correlate"Two separate charts, stacked vertically
3D chartsDistorts proportions, adds no informationFlat 2D versions
Descriptive titlesDon't tell the reader what to thinkAction titles via action_title()
Legend boxesForce the reader to look away from the dataDirect labels on the data
Excessive gridlinesCreate visual clutterLight y-axis gridlines only, or none
Truncated y-axesExaggerate small differences (for bar charts)Start at zero for bar charts
Cluttered annotationsAnnotating every data point defeats the purposeAnnotate only the story
Default matplotlib stylingLooks generic, unprofessionalAlways apply swd_style() first
Rainbow / red-green pairsVisual noise; red-green is unreadable for ~8% of menGray + 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() (not save_chart())

Signals

GitHub stars
298
Forks
137
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
visualization-patterns
Source
github.com/ai-analyst-lab/ai-analyst