$ pip install altair vega_datasetsCore lib + sample datasets. Addvl-convert-pythonfor PNG/SVG.import altair as alt★The universal alias.import pandas as pdAltair is built around tidy DataFrames.from altair.datasets import dataBuilt-in samples:data.cars(),data.stocks().alt.renderers.enable("...")Rarely needed — notebooks auto-render.
alt.Chart(df)★Wrap a DataFrame — the start of every chart.alt.Chart("data.csv")A URL or path — data stays external (no row cap).alt.Chart(df).mark_point().encode(...)★The recipe: data + mark + encoding.alt.Chart(df, width=400, height=300)Size can be set at construction too.data: DataFrame · CSV/JSON URL · alt.DataMust be tidy — one row per observation.
.mark_point()★Scatter (open shapes);mark_circle/mark_square= filled..mark_line()★Line;mark_trailvaries width by a field..mark_bar()★Bars & histograms..mark_area()Filled area; stack or overlap..mark_rect()Heatmap cells / 2-D bins..mark_rule() · .mark_tick()Reference lines · 1-D strip plots..mark_text() · .mark_arc()Labels · pie & donut wedges..mark_boxplot() · .mark_geoshape()Stats box · maps (alsoerrorband/errorbar)..mark_bar(color="steelblue", opacity=.7)A constant style for every mark (not a column).
.encode(x="a", y="b")★Position — the two you'll use most.color="c"★Colour by a column; drives the legend.size="c" · shape="c" · opacity="c"Bubble size · marker shape · transparency.tooltip=["a", "b"]★Hover fields — instant interactivity.theta="v" · text="label"Pie angle · text content.detail="id" · order="seq"Group without a visual · draw/stack order.column="c" · row="c"Split into a grid of small multiples.x2="a2" · xOffset="g"Range ends (bars/areas) · grouped bars.
":Q" quantitative★Continuous numbers → continuous axis.":N" nominal★Unordered category → distinct colours.":O" ordinalOrdered category → ordered scale.":T" temporal★Dates/times → time axis.:G= geojson.x="a:Q"★Shorthand string — type drives everything.alt.X("a", type="quantitative")The verbose equivalent of"a:Q".# type is auto-inferred from the dtypeSpell it out when the guess is wrong.
alt.X("a:Q", scale=alt.Scale(zero=False))Don't force the axis to start at 0.alt.Y("b:N", sort="-x")★Sort categories by another channel.alt.X("a:Q").title("Speed")Fluent per-channel title.alt.Color("c:N", scale=alt.Scale(scheme="viridis"))Pick a colour scheme.alt.Y("v:Q", stack="normalize")100% stacked;stack=Noneto overlap.alt.Tooltip("v:Q", format=".2f")Formatted hover value.
y="mean(v):Q"★Aggregate right inside the field string.y="count():Q"★Row count — no field needed.y="sum(v):Q"Alsomedian,min,max,stdev,q1.alt.X("v:Q", bin=True)★Auto-bin for a histogram.bin=alt.Bin(maxbins=30)Control the bin count / step.alt.X("date:T", timeUnit="yearmonth")Bucket time (year, month, hours…).
.transform_filter(alt.datum.v > 5)★Keep matching rows;alt.datum= a row..transform_calculate(z="datum.x + datum.y")Derive a new field..transform_aggregate(m="mean(v)", groupby=["c"])Group-and-summarise..transform_window(r="rank()", sort=[..])Running totals, ranks, moving averages..transform_fold(["a","b"], as_=["key","val"])★Wide → long to tidy your data..transform_lookup(...)Join fields from another dataset..transform_regression("x", "y")Trend line; alsotransform_density,_loess.
alt.Scale(scheme="category10")Named colour palette.alt.Scale(type="log") · domain=[0,100]Log axis · fixed range.alt.Axis(format="$,.0f", labelAngle=-45)Tick format & label rotation.alt.Legend(orient="bottom")Move / restyle the legend.alt.X("a:Q", axis=None)Hide an axis entirely.alt.Color("c:N", legend=None)Drop the legend but keep the colours.
.properties(width=400, height=300)★Set the drawing size..properties(title="My chart")A chart title..properties(width="container")Fill the parent element (responsive)..configure_view(strokeWidth=0)Remove the outer border..configure_axis(labelFontSize=12)Global axis styling..configure_title(anchor="start")Left-align the title.configure_*= chart-wide.
c1 + c2★Layer — overlay (e.g. line + points).c1 | c2★hconcat — side by side.c1 & c2vconcat — stacked top to bottom.alt.layer(c1, c2) · alt.hconcat(..)Function forms of+/|/&.chart.facet(column="c:N", columns=3)★Small multiples from one column.chart.repeat(column=["a","b","c"])Repeat a template across fields (SPLOM).
sel = alt.selection_point()★Click to select points.brush = alt.selection_interval()★Drag a rectangle (brush).chart.add_params(sel)★Attach a param — replacesadd_selection.alt.selection_point(fields=["Origin"], bind="legend")Clickable, filtering legend.p = alt.param(bind=alt.binding_range(min=0, max=10))A slider bound to a variable.alt.selection_interval(bind="scales")Bind the brush to pan & zoom.
alt.when(sel).then("Origin:N").otherwise(alt.value("lightgray"))★Colour selected rows; grey the rest.color=alt.when(brush).then(..).otherwise(..)Drop it straight intoencode().alt.when(alt.datum.v > 5).then(..)Condition on a data predicate, not a selection..when(a).then(x).when(b).then(y)Chain multiple conditions.alt.value("red")A literal constant (not a column reference).alt.condition(sel, a, b)olderPre-5.5 style;when/thenis preferred now.
chart.interactive()★One-call pan + zoom.lower.transform_filter(brush)★Filter one chart by a brush in another.points.add_params(brush) + shared dataTwo charts, one param → linked brushing.bind="legend"Turn the legend into a filter control.# selection → filter is the core idiomSelect in one view, drive another.
alt.theme.enable("dark")★Apply to every chart in the session.alt.theme.names()List built-ins:default,fivethirtyeight,ggplot2,vox…alt.theme.activeThe name currently in effect.@alt.theme.register("my", enable=True)Register a reusable custom theme.alt.themes.enable(..)oldPre-5.5 API — nowalt.theme(singular).
chart.save("chart.html")★Standalone interactive HTML (needs no extras).chart.save("chart.png")Raster; needsvl-convert-python.chart.save("chart.svg")Vector; also.pdfvia vl-convert.chart.to_json()★The Vega-Lite spec — the source of truth.chart.to_dict()Same spec as a Python dict.chart.save("c.html", inline=True)Bundle the JS so it works offline.
scatter: mark_point().encode(x="a:Q", y="b:Q")Two quantitative fields.bar: mark_bar().encode(x="c:N", y="sum(v):Q")Category vs aggregate.line: mark_line().encode(x="d:T", y="v:Q", color="s:N")Time series, one line per series.histogram: mark_bar().encode(alt.X("v:Q", bin=True), y="count()")Binned + counted.heatmap: mark_rect().encode(x="a:O", y="b:O", color="mean(v):Q")Two categories + a colour value.pie: mark_arc().encode(theta="v:Q", color="c:N")Angle by value; alsomark_boxplotfor stats.
# data must be tidy / long★One row per observation;transform_foldto reshape.alt.Chart("data.csv")URL data isn't embedded → sidesteps the row cap.alt.data_transformers.enable("vegafusion")Handle large datasets efficiently.alt.data_transformers.disable_max_rows()Override the 5,000-row guard (embeds all rows).MaxRowsErrorgotchaDefault cap is 5,000 rows — see the two lines above.