Skip to content

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

pip install altair pandas      # or: uv add altair pandas
import altair as alt

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

Official Documentation

Last Updated

2026-09-06 — Initial version.