Light Touches with Matplotlib
Seaborn returns matplotlib objects — add titles, labels, and limits with a few lines, without leaving Seaborn behind.
Seaborn does not replace matplotlib — it is built on top of it. Every Seaborn function ultimately draws with matplotlib and hands you back a matplotlib object when it is done. That is great news: it means you can do all the hard statistical work in Seaborn's concise vocabulary, and then reach for a little matplotlib only for the final polish — a real title, human-readable axis labels, a sensible axis range.
This is not a matplotlib course, so we keep it light. The goal is to learn the handful of one-liners that turn a good exploratory chart into a presentable one, and — just as important — to learn which object you got back so you call the right one-liner.
Two kinds of return value
Seaborn has two families of plotting functions, and they hand back two different things:
| Family | Examples | Returns | Polish via |
|---|---|---|---|
| Axes-level | scatterplot, histplot, boxplot, heatmap | a matplotlib Axes | ax.set_title(...), ax.set_xlabel(...), ... |
| Figure-level | relplot, displot, catplot, pairplot | a Seaborn grid g | g.set_axis_labels(...), g.figure.suptitle(...), ... |
The rule of thumb: an axes-level function draws on a single Axes and
gives it back, so you customize that Axes directly. A figure-level
function builds and manages its own figure (possibly with many panels), so
you customize through the grid it returns. Using the wrong one is the most
common source of "why did my title land on the wrong panel?" confusion.
Polishing an axes-level plot
Axes-level functions return a matplotlib Axes. Capture it in a variable —
conventionally ax — and then call its setter methods:
Four small calls and the chart reads like a finished figure: a title that names the comparison, axes labeled with units, and limits that frame the data deliberately. None of this required leaving Seaborn for more than a line at a time.
Controlling the figure size
Sometimes you want to set the figure size before drawing. Create the
figure and axes yourself with plt.subplots, then pass that ax into the
Seaborn function. This is the one place you reach for
import matplotlib.pyplot as plt:
Because you made the Axes, you control its size and placement, and you can
even put several Seaborn plots onto different axes of the same figure. This
plt.subplots(...) + ax=ax pattern only works with axes-level
functions — figure-level functions insist on making their own figure.
Never wrap a figure-level plot in plt.subplots
A figure-level function (relplot, displot, catplot, ...) creates its
own figure and ignores any ax= you pass. If you make a fig, ax and then
call relplot, you'll get two figures — your empty one and Seaborn's. Use
plt.subplots only with axes-level functions.
Polishing a figure-level grid
Figure-level functions return a grid object, conventionally g (a
FacetGrid, PairGrid, or JointGrid). It is not an Axes, so it has
its own polishing methods that apply across all panels at once:
A few things to notice:
g.set_axis_labels("X", "Y")labels the shared x and y across all panels — you do not label each one separately.g.set_titles("{col_name}")sets each panel's title from the value of the faceting column (here, eachsex). The{col_name}is a template Seaborn fills in per panel.g.figure.suptitle("...")adds one overarching title for the whole figure. Because it sits above the panels, you usually follow it withg.figure.subplots_adjust(top=0.9)to make room so it does not overlap the panel titles.
When the grid has a legend (from hue), you can rename it with
g.legend.set_title("..."). To set a shared axis range across all panels,
use g.set(xlim=(...), ylim=(...)). And if you need a single underlying
Axes — say, to draw one more thing on it — reach it with g.ax (for a
single-panel grid) or g.axes (the array of all panels).
Do not use plt.title() on a figure-level grid
plt.title(...) sets the title of matplotlib's current axes — which, on a
multi-panel grid, is whichever panel was drawn last. Your title lands on the
wrong panel. For a one big title use g.figure.suptitle(...); for per-panel
titles use g.set_titles(...). The same caution applies to plt.xlabel /
plt.ylabel — prefer g.set_axis_labels(...).
You wrote g = sns.relplot(data=tips, x="total_bill", y="tip", col="time")
and want one overall title centered above both panels. Which call is
right?
plt.title("Tips by service time")
g.figure.suptitle("Tips by service time")
g.set_xlabel("Tips by service time")
ax.set_title("Tips by service time")
Rotating crowded tick labels
When category names along the x-axis overlap, rotate them. On an Axes,
ax.tick_params(axis="x", rotation=45) does it in one line:
An equivalent, slightly more flexible approach is
plt.setp(ax.get_xticklabels(), rotation=45), which lets you also set
things like horizontal alignment. Either is fine; pick whichever reads
better to you. (On a figure-level grid you would rotate via
g.tick_params(axis="x", rotation=45) or by looping over g.axes.flat.)
A worked example: from raw to readable
Putting it together on a single axes-level chart. We start with a bare
scatterplot and apply the polish in a few lines: a title that frames the
relationship, axis labels with units, and a deliberate y-range starting at
zero.
The statistics — the grouping by origin, the color mapping, the legend —
were all Seaborn. The polish — the title, the unit-bearing labels, the
y-range, the lighter frame — was a handful of matplotlib lines on the Axes
Seaborn handed back. That division of labor is the whole idea: Seaborn for
the substance, a light touch of matplotlib for the finish.
Your turn
Draw a histogram of total_bill from the tips dataset with
sns.histplot, capturing the returned Axes in a variable named ax.
Then:
- Set the title to exactly
Distribution of total bills. - Set the x-axis label to exactly
Total bill (USD).
Check your understanding
You call ax = sns.boxplot(data=tips, x="day", y="total_bill"). What is
ax, and how do you give the chart a title?
A Seaborn FacetGrid; title it with ax.set_titles(...).
A matplotlib Axes; title it with ax.set_title("...").
A pandas DataFrame; add a title by adding a column.
A figure-level grid; title it with g.figure.suptitle(...).
Why can you write fig, ax = plt.subplots(figsize=(7,4)) and then
sns.scatterplot(..., ax=ax), but the same trick does not work with
sns.relplot?
Because relplot cannot draw scatter plots.
Because scatterplot does not accept an ax argument.
Because scatterplot is axes-level and draws on the Axes you pass, while relplot is figure-level and builds its own figure.
Because plt.subplots only works after set_theme.
On a faceted grid g = sns.catplot(data=tips, x="day", y="tip", col="time", kind="box"), how do you label the shared x and y axes across both panels?
plt.xlabel("Day"); plt.ylabel("Tip")
g.set_axis_labels("Day", "Tip")
g.set_title("Day", "Tip")
ax.set_xlabel("Day"); ax.set_ylabel("Tip")
You now have the light-touch matplotlib you need: capture the Axes from an
axes-level call (or the grid from a figure-level one), then add a title,
labels, and limits in a few lines. Next we put it all together — themes,
color, and these finishing touches — into the craft of telling a clear
story with data.
Color Palettes
Color is an encoding, not decoration — matching the palette type (qualitative, sequential, diverging) to your data, with an eye on perception and accessibility.
Telling a Story with Data
From exploration to communication — choose the right chart, cut the clutter, and guide the reader's eye.