Basic 3D Surface Plot — Altair

A 3D surface plot visualizes a function of two variables as a continuous surface in three-dimensional space. The height (z-axis) represents the function value at each (x, y) point, with color encoding the same information to enhance depth perception. This visualization is ideal for understanding mathematical functions, terrain data, and response surfaces where the relationship between two inputs and one output needs to be explored.

Basic 3D Surface Plot rendered with Altair

Python source (Altair)

""" anyplot.ai
surface-basic: Basic 3D Surface Plot
Library: altair 6.1.0 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-05
"""

import os

import altair as alt
import numpy as np
import pandas as pd


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"

# Data - create a smooth surface z = sin(x) * cos(y)
np.random.seed(42)

# Grid setup - 50x50 for smoother surface
n_points = 50
x = np.linspace(-3, 3, n_points)
y = np.linspace(-3, 3, n_points)
X, Y = np.meshgrid(x, y)

# Surface function
Z = np.sin(X) * np.cos(Y)

# 3D to 2D projection (elevation=25, azimuth=45)
elev_rad = np.radians(25)
azim_rad = np.radians(45)

# Rotation around z-axis (azimuth)
X_rot = X * np.cos(azim_rad) - Y * np.sin(azim_rad)
Y_rot = X * np.sin(azim_rad) + Y * np.cos(azim_rad)

# Rotation around x-axis (elevation) and project
X_proj = X_rot
Z_proj = Y_rot * np.sin(elev_rad) + Z * np.cos(elev_rad)

# Build facets with overlapping boundaries to reduce edge artifacts
rect_data = []
overlap = 0.02
for i in range(n_points - 1):
    for j in range(n_points - 1):
        # Get projected quad corners
        corners_x = [X_proj[i, j], X_proj[i, j + 1], X_proj[i + 1, j], X_proj[i + 1, j + 1]]
        corners_z = [Z_proj[i, j], Z_proj[i, j + 1], Z_proj[i + 1, j], Z_proj[i + 1, j + 1]]

        # Add slight overlap to reduce visual gaps
        x1 = min(corners_x) - overlap
        x2 = max(corners_x) + overlap
        y1 = min(corners_z) - overlap
        y2 = max(corners_z) + overlap

        # Average z for color (original Z, not projected)
        avg_z = (Z[i, j] + Z[i, j + 1] + Z[i + 1, j + 1] + Z[i + 1, j]) / 4

        # Depth for sorting (painter's algorithm - back to front)
        depth = (Y_rot[i, j] + Y_rot[i, j + 1] + Y_rot[i + 1, j + 1] + Y_rot[i + 1, j]) / 4

        rect_data.append({"x1": x1, "x2": x2, "y1": y1, "y2": y2, "z": avg_z, "depth": depth})

# Sort by depth (back to front)
rect_data.sort(key=lambda r: r["depth"], reverse=True)

# Assign order for rendering
for idx, r in enumerate(rect_data):
    r["order"] = idx

df_rects = pd.DataFrame(rect_data)

# Create surface chart with filled rectangles
surface = (
    alt.Chart(df_rects)
    .mark_rect(strokeWidth=0.3, stroke=INK_SOFT, strokeOpacity=0.15)
    .encode(
        x=alt.X(
            "x1:Q",
            axis=alt.Axis(
                title="X axis (projected)",
                labelFontSize=18,
                titleFontSize=22,
                labelColor=INK_SOFT,
                titleColor=INK,
                domainColor=INK_SOFT,
                tickColor=INK_SOFT,
                gridOpacity=0.10,
                gridColor=INK,
            ),
        ),
        y=alt.Y(
            "y1:Q",
            axis=alt.Axis(
                title="Z axis (height)",
                labelFontSize=18,
                titleFontSize=22,
                labelColor=INK_SOFT,
                titleColor=INK,
                domainColor=INK_SOFT,
                tickColor=INK_SOFT,
                gridOpacity=0.10,
                gridColor=INK,
            ),
        ),
        x2=alt.X2("x2:Q"),
        y2=alt.Y2("y2:Q"),
        color=alt.Color(
            "z:Q",
            scale=alt.Scale(scheme="viridis"),
            legend=alt.Legend(
                title="Surface Value",
                titleFontSize=20,
                labelFontSize=16,
                fillColor=ELEVATED_BG,
                strokeColor=INK_SOFT,
                labelColor=INK_SOFT,
                titleColor=INK,
            ),
        ),
        order=alt.Order("order:Q"),
        tooltip=[
            alt.Tooltip("z:Q", title="Surface value", format=".3f"),
            alt.Tooltip("x1:Q", title="X coordinate", format=".2f"),
            alt.Tooltip("y1:Q", title="Z coordinate", format=".2f"),
        ],
    )
)

# Add pan and zoom interactivity
pan_zoom = alt.selection_interval(bind="scales", encodings=["x", "y"])

# Combine into final chart with interactivity
chart = (
    surface.add_params(pan_zoom)
    .properties(
        width=1600,
        height=900,
        title=alt.Title("surface-basic · altair · anyplot.ai", fontSize=28, color=INK),
        background=PAGE_BG,
    )
    .configure_view(strokeWidth=0, fill=PAGE_BG)
    .configure_axis(grid=True, gridOpacity=0.10)
    .configure_title(color=INK)
)

# Save outputs
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
chart.save(f"plot-{THEME}.html")

Part of Basic 3D Surface Plot on anyplot.ai.

Other implementations