pythonforbio.
[WASM idle]
Plotting06/14

Figure-level vs Axes-level

Starting sandbox…
Beginnerlesson

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 functionTypeError: got an unexpected keyword argument 'ax'.
  • figsize= on a figure-level function. Use height/aspect.
  • Expecting col= to work on scatterplot. It does not; use relplot.
  • plt.savefig() after a figure-level call — may grab the wrong figure. Use g.savefig().
  • plt.title() after relplot — lands on one facet, not the grid. Use g.figure.suptitle().
  • Not knowing about g.axes_dict and 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

scratch

No output yet — run the code to populate this drawer.