To create 3D subplots in Matplotlib, pass projection="3d" to the subplot you want in 3D: fig.add_subplot(1, 2, 2, projection="3d"), or make every subplot 3D with plt.subplots(2, 2, subplot_kw={"projection": "3d"}). You can mix 2D and 3D subplots in one figure and use any 3D plot type (surface, wireframe, scatter, line) in each. This guide shows each approach with real Matplotlib windows, including 3D scatter subplots, per-panel projections with subplot_mosaic and fixing cut-off labels.
Tested with Python 3.12.5, Matplotlib 3.11.2 and NumPy 2.5.3; the plots are real Matplotlib windows. Reference: 3D plots as subplots and the mplot3d toolkit.
Mix 2D and 3D subplots with add_subplot()
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-3, 3, 60)
X, Y = np.meshgrid(x, x)
Z = np.exp(-(X ** 2 + Y ** 2) / 2)
fig = plt.figure(figsize=(10, 4.5))
ax1 = fig.add_subplot(1, 2, 1) # a normal 2D subplot
ax1.contourf(X, Y, Z, levels=15, cmap="viridis")
ax1.set_title("2D contour")
ax2 = fig.add_subplot(1, 2, 2, projection="3d") # a 3D subplot next to it
ax2.plot_surface(X, Y, Z, cmap="viridis")
ax2.set_title("3D surface")
plt.tight_layout()
plt.show()
fig.add_subplot(111, projection="3d") is the same idea for a figure with a single 3D plot (1 row, 1 column, first subplot).
A grid of 3D subplots with plt.subplots()
subplot_kw passes its keywords to every subplot, so all of them become 3D axes:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(-3, 3, 40)
X, Y = np.meshgrid(x, x)
rng = np.random.default_rng(3)
fig, axes = plt.subplots(2, 2, figsize=(8, 6), subplot_kw={"projection": "3d"}) # every subplot is 3D
axes[0, 0].plot_surface(X, Y, np.sin(np.hypot(X, Y)), cmap="coolwarm")
axes[0, 0].set_title("plot_surface")
axes[0, 1].plot_wireframe(X, Y, X * np.exp(-X ** 2 - Y ** 2), rstride=3, cstride=3)
axes[0, 1].set_title("plot_wireframe")
pts = rng.normal(size=(200, 3))
axes[1, 0].scatter(pts[:, 0], pts[:, 1], pts[:, 2], c=pts[:, 2], cmap="plasma", s=12)
axes[1, 0].set_title("scatter")
t = np.linspace(0, 6 * np.pi, 300)
axes[1, 1].plot(np.cos(t), np.sin(t), t, color="tab:green")
axes[1, 1].set_title("3D line (helix)")
plt.tight_layout()
plt.show()
3D scatter plots in subplots
A common use of 3D subplots is to show the same point cloud from several angles, since readers of a static image cannot rotate it:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(8)
groups = {"A": rng.normal(0, 1, (80, 3)), "B": rng.normal(3, 1, (80, 3))}
fig, axes = plt.subplots(1, 3, figsize=(12, 4.2), subplot_kw={"projection": "3d"})
for ax, (elev, azim) in zip(axes, [(20, -60), (20, 30), (80, -90)]):
for name, pts in groups.items():
ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2], s=10, label=name)
ax.view_init(elev=elev, azim=azim) # same data, three camera angles
ax.set_title(f"elev={elev}, azim={azim}")
axes[0].legend()
plt.tight_layout()
plt.show()
For more on 3D scatter plots (colours, sizes, saving and rotating), see how to create a 3D scatter plot in Matplotlib.
Only some panels in 3D: subplot_mosaic
In Matplotlib 3.7 and later, subplot_mosaic() accepts per_subplot_kw, so you can make just one named panel 3D in a complex layout:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 200)
fig, axd = plt.subplot_mosaic([["surface", "line"], ["surface", "bars"]], figsize=(10, 6),
per_subplot_kw={"surface": {"projection": "3d"}}) # only one panel is 3D
X, Y = np.meshgrid(np.linspace(-2, 2, 40), np.linspace(-2, 2, 40))
axd["surface"].plot_surface(X, Y, X * Y, cmap="viridis")
axd["line"].plot(x, np.sin(x))
axd["bars"].bar(["A", "B", "C"], [3, 5, 2])
for name, ax in axd.items():
ax.set_title(name)
plt.tight_layout()
plt.show()
Do you need to import Axes3D?
No. Since Matplotlib 3.2 the 3D projection is registered automatically; from mpl_toolkits.mplot3d import Axes3D is only needed in very old versions. The object you get back is a 3D axes with extra methods such as set_zlabel():
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig = plt.figure()
ax2d = fig.add_subplot(1, 2, 1)
ax3d = fig.add_subplot(1, 2, 2, projection="3d") # no "from mpl_toolkits.mplot3d import Axes3D" needed
print("2D axes:", type(ax2d).__name__)
print("3D axes:", type(ax3d).__name__, "from", type(ax3d).__module__)
print("has set_zlabel?", hasattr(ax2d, "set_zlabel"), "/", hasattr(ax3d, "set_zlabel"))
Output:
2D axes: Axes
3D axes: Axes3D from mpl_toolkits.mplot3d.axes3d
has set_zlabel? False / True
projection="3d" returns an Axes3D.Fix cut-off labels in 3D subplots
3D axes fill their whole subplot area, so in narrow or crowded figures the z-axis label is easily clipped at the edge. tight_layout() alone does not always help; shrinking the 3D box with set_box_aspect(None, zoom=...) (Matplotlib 3.6+) leaves more margin:
import matplotlib.pyplot as plt
import numpy as np
X, Y = np.meshgrid(np.linspace(-3, 3, 40), np.linspace(-3, 3, 40))
Z = np.cos(X) * np.sin(Y)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4.5), subplot_kw={"projection": "3d"})
for ax in (ax1, ax2):
ax.plot_surface(X, Y, Z, cmap="terrain")
ax.set_xlabel("X axis")
ax.set_ylabel("Y axis")
ax.set_zlabel("Z value")
ax1.set_title("default box size")
ax2.set_box_aspect(None, zoom=0.85) # shrink the box so labels fit
ax2.set_title("set_box_aspect(None, zoom=0.85)")
plt.tight_layout()
plt.show()
zoom=0.85 draws the 3D box smaller, leaving more margin for the labels.You can also give the figure more width, or increase ax.zaxis.labelpad.
More Matplotlib 3D and subplot tutorials:
- Create a 3D scatter plot in Matplotlib
- Create multiple plots in one figure
- Fix overlapping subplots with tight_layout()
- Add a secondary y-axis in Matplotlib
Frequently asked questions
How do I create 3D subplots in Matplotlib?
Use fig.add_subplot(rows, cols, index, projection="3d") for each 3D subplot, or plt.subplots(rows, cols, subplot_kw={"projection": "3d"}) to make them all 3D.
What does fig.add_subplot(111, projection=’3d’) mean?
It adds a single subplot (1 row, 1 column, position 1) that uses the 3D projection, so you can call 3D methods such as plot_surface() and scatter(x, y, z) on it.
Can I mix 2D and 3D subplots in one figure?
Yes. Create the 2D subplots normally and pass projection="3d" only to the ones that should be 3D, or use subplot_mosaic(per_subplot_kw=...).
Do I need to import Axes3D for 3D plots?
Not in Matplotlib 3.2 or later; projection="3d" works without the import.
How do I plot 3D scatter plots in subplots?
Create 3D subplots with subplot_kw={"projection": "3d"} and call ax.scatter(x, y, z) on each; use view_init() to show different angles.
Why is the z-axis label cut off in my 3D subplot?
The 3D box fills the axes area. Use ax.set_box_aspect(None, zoom=0.85), a wider figure, or a larger ax.zaxis.labelpad.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn