Skip to content

Surface Plotting

Mental Model

A surface plot visualizes \(z = f(x, y)\) by evaluating the function on a meshgrid and passing the three same-shaped arrays (X, Y, Z) to Matplotlib's plot_surface. The meshgrid provides the coordinates; the function evaluation provides the heights. Contour plots are the 2D projection of the same data.

The computational core here is NumPy, not Matplotlib: the meshgrid construction and function evaluation (the expensive, vectorized part) happen entirely in NumPy. Matplotlib only consumes the resulting arrays for rendering. Getting the grid computation right — matching shapes, choosing dense vs sparse, evaluating efficiently — matters more than plot styling.

3D Surface Basics

Mesh grids are essential for creating 3D surface plots with Matplotlib. The plot_surface method requires X, Y, and Z arrays of the same shape.

1. Simple Surface

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-2, 2, 50) y = np.linspace(-2, 2, 50) X, Y = np.meshgrid(x, y) Z = X2 + Y2

fig, ax = plt.subplots(subplot_kw={'projection': '3d'})
ax.plot_surface(X, Y, Z)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_title('Paraboloid')
plt.show()

if name == "main": main() ```

2. Figure Sizing

Use the golden ratio for aesthetically pleasing proportions.

```python import numpy as np import matplotlib.pyplot as plt

def main(): phi = 1.61803398875 # golden ratio

x = np.linspace(-2, 2, 50)
y = np.linspace(-2, 2, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig, ax = plt.subplots(
    figsize=(5 * phi, 5),
    subplot_kw={'projection': '3d'}
)
ax.plot_surface(X, Y, Z)
ax.set_title('Ripple')
plt.show()

if name == "main": main() ```

3. Subplot Projection

The projection='3d' must be specified in subplot_kw.

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-3, 3, 60) y = np.linspace(-3, 3, 60) X, Y = np.meshgrid(x, y)

fig, axes = plt.subplots(
    1, 2,
    figsize=(12, 5),
    subplot_kw={'projection': '3d'}
)

# First surface
Z1 = X**2 - Y**2
axes[0].plot_surface(X, Y, Z1)
axes[0].set_title('Saddle')

# Second surface
Z2 = np.sin(X) * np.cos(Y)
axes[1].plot_surface(X, Y, Z2)
axes[1].set_title('Wave')

plt.tight_layout()
plt.show()

if name == "main": main() ```

Surface Appearance

1. Colormaps

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-3, 3, 80) y = np.linspace(-3, 3, 80) X, Y = np.meshgrid(x, y) Z = np.sin(np.sqrt(X2 + Y2))

fig, ax = plt.subplots(
    figsize=(8, 6),
    subplot_kw={'projection': '3d'}
)

surf = ax.plot_surface(
    X, Y, Z,
    cmap=plt.cm.coolwarm,
    linewidth=0,
    antialiased=True
)

fig.colorbar(surf, shrink=0.5, aspect=10)
ax.set_title('Colormap: coolwarm')
plt.show()

if name == "main": main() ```

2. Stride and Wireframe

Control mesh density with rstride and cstride.

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-3, 3, 100) y = np.linspace(-3, 3, 100) X, Y = np.meshgrid(x, y) Z = np.exp(-(X2 + Y2) / 4)

phi = 1.61803398875
fig, ax = plt.subplots(
    figsize=(5 * phi, 5),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(
    X, Y, Z,
    rstride=2,        # row stride
    cstride=2,        # column stride
    cmap=plt.cm.viridis,
    linewidth=0.5,
    antialiased=True
)

ax.set_title('Surface with Stride')
plt.show()

if name == "main": main() ```

3. Edge Colors

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-2, 2, 30) y = np.linspace(-2, 2, 30) X, Y = np.meshgrid(x, y) Z = X * np.exp(-X2 - Y2)

fig, ax = plt.subplots(
    figsize=(8, 6),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(
    X, Y, Z,
    cmap='plasma',
    edgecolor='black',
    linewidth=0.3,
    alpha=0.8
)

ax.set_title('Surface with Edges')
plt.show()

if name == "main": main() ```

PDF Surfaces

1. Standard Normal

```python import numpy as np import matplotlib.pyplot as plt

def bivariate_normal(X, Y, mu_x=0, mu_y=0, sigma=1): """Standard bivariate normal PDF.""" coef = 1 / (2 * np.pi * sigma2) exp_term = np.exp(-(X2 + Y2) / (2 * sigma2)) return coef * exp_term

def main(): x = np.linspace(-4, 4, 100) y = np.linspace(-4, 4, 100) X, Y = np.meshgrid(x, y) Z = bivariate_normal(X, Y)

phi = 1.61803398875
fig, ax = plt.subplots(
    figsize=(5 * phi, 5),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(
    X, Y, Z,
    rstride=2,
    cstride=2,
    cmap=plt.cm.coolwarm,
    linewidth=0.5,
    antialiased=True
)

ax.set_title('Standard Normal PDF', fontsize=15)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('f(x, y)')
ax.set_zticks([0.05, 0.10, 0.15])

plt.show()

if name == "main": main() ```

2. Correlated Normal

```python import numpy as np import matplotlib.pyplot as plt

def correlated_normal(X, Y, rho=0.5): """Bivariate normal with correlation.""" coef = 1 / (2 * np.pi * np.sqrt(1 - rho2)) quad = (X2 - 2rhoXY + Y2) / (2 * (1 - rho*2)) return coef * np.exp(-quad)

def main(): x = np.linspace(-3, 3, 80) y = np.linspace(-3, 3, 80) X, Y = np.meshgrid(x, y)

fig, axes = plt.subplots(
    1, 3,
    figsize=(15, 5),
    subplot_kw={'projection': '3d'}
)

correlations = [-0.7, 0, 0.7]

for ax, rho in zip(axes, correlations):
    Z = correlated_normal(X, Y, rho)
    ax.plot_surface(X, Y, Z, cmap='viridis', linewidth=0)
    ax.set_title(f'ρ = {rho}')
    ax.set_xlabel('x')
    ax.set_ylabel('y')

plt.tight_layout()
plt.show()

if name == "main": main() ```

3. Mixture of Gaussians

```python import numpy as np import matplotlib.pyplot as plt

def gaussian_2d(X, Y, mu_x, mu_y, sigma): """Single 2D Gaussian.""" return np.exp(-((X - mu_x)2 + (Y - mu_y)2) / (2 * sigma**2))

def main(): x = np.linspace(-5, 5, 100) y = np.linspace(-5, 5, 100) X, Y = np.meshgrid(x, y)

# Mixture of three Gaussians
Z = (0.5 * gaussian_2d(X, Y, -2, 0, 1) +
     0.3 * gaussian_2d(X, Y, 2, 1, 0.8) +
     0.2 * gaussian_2d(X, Y, 0, -2, 1.2))

phi = 1.61803398875
fig, ax = plt.subplots(
    figsize=(5 * phi, 5),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(
    X, Y, Z,
    cmap='magma',
    linewidth=0,
    antialiased=True
)

ax.set_title('Gaussian Mixture')
plt.show()

if name == "main": main() ```

Contour Integration

1. Surface with Contours

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-3, 3, 80) y = np.linspace(-3, 3, 80) X, Y = np.meshgrid(x, y) Z = np.sin(np.sqrt(X2 + Y2))

fig, ax = plt.subplots(
    figsize=(10, 8),
    subplot_kw={'projection': '3d'}
)

# Surface
ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.8)

# Contours on z = -1.5 plane
ax.contour(X, Y, Z, zdir='z', offset=-1.5, cmap='viridis')

ax.set_zlim(-1.5, 1)
ax.set_title('Surface with Floor Contours')
plt.show()

if name == "main": main() ```

2. Side Projections

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-2, 2, 60) y = np.linspace(-2, 2, 60) X, Y = np.meshgrid(x, y) Z = X2 - Y2

fig, ax = plt.subplots(
    figsize=(10, 8),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.7)

# Project onto walls
ax.contour(X, Y, Z, zdir='x', offset=-2.5, cmap='coolwarm')
ax.contour(X, Y, Z, zdir='y', offset=2.5, cmap='coolwarm')

ax.set_xlim(-2.5, 2)
ax.set_ylim(-2, 2.5)
ax.set_title('Saddle with Projections')
plt.show()

if name == "main": main() ```

3. 2D Contour Alone

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-3, 3, 100) y = np.linspace(-3, 3, 100) X, Y = np.meshgrid(x, y) Z = np.exp(-(X2 + Y2) / 2) / (2 * np.pi)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Contour lines
cs1 = axes[0].contour(X, Y, Z, levels=10)
axes[0].clabel(cs1, inline=True, fontsize=8)
axes[0].set_title('Contour Lines')
axes[0].set_aspect('equal')

# Filled contours
cs2 = axes[1].contourf(X, Y, Z, levels=20, cmap='Blues')
fig.colorbar(cs2, ax=axes[1])
axes[1].set_title('Filled Contours')
axes[1].set_aspect('equal')

plt.tight_layout()
plt.show()

if name == "main": main() ```

View and Animation

1. View Angles

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-2, 2, 50) y = np.linspace(-2, 2, 50) X, Y = np.meshgrid(x, y) Z = np.sin(X) * np.cos(Y)

fig, axes = plt.subplots(
    1, 3,
    figsize=(15, 5),
    subplot_kw={'projection': '3d'}
)

views = [(30, 45), (60, 30), (90, 0)]
titles = ['Default', 'High Angle', 'Top View']

for ax, (elev, azim), title in zip(axes, views, titles):
    ax.plot_surface(X, Y, Z, cmap='viridis')
    ax.view_init(elev=elev, azim=azim)
    ax.set_title(f'{title}\nelev={elev}, azim={azim}')

plt.tight_layout()
plt.show()

if name == "main": main() ```

2. Interactive Rotation

```python import numpy as np import matplotlib.pyplot as plt

def main(): """ Interactive plots allow rotation with mouse drag. Use plt.show() without saving for interaction. """ x = np.linspace(-3, 3, 80) y = np.linspace(-3, 3, 80) X, Y = np.meshgrid(x, y) Z = np.exp(-(X2 + Y2) / 4)

fig, ax = plt.subplots(
    figsize=(8, 6),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(X, Y, Z, cmap='plasma')
ax.set_title('Drag to Rotate')

plt.show()

if name == "main": main() ```

3. Saving Views

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-2, 2, 60) y = np.linspace(-2, 2, 60) X, Y = np.meshgrid(x, y) Z = X2 + Y2

fig, ax = plt.subplots(
    figsize=(8, 6),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(X, Y, Z, cmap='viridis')
ax.view_init(elev=25, azim=45)
ax.set_title('Paraboloid')

# Save with specific resolution
plt.savefig('surface.png', dpi=150, bbox_inches='tight')
plt.close()

print("Saved: surface.png")

if name == "main": main() ```

Alternative Plots

1. Wireframe

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-2, 2, 30) y = np.linspace(-2, 2, 30) X, Y = np.meshgrid(x, y) Z = np.sin(X2 + Y2)

fig, axes = plt.subplots(
    1, 2,
    figsize=(12, 5),
    subplot_kw={'projection': '3d'}
)

axes[0].plot_surface(X, Y, Z, cmap='viridis', alpha=0.8)
axes[0].set_title('Surface')

axes[1].plot_wireframe(X, Y, Z, color='steelblue', linewidth=0.5)
axes[1].set_title('Wireframe')

plt.tight_layout()
plt.show()

if name == "main": main() ```

2. Scatter on Surface

```python import numpy as np import matplotlib.pyplot as plt

def main(): # Surface x = np.linspace(-2, 2, 40) y = np.linspace(-2, 2, 40) X, Y = np.meshgrid(x, y) Z = np.exp(-(X2 + Y2))

# Sample points
np.random.seed(42)
xs = np.random.uniform(-2, 2, 50)
ys = np.random.uniform(-2, 2, 50)
zs = np.exp(-(xs**2 + ys**2))

fig, ax = plt.subplots(
    figsize=(8, 6),
    subplot_kw={'projection': '3d'}
)

ax.plot_surface(X, Y, Z, cmap='Blues', alpha=0.6)
ax.scatter(xs, ys, zs, c='red', s=30)
ax.set_title('Surface with Samples')

plt.show()

if name == "main": main() ```

3. Heatmap Alternative

```python import numpy as np import matplotlib.pyplot as plt

def main(): x = np.linspace(-3, 3, 100) y = np.linspace(-3, 3, 100) X, Y = np.meshgrid(x, y) Z = np.exp(-(X2 + Y2) / 2)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Heatmap (imshow)
im = axes[0].imshow(
    Z,
    extent=[-3, 3, -3, 3],
    origin='lower',
    cmap='hot',
    aspect='equal'
)
fig.colorbar(im, ax=axes[0])
axes[0].set_title('Heatmap (imshow)')

# Pseudocolor (pcolormesh)
pc = axes[1].pcolormesh(X, Y, Z, cmap='hot', shading='auto')
fig.colorbar(pc, ax=axes[1])
axes[1].set_title('Pseudocolor (pcolormesh)')
axes[1].set_aspect('equal')

plt.tight_layout()
plt.show()

if name == "main": main() ```

Core Pipeline Summary

Every surface plot follows three steps:

text 1. meshgrid → X, Y = np.meshgrid(x, y) 2. compute → Z = f(X, Y) 3. visualize → ax.plot_surface(X, Y, Z)

All variations (colormaps, contours, wireframes, projections) are styling on top of this same pipeline. The meshgrid provides coordinates; the function evaluation provides heights; the plotting call renders the result.


Exercises

Exercise 1. Create a meshgrid for x and y both ranging from -5 to 5 with 50 points. Compute Z = np.sin(np.sqrt(X**2 + Y**2)) and print the shape of Z. Describe what this surface looks like geometrically.

Solution to Exercise 1
import numpy as np

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))
print(f"Z shape: {Z.shape}")  # (50, 50)
# This is a circular ripple pattern centered at the origin.

Exercise 2. Using meshgrid, evaluate and print the minimum and maximum of the function f(x, y) = x * exp(-(x^2 + y^2)) on the grid [-3, 3] x [-3, 3] with 200 points per axis.

Solution to Exercise 2
import numpy as np

x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = X * np.exp(-(X**2 + Y**2))
print(f"Min: {Z.min():.6f}")
print(f"Max: {Z.max():.6f}")

Exercise 3. Create meshgrid coordinates for a parametric surface: a hemisphere defined by Z = sqrt(max(0, 1 - X^2 - Y^2)) on the grid [-1, 1] x [-1, 1]. Use np.where or np.clip to handle the square root of negative values. Print the shape of Z and its maximum value.

Solution to Exercise 3
import numpy as np

x = np.linspace(-1, 1, 100)
y = np.linspace(-1, 1, 100)
X, Y = np.meshgrid(x, y)
R2 = X**2 + Y**2
Z = np.sqrt(np.clip(1 - R2, 0, None))
print(f"Z shape: {Z.shape}")  # (100, 100)
print(f"Z max: {Z.max():.4f}")  # 1.0