| name | data-visualization |
|---|---|
| description | Create effective data visualizations with Python (matplotlib, seaborn, plotly). Use this when building charts, choosing the right chart type for a dataset, creating publication-quality figures, or applying design principles like accessibility and color theory. |
Chart selection guidance, Python visualization code patterns, design principles, and accessibility considerations for creating effective data visualizations.
| What You're Showing | Best Chart | Alternatives |
|---|---|---|
| Trend over time | Line chart | Area chart (if showing cumulative or composition) |
| Comparison across categories | Vertical bar chart | Horizontal bar (many categories), lollipop chart |
| Ranking | Horizontal bar chart | Dot plot, slope chart (comparing two periods) |
| Part-to-whole composition | Stacked bar chart | Treemap (hierarchical), waffle chart |
| Composition over time | Stacked area chart | 100% stacked bar (for proportion focus) |
| Distribution | Histogram | Box plot (comparing groups), violin plot, strip plot |
| Correlation (2 variables) | Scatter plot | Bubble chart (add 3rd variable as size) |
| Correlation (many variables) | Heatmap (correlation matrix) | Pair plot |
| Geographic patterns | Choropleth map | Bubble map, hex map |
| Flow / process | Sankey diagram | Funnel chart (sequential stages) |
| Relationship network | Network graph | Chord diagram |
| Performance vs. target | Bullet chart | Gauge (single KPI only) |
| Multiple KPIs at once | Small multiples | Dashboard with separate charts |
- Pie charts: Avoid unless <6 categories and exact proportions matter less than rough comparison. Humans are bad at comparing angles. Use bar charts instead.
- 3D charts: Never. They distort perception and add no information.
- Dual-axis charts: Use cautiously. They can mislead by implying correlation. Clearly label both axes if used.
- Stacked bar (many categories): Hard to compare middle segments. Use small multiples or grouped bars instead.
- Donut charts: Slightly better than pie charts but same fundamental issues. Use for single KPI display at most.
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import seaborn as sns
import pandas as pd
import numpy as np
# Professional style setup
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams.update({
'figure.figsize': (10, 6),
'figure.dpi': 150,
'font.size': 11,
'axes.titlesize': 14,
'axes.titleweight': 'bold',
'axes.labelsize': 11,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'legend.fontsize': 10,
'figure.titlesize': 16,
})
# Colorblind-friendly palettes
PALETTE_CATEGORICAL = ['#4C72B0', '#DD8452', '#55A868', '#C44E52', '#8172B3', '#937860']
PALETTE_SEQUENTIAL = 'YlOrRd'
PALETTE_DIVERGING = 'RdBu_r'fig, ax = plt.subplots(figsize=(10, 6))
for label, group in df.groupby('category'):
ax.plot(group['date'], group['value'], label=label, linewidth=2)
ax.set_title('Metric Trend by Category', fontweight='bold')
ax.set_xlabel('Date')
ax.set_ylabel('Value')
ax.legend(loc='upper left', frameon=True)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Format dates on x-axis
fig.autofmt_xdate()
plt.tight_layout()
plt.savefig('trend_chart.png', dpi=150, bbox_inches='tight')fig, ax = plt.subplots(figsize=(10, 6))
# Sort by value for easy reading
df_sorted = df.sort_values('metric', ascending=True)
bars = ax.barh(df_sorted['category'], df_sorted['metric'], color=PALETTE_CATEGORICAL[0])
# Add value labels
for bar in bars:
width = bar.get_width()
ax.text(width + 0.5, bar.get_y() + bar.get_height()/2,
f'{width:,.0f}', ha='left', va='center', fontsize=10)
ax.set_title('Metric by Category (Ranked)', fontweight='bold')
ax.set_xlabel('Metric Value')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.savefig('bar_chart.png', dpi=150, bbox_inches='tight')fig, ax = plt.subplots(figsize=(10, 6))
ax.hist(df['value'], bins=30, color=PALETTE_CATEGORICAL[0], edgecolor='white', alpha=0.8)
# Add mean and median lines
mean_val = df['value'].mean()
median_val = df['value'].median()
ax.axvline(mean_val, color='red', linestyle='--', linewidth=1.5, label=f'Mean: {mean_val:,.1f}')
ax.axvline(median_val, color='green', linestyle='--', linewidth=1.5, label=f'Median: {median_val:,.1f}')
ax.set_title('Distribution of Values', fontweight='bold')
ax.set_xlabel('Value')
ax.set_ylabel('Frequency')
ax.legend()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.savefig('histogram.png', dpi=150, bbox_inches='tight')fig, ax = plt.subplots(figsize=(10, 8))
# Pivot data for heatmap format
pivot = df.pivot_table(index='row_dim', columns='col_dim', values='metric', aggfunc='sum')
sns.heatmap(pivot, annot=True, fmt=',.0f', cmap='YlOrRd',
linewidths=0.5, ax=ax, cbar_kws={'label': 'Metric Value'})
ax.set_title('Metric by Row Dimension and Column Dimension', fontweight='bold')
ax.set_xlabel('Column Dimension')
ax.set_ylabel('Row Dimension')
plt.tight_layout()
plt.savefig('heatmap.png', dpi=150, bbox_inches='tight')categories = df['category'].unique()
n_cats = len(categories)
n_cols = min(3, n_cats)
n_rows = (n_cats + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 4*n_rows), sharex=True, sharey=True)
axes = axes.flatten() if n_cats > 1 else [axes]
for i, cat in enumerate(categories):
ax = axes[i]
subset = df[df['category'] == cat]
ax.plot(subset['date'], subset['value'], color=PALETTE_CATEGORICAL[i % len(PALETTE_CATEGORICAL)])
ax.set_title(cat, fontsize=12)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Hide empty subplots
for j in range(i+1, len(axes)):
axes[j].set_visible(False)
fig.suptitle('Trends by Category', fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.savefig('small_multiples.png', dpi=150, bbox_inches='tight')def format_number(val, format_type='number'):
"""Format numbers for chart labels."""
if format_type == 'currency':
if abs(val) >= 1e9:
return f'${val/1e9:.1f}B'
elif abs(val) >= 1e6:
return f'${val/1e6:.1f}M'
elif abs(val) >= 1e3:
return f'${val/1e3:.1f}K'
else:
return f'${val:,.0f}'
elif format_type == 'percent':
return f'{val:.1f}%'
elif format_type == 'number':
if abs(val) >= 1e9:
return f'{val/1e9:.1f}B'
elif abs(val) >= 1e6:
return f'{val/1e6:.1f}M'
elif abs(val) >= 1e3:
return f'{val/1e3:.1f}K'
else:
return f'{val:,.0f}'
return str(val)
# Usage with axis formatter
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x, p: format_number(x, 'currency')))import plotly.express as px
import plotly.graph_objects as go
# Simple interactive line chart
fig = px.line(df, x='date', y='value', color='category',
title='Interactive Metric Trend',
labels={'value': 'Metric Value', 'date': 'Date'})
fig.update_layout(hovermode='x unified')
fig.write_html('interactive_chart.html')
fig.show()
# Interactive scatter with hover data
fig = px.scatter(df, x='metric_a', y='metric_b', color='category',
size='size_metric', hover_data=['name', 'detail_field'],
title='Correlation Analysis')
fig.show()- Use color purposefully: Color should encode data, not decorate
- Highlight the story: Use a bright accent color for the key insight; grey everything else
- Sequential data: Use a single-hue gradient (light to dark) for ordered values
- Diverging data: Use a two-hue gradient with neutral midpoint for data with a meaningful center
- Categorical data: Use distinct hues, maximum 6-8 before it gets confusing
- Avoid red/green only: 8% of men are red-green colorblind. Use blue/orange as primary pair
- Title states the insight: "Revenue grew 23% YoY" beats "Revenue by Month"
- Subtitle adds context: Date range, filters applied, data source
- Axis labels are readable: Never rotated 90 degrees if avoidable. Shorten or wrap instead
- Data labels add precision: Use on key points, not every single bar
- Annotation highlights: Call out specific points with text annotations
- Reduce chart junk: Remove gridlines, borders, backgrounds that don't carry information
- Sort meaningfully: Categories sorted by value (not alphabetically) unless there's a natural order (months, stages)
- Appropriate aspect ratio: Time series wider than tall (3:1 to 2:1); comparisons can be squarer
- White space is good: Don't cram charts together. Give each visualization room to breathe
- Bar charts start at zero: Always. A bar from 95 to 100 exaggerates a 5% difference
- Line charts can have non-zero baselines: When the range of variation is meaningful
- Consistent scales across panels: When comparing multiple charts, use the same axis range
- Show uncertainty: Error bars, confidence intervals, or ranges when data is uncertain
- Label your axes: Never make the reader guess what the numbers mean
- Never rely on color alone to distinguish data series
- Add pattern fills, different line styles (solid, dashed, dotted), or direct labels
- Test with a colorblind simulator (e.g., Coblis, Sim Daltonism)
- Use the colorblind-friendly palette:
sns.color_palette("colorblind")
- Include alt text describing the chart's key finding
- Provide a data table alternative alongside the visualization
- Use semantic titles and labels
- Sufficient contrast between data elements and background
- Text size minimum 10pt for labels, 12pt for titles
- Avoid conveying information only through spatial position (add labels)
- Consider printing: does the chart work in black and white?
Before sharing a visualization:
- Chart works without color (patterns, labels, or line styles differentiate series)
- Text is readable at standard zoom level
- Title describes the insight, not just the data
- Axes are labeled with units
- Legend is clear and positioned without obscuring data
- Data source and date range are noted
- Name the message of the chart before choosing its form.
- Choose chart type from the data relationship (comparison, trend, distribution, part-to-whole).
- Encode honestly: zero-based bars, unbroken axes, colorblind-safe palettes, no color-only meaning.
- Label directly where possible; title with the takeaway, not the dataset name.
- Review against accessibility and the audience's decision context.
- Pie charts for more than a handful of categories, or any precise comparison.
- Truncated bar axes that inflate small differences into visual drama.
- Dual y-axes implying correlation the data does not establish.
- Meaning carried by color alone, invisible to colorblind readers and grayscale prints.
Use this extension for information-bearing color and visual signaling in charts, explanatory diagrams, infographics, study materials, dashboards, knowledge maps, and related visual surfaces. The evidence summary is in references/information-design-evidence.md; machine-readable defaults are in visual-grammar.json.
The strongest practical evidence supports signaling: selective, meaningful visual cues can improve attention, organization, retention, and transfer when they clarify what belongs together or where the reader should look. This does not establish universal hue psychology. Do not claim that red inherently impairs reasoning, blue creates creativity, green improves learning, or similar fixed effects.
- Color encodes structure; it does not substitute for structure.
- Salience is relational. A hue attracts attention because of contrast with its surround and competing signals, not because it is intrinsically dominant.
- Hue gets one independent semantic dimension per local visual field. If hue means component family, evidence state must use another channel such as shape + label.
- Critical meaning is redundant. Repeat information-bearing color with a label, shape, line style, pattern, icon, enclosure, or position.
- Luminance and contrast are load-bearing. Critical boundaries need adequate lightness contrast and line weight, not hue alone.
- Stable mappings support learning. Reuse the same mapping for the same meaning across an artifact family.
- Exact claims remain explicit. Labels, numbers, provenance, uncertainty, and status carry exact meaning.
Recommended visual-channel allocation:
| Channel | Preferred meaning |
|---|---|
| Hue | one categorical family or one semantic dimension |
| Lightness | emphasis or ordered magnitude |
| Shape | state class or category redundancy |
| Position | structural layer or reading order |
| Line direction | processing, dependency, causal, or temporal flow |
| Line style | current, provisional, historical, or unavailable |
| Border | authority, selection, or scope boundary |
| Text | exact semantic meaning |
| Pattern | color-independent redundancy |
These are communication defaults, not transfers of epistemic authority or project canon:
| State | Color | Required non-color redundancy |
|---|---|---|
| supported | #009E73 |
solid circle/line + SUPPORTED |
| falsified | #D55E00 |
octagon/cross + FALSIFIED |
| errored | #CC79A7 |
diamond/zigzag + ERROR |
| unavailable / NA | #F0E442 |
hollow square/dotted line + NA |
| historical provenance | #6B7280 |
dashed enclosure + HISTORICAL |
| current maintained authority | #0072B2 |
double border + CURRENT |
Project branding may override hues, but not contrast, redundancy, or semantic-audit requirements.
- Declare the message. Write one sentence describing what the reader should understand or decide.
- Declare semantic dimensions. List component family, evidence state, modality, recursion depth, authority/provenance, uncertainty, temporal status, magnitude, or other variables and assign each to a visual channel.
- Establish the neutral substrate. Most content should be neutral or low-chroma so accents retain signaling power.
- Allocate a salience budget. Reserve the strongest saturation/contrast for the information deserving first attention; decoration must never outrank evidence/status distinctions.
- Add redundant state markers. No critical state is color-only.
- Check contrast. Minimum targets: normal text
4.5:1; large text3:1; essential graphical objects and state boundaries3:1. - Run four publication gates: grayscale, color-vision-deficiency review, contrast, and semantic audit.
- Preserve a nonvisual representation. Alt text or structured metadata states the message, entities, relations, flow, statuses, uncertainty, and necessary quantitative values.
A deterministic manifest can be checked with:
python data-visualization/information_design_audit.py \
data-visualization/examples/information-design-manifest.jsonThe checker verifies declared WCAG contrast and color-independent state redundancy. It does not claim to simulate human perception or prove comprehension.
- Rainbow decoration where every node competes for attention.
- Reusing one hue for both component identity and evidence status.
- White or light text on yellow/orange fills without a contrast check.
- Thin isoluminant colored lines as the only critical boundary.
- Treating
NAas zero or implying neutrality when data is unavailable. - Coloring recursion depth progressively redder unless severity is actually being encoded.
- Letting risk-matrix color bands replace the underlying number or threshold definition.
- Calling a palette "colorblind-safe" and assuming its text contrast therefore passes WCAG.
- Claiming an automated contrast check proves accessibility, memory, comprehension, or emotional effect.
Imported from anthropics/knowledge-work-plugins @ 94e1a08 (data/skills/data-visualization/), Apache-2.0.
Local modifications: trigger phrasing normalized to skill-lib convention; the Workflow/Anti-patterns/Provenance bookend and the clearly marked The Interdependency information-design extension were appended. The imported upstream body remains otherwise unmodified. The extension is evidence-grounded in references/information-design-evidence.md. See ATTRIBUTION.md at repo root.
hmmm
- Image-level protan/deutan/tritan simulation is intentionally not claimed by the stdlib audit; rendered-artifact perceptual review remains a separate tool/human gate.
- Exact project brand palettes may override default hues while preserving the shared grammar and publication gates.
- Whether image-generation workflows should emit a sidecar design manifest automatically is unresolved.
- Upstream re-sync cadence with
anthropics/knowledge-work-pluginsis undecided; drift against upstream is currently invisible.