Introduction to Altair

ISOM 675 · Data Visualization

Prasanna Parasurama

What is Altair?

Altair is a declarative visualization library in Python, built on top of Vega-Lite.

Python specification

first_chart = (
    alt.Chart(df)
    .mark_bar()
    .encode(
        x="Category:N",
        y="Value:Q",
    )
)

Vega-Lite specification

{
  "data": {"values": [
    {"Category":"A","Value":5},
    {"Category":"B","Value":8},
    {"Category":"C","Value":3}
  ]},
  "mark": "bar",
  "encoding": {
    "x": {"field": "Category",
          "type": "nominal"},
    "y": {"field": "Value",
          "type": "quantitative"}
  }
}

Rendered chart

Anatomy of an Altair Chart

Data → mark → encoding → chart properties

alt.Chart(data).mark_*().encode(...).properties(...)
bar_chart = (
    alt.Chart(df)            # data
    .mark_bar()              # mark
    .encode(                 # channels
        x="Category:N",      # Category → x
        y="Value:Q",         # Value → y
        color="Group:N",     # Group → color
    )
    .properties(title=alt.Title(
        "Values by category",
        subtitle="East and West groups",
    ))
)

Input data · df

Category Value Group
0 A 5 East
1 B 8 West
2 C 3 East

Wide vs. long data

Wide form

One row per Month (independent variable). Metadata appears in row and column labels.

Input data · wide_sales

North South
Month
1 12 9
2 18 13
3 15 17

Wide to long with melt()

long_sales = wide_sales.reset_index().melt(
    id_vars="Month",
    var_name="Region", value_name="Sales",
)

Long form

One row per observation. Metadata appears as values in the table.

Reshaped data · long_sales

Month Region Sales
0 1 North 12
1 2 North 18
2 3 North 15
3 1 South 9
4 2 South 13
5 3 South 17

It’s easier to work with long-form data in Altair. Why?

A mark determines what type of chart is drawn

base = (
    alt.Chart(monthly_sales)
    .encode(
        x="Month:O", y="Sales:Q",
    )
)
bars = base.mark_bar()
line = base.mark_line(point=True)
dots = base.mark_point(size=100)

Input data · monthly_sales

Month Sales
0 1 12
1 2 18
2 3 15
3 4 22
4 5 19
5 6 26

Altair data types

Priority is ranked 1–3. N uses distinct hues; O uses ordered shades.

Type Data type
Q Quantitative: measured amounts
N Nominal: categories without an order
O Ordinal: categories with an order
T Temporal: dates and times
priority_base = (
    alt.Chart(priority_points)
    .mark_circle(size=130)
    .encode(x="Hours:Q", y="Impact:Q")
)
nominal = priority_base.encode(
    color="Priority:N"
)
ordinal = priority_base.encode(
    color="Priority:O"
)

Input data · priority_points

Hours Impact Priority
0 2 3 1
1 3 7 2
2 4 5 3
3 5 8 1
4 6 4 2
5 7 9 3

Channels

Channel Type Channels Effect
Position x, y, x2, y2, longitude, latitude, xOffset, yOffset Places a mark or defines its span
Color color, fill Changes the hue or interior
Shape shape Selects a symbol
Size size Changes mark area or thickness
Facet facet, row, column Repeats a view by group
Sequence order Controls draw or stack order

Position: x and y

scatter = (
    alt.Chart(scatter_points)
    .mark_circle(size=90)
    .encode(x="X:Q", y="Y:Q")
)

Ranges: x2 and y2

schedule = (
    alt.Chart(project_tasks).mark_bar()
    .encode(
        x="Start:T", x2="End:T",
        y="Task:N",
    )
)

range_bars = (
    alt.Chart(group_ranges).mark_bar()
    .encode(
        x="Group:N", y="Low:Q",
        y2="High:Q",
    )
)

Geographic position: longitude and latitude

land = (
    alt.Chart(world_countries)
    .mark_geoshape(
        fill="lightgray", stroke="white",
    )
)
city_points = (
    alt.Chart(city_locations)
    .mark_circle(size=100)
    .encode(
        longitude="Longitude:Q",
        latitude="Latitude:Q",
        tooltip="City:N",
    )
)
city_map = (
    land + city_points
).project(type="equalEarth")

Offsets separate marks within a category

vertical = (
    alt.Chart(channel_sales).mark_bar()
    .encode(
        x="Category:N", xOffset="Channel:N",
        y="Value:Q", color="Channel:N",
    )
)
horizontal = (
    alt.Chart(channel_sales).mark_bar()
    .encode(
        x="Value:Q", y="Category:N",
        yOffset="Channel:N",
        color="Channel:N",
    )
)

Color, fill, and stroke

outlines = (
    alt.Chart(scatter_points)
    .mark_point(filled=False, size=160)
    .encode(
        x="X:Q", y="Y:Q", color="Group:N",
    )
)
filled = (
    alt.Chart(scatter_points)
    .mark_point(
        filled=True, size=160,
        stroke="black",
    )
    .encode(
        x="X:Q", y="Y:Q", fill="Group:N",
    )
)

Shape and size

shapes = (
    alt.Chart(scatter_points)
    .mark_point(size=150)
    .encode(
        x="X:Q", y="Y:Q", shape="Group:N",
    )
)
sizes = (
    alt.Chart(scatter_points)
    .mark_circle()
    .encode(
        x="X:Q", y="Y:Q", size="Magnitude:Q",
    )
)

Order controls drawing sequence

Order sets stack position or the connection sequence within each line series.

priority_order = {"Low": 1, "Medium": 2, "High": 3}
product_segments["PriorityOrder"] = (
    product_segments["Priority"].map(priority_order)
)
stack_base = alt.Chart(product_segments).mark_bar()
stack_base = stack_base.encode(
    x="Product:N", y="Value:Q", color="Priority:N",
)
priority_stack = stack_base.encode(
    order="PriorityOrder:Q",
    color=alt.Color("Priority:N", legend=alt.Legend(
        values=["High", "Medium", "Low"],
    )),
)

Input data · Product A

Product Priority Value PriorityOrder
0 A Low 8 1
1 A Medium 5 2
2 A High 3 3

Layering overlays charts on the same axes

monthly_bars = (
    alt.Chart(monthly_sales)
    .mark_bar()
    .encode(x="Month:O", y="Sales:Q")
)
average = (
    alt.Chart(monthly_sales)
    .mark_rule(color=SECONDARY)
    .encode(y="mean(Sales):Q")
)
layered = monthly_bars + average

Facet repeats one chart for each group

small_multiples = (
    alt.Chart(regional_sales)
    .mark_line(point=True)
    .encode(x="Month:O", y="Sales:Q")
    .properties(width=190, height=210)
    .facet(column="Region:N")
)

Input data · regional_sales · first two months

Month Region Sales
0 1 North 12
6 1 South 9
1 2 North 18
7 2 South 13

Concatenation places complete charts together

sales = (
    alt.Chart(monthly_sales)
    .mark_bar()
    .encode(x="Month:O", y="Sales:Q")
)
costs = (
    alt.Chart(monthly_sales)
    .mark_line(point=True)
    .encode(x="Month:O", y="Costs:Q")
)
side_by_side = sales | costs

Facet versus concatenation

Facet: one chart template, split by a field. Concat: explicitly combine charts.

faceted = (
    alt.Chart(regional_sales)
    .mark_line(point=True)
    .encode(x="Month:O", y="Sales:Q")
    .properties(width=160, height=150)
    .facet(column="Region:N")
)
base_line = (
    alt.Chart(regional_sales)
    .mark_line(point=True)
    .encode(x="Month:O", y="Sales:Q")
)
north = base_line.transform_filter(
    alt.datum.Region == "North")
south = base_line.transform_filter(
    alt.datum.Region == "South")
concatenated = north | south

Transform data within a chart

histogram = (
    alt.Chart(sample_values)
    .transform_filter(alt.datum.Value >= 0)
    .mark_bar()
    .encode(
        x=alt.X("Value:Q", bin=True),
        y="count()",
    )
)

Customize the chart’s guides and text

legend = alt.Legend(orient="right")
chart_title = alt.Title(
    "Monthly sales",
    subtitle="North and South, January–June",
    anchor="start",
)
polished = (
    base_line.encode(
        x=alt.X(
            "Date:T", title="Month",
            axis=alt.Axis(format="%b"),
        ),
        y=alt.Y("Sales:Q", title="Sales ($000)"),
        color=alt.Color(
            "Region:N", legend=legend,
        ),
        tooltip=["Region:N", "Date:T", "Sales:Q"],
    )
    .properties(
        width=390, height=250, title=chart_title,
    )
)