🎨 Transform Figma mockups into production-ready Dash apps. Join us November 10. Save your spot.

Chris Parmer

Chris Parmer

October 27, 2026

3D Surface Plots - A Complete Guide

TL;DR

  • A 3D surface plot draws a value measured over a grid of two variables (z over x and y) as a sheet, so the shape of the whole grid is visible at once: ridges, valleys, slopes and cliffs.
  • 3D surface plots are also known as surface plots, surface charts and 3-D surface charts, and are similar to contour plots and heatmaps.
  • Use one when the shape is the point: terrain, volatility surfaces, yield curves over time, dose-response matrices, power output by hour and day.
  • 3D hides whatever sits behind a peak and makes exact values hard to read, so choose the camera on purpose, add contour lines, and pair the surface with a heatmap or line chart when readers need numbers. Interactive HTML, where readers can rotate it, is where a surface works best.
  • Any coding agent can make one with Plotly in a minute. In Python it's go.Figure(go.Surface(z=grid)).

What is a 3D surface plot?

3D surface plots look like this:

3D surface of average April net demand on the California grid by hour of day, one row per year from 2019 to 2026. Midday net demand falls from 10.7 GW in 2019 to -1.6 GW in 2026, shown in blue, and the evening ramp rises to about 21 GW by 8 p.m.

Data: California ISO, Today's Outlook history.

How do you read a 3D surface plot? In the example above, notice these things:

  • The two flat axes are the grid the data lives on, with the hour of day running left to right and the year running from the oldest at the back to the newest at the front. Each thin black line across the surface is one year's average April day on the California grid.
  • The height of the surface is net demand, the power other plants still had to supply after wind and solar. Color repeats the height, so the orange ridges are the night and evening hours and the pale middle of the day is where wind and solar cover more than half of the load.
  • Reading from back to front shows the change over time. The midday valley gets deeper every year, which is the "duck curve" growing as wind and solar cover a larger share of the midday demand each year.
  • The blue patch at the front of the valley is where net demand went below zero, meaning wind and solar produced more than all of the demand. It first shows up at midday in 2025.
  • Watch out for perspective, because a 3D view makes rows near the front look bigger and can hide low points behind high ones. Use the colorbar, or hover over the surface in an interactive version, for exact values such as the 2026 evening ramp that the subtitle puts at 22.9 GW.

It's often used for charts like these:

3D implied volatility surface of S&P 500 options at the close of 23 October 2026, by strike as a percent of the index and days to expiry. The surface rises steeply toward low strikes and short expiries: one-month puts 20% below the index are at 41% volatility against 15% at the money. Dots mark the 3,343 option quotes

An implied volatility surface. One-month S&P 500 puts 20% below the index traded at 41% implied volatility on 23 October 2026, 2.7 times the 15% at the money. Data: Cboe delayed quotes for SPX options.

3D terrain surface of Mount St. Helens from USGS elevation data, with the horseshoe crater opening to the north toward the viewer, contour lines every 100 m and terrain colors from green lowlands to a pale summit at 2,525 m on this grid

Terrain. Mount St. Helens is 401 m lower than before its 1980 eruption (USGS), and its horseshoe crater opens to the north. Data: USGS 3D Elevation Program.

3D dose-response surface of bortezomib and clofarabine across the NCI-60 cancer cell lines, with growth falling from 100% untreated to -54% at the highest doses of both, below a translucent gray sheet showing the -26% expected if the drugs acted independently

A dose-response surface. At the highest doses, bortezomib plus clofarabine brought average cancer cell growth to -54% (cells killed), about twice the -26% expected if the two drugs acted independently. Data: NCI-ALMANAC (Holbeck et al., 2017).

A short history

In 1879 the Italian statistician Luigi Perozzo drew Sweden's census population for 1750 to 1875 as a 3D surface of age by year, one of the best-known early stereograms of statistical data (Friendly and Denis, Milestones). Here is his layout redrawn with Plotly on Statistics Sweden's single-age series, which starts in 1860:

Perozzo's 1879 stereogram layout redrawn with Plotly: a 3D surface of Sweden's population per year of age in 5-year age bands, 1860 to 2024, with lines every 10 years, rising from 3.86 million people in 1860 to 10.59 million in 2024

Depth has a mixed record since then. When Michael Siegrist tested pie and bar charts with and without it, readers judged 3D pie charts less accurately and took longer on 3D bar charts (Siegrist, 1996), so use 3D for grids that really have two dimensions, never to decorate a flat chart.

Alternatives to a 3D surface plot

Here's the same duck curve drawn four ways. Some are better for the question, some worse, some just different:

The duck curve drawn four ways: a 3D surface, a heatmap of hour by year, a contour plot with labeled levels every 4 GW, and a line chart with one line per year from 2019 to 2026
  • Heatmap (better for reading values). Every cell is visible and nothing hides behind a peak, though color is less precise than height or position. See our heatmap guide.
  • Contour plot (works too). Lines of equal value show where a level is crossed, the way topographic maps show terrain, and you can label them. See our contour plot guide.
  • Line chart (better for a few rows). One line per year gives exact values and makes the 2025-2026 drop below zero obvious. Past about ten lines it gets crowded.
  • 3D surface (different). It shows the overall shape fastest and is the most engaging, especially when readers can rotate it, but it's the hardest to read precisely.

How to make a 3D surface plot

With AI

ChatGPT, Claude Code, GitHub Copilot, Cursor and other AI tools are excellent at writing Plotly code. Name Plotly in the prompt, say what's on each axis and what the color means, and list the checks you want:

Make a plotly 3D surface from yields.csv (columns: date, tenor_years, yield_pct; one row per date and tenor).
- Pivot to a grid: rows = date, columns = tenor_years. Keep missing cells as NaN, don't fill them.
- go.Surface with time on x and tenor on y; color the surface by the 10-year minus 3-month spread
through surfacecolor, on a red-gray-blue scale centered on zero.
- aspectmode manual, aspectratio x=2.2, y=0.8, z=0.9. Try three camera eyes and keep the one
where nothing important is hidden.
- Soft lighting (specular 0.05) and a title that states the takeaway with a number.
- Also make a line chart of the two tenors and their spread.
- Save both as HTML and as PNG at scale=2, then check the PNG for hidden or clipped labels.

With Plotly Studio

Plotly Studio is a desktop app where you point an AI at a dataset and it builds charts and a Dash app around your questions. Ask for a surface the same way:

Show a 3D surface of hourly solar output by hour of day and day of the year, with a white-to-orange colorscale where zero is white, viewed from the morning side, and a heatmap of the same data next to it.

Make a 3D surface plot in Python or JavaScript

This is the code AI tools write under the hood. In Python, go.Surface needs only a 2D array; this is Mount Bruno's elevation grid from Plotly's documentation:

import pandas as pd
import plotly.graph_objects as go
z = pd.read_csv("https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv", index_col=0)
fig = go.Figure(go.Surface(z=z.values, colorscale="Viridis",
contours=dict(z=dict(show=True, project_z=True, usecolormap=True))))
fig.update_layout(scene_camera=dict(eye=dict(x=1.6, y=-1.4, z=0.8)))
fig.show()

If your data is in long format, one row per measurement, pivot it first with df.pivot(index="y", columns="x", values="z") and pass the column and index values as x and y.

In JavaScript, plotly.js takes z as an array of rows, one per y value. These are California's April net demand numbers from the chart at the top:

<div id="chart"></div>
<script src="https://cdn.plot.ly/plotly-4.1.1.min.js"></script>
<script>
Plotly.newPlot("chart", [{
type: "surface",
x: [0, 6, 12, 18, 23], // hour of day
y: [2019, 2022, 2025, 2026], // April of each year
z: [[18.3, 19.7, 10.7, 20.1, 19.6],
[17.8, 18.3, 4.7, 17.0, 18.8],
[18.0, 18.6, -0.8, 15.4, 18.8],
[17.7, 17.8, -1.4, 14.2, 18.2]],
contours: {z: {show: true, start: 0, end: 20, size: 4}}
}], {scene: {zaxis: {title: {text: "GW"}}}});
</script>

Share it

Every Plotly chart has a Share chart button (the cloud icon in its toolbar) that uploads it to Plotly Cloud and gives you a link. Only you can see it until you change its visibility. For a 3D surface this is the best way to share it, because readers can rotate it and hover for values.

For a live app, put the chart in a Dash app and publish it with the Plotly Cloud CLI. Coding agents can run these commands for you:

pip install "dash[cloud]"
plotly user login
plotly app publish

If your data has to stay in your own cloud or data center, publish to Dash Enterprise instead.

Dos and don'ts

Plotly's defaults are made for exploring, and a surface on its own looks fine until you ask what a reader can actually see. Here is the same Mount St. Helens elevation grid, in the same colors, both ways.

Don't leave the box as a cube, which stretches this mountain about 4.5 times without saying so, label the axes with grid indexes, skip contour lines, or look from the side where the crater is hidden:

Mount St. Helens drawn with terrain colors in plotly's default cube, which stretches it into a tall spike, with x and y axes numbered 0 to 220 by grid index, no contour lines, and a camera on the south-west side so the crater that opens to the north can't be seen

Do use real units with true proportions and state any exaggeration, add contour lines, light it softly, and point the camera at the feature that matters:

The same Mount St. Helens grid in the same terrain colors with axes in kilometers, 2x vertical exaggeration stated on the elevation axis, contour lines every 100 m, light from the northwest and a camera looking down from the north into the horseshoe crater and the lava dome inside it

Choose the camera on purpose

In a static image the camera decides what is in front and what is hidden. Here are four cameras on the duck curve.

Plotly's default angle. It's a reasonable start, but here it looks from the late-evening corner, so time runs away from the reader and the evening ramp overlaps the midday dip.

The duck curve surface from plotly's default camera angle, looking from the late-evening and 2026 corner, so the years run away from the reader and the evening ramp overlaps the midday dip

From 2019 (avoid). The oldest and highest curve is in front, so it hides the deeper recent years.

The duck curve surface seen from the 2019 side, where the shallow 2019 curve sits in front and the deeper 2025 and 2026 curves are hidden behind it

From 2026, with the x axis reversed (best here). The deepest curve is in front, every year is visible, and the day still runs left to right.

The duck curve surface seen from the 2026 side with the hour axis reversed so midnight is on the left, showing the blue below-zero midday dip in front and every earlier year behind it

From straight above. The surface becomes a heatmap with contour lines, so nothing is hidden but the shape is gone.

The duck curve surface seen from directly above, which looks like a heatmap with contour lines: orange at night, pale at midday and a blue patch at midday in 2025 and 2026

Checklist

Do

  • Write a title that states the takeaway with a number.
  • Try at least three cameras with scene.camera.eye and keep the one that hides nothing important.
  • Set aspectmode="manual" and aspectratio, and state any vertical exaggeration.
  • Add contour lines with contours.z, and use surfacecolor when a second variable deserves the color.
  • Keep missing cells as NaN so they show as holes, and mark measured points when the surface is interpolated.
  • Publish the interactive HTML too, and pair the surface with a heatmap or line chart when readers need numbers.

Don't

  • Use a surface when either axis is a category, or for a handful of curves.
  • Use a rainbow colorscale such as Jet, which creates false boundaries.
  • Trust the default cube, which stretches flat data and squashes tall data.
  • Let a peak hide the part of the data that matters.

Options to customize

These are the go.Surface options, and the 3D scene settings around it, that matter most. The full list is in the Plotly 3D surface documentation.

  1. z: a 2D array of values, one row per y and one column per x.
  2. x, y: the coordinates of the columns and rows; 2D arrays make a parametric surface such as a polar grid.
  3. surfacecolor: a second 2D array that sets the color instead of z, for a second variable.
  4. colorscale, cmin, cmax, cmid: the color mapping; cmid=0 for signed data, or set cmin and cmax to put a color break at sea level.
  5. contours.x, contours.y, contours.z: lines of equal value (show, start, end, size, color, width).
  6. contours.z.project.z, usecolormap: copy the contours onto the floor of the box.
  7. contours.z.highlight: lines that follow the mouse in the interactive version.
  8. hidesurface: draw only the contour lines, for a wireframe look.
  9. opacity: transparency, for a second, reference surface.
  10. lighting: ambient, diffuse, specular, roughness and fresnel; low specular and high roughness avoid a plastic look.
  11. lightposition: where the light comes from, such as the northwest for terrain.
  12. hovertemplate, customdata: hover text with units, labels and counts.
  13. scene.camera.eye, center, up: the camera position and target.
  14. scene.camera.projection.type: orthographic removes perspective, so equal heights look equal.
  15. scene.aspectmode, aspectratio: the box proportions.
  16. scene.xaxis.type (and y, z): linear, log, date or category; a log z axis suits rates.
  17. scene.xaxis.autorange="reversed": flip an axis to keep the reading direction when the camera turns around.
  18. scene.annotations: text with arrows anchored to 3D points.
  19. go.Mesh3d (x, y, z, i, j, k, delaunayaxis, alphahull, intensity): a surface from triangles or scattered points, for data that isn't on a grid.

More 3D surface plot examples

Ten more surfaces built from real public data, each with its source, the technique that makes it work and a prompt to rebuild it.

1. The US Treasury yield curve since 1982

3D surface of monthly US Treasury yields from 3 months to 10 years, January 1982 to September 2026, colored by the 10-year minus 3-month spread. The surface falls from about 14% in 1982 to near zero in 2009-2015 and 2020-2021, and the 2022-2024 period is a red band where the curve was inverted for 25 months

Each of the 537 months is one yield curve from 3 months to 10 years, and side by side they show the 40-year fall in rates and the jump in 2022. The 3-month yield sat above the 10-year for 25 months in a row, from November 2022 to November 2024, the longest run since at least 1982. Data: Federal Reserve H.15 via FRED, monthly averages.

Tip: the color comes from surfacecolor set to the 10-year minus 3-month spread, so every inverted month is a red band whatever its height.

Get the data from FRED's monthly constant-maturity Treasury yields (series GS3M, GS6M, GS1,
GS2, GS3, GS5, GS7, GS10, monthly averages of the Federal Reserve H.15 release)
and shape it into a table
(columns: month, maturity, maturity_years, yield_pct; January 1982 to the latest month).
Build a plotly go.Surface of the yield curve over time.
- Pivot to a grid: x = month as decimal years, y = maturity as evenly spaced positions labeled
3M ... 10Y, z = yield_pct.
- surfacecolor = 10Y minus 3M for that month (the same value across the row), red-gray-blue
scale from -3 to +3, so inverted months are red.
- aspectratio x=2.4, y=0.8, z=1.0; camera from the front-left (eye x=-0.6, y=-2.35, z=1.1).
- Title with the longest run of inverted months; muted subtitle with the source.

Another way to see it: two lines and their difference read more precisely than the surface. The spread fell to -1.74 points in May 2023, a number the surface can't give you.

Line chart of the 3-month and 10-year Treasury yields since 1982, with the 10-year minus 3-month spread below it, shaded red where negative; the longest negative stretch is labeled 25 months inverted, Nov 2022 to Nov 2024

2. California's solar output, hour by hour, for a year

3D surface of hourly solar output on the California ISO grid for every day of 2025, by hour of day and date. A wide ridge peaks at 21.6 GW at noon on 30 July, narrows in winter, and is notched by cloudy and stormy days

Every day of 2025 is one bell curve of solar output, and together they form a ridge that is wide in summer and narrow in winter. The grid's solar peaked at 21.6 GW at noon on 30 July, and the sunniest day (16 June, 234 GWh) made 15 times the energy of the darkest (31 December, 16 GWh). Data: California ISO, Today's Outlook history.

Tip: the camera looks from the morning side with a light from the same direction (lightposition), so the notches left by storms are shaded.

Get the data from the California ISO's Today's Outlook history (daily renewables file, 5-minute
solar output in MW, for every day of 2025)
and shape it into a table
(columns: date, hour, solar_mw = hourly mean of the 5-minute values, negatives set to 0).
Build a plotly go.Surface with hour of day on x, day of year on y (ticks at the first of each
month) and GW on z. White-to-yellow-to-orange colorscale with zero as white.
Camera from the morning side (eye x=-1.75, y=-1.05, z=0.9), lightposition from the same side.
Fill the missing spring-forward 2 a.m. with 0. Title with the peak and the best/worst day ratio.

3. Where a wind farm's energy comes from

3D polar surface of energy produced by the six Kelmarsh wind turbines in 2017 by wind speed (distance from the center) and wind direction, with N, E, S and W marked. One mountain rises in the south-west, the direction that produced 53% of the 32.3 GWh

This is a wind rose with height: each cell is the energy the six Kelmarsh turbines produced at that wind speed and direction in 2017. Winds from 188 to 263 degrees, a fifth of the compass, produced 53% of the 32.3 GWh. Data: Plumley (2022), Kelmarsh wind farm data, Zenodo, 10-minute SCADA records.

Tip: a parametric surface, where x, y and z are all 2D arrays (x = speed × sin(direction), y = speed × cos(direction)), draws a polar grid.

Get the data from the Kelmarsh wind farm data on Zenodo (doi 10.5281/zenodo.5841834,
2017 SCADA file, six Senvion MM92 turbines, 10-minute rows)
and shape it into a table
(columns: speed_ms bin center (0.5 m/s bins, 2-16 m/s), direction_deg bin center (15-degree
bins centered on 0..345), energy_mwh = power x 1/6 h summed over turbines, turbine_hours).
Drop rows where power <= 0 above 4 m/s (turbine not running).
Build a plotly parametric go.Surface: x = speed*sin(direction), y = speed*cos(direction),
z = energy, with north up. Close the circle by repeating the first direction, start at radius 0.
Hide the axes; draw speed rings and N/E/S/W as Scatter3d lines and text on the floor.
Title with the share of energy from the south-west.

4. Every US flu season since 1997

3D surface of the weekly share of US doctor visits for influenza-like illness, by week of the flu season and season from 1997-98 to 2025-26. Winter peaks rise from the surface, the October 2009 pandemic peak stands out at the start of a season, and 2025-26 peaks highest at 8.3%

Folding 29 years of weekly data into season by week lines up the winter peaks. The 2025-26 season peaked at 8.3% of outpatient visits in late December, the highest in the record, ahead of 2024-25 (7.8%) and the October 2009 H1N1 pandemic wave (7.7%). Data: CDC ILINet (FluView), weighted %ILI, via the Delphi Epidata API.

Tip: the summer weeks CDC didn't report before 2002-03 are NaN and draw as holes instead of fake zeros.

Get the data from CDC ILINet (FluView), national weighted %ILI by MMWR week, via the Delphi
Epidata API (fluview, region nat, from week 40 of 1997)
and shape it into a table
(columns: season like "2009-10", season_week (1 = MMWR week 40), epiweek, wili; drop week 53
and the season in progress).
Build a plotly go.Surface: season week on x (ticks Oct, Dec, Feb, ...), season index on y
(label a few seasons), %ILI on z, a cream-to-dark-red sequential scale, missing weeks as NaN.
High camera (eye x=-0.7, y=1.45, z=1.75) so tall seasons don't hide others.
Title with the highest peak. Also make a heatmap of the same grid with a category y axis.

Another way to see it: as a heatmap every season is visible, including the spring and autumn waves of the 2009 pandemic and the flat 2020-21 season.

Heatmap of weekly %ILI with one row per flu season from 1997-98 to 2025-26 and one column per week from October to September; dark red cells mark the 2003-04, 2009-10 autumn, 2017-18, 2022-23, 2024-25 and 2025-26 peaks

5. US death rates by age and year

3D surface of US death rates by single year of age and calendar year, 1950 to 2023, on a log scale. The surface drops steeply from infancy to a low around age 10 and then rises in a straight slope with age, and the whole surface sinks slowly over the decades

Demographers call this a Lexis surface. US infant mortality fell 84% from 1950 to 2023, and on a log scale adult death rates rise in a straight line, doubling about every 9 years of age. In 2021, rates at ages 30 to 50 were 38% to 50% higher than in 2019. Data: UN World Population Prospects 2024, life tables (estimates).

Tip: rates span four orders of magnitude, so scene.zaxis.type="log" and surfacecolor=np.log10(rate) make equal steps of height and color equal ratios.

Get the data from the UN World Population Prospects 2024 complete life tables (both sexes,
estimates 1950-2023, the WPP2024_Life_Table_Complete file) for the United States
and shape it into a table
(columns: year, age (0-100), mx = central death rate, ex = life expectancy).
Build a plotly go.Surface: year on x, age on y, mx on z with a log z axis
(ticks 0.01% ... 100%), surfacecolor = log10(mx) on a light-to-dark purple scale.
Camera from 2023 looking back (eye x=1.9, y=-0.75, z=0.55).
Title with the fall in infant mortality and the doubling time of adult death rates
(fit log(mx) on age for ages 35-85).

6. What makes a batted ball a hit

3D surface of the share of 2025 MLB batted balls that became hits, by exit velocity and launch angle. A red ridge of high hit rates runs along hard-hit line drives and fly balls, and soft or steep balls stay near zero; holes mark bins with fewer than 30 balls

Statcast measures every batted ball's speed and angle, and the chance of a hit rises steeply with both. In 2025, balls hit at 100 mph or more between 20 and 36 degrees became hits 72% of the time and home runs 56% of the time, against 33% hits at the same angles below 90 mph. Data: MLB Statcast via Baseball Savant, 120,216 batted balls.

Tip: bins with fewer than 30 balls are set to NaN, so the surface has holes instead of spikes from three or four batted balls.

Get the data from MLB Statcast via Baseball Savant's search CSV export (2025 regular season,
every pitch, requested a few days at a time)
and shape it into a table
(columns: exit_velo_mph bin center (2 mph bins), launch_angle_deg bin center (4-degree bins),
balls, hits, home_runs, hit_rate; keep balls in play with launch_speed and launch_angle).
Build a plotly go.Surface of hit rate (%) by exit velocity (x) and launch angle (y),
limited to 50+ mph and -40 to 70 degrees. Set cells with fewer than 30 balls to NaN.
Show the ball count in the hover via customdata. Camera from the low-velocity corner.
Title with the hit rate and home run rate for 100+ mph at 20-36 degrees.

7. The Island of Hawai'i from the seafloor

3D relief surface of the Island of Hawai'i and the seafloor around it, with deep blue ocean floor to -5,818 m, a black sea-level coastline, and green and brown slopes rising to 4,079 m at Mauna Kea and Mauna Loa, drawn at 4x vertical exaggeration

Most of the island is under water. On this grid it rises 9.9 km from the deepest seafloor nearby to the summits of Mauna Kea and Mauna Loa, more than Everest's 8.8 km above sea level, which is why Mauna Kea is often called the tallest mountain on Earth from base to summit. Data: NOAA ETOPO1 global relief model, 1 arc-minute.

Tip: with cmin=-6000 and cmax=4200, the colorscale changes from blues to greens exactly at sea level, and a single contours.z line at 0 draws the coastline.

Get the data from NOAA's ETOPO1 1 arc-minute global relief model (ERDDAP dataset etopo180)
for 18.3-20.8 N, 156.6-154.2 W
and shape it into a table
(a grid: first column lat, column names are longitudes, values are elevation or depth in meters).
Build a plotly go.Surface with x and y in km and 4x vertical exaggeration (state it).
- Colorscale with the blue-to-green break exactly at sea level (cmin -6000, cmax 4200).
- contours.z with start=0, end=0.1, size=1, black, width 4: the coastline.
- Hide the axes, add a colorbar in meters, camera from the southeast.
- Title with the relief from the deepest seafloor to the highest summit.

8. The slab of ocean floor sinking under Tonga

3D view of 23,420 earthquakes under Tonga and Fiji since 1973 as small dots, with an orange triangulated sheet tracing the subducting slab from about 100 km down to about 700 km and the seafloor with the Tonga Trench drawn at the top

The Pacific plate dives under Tonga at the Tonga Trench, and the earthquakes inside it trace the sinking slab. Of the 23,420 earthquakes of magnitude 4.5 and above recorded since 1973, 43% were deeper than 300 km, and the deepest was at 701 km. Data: USGS ANSS ComCat; NOAA ETOPO1 for the seafloor.

Tip: go.Mesh3d with delaunayaxis="z" triangulates the median depth of the deep quakes in half-degree cells into a sheet, with no grid needed.

Get the data from the USGS ANSS Comprehensive Catalog (FDSN event service, magnitude 4.5+,
26-15 S, 176-188 E, 1973 to today) and NOAA ETOPO1 relief for the same box
and shape it into tables
(quakes: time, lat, lon (0-360 east), depth_km, mag; seafloor: a grid of elevation in meters).
Build a plotly 3D figure:
- Bin quakes deeper than 100 km into 0.5-degree cells with 10+ events; median depth per cell.
- go.Mesh3d of the cell centers with z = -depth, delaunayaxis="z", intensity = depth, orange scale.
- go.Scatter3d of every quake as tiny semi-transparent dark points.
- go.Surface at z=0 colored by seafloor depth via surfacecolor.
- Camera from the south so the westward dip is visible. Title with the share deeper than 300 km.

9. The Arctic is warming fastest

3D surface of temperature anomaly by latitude and year, 1880-2025, relative to 1951-1980, as a 5-year running mean. The surface is blue before about 1920 and turns red after 1980, and a red wall rises at the northern edge after 2000 where the Arctic averaged 2.6 C above its baseline

Warming isn't even across latitudes. From 2016 to 2025 the Arctic (65-90°N) averaged 2.6 °C above its 1951-1980 level, 2.5 times the global 1.0 °C, and on the surface the Arctic rises as a wall at the far edge. Data: NASA GISTEMP v4, 2x2 degree grid.

Tip: the latitude axis is spaced by sin(latitude), so each band's width matches its share of the Earth's surface.

Get the data from NASA GISTEMP v4 (gistemp1200_GHCNv4_ERSSTv5 2x2 degree monthly grid,
anomalies vs 1951-1980)
and shape it into a table
(columns: year (1880-2025), lat (2-degree band center), anomaly_c = annual mean of the
monthly zonal means, requiring 9+ months).
Build a plotly go.Surface: year on x, sin(latitude) on y (labeled 60°S ... 90°N), anomaly on z,
smoothed with a 5-year running mean along years.
- Diverging blue-white-red scale centered on 0, clipped at ±3 °C.
- If the y tick labels end up behind the surface, add them as Scatter3d text before 1880.
- Title with the Arctic vs global average for 2016-2025, computed from the raw annual values.

Another way to see it: a heatmap of the raw annual values needs no camera, and the Arctic's red band since 2000 is the first thing you see.

Heatmap of temperature anomaly by latitude band and year from 1880 to 2025, blue in the early decades and red in recent ones, with the deepest red along the top rows (the Arctic) since 2000

10. The Keeling curve, folded by year

3D surface of monthly CO2 at Mauna Loa by month and year from 1958 to 2026, rising from about 316 ppm to 427 ppm, colored brown above the trend in spring and green below it in late summer and autumn

The Keeling curve rises from 316 ppm in 1959 to 427 ppm in 2025, and every year northern forests pull it down by about 6 ppm in summer and release it in winter. Folded into month by year, the trend becomes the slope and the cycle becomes stripes. Data: NOAA Global Monitoring Laboratory and Scripps, Mauna Loa monthly means.

Tip: height is the CO2 level, but surfacecolor is only the seasonal part (each month minus NOAA's deseasonalized value), so one surface shows two things.

Get the data from NOAA GML's Mauna Loa monthly mean CO2 file (co2_mm_mlo.csv, with NOAA's
deseasonalized values)
and shape it into a table
(columns: year, month, co2_ppm, deseasonalized_ppm, seasonal_ppm = co2_ppm - deseasonalized_ppm).
Build a plotly go.Surface: month on x (Jan-Dec), year on y, co2_ppm on z,
surfacecolor = seasonal_ppm on a green-white-brown scale from -4 to +4.
Camera from the December side (eye x=1.85, y=-1.35, z=0.95).
Title with the first and last full-year means and the average yearly swing of the last 10 years.

FAQ

bluesly
x
instagram
youtube
medium
fcaebook

Product

© 2026
Plotly. All rights reserved.
Cookie Preferences
aicpa/soc2