Images and 3D: imshow, contour and mplot3d

Gridded data on a colour scale, contour lines you can read, and the honest limits of a 3-D surface plot.

Grids with imshow and pcolormesh

imshow treats an array as pixels and is the fastest way to view a matrix. pcolormesh accepts explicit coordinate arrays and handles irregular spacing, at the cost of speed. Both need origin and aspect set deliberately or your image will be upside down and stretched.

import numpy as np
import matplotlib.pyplot as plt

grid = np.random.default_rng(0).random((40, 60))

fig, ax = plt.subplots(figsize=(7, 4))
im = ax.imshow(grid, origin="lower", extent=[0, 10, 0, 5],
               aspect="auto", cmap="magma", interpolation="nearest")
fig.colorbar(im, ax=ax, label="Intensity")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Channel")

# irregular grid: pcolormesh takes the cell edges
ax.pcolormesh(x_edges, y_edges, grid, shading="auto", cmap="viridis")
ax.set_aspect("equal")
FunctionInputNote
imshowOne arrayFast; set origin and extent
pcolormeshCoordinate arrays plus valuesIrregular grids, slower
contourX, Y, ZLines at chosen levels
contourfX, Y, ZFilled bands; combine with contour
hist2dTwo 1-D samplesDensity without making a grid first

Contour lines

x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X ** 2 + Y ** 2)) + 0.4 * np.exp(-((X - 1.5) ** 2 + Y ** 2))

cs = ax.contourf(X, Y, Z, levels=20, cmap="viridis")
ax.contour(X, Y, Z, levels=8, colors="white", linewidths=0.5, alpha=0.5)
ax.clabel(cs, inline=True, fontsize=7, fmt="%.2f")
fig.colorbar(cs, ax=ax, label="Density")
  • meshgrid turns two 1-D axes into the 2-D coordinate arrays contour expects.
  • A modest number of levels reads better than many; label them rather than relying on the colorbar alone.
  • Filled contours plus thin line contours give both magnitude and shape in one panel.
  • Contour lines interpolate between samples, so a coarse grid invents smoothness that is not in the data.

A first mplot3d surface

fig = plt.figure(figsize=(7, 5))
ax3 = fig.add_subplot(projection="3d")

ax3.plot_surface(X, Y, Z, cmap="viridis", linewidth=0, antialiased=True)
ax3.contour(X, Y, Z, zdir="z", offset=Z.min(), cmap="viridis", alpha=0.6)
ax3.set_xlabel("x")
ax3.set_zlabel("density")
ax3.view_init(elev=30, azim=-60)      # camera angles in degrees

# sometimes the 2-D view is simply better
fig, ax = plt.subplots()
ax.contourf(X, Y, Z, levels=20, cmap="viridis")
⚠️
A surface hides the values behind it, so two readers can disagree about which peak is higher. Use 3-D for shape and intuition, and a 2-D contour or small multiples when the reader has to compare numbers.

FAQ

Why is my image upside down?
imshow puts row 0 at the top by default, matching screen coordinates. Pass origin="lower" when row 0 should be at the bottom, as with spatial or time axes.
Why is a 3-D plot so slow?
Every artist is projected and depth-sorted on each draw. Reduce the grid resolution, drop antialiased, and avoid animating 3-D surfaces; 2-D contour views are far cheaper.

Colormaps, colour mapping and accessibility Animations and interactive figures

Last refreshed 2026-09-18.