Earth & Environmental Computing · Week 07 · Course session · 43 slides
Download HTML
Earth & Environmental Computing · Computation in the Earth and Environment · Fall 2026

Scientific Visualization

A scientific figure exists to make one claim visible. Everything else is decoration.

Week 07·Course session·Open Course Materials | Adapt for your classroom
Learning objectives

By the end of this lecture, you will be able to

  • State the single message of a figure before building it
  • Name the parts of a matplotlib figure and control each one through ax
  • Apply the completeness checklist: labels, units, legend, size
  • Choose axis scales deliberately, including log axes
  • Pick a colormap that matches the data: sequential, diverging or qualitative
  • Write a three-part caption and save the figure as a deliverable
Contents

Today's Lecture

01Message first
05Color
02Anatomy of a figure
06Choosing the plot
03Critique and rebuild
07Captions and saving
04Axis scales
08Group Lab 1
Recap

Last Week in Sixty Seconds

  • groupby = typical; resample = actual; the difference = anomaly
  • The river peaks days after the rain: you measured the lag
  • Merges drop rows silently; len() catches them
  • Today: make that lag visible and defensible to a reader
Data

Today's Datasets

FileSourceCoverageUsed for
iowa_streamflow_daily.csvUSGS NWIS, site 05454500, Iowa River at Iowa CityDaily, 2015–2025River panel, flow-duration curve
iowa_city_weather_daily.csvIowa Environmental Mesonet, station IOWDaily, 2019–2025Rain panel, scatter, histogram
synthetic_temperature_grid.csvCourse-generated, seed 410050 × 50 grid, 10 × 8 kmColormap choices
synthetic_elevation_grid.csvCourse-generated, seed 410050 × 50 grid, 10 × 8 kmGridded field map
01

Message First

Decide what the reader should see before writing any plotting code.

01 · Message first

Message First

The test

  • Say the figure's claim in one sentence
  • Cannot say it? Not ready to design
  • Every element either serves the claim or leaves

Why it matters

  • Figures are read 10x more than text
  • Reviewers judge papers by figures first
  • Your final project is graded this way too
01 · Message first

From Question to Claim to Figure

QuestionOne-sentence claimFigure
How did the river respond to the May 2019 storms?The Iowa River peaked at 12,600 cfs on 29 May, one day after 42.7 mm of rain.Two-panel rain and river
How rare are flows like spring 2019?Flows above 6,520 cfs occur on only 10% of days in 2015–2025.Flow-duration curve, log axis
Where is the site warmer than average?A north–south band near x = 4 km runs about 1 °C above the site mean.Anomaly map, diverging colormap
02

Anatomy of a Figure

Every visible element is an object you can name and control.

02 · Anatomy of a figure

Figure and Axes Vocabulary

Iowa River at Iowa City, 201904,0008,00012,000JanMarMayJulSepNovDischarge (cfs)Date, 2019Daily mean30-day mean12345678
1Figure: the canvasfig
2Axes: the plot areaax
3Titleax.set_title()
4Y-axis labelax.set_ylabel()
5X-axis labelax.set_xlabel()
6Tick labelsax.tick_params()
7Legendax.legend()
8Line (Line2D)ax.plot()
02 · Anatomy of a figure

Two Ways to Call Matplotlib

pyplot state machine

plt.plot(s["date"], s["discharge_cfs"])
plt.ylabel("Discharge (cfs)")
plt.title("Iowa River, 2019")
plt.show()

Acts on whichever axes is "current". Fine for a quick look; ambiguous once there are two panels.

Explicit figure and axes · use this

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(s["date"], s["discharge_cfs"])
ax.set_ylabel("Discharge (cfs)")
ax.set_title("Iowa River, 2019")

Every call names its target. The same code scales to two, four or nine panels without changes.

02 · Anatomy of a figure

Multi-Panel Layouts

fig, (ax_rain, ax_flow) = plt.subplots(
    2, 1,
    sharex=True,
    figsize=(10, 6),
    gridspec_kw={"height_ratios": [1, 2]},
    layout="constrained",
)
  • 2, 1: two rows, one column; returns two axes you can name
  • sharex=True: one time axis, so dates line up straight down
  • height_ratios: the effect (river) gets twice the height of the cause (rain)
  • layout="constrained": matplotlib spaces labels so panels do not overlap
  • figsize is in inches; 10 × 6 fits a slide or a full-width page
03

Critique and Rebuild

One bad figure, five problems, three steps to fix it.

03 · Live critique

Find Five Problems

fig, ax = plt.subplots(figsize=(3, 2))
ax.plot(stream["date"], stream["discharge_cfs"])
ax.plot(weather["date"],
        weather["precipitation_mm"])
plt.show()

Same 2019 data as the rest of this section. Work in pairs for two minutes.

0200040006000800010000120002019-012019-032019-052019-072019-092019-11
03 · Live critique

Five Problems, Named

0200040006000800010000120002019-012019-032019-052019-072019-092019-1112345
1No axis labels or units: the reader cannot check a single value
2Rain (mm) and flow (cfs) share one axis, so rain flattens to zero
3No legend: which line is which?
43 × 2 inch canvas: ticks are unreadable once printed
5No stated message: nothing tells the reader where to look
03 · The checklist

Non-negotiable Elements

01

Labels + units

Discharge (cfs). A figure without units cannot be checked.

02

Readable size

Text legible from the back row, or in a two-column PDF.

03

Legend

Whenever two lines share a panel. No guessing.

04

Stated message

Title or caption says what to see, not just what it is.

This checklist is the figure rubric for every remaining lab.

03 · Rebuild, step 1 of 3

Two Panels, One Time Axis

w = weather[weather["date"].dt.year == 2019]
s = stream[stream["date"].dt.year == 2019]

fig, (ax_rain, ax_flow) = plt.subplots(
    2, 1, sharex=True, figsize=(10, 6),
    gridspec_kw={"height_ratios": [1, 2]})

ax_rain.bar(w["date"], w["precipitation_mm"],
            width=1)
ax_flow.plot(s["date"], s["discharge_cfs"])

Fixes problem 2. Rain is daily totals, so it is drawn as bars; flow is continuous, so it is a line.

04000800012000020402019-012019-052019-09
03 · Rebuild, step 2 of 3

Labels, Units, Inverted Rain

ax_rain.invert_yaxis()
ax_rain.set_ylabel("Rain (mm/day)")
ax_flow.set_ylabel("Discharge (cfs)")
ax_flow.set_xlabel("Date, 2019")

ax_flow.xaxis.set_major_formatter(
    mdates.DateFormatter("%b"))
ax_flow.yaxis.set_major_formatter(
    StrMethodFormatter("{x:,.0f}"))

Fixes problems 1 and 4. Month names replace ISO dates; thousands separators make 12,000 readable at a glance.

04,0008,00012,00002040JanMarMayJulSepNovRain (mm/day)Discharge (cfs)Date, 2019
03 · Rebuild, step 3 of 3

One Highlight, One Annotation

flood = (pd.Timestamp("2019-05-17"),
         pd.Timestamp("2019-07-04"))
for ax in (ax_rain, ax_flow):
    ax.axvspan(*flood, color="0.92", zorder=0)

peak = s.loc[s["discharge_cfs"].idxmax()]
q = peak["discharge_cfs"]
ax_flow.annotate(f"Peak {q:,.0f} cfs, 29 May",
    xy=(peak["date"], q), xytext=(20, 0),
    textcoords="offset points",
    arrowprops={"arrowstyle": "-"})

Fixes problem 5. The band marks 17 May to 4 July, when flow rose and stayed above 9,000 cfs.

04,0008,00012,00002040JanMarMayJulSepNovRain (mm/day)Discharge (cfs)Date, 2019Peak 12,600 cfs, 29 May42.7 mm on 28 May
03 · Result

The Iowa River Peaked One Day After 42.7 mm of Rain

04,0008,00012,00002040JanFebMarAprMayJunJulAugSepOctNovDecRain (mm/day)Discharge (cfs)Date, 2019Peak 12,600 cfs, 29 May42.7 mm on 28 May

figures/grouplab01_rain_river_2019.png

Pause and think

?

Your two lines are distinguishable only by color. Name two situations where the figure fails, and the fix.

Grayscale printing and colorblind readers
Fix: vary line style or markers too; color is never the only encoding
04

Axis Scales

When values span orders of magnitude, a linear axis hides most of the data.

04 · Axis scales

When Linear Axes Lie

Linear axis

05,00010,00015,000020406080100Discharge (cfs)Percent of days flow is exceededQ10 6,520Q50 1,850Q90 258

Log axis

1001,00010,000020406080100Discharge (cfs), log scalePercent of days flow is exceededQ10 6,520Q50 1,850Q90 258

Iowa River at Iowa City, 2015–2025: 89 to 14,800 cfs, a 166-fold range. On the linear axis, everything below flood flow flattens.

04 · Axis scales

Building the Flow-Duration Curve

from earthcourse.hydro import flow_duration_curve

fdc = flow_duration_curve(stream)  # 4,018 days

fig, ax = plt.subplots(figsize=(8, 5))
ax.semilogy(fdc["exceedance_probability"] * 100,
            fdc["discharge_cfs"])
ax.set_xlabel("Percent of days flow is exceeded")
ax.set_ylabel("Discharge (cfs)")
ax.grid(True, which="both", alpha=0.25)
StatisticFlow (cfs)Meaning
Q106,520High flow: exceeded on 10% of days
Q501,850Median daily flow
Q90258Low flow: exceeded on 90% of days

The 2019 peak, 12,600 cfs, sits above Q10 by a factor of two.

05

Color

A colormap is an axis. Choose it the way you choose a scale.

05 · Color

Rainbow Versus Sequential

jet

222324010 km80°C

jet, in grayscale

222324010 km80°C

viridis

222324010 km80°C

viridis, in grayscale

222324010 km80°C
  • jet's bright cyan and yellow bands invent edges the smooth field does not have; in grayscale its order is lost
  • viridis gets brighter from low to high, so the order survives printing and most color-vision deficiencies
05 · Color

Three Kinds of Colormap

TypeUse when the data areExample in our datamatplotlibMap
SequentialMagnitudes from low to highElevation (m), temperature (°C)viridis, YlGnBu
DivergingDepartures from a meaningful centerTemperature anomaly from the site meanRdBu_r, centered on 0
QualitativeUnordered categoriesStation, land-cover classtab10
AvoidNever for quantitative dataIt invents boundariesjet, rainbow
05 · Color

Diverging Maps for Anomalies

grid = temp.pivot(index="y_km", columns="x_km",
                  values="temperature_c")
anom = grid - grid.values.mean()
lim = np.abs(anom.values).max()

fig, ax = plt.subplots(figsize=(7, 5))
im = ax.pcolormesh(
    grid.columns, grid.index, anom,
    cmap="RdBu_r", vmin=-lim, vmax=lim)
fig.colorbar(im, ax=ax, label="Anomaly (°C)")
ax.set_aspect("equal")
ax.set_xlabel("x (km)")
ax.set_ylabel("y (km)")
−10+10510048x (km)y (km)Temperature anomaly (°C)
06

Choosing the Plot

The question decides the plot type.

06 · Choosing the plot

Which Plot for Which Question

QuestionPlotWatch for
How does it change in time?Line (ax.plot)Too many lines = spaghetti
How do two variables relate?Scatter (ax.scatter)Overplotting: use alpha
How is it distributed?Histogram (ax.hist)Bin count changes the story
What does the field look like?imshow / contourfColorbar with units
Orders of magnitude?semilogyLog-scale ticks readable
06 · Choosing the plot

Five Questions on Our Data

How does flow change through 2019?

10k cfsJanDec 2019
ax.plot

Daily discharge, USGS 05454500

How does dew point relate to air temperature?

−25 °Cair 30 °Cdew point
ax.scatter(alpha=0.1)

2,546 days, IEM station IOW

How is daily air temperature distributed?

−25 °C30 °Cdays
ax.hist(bins=30)

Daily mean, 2019–2025

What does the terrain look like?

010 kmm
ax.imshow

Synthetic elevation, 204–259 m

How often is each flow exceeded?

10k cfs1000%100% exceed.
ax.semilogy

Flow-duration curve, 2015–2025

06 · Choosing the plot

Plotting Functions at a Glance

FunctionDrawsUse it to showExample in our data
ax.plot(x, y)LinesChange over timeDaily discharge, 2019
ax.scatter(x, y)PointsHow two variables relateDew point vs. air temperature
ax.bar(x, height)BarsTotals per groupRain total per month, 2019
ax.hist(values, bins=30)Counts in binsHow one variable is spreadDaily mean temperature
ax.boxplot(groups)Boxes and whiskersDistributions side by sideDischarge by month
ax.fill_between(x, low, high)Shaded bandA range or uncertaintyFlow range by day of year
ax.errorbar(x, y, yerr=sd)Points with barsA mean and its spreadTemperature by month, ±1 SD
ax.imshow(grid)Cells by array indexA field on a regular gridElevation, 50 × 50 cells
ax.pcolormesh(x, y, grid)Cells at x, yA field in real coordinatesTemperature grid, km axes
ax.contourf(x, y, grid)Filled contoursLevels of a smooth surfaceElevation contours
ax.semilogy(x, y)Line, log y axisValues across orders of magnitudeFlow-duration curve

All eleven are methods of ax from fig, ax = plt.subplots(). pandas shortcuts such as df.plot() and df.hist() call the same functions.

07

Captions and Saving

The caption carries the claim; the saved file is the deliverable.

07 · Captions

The Three-Part Caption

What

Daily discharge of the Iowa River at Iowa City for 2019, with daily precipitation inverted above.

How

USGS station 05454500 and Iowa Environmental Mesonet data; highlighted band marks the spring flood, 17 May to 4 July.

Notice

The river peaked at 12,600 cfs on 29 May, one day after 42.7 mm of rain; flow stayed above 9,000 cfs into early July.

Pause and think

?

A caption reads: "Figure 1. Discharge versus time." Which of the three parts is present, and what is missing?

Only "what", barely: no how, no notice
A reader learns nothing they could not see already
07 · Captions

From Caption to 200-Word Interpretation

01 · Pattern

What the eye should find

Flow rose from about 2,000 to 12,600 cfs over two weeks of repeated storms.

02 · Mechanism

The physical reason

214 mm fell between 17 and 29 May on already wet soils, so most rain became runoff.

03 · Caveat

One honest limitation

Rain is from one station, IOW; the Iowa River drains a much larger basin upstream.

Group Lab 1 grades all three, not just the picture.

07 · Saving

Saving Figures Like a Professional

from earthcourse.plotting import save_figure

saved_path = save_figure(
    fig,
    PROJECT_ROOT / "figures"
    / "grouplab01_rain_river_2019.png",
)
print(saved_path)
  • Creates figures/ if it does not exist
  • Saves at 150 dpi with bbox_inches="tight", so labels are never clipped
  • The saved file is the deliverable; name it like you mean it: lab, content, year
  • Reference the filename in your caption and README
08

Group Lab 1

Group labs begin this week.

08 · Group labs begin

How Group Lab 1 Runs

1

Module A

Data validation: date ranges, missing values, units confirmed before plotting.

2

Module B

Statistics: the numbers the figure will carry (peaks, means, reference lines).

3

Module C

Figure and caption: design, critique, 200-word interpretation.

Groups of 2-3; roles posted before Wednesday; everyone signs a contribution statement.

08 · This week's lab

Group Lab 1: Scientific Visualization

Lab session

  • Critique the deliberately bad figure
  • Build the two-panel rain-and-river figure
  • Flow-duration curve on a log axis

Deliverables (Friday)

  • One polished, saved figure
  • Three-part caption
  • 200-word interpretation
08 · Pitfalls

Where Group Figures Go Wrong

  • Module C waits for final data instead of designing with placeholders
  • Over-decoration: gridlines and colors that add nothing
  • Captions that describe axes instead of stating the finding
  • A boring year chosen when a flood year was available
Summary

Key Takeaways

  • One figure, one claim, said in one sentence
  • Work through fig, ax; every element has a method
  • Labels, units, legend, size: the non-negotiables
  • Log axes when data span orders of magnitude
  • Sequential for magnitude, diverging for anomalies, never rainbow
  • Captions: what, how, notice
Discussion · Next week: statistics and regression

?

Find one published figure this week, from any paper, that fails the checklist. Bring it Friday; the best bad example earns bragging rights.

Next week: quantify what the figure suggests, with uncertainty attached