HOWTO · Matplotlib
How to Create a Surface Plot in Matplotlib
Learn how to create a Matplotlib 3D surface plot from grid-shaped data, then customize its colors, view, and saved figure.
On this page
To create a 3D surface plot in Matplotlib, prepare compatible two-dimensional X, Y, and Z arrays, create an axes with projection="3d", and pass the arrays to Axes3D.plot_surface. The example below builds a coordinate grid from a mathematical function, labels the axes, adds a color bar, and saves the figure. The same workflow works with measured values when their coordinates and heights can be represented on a rectangular grid.
Setting Up Your Environment
Install NumPy for array and grid operations and Matplotlib for plotting. In a terminal, run python -m pip install numpy matplotlib in the Python environment you plan to use. If you already manage packages with an environment or notebook, install both packages there instead so they are available to the interpreter running your script.
The example imports NumPy and matplotlib.pyplot. Matplotlib’s 3D plotting support is included with Matplotlib; projection="3d" creates the 3D axes, so a separate Axes3D import is not needed. The method used on that axes is Axes3D.plot_surface.
Preparing Your Data
A surface plot assigns a height to each location on a two-coordinate plane. For regularly spaced coordinates, np.linspace() creates one-dimensional coordinate values and np.meshgrid() expands them into two-dimensional X and Y grids. Calculate Z from those grids, or provide measured heights arranged at the same grid locations.
The three arrays must have compatible shapes: each Z[i, j] is the height at the coordinate pair X[i, j], Y[i, j]. Current Matplotlib versions also accept one-dimensional x and y coordinate vectors and expand them against a two-dimensional Z, but the height array itself must still match the grid dimensions: a Z computed on a different grid than X and Y raises a ValueError, as the troubleshooting section below shows with a real run. Build the coordinate grids first, then calculate or reshape the heights on those same grids. For scattered measurements that do not form a rectangular grid, prepare or interpolate grid data before using plot_surface.
Creating the Surface Plot
This complete example uses the smooth radial function sin(sqrt(x² + y²)). It creates the grids, draws the surface, labels all three dimensions, adds a color scale, and saves the result as surface-plot.png in the current directory. Run it in an environment with NumPy and Matplotlib installed. The example has no printed console output; its result is a saved image and a Matplotlib figure window when the active backend supports display.
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="viridis")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Surface Plot")
fig.colorbar(surface, ax=ax, label="Z value")
fig.savefig("surface-plot.png", dpi=150, bbox_inches="tight")
plt.show()
Because this script does not print text, there is no console output block. The color bar associates the surface colors with their values; the grid supplies the geometry, while the colormap controls its appearance. In a notebook, the figure is generally displayed by the plotting environment; in a script, plt.show() asks the configured Matplotlib backend to display it.
Customizing Your Surface Plot
The example below applies the most common customizations to the same surface. It switches the colormap to plasma, removes the mesh edges, and rotates the camera to a fixed elevation and azimuth before adding the color bar and saving the figure as customized-surface-plot.png.
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
surface = ax.plot_surface(X, Y, Z, cmap="plasma", edgecolor="none")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Customized Surface Plot")
ax.view_init(elev=30, azim=45)
fig.colorbar(surface, ax=ax, label="Z value")
fig.savefig("customized-surface-plot.png", dpi=150, bbox_inches="tight")
plt.show()
Choose a colormap that makes the change in Z easy to read. In this example, cmap="plasma" replaces the first example’s viridis progression, and the color bar helps readers interpret that mapping. The edgecolor="none" option removes the drawn mesh edges, which can distract from a continuous surface on a dense grid; keep the default edges when the sampling grid itself should stay visible.
To change the camera, call ax.view_init(elev=30, azim=45) before saving or displaying the figure. elev changes the viewing elevation and azim rotates the view around the vertical axis. Adjust these values until the shape and axes are clear; the best angle depends on the data and what you want to show. Compared with the first figure, the rotated camera in the second figure exposes more of the far slope of the central peak.
For a dense grid, plot_surface can use row and column sampling controls such as rcount and ccount. Lower counts draw fewer sampled rows and columns and can make a large surface quicker to render, at the cost of visible detail. Keep enough samples to preserve important changes in the data. Save a shareable image with fig.savefig("surface-plot.png", dpi=150, bbox_inches="tight"); call it before plt.show() for compatibility with display backends that close the figure after showing it. Use a different filename or directory when you need to retain multiple versions.
Compare With a Wireframe Plot
A wireframe draws only the grid lines of the same surface, without filled faces or a colormap. Axes3D.plot_wireframe accepts the same X, Y, and Z grids, so you can switch between the two views without preparing new data. A wireframe helps when the grid structure itself matters, when a filled surface would hide the far side of the shape, or as a quick structural check of unfamiliar data before you commit to a full surface render.
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_wireframe(X, Y, Z, rstride=2, cstride=2, color="tab:blue")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("Wireframe Plot")
fig.savefig("wireframe-plot.png", dpi=150, bbox_inches="tight")
plt.show()
The rstride and cstride arguments skip rows and columns so the lines stay readable; the example draws every second row and column of a 50 by 50 grid. Unlike plot_surface, a wireframe has no face colors, so there is nothing for a color bar to explain. For a final presentation, the filled surface usually communicates height differences better, while the wireframe shows more clearly how the grid itself is sampled.
Fix a Shape Mismatch Error
The most common plot_surface failure is a Z array whose shape does not match the coordinate grids. The script below computes Z on a 50 by 50 grid but passes it together with the 100 by 100 X and Y grids:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
x_small = np.linspace(-5, 5, 50)
y_small = np.linspace(-5, 5, 50)
X_small, Y_small = np.meshgrid(x_small, y_small)
Z = np.sin(np.sqrt(X_small**2 + Y_small**2))
fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")
ax.plot_surface(X, Y, Z, cmap="viridis")
plt.show()
Running the script stops at the plot_surface call with this error:
ValueError: shape mismatch: objects cannot be broadcast to a single shape. Mismatch is between arg 0 with shape (100, 100) and arg 2 with shape (50, 50).
To fix it, compute Z from the same grids you pass to plot_surface, as the first example does, so that Z.shape equals X.shape and Y.shape. When you are unsure, print the three shapes before plotting and compare them: every entry of Z needs exactly one coordinate pair, neither more nor fewer.
Conclusion
The key steps are to create coordinate grids, make Z match those grids, add a 3D axes, and call Axes3D.plot_surface. A colormap and color bar explain the height values, while the view and sampling controls help present the shape clearly. When you need to inspect the grid itself, plot_wireframe renders the same data as lines. Save the figure when you need an image file, and adjust the grid resolution or camera for the data you are plotting.
FAQ
-
What data can I use for a surface plot?
Use values defined over two coordinates, with each height corresponding to a location in a rectangular grid. A function is convenient for a demonstration; measured values work when arranged on compatible coordinate grids. Scattered points need to be converted to a grid first. -
How do I install the required libraries?
Install NumPy and Matplotlib in the environment that runs your script withpython -m pip install numpy matplotlib. -
Can I change the surface colors?
Yes. Set thecmapargument ofplot_surface, then add a color bar withfig.colorbar(surface, ax=ax)to show how colors correspond to values. -
How do I change the viewing angle?
Useax.view_init(elev=..., azim=...). Elevation changes the camera’s height and azimuth changes its rotation around the plot. -
Why must
X,Y, andZhave compatible shapes?
Matplotlib needs a coordinate pair and a height for each grid location. IfZwas computed on a different grid,plot_surfaceraises aValueErrorabout shapes that cannot be broadcast together. Create the coordinate grids withnp.meshgrid()and calculateZfrom those same grids so corresponding entries line up.