Altair is a declarative visualization library based on Vega-Lite. pip install altair. import altair as alt. Basic chart: alt.Chart(df).mark_point().encode(x="col_a:Q", y="col_b:Q", color="category:N"). Shorthand data types: :Q quantitative, :O ordinal, :N nominal, :T temporal. Mark types: mark_point, mark_line, mark_bar, mark_area, mark_circle, mark_rect, mark_boxplot, mark_rule. Encodings: alt.X("col:Q", bin=True), alt.Y("count():Q"), alt.Color("c:N", scale=alt.Scale(scheme="category10")), alt.Size("val:Q"), alt.Tooltip(["a", "b", "c"]). Interactive: selection = alt.selection_interval(), chart.add_params(selection).encode(color=alt.condition(selection, "cat:N", alt.value("lightgray"))). Point selection: sel = alt.selection_point(fields=["category"]). Transforms: .transform_filter(alt.datum.value > 0), .transform_aggregate(mean_val="mean(value)", groupby=["group"]), .transform_bin("x_bin", "x"), .transform_calculate("log_x", "log(datum.x)"), .transform_fold(["a","b","c"], as_=["key","value"]). Facet: chart.facet(column="col:N", row="row:N") or shorthand facet(facet=alt.Facet("col:N", columns=3)). Composition: chart1 | chart2 (hconcat), chart1 & chart2 (vconcat), alt.layer(base, points). Resolve: .resolve_scale(y="independent"). Properties: .properties(width=400, height=300, title="My Chart"). Save: chart.save("chart.html") or chart.save("chart.json"). Show: chart in Jupyter renders automatically. Claude Code generates Altair interactive dashboards, linked selection charts, small-multiples facets, and Vega-Lite JSON specs.
CLAUDE.md for Altair
## Altair Stack
- Version: altair >= 5.0 (uses Vega-Lite v5)
- Chart: alt.Chart(df).mark_TYPE().encode(x=, y=, color=, tooltip=)
- Types: :Q quantitative | :O ordinal | :N nominal | :T temporal
- Marks: mark_point/line/bar/area/circle/rect/boxplot/rule/text
- Interactive: selection_interval/selection_point + add_params() + condition()
- Transforms: transform_filter/aggregate/bin/calculate/fold/window/joinaggregate
- Compose: chart1 | chart2 (H) | chart1 & chart2 (V) | alt.layer(c1, c2)
- Export: chart.save("path.html") | chart.to_dict() for JSON spec
Altair Declarative Visualization Pipeline
# viz/altair_pipeline.py — declarative statistical visualization with Altair
from __future__ import annotations
import numpy as np
import pandas as pd
import altair as alt
from pathlib import Path
# Enable max rows for large datasets (default is 5000)
alt.data_transformers.enable("vegafusion") # or "default" for small data
# ── 1. Basic chart builders ───────────────────────────────────────────────────
def scatter_chart(
df: pd.DataFrame,
x: str,
y: str,
color: str = None,
size: str = None,
tooltip: list[str] = None,
title: str = "",
width: int = 400,
height: int = 300,
) -> alt.Chart:
"""
Interactive scatter plot.
Supports hover tooltip, zoom, and pan by default.
"""
encoding = {
"x": alt.X(f"{x}:Q", title=x),
"y": alt.Y(f"{y}:Q", title=y),
}
if color:
encoding["color"] = alt.Color(f"{color}:N")
if size:
encoding["size"] = alt.Size(f"{size}:Q")
if tooltip:
encoding["tooltip"] = tooltip
return (
alt.Chart(df)
.mark_circle(opacity=0.7)
.encode(**encoding)
.properties(width=width, height=height, title=title)
.interactive()
)
def bar_chart(
df: pd.DataFrame,
x: str,
y: str,
color: str = None,
horizontal: bool = False,
sort: str = "-y", # "-y" descending, "x" ascending
title: str = "",
width: int = 400,
height: int = 300,
) -> alt.Chart:
"""Bar chart with optional color grouping."""
if horizontal:
x_enc = alt.X(f"{y}:Q", title=y)
y_enc = alt.Y(f"{x}:N", sort=sort, title=x)
else:
x_enc = alt.X(f"{x}:N", sort=sort, title=x)
y_enc = alt.Y(f"{y}:Q", title=y)
encoding = {"x": x_enc, "y": y_enc}
if color:
encoding["color"] = alt.Color(f"{color}:N")
return (
alt.Chart(df)
.mark_bar()
.encode(**encoding)
.properties(width=width, height=height, title=title)
)
def line_chart(
df: pd.DataFrame,
x: str,
y: str,
color: str = None,
x_type: str = "T", # "T" temporal, "Q" quantitative, "O" ordinal
point: bool = True,
title: str = "",
width: int = 500,
height: int = 280,
) -> alt.Chart:
"""Line chart with optional point markers."""
encoding = {
"x": alt.X(f"{x}:{x_type}", title=x),
"y": alt.Y(f"{y}:Q", title=y),
}
if color:
encoding["color"] = alt.Color(f"{color}:N")
mark_kwargs = {"point": {"filled": True, "size": 40}} if point else {}
return (
alt.Chart(df)
.mark_line(**mark_kwargs)
.encode(**encoding)
.properties(width=width, height=height, title=title)
.interactive()
)
def histogram(
df: pd.DataFrame,
col: str,
n_bins: int = 30,
color: str = None,
title: str = "",
width: int = 400,
height: int = 260,
) -> alt.Chart:
"""Histogram with Altair binning."""
encoding = {
"x": alt.X(f"{col}:Q", bin=alt.Bin(maxbins=n_bins), title=col),
"y": alt.Y("count():Q", title="Count"),
}
if color:
encoding["color"] = alt.Color(f"{color}:N")
return (
alt.Chart(df)
.mark_bar(opacity=0.7)
.encode(**encoding)
.properties(width=width, height=height, title=title or f"Distribution of {col}")
)
# ── 2. Interactive selections ─────────────────────────────────────────────────
def linked_scatter_with_hist(
df: pd.DataFrame,
x: str,
y: str,
color: str = None,
) -> alt.VConcatChart:
"""
Linked scatter + histogram.
Brush in the scatter highlights bars in the histogram.
"""
brush = alt.selection_interval()
highlight = alt.condition(brush, alt.value(0.8), alt.value(0.1))
scatter = (
alt.Chart(df)
.mark_circle()
.encode(
x=f"{x}:Q", y=f"{y}:Q",
color=alt.Color(f"{color}:N") if color else alt.value("steelblue"),
opacity=highlight,
tooltip=[x, y] + ([color] if color else []),
)
.add_params(brush)
.properties(width=400, height=280, title="Select a region to highlight")
)
hist = (
alt.Chart(df)
.mark_bar()
.encode(
x=alt.X(f"{x}:Q", bin=alt.Bin(maxbins=30)),
y="count():Q",
color=alt.condition(brush, alt.value("steelblue"), alt.value("lightgray")),
)
.transform_filter(brush)
.properties(width=400, height=120, title=f"Filtered distribution of {x}")
)
return scatter & hist
def multi_series_selector(
df: pd.DataFrame,
x: str,
y: str,
series: str,
) -> alt.LayerChart:
"""
Multi-series line chart with click-to-highlight a series.
"""
sel = alt.selection_point(fields=[series], bind="legend")
base = (
alt.Chart(df).encode(
x=alt.X(f"{x}:T", title=x),
y=alt.Y(f"{y}:Q", title=y),
color=alt.Color(f"{series}:N"),
opacity=alt.condition(sel, alt.value(1.0), alt.value(0.1)),
tooltip=[x, y, series],
)
.add_params(sel)
.properties(width=540, height=320)
)
return base.mark_line() + base.mark_point()
# ── 3. Data transforms ────────────────────────────────────────────────────────
def grouped_bar_from_long(
df: pd.DataFrame,
x: str,
y: str,
color: str,
title: str = "",
) -> alt.Chart:
"""Grouped bar chart from long-format DataFrame."""
return (
alt.Chart(df)
.mark_bar()
.encode(
x=alt.X(f"{x}:N", title=x),
y=alt.Y(f"{y}:Q", title=y),
color=alt.Color(f"{color}:N"),
xOffset=alt.XOffset(f"{color}:N"), # Side-by-side grouping
tooltip=[x, y, color],
)
.properties(title=title)
)
def aggregate_bar(
df: pd.DataFrame,
group: str,
value: str,
agg_fn: str = "mean", # "mean" | "sum" | "count" | "median"
sort: str = "-y",
title: str = "",
) -> alt.Chart:
"""Bar chart with in-Vega aggregation (no pre-aggregation needed)."""
return (
alt.Chart(df)
.mark_bar()
.encode(
x=alt.X(f"{group}:N", sort=sort),
y=alt.Y(f"{agg_fn}({value}):Q", title=f"{agg_fn}({value})"),
tooltip=[group, alt.Tooltip(f"{agg_fn}({value}):Q", format=".2f")],
)
.properties(title=title or f"{agg_fn}({value}) by {group}")
)
def binned_heatmap(
df: pd.DataFrame,
x: str,
y: str,
n_bins: int = 20,
title: str = "",
) -> alt.Chart:
"""2D binned heatmap (density) — good for large scatter datasets."""
return (
alt.Chart(df)
.mark_rect()
.encode(
x=alt.X(f"{x}:Q", bin=alt.Bin(maxbins=n_bins)),
y=alt.Y(f"{y}:Q", bin=alt.Bin(maxbins=n_bins)),
color=alt.Color("count():Q", scale=alt.Scale(scheme="blues")),
tooltip=[
alt.Tooltip(f"{x}:Q", bin=True),
alt.Tooltip(f"{y}:Q", bin=True),
"count():Q",
],
)
.properties(title=title or f"2D Density: {x} vs {y}")
)
# ── 4. Small multiples (facets) ───────────────────────────────────────────────
def faceted_bars(
df: pd.DataFrame,
x: str,
y: str,
facet_col: str,
columns: int = 3,
title: str = "",
) -> alt.FacetChart:
"""Small multiples bar chart — one panel per facet value."""
base = (
alt.Chart(df)
.mark_bar()
.encode(
x=alt.X(f"{x}:N"),
y=alt.Y(f"{y}:Q"),
color=alt.Color(f"{x}:N"),
tooltip=[x, y],
)
.properties(width=180, height=140)
)
return base.facet(
facet=alt.Facet(f"{facet_col}:N", columns=columns),
title=title,
)
def faceted_lines(
df: pd.DataFrame,
x: str,
y: str,
facet_col: str,
color: str = None,
columns: int = 3,
independent_y: bool = True,
) -> alt.FacetChart:
"""Faceted line charts, optionally with independent y-axes."""
base = (
alt.Chart(df)
.mark_line()
.encode(
x=alt.X(f"{x}:Q"),
y=alt.Y(f"{y}:Q"),
color=alt.Color(f"{color}:N") if color else alt.value("steelblue"),
)
.properties(width=180, height=140)
)
fc = base.facet(
facet=alt.Facet(f"{facet_col}:N", columns=columns),
)
if independent_y:
fc = fc.resolve_scale(y="independent")
return fc
# ── 5. Composition ────────────────────────────────────────────────────────────
def scatter_with_marginals(
df: pd.DataFrame,
x: str,
y: str,
color: str = None,
) -> alt.VConcatChart:
"""
Scatter plot with marginal histogram strips (top + right).
Classic bivariate distribution view.
"""
points = (
alt.Chart(df)
.mark_circle(opacity=0.6)
.encode(
x=alt.X(f"{x}:Q"),
y=alt.Y(f"{y}:Q"),
color=alt.Color(f"{color}:N") if color else alt.value("steelblue"),
tooltip=[x, y] + ([color] if color else []),
)
.properties(width=360, height=300)
)
top_hist = (
alt.Chart(df)
.mark_bar(opacity=0.6)
.encode(
x=alt.X(f"{x}:Q", bin=alt.Bin(maxbins=25), axis=None),
y=alt.Y("count():Q", title=""),
)
.properties(width=360, height=60)
)
return top_hist & points
# ── 6. Export ─────────────────────────────────────────────────────────────────
def save_chart(chart, path: str) -> str:
"""Save chart as HTML (interactive) or JSON (Vega-Lite spec)."""
Path(path).parent.mkdir(parents=True, exist_ok=True)
chart.save(path)
print(f"Chart saved: {path}")
return path
def chart_to_spec(chart) -> dict:
"""Return the Vega-Lite JSON specification as a Python dict."""
return chart.to_dict()
# ── Demo ──────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile, os
print("Altair Declarative Visualization Demo")
print("=" * 50)
np.random.seed(42)
n = 300
df = pd.DataFrame({
"x": np.random.randn(n),
"y": np.random.randn(n) * 1.5 + 1,
"size": np.random.exponential(20, n),
"group": np.random.choice(["A", "B", "C"], n),
"month": pd.date_range("2024-01-01", periods=n, freq="D")[:n],
"value": np.cumsum(np.random.randn(n)) + 50,
})
with tempfile.TemporaryDirectory() as tmpdir:
# Scatter
sc = scatter_chart(df, "x", "y", color="group", tooltip=["x","y","group"])
save_chart(sc, f"{tmpdir}/scatter.html")
# Bar
bc = aggregate_bar(df, "group", "value", agg_fn="mean")
save_chart(bc, f"{tmpdir}/bar.html")
# Linked
linked = linked_scatter_with_hist(df, "x", "y", color="group")
save_chart(linked, f"{tmpdir}/linked.html")
# Binned heatmap
hm = binned_heatmap(df, "x", "y")
save_chart(hm, f"{tmpdir}/heatmap.html")
# Faceted
facet_df = pd.DataFrame({
"product": np.repeat(["A","B","C","D"], 50),
"region": np.tile(np.random.choice(["N","S","E","W"], 50), 4),
"sales": np.random.exponential(100, 200),
})
fc = faceted_bars(facet_df, "region", "sales", "product", columns=2)
save_chart(fc, f"{tmpdir}/facet.html")
print(f"\nAll Altair charts saved to {tmpdir}")
print("Open .html files in a browser to see interactive charts")
# Print spec excerpt
small_chart = bar_chart(
pd.DataFrame({"cat": list("ABCDE"), "val": [10, 25, 15, 30, 20]}),
"cat", "val"
)
spec = chart_to_spec(small_chart)
print(f"\nVega-Lite spec keys: {list(spec.keys())}")
For the Matplotlib/Seaborn alternative for static publication plots — Matplotlib/Seaborn produce print-ready PDFs while Altair’s Vega-Lite declarative spec generates interactive SVG-based HTML charts with automatic tooltips, zoom, and selection linking across panels, and the selection_interval brush that filters linked charts requires zero JavaScript — it’s expressed as a Python data binding, making Altair the fastest path from a pandas DataFrame to a cross-filtered interactive dashboard. For the Plotly Express alternative when drop-in interactive charts are needed — Plotly Express is imperative (one call per chart type) while Altair’s grammar composes arbitrary mark types and encodings, transform_fold unpivots wide DataFrames inside the spec, and facet(columns=3) auto-wraps small multiples without computing subplot geometry, making Altair more powerful for statistical reporting where the chart structure should mirror the data model. The Claude Skills 360 bundle includes Altair skill sets covering scatter, bar, line, histogram, and binned heatmap, interactive brushing with selection_interval and selection_point, linked charts, multi-panel facets, grouped bars with xOffset, data transforms, and chart composition with hconcat, vconcat, and layer. Start with the free tier to try declarative visualization code generation.