Plotting NetCDF data#
NetCDF.plot is a dedicated, xarray-aligned plotting surface — it does not inherit
Dataset.plot's GeoTIFF / Sentinel-imagery semantics. Instead of band=<int> you pick a
variable; instead of loose keyword soup you pass small, frozen option dataclasses; and you get
first-class support for selecting along non-spatial dimensions, curvilinear grids, contour/pcolormesh
artists, small-multiples (facets), animation, and lazy (dask-backed) reads.
from pyramids.netcdf import Selectors, ColourOpts, FacetSpec
from pyramids.netcdf import NetCDF
nc = NetCDF.read_file("era5.nc")
nc.plot("t2m") # the simplest call
Plotting requires the
[viz]extra:pip install 'pyramids-gis[viz]'(which pinscleopatra[tiles]). Animation/lazy reads additionally need the[lazy]extra.
NetCDF.plot returns a cleopatra ArrayGlyph
(or a FacetGrid when faceting) — use glyph.fig / glyph.ax to drop down to raw matplotlib.
The full signature:
NetCDF.plot(
variable=None, *,
selectors=None, # Selectors(time=, level=, member=, sel=, isel=)
colour=None, # ColourOpts(cmap=, vmin=, vmax=, robust=, levels=, norm=, center=, extend=, add_colorbar=, cbar_kwargs=)
facet=None, # FacetSpec(col=, row=, col_wrap=)
coords=None, # (x_2d, y_2d) curvilinear coordinates -> pcolormesh
kind="auto", # "auto" | "imshow" | "pcolormesh" | "contour" | "contourf"
animate=None, # True | dimension name
chunks=None, # dask chunks -> lazy read
basemap=None, # True | provider name
exclude_value=None,
title=None, ax=None, figsize=None,
**kwargs, # forwarded to cleopatra's ArrayGlyph (color_scale, cbar_label, ...)
)
See the Plotting reference for the parameter table and the
auto-generated Selectors / ColourOpts / FacetSpec docs, and the
NetCDF Class reference for the rendered method docstring.
1. Pick a variable#
If the file has a single data variable (or one has been selected), nc.plot() works with no
arguments. Otherwise name it:
2. Select along non-spatial dimensions#
A NetCDF variable is often 3-D (time, lat, lon) or 4-D (time, level, lat, lon). Use
Selectors to reduce it to a 2-D slice — time= / level= / member= are convenience aliases for
the common CF dimensions, and sel= / isel= are the generic label- and integer-based escape
hatches (mirroring xarray.Dataset.sel / .isel):
from pyramids.netcdf import Selectors
# label-based: a timestamp and a pressure level
nc.plot("t", selectors=Selectors(time="2020-07-01", level=850))
# integer-based: the 0th time step
nc.plot("t2m", selectors=Selectors(isel={"time": 0}))
# an ensemble member plus a generic label selection
nc.plot("t2m", selectors=Selectors(member=3, sel={"forecast_period": "24h"}))
If you leave a non-spatial dimension unpinned, the leading slice is rendered (and, for animate=,
it becomes the frame axis — see below).
3. Colour mapping#
Group all colour options under ColourOpts — these mirror xarray's plotting kwargs:
from pyramids.netcdf import ColourOpts
# robust limits (2nd / 98th percentile), a diverging cmap centred on zero
nc.plot("t2m_anomaly", colour=ColourOpts(cmap="RdBu_r", robust=True, center=0.0))
# explicit limits + discrete levels + extend arrows on the colorbar
nc.plot("tp", colour=ColourOpts(vmin=0, vmax=50, levels=11, extend="max"))
# pass a custom matplotlib Normalize
import matplotlib.colors as mcolors
nc.plot("chlor_a", colour=ColourOpts(norm=mcolors.LogNorm(vmin=0.01, vmax=10)))
# suppress the colorbar entirely
nc.plot("t2m", colour=ColourOpts(add_colorbar=False))
For the legacy color_scale / gamma / line_threshold / line_scale / bounds / midpoint
knobs, pass them through **kwargs — they reach cleopatra's ArrayGlyph directly. color_scale
takes a cleopatra.styles.ColorScale string
alias ("linear", "power", "sym-lognorm", "boundary-norm", "midpoint"; case-insensitive):
4. Render kind#
By default pyramids picks imshow for regular (1-D coordinate) grids. Override with kind=:
nc.plot("sst", kind="pcolormesh") # honours irregular spacing
nc.plot("z500", kind="contour") # line contours
nc.plot("z500", kind="contourf") # filled contours
Valid kinds: "auto", "imshow", "pcolormesh", "contour", "contourf".
5. Curvilinear grids#
WRF, ROMS, NEMO and other models store 2-D latitude/longitude arrays rather than 1-D axes. Pass them
explicitly via coords=(x_2d, y_2d) (which forces pcolormesh):
If you don't pass coords, pyramids tries to auto-detect them from the variable's CF coordinates
attribute and from the common model conventions (XLAT/XLONG for WRF, lat_rho/lon_rho for
ROMS, nav_lat/nav_lon for NEMO). When auto-detection falls back to the bounding-box extent
instead, a logging.WARNING is emitted so you know the rendering used the regular-grid path.
6. Faceting (small multiples)#
Render one panel per coordinate value along a dimension with FacetSpec — the panels share a single
colorbar and colour scale:
from pyramids.netcdf import FacetSpec
# one column per month, wrap after 4
nc.plot("t2m", facet=FacetSpec(col="time", col_wrap=4))
# a 2-D grid: time across, level down
nc.plot("t", facet=FacetSpec(col="time", row="level"))
Faceting returns a cleopatra.array_glyph.FacetGrid; iterate grid.axes for per-panel matplotlib
access. A basemap= overlay is applied to every visible panel.
7. Animation#
Animate over a dimension with animate= — pass the dimension name, or True to animate over the
leading non-spatial dimension:
anim = nc.plot("t2m", animate="time") # an ArrayGlyph driving a matplotlib FuncAnimation
anim = nc.plot("t2m", animate=True) # same, when `time` is the leading dim
# combine with selectors to fix the *other* dims first
anim = nc.plot("t", selectors=Selectors(level=500), animate="time")
Pair it with chunks= (see below) and pyramids streams one frame at a time from disk via a
data_getter callback — the whole cube is never held in memory.
8. Lazy / dask reads#
For multi-GB reanalysis files, pass chunks= to switch the read to a dask-backed lazy path; only
the slice (or animation frame) actually rendered is materialised:
nc.plot("t2m", chunks={"time": 1}) # lazy static plot of the leading slice
nc.plot("t2m", animate="time", chunks={"time": 1}) # lazy per-frame streaming
Requires the [lazy] extra (pip install 'pyramids-gis[lazy]'). See
Lazy NetCDF for chunk-size guidance.
9. Basemap underlay#
Overlay a web-tile basemap with basemap=True (OpenStreetMap) or basemap="<provider>":
This routes through cleopatra's tile helpers (cleopatra.tiles.add_tiles), so it needs the [viz]
extra. When faceting, one tile layer is fetched per panel.
10. Escape hatch — raw matplotlib#
NetCDF.plot returns the cleopatra glyph; reach the underlying figure/axes for any customisation
cleopatra doesn't expose:
glyph = nc.plot("t2m", title="2 m temperature")
glyph.ax.set_xlabel("longitude")
glyph.fig.savefig("t2m.png", dpi=200)
What NetCDF.plot does not accept#
The GeoTIFF / Sentinel kwargs from Dataset.plot are rejected on NetCDF.plot with a TypeError
that points at the replacement:
GeoTIFF kwarg (Dataset.plot) |
NetCDF equivalent (NetCDF.plot) |
|---|---|
band=<int> |
variable= + selectors= |
rgb=, surface_reflectance=, cutoff= |
(no NetCDF equivalent — true-colour composites are a raster concept) |
percentile= |
colour=ColourOpts(robust=True) |
overview=, overview_index= |
(NetCDF has no GDAL overview pyramids) |
Under the hood#
NetCDF.plot is a thin facade over pyramids.netcdf._plot.NetCDFPlot, which resolves the variable,
selectors, curvilinear coordinates, facet stack, and animation axis, then hands a 2-D (or stacked)
array to the shared pyramids.dataset._plot_helpers.render_array core — the same renderer used by
Dataset.plot, DatasetCollection.plot (animate mode), and (via mesh_render) UgridDataset.plot.
That core builds a cleopatra ArrayGlyph and dispatches to plot / facet / animate. See the
architecture diagrams.