How to Create a 3D Scatter Plot in Python Matplotlib

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()
Basic Matplotlib 3D scatter plot of 150 random points with X, Y and Z axis labels
A 3D scatter plot. Drag with the mouse in the window to rotate it.

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()
Matplotlib 3D scatter plot with points coloured by Z using the viridis colormap, marker sizes from a fourth variable and a colorbar
Colour shows Z; marker size shows a fourth variable.

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()
Matplotlib 3D scatter plot of three clusters with circle, triangle and square markers and a legend
One 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()
The same Matplotlib 3D scatter plot shown from three camera angles set with view_init elevation and azimuth
Three views of the same data.

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
Command Prompt output listing the saved 3D scatter files: PNG, PDF, pickle and a rotating GIF with their sizes in KB
The four saved files and their sizes.

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()
Matplotlib window showing a 3D scatter plot reopened from a pickle file, still interactive, titled Reloaded from scatter3d.pickle
The figure loaded back from 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) and depthshade=False for 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:

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().