Altair
Altair exposes a declarative grammar-of-graphics API that compiles to Vega-Lite - charts become JSON specs you can version, diff, and embed on the web.
Recipe
Quick-reference recipe card - copy-paste ready.
import altair as alt
import pandas as pd
df = pd.read_csv("sales.csv", parse_dates=["ordered_at"])
chart = (
alt.Chart(df)
.mark_line(point=True)
.encode(x="ordered_at:T", y="sum(revenue):Q", color="region:N")
.properties(width=600, height=300, title="Revenue by region")
)
chart.save("revenue.json")When to reach for this:
- Specs you want in git as JSON, not imperative plotting code
- Layered charts (
+,|) with readable composition - Embedding in static sites via Vega-Embed
- Teaching visualization grammar without matplotlib boilerplate
Working Example
import altair as alt
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
df = pd.DataFrame(
{
"region": np.repeat(["East", "West"], 150),
"revenue": rng.normal(150, 35, 300).clip(20),
"units": rng.integers(1, 60, 300),
}
)
base = alt.Chart(df).encode(
x=alt.X("revenue:Q", bin=alt.Bin(maxbins=20), title="Revenue"),
color="region:N",
)
hist = base.mark_bar(opacity=0.7).properties(width=400, height=250, title="Distribution")
scatter = (
alt.Chart(df)
.mark_circle(size=60, opacity=0.5)
.encode(
x="units:Q",
y="revenue:Q",
color="region:N",
tooltip=["region", "units", "revenue"],
)
.properties(width=400, height=250, title="Units vs revenue")
)
dashboard = alt.hconcat(hist, scatter).resolve_scale(color="shared")
rule = (
alt.Chart(pd.DataFrame({"y": [150]}))
.mark_rule(color="firebrick", strokeDash=[4, 4])
.encode(y="y:Q")
)
layered = scatter + rule
dashboard.save("dashboard.json")
layered.save("scatter_target.json")What this demonstrates:
- Bin encoding in declarative form
- Horizontal concat with shared color scale
- Layered rule annotation with
+ - JSON export for embedding
Deep Dive
How It Works
Chart(data)+mark_*+encodechannels (x,y,color,tooltip).- Shorthand
:Qquantitative,:Nnominal,:Ttemporal,:Oordinal. - Transforms (
transform_filter,transform_aggregate) stay in spec. - Compiler emits Vega-Lite JSON rendered by JS in browser or PNG via altair_saver.
Encoding Types
| Shorthand | Type |
|---|---|
:Q | Quantitative |
:N | Nominal (categories) |
:O | Ordered categories |
:T | Temporal |
Python Notes
import altair as alt
alt.data_transformers.disable_max_rows() # only when you accept large embeds
alt.renderers.enable("json") # notebook default varies by versionGotchas
- Default 5000 row limit - Altair samples large frames. Fix: aggregate in pandas/Polars first or disable max rows knowingly.
- Wide data - multiple y columns need
transform_foldor melt. Fix: tidy to long format upstream. - Sorting nominal axes - default alphabetical misorders months. Fix:
sort=["Jan", "Feb", ...]or:Owith order. - Saving PNG without dependencies - needs vl-convert or altair_saver. Fix: ship JSON to front-end or install converter in CI.
- Interactive selections on huge data - slow client-side. Fix: filter data server-side before chart.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Plotly | Rich built-in interactivity | You want Vega-Lite spec portability |
| matplotlib | Mature print workflows | Declarative JSON is the deliverable |
| ggplot (plotnine) | R ggplot migration | Vega embed is the target |
| seaborn | Fast pandas EDA | Spec serialization matters |
FAQs
Why Vega-Lite JSON?
- Front-end can render without re-running Python.
- Specs diff cleanly in code review.
How do I facet?
alt.Chart(df).mark_point().encode(x="x", y="y").facet("region:N")How do I filter in the spec?
alt.Chart(df).transform_filter(alt.datum.revenue > 100).mark_bar().encode(...)How do I combine charts?
hconcat,vconcat,|and&operators layer or concat.resolve_scalealigns axes and legends.
Can Altair read Polars?
- Pass Polars via
pl.DataFramewhen supported or.to_pandas().
How do I add mean line?
rule = alt.Chart(df).mark_rule(strokeDash=[4,4]).encode(y="mean(revenue):Q")
chart + ruleHow do I embed in HTML?
- Save JSON; use Vega-Embed script tag in static page.
- Or
chart.save("out.html")when HTML renderer configured.
What about accessibility?
- Provide tooltips and meaningful axis titles.
- Do not rely on color alone - use shape or facet too.
How do I set color scheme?
.encode(color=alt.Color("region:N", scale=alt.Scale(scheme="set2")))Is Altair good for maps?
- Basic geo supported via mark_geoshape.
- Heavy GIS often needs dedicated tools.
Related
- Plotly - interactive alternative
- Visualization Basics - chart choice
- Visualization Best Practices - accessible color
- Plotting Large Data - aggregate first
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.