Lesson 16
Customising and Saving Seaborn Charts
Finish Seaborn charts with Matplotlib: draw into subplots with ax=, set titles and limits with ax.set, move the legend, and save figure-level grids.
Seaborn draws the chart; Matplotlib finishes it. Axes-level functions hand back an Axes, so every Matplotlib method you know still works. Figure-level functions hand back a grid with its own methods for labels, titles, and saving.

'%20stroke-width%3D'3'%2F%3E%3Crect%20x%3D'48'%20y%3D'48'%20width%3D'544'%20height%3D'48'%20rx%3D'14'%20fill%3D'%231E293B'%2F%3E%3Crect%20class%3D'mdfx-an'%20x%3D'48'%20y%3D'48'%20width%3D'6'%20height%3D'48'%20rx%3D'3'%20fill%3D'url(%23g)'%20opacity%3D'1'%20style%3D'animation-delay%3A0.00s'%2F%3E%3Ctext%20x%3D'68'%20y%3D'69'%20fill%3D'%23F8FAFC'%20font-size%3D'15'%20font-family%3D'ui-monospace%2Cmonospace'%3Eax%20%3D%20sns.boxplot(...)%3C%2Ftext%3E%3Ctext%20x%3D'68'%20y%3D'87'%20fill%3D'%23CBD5E1'%20font-size%3D'13'%20font-family%3D'ui-sans-serif%2Csystem-ui'%3Eaxes-level%3A%20you%20get%20the%20Axes%20back%3C%2Ftext%3E%3Crect%20x%3D'48'%20y%3D'106'%20width%3D'544'%20height%3D'48'%20rx%3D'14'%20fill%3D'%231E293B'%2F%3E%3Crect%20class%3D'mdfx-an'%20x%3D'48'%20y%3D'106'%20width%3D'6'%20height%3D'48'%20rx%3D'3'%20fill%3D'url(%23g)'%20opacity%3D'1'%20style%3D'animation-delay%3A0.50s'%2F%3E%3Ctext%20x%3D'68'%20y%3D'127'%20fill%3D'%23F8FAFC'%20font-size%3D'15'%20font-family%3D'ui-monospace%2Cmonospace'%3Eax.set(title%3D...%2C%20ylim%3D...)%3C%2Ftext%3E%3Ctext%20x%3D'68'%20y%3D'145'%20fill%3D'%23CBD5E1'%20font-size%3D'13'%20font-family%3D'ui-sans-serif%2Csystem-ui'%3Elabels%2C%20limits%2C%20and%20titles%20in%20one%20call%3C%2Ftext%3E%3Crect%20x%3D'48'%20y%3D'164'%20width%3D'544'%20height%3D'48'%20rx%3D'14'%20fill%3D'%231E293B'%2F%3E%3Crect%20class%3D'mdfx-an'%20x%3D'48'%20y%3D'164'%20width%3D'6'%20height%3D'48'%20rx%3D'3'%20fill%3D'url(%23g)'%20opacity%3D'1'%20style%3D'animation-delay%3A1.00s'%2F%3E%3Ctext%20x%3D'68'%20y%3D'185'%20fill%3D'%23F8FAFC'%20font-size%3D'15'%20font-family%3D'ui-monospace%2Cmonospace'%3Esns.move_legend(ax%2C%20...)%3C%2Ftext%3E%3Ctext%20x%3D'68'%20y%3D'203'%20fill%3D'%23CBD5E1'%20font-size%3D'13'%20font-family%3D'ui-sans-serif%2Csystem-ui'%3Eput%20the%20legend%20where%20it%20fits%3C%2Ftext%3E%3Crect%20x%3D'48'%20y%3D'222'%20width%3D'544'%20height%3D'48'%20rx%3D'14'%20fill%3D'%231E293B'%2F%3E%3Crect%20class%3D'mdfx-an'%20x%3D'48'%20y%3D'222'%20width%3D'6'%20height%3D'48'%20rx%3D'3'%20fill%3D'url(%23g)'%20opacity%3D'1'%20style%3D'animation-delay%3A1.50s'%2F%3E%3Ctext%20x%3D'68'%20y%3D'243'%20fill%3D'%23F8FAFC'%20font-size%3D'15'%20font-family%3D'ui-monospace%2Cmonospace'%3Eg%20%3D%20sns.relplot(...)%3C%2Ftext%3E%3Ctext%20x%3D'68'%20y%3D'261'%20fill%3D'%23CBD5E1'%20font-size%3D'13'%20font-family%3D'ui-sans-serif%2Csystem-ui'%3Efigure-level%3A%20you%20get%20a%20grid%20back%3C%2Ftext%3E%3Crect%20x%3D'48'%20y%3D'280'%20width%3D'544'%20height%3D'48'%20rx%3D'14'%20fill%3D'%231E293B'%2F%3E%3Crect%20class%3D'mdfx-an'%20x%3D'48'%20y%3D'280'%20width%3D'6'%20height%3D'48'%20rx%3D'3'%20fill%3D'url(%23g)'%20opacity%3D'1'%20style%3D'animation-delay%3A2.00s'%2F%3E%3Ctext%20x%3D'68'%20y%3D'301'%20fill%3D'%23F8FAFC'%20font-size%3D'15'%20font-family%3D'ui-monospace%2Cmonospace'%3Eg.savefig(%22chart.png%22)%3C%2Ftext%3E%3Ctext%20x%3D'68'%20y%3D'319'%20fill%3D'%23CBD5E1'%20font-size%3D'13'%20font-family%3D'ui-sans-serif%2Csystem-ui'%3Esave%20the%20whole%20figure%2C%20trimmed%3C%2Ftext%3E%3C%2Fsvg%3E)
Everything in one ax.set call
ax.set takes any Axes property as a keyword: title, xlabel, ylabel, xlim, ylim, and more. It replaces a run of separate plt.title, plt.xlabel, and plt.ylim calls.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
rng = np.random.default_rng(26)
study = pd.DataFrame({
"track": rng.choice(["Python", "Data", "Web"], 120),
"mode": rng.choice(["online", "classroom"], 120),
"week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()
ax = sns.boxplot(data=study, x="track", y="score")
ax.set(title="Quiz scores by track", xlabel="", ylabel="score out of 100", ylim=(30, 95))
plt.show()
print(ax.get_title(), ax.get_ylim())Output:
Quiz scores by track (30.0, 95.0)Seaborn inside subplots
Create the grid with plt.subplots, then pass each Axes as ax=. Seaborn charts, plain Matplotlib charts, and text can share one figure.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
rng = np.random.default_rng(26)
study = pd.DataFrame({
"track": rng.choice(["Python", "Data", "Web"], 120),
"mode": rng.choice(["online", "classroom"], 120),
"week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()
fig, (left, right) = plt.subplots(1, 2, figsize=(11, 4))
sns.histplot(data=study, x="hours", binwidth=1, ax=left)
sns.scatterplot(data=study, x="hours", y="score", hue="track", ax=right)
left.set_title("How long learners study")
right.set_title("And what it is worth")
fig.suptitle("Study hours", fontsize=16)
plt.tight_layout()
plt.show()
print(len(fig.axes), "axes")Output:
2 axesMove the legend
sns.move_legend rebuilds the legend in a new place and keeps its handles. bbox_to_anchor=(1, 1) with "upper left" sits it just outside the right edge, and title renames it.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
rng = np.random.default_rng(26)
study = pd.DataFrame({
"track": rng.choice(["Python", "Data", "Web"], 120),
"mode": rng.choice(["online", "classroom"], 120),
"week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()
ax = sns.scatterplot(data=study, x="hours", y="score", hue="track")
sns.move_legend(ax, "upper left", bbox_to_anchor=(1, 1), title="Track", frameon=False)
plt.show()
print(ax.get_legend().get_title().get_text())Output:
TrackFinishing a figure-level grid
A FacetGrid has set for every panel, set_axis_labels, set_titles, and figure for the whole Matplotlib figure. Size comes from height and aspect when you create it, not from figsize.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
rng = np.random.default_rng(26)
study = pd.DataFrame({
"track": rng.choice(["Python", "Data", "Web"], 120),
"mode": rng.choice(["online", "classroom"], 120),
"week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()
g = sns.catplot(data=study, x="track", y="score", col="mode", kind="violin",
height=3.2, aspect=1.1, inner="quart")
g.set_axis_labels("", "score")
g.set_titles("{col_name}")
g.set(ylim=(30, 95))
g.figure.suptitle("Scores by track and mode", y=1.04)
plt.show()
print(g.figure.get_size_inches().round(2))Output:
[7.15 3.2 ]Save the result
Axes-level charts save with plt.savefig like any Matplotlib figure. Grids have g.savefig, which trims the margins by default so outside legends and titles are not cut off.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
rng = np.random.default_rng(26)
study = pd.DataFrame({
"track": rng.choice(["Python", "Data", "Web"], 120),
"mode": rng.choice(["online", "classroom"], 120),
"week": rng.integers(1, 7, 120),
})
study["hours"] = (rng.uniform(1, 6, 120) + study["week"] * 0.8).round(1)
study["score"] = (35 + study["hours"] * 4.5 + rng.normal(0, 7, 120)).clip(0, 100).round()
import os
ax = sns.barplot(data=study, x="track", y="score", errorbar=None)
plt.savefig("means.png", dpi=120, bbox_inches="tight")
plt.close()
g = sns.relplot(data=study, x="hours", y="score", col="mode", height=3)
g.savefig("grid.png", dpi=120)
plt.close("all")
print(os.path.exists("means.png"), os.path.exists("grid.png"))Output:
True TrueIn this page's playground the files go to a temporary folder in your browser's memory and disappear when you reload.
Key takeaways
-
Axes-level functions return an Axes; finish it with ax.set(...)
-
Pass ax= to place Seaborn charts inside plt.subplots
-
sns.move_legend relocates and retitles a legend
-
Grids use set_axis_labels, set_titles, figure.suptitle, and g.savefig
-
Next: What Is Scikit-learn
-
Previous: Themes, Contexts, and Palettes
Loading playground…
Check your understanding
Customising and Saving Seaborn Charts Quiz
What do axes-level Seaborn functions return?
1 / 10