Making multi-panel plots using Cartopy

This notebook will demonstrate how to make multi-panel contour plots with Cartopy, including adding a single colorbar and title for all panels.

Data

The Subseasonal Experiment (SubX)

Further information on SubX is available from Pegion et al. 2019 and the SubX project website

The SubX public database is hosted on the International Research Institute for Climate and Society (IRI) data server http://iridl.ldeo.columbia.edu/SOURCES/.Models/.SubX/

SubX Forecasts are hosted locally on COLA computers at the following location: /shared/subx/forecast/weekly/

Specifically, we will use the forecast for 2m temperatures anomalies made on Apr 16, 2020, located in: /shared/subx/forecast/weekly/20200416/data/fcst_20200416.anom.tas_2m.nc

[77]:
import xarray as xr
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

import cartopy.feature as cfeature
import cartopy.crs as ccrs
import cartopy.mpl.ticker as cticker
from cartopy.util import add_cyclic_point

Set the path and filename

[78]:
path='/shared/subx/forecast/weekly/20200416/data/'
fname='fcst_20200416.anom.tas_2m.nc'
[79]:
ds=xr.open_dataset(path+fname)

This dataset contains weekly forecasts for 2m temperature anomalies from each of the SubX Models and the multi-model ensemble (MME). These are listed as the Data Variables. The forecats have 4 times, corresponding to weeks 1, 2, 3, and 4.

[80]:
ds
[80]:
Show/Hide data repr Show/Hide attributes
xarray.Dataset
    • lat: 181
    • lon: 360
    • time: 4
    • lat
      (lat)
      float32
      -90.0 -89.0 -88.0 ... 89.0 90.0
      standard_name :
      latitude
      long_name :
      latitude
      units :
      degrees_north
      array([-90., -89., -88., -87., -86., -85., -84., -83., -82., -81., -80., -79.,
             -78., -77., -76., -75., -74., -73., -72., -71., -70., -69., -68., -67.,
             -66., -65., -64., -63., -62., -61., -60., -59., -58., -57., -56., -55.,
             -54., -53., -52., -51., -50., -49., -48., -47., -46., -45., -44., -43.,
             -42., -41., -40., -39., -38., -37., -36., -35., -34., -33., -32., -31.,
             -30., -29., -28., -27., -26., -25., -24., -23., -22., -21., -20., -19.,
             -18., -17., -16., -15., -14., -13., -12., -11., -10.,  -9.,  -8.,  -7.,
              -6.,  -5.,  -4.,  -3.,  -2.,  -1.,   0.,   1.,   2.,   3.,   4.,   5.,
               6.,   7.,   8.,   9.,  10.,  11.,  12.,  13.,  14.,  15.,  16.,  17.,
              18.,  19.,  20.,  21.,  22.,  23.,  24.,  25.,  26.,  27.,  28.,  29.,
              30.,  31.,  32.,  33.,  34.,  35.,  36.,  37.,  38.,  39.,  40.,  41.,
              42.,  43.,  44.,  45.,  46.,  47.,  48.,  49.,  50.,  51.,  52.,  53.,
              54.,  55.,  56.,  57.,  58.,  59.,  60.,  61.,  62.,  63.,  64.,  65.,
              66.,  67.,  68.,  69.,  70.,  71.,  72.,  73.,  74.,  75.,  76.,  77.,
              78.,  79.,  80.,  81.,  82.,  83.,  84.,  85.,  86.,  87.,  88.,  89.,
              90.], dtype=float32)
    • lon
      (lon)
      float32
      0.0 1.0 2.0 ... 357.0 358.0 359.0
      standard_name :
      longitude
      long_name :
      longitude
      units :
      degrees_east
      array([  0.,   1.,   2., ..., 357., 358., 359.], dtype=float32)
    • time
      (time)
      datetime64[ns]
      2020-04-24 ... 2020-05-15
      standard_name :
      time
      long_name :
      Time of measurements
      array(['2020-04-24T00:00:00.000000000', '2020-05-01T00:00:00.000000000',
             '2020-05-08T00:00:00.000000000', '2020-05-15T00:00:00.000000000'],
            dtype='datetime64[ns]')
    • GEOS_V2p1
      (time, lat, lon)
      float32
      ...
      name :
      GEOS_V2p1
      long_name :
      GMAO-GEOS_V2p1 20200411
      units :
      degC
      [260640 values with dtype=float32]
    • CCSM4
      (time, lat, lon)
      float32
      ...
      name :
      CCSM4
      long_name :
      RSMAS-CCSM4 20200412
      units :
      degC
      [260640 values with dtype=float32]
    • FIMr1p1
      (time, lat, lon)
      float32
      ...
      name :
      FIMr1p1
      long_name :
      ESRL-FIMr1p1 20200415
      units :
      degC
      [260640 values with dtype=float32]
    • GEPS6
      (time, lat, lon)
      float32
      ...
      name :
      GEPS6
      long_name :
      ECCC-GEPS6 20200416
      units :
      degC
      [260640 values with dtype=float32]
    • NESM
      (time, lat, lon)
      float32
      ...
      name :
      NESM
      long_name :
      NRL-NESM 20200414 20200413 20200412 20200411
      units :
      degC
      [260640 values with dtype=float32]
    • GEFS
      (time, lat, lon)
      float32
      ...
      name :
      GEFS
      long_name :
      EMC-GEFS 20200415
      units :
      degC
      [260640 values with dtype=float32]
    • MME
      (time, lat, lon)
      float32
      ...
      name :
      MME
      long_name :
      MME
      units :
      degC
      [260640 values with dtype=float32]
  • title :
    SubX Weekly Forecast Anomalies
    long_title :
    SubX Weekly Forecast Anomalies
    comments :
    SubX project http://cola.gmu.edu/~kpegion/subx/
    institution :
    IRI
    source :
    SubX IRI
    CreationDate :
    2020/04/17 08:55:49
    CreatedBy :
    rfreelan
    MatlabSource :
    calcAnomsFCST

Following our previous example (Making maps using Cartopy), we can make a map of the Multi-model Ensemble week 1 forecast using Cartopy.

[81]:
fig = plt.figure(figsize=(11,8.5))

# Set the axes using the specified map projection
ax=plt.axes(projection=ccrs.PlateCarree())

# Choose the Multi-model ensemble (MME) and week1 (time index of 0)
data=ds['MME'][0,:,:]

# Add cyclic point to data
data, lons = add_cyclic_point(data, coord=ds['lon'])


# Make a filled contour plot
cs=ax.contourf(lons, ds['lat'], data,
            transform = ccrs.PlateCarree(),cmap='coolwarm',extend='both')

# Add coastlines
ax.coastlines()

# Define the xticks for longitude
ax.set_xticks(np.arange(-180,181,60), crs=ccrs.PlateCarree())
lon_formatter = cticker.LongitudeFormatter()
ax.xaxis.set_major_formatter(lon_formatter)

# Define the yticks for latitude
ax.set_yticks(np.arange(-90,91,30), crs=ccrs.PlateCarree())
lat_formatter = cticker.LatitudeFormatter()
ax.yaxis.set_major_formatter(lat_formatter)

# Add colorbar
cbar = plt.colorbar(cs)
../_images/examples_multi-panel-cartopy_11_0.png

Single page with multi-panels

Now let’s make a single page showing the forecast for week 1 for each model. To do that, we will need to divide the page into subplots using plt.subplots.

  1. We need to get a list of all our models from the xarray.Dataset. You can see from this list that we have 7 models.

[82]:
models=list(ds.keys())
models
[82]:
['GEOS_V2p1', 'CCSM4', 'FIMr1p1', 'GEPS6', 'NESM', 'GEFS', 'MME']
  1. We need to define the number of rows and columns on our page. With 7 models, we will need to divide our page into 3 rows and 3 columns, even though we won’t use them all.

[83]:
nrows=3
ncols=3
[84]:
# Define the figure and each axis for the 3 rows and 3 columns
fig, axs = plt.subplots(nrows=nrows,ncols=ncols,
                        subplot_kw={'projection': ccrs.PlateCarree()},
                        figsize=(11,8.5))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
axs=axs.flatten()

#Loop over all of the models
for i,model in enumerate(models):

        # Select the week 1 forecast from the specified model
        data=ds[model][0,:,:]

        # Add the cyclic point
        data,lons=add_cyclic_point(data,coord=ds['lon'])

        # Contour plot
        cs=axs[i].contourf(lons,ds['lat'],data,
                          transform = ccrs.PlateCarree(),
                          cmap='coolwarm',extend='both')

        # Title each subplot with the name of the model
        axs[i].set_title(model)

        # Draw the coastines for each subplot
        axs[i].coastlines()
../_images/examples_multi-panel-cartopy_17_0.png

Get rid of the extra panels

Since we created a 3x3 grid of GeoAxes, the boxes for those axes appear even though we don’t want them. We can delete them.

[85]:
# Define the figure and each axis for the 3 rows and 3 columns
fig, axs = plt.subplots(nrows=nrows,ncols=ncols,
                        subplot_kw={'projection': ccrs.PlateCarree()},
                        figsize=(11,8.5))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
axs=axs.flatten()

#Loop over all of the models
for i,model in enumerate(models):

        # Select the week 1 forecast from the specified model
        data=ds[model][0,:,:]

        # Add the cyclic point
        data,lons=add_cyclic_point(data,coord=ds['lon'])

        # Contour plot
        cs=axs[i].contourf(lons,ds['lat'],data,
                          transform = ccrs.PlateCarree(),
                          cmap='coolwarm',extend='both')

        # Title each subplot with the name of the model
        axs[i].set_title(model)

        # Draw the coastines for each subplot
        axs[i].coastlines()

# Delete the unwanted axes
for i in [7,8]:
    fig.delaxes(axs[i])
../_images/examples_multi-panel-cartopy_20_0.png

Make Consistent contour intervals across all panels

Typically in atmosphere, ocean, and climate science, we make multi-panel plots because we want to easily compare across the panels. To do this, we need the contour intervals to be the same for each panel. To do this, we specify the levels in the plt.contourf call.

[86]:
# Define the contour levels to use in plt.contourf
clevs=np.arange(-12,13,1)


# Define the figure and each axis for the 3 rows and 3 columns
fig, axs = plt.subplots(nrows=nrows,ncols=ncols,
                        subplot_kw={'projection': ccrs.PlateCarree()},
                        figsize=(11,8.5))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
axs=axs.flatten()

#Loop over all of the models
for i,model in enumerate(models):

        # Select the week 1 forecast from the specified model
        data=ds[model][0,:,:]

        # Add the cyclic point
        data,lons=add_cyclic_point(data,coord=ds['lon'])

        # Contour plot
        cs=axs[i].contourf(lons,ds['lat'],data,clevs,
                          transform = ccrs.PlateCarree(),
                          cmap='coolwarm',extend='both')

        # Title each subplot with the name of the model
        axs[i].set_title(model)

        # Draw the coastines for each subplot
        axs[i].coastlines()

# Delete the unwanted axes
for i in [7,8]:
    fig.delaxes(axs[i])
../_images/examples_multi-panel-cartopy_22_0.png

Add a single colorbar and big title

[87]:
# Define the contour levels to use in plt.contourf
clevs=np.arange(-12,13,1)


# Define the figure and each axis for the 3 rows and 3 columns
fig, axs = plt.subplots(nrows=nrows,ncols=ncols,
                        subplot_kw={'projection': ccrs.PlateCarree()},
                        figsize=(11,8.5))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
axs=axs.flatten()

#Loop over all of the models
for i,model in enumerate(models):

        # Select the week 1 forecast from the specified model
        data=ds[model][0,:,:]

        # Add the cyclic point
        data,lons=add_cyclic_point(data,coord=ds['lon'])

        # Contour plot
        cs=axs[i].contourf(lons,ds['lat'],data,clevs,
                          transform = ccrs.PlateCarree(),
                          cmap='coolwarm',extend='both')

        # Title each subplot with the name of the model
        axs[i].set_title(model)

        # Draw the coastines for each subplot
        axs[i].coastlines()

# Delete the unwanted axes
for i in [7,8]:
    fig.delaxes(axs[i])

# Adjust the location of the subplots on the page to make room for the colorbar
fig.subplots_adjust(bottom=0.2, top=0.9, left=0.1, right=0.9,
                    wspace=0.02, hspace=0.02)

# Add a colorbar axis at the bottom of the graph
cbar_ax = fig.add_axes([0.2, 0.2, 0.6, 0.02])

# Draw the colorbar
cbar=fig.colorbar(cs, cax=cbar_ax,orientation='horizontal')

# Add a big title at the top
plt.suptitle('SubX Week 1 2m Temperature Anomalies ($^\circ$C): Apr 16, 2020 Initialized Forecasts')
[87]:
Text(0.5, 0.98, 'SubX Week 1 2m Temperature Anomalies ($^\\circ$C): Apr 16, 2020 Initialized Forecasts')
../_images/examples_multi-panel-cartopy_24_1.png

Add lat-lon labels

[88]:
# Define the contour levels to use in plt.contourf
clevs=np.arange(-12,13,1)


# Define the figure and each axis for the 3 rows and 3 columns
fig, axs = plt.subplots(nrows=nrows,ncols=ncols,
                        subplot_kw={'projection': ccrs.PlateCarree()},
                        figsize=(11,8.5))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
axs=axs.flatten()

#Loop over all of the models
for i,model in enumerate(models):

        # Select the week 1 forecast from the specified model
        data=ds[model][0,:,:]

        # Add the cyclic point
        data,lons=add_cyclic_point(data,coord=ds['lon'])

        # Contour plot
        cs=axs[i].contourf(lons,ds['lat'],data,clevs,
                          transform = ccrs.PlateCarree(),
                          cmap='coolwarm',extend='both')

        # Title each subplot with the name of the model
        axs[i].set_title(model)

        # Draw the coastines for each subplot
        axs[i].coastlines()

        # Longitude labels
        axs[i].set_xticks(np.arange(-180,181,60), crs=ccrs.PlateCarree())
        lon_formatter = cticker.LongitudeFormatter()
        axs[i].xaxis.set_major_formatter(lon_formatter)

        # Latitude labels
        axs[i].set_yticks(np.arange(-90,91,30), crs=ccrs.PlateCarree())
        lat_formatter = cticker.LatitudeFormatter()
        axs[i].yaxis.set_major_formatter(lat_formatter)

# Delete the unwanted axes
for i in [7,8]:
    fig.delaxes(axs[i])

# Adjust the location of the subplots on the page to make room for the colorbar
fig.subplots_adjust(bottom=0.2, top=0.9, left=0.1, right=0.9,
                    wspace=0.02, hspace=0.02)

# Add a colorbar axis at the bottom of the graph
cbar_ax = fig.add_axes([0.2, 0.2, 0.6, 0.02])

# Draw the colorbar
cbar=fig.colorbar(cs, cax=cbar_ax,orientation='horizontal')

# Add a big title at the top
plt.suptitle('SubX Week 1 2m Temperature Anomalies ($^\circ$C): Apr 16, 2020 Initialized Forecasts')
[88]:
Text(0.5, 0.98, 'SubX Week 1 2m Temperature Anomalies ($^\\circ$C): Apr 16, 2020 Initialized Forecasts')
../_images/examples_multi-panel-cartopy_26_1.png

Fix the spacing

Change the spacing values in fig.subplots_adjust

[89]:
# Define the contour levels to use in plt.contourf
clevs=np.arange(-12,13,1)


# Define the figure and each axis for the 3 rows and 3 columns
fig, axs = plt.subplots(nrows=nrows,ncols=ncols,
                        subplot_kw={'projection': ccrs.PlateCarree()},
                        figsize=(11,8.5))

# axs is a 2 dimensional array of `GeoAxes`.  We will flatten it into a 1-D array
axs=axs.flatten()

#Loop over all of the models
for i,model in enumerate(models):

        # Select the week 1 forecast from the specified model
        data=ds[model][0,:,:]

        # Add the cyclic point
        data,lons=add_cyclic_point(data,coord=ds['lon'])

        # Contour plot
        cs=axs[i].contourf(lons,ds['lat'],data,clevs,
                          transform = ccrs.PlateCarree(),
                          cmap='coolwarm',extend='both')

        # Title each subplot with the name of the model
        axs[i].set_title(model)

        # Draw the coastines for each subplot
        axs[i].coastlines()

        # Longitude labels
        axs[i].set_xticks(np.arange(-180,181,60), crs=ccrs.PlateCarree())
        lon_formatter = cticker.LongitudeFormatter()
        axs[i].xaxis.set_major_formatter(lon_formatter)

        # Latitude labels
        axs[i].set_yticks(np.arange(-90,91,30), crs=ccrs.PlateCarree())
        lat_formatter = cticker.LatitudeFormatter()
        axs[i].yaxis.set_major_formatter(lat_formatter)

# Delete the unwanted axes
for i in [7,8]:
    fig.delaxes(axs[i])

# Adjust the location of the subplots on the page to make room for the colorbar
fig.subplots_adjust(bottom=0.25, top=0.9, left=0.05, right=0.95,
                    wspace=0.1, hspace=0.5)

# Add a colorbar axis at the bottom of the graph
cbar_ax = fig.add_axes([0.2, 0.15, 0.6, 0.02])

# Draw the colorbar
cbar=fig.colorbar(cs, cax=cbar_ax,orientation='horizontal')

# Add a big title at the top
plt.suptitle('SubX Week 1 2m Temperature Anomalies ($^\circ$C): Apr 16, 2020 Initialized Forecasts')
[89]:
Text(0.5, 0.98, 'SubX Week 1 2m Temperature Anomalies ($^\\circ$C): Apr 16, 2020 Initialized Forecasts')
../_images/examples_multi-panel-cartopy_28_1.png