Altair Essentials¶
A declarative Python API for Vega-Lite: you describe what to show (marks + encodings) and Altair renders the chart. Clean defaults, automatic scales and legends, and rich interaction via selections.
Table of Contents¶
- Installation / Setup
- Core Syntax
- Common Operations
- Practical Examples
- Common Patterns
- Best Practices
- Common Mistakes
- Troubleshooting
- Related Cheat Sheets
- Official Documentation
Installation / Setup¶
Charts render natively in Jupyter, VSCode, Colab, and the Altair viewer. For notebooks also consider pip install vl-convert-python (needed for some static PNG export paths).
Core Syntax¶
Marks & encodings¶
chart = alt.Chart(df).mark_point().encode(
x="total_bill:Q",
y="tip:Q",
color="day:N",
tooltip=["total_bill", "tip"],
).properties(title="Tips", width=500, height=350)
chart.show() # or render in Jupyter by evaluating the last expression
| Mark | Method | Use |
|---|---|---|
| Point/circle | mark_point() / mark_circle() |
scatter |
| Line | mark_line() |
trends, time series |
| Bar | mark_bar() |
categories / histograms (with transform) |
| Box | mark_boxplot(extent="min-max") |
distribution per group |
| Rect | mark_rect() |
heatmaps (after aggregation) |
| Rule | mark_rule() |
mean/median lines |
| Text | mark_text() |
labels |
| Area | mark_area() |
cumulative/stacked trends |
Encodings & channel types¶
# Shorthand: channel="field:type" N=nominal, O=ordinal, Q=quantitative, T=temporal
x="body_mass_g:Q", y="species:N", color="species:N", size="pop:Q"
x=alt.X("flipper_length_mm:Q", bin=True), color=alt.Color("species:N", title="Species")
x=alt.X("date:T", timeUnit="yearmonth"), y="passengers:Q"
order="date:T", tooltip=["a", "b"], opacity="value:Q"
Altair infers types from pandas dtypes — verify strings aren't silently treated as categories when you mean numbers.
Histograms & heatmaps need transforms¶
# Histogram
alt.Chart(df).mark_bar().encode(
x=alt.X("flipper_length_mm:Q", bin=alt.Bin(maxbins=30)),
y="count()",
)
# Heatmap of two categoricals
alt.Chart(df).mark_rect().encode(
x="day:N", y="time:N",
color=alt.Color("count()", scale=alt.Scale(scheme="blues")),
)
Common Operations¶
Layering & composing¶
mean_rule = alt.Chart(df).mark_rule(color="red").encode(y="mean(total_bill):Q")
scatter = alt.Chart(df).mark_point().encode(x="total_bill:Q", y="tip:Q")
scatter + mean_rule # layered chart
alt.layer(scatter, mean_rule) # same thing
alt.hconcat(chart1, chart2) # side by side (also: | operator)
alt.vconcat(chart1, chart2) # stacked (also: & operator)
Facets (small multiples)¶
alt.Chart(penguins).mark_point().encode(
x="bill_length_mm:Q", y="bill_depth_mm:Q",
color="species:N",
).facet(column="island:N")
# facet rows + columns
chart.facet(row="sex:N", column="day:N")
Interactivity & selections¶
# Zoom/pan everywhere + tooltip
alt.Chart(df).mark_point().encode(
x="x:Q", y="y:Q", tooltip=["name"]
).interactive()
# Brush selection that highlights matching points
brush = alt.selection_interval() # or selection_point()
alt.Chart(df).mark_point().encode(
x="total_bill:Q", y="tip:Q",
color=alt.condition(brush, "day:N", alt.value("lightgray")),
).add_params(brush)
# Legend filter: click legend values
sel = alt.selection_point(fields=["species"], bind="legend")
alt.Chart(penguins).mark_point().encode(
x="flipper_length_mm:Q", y="body_mass_g:Q",
color=alt.condition(sel, "species:N", alt.value("lightgray")),
).add_params(sel)
Axes, titles, legends, styles¶
chart = chart.properties(
title={"text": "Tips by day", "subtitle": "n = 244"},
width=400, height=300,
)
chart = chart.encode(
x=alt.X("total_bill:Q", axis=alt.Axis(title="Total bill (USD)")),
color=alt.Color("day:N", legend=alt.Legend(title="Day", orient="top")),
)
# Color schemes: viridis, category10, tableau10, blues, reds, coolwarm, dark2
alt.Chart(...).mark_point().encode(color=alt.Color("z:Q", scale=alt.Scale(scheme="viridis")))
Saving¶
chart.save("chart.html") # standalone interactive HTML
chart.save("chart.json") # Vega-Lite spec
chart.save("chart.png", scale_factor=2) # requires vl-convert-python / altair_saver
Practical Examples¶
1. Interactive scatter with brushing (data science demo)¶
import altair as alt
import pandas as pd
df = pd.read_csv("penguins.csv").dropna()
brush = alt.selection_interval()
pts = alt.Chart(df).mark_point().encode(
x="bill_length_mm:Q", y="bill_depth_mm:Q",
color=alt.condition(brush, "species:N", alt.value("lightgray")),
opacity=alt.condition(brush, alt.value(1), alt.value(0.3)),
).add_params(brush).properties(title="Brush to highlight species")
# histogram that reacts to the brush
bars = alt.Chart(df).mark_bar().encode(
x="species:N", y="count()",
color=alt.condition(brush, "species:N", alt.value("lightgray")),
).add_params(brush)
(pts | bars).properties(width=380)
2. Layered mean + raw trend¶
base = alt.Chart(df).encode(x="date:T")
line = base.mark_line(color="steelblue").encode(y="sales:Q")
mean = base.mark_rule(color="firebrick", strokeDash=[4, 4]).encode(y="mean(sales):Q")
(line + mean).properties(title="Monthly sales with mean")
3. Binned heatmap for large data¶
alt.Chart(df).mark_rect().encode(
x=alt.X("total_bill:Q", bin=alt.Bin(maxbins=20)),
y=alt.Y("tip:Q", bin=alt.Bin(maxbins=20)),
color=alt.Color("count()", scale=alt.Scale(scheme="blues")),
).properties(title="Density of tips vs bill")
Common Patterns¶
# Consistent base chart reuse
base = alt.Chart(df).encode(x="flipper_length_mm:Q").properties(width=300)
hist = base.mark_bar().encode(y="count()")
kde_ish = base.transform_density("flipper_length_mm").mark_line().encode(
x=alt.X("value:Q"), y="density:Q")
# Aggregated bar (group means)
alt.Chart(df).mark_bar().encode(
x="day:N",
y="mean(total_bill):Q",
color=alt.condition(brush, "day:N", alt.value("lightgray")),
)
# Transform before plot (filter rows)
alt.Chart(df).transform_filter(alt.datum.total_bill > 10).mark_point().encode(...)
Best Practices¶
- Think in encodings, not pixels — Altair's defaults handle scales, axes, legends, and colors.
- Aggregate inside the spec (
y="mean(x):Q",y="count()") — don't precompute unless you must. - Use
transform_*(transform_filter,transform_density,transform_bin) for declarative data steps. - Give channels explicit types in shorthand (
:Q,:N,:T) when inference could misread. - Compose charts with
+,|,&,.facet()instead of hand-laying subplots. - Add
.interactive()for instant pan/zoom; selections give real linked interaction. - Data > ~10k points: aggregate, sample, or use
mark_circle+ binned encodings to keep Vega fast.
Common Mistakes¶
| Mistake | Consequence | Correct approach |
|---|---|---|
Forgetting :Q/:N on numeric-looking strings |
Treated as categories | Explicit type or pd.to_numeric |
| Plotting every row of 100k+ rows | Slow rendering | Bin, aggregate, or sample first |
| Expecting matplotlib-style side effects | Charts only appear when it's the last expression | Assign to variable and render, or chart.show() |
| Aggregation missing for heatmap | Vega error: aggregates must be explicit | color="count()" with mark_rect |
chart + chart2 mixing incompatible data |
Weird scales | Ensure shared fields/types or use resolve_scale |
Old altair_saver PNG flow |
Deprecation errors | Use vl-convert-python or save HTML/JSON |
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Chart blank in Jupyter | Renderer not enabled | pip install altair_viewer; check alt.renderers.enable("default") |
Cannot read properties of undefined (reading 'factory') |
Vega-Lite/Altair version mismatch | pip install -U altair vl-convert-python |
| Scale collapses to one tick | Wrong inferred type | Explicit :Q / :T |
| Save PNG fails | Converter missing | pip install vl-convert-python |
Data values all NaN after transform |
Field name/type mismatch | Use alt.datum.field and verify column names |
Related Cheat Sheets¶
- plotly.md · bokeh.md — interactive alternatives with different models
- matplotlib.md · seaborn.md — static statistical charts
- pandas.md — tidy DataFrames feed Altair directly
- Data Visualization category index · Home · Index
Official Documentation¶
Last Updated¶
2026-09-06 — Initial version.