To make a 3D scatter plot in Python with Matplotlib, create a 3D axes with fig.add_subplot(projection="3d") and call ax.scatter(x, y, z). The same scatter() options you know from 2D plots (c, s, cmap, marker, label) work in 3D. This guide covers the basic Matplotlib 3D scatter plot, colour and size by value, groups with a legend, rotating the view, saving the plot as PNG, PDF, a rotating GIF or a reusable figure, and an interactive HTML version.
Tested with Python 3.12.5, Matplotlib 3.11.2, NumPy 2.5.3 and Plotly 7.1.0. The plots are real Matplotlib windows; the saved file sizes come from the Windows Command Prompt. Reference: Matplotlib 3D scatterplot example and Axes3D.view_init.
Basic 3D scatter plot
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
n = 150
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)
z = 0.8 * x + 0.5 * y + rng.normal(0, 0.4, n)
fig = plt.figure(figsize=(7, 6))
ax = fig.add_subplot(projection="3d") # a 3D axes
ax.scatter(x, y, z) # x, y and z coordinates of each point
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("3D scatter plot")
plt.show()

You do not need to import Axes3D in current Matplotlib versions; projection="3d" is enough. With plt.subplots(), write fig, ax = plt.subplots(subplot_kw={"projection": "3d"}).
Colour and size the points by value
Pass values to c (with a cmap) to colour the points and to s to size them. Here the colour shows Z and the size shows a fourth variable. Add a colorbar so readers can read the colours:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
n = 150
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)
z = 0.8 * x + 0.5 * y + rng.normal(0, 0.4, n)
sales = rng.uniform(1, 10, n) # a 4th variable
sizes = 10 * sales # marker size from it
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(projection="3d")
points = ax.scatter(x, y, z, c=z, cmap="viridis", s=sizes, alpha=0.8, edgecolor="k", linewidth=0.3)
fig.colorbar(points, ax=ax, shrink=0.6, pad=0.1, label="Z value")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
plt.show()

Several groups with different markers and a legend
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(1)
centres = {"Group A": (0, 0, 0), "Group B": (4, 4, 1), "Group C": (0, 4, 4)}
fig = plt.figure(figsize=(7, 6))
ax = fig.add_subplot(projection="3d")
for (name, centre), marker in zip(centres.items(), ["o", "^", "s"]):
pts = rng.normal(centre, 0.8, size=(60, 3))
ax.scatter(pts[:, 0], pts[:, 1], pts[:, 2], marker=marker, label=name, s=30)
ax.legend()
ax.set_xlabel("Feature 1")
ax.set_ylabel("Feature 2")
ax.set_zlabel("Feature 3")
plt.show()

scatter() call per group, each with a label.Rotate a 3D scatter plot
In a window you can rotate with the mouse. In code, set the camera with ax.view_init(elev, azim): elev tilts the view up or down, azim turns it around the vertical axis (Matplotlib 3.6+ also accepts roll):
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(42)
n = 150
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)
z = 0.8 * x + 0.5 * y + rng.normal(0, 0.4, n)
fig = plt.figure(figsize=(12, 4))
views = [(30, -60), (20, 30), (70, 45)] # (elevation, azimuth) in degrees
for i, (elev, azim) in enumerate(views, start=1):
ax = fig.add_subplot(1, 3, i, projection="3d")
ax.scatter(x, y, z, c=z, cmap="plasma", s=12)
ax.view_init(elev=elev, azim=azim) # rotate the camera
ax.set_title(f"elev={elev}, azim={azim}")
plt.tight_layout()
plt.show()

Save a 3D scatter plot (PNG, PDF, rotating GIF, pickle)
savefig() saves the current view. bbox_inches="tight" matters for 3D plots because the z-axis label is often cut off otherwise. A FuncAnimation that changes the azimuth produces a rotating GIF, and pickle saves the whole figure object so you can reopen it later and keep rotating it:
import os
import pickle
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
rng = np.random.default_rng(42)
n = 150
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)
z = 0.8 * x + 0.5 * y + rng.normal(0, 0.4, n)
fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(projection="3d")
ax.scatter(x, y, z, c=z, cmap="viridis")
ax.set_zlabel("Z")
fig.savefig("scatter3d.png", dpi=200, bbox_inches="tight") # image
fig.savefig("scatter3d.pdf", bbox_inches="tight") # vector file for print
with open("scatter3d.pickle", "wb") as f: # the figure object itself
pickle.dump(fig, f)
def turn(angle):
ax.view_init(elev=25, azim=angle)
ani = FuncAnimation(fig, turn, frames=range(0, 360, 4), interval=50)
ani.save("scatter3d_rotation.gif", writer="pillow", fps=20) # rotating GIF
for name in ["scatter3d.png", "scatter3d.pdf", "scatter3d.pickle", "scatter3d_rotation.gif"]:
print(f"{name:<24} {os.path.getsize(name) / 1024:8.1f} KB")
Output:
scatter3d.png 156.9 KB
scatter3d.pdf 45.0 KB
scatter3d.pickle 173.4 KB
scatter3d_rotation.gif 2833.3 KB

Reopen the pickled figure in the same Matplotlib version:
import pickle
import matplotlib.pyplot as plt
with open("scatter3d.pickle", "rb") as f:
fig = pickle.load(f) # the saved 3D figure, still interactive
fig.axes[0].set_title("Reloaded from scatter3d.pickle")
plt.show()

scatter3d.pickle.Interactive 3D scatter plot you can share (HTML)
Matplotlib windows are interactive only on your computer. To share a plot people can rotate in a browser, Plotly’s scatter_3d writes a stand-alone HTML file:
import os
import numpy as np
import plotly.express as px
rng = np.random.default_rng(42)
n = 150
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)
z = 0.8 * x + 0.5 * y + rng.normal(0, 0.4, n)
fig = px.scatter_3d(x=x, y=y, z=z, color=z, title="Interactive 3D scatter")
fig.write_html("scatter3d.html") # opens in any browser; drag to rotate
fig.write_html("scatter3d_cdn.html", include_plotlyjs="cdn") # loads plotly.js from the web instead
for name in ["scatter3d.html", "scatter3d_cdn.html"]:
print(f"{name:<20} {os.path.getsize(name) / 1024:8.0f} KB")
Output:
scatter3d.html 4723 KB
scatter3d_cdn.html 16 KB
The first file embeds the whole plotly.js library, so it works offline but is several megabytes. With include_plotlyjs="cdn" the file is tiny and loads the library from the internet when opened.
In Jupyter, %matplotlib widget (the ipympl package) also makes Matplotlib 3D plots rotatable inside the notebook.
Tips for readable 3D scatter plots
- Label all three axes; without depth cues it is hard to tell which axis is which.
- Use colour for the most important variable: depth makes positions hard to compare.
- For thousands of points (point clouds), use small markers (
s=1) anddepthshade=Falsefor speed. - Show two or three fixed views side by side in reports, since readers cannot rotate a printed image.
- Check whether a 2D scatter plot with colour would be clearer before choosing 3D.
More Matplotlib 3D and scatter tutorials:
- Create 3D subplots in Matplotlib
- Update a plot in a loop in Matplotlib
- Plot a line of best fit in Python
- Change the background colour of a plot
Frequently asked questions
How do I make a 3D scatter plot in Matplotlib?
Create a 3D axes with ax = fig.add_subplot(projection="3d") and call ax.scatter(x, y, z). Label the axes with set_xlabel(), set_ylabel() and set_zlabel().
Can I use plt.scatter for a 3D plot?
No: plt.scatter() draws on a 2D axes. Create a 3D axes first and call ax.scatter(x, y, z) on it.
How do I colour a 3D scatter plot by value?
Pass the values to c and choose a colormap: ax.scatter(x, y, z, c=values, cmap="viridis"), then add fig.colorbar().
How do I rotate a 3D scatter plot in Matplotlib?
Drag in the plot window, or set the view in code with ax.view_init(elev=30, azim=45). Animate the azimuth with FuncAnimation for a rotating plot.
How do I save a 3D scatter plot?
Use fig.savefig("plot.png", dpi=200, bbox_inches="tight"). Save a rotating GIF with FuncAnimation and writer="pillow", or pickle the figure to reopen it later.
How do I make an interactive 3D scatter plot?
Matplotlib windows are interactive on your computer; for a shareable version use Plotly’s px.scatter_3d() and write_html().

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