An ECMWF-style temperature animation over Europe — June → July 2026¶
This showcase reproduces the look ECMWF posts on social media — the glowing blue-to-magenta
2 m-temperature maps on a dark canvas with country borders — for the whole of
1 June → 15 July 2026, one frame per day, straight from earthlens.
The colour is not hand-rolled: cleopatra ships style="2t", the genuine ECMWF Magics palette
for 2 m temperature (the GRIB shortName 2t, vendored from ECMWF's own palettes.json). pyramids
forwards style= straight through Dataset / DatasetCollection, so a single cube.plot(style="2t")
gives the ECMWF colour scale, and add_reference_map(style="ecmwf-dark") drops the dark basemap and
borders underneath.
On "till now": ERA5-Land's daily aggregate is not real-time — verified live, its most recent published day as of this writing is 15 July 2026 (about nine days behind). So the animation runs 1 June → 15 July; extend
ENDonce newer days publish.Needs
GEE_SERVICE_ACCOUNT/GEE_SERVICE_KEYandpyramids-gis[viz](cleopatra ≥ 0.26).
Setup¶
pyramids supplies Dataset / DatasetCollection; cleopatra supplies the ECMWF "2t" preset, the
dark-basemap inset, and apply_blank_canvas for the dark look; earthlens supplies the unified
EarthLens entry point.
import base64
import os
import warnings
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from cleopatra.styling.colors import DATA_STYLES, resolve_colormap
from cleopatra.styling.styles import apply_blank_canvas
from IPython.display import HTML
from loguru import logger
from pyramids.dataset import Dataset
from pyramids.dataset.collection import DatasetCollection
from earthlens.core import EarthLens
warnings.filterwarnings("ignore")
logger.remove()
Credentials, region and window¶
Credentials come from a repo-root .env (loaded with python-dotenv). EUROPE is a generous
continental box; START / END bracket the June-to-mid-July window.
from dotenv import find_dotenv, load_dotenv
load_dotenv(find_dotenv(usecwd=True))
SERVICE_ACCOUNT = os.environ["GEE_SERVICE_ACCOUNT"]
SERVICE_KEY = os.environ["GEE_SERVICE_KEY"]
EUROPE = {"lat_lim": [34.0, 62.0], "lon_lim": [-12.0, 32.0]}
START, END = "2026-06-01", "2026-07-15" # 07-15 = latest published ERA5-Land daily day
OUT = Path("out") / "europe_temperature_2026"
OUT.mkdir(parents=True, exist_ok=True)
1 · Pull one daily 2 m-temperature tile per day¶
ERA5-Land's daily aggregate is hosted on Earth Engine, so we reach temperature_2m through the gee
key. One daily-mean GeoTIFF per day over the continental box. Already-downloaded tiles are reused, so
a rerun does not re-hit Earth Engine.
raw_dir = OUT / "t2m_daily"
raw_dir.mkdir(parents=True, exist_ok=True)
cached = sorted(raw_dir.glob("*.tif"))
if cached: # reuse a previous run's tiles instead of re-pulling from Earth Engine
eu_paths = cached
print(len(eu_paths), "daily tiles (cached)")
else:
job = EarthLens(
data_source="gee",
dataset="ECMWF/ERA5_LAND/DAILY_AGGR",
variables=["temperature_2m"],
start=START,
end=END,
temporal_resolution="daily",
scale=10000.0,
path=raw_dir,
export_via="url",
**EUROPE,
)
job.authenticate(service_account=SERVICE_ACCOUNT, service_key=SERVICE_KEY)
eu_paths = job.download(progress_bar=False)
print(len(eu_paths), "daily tiles")
2 · Convert kelvin to °C¶
ERA5-Land 2 m temperature is in kelvin with 0 as the sea/fill value; mask everything below 200 K —
well under any real surface temperature, so it catches the 0 fill without touching valid data — and
subtract 273.15. Each converted day is written into t2m_celsius/ and the day is recorded as a frame
label.
celsius_dir = OUT / "t2m_celsius"
celsius_dir.mkdir(parents=True, exist_ok=True)
celsius_paths = []
for path in sorted(eu_paths):
field = Dataset.read_file(str(path))
field = field.apply(lambda kelvin: np.where(kelvin > 200, kelvin - 273.15, np.nan))
cpath = celsius_dir / Path(path).name
field.to_file(str(cpath))
celsius_paths.append(cpath)
labels = [
pd.to_datetime(Path(p).stem.split("_")[-1]).strftime("%b %d")
for p in sorted(eu_paths)
]
len(celsius_paths), labels[0], "->", labels[-1]
3 · Animate with the ECMWF "2t" preset + a dark base map¶
This is the ECMWF-social-media recipe, one call at a time:
cube.plot(style="2t")— the genuine ECMWF Magics 2 m-temperature palette.apply_blank_canvas(..., facecolor="black")— the dark-animation canvas.save_animation(..., fps=5, crf=23)— writes the frames out as an mp4.add_reference_map(style="ecmwf-dark", ...)— the dark basemap with country borders underneath.
The "2t" palette is flat-saturated at both ends by design, with the gradient packed into the middle,
so we widen its range to [-10, 50] °C — that puts the real 5–38 °C daily-mean spread into the
gradient band instead of the saturated caps, and it reads like ECMWF's. DATA_STYLES is cleopatra's
shared module-level table, so the two injected keys are removed again in finally to avoid leaking a
mutated "2t" style into any other cell or notebook in the same session.
west, east = EUROPE["lon_lim"]
south, north = EUROPE["lat_lim"]
# The ECMWF "2t" 2 m-temperature palette (cleopatra's temperature_2m preset),
# stretched to a fixed -10..50 degC range so the heat spread lands in the gradient
# band rather than saturating the palette's flat ends.
t2m_cmap = resolve_colormap(next(iter(DATA_STYLES["temperature_2m"].values()))["cmap"])
cube = DatasetCollection.from_files(celsius_paths)
glyph = cube.plot(cmap=t2m_cmap, vmin=-10, vmax=50, figsize=(8.5, 7.6))
apply_blank_canvas(glyph.ax, facecolor="black") # the ECMWF/CAMS dark-animation look
# The reference map's extent is in degrees, matching the bbox the frames were
# cropped to, so the inset lines up with the data underneath it.
glyph.add_reference_map(style="dark", extent=[west, south, east, north])
mp4 = OUT / "europe_t2m_2026.mp4"
glyph.save_animation(str(mp4), fps=5, crf=23)
plt.close("all")
mp4
Embed the MP4 straight into the notebook as a base64 data URI so the animation travels with the file.
encoded = base64.b64encode(mp4.read_bytes()).decode()
HTML(
f'<video src="data:video/mp4;base64,{encoded}" width="760" '
'autoplay loop muted playsinline controls></video>'
)
Notes¶
- Swap the preset.
style="2t"is 2 m temperature; cleopatra vendors other ECMWF Magics palettes keyed by GRIB shortName. Inspectfrom cleopatra.styling.colors import DATA_STYLES; sorted(DATA_STYLES). - Lighter look. Drop
apply_blank_canvasand switch toadd_reference_map(style="ecmwf-light")for the white-background variant. - Longer / smoother. Extend
ENDas newer ERA5-Land days publish, or raisefpsfor a faster playback. Lowerscalefor a sharper (heavier) movie. - Same recipe elsewhere. Change
EUROPEto any box and re-run — the whole animation is onecube.plot(style=...)→apply_blank_canvas→add_reference_map→save_animationsequence.