DataLens

Faceting (small multiples)

Split one crowded chart into a grid of small, comparable charts with facet_col and facet_row, and control shared axes and panel titles.

Level: Intermediate · 7 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Put five categories' monthly revenue on one line chart and you get spaghetti: five tangled lines, a legend to decode, and nobody can follow Stationery. Faceting (also called small multiples) draws the same chart once per category, side by side, on the same scales. Each panel is simple, and comparing them is easy because the eye jumps between identical layouts.

Express makes it one argument: facet_col=.

import pandas as pd
import plotly.express as px

sales = pd.read_csv("sales.csv")
completed = sales[sales["status"] == "completed"]
monthly_cat = (completed.assign(month=completed["order_date"].str[:7])
               .groupby(["month", "category"], as_index=False)["revenue"].sum())
print(monthly_cat.head())
print(len(monthly_cat))
     month    category  revenue
0  2024-01      Coffee   285000
1  2024-01        Home   270000
2  2024-01     Kitchen   960000
3  2024-01  Stationery   178000
4  2024-02      Coffee   635000
24

Spaghetti first

The obvious way to show monthly revenue for five categories is one line chart with five coloured lines. Try it and see how readable it is:

fig = px.line(monthly_cat, x="month", y="revenue", color="category", markers=True,
              title="Revenue by category (one chart)")
fig.show()

Five crossing lines. You can read the biggest one, but the smaller categories are lost.

facet_col: one panel per category

Now the same data, but instead of five lines on one chart, each category gets its own small chart, side by side. The only change is facet_col= instead of color=:

fig = px.line(monthly_cat, x="month", y="revenue", facet_col="category", markers=True,
              title="Revenue by category (small multiples)")
fig.show()
print(len(fig.data), [a.text for a in fig.layout.annotations])
5 ['category=Coffee', 'category=Home', 'category=Kitchen', 'category=Stationery', 'category=Electronics']

Each category gets its own panel (and its own trace). The panels share the y-axis, so a tall line really is a big category, and the shapes are easy to compare: which categories grew, which were steady, which had one big month.

Wrapping into a grid

Five panels in one row get narrow. facet_col_wrap starts a new row after N panels:

fig = px.line(monthly_cat, x="month", y="revenue", facet_col="category",
              facet_col_wrap=3, markers=True, height=500)
fig.show()

facet_row= splits into rows by another variable. Combine both for a grid of two variables, for example category × channel:

monthly_cat_ch = (completed.assign(month=completed["order_date"].str[:7])
                  .groupby(["month", "category", "channel"], as_index=False)["revenue"].sum())
fig = px.bar(monthly_cat_ch, x="month", y="revenue", facet_col="category", facet_row="channel",
             height=450)
fig.show()

Shared or independent axes

By default, facets share axes. That's usually right: the point of small multiples is comparing like with like. But when one panel dwarfs the others, the small ones become flat lines. If you care about each panel's shape more than its size, free the y-axes:

fig = px.line(monthly_cat, x="month", y="revenue", facet_col="category",
              facet_col_wrap=3, markers=True, height=500)
fig.update_yaxes(matches=None, showticklabels=True)
fig.show()

matches=None unlinks the axes, and showticklabels=True shows each panel's own scale (hidden by default on inner panels).

Warning

Independent axes make every panel look equally important. A category with Rp 50.000 of revenue can look as dramatic as one with Rp 5.000.000. When you free the axes, say so in the title or subtitle.

Cleaner panel titles

Express titles each panel category=Coffee. Strip the prefix with for_each_annotation, which runs a function on every annotation (the panel titles are annotations):

fig = px.line(monthly_cat, x="month", y="revenue", facet_col="category", markers=True)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.show()
print([a.text for a in fig.layout.annotations])
['Coffee', 'Home', 'Kitchen', 'Stationery', 'Electronics']

Ordering the panels

Panels follow the data order unless you set category_orders. Put the most important panel first, or follow a natural order:

order = (monthly_cat.groupby("category")["revenue"].sum()
         .sort_values(ascending=False).index.tolist())
fig = px.line(monthly_cat, x="month", y="revenue", facet_col="category",
              category_orders={"category": order}, markers=True)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))
fig.show()
print(order)
['Electronics', 'Kitchen', 'Coffee', 'Home', 'Stationery']

Facet or colour?

Use colour (one chart) whenUse facets when
2-3 groups4+ groups
the lines rarely crosslines overlap and tangle
the exact comparison at each x mattersthe overall shape of each group matters

Why this matters for clean code

Faceting moves complexity out of the chart and into a consistent layout. In code, it means one px call with facet_col= instead of a loop that builds five figures with five sets of formatting that slowly drift apart. Same data, same function, same styling for every panel: a single source of truth for the whole grid.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Box, violin, and distribution plots →