Skip to content

Contour and Surface Plots

This document covers techniques for combining 2D contour plots with 3D surface visualizations to provide complementary views of the same data.

Setup

import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D

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

Side-by-Side Visualization

1. Basic Comparison

fig = plt.figure(figsize=(14, 5))

# 2D Contour
ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z, levels=20, cmap='viridis')
ax1.contour(X, Y, Z, levels=10, colors='black', linewidths=0.5)
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_title('2D Contour Plot')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1)

# 3D Surface
ax2 = fig.add_subplot(1, 2, 2, projection='3d')
ax2.plot_surface(X, Y, Z, cmap='viridis', linewidth=0)
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_zlabel('z')
ax2.set_title('3D Surface Plot')

plt.tight_layout()
plt.show()

2. Multiple View Angles

fig = plt.figure(figsize=(15, 10))

# Contour plot
ax1 = fig.add_subplot(2, 2, 1)
cf = ax1.contourf(X, Y, Z, levels=15, cmap='viridis')
cs = ax1.contour(X, Y, Z, levels=8, colors='white', linewidths=0.5)
ax1.clabel(cs, inline=True, fontsize=8)
ax1.set_title('Contour (Top View)')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1)

# 3D views
views = [(30, -60), (60, -60), (30, 45)]
titles = ['Standard View', 'High Angle', 'Rotated View']

for idx, ((elev, azim), title) in enumerate(zip(views, titles)):
    ax = fig.add_subplot(2, 2, idx + 2, projection='3d')
    ax.plot_surface(X, Y, Z, cmap='viridis', linewidth=0)
    ax.view_init(elev=elev, azim=azim)
    ax.set_title(f'3D Surface: {title}')
    ax.set_xlabel('x')
    ax.set_ylabel('y')

plt.tight_layout()
plt.show()

Contour Projection on 3D Plot

Project contours onto the base plane of a 3D surface plot.

1. Basic Projection

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

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

# Contour projection on z=0 plane
ax.contour(X, Y, Z, zdir='z', offset=0, cmap='viridis', levels=10)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_title('Surface with Contour Projection')
plt.show()

2. Filled Contour Projection

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

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

# Filled contour projection
ax.contourf(X, Y, Z, zdir='z', offset=-0.2, cmap='viridis', levels=15, alpha=0.8)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_zlim(-0.2, 1.1)
ax.set_title('Surface with Filled Contour Projection')
plt.show()

3. Multiple Projections

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

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

# XY plane projection (bottom)
ax.contourf(X, Y, Z, zdir='z', offset=-0.1, cmap='viridis', levels=10, alpha=0.7)

# XZ plane projection (back)
ax.contourf(X, Y, Z, zdir='y', offset=3.5, cmap='viridis', levels=10, alpha=0.5)

# YZ plane projection (side)
ax.contourf(X, Y, Z, zdir='x', offset=-3.5, cmap='viridis', levels=10, alpha=0.5)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_xlim(-3.5, 3)
ax.set_ylim(-3, 3.5)
ax.set_zlim(-0.1, 1.1)
ax.set_title('Surface with Multiple Contour Projections')
plt.show()

Wireframe with Contours

1. Wireframe Surface

fig = plt.figure(figsize=(14, 5))

# Filled contour
ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z, levels=20, cmap='coolwarm')
ax1.set_title('Filled Contour')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1)

# Wireframe
ax2 = fig.add_subplot(1, 2, 2, projection='3d')
ax2.plot_wireframe(X, Y, Z, rstride=5, cstride=5, color='blue', linewidth=0.5)
ax2.set_title('Wireframe')

plt.tight_layout()
plt.show()

2. Wireframe with Contour Projection

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

# Wireframe
ax.plot_wireframe(X, Y, Z, rstride=5, cstride=5, color='navy', linewidth=0.5, alpha=0.7)

# Contour projection
ax.contour(X, Y, Z, zdir='z', offset=0, cmap='Blues', levels=10)

ax.set_zlim(0, 1.1)
ax.set_title('Wireframe with Contour Projection')
plt.show()

1. Gaussian Function

Z_gauss = np.exp(-(X**2 + Y**2))

fig = plt.figure(figsize=(14, 5))

ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z_gauss, levels=20, cmap='viridis')
ax1.contour(X, Y, Z_gauss, levels=10, colors='white', linewidths=0.5)
ax1.set_title('Gaussian: $e^{-(x^2+y^2)}$')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1)

ax2 = fig.add_subplot(1, 2, 2, projection='3d')
ax2.plot_surface(X, Y, Z_gauss, cmap='viridis')
ax2.set_title('Gaussian Surface')

plt.tight_layout()
plt.show()

2. Saddle Function

Z_saddle = X**2 - Y**2

fig = plt.figure(figsize=(14, 5))

ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z_saddle, levels=20, cmap='coolwarm')
ax1.contour(X, Y, Z_saddle, levels=[0], colors='black', linewidths=2)  # Zero contour
ax1.set_title('Saddle: $x^2 - y^2$')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1)

ax2 = fig.add_subplot(1, 2, 2, projection='3d')
ax2.plot_surface(X, Y, Z_saddle, cmap='coolwarm')
ax2.set_title('Saddle Surface')

plt.tight_layout()
plt.show()

3. Sinusoidal Function

Z_sin = np.sin(X) * np.cos(Y)

fig = plt.figure(figsize=(14, 5))

ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z_sin, levels=20, cmap='RdBu')
ax1.contour(X, Y, Z_sin, levels=[0], colors='black', linewidths=1)
ax1.set_title('$\\sin(x)\\cos(y)$')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1)

ax2 = fig.add_subplot(1, 2, 2, projection='3d')
ax2.plot_surface(X, Y, Z_sin, cmap='RdBu')
ax2.set_title('Sinusoidal Surface')

plt.tight_layout()
plt.show()
functions = [
    (np.exp(-(X**2 + Y**2)), 'Gaussian', 'viridis'),
    (X**2 - Y**2, 'Saddle', 'coolwarm'),
    (np.sin(X) * np.cos(Y), 'sin(x)cos(y)', 'RdBu'),
    (np.sin(np.sqrt(X**2 + Y**2)), 'Ripple', 'plasma')
]

fig = plt.figure(figsize=(16, 12))

for idx, (Z_func, title, cmap) in enumerate(functions):
    # Contour
    ax1 = fig.add_subplot(4, 2, 2*idx + 1)
    cf = ax1.contourf(X, Y, Z_func, levels=15, cmap=cmap)
    ax1.contour(X, Y, Z_func, levels=8, colors='black', linewidths=0.3)
    ax1.set_title(f'{title} (Contour)')
    ax1.set_aspect('equal')
    plt.colorbar(cf, ax=ax1)

    # Surface
    ax2 = fig.add_subplot(4, 2, 2*idx + 2, projection='3d')
    ax2.plot_surface(X, Y, Z_func, cmap=cmap, linewidth=0)
    ax2.set_title(f'{title} (Surface)')

plt.tight_layout()
plt.show()

Bivariate Normal Distribution

1. Standard Normal

from scipy import stats

x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
pos = np.dstack((X, Y))

rv = stats.multivariate_normal([0, 0], [[1, 0], [0, 1]])
Z = rv.pdf(pos)

fig = plt.figure(figsize=(14, 5))

ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z, levels=20, cmap='Blues')
ax1.contour(X, Y, Z, levels=10, colors='navy', linewidths=0.5)
ax1.set_title('Standard Bivariate Normal')
ax1.set_aspect('equal')
plt.colorbar(cf, ax=ax1, label='Density')

ax2 = fig.add_subplot(1, 2, 2, projection='3d')
ax2.plot_surface(X, Y, Z, cmap='Blues')
ax2.set_title('PDF Surface')
ax2.set_zlabel('Density')

plt.tight_layout()
plt.show()

2. Correlation Comparison

rhos = [-0.7, 0, 0.7]

fig = plt.figure(figsize=(15, 8))

for idx, rho in enumerate(rhos):
    rv = stats.multivariate_normal([0, 0], [[1, rho], [rho, 1]])
    Z = rv.pdf(pos)

    # Contour
    ax1 = fig.add_subplot(2, 3, idx + 1)
    cf = ax1.contourf(X, Y, Z, levels=15, cmap='Blues')
    ax1.set_title(f'ρ = {rho}')
    ax1.set_aspect('equal')
    plt.colorbar(cf, ax=ax1)

    # Surface
    ax2 = fig.add_subplot(2, 3, idx + 4, projection='3d')
    ax2.plot_surface(X, Y, Z, cmap='Blues')
    ax2.set_title(f'Surface (ρ = {rho})')
    ax2.view_init(30, -60)

plt.tight_layout()
plt.show()

Interactive-Style Dashboard

Z = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)

fig = plt.figure(figsize=(16, 10))

# Main 3D surface (large)
ax_3d = fig.add_subplot(2, 2, 1, projection='3d')
surf = ax_3d.plot_surface(X, Y, Z, cmap='viridis', alpha=0.9)
ax_3d.contourf(X, Y, Z, zdir='z', offset=0, cmap='viridis', levels=10, alpha=0.5)
ax_3d.set_xlabel('x')
ax_3d.set_ylabel('y')
ax_3d.set_zlabel('f(x,y)')
ax_3d.set_title('3D Surface with Contour Projection', fontsize=12)
ax_3d.view_init(30, -60)

# Top-down contour view
ax_top = fig.add_subplot(2, 2, 2)
cf = ax_top.contourf(X, Y, Z, levels=20, cmap='viridis')
cs = ax_top.contour(X, Y, Z, levels=10, colors='white', linewidths=0.5)
ax_top.clabel(cs, inline=True, fontsize=7, fmt='%.3f')
ax_top.set_xlabel('x')
ax_top.set_ylabel('y')
ax_top.set_title('Contour Plot (Top View)', fontsize=12)
ax_top.set_aspect('equal')
plt.colorbar(cf, ax=ax_top)

# X cross-section
ax_xsec = fig.add_subplot(2, 2, 3)
mid_idx = len(y) // 2
ax_xsec.plot(x, Z[mid_idx, :], 'b-', linewidth=2)
ax_xsec.fill_between(x, Z[mid_idx, :], alpha=0.3)
ax_xsec.set_xlabel('x')
ax_xsec.set_ylabel('f(x, 0)')
ax_xsec.set_title('Cross-Section at y = 0', fontsize=12)
ax_xsec.grid(alpha=0.3)

# Y cross-section
ax_ysec = fig.add_subplot(2, 2, 4)
ax_ysec.plot(y, Z[:, mid_idx], 'r-', linewidth=2)
ax_ysec.fill_between(y, Z[:, mid_idx], alpha=0.3, color='red')
ax_ysec.set_xlabel('y')
ax_ysec.set_ylabel('f(0, y)')
ax_ysec.set_title('Cross-Section at x = 0', fontsize=12)
ax_ysec.grid(alpha=0.3)

plt.suptitle('Standard Bivariate Normal Distribution Analysis', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Publication-Quality Figure

from scipy import stats

# Setup
x = np.linspace(-3, 3, 150)
y = np.linspace(-3, 3, 150)
X, Y = np.meshgrid(x, y)
pos = np.dstack((X, Y))

rho = 0.6
rv = stats.multivariate_normal([0, 0], [[1, rho], [rho, 1]])
Z = rv.pdf(pos)

fig = plt.figure(figsize=(14, 5))

# Contour plot
ax1 = fig.add_subplot(1, 2, 1)
cf = ax1.contourf(X, Y, Z, levels=15, cmap='Blues')
cs = ax1.contour(X, Y, Z, levels=8, colors='navy', linewidths=0.6, alpha=0.8)
ax1.clabel(cs, inline=True, fontsize=8, fmt='%.3f')
ax1.set_xlabel('$X$', fontsize=12)
ax1.set_ylabel('$Y$', fontsize=12)
ax1.set_title('Contour Plot', fontsize=13)
ax1.set_aspect('equal')
ax1.tick_params(labelsize=10)
cbar1 = plt.colorbar(cf, ax=ax1)
cbar1.set_label('Probability Density', fontsize=11)

# Surface plot
ax2 = fig.add_subplot(1, 2, 2, projection='3d')
surf = ax2.plot_surface(X, Y, Z, cmap='Blues', linewidth=0, antialiased=True)
ax2.contourf(X, Y, Z, zdir='z', offset=0, cmap='Blues', levels=10, alpha=0.5)
ax2.set_xlabel('$X$', fontsize=11, labelpad=10)
ax2.set_ylabel('$Y$', fontsize=11, labelpad=10)
ax2.set_zlabel('Density', fontsize=11, labelpad=10)
ax2.set_title('Surface Plot', fontsize=13)
ax2.view_init(25, -50)
ax2.tick_params(labelsize=9)

# White panes
ax2.w_xaxis.set_pane_color((0.98, 0.98, 0.98, 1.0))
ax2.w_yaxis.set_pane_color((0.98, 0.98, 0.98, 1.0))
ax2.w_zaxis.set_pane_color((0.98, 0.98, 0.98, 1.0))

plt.suptitle(f'Bivariate Normal Distribution ($\\rho = {rho}$)', 
             fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

Summary

Visualization Best For
Contour only 2D view, precise level reading
Surface only Overall shape understanding
Contour + Surface side-by-side Comprehensive analysis
Surface + Contour projection 3D context with 2D precision
Wireframe + Contour Structure visualization
Dashboard (multiple views) Complete exploration