Reference Data — Coastlines, Borders & Relief#
The cleopatra.basemap.reference module draws public cartographic reference data
underneath your own plot — the cartopy ax.coastlines() /
GeoAxes.stock_img() niche, and the vector/raster sibling of
add_tiles:
add_features— a Natural Earth vector layer:coastline,borders,land,ocean,rivers, orlakes.add_relief— a global hypsometric relief backdrop.
Both fetch a small, fixed public dataset that cleopatra re-hosts as a dependency-light artifact (gzipped GeoJSON / PNG), cache it on disk, and render it with matplotlib. They acquire reference data only — they never read your files and never import GDAL or geopandas.
Dependencies#
add_features in EPSG:4326 needs nothing beyond numpy + matplotlib: the
layers are pre-converted to gzipped GeoJSON and read with the standard library.
Two paths use the optional cleopatra[tiles] extra:
add_reliefdecodes a PNG with Pillow.add_features(..., crs=...)reprojection uses pyproj.
If a required package is missing, the function raises a clear ImportError with
the install hint.
Usage#
Draw a relief backdrop with coastlines and country borders on a global lon/lat (EPSG:4326) map:
import matplotlib
matplotlib.use("Agg") # any backend; Agg shown for headless rendering
import matplotlib.pyplot as plt
from cleopatra.basemap.reference import add_relief, add_features
fig, ax = plt.subplots(figsize=(8, 4))
ax.set_xlim(-180, 180)
ax.set_ylim(-90, 90)
ax.set_aspect("equal") # 1° lon == 1° lat, so the map is not stretched
add_relief(ax, resolution="low") # hypsometric backdrop
add_features(ax, "coastline", "110m", colors="black")
add_features(ax, "borders", "110m", colors="0.4")
fig.savefig("world.png", dpi=100)

A regional map with a filled land layer and higher-resolution coastline:
fig, ax = plt.subplots()
ax.set_xlim(-20, 40) # Europe / N. Africa, in lon/lat
ax.set_ylim(0, 60)
ax.set_aspect("equal")
add_features(ax, "ocean", "50m", facecolors="#bdd7e7")
add_features(ax, "land", "50m", facecolors="0.9", edgecolors="0.5")
add_features(ax, "coastline", "50m", colors="navy", linewidths=0.8)
fig.savefig("europe.png")

If your data is in a projected CRS, pass crs= so the vectors are reprojected
to match (requires pyproj):
The raw data is also available without drawing:
from cleopatra.basemap.reference import natural_earth, natural_earth_polygons, relief
parts = natural_earth("coastline", "110m") # list of (N, 2) lon/lat arrays
rgb = relief("low") # (H, W, 3) uint8 RGB array
natural_earth returns exterior rings only, which is right for line layers and
polygon outlines. For a correct filled polygon on a non-matplotlib tier, use
natural_earth_polygons(layer, resolution) on the polygon layers (land,
ocean, lakes): it returns each polygon as [exterior_ring, *hole_rings], so
ocean's continent-shaped holes and land's lake-shaped holes are preserved rather
than filled over.
To discover the valid arguments, call available_layers() and
available_resolutions() for the vector layers, and
available_relief_resolutions() for the relief products.
Note
add_features / add_relief read the axes' current xlim/ylim and
preserve them, so plot your data first. Polygon layers
(land/ocean/lakes) are drawn as a filled PathCollection with
interior holes cut out (style with facecolors / edgecolors /
linewidths); line layers (coastline/rivers/borders) as a
LineCollection (style with colors / linewidths). Coordinates are
EPSG:4326 unless you pass crs=. Set ax.set_aspect("equal") on lon/lat
maps so degrees render at the same scale (matplotlib's default
aspect="auto" otherwise stretches the map to fill the figure).
Caching
The first call downloads the asset from the cleopatra
basemap-data-v1
release and caches it under ~/.cleopatra/naturalearth; subsequent calls
work offline. Override the location with the CLEOPATRA_CACHE_DIR
environment variable, or discover/resolve it programmatically with
Config.get_cache_dir(). Downloads are restricted to http(s) URLs.
Migrating from pyramids.basemap#
This data used to live in pyramids.basemap (natural_earth / relief). It
has moved to cleopatra.basemap.reference, which is the matplotlib map-decoration layer
— the same boundary the web-tile basemaps already follow (pyramids.basemap
forwarded to cleopatra.basemap.tiles). cleopatra hosts its own copy of the
assets and has no dependency on pyramids.
Old (pyramids.basemap) |
New (cleopatra.basemap.reference) |
Notes |
|---|---|---|
natural_earth(layer, resolution) → FeatureCollection |
natural_earth(layer, resolution) → list[np.ndarray] |
Now returns plain (N, 2) lon/lat arrays (exterior rings for polygons), not a GIS feature object. |
relief(resolution) → GDAL Dataset |
relief(resolution) → (H, W, 3) uint8 array |
Now a NumPy RGB array; the asset is a PNG (no GeoTIFF / GDAL). |
| (draw it yourself) | add_features(ax, layer, resolution) |
New axes helper — the ax.coastlines() analogue. |
| (draw it yourself) | add_relief(ax, resolution) |
New axes helper — the stock_img() analogue. |
PYRAMIDS_CACHE_DIR, ~/.pyramids/naturalearth |
CLEOPATRA_CACHE_DIR, ~/.cleopatra/naturalearth |
Cache env var and default directory renamed. |
The pyramids.basemap.natural_earth / relief entry points are deprecated and
emit a DeprecationWarning; update imports to cleopatra.basemap.reference. Resolutions
are unchanged (110m / 50m / 10m for vectors; low / medium for relief),
as are the six layer names.
Module Documentation#
cleopatra.basemap.reference
#
Reference-basemap backdrops for matplotlib axes.
Two axes-level helpers that draw public cartographic reference data
underneath your own plotted data -- the cartopy
GeoAxes.stock_img() / ax.coastlines() niche, and the vector/raster
sibling of cleopatra.basemap.tiles.add_tiles:
add_relief-- a global hypsometric relief image (thestock_img()analogue).add_features-- a Natural Earth vector layer: coastline, borders, land, ocean, rivers, or lakes (thecoastlines()analogue).
Both fetch a small, fixed public dataset that cleopatra re-hosts as a dependency-light artifact, cache it on disk, and render it with matplotlib. They acquire reference data only; they never read user files and never touch GDAL/geopandas:
- Relief is re-hosted as a plain PNG, so decoding needs only Pillow
(already part of the
cleopatra[tiles]extra). Every relief product is a global EPSG:4326 raster, so its extent is hardcoded rather than read from a geotransform. - Natural Earth layers are pre-converted (offline, maintainer-side) to
gzipped GeoJSON, so reading them needs only the standard library
(
json+gzip) plus numpy. Drawing in EPSG:4326 needs nothing beyond matplotlib; reprojecting to another CRS lazily usespyproj(also in the[tiles]extra).
The cache directory defaults to ~/.cleopatra/naturalearth and can be
overridden with the CLEOPATRA_CACHE_DIR environment variable; it is
resolved by cleopatra.config.Config.get_cache_dir, where the setting is
also discoverable. Downloads are restricted to http(s) URLs.
Examples:
Draw a relief backdrop and a coastline over data plotted in lon/lat (EPSG:4326):
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import matplotlib.pyplot as plt
>>> from cleopatra.basemap.reference import add_relief, add_features
>>> fig, ax = plt.subplots()
>>> ax.set_xlim(-20, 40); ax.set_ylim(0, 60)
>>> _ = add_relief(ax, resolution="low")
>>> _ = add_features(ax, "coastline", "50m")
add_features(ax, layer='coastline', resolution='110m', *, crs=None, zorder=0, **style)
#
Draw a Natural Earth reference layer on an Axes.
The cartopy ax.coastlines() analogue. Polygon layers
(land/ocean/lakes) are drawn hole-aware as a filled
PathCollection (so ocean's continent cut-outs and islands-in-lakes
render correctly); line layers (coastline/rivers/borders) as a
LineCollection. The source data is EPSG:4326; pass crs to reproject
the geometry into the axes' CRS (requires pyproj). The current axis
limits are preserved.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ax
|
Any
|
A matplotlib |
required |
layer
|
str
|
One of |
'coastline'
|
resolution
|
str
|
One of |
'110m'
|
crs
|
int | str | None
|
CRS of the data on |
None
|
zorder
|
int
|
Matplotlib draw order for the layer. |
0
|
**style
|
Any
|
Overrides merged over the per-kind defaults and
forwarded to the underlying collection. Use polygon keys for
polygon layers ( |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
matplotlib.axes.Axes: The same axes, for chaining. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
ImportError
|
If |
ConnectionError
|
If the asset must be downloaded and the fetch fails. |
Examples:
- Overlay a coastline and country borders on a lon/lat map
(downloads each layer on first use, then caches it):
>>> import matplotlib >>> matplotlib.use("Agg") >>> import matplotlib.pyplot as plt >>> from cleopatra.basemap.reference import add_features >>> fig, ax = plt.subplots() >>> ax.set_xlim(-20, 40); ax.set_ylim(0, 60) # doctest: +SKIP >>> ax = add_features(ax, "coastline", "50m", colors="navy") # doctest: +SKIP >>> ax = add_features(ax, "borders", "50m") # doctest: +SKIP - Unknown layers are rejected before any download:
>>> import matplotlib >>> matplotlib.use("Agg") >>> import matplotlib.pyplot as plt >>> from cleopatra.basemap.reference import add_features >>> fig, ax = plt.subplots() >>> add_features(ax, "countries") Traceback (most recent call last): ... ValueError: Unknown layer 'countries'. Choose from ['coastline', 'land', 'ocean', 'rivers', 'lakes', 'borders'].
Source code in src/cleopatra/basemap/reference.py
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 | |
add_relief(ax, resolution='low', *, extent=None, alpha=1.0, zorder=-1, interpolation='bilinear', crs=None)
#
Draw a global hypsometric relief backdrop under existing data.
The cartopy GeoAxes.stock_img() analogue. Assumes the axes are in
EPSG:4326 (lon/lat) unless crs says otherwise, in which case the relief
is warped into that CRS. The current axis limits are preserved so adding
the backdrop never changes the view.
Note
The relief image is equirectangular (EPSG:4326). On an EPSG:4326 axis
(the default, crs=None) a lon/lat extent within the global bounds
is cropped out of the global image and placed over that box, so a
regional call shows that region's terrain rather than the whole world
squashed into it (the recurring #177 footgun). The crop is placed at
its snapped pixel edges so the terrain registers at true scale; the
axis limits (preserved below) then clip to the requested view.
extent=None places the whole globe. An extent OUTSIDE the global
lon/lat bounds on a 4326 axis is instead stretched by imshow to fit
-- visually acceptable for small extents but not a true reprojection.
For a non-EPSG:4326 crs the relief is warped into the axis
CRS (per-pixel inverse reprojection via pyproj) so it lines up under
data plotted in that CRS, exactly like add_features / add_tiles.
The placement box defaults to the current axis view; parts of the box
that fall outside the CRS's domain are left transparent. The box (and
the axis view it defaults from) is assumed non-inverted -- west <
east, south < north. This path needs pyproj (the [tiles] extra).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ax
|
Any
|
A matplotlib |
required |
resolution
|
str
|
|
'low'
|
extent
|
tuple[float, float, float, float] | None
|
|
None
|
alpha
|
float
|
Backdrop opacity in |
1.0
|
zorder
|
int
|
Matplotlib draw order ( |
-1
|
interpolation
|
str
|
Interpolation passed to |
'bilinear'
|
crs
|
int | str | None
|
CRS of the data on the axis. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
matplotlib.axes.Axes: The same axes, for chaining. |
Raises:
| Type | Description |
|---|---|
ImportError
|
If Pillow (the |
TypeError
|
If |
ValueError
|
If |
ConnectionError
|
If the asset must be downloaded and the fetch fails. |
Examples:
- Draw a relief backdrop under data already plotted in lon/lat
(downloads the asset on first use, then caches it):
>>> import matplotlib >>> matplotlib.use("Agg") >>> import matplotlib.pyplot as plt >>> from cleopatra.basemap.reference import add_relief >>> fig, ax = plt.subplots() >>> ax.set_xlim(-180, 180); ax.set_ylim(-90, 90) # doctest: +SKIP >>> ax = add_relief(ax, "low") # doctest: +SKIP >>> len(ax.images) # doctest: +SKIP 1 - Unknown resolutions are rejected before any download:
>>> import matplotlib >>> matplotlib.use("Agg") >>> import matplotlib.pyplot as plt >>> from cleopatra.basemap.reference import add_relief >>> fig, ax = plt.subplots() >>> add_relief(ax, "high") Traceback (most recent call last): ... ValueError: Unknown relief resolution 'high'. Choose from ['low', 'medium'].
Source code in src/cleopatra/basemap/reference.py
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | |
available_layers()
#
Return the Natural Earth layers that can be requested.
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: Valid |
Examples:
>>> from cleopatra.basemap.reference import available_layers
>>> available_layers()
['coastline', 'land', 'ocean', 'rivers', 'lakes', 'borders']
Source code in src/cleopatra/basemap/reference.py
available_relief_resolutions()
#
Return the relief resolutions that can be requested.
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: The valid |
Examples:
>>> from cleopatra.basemap.reference import available_relief_resolutions
>>> available_relief_resolutions()
['low', 'medium']
Source code in src/cleopatra/basemap/reference.py
available_resolutions()
#
Return the supported Natural Earth resolutions.
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: |
Examples:
>>> from cleopatra.basemap.reference import available_resolutions
>>> available_resolutions()
['110m', '50m', '10m']
Source code in src/cleopatra/basemap/reference.py
natural_earth(layer='coastline', resolution='110m')
#
Fetch (and cache) a Natural Earth layer as coordinate arrays.
The layer is downloaded as preprocessed gzipped GeoJSON and parsed
with the standard library only -- no GDAL/geopandas. Coordinates are
EPSG:4326 lon/lat. Polygon layers return exterior rings only; use
natural_earth_polygons for hole-aware coordinate data, or add_features
for hole-aware filled rendering.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer
|
str
|
One of |
'coastline'
|
resolution
|
str
|
One of |
'110m'
|
Returns:
| Type | Description |
|---|---|
list[ndarray]
|
list[numpy.ndarray]: One |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ConnectionError
|
If the asset must be downloaded and the fetch fails. |
Examples:
- Fetch coastlines and inspect the parts (downloads on first use, then reads from the cache):
- Unknown layers are rejected before any download:
Source code in src/cleopatra/basemap/reference.py
natural_earth_polygons(layer, resolution='110m')
#
Fetch (and cache) a Natural Earth polygon layer as hole-aware rings.
The engine-neutral data analogue of add_features: where natural_earth
returns one flat (N, 2) array per part and keeps exterior rings only, this
returns each polygon as [exterior_ring, *hole_rings], so a consumer that is
not matplotlib (a web/MapLibre tier, a 3-D tier, a GeoJSON builder) can draw
a correct filled polygon. The holes matter: ocean polygons carry
continent-shaped holes and land polygons carry lake-shaped holes, so a fill
built from exterior rings alone paints over them.
Only the polygon layers (land, ocean, lakes) are accepted; the line
layers (coastline, rivers, borders) have no fillable interior, so use
natural_earth for those. Coordinates are EPSG:4326 lon/lat and the ring
winding is the source GeoJSON's (not normalised); a renderer that needs a
specific winding should orient the rings itself, as add_features does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
layer
|
str
|
One of the polygon layers in |
required |
resolution
|
str
|
One of |
'110m'
|
Returns:
| Type | Description |
|---|---|
list[list[ndarray]]
|
list[list[numpy.ndarray]]: One entry per polygon part; each entry is
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ConnectionError
|
If the asset must be downloaded and the fetch fails. |
Examples:
- Fetch ocean polygons hole-aware (downloads on first use, then reads from the cache); the 110m ocean comes back as two parts -- a small hole-free polygon and the main ocean, which carries one exterior ring plus a hole per landmass:
- Line layers have no fillable interior and are rejected before any download:
Source code in src/cleopatra/basemap/reference.py
relief(resolution='low')
#
Fetch (and cache) a hypsometric relief product as an RGB array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolution
|
str
|
|
'low'
|
Returns:
| Type | Description |
|---|---|
ndarray
|
numpy.ndarray: An |
Raises:
| Type | Description |
|---|---|
ImportError
|
If Pillow (the |
ValueError
|
If |
ConnectionError
|
If the asset must be downloaded and the fetch fails. |
OSError
|
If the cached file cannot be decoded as an image (the poisoned file is removed first so a retry re-downloads). |
Examples:
- Unknown resolutions raise
ValueErrorbefore any download: