How to Create a 3D Surface Plot in Plotly
3D surface plots represent three-dimensional data where two variables define positions on a plane and a third variable determines height. They're invaluable when you need to visualize mathematical...
Key Insights
- 3D surface plots excel at visualizing continuous functions of two variables and are essential for mathematical modeling, terrain visualization, and scientific data analysis where height represents a third dimension
- Plotly’s
go.Surface()requires Z-axis data as a 2D array with optional X and Y coordinates, making numpy’smeshgrid()your best friend for generating coordinate matrices from ranges - Interactive features like custom hover templates, adjustable camera angles, and dynamic colorscales make Plotly superior to static plotting libraries for exploratory 3D visualization
Introduction to 3D Surface Plots
3D surface plots represent three-dimensional data where two variables define positions on a plane and a third variable determines height. They’re invaluable when you need to visualize mathematical functions like z = f(x, y), topographical elevation data, heat distribution across a surface, or any scenario where you have a continuous relationship between three variables.
Unlike scatter plots that show discrete points, surface plots create a continuous mesh that interpolates between data points, making patterns and trends immediately visible. Plotly stands out for this task because it generates interactive WebGL-based visualizations that users can rotate, zoom, and explore—capabilities that static matplotlib plots simply can’t match.
Setting Up Your Environment
You’ll need three core libraries: Plotly for visualization, NumPy for numerical operations, and Pandas for data manipulation. Install them if you haven’t already:
pip install plotly numpy pandas
Here are the imports you’ll use throughout this tutorial:
import plotly.graph_objects as go
import numpy as np
import pandas as pd
from plotly.subplots import make_subplots
The graph_objects module gives you fine-grained control over plot components, which is exactly what you want for customized 3D visualizations.
Creating Your First Basic 3D Surface Plot
Let’s start with a classic mathematical surface: the “sombrero” function, z = sin(√(x² + y²)) / √(x² + y²). This creates a ripple pattern that demonstrates surface plotting fundamentals.
First, generate a mesh grid—a pair of 2D arrays representing all combinations of x and y coordinates:
# Create coordinate arrays
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
# Generate mesh grid
X, Y = np.meshgrid(x, y)
# Calculate Z values
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R) / (R + 1e-10) # Add small value to avoid division by zero
# Create the surface plot
fig = go.Figure(data=[go.Surface(z=Z, x=X, y=Y)])
fig.update_layout(
title='Sombrero Function',
scene=dict(
xaxis_title='X Axis',
yaxis_title='Y Axis',
zaxis_title='Z Axis'
),
width=800,
height=700
)
fig.show()
The meshgrid() function is crucial here. If x has 100 elements and y has 100 elements, meshgrid() creates two 100×100 arrays where X contains x-coordinates and Y contains y-coordinates for every point on the grid. Your Z array must match these dimensions.
Customizing Surface Appearance
Default surfaces work fine, but customization makes your visualizations communicate better. Plotly offers extensive control over colors, contours, and lighting.
# Generate data
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)) * np.cos(2 * X) * np.sin(2 * Y)
# Create surface with custom styling
fig = go.Figure(data=[go.Surface(
z=Z,
x=X,
y=Y,
colorscale='Viridis', # Choose from built-in scales or define custom
showscale=True,
colorbar=dict(title='Height', thickness=20, len=0.7),
contours=dict(
z=dict(
show=True,
usecolormap=True,
highlightcolor="limegreen",
project=dict(z=True)
)
),
opacity=0.9,
lighting=dict(
ambient=0.4,
diffuse=0.8,
specular=0.6,
roughness=0.5
)
)])
# Adjust camera angle
fig.update_layout(
scene=dict(
camera=dict(
eye=dict(x=1.5, y=1.5, z=1.3)
),
xaxis_title='X',
yaxis_title='Y',
zaxis_title='Z'
),
title='Custom Styled Surface'
)
fig.show()
The contours parameter adds projection lines onto the coordinate planes, helping viewers understand the surface’s position in 3D space. The lighting dictionary controls how the surface responds to virtual light sources—adjust these values to emphasize different surface features.
For colorscales, Plotly includes dozens of built-in options: ‘Viridis’, ‘Plasma’, ‘Jet’, ‘Hot’, ‘Cool’, ‘Rainbow’, and more. You can also define custom scales:
custom_colorscale = [
[0, 'rgb(0, 0, 100)'],
[0.5, 'rgb(255, 255, 255)'],
[1, 'rgb(100, 0, 0)']
]
fig.update_traces(colorscale=custom_colorscale)
Working with Real-World Data
Real datasets rarely come as perfect mesh grids. Here’s how to handle tabular data with x, y, z coordinates:
# Simulate real-world data (e.g., elevation measurements)
np.random.seed(42)
n_points = 50
# Create irregular data points
x_data = np.random.uniform(-10, 10, n_points)
y_data = np.random.uniform(-10, 10, n_points)
z_data = np.sin(np.sqrt(x_data**2 + y_data**2)) + np.random.normal(0, 0.1, n_points)
# Create DataFrame
df = pd.DataFrame({
'x': x_data,
'y': y_data,
'z': z_data
})
# For surface plots, you need gridded data
# Create interpolation grid
xi = np.linspace(df['x'].min(), df['x'].max(), 50)
yi = np.linspace(df['y'].min(), df['y'].max(), 50)
Xi, Yi = np.meshgrid(xi, yi)
# Use scipy for interpolation
from scipy.interpolate import griddata
Zi = griddata((df['x'], df['y']), df['z'], (Xi, Yi), method='cubic')
# Handle NaN values that might appear at edges
Zi = np.nan_to_num(Zi, nan=0.0)
fig = go.Figure(data=[go.Surface(z=Zi, x=Xi, y=Yi, colorscale='Earth')])
fig.update_layout(title='Interpolated Surface from Scattered Data')
fig.show()
The griddata() function from scipy interpolates scattered points onto a regular grid. The method parameter accepts ’linear’, ‘cubic’, or ’nearest’—cubic produces smoother surfaces but may introduce artifacts if your data is sparse.
Adding Interactivity and Annotations
Plotly’s interactivity is where it shines. Custom hover templates show exactly the information users need:
x = np.linspace(-2, 2, 40)
y = np.linspace(-2, 2, 40)
X, Y = np.meshgrid(x, y)
Z = X * np.exp(-X**2 - Y**2)
fig = go.Figure(data=[go.Surface(
z=Z,
x=X,
y=Y,
hovertemplate='X: %{x:.2f}<br>Y: %{y:.2f}<br>Z: %{z:.2f}<extra></extra>',
colorscale='RdBu'
)])
# Add annotations at specific points
fig.add_trace(go.Scatter3d(
x=[0], y=[0], z=[0],
mode='markers+text',
marker=dict(size=10, color='red'),
text=['Peak'],
textposition='top center',
showlegend=False
))
fig.update_layout(
scene=dict(
xaxis_title='X Coordinate',
yaxis_title='Y Coordinate',
zaxis_title='Function Value'
),
title='Interactive Surface with Annotations'
)
fig.show()
You can combine multiple surfaces in one plot to compare functions or show relationships:
Z1 = np.sin(np.sqrt(X**2 + Y**2))
Z2 = np.cos(np.sqrt(X**2 + Y**2))
fig = go.Figure(data=[
go.Surface(z=Z1, x=X, y=Y, colorscale='Blues', name='Sin', opacity=0.8),
go.Surface(z=Z2, x=X, y=Y, colorscale='Reds', name='Cos', opacity=0.8)
])
fig.update_layout(title='Multiple Surfaces')
fig.show()
Exporting and Embedding
Save your interactive plots as standalone HTML files:
fig.write_html("surface_plot.html")
For static images (requires kaleido):
pip install kaleido
fig.write_image("surface_plot.png", width=1200, height=800)
fig.write_image("surface_plot.pdf") # Vector format for publications
Embed in Jupyter notebooks by simply calling fig.show(), or in web applications by including the HTML file or using Plotly’s JavaScript library.
For dashboards, integrate with Dash:
from dash import Dash, dcc, html
app = Dash(__name__)
app.layout = html.Div([
dcc.Graph(figure=fig)
])
if __name__ == '__main__':
app.run_server(debug=True)
Final Thoughts
3D surface plots in Plotly give you powerful visualization capabilities with minimal code. Start with meshgrid() for mathematical functions, use griddata() for scattered real-world data, and leverage Plotly’s interactive features to let users explore your data from every angle. The key is understanding that surface plots need regularly gridded Z data—once you have that structure right, customization and interactivity follow naturally.