Figure-level vs Axes-level
In one line: axes-level functions draw into an Axes you give them; figure-level functions create and own a whole Figure — and confusing the two causes most seaborn frustration.
The map
FIGURE-LEVEL → wraps these AXES-LEVEL functions
─────────────────────────────────────────────────────────
relplot → scatterplot, lineplot
displot → histplot, kdeplot, ecdfplot, rugplot
catplot → stripplot, swarmplot, boxplot, violinplot,
boxenplot, pointplot, barplot, countplot
lmplot → regplot
Standalone figure-level functions with no axes-level twin: pairplot, jointplot, clustermap.
Standalone axes-level with no figure-level wrapper: heatmap, residplot.
The differences
| Axes-level | Figure-level | |
|---|---|---|
Accepts ax= |
yes | no — TypeError |
| Creates a Figure | no | yes |
| Returns | the Axes |
FacetGrid / JointGrid / ClusterGrid |
Faceting (col=, row=) |
no | yes |
| Sizing | your figsize |
height= and aspect= |
| Legend placement | inside the Axes | outside the grid |
| Composable into a panel | yes | no |
Choosing
Axes-level when the plot is one panel of a figure you are composing:
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
sns.scatterplot(data=df, x="a", y="b", ax=axes[0])
sns.histplot(data=df, x="a", ax=axes[1])
sns.boxplot(data=long, x="g", y="v", ax=axes[2])
fig.savefig("panel.pdf", bbox_inches="tight")
Figure-level when you want small multiples across a variable:
g = sns.relplot(data=long, x="dose", y="response",
col="gene", col_wrap=4, hue="tissue",
kind="line", height=2.5, aspect=1.2)
The rule of thumb: faceting → figure-level; composing → axes-level.
Sizing figure-level plots
g = sns.relplot(data=df, x="a", y="b", col="gene",
height=3, # height of EACH facet, in inches
aspect=1.5) # width = height * aspect
There is no figsize. Total size is height × aspect × n_cols wide by height × n_rows tall. Passing figsize is an error, and this is probably the most common seaborn stumble.
You can override afterwards, though it may disturb the layout:
g.figure.set_size_inches(10, 6)
Customising a FacetGrid
g = sns.relplot(data=long, x="dose", y="response", col="gene", col_wrap=3)
g.set_axis_labels("Dose (µM)", "Response")
g.set_titles("{col_name}") # or "{col_var} = {col_name}"
g.set(xscale="log", ylim=(0, None))
g.tight_layout()
g.add_legend(title="Tissue")
g.legend.set_bbox_to_anchor((1.02, 0.5))
g.despine(left=True)
g.savefig("facets.pdf", dpi=300)
# reach through to matplotlib
g.figure # the Figure
g.axes # ndarray of Axes
g.axes.flat # iterate
g.axes_dict["TP53"] # by facet value
for ax in g.axes.flat:
ax.axhline(0, ls="--", c="grey", lw=0.8)
for gene, ax in g.axes_dict.items():
ax.set_title(GENE_LABELS.get(gene, gene))
g.axes_dict is the useful one for per-facet customisation — it is keyed by the facet variable's value, so you can annotate specific panels.
g.map and g.map_dataframe add extra layers to every facet:
g.map_dataframe(sns.rugplot, x="dose", height=0.05, color="grey")
Converting between them
If you have a figure-level call and need it inside your own layout, use the axes-level twin and do the faceting yourself:
# instead of: sns.relplot(..., col="gene") ← cannot take ax=
fig, axes = plt.subplots(2, 3, figsize=(12, 7), sharex=True, sharey=True)
for ax, (gene, sub) in zip(axes.flat, long.groupby("gene")):
sns.lineplot(data=sub, x="dose", y="response", hue="tissue",
ax=ax, legend=(ax is axes.flat[0]))
ax.set_title(gene)
More code, complete control. legend=(ax is axes.flat[0]) draws the legend only once instead of six times.
Legends
Figure-level plots put the legend outside the grid so it does not obscure data. Axes-level plots put it inside:
ax = sns.scatterplot(data=df, x="a", y="b", hue="g")
ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", frameon=False)
sns.move_legend(ax, "upper left", bbox_to_anchor=(1.02, 1)) # cleaner
sns.move_legend (0.11.2+) is the tidy way — it preserves the title and formatting that ax.legend() would discard.
clustermap and jointplot
Figure-level with their own grid objects:
cg = sns.clustermap(mat, z_score=0, cmap="vlag", figsize=(8, 10))
cg.ax_heatmap # the main panel
cg.ax_row_dendrogram; cg.ax_col_dendrogram
cg.ax_cbar
cg.dendrogram_row.reordered_ind # ← the clustered row order
jg = sns.jointplot(data=df, x="a", y="b", kind="hex")
jg.ax_joint; jg.ax_marg_x; jg.ax_marg_y
jg.ax_joint.axhline(0)
cg.dendrogram_row.reordered_ind is genuinely useful — it gives you the gene order the clustering produced, which you can then export or reuse in another plot.
Common mistakes
ax=on a figure-level function →TypeError: got an unexpected keyword argument 'ax'.figsize=on a figure-level function. Useheight/aspect.- Expecting
col=to work onscatterplot. It does not; userelplot. plt.savefig()after a figure-level call — may grab the wrong figure. Useg.savefig().plt.title()afterrelplot— lands on one facet, not the grid. Useg.figure.suptitle().- Not knowing about
g.axes_dictand looping awkwardly. - A legend drawn once per facet in a manual loop.
See also
Seaborn · Faceting · Figure and Axes · pyplot vs Object-Oriented API · Subplots and Layout · Applied - Heatmaps and Clustermaps