Open In Colab

1.7. Visualization with Matplotlib and Cartopy#

1.7.1. Matplotlib#

images.png

Matplotlib: Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python.

Website: https://matplotlib.org/

GitHub: matplotlib/matplotlib

In the previous notebook, we saw some basic examples of plotting and visualization in the context of learning numpy. In this notebook, we dive much deeper. The goal is to understand how matplotlib represents figures internally.

from matplotlib import pyplot as plt
import numpy as np
%matplotlib inline

1.7.2. Figure and Axes#

The figure is the highest level of organization of matplotlib objects. If we want, we can create a figure explicitly.

fig = plt.figure()
<Figure size 640x480 with 0 Axes>
fig = plt.figure(figsize=(13, 5))
<Figure size 1300x500 with 0 Axes>
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
../_images/0cd2143e1949a3d03de831e735668482a4f44733070ddebdd1e22d739f5001c6.png
fig = plt.figure()
ax = fig.add_axes([0, 0, 0.5, 1])
../_images/3e6a2838f605ea3db0bcc8f26c58707e847655109f9cd88ca20e8c24498d1783.png
fig = plt.figure()
ax1 = fig.add_axes([0, 0, 0.5, 1])
ax2 = fig.add_axes([0.6, 0, 0.3, 0.5], facecolor='g')
../_images/db260ddfaba1b52ba4e884a7c7b59343935badb3a046c32f2dd903361b7712e4.png

1.7.3. Subplots#

Subplot syntax is one way to specify the creation of multiple axes.

fig = plt.figure()
axes = fig.subplots(nrows=2, ncols=3)
../_images/8af4b462ad9f0cd812e6e3c765a0a02e9911bad39c293f76a56cd3e0cb4a7b84.png
fig = plt.figure(figsize=(12, 6))
axes = fig.subplots(nrows=2, ncols=3)
../_images/beeada7066a1f33b64013e9bb4ccd4a27face0ca263fe20aff25a212a1b7ed9f.png
axes
array([[<Axes: >, <Axes: >, <Axes: >],
       [<Axes: >, <Axes: >, <Axes: >]], dtype=object)

There is a shorthand for doing this all at once, which is our recommended way to create new figures!

fig, ax = plt.subplots()
../_images/28903d5f9c20c3c36c1bd5299b135fc7340b2a8c04e459444f081bed1795869c.png
fig, axes = plt.subplots(ncols=2, figsize=(8, 4), subplot_kw={'facecolor': 'g'})
../_images/4c1d8777a599aa227e68b4528ef68592589bc195d0ea0edf37f9f6dc20244f78.png
axes
array([<Axes: >, <Axes: >], dtype=object)

1.7.4. Drawing into Axes#

All plots are drawn into axes. It is easiest to understand how matplotlib works if you use the object-oriented style.

# create some data to plot
import numpy as np
x = np.linspace(-np.pi, np.pi, 100)
y = np.cos(x)
z = np.sin(6*x)
fig, ax = plt.subplots()
ax.plot(x, y)
[<matplotlib.lines.Line2D at 0x126713740>]
../_images/c91225832ed42c335a26cde323642011d9da54def57e825a1e973f12be0e5649.png

This does the same thing as

plt.plot(x, y)
[<matplotlib.lines.Line2D at 0x126c2fe30>]
../_images/c91225832ed42c335a26cde323642011d9da54def57e825a1e973f12be0e5649.png

This starts to matter when we have multiple axes to worry about.

fig, axes = plt.subplots(figsize=(8, 4), ncols=2)
ax0, ax1 = axes
ax0.plot(x, y)
ax1.plot(x, z)
[<matplotlib.lines.Line2D at 0x126d31880>]
../_images/5eaecbd54918943b5cd4de6abfa4565d5261ab8beb92ec2e9f7af02453ef4207.png

1.7.5. Labeling Plots#

fig, axes = plt.subplots(figsize=(8, 4), ncols=2)
ax0, ax1 = axes

ax0.plot(x, y)
ax0.set_xlabel('x')
ax0.set_ylabel('y')
ax0.set_title('x vs. y')

ax1.plot(x, z)
ax1.set_xlabel('x')
ax1.set_ylabel('z')
ax1.set_title('x vs. z')

# squeeze everything in
plt.tight_layout()
../_images/5f26b01ee73a2c679a23b5b23dc205195b6d797dba91a2f7d9366e0834cde9a1.png

1.7.6. Customizing Line Plots#

fig, ax = plt.subplots()
ax.plot(x, y, x, z)
[<matplotlib.lines.Line2D at 0x1268c5a90>,
 <matplotlib.lines.Line2D at 0x1268c60f0>]
../_images/bbeaf11dbfbdfcb62249dff55a3494899d04468d09db7924fc807814765c0cf3.png
# It’s simple to switch axes
fig, ax = plt.subplots()
ax.plot(y, x, z, x)
[<matplotlib.lines.Line2D at 0x12678aea0>,
 <matplotlib.lines.Line2D at 0x1267ba0f0>]
../_images/927c5a9c6d5708757fb6b6a8f160c53d9b76f1f75f9cd089f4914d2f0521a8c6.png

A “parametric” graph:

fig, ax = plt.subplots()
ax.plot(y, z)
[<matplotlib.lines.Line2D at 0x12675b7d0>]
../_images/0f72a30c6bf1f055f95a14f7c6482a78b2a8730e4a7f60f5594c7341817c3bfd.png

1.7.6.1. Line Styles#

fig, axes = plt.subplots(figsize=(16, 5), ncols=3)
axes[0].plot(x, y, linestyle='dashed')
axes[0].plot(x, z, linestyle='--')

axes[1].plot(x, y, linestyle='dotted')
axes[1].plot(x, z, linestyle=':')

axes[2].plot(x, y, linestyle='dashdot', linewidth=5)
axes[2].plot(x, z, linestyle='-.', linewidth=0.5)
[<matplotlib.lines.Line2D at 0x10be3c500>]
../_images/b810cb2056df1321b9c817f4a15bbf79d378c51680c15c611f25ca1c45614de2.png

1.7.6.2. Colors#

As described in the colors documentation, there are some special codes for commonly used colors:

  • b: blue

  • g: green

  • r: red

  • c: cyan

  • m: magenta

  • y: yellow

  • k: black

  • w: white

fig, ax = plt.subplots()
ax.plot(x, y, color='k')
ax.plot(x, z, color='r')
[<matplotlib.lines.Line2D at 0x126ed9820>]
../_images/b86c2b8ff0dd4b3818aaaf2cc4740a0609dbaa5f9100f527a7127ed0a8a2a25c.png
# Other ways to specify colors:

fig, axes = plt.subplots(figsize=(16, 5), ncols=3)

# grayscale
axes[0].plot(x, y, color='0.8')
axes[0].plot(x, z, color='0.2')

# RGB tuple
axes[1].plot(x, y, color=(1, 0, 0.7))
axes[1].plot(x, z, color=(0, 0.4, 0.3))

# HTML hex code
axes[2].plot(x, y, color='#00dcba')
axes[2].plot(x, z, color='#b029ee')
[<matplotlib.lines.Line2D at 0x126fae4b0>]
../_images/06212adbffaf7568de7cca506282c7d85114bbbc51e4796e7f4504a38dc60631.png

There is a default color cycle built into matplotlib.

plt.rcParams['axes.prop_cycle']
'color'
'#1f77b4'
'#ff7f0e'
'#2ca02c'
'#d62728'
'#9467bd'
'#8c564b'
'#e377c2'
'#7f7f7f'
'#bcbd22'
'#17becf'
fig, ax = plt.subplots(figsize=(12, 10))
for factor in np.linspace(0.2, 1, 11):
    ax.plot(x, factor*y)
../_images/e3c4937f2ca391f5e8e7ec05dffb168dbdde71d236358a13c0b0f34b04401ca8.png

1.7.6.3. Markers#

There are lots of different markers availabile in matplotlib!

fig, axes = plt.subplots(figsize=(12, 5), ncols=2)

axes[0].plot(x[:20], y[:20], marker='.')
axes[0].plot(x[:20], z[:20], marker='o')

axes[1].plot(x[:20], z[:20], marker='^',
             markersize=10, markerfacecolor='r',
             markeredgecolor='k')
[<matplotlib.lines.Line2D at 0x127163020>]
../_images/d6d6747df055d28a87a639787b7bc1ccdf9bd9e4dc164c726d134d7b1415a975.png

1.7.6.4. Label, Ticks, and Gridlines#

fig, ax = plt.subplots(figsize=(12, 7))
ax.plot(x, y)

ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title(r'A complicated math function: $f(x) = \cos(x)$')

ax.set_xticks(np.pi * np.array([-1, 0, 1]))
ax.set_xticklabels([r'$-\pi$', '0', r'$\pi$'])
ax.set_yticks([-1, 0, 1])

ax.set_yticks(np.arange(-1, 1.1, 0.2), minor=True)
#ax.set_xticks(np.arange(-3, 3.1, 0.2), minor=True)

ax.grid(which='minor', linestyle='--')
ax.grid(which='major', linewidth=2)
../_images/0b882b5dddff1007e1902a195c4317ef98e881840e41a51d526dc6dc241dbcb3.png

1.7.6.5. Axis Limits#

fig, ax = plt.subplots()
ax.plot(x, y, x, z)
ax.set_xlim(-5, 5)
ax.set_ylim(-3, 3)
(-3.0, 3.0)
../_images/81d319d7829033cd33469316dc56db42c667afebfd818cd949416615db35156c.png

1.7.6.6. Text Annotations#

fig, ax = plt.subplots()
ax.plot(x, y)
ax.text(-3, 0.3, 'hello world')
ax.annotate('the maximum', xy=(0, 1),
             xytext=(0, 0), arrowprops={'facecolor': 'k'})
Text(0, 0, 'the maximum')
../_images/8e4243d1b0330ddf7b2d7bebddf9e7cf47136cad9a85aedf7dd9f23bef963549.png

1.7.6.7. Scatter Plots#

fig, ax = plt.subplots()

splot = ax.scatter(y, z, c=x, s=(100*z**2 + 5))
fig.colorbar(splot)
<matplotlib.colorbar.Colorbar at 0x12732e900>
../_images/0aa568fcf54c98ed7d2550d68075c642260bb5fd038e653464739ed3938210c0.png

1.7.6.8. Bar Plots#

labels = ['first', 'second', 'third']
values = [10, 5, 30]

fig, axes = plt.subplots(figsize=(10, 5), ncols=2)
axes[0].bar(labels, values)
axes[1].barh(labels, values)
<BarContainer object of 3 artists>
../_images/c7f9ca1540679f81a9eee7a4c71b4a412148f0dad4b9bd62760f84eebaeb88f5.png

1.7.7. 2D Plotting Methods#

1.7.7.1. imshow#

## imshow
x1d = np.linspace(-2*np.pi, 2*np.pi, 100)
y1d = np.linspace(-np.pi, np.pi, 50)
xx, yy = np.meshgrid(x1d, y1d)
f = np.cos(xx) * np.sin(yy)
print(f.shape)
(50, 100)
fig, ax = plt.subplots(figsize=(12,4), ncols=2)
ax[0].imshow(f)
ax[1].imshow(f, origin='lower')
<matplotlib.image.AxesImage at 0x126a4a570>
../_images/c586420a97013117100b1a665366e55994d268624df743c9f257f26d0305f398.png

1.7.7.2. pcolormesh#

fig, ax = plt.subplots(ncols=2, figsize=(12, 5))
pc0 = ax[0].pcolormesh(x1d, y1d, f)
pc1 = ax[1].pcolormesh(xx, yy, f)
fig.colorbar(pc0, ax=ax[0])
fig.colorbar(pc1, ax=ax[1])
<matplotlib.colorbar.Colorbar at 0x12753f2c0>
../_images/b21415a1cdc1ca3688126fc1e215daa37ad5a5f8bdb5df32ebd7316258cc3614.png
x_sm, y_sm, f_sm = xx[:10, :10], yy[:10, :10], f[:10, :10]

fig, ax = plt.subplots(figsize=(12,5), ncols=2)

# last row and column ignored!
ax[0].pcolormesh(x_sm, y_sm, f_sm, edgecolors='k')

# same!
ax[1].pcolormesh(x_sm, y_sm, f_sm[:-1, :-1], edgecolors='k')
<matplotlib.collections.QuadMesh at 0x1276128a0>
../_images/1e85099809a32cb7ac471e233a66e665cbb238a0382be5a13ab36c4e7ec1472e.png
y_distorted = y_sm*(1 + 0.1*np.cos(6*x_sm))

plt.figure(figsize=(12,6))
plt.pcolormesh(x_sm, y_distorted, f_sm[:-1, :-1], edgecolors='w')
plt.scatter(x_sm, y_distorted, c='k')
<matplotlib.collections.PathCollection at 0x1276c0d40>
../_images/d0a768a42af7424b7b27df74d5bf5785364e2c59c6fcd9a93ba56ea6cdcbcee7.png

1.7.7.3. contour / contourf#

fig, ax = plt.subplots(figsize=(12, 5), ncols=2)

# same thing!
ax[0].contour(x1d, y1d, f)
ax[1].contour(xx, yy, f)
<matplotlib.contour.QuadContourSet at 0x12756caa0>
../_images/e34ef8d951b1ed86db508d3971c0623580fb0c7716f7fca8cb28a5f5e34d3b55.png
fig, ax = plt.subplots(figsize=(12, 5), ncols=2)

c0 = ax[0].contour(xx, yy, f, 5)
c1 = ax[1].contour(xx, yy, f, 20)

plt.clabel(c0, fmt='%2.1f')
plt.colorbar(c1, ax=ax[1])
<matplotlib.colorbar.Colorbar at 0x127275430>
../_images/16ddd836f5dd31e7e021a8eb2daaa28d9c42475d0e1588ea8938c552798061ed.png
fig, ax = plt.subplots(figsize=(12, 5), ncols=2)

clevels = np.arange(-1, 1, 0.2) + 0.1

cf0 = ax[0].contourf(xx, yy, f, clevels, cmap='RdBu_r', extend='both')
cf1 = ax[1].contourf(xx, yy, f, clevels, cmap='inferno', extend='both')

fig.colorbar(cf0, ax=ax[0])
fig.colorbar(cf1, ax=ax[1])
<matplotlib.colorbar.Colorbar at 0x127b50e90>
../_images/b5520af703baf49531f11f964d73c863f3d820a99212dbd68bb26717f922c5f5.png

1.7.7.4. quiver#

u = -np.cos(xx) * np.cos(yy)
v = -np.sin(xx) * np.sin(yy)

fig, ax = plt.subplots(figsize=(12, 7))
ax.contour(xx, yy, f, clevels, cmap='RdBu_r', extend='both', zorder=0)
ax.quiver(xx[::4, ::4], yy[::4, ::4],
           u[::4, ::4], v[::4, ::4], zorder=1)
<matplotlib.quiver.Quiver at 0x12783ae70>
../_images/f5f674b9ecbf938ec825201e91823c3587aafb5dc7a6f1dffa3eab3579c3e6ee.png

1.7.7.5. streamplot#

fig, ax = plt.subplots(figsize=(12, 7))
ax.streamplot(xx, yy, u, v, density=2, color=(u**2 + v**2))
<matplotlib.streamplot.StreamplotSet at 0x1270e6d20>
../_images/633de1ed184e0c2774ff9d16ce60d2a3295b0720a28ee0706f9038db09b478d8.png

1.7.8. Cartopy#

cartopy.png

Making maps is a fundamental part of geoscience research. Maps differ from regular figures in the following principle ways:

  • Maps require a projection of geographic coordinates on the 3D Earth to the 2D space of your figure.

  • Maps often include extra decorations besides just our data (e.g. continents, country borders, etc.)

Mapping is a notoriously hard and complicated problem, mostly due to the complexities of projection.

In this lecture, we will learn about Cartopy, one of the most common packages for making maps within python. Another popular and powerful library is Basemap; however, Basemap is going away and being replaced with Cartopy in the near future. For this reason, new python learners are recommended to learn Cartopy.

A lot of the material in this lesson was adopted from Phil Elson’s excellent Cartopy Tutorial. Phil is the creator of Cartopy and published his tutorial under an open license.

1.7.9. Background: Projections#

Most of our media for visualization are flat. Our two most common media are flat:

  • Paper

  • Screen

flat_medium.jpg

1.7.10. Introducing Cartopy#

Cartopy makes use of the powerful PROJ, numpy and shapely libraries and includes a programatic interface built on top of matplotlib for the creation of publication quality maps.

Key features of cartopy are its object oriented projection definitions, and its ability to transform points, lines, vectors, polygons and images between those projections.

1.7.10.1. Cartopy Projections and Other Reference Systems#

In Cartopy, each projection is a class. Most classes of projection can be configured in projection-specific ways, although Cartopy takes an opinionated stance on sensible defaults.

Let’s create a Plate Carree projection instance.

To do so, we need cartopy’s crs module. This is typically imported as ccrs (Cartopy Coordinate Reference Systems).

First, let’s install cartopy to use it in this Google Colab notebook. 💻 We have to first install shapely without binary, which hopefully should prevent your Google Colab notebook from crashing.

!pip install --no-binary 'shapely==1.6.4' 'shapely==1.6.4' --force
!pip install cartopy # Install latest version of cartopy
Collecting shapely==1.6.4
  Using cached Shapely-1.6.4.tar.gz (224 kB)
  Preparing metadata (setup.py) ... ?25l- error
  error: subprocess-exited-with-error
  
  × python setup.py egg_info did not run successfully.
   exit code: 1
  ╰─> [13 lines of output]
      Failed `CDLL(/Library/Frameworks/GEOS.framework/Versions/Current/GEOS)`
      Failed `CDLL(/opt/local/lib/libgeos_c.dylib)`
      Traceback (most recent call last):
        File "<string>", line 2, in <module>
        File "<pip-setuptools-caller>", line 34, in <module>
        File "/private/var/folders/7l/4c3bq00s0r994qdpmp2gfm6r0000gq/T/pip-install-zetozgn2/shapely_ce2ccbd32f6648d0be42a499a2f447bf/setup.py", line 80, in <module>
          from shapely._buildcfg import geos_version_string, geos_version, \
        File "/private/var/folders/7l/4c3bq00s0r994qdpmp2gfm6r0000gq/T/pip-install-zetozgn2/shapely_ce2ccbd32f6648d0be42a499a2f447bf/shapely/_buildcfg.py", line 185, in <module>
          lgeos = load_dll('geos_c', fallbacks=alt_paths)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/private/var/folders/7l/4c3bq00s0r994qdpmp2gfm6r0000gq/T/pip-install-zetozgn2/shapely_ce2ccbd32f6648d0be42a499a2f447bf/shapely/_buildcfg.py", line 159, in load_dll
          raise OSError(
      OSError: Could not find library geos_c or load any of its variants ['/Library/Frameworks/GEOS.framework/Versions/Current/GEOS', '/opt/local/lib/libgeos_c.dylib']
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: metadata-generation-failed

× Encountered error while generating package metadata.
╰─> See above for output.

note: This is an issue with the package mentioned above, not pip.
hint: See above for details.
?25h
Collecting cartopy
  Using cached cartopy-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl.metadata (6.1 kB)
Requirement already satisfied: numpy>=1.23 in /Users/itam/miniconda3/lib/python3.12/site-packages (from cartopy) (2.3.1)
Requirement already satisfied: matplotlib>=3.6 in /Users/itam/miniconda3/lib/python3.12/site-packages (from cartopy) (3.10.3)
Requirement already satisfied: shapely>=2.0 in /Users/itam/miniconda3/lib/python3.12/site-packages (from cartopy) (2.1.1)
Requirement already satisfied: packaging>=21 in /Users/itam/miniconda3/lib/python3.12/site-packages (from cartopy) (23.2)
Requirement already satisfied: pyshp>=2.3 in /Users/itam/miniconda3/lib/python3.12/site-packages (from cartopy) (2.3.1)
Collecting pyproj>=3.3.1 (from cartopy)
  Using cached pyproj-3.7.2.tar.gz (226 kB)
  Installing build dependencies ... ?25l-
 \
 |
 /
 done
?25h  Getting requirements to build wheel ... ?25l-
 error
  error: subprocess-exited-with-error
  
  × Getting requirements to build wheel did not run successfully.
   exit code: 1
  ╰─> [1 lines of output]
      proj executable not found. Please set the PROJ_DIR variable. For more information see: https://pyproj4.github.io/pyproj/stable/installation.html
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error

× Getting requirements to build wheel did not run successfully.
 exit code: 1
╰─> See above for output.

note: This error originates from a subprocess, and is likely not a problem with pip.
?25h
import cartopy.crs as ccrs
import cartopy
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[43], line 1
----> 1 import cartopy.crs as ccrs
      2 import cartopy

ModuleNotFoundError: No module named 'cartopy'

Cartopy’s projection list tells us that the Plate Carree projection is available with the ccrs.PlateCarree class:

Note: we need to instantiate the class in order to do anything related to projections with it!

ccrs.PlateCarree()

1.7.10.2. Drawing a map#

Cartopy optionally depends upon matplotlib, and each projection knows how to create a matplotlib Axes (or AxesSubplot) that can represent itself.

The Axes that the projection creates is a cartopy.mpl.geoaxes.GeoAxes. This Axes subclass overrides some of matplotlib’s existing methods, and adds a number of extremely useful ones for drawing maps.

We’ll go back and look at those methods shortly, but first, let’s actually see the cartopy+matplotlib dance in action:

%matplotlib inline
import matplotlib.pyplot as plt

plt.axes(projection=ccrs.PlateCarree())

That was a little underwhelming, but we can see that the Axes created is indeed one of those GeoAxes[Subplot] instances.

One of the most useful methods that this class adds on top of the standard matplotlib Axes class is the coastlines method. With no arguments, it will add the Natural Earth 1:110,000,000 scale coastline data to the map.

plt.figure()
ax = plt.axes(projection=ccrs.PlateCarree())
ax.coastlines()

We could have also created a matplotlib subplot with one of the many approaches that exist. For example, the plt.subplots function could be used:

fig, ax = plt.subplots(subplot_kw={'projection': ccrs.PlateCarree()})
ax.coastlines()

Projection classes have options we can use to customize the map

ccrs.PlateCarree?
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()

1.7.10.3. Useful Methods of a GeoAxes#

The cartopy.mpl.geoaxes.GeoAxes class adds a number of useful methods.

Let’s take a look at:

  • set_global - zoom the map out as much as possible

  • set_extent - zoom the map to the given bounding box

  • gridlines - add gridlines (and optionally labels) to the axes

  • coastlines - add Natural Earth coastlines to the axes

  • stock_img - add a low-resolution Natural Earth background image to the axes

  • imshow - add an image (numpy array) to the axes

  • add_geometries - add a collection of geometries (Shapely) to the axes

1.7.10.4. Some More Examples of Different Global Projections#

projections = [ccrs.PlateCarree(),
               ccrs.Robinson(),
               ccrs.Mercator(),
               ccrs.Orthographic(),
               ccrs.InterruptedGoodeHomolosine()
              ]


for proj in projections:
    plt.figure()
    ax = plt.axes(projection=proj)
    ax.stock_img()
    ax.coastlines()
    ax.set_title(f'{type(proj)}')

1.7.10.5. Regional Maps#

To create a regional map, we use the set_extent method of GeoAxis to limit the size of the region.

central_lon, central_lat = -10, 45
extent = [-40, 20, 30, 60]
ax = plt.axes(projection=ccrs.Orthographic(central_lon, central_lat))
ax.set_extent(extent)
ax.gridlines()
ax.coastlines(resolution='50m')

1.7.11. Adding Features to the Map#

To give our map more styles and details, we add cartopy.feature objects. Many useful features are built in. These “default features” are at coarse (110m) resolution.

Name

Description

cartopy.feature.BORDERS

Country boundaries

cartopy.feature.COASTLINE

Coastline, including major islands

cartopy.feature.LAKES

Natural and artificial lakes

cartopy.feature.LAND

Land polygons, including major islands

cartopy.feature.OCEAN

Ocean polygons

cartopy.feature.RIVERS

Single-line drainages, including lake centerlines

Below we illustrate these features in a customized map of North America.

import cartopy.feature as cfeature
import numpy as np

central_lat = 37.5
central_lon = -96
extent = [-120, -70, 24, 50.5]
central_lon = np.mean(extent[:2])
central_lat = np.mean(extent[2:])

plt.figure(figsize=(12, 6))
ax = plt.axes(projection=ccrs.AlbersEqualArea(central_lon, central_lat))
ax.set_extent(extent)

ax.add_feature(cartopy.feature.OCEAN)
ax.add_feature(cartopy.feature.LAND, edgecolor='black')
ax.add_feature(cartopy.feature.LAKES, edgecolor='black')
ax.add_feature(cartopy.feature.RIVERS)
ax.gridlines()

If we want higher-resolution features, Cartopy can automatically download and create them from the Natural Earth Data database or the GSHHS dataset database.

rivers_50m = cfeature.NaturalEarthFeature('physical', 'rivers_lake_centerlines', '50m')

plt.figure(figsize=(12, 6))
ax = plt.axes(projection=ccrs.AlbersEqualArea(central_lon, central_lat))
ax.set_extent(extent)

ax.add_feature(cartopy.feature.OCEAN)
ax.add_feature(cartopy.feature.LAND, edgecolor='black')
ax.add_feature(rivers_50m, facecolor='None', edgecolor='b')
ax.gridlines()

1.7.12. Adding Data to the Map#

Now that we know how to create a map, let’s add our data to it! That’s the whole point.

Because our map is a matplotlib axis, we can use all the familiar maptplotlib commands to make plots. By default, the map extent will be adjusted to match the data. We can override this with the .set_global or .set_extent commands.

# create some test data
new_york = dict(lon=-74.0060, lat=40.7128)
honolulu = dict(lon=-157.8583, lat=21.3069)
lons = [new_york['lon'], honolulu['lon']]
lats = [new_york['lat'], honolulu['lat']]

Key point: the data also have to be transformed to the projection space.

This is done via the transform= keyword in the plotting method. The argument is another cartopy.crs object. If you don’t specify a transform, Cartopy assume that the data is using the same projection as the underlying GeoAxis.

From the Cartopy Documentation:

“The core concept is that the projection of your axes is independent of the coordinate system your data is defined in. The projection argument is used when creating plots and determines the projection of the resulting plot (i.e. what the plot looks like). The transform argument to plotting functions tells Cartopy what coordinate system your data are defined in.”

ax = plt.axes(projection=ccrs.PlateCarree())
ax.plot(lons, lats, label='Equirectangular straight line')
ax.plot(lons, lats, label='Great Circle', transform=ccrs.Geodetic())
ax.coastlines()
ax.legend()
ax.set_global()

1.7.12.1. Plotting 2D (Raster) Data#

The same principles apply to 2D data. Below we create some example data defined in regular lat / lon coordinates.

import numpy as np
lon = np.linspace(-80, 80, 25)
lat = np.linspace(30, 70, 25)
lon2d, lat2d = np.meshgrid(lon, lat)
data = np.cos(np.deg2rad(lat2d) * 4) + np.sin(np.deg2rad(lon2d) * 4)
plt.contourf(lon2d, lat2d, data)

Now we create a PlateCarree projection and plot the data on it without any transform keyword. This happens to work because PlateCarree is the simplest projection of lat / lon data.

ax = plt.axes(projection=ccrs.PlateCarree())
ax.set_global()
ax.coastlines()
ax.contourf(lon, lat, data)

However, if we try the same thing with a different projection, we get the wrong result.

projection = ccrs.RotatedPole(pole_longitude=-177.5, pole_latitude=37.5)
ax = plt.axes(projection=projection)
ax.set_global()
ax.coastlines()
ax.contourf(lon, lat, data)

To fix this, we need to pass the correct transform argument to contourf:

projection = ccrs.RotatedPole(pole_longitude=-177.5, pole_latitude=37.5)
ax = plt.axes(projection=projection)
ax.set_global()
ax.coastlines()
ax.contourf(lon, lat, data, transform=ccrs.PlateCarree())

1.7.12.2. Showing Images#

We can plot a satellite image easily on a map if we know its extent

import pooch
img = pooch.retrieve('https://unils-my.sharepoint.com/:u:/g/personal/tom_beucler_unil_ch/ER4UQukLUFBLjaLKpR_df48BDq1pVvmZqvs3uSyZv4q_RA?download=1',
                          known_hash='caede90c5d3a0628d4f63d486351bcff35749858d45cd2f7b471d495e40fe493',
                          processor=pooch.Unzip()
                          )
fig = plt.figure(figsize=(8, 12))

img_extent = (-120.67660000000001, -106.32104523100001, 13.2301484511245, 30.766899999999502)
img = plt.imread(img[0])

ax = plt.axes(projection=ccrs.PlateCarree())

# set a margin around the data
ax.set_xmargin(0.05)
ax.set_ymargin(0.10)

# add the image. Because this image was a tif, the "origin" of the image is in the
# upper left corner
ax.imshow(img, origin='upper', extent=img_extent, transform=ccrs.PlateCarree())
ax.coastlines(resolution='50m', color='black', linewidth=1)

# mark a known place to help us geo-locate ourselves
ax.plot(-117.1625, 32.715, 'bo', markersize=7, transform=ccrs.Geodetic())
ax.text(-117, 33, 'San Diego', transform=ccrs.Geodetic())

1.7.13. Bonus: Xarray Integration#

Cartopy transforms can be passed to xarray! This creates a very quick path for creating professional looking maps from netCDF data.

import pooch
import urllib.request
datafile = pooch.retrieve('https://unils-my.sharepoint.com/:u:/g/personal/tom_beucler_unil_ch/EdY_7ttKxltLp-u6r1MpDggBvqGb355_VecnfPRLEIsMog?download=1',
                          known_hash='af8ff05bfeb8da2ec763773bfcc06112f20cb7167613359c98a2b80313c45b73')
import xarray as xr
ds = xr.open_dataset(datafile, drop_variables=['time_bnds'])
ds
sst = ds.sst.sel(time='2000-01-01', method='nearest')
fig = plt.figure(figsize=(9,6))
ax = plt.axes(projection=ccrs.Robinson())
ax.coastlines()
ax.gridlines()
sst.plot(ax=ax, transform=ccrs.PlateCarree(),
         vmin=2, vmax=30, cbar_kwargs={'shrink': 0.4})

1.7.14. Doing More#

Browse the Cartopy Gallery to learn about all the different types of data and plotting methods available!