.claude/skills/visualization-patterns/SKILL.md
# Skill: Visualization Patterns ## 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. Always apply the active theme unless the user specifies otherwise. Default theme: `minimal`. ## Instructions ### Pre-flight: Load Learnings Before executing, check `.knowledge/learnings/index.md` for relevant entrie
npx skillsauth add ai-analyst-lab/ai-analyst .claude/skills/visualization-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Ensure every chart Claude Code produces follows high-quality design standards with named themes, consistent styling, and clear data communication.
Apply this skill whenever generating a chart, graph, or data visualization. Always apply the active theme unless the user specifies otherwise. Default theme: minimal.
Before executing, check .knowledge/learnings/index.md for relevant entries:
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.
#D97706) for the primary focus, Accent Red (#DC2626) for a secondary callout. Everything else is gray.Implementation: Always apply the SWD style before generating any chart:
from helpers.chart_helpers import swd_style, highlight_bar, highlight_line, action_title, save_chart
colors = swd_style() # Loads .mplstyle + returns color palette
Use highlight_bar() for bar charts (highlights one bar, grays the rest), highlight_line() for line charts (highlights one series, grays the rest), and action_title() for all chart titles.
Before finalizing any chart, verify each item:
#E5E7EB), y-axis only$45 not $45.00; use 12% not 12.0%#F7F6F2)12% not 12.347%)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" |
All chart helpers live in helpers/chart_helpers.py. The style file is helpers/analytics_chart_style.mplstyle. The full style guide with before/after examples is in helpers/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 |
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},
}
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},
}
minimalMINIMAL_THEME = {
"colors": {
"primary": "#333333",
"secondary": "#999999",
"accent": "#2563EB",
"palette": ["#2563EB", "#DC2626", "#059669", "#D97706", "#7C3AED", "#DB2777"],
"background": "#FFFFFF",
"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},
}
corporateCORPORATE_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},
}
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()
| 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 | Donut chart | ≤5 segments, one variable | | 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 |
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
fig, ax = plt.subplots(figsize=(10, 6))
categories = ["Mobile", "Desktop", "Tablet"]
values = [45, 35, 20]
colors = ["#D03A2B", "#666666", "#666666"] # Accent on key finding
bars = ax.bar(categories, values, color=colors, width=0.6)
# Direct labels
for bar, val in zip(bars, values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
f"{val}%", ha="center", fontsize=12, fontweight="bold")
ax.set_title("Mobile drives nearly half of all sessions", loc="left",
fontfamily="Georgia", fontsize=18, fontweight="bold")
ax.set_ylabel("")
ax.set_ylim(0, 55)
apply_theme(fig, ax, NYT_THEME)
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(dates, revenue, color="#2563EB", linewidth=2)
# Annotate the inflection point
ax.annotate("Feature launch\n+23% MoM", xy=(launch_date, launch_value),
xytext=(launch_date - timedelta(days=30), launch_value + 50000),
fontsize=9, fontstyle="italic",
arrowprops=dict(arrowstyle="->", color="#666666"))
# Direct label on endpoint
ax.text(dates[-1], revenue[-1], f"${revenue[-1]/1e6:.1f}M",
fontsize=11, fontweight="bold", va="bottom")
ax.set_title("Revenue grew 23% after feature launch", loc="left")
apply_theme(fig, ax, MINIMAL_THEME)
# Use accent for the key finding, gray for everything else
colors = ["#E0E0E0"] * len(categories)
colors[key_index] = theme["colors"]["accent"] # Highlight the story
| 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 |
| More than 2 colors | Creates visual noise, dilutes focus | Gray + Action Amber + optional Accent Red |
Before including any chart in an analysis:
savefig() (not save_chart())testing
# Skill: {{BLANK_1_SKILL_NAME}} ## Purpose {{BLANK_2_WHEN_TO_FIRE}} ## When to Use Fires automatically when the user asks Claude to do something that matches the trigger condition above. ## Instructions 1. Detect the trigger condition 2. Execute your guardrail check 3. If the check matters, print a clear, visible warning with "{{BLANK_3_SIGNATURE_PHRASE}}" as the first line 4. Continue with the analysis, incorporating the warning into the output ## Anti-Patterns - Do not fire when the condit
development
# Skill: Triangulation / Sanity Check ## Purpose Cross-reference analytical findings against multiple data sources, external benchmarks, and common sense to catch errors before they become bad decisions. ## When to Use Apply this skill after every analysis, before presenting findings to stakeholders, and whenever a result seems surprising. If a finding would change a decision, it MUST be triangulated first. ## Instructions ### Triangulation Framework Every finding gets checked through four
data-ai
# Skill: Tracking Gap Identification ## Purpose Assess whether the data needed for an analysis actually exists, identify what's missing, and produce prioritized instrumentation requests for engineering when gaps are found. ## When to Use Apply this skill after the Data Explorer agent inventories available data, when an analysis requires data that might not exist, or when initial query results suggest incomplete tracking. Run before committing to an analysis approach. ## Instructions ### Gap
testing
# Skill: Switch Dataset ## Purpose Change the active dataset. Updates the active pointer, validates the target dataset exists, and confirms with a summary of what's now active. ## When to Use Invoke as `/switch-dataset {name}` when the user wants to analyze a different dataset than the currently active one. ## Instructions ### Step 1: Validate the target dataset 1. Read `data_sources.yaml` to check if `{name}` exists as a registered source 2. If not found, try fuzzy matching (case-insensiti