Figure-Level vs. Axes-Level
The one structural idea behind Seaborn's whole API — why some functions facet and return a grid while others draw on a single Axes — and how to choose.
Seaborn has two kinds of plotting functions, and almost every "wait, why doesn't that work?" moment comes from mixing them up. Once you see the split clearly, the entire API organizes itself into a simple map.
This is a foundational page. Spend the time here — it pays off on every later chapter, because each chart type comes in both flavors.
Two kinds of functions
Axes-level functions draw onto a single matplotlib Axes (one set of
x/y axes). They behave like ordinary matplotlib commands: they take an ax=
argument, draw on it, and return that Axes. Examples: scatterplot,
lineplot, histplot, kdeplot, boxplot, violinplot, barplot,
countplot, regplot, heatmap.
Figure-level functions manage an entire figure. They create the
figure and its axes for you, can split the data into a grid of panels
(facets), and return a grid object (FacetGrid, JointGrid, or
PairGrid) — not an Axes. Examples: relplot, displot, catplot,
lmplot, jointplot, pairplot.
Here's the key relationship: each figure-level function is a wrapper
around a family of axes-level functions, and the kind argument picks
which one to use.
So sns.relplot(..., kind="scatter") ends up calling scatterplot under
the hood, and sns.catplot(..., kind="box") calls boxplot. The
figure-level function adds faceting, figure management, and a legend placed
outside the plot — on top of the same drawing.
jointplot and pairplot are figure-level too
jointplot and pairplot are also figure-level (they return JointGrid
and PairGrid), but they're not simple one-kind wrappers — they assemble
several axes-level plots into a fixed layout (a center plot with margins;
a matrix of pairwise panels). We give each its own chapter later.
Seeing the difference in what they return
The cleanest way to tell them apart is to look at what comes back. An
axes-level function returns a matplotlib Axes:
A figure-level function returns a Seaborn grid object:
That difference in return type is not a technicality — it determines what you can do next.
What axes-level gives you: composition
Because axes-level functions accept an ax=, you can build your own
matplotlib figure and place several different plots into it. Figure-level
functions can't do this — they insist on owning the whole figure.
This is the axes-level superpower: drop a Seaborn plot into any matplotlib layout, next to anything else.
What figure-level gives you: faceting
Figure-level functions own the figure so they can split it into a grid of
panels — one per category — with col and row. This is small multiples,
and it's a single keyword:
Try doing that with scatterplot and you'd be writing the subplot loop
yourself. Faceting is the reason figure-level functions exist.
You want one panel per category (small multiples) using a single
Seaborn call with col=. Which kind of function do you need?
An axes-level function like scatterplot.
A figure-level function like relplot.
Either one — col works on both.
Neither — Seaborn can't facet.
Choosing between them
| Question | Use figure-level | Use axes-level |
|---|---|---|
Do I want facets (col/row)? | Yes → relplot/displot/catplot | — |
| Do I need to place this next to other plots on one figure? | — | Yes → scatterplot, boxplot, ... with ax= |
| Do I want a clean legend placed outside automatically? | Figure-level does this | Legend goes inside the Axes |
| How do I set the size? | height + aspect (per facet) | figsize via plt.subplots |
A simple rule of thumb:
- Exploring, or you might want facets? Start with the figure-level
function (
relplot,displot,catplot). - Assembling a custom multi-panel figure, or composing with matplotlib?
Use the axes-level function and pass
ax=.
Don't try to put a figure-level plot inside a subplot
A common mistake is fig, ax = plt.subplots(); sns.relplot(..., ax=ax).
Figure-level functions don't take an ax= — they make their own figure,
so you'll get an error or a stray empty Axes. If you need it inside an
existing layout, switch to the axes-level version (scatterplot).
Setting size: height/aspect vs figsize
One more practical consequence: figure-level functions are sized with
height (inches per facet) and aspect (width = height * aspect), not
figsize. Axes-level functions use the matplotlib figure's figsize. Mixing
these up is another common source of "why is my plot the wrong size?"
Your turn
Use a figure-level function to draw a scatter of total_bill
(x) vs tip (y) from tips, split into one column per time
(Lunch and Dinner). Use sns.relplot with col="time" and assign the
result to g.
Because time has two values, you should get a 1-row, 2-column grid of
panels.
Check your understanding
What does an axes-level function (e.g. sns.boxplot) return?
A FacetGrid that manages the whole figure.
A matplotlib Axes object that it drew on.
The DataFrame, with a new column added.
Nothing — they only have side effects.
Which call will fail or misbehave?
sns.scatterplot(data=df, x="a", y="b", ax=my_ax)
sns.relplot(data=df, x="a", y="b", col="g")
sns.relplot(data=df, x="a", y="b", ax=my_ax)
sns.boxplot(data=df, x="g", y="a")
sns.catplot(data=tips, x="day", y="total_bill", kind="violin") produces a
violin plot. Which axes-level function is doing the actual drawing underneath?
sns.boxplot
sns.violinplot
sns.stripplot
sns.relplot
You're sizing a relplot and it ignores figsize=(10, 4). Why, and what
should you use?
relplot has a bug; report it.
Figure-level functions are sized with height and aspect (per facet), not figsize.
You must call plt.figure(figsize=...) first.
figsize only works in dark mode.
You now hold the master key to Seaborn's API: every chart type appears as an axes-level drawer and a figure-level wrapper, and you know when to pick each. From here on we tour the chart families — starting with relational plots, where two variables meet.