3D Plotting
Matplotlib's 3D support (the mplot3d toolkit) renders surfaces, wireframes, and point clouds in a rotatable 3D axes. It is adequate for exploring functions of two variables and visualizing scattered 3D data - and honest about its limits: it is software-rendered, not as fast or as pretty as dedicated tools, and most 3D stories are better told with a few 2D projections. Use it to understand the shape; use projections to communicate it.
A 3D Surface
import numpy as np
import matplotlib.pyplot as plt
X, Y = np.meshgrid(np.linspace(-3, 3, 80), np.linspace(-3, 3, 80))
Z = np.sin(np.sqrt(X ** 2 + Y ** 2))
fig = plt.figure(figsize=(7, 5))
ax = fig.add_subplot(projection="3d")
ax.plot_surface(X, Y, Z, cmap="viridis", edgecolor="none", alpha=0.9)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("z")
ax.view_init(elev=25, azim=130) # camera angle
ax.set_box_aspect([1, 1, 0.4]) # proportions of the box
plt.show()
fig.add_subplot(projection="3d") creates the 3D axes. plot_surface maps the (X, Y, Z) grid to a shaded mesh; cmap colors it by height. The camera is controlled with view_init(elev=..., azim=...) - elevation in degrees above the plane, azimuth the rotation around it - and in a live window you can drag to rotate instead. set_box_aspect fights the default "stretched cube" look by setting the box proportions explicitly.
Wireframes and Contour Projections
import numpy as np
import matplotlib.pyplot as plt
X, Y = np.meshgrid(np.linspace(-3, 3, 40), np.linspace(-3, 3, 40))
Z = np.sin(X) * np.cos(Y)
fig = plt.figure(figsize=(10, 4))
ax1 = fig.add_subplot(1, 2, 1, projection="3d")
ax1.plot_wireframe(X, Y, Z, color="tab:blue", linewidth=0.4)
ax1.view_init(elev=20, azim=120)
ax2 = fig.add_subplot(1, 2, 2)
ax2.contourf(X, Y, Z, levels=20, cmap="viridis")
ax2.contour(X, Y, Z, levels=20, colors="k", linewidths=0.3)
fig.tight_layout()
plt.show()
The two panels are the same data: a rotatable wireframe for exploration, a 2D filled contour for reading. The second is almost always the one that goes in the report - the 3D view is how you found the interesting angle, the projection is how the reader sees it.
3D Scatter
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(12)
x = rng.normal(size=500)
y = rng.normal(size=500)
z = rng.normal(size=500)
fig = plt.figure(figsize=(7, 5))
ax = fig.add_subplot(projection="3d")
ax.scatter(x, y, z, c=z, cmap="RdYlBu", s=12, alpha=0.7)
ax.set_xlabel("x"); ax.set_ylabel("y"); ax.set_zlabel("z")
plt.show()
3D scatter shows the joint distribution of three variables - the standard view for "do these three factors cluster together?" Color a fourth variable with c as in 2D. Rotate with the mouse in a live window to find the orientation that separates the clusters; then save two or three fixed angles as a small figure.
Honest Limits
- Overplotting hides structure faster in 3D than in 2D; alpha and small markers are mandatory, and faceted 2D pairs often say more.
- Axis labels and tick density fight each other in 3D; keep the box proportions honest (
set_box_aspect) or the shape lies. - Performance: a few thousand surface points is fine; beyond that, decimate the grid or switch to a rendering tool.
Summary
Create 3D axes with projection="3d", draw with plot_surface, plot_wireframe, and scatter, set the camera with view_init, and keep proportions honest with set_box_aspect. Use the rotation to find the story, then tell it with fixed-angle views and 2D contour projections - the 3D plot is the instrument, the projection is the exhibit.