A true-colour time-lapse of the June 2026 European heat wave¶
The maps in the heat-wave case study show temperature; this notebook shows the view from orbit — an animated true-colour time-lapse of Western Europe across the heat wave, one frame per day. Animated satellite imagery is the thing readers actually stop and watch, and it tells the heat-wave story in its own way: during the event, a high-pressure ridge keeps the skies over the hot region (Iberia, France) clear, while weather systems churn cloud around the edges.
We use MODIS daily surface reflectance (MOD09GA) — its once-a-day global coverage is what makes a
smooth day-by-day animation possible (Sentinel-2's 5-day revisit is far too sparse). Everything is
pulled through the earthlens gee backend.
Setup¶
pyramids reads the GeoTIFFs, matplotlib.animation builds the movie, and IPython.display.HTML
embeds it as an in-browser player. Earth Engine needs a service account, read from the
GEE_SERVICE_ACCOUNT / GEE_SERVICE_KEY environment variables.
import base64
import os
from pathlib import Path
import matplotlib.animation as animation
import matplotlib.pyplot as plt
from IPython.display import HTML
from matplotlib.animation import PillowWriter
from pyramids.dataset import Dataset
from earthlens.core import EarthLens
SERVICE_ACCOUNT = os.environ["GEE_SERVICE_ACCOUNT"]
SERVICE_KEY = os.environ["GEE_SERVICE_KEY"]
OUT = Path("out") / "timelapse"
OUT.mkdir(parents=True, exist_ok=True)
Pull one true-colour frame per day¶
We request the visible MODIS bands over Western Europe for 17–27 June 2026 at a continental ~4 km scale — a daily true-colour GeoTIFF per day. (Daytime clouds and the occasional MODIS orbital swath gap are part of the picture: they are the weather moving.)
job = EarthLens(
data_source="gee",
dataset="MODIS/061/MOD09GA",
variables=["sur_refl_b01", "sur_refl_b04", "sur_refl_b03"],
start="2026-06-17",
end="2026-06-27",
temporal_resolution="daily",
scale=4000.0,
path=OUT,
export_via="url",
lat_lim=[38.0, 54.0],
lon_lim=[-10.0, 14.0],
)
job.authenticate(service_account=SERVICE_ACCOUNT, service_key=SERVICE_KEY)
frame_paths = job.download(progress_bar=False)
len(frame_paths)
Build the animation¶
pyramids renders the true colour for us: Dataset.plot(rgb_options={"rgb": [0, 1, 2], "surface_reflectance": ...})
composites the red/green/blue bands and stretches the reflectance — no hand-rolled band maths. We drive
a matplotlib animation that re-plots each daily Dataset into the same axis and stamps the date, then
save it as an animated GIF and embed it inline. A GIF plays in any viewer — Jupyter, VS Code,
GitHub, nbviewer — with no JavaScript and no "trusted notebook" step (the reason an to_jshtml player
often shows up blank).
datasets = [Dataset.read_file(p) for p in frame_paths]
dates = [p.stem.split("_")[-1] for p in frame_paths]
fig, ax = plt.subplots(figsize=(6, 4.3), dpi=64)
def draw_frame(i):
ax.clear()
datasets[i].plot(
rgb_options={"rgb": [0, 1, 2], "surface_reflectance": 3200}, ax=ax, fig=fig
)
ax.set_title(f"MODIS true colour — {dates[i]}")
ax.axis("off")
anim = animation.FuncAnimation(fig, draw_frame, frames=len(datasets))
gif_path = OUT / "heatwave_timelapse.gif"
anim.save(gif_path, writer=PillowWriter(fps=2))
plt.close(fig)
Embed the GIF straight into the notebook as a base64 data URI, so the animation travels with the file and needs no companion image.
encoded = base64.b64encode(gif_path.read_bytes()).decode()
HTML(
f'<img src="data:image/gif;base64,{encoded}" alt="MODIS true-colour time-lapse" />'
)
Press play. Watch the cloud-free lobe park over Iberia and France through the peak of the event — that clear, high-pressure air is exactly what let the surface bake — while Atlantic and Alpine weather swirls cloud around it and the snow line clings to the Alps. The diagonal seams that flit through some frames are MODIS orbital swath edges, a normal artefact of stitching a daily global sensor.
To make it your own: change lat_lim / lon_lim to another region, widen the date range, or drop
scale to a smaller number for a sharper (heavier) movie. Swap MOD09GA for MYD09GA to animate the
afternoon (Aqua) overpass instead of the morning (Terra) one.