Skip to content

Surface Plotting

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

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 = X**2 + Y**2

    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.

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.

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

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(X**2 + Y**2))

    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.

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(-(X**2 + Y**2) / 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

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(-X**2 - Y**2)

    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

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 * sigma**2)
    exp_term = np.exp(-(X**2 + Y**2) / (2 * sigma**2))
    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

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 - rho**2))
    quad = (X**2 - 2*rho*X*Y + Y**2) / (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

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

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(X**2 + Y**2))

    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

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 = X**2 - Y**2

    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

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(-(X**2 + Y**2) / 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

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

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(-(X**2 + Y**2) / 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

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 = X**2 + Y**2

    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

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(X**2 + Y**2)

    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

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(-(X**2 + Y**2))

    # 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

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(-(X**2 + Y**2) / 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()