Basic Plotting#

BEFORE BEGINNING THIS EXERCISE - Check that your kernel (upper right corner, above) is NPL 2023b. This should be the default kernel, but if it is not, click on that button and select NPL 2023b.


This activity was developed primarily by Cecile Hannay and Jesse Nusbaumer.


For the atmospheric data, we will look at common variables in the atmospheric diagnostics. This notebook covers 3 basic plotting examples:

Exercise 1: Global lat/lon of surface temperature

Exercise 2: Zonal mean of short wave cloud forcing

Exercise 3: Temperature zonal mean with vertical levels

Some of the plotting in these examples are based on the AMWG Diagnostics Framework (ADF) and some are natively from the xarray functionality. xarray will be used for the data I/O, analysis, and some plotting, matplotlib and cartopy will aid in plotting, and numpy for calculations

import os

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import cftime
import matplotlib as mpl
import matplotlib.path as mpath
import matplotlib.pyplot as plt
import numpy as np
import xarray as xr
from matplotlib.gridspec import GridSpec
from matplotlib.lines import Line2D
from mpl_toolkits.axes_grid1 import make_axes_locatable

The first step is to grab an atmosphere (CAM) history file from your CESM model run

# Set your username here:
username = "PUT_USER_NAME_HERE"

# Here we point to the archive directory from your b1850.run_length simulation
monthly_output_path = f"/glade/derecho/scratch/{username}/archive/b1850.run_length/atm/hist"

# If you were unable to successfully run the b1850.run_length simulation, then feel free to use
# this provided simulation data instead:
#monthly_output_path = "/glade/campaign/cesm/tutorial/tutorial_2023_archive/b1850.run_length/atm/hist"

# Name of history file to plot
file_name = "b1850.run_length.cam.h0.0003-07.nc"

files = os.path.join(monthly_output_path, file_name)
files
'/glade/derecho/scratch/PUT_USER_NAME_HERE/archive/b1850.run_length/atm/hist/b1850.run_length.cam.h0.0003-07.nc'
ds = xr.open_dataset(files)
ds
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/file_manager.py:211, in CachingFileManager._acquire_with_cache_info(self, needs_lock)
    210 try:
--> 211     file = self._cache[self._key]
    212 except KeyError:

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/lru_cache.py:56, in LRUCache.__getitem__(self, key)
     55 with self._lock:
---> 56     value = self._cache[key]
     57     self._cache.move_to_end(key)

KeyError: [<class 'netCDF4._netCDF4.Dataset'>, ('/glade/derecho/scratch/PUT_USER_NAME_HERE/archive/b1850.run_length/atm/hist/b1850.run_length.cam.h0.0003-07.nc',), 'r', (('clobber', True), ('diskless', False), ('format', 'NETCDF4'), ('persist', False)), 'da467520-dd8b-4048-a691-3445d0f0f93e']

During handling of the above exception, another exception occurred:

FileNotFoundError                         Traceback (most recent call last)
Cell In[3], line 1
----> 1 ds = xr.open_dataset(files)
      2 ds

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/api.py:566, in open_dataset(filename_or_obj, engine, chunks, cache, decode_cf, mask_and_scale, decode_times, decode_timedelta, use_cftime, concat_characters, decode_coords, drop_variables, inline_array, chunked_array_type, from_array_kwargs, backend_kwargs, **kwargs)
    554 decoders = _resolve_decoders_kwargs(
    555     decode_cf,
    556     open_backend_dataset_parameters=backend.open_dataset_parameters,
   (...)
    562     decode_coords=decode_coords,
    563 )
    565 overwrite_encoded_chunks = kwargs.pop("overwrite_encoded_chunks", None)
--> 566 backend_ds = backend.open_dataset(
    567     filename_or_obj,
    568     drop_variables=drop_variables,
    569     **decoders,
    570     **kwargs,
    571 )
    572 ds = _dataset_from_backend_dataset(
    573     backend_ds,
    574     filename_or_obj,
   (...)
    584     **kwargs,
    585 )
    586 return ds

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/netCDF4_.py:590, in NetCDF4BackendEntrypoint.open_dataset(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, use_cftime, decode_timedelta, group, mode, format, clobber, diskless, persist, lock, autoclose)
    569 def open_dataset(  # type: ignore[override]  # allow LSP violation, not supporting **kwargs
    570     self,
    571     filename_or_obj: str | os.PathLike[Any] | BufferedIOBase | AbstractDataStore,
   (...)
    587     autoclose=False,
    588 ) -> Dataset:
    589     filename_or_obj = _normalize_path(filename_or_obj)
--> 590     store = NetCDF4DataStore.open(
    591         filename_or_obj,
    592         mode=mode,
    593         format=format,
    594         group=group,
    595         clobber=clobber,
    596         diskless=diskless,
    597         persist=persist,
    598         lock=lock,
    599         autoclose=autoclose,
    600     )
    602     store_entrypoint = StoreBackendEntrypoint()
    603     with close_on_error(store):

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/netCDF4_.py:391, in NetCDF4DataStore.open(cls, filename, mode, format, group, clobber, diskless, persist, lock, lock_maker, autoclose)
    385 kwargs = dict(
    386     clobber=clobber, diskless=diskless, persist=persist, format=format
    387 )
    388 manager = CachingFileManager(
    389     netCDF4.Dataset, filename, mode=mode, kwargs=kwargs
    390 )
--> 391 return cls(manager, group=group, mode=mode, lock=lock, autoclose=autoclose)

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/netCDF4_.py:338, in NetCDF4DataStore.__init__(self, manager, group, mode, lock, autoclose)
    336 self._group = group
    337 self._mode = mode
--> 338 self.format = self.ds.data_model
    339 self._filename = self.ds.filepath()
    340 self.is_remote = is_remote_uri(self._filename)

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/netCDF4_.py:400, in NetCDF4DataStore.ds(self)
    398 @property
    399 def ds(self):
--> 400     return self._acquire()

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/netCDF4_.py:394, in NetCDF4DataStore._acquire(self, needs_lock)
    393 def _acquire(self, needs_lock=True):
--> 394     with self._manager.acquire_context(needs_lock) as root:
    395         ds = _nc4_require_group(root, self._group, self._mode)
    396     return ds

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/contextlib.py:135, in _GeneratorContextManager.__enter__(self)
    133 del self.args, self.kwds, self.func
    134 try:
--> 135     return next(self.gen)
    136 except StopIteration:
    137     raise RuntimeError("generator didn't yield") from None

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/file_manager.py:199, in CachingFileManager.acquire_context(self, needs_lock)
    196 @contextlib.contextmanager
    197 def acquire_context(self, needs_lock=True):
    198     """Context manager for acquiring a file."""
--> 199     file, cached = self._acquire_with_cache_info(needs_lock)
    200     try:
    201         yield file

File /glade/u/apps/opt/conda/envs/npl-2023b/lib/python3.10/site-packages/xarray/backends/file_manager.py:217, in CachingFileManager._acquire_with_cache_info(self, needs_lock)
    215     kwargs = kwargs.copy()
    216     kwargs["mode"] = self._mode
--> 217 file = self._opener(*self._args, **kwargs)
    218 if self._mode == "w":
    219     # ensure file doesn't get overridden when opened again
    220     self._mode = "a"

File src/netCDF4/_netCDF4.pyx:2464, in netCDF4._netCDF4.Dataset.__init__()

File src/netCDF4/_netCDF4.pyx:2027, in netCDF4._netCDF4._ensure_nc_success()

FileNotFoundError: [Errno 2] No such file or directory: '/glade/derecho/scratch/PUT_USER_NAME_HERE/archive/b1850.run_length/atm/hist/b1850.run_length.cam.h0.0003-07.nc'

Exercise 1: Make a lat-lon plot of TS#

To highlight plotting the variables from the CESM atmosphere (CAM) file, the first example will plot a simple global lat/lon plot of surface temperature TS

Grab data from first time stamp#

NOTE: This dataset has only one time

ts_0 = ds.TS.sel({"time": ds.TS.time.values[0]}).squeeze()
ts_0

The next step is to set up the map. Since we are plotting a global lat/lon, we will use the “Plate Carree” projection.

# define the colormap
cmap = mpl.colormaps["jet"]

# set up the figure with a Plate Carree projection
fig = plt.figure(figsize=(15, 10))

ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())

# Plot the first timeslice of TS
img = ax.pcolormesh(ds.lon, ds.lat, ts_0, cmap=cmap, transform=ccrs.PlateCarree())

plt.title("Surface Temperature", fontsize=20)

# Set up colorbar
plt.colorbar(img, orientation="vertical", fraction=0.0242, pad=0.01)
plt.show()
Click here for the solution

plot example

Figure: Plotting solution.

Question:

The colorbar limits are set automatically by pcolormesh. How could you change the arguments for the pcolormesh function to set the plotting limits?

Click here for hints

Choose a maximum temperature of 300K and minimum temperature of 225K.

img = ax.pcolormesh(ds.lon, ds.lat, ts_0, vmax=300, vmin=225, cmap=cmap, transform=ccrs.PlateCarree())

Question:

How could we change the central longitude?

Click here for hints The default central longitude is 0. Try setting it to 180. Then try other values from 0-360.
ax = fig.add_subplot(1,1,1, projection=ccrs.PlateCarree(central_longitude=180))

A second quick example is for xarray’s built-in plotting which uses the matplotlib and cartopy in the background. xarray makes creating a basic plot fairly simple.

# Xarray native plotting

# Set up figure and axis
fig, ax = plt.subplots(1, figsize=(20, 10))

# Plot the data straight from the xarray dataset
ts_0.plot.contourf(cmap="jet", levels=np.arange(220, 321, 5))

plt.show()
Click here for the solution

plot example

Figure: Plotting solution.


Exercise 2: Zonal plot of SWCF#

The second example will plot the short wave cloud forcing (SWCF) zonally.

Grab the variable data and mean over the the lon value

ds_swcf = ds.SWCF

# Get all the dataset dimensions
d = ds_swcf.dims

# Grab all dimensions to mean the lon values from
davgovr = [dim for dim in d if dim not in ("lev", "lat")]

# Make new dataset of zonal mean values
ds_swcf_zonal = ds_swcf.mean(dim=davgovr)

# print some values of new zonally-averaged SWCF variable
ds_swcf_zonal
# Create figure and axis
fig, ax = plt.subplots(1, figsize=(12, 7))

# Set Main title for subplots:
plt.title("Short Wave Cloud Forcing", fontsize=20)

# Plot value on y-axis and latitude on the x-axis
ax.plot(ds_swcf_zonal.lat, ds_swcf_zonal, c="goldenrod")

ax.set_xlim(
    [max([ds_swcf_zonal.lat.min(), -90.0]), min([ds_swcf_zonal.lat.max(), 90.0])]
)

ax.set_xlabel("Latitude")

plt.show()
Click here for the solution

plot example

Figure: Plotting solution.

Question

What code could you add to set a legend for the plot line?

Click here for hints

enter the following lines after the ax.set_xlabel command and before the plt.show() command.

label = ds_swcf.time.values[0].strftime() # -> '0001-02-01 00:00:00'
line = Line2D([0], [0], label=label,
                        color="blue")
                        
fig.legend(handles=[line],bbox_to_anchor=(-0.15, 0.15, 1.05, .102),loc="right",
                   borderaxespad=0.0,fontsize=16,frameon=False)

Question

What else could you label the line legend, if anything?

Click here for hints

Try setting the label value to something else like your simulation run name, the season or month, etc.

label = 'Season '

Question

How can you change the color on the plot and the legend?

Click here for hints

To change colors, try a different named color (e.g. “red” or “goldenrod”). You should make sure both the plot and the legend match or your plot won’t make sense.

Click here for a solution

plot example

Figure: Plotting solution.


Exercise 3: Plot of zonal T#

This example will plot the 3D zonal mean of temperature T with the pressure as the y-variable and latitude as the x-variable. We are showing you four different ways to make the same plot since each is valuable for plots with pressure on the y-axis and how to format those to get more information from the data.

# Remove all dimensions of length one
ds_t = ds.T.squeeze()
ds_t

Plot 1: Natively via xarray#

This plot uses the xarray native grids for the plot

  • y-axis is increasing with height

  • y-axis is not in log pressure

# Average over all dimensions except `lev` and `lat`.
d = ds_t.dims
davgovr = [dim for dim in d if dim not in ("lev", "lat")]

DS = ds_t.mean(dim=davgovr)
DS.transpose("lat", "lev", ...)
fig, ax = plt.subplots(1, figsize=(20, 10))
DS.plot.contourf(ax=ax, y="lev", cmap="Spectral_r")
plt.show()
Click here for a solution

plot example

Figure: Plotting solution.

Plot 2: Quick ADF style#

This plot uses the quick style of the AMWG Diagnostics Framework (ADF) packages.

  • y-axis is increasing with height

  • y-axis is not in log pressure

# Average over all dimensions except `lev` and `lat`.
d = ds_t.dims
davgovr = [dim for dim in d if dim not in ("lev", "lat")]

DS = ds_t.mean(dim=davgovr)

lev = DS["lev"]
lat = DS["lat"]
mlev, mlat = np.meshgrid(lev, lat)

# Generate zonal plot:
fig, ax = plt.subplots(1, figsize=(15, 7))

# Create zonal plot with vertical levels
img = ax.contourf(mlat, mlev, DS.transpose("lat", "lev"), cmap="Spectral_r")

# Format axis and ticks
ax.set_xlabel("Latitude")
fig.colorbar(img, ax=ax, location="right")
Click here for a solution

plot example

Figure: Plotting solution.

Questions

  • What differences do you see between the ADF quick plot and the xarray native plot?

  • Why might you want to use one or the other?

  • Where, vertically, do high pressures occur and what does this mean for where the ground would be located on this plot?

Click here for hints

Notice that the colorbar and axes labels are different between the two plots. Also note that the y-axis is going from lower to higher pressure values, but in the real atmosphere the highest pressures are near the surface (so the plot is inverted relative to the actual height of the layers above the Earth’s surface).

Plot 3: ADF style with reversed y-axis#

  • y-axis is decreasing with height

  • y-axis is not in log pressure

# Average over all dimensions except `lev` and `lat`.
d = ds_t.dims
davgovr = [dim for dim in d if dim not in ("lev", "lat")]

DS = ds_t.mean(dim=davgovr)

# print(DS.lev.min(),DS.lev.max())

lev = DS["lev"]
lat = DS["lat"]
mlev, mlat = np.meshgrid(lev, lat)

# Generate zonal plot:
fig, ax = plt.subplots(1, figsize=(15, 7))

# Create zonal plot with vertical levels
img = ax.contourf(mlat, mlev, DS.transpose("lat", "lev"), cmap="Spectral_r")

# Format axis and ticks
plt.gca().invert_yaxis()
ax.tick_params(which="minor", length=4, color="r")

# Set up colorbar
cbar_ax = fig.add_axes([0, 0, 0.1, 0.1])
posn = ax.get_position()

# Set position and size of colorbar position: [left, bottom, width, height]
cbar_ax.set_position([posn.x0 + posn.width + 0.005, posn.y0, 0.02, posn.height])

ax.set_xlabel("Latitude")
fig.colorbar(img, cax=cbar_ax, orientation="vertical")
Click here for a solution

plot example

Figure: Plotting solution.

Question:

  • What differences do you see on the y-axis between the result from Plot 2 and Plot 3?

Click here for hints

The plots are mirror images of each other because the y-axis has been flipped. Plot 2 has the highest pressures at the top of the y-axis while Plot 3 has the highest pressures at the bottom of the y-axis.

Question:

  • Where, vertically, do high pressures occur and what does this mean for where the ground would be located on these two figures ?

Click here for hints

The highest pressures in the atmosphere occur at the ground where the most atmospheric mass is pressing downward. This means that Plot 2 is oriented so the location where the ground is is the top of the plot, or up. In Plot 3 the orientation is so that the ground would be at the bottom of the plot, or down. Thus Plot 3 is often more intuitive to read because it’s oriented with the way we perceive the atmosphere.

Plot 4: Complex ADF style#

  • y-axis is decreasing with height

  • y-axis is in log pressure

# Average over all dimensions except `lev` and `lat`.
d = ds_t.dims
davgovr = [dim for dim in d if dim not in ("lev", "lat")]

DS = ds_t.mean(dim=davgovr)

# print(DS.lev.min(),DS.lev.max())

lev = DS["lev"]
lat = DS["lat"]
mlev, mlat = np.meshgrid(lev, lat)

# Generate zonal plot:
fig, ax = plt.subplots(1, figsize=(15, 7))

# Create zonal plot with vertical levels
img = ax.contourf(mlat, mlev, DS.transpose("lat", "lev"), cmap="Spectral_r")

# Format axis and ticks
plt.yscale("log")
plt.gca().invert_yaxis()
ax.tick_params(which="minor", length=4, color="r")

# Set up colorbar
cbar_ax = fig.add_axes([0, 0, 0.1, 0.1])
posn = ax.get_position()

# Set position and size of colorbar position: [left, bottom, width, height]
cbar_ax.set_position([posn.x0 + posn.width + 0.005, posn.y0, 0.02, posn.height])

ax.set_xlabel("Latitude")
fig.colorbar(img, cax=cbar_ax, orientation="vertical")
Click here for a solution

plot example

Figure: Plotting solution.

Question:

  • What differences do you see on the y-axis between the result from Plot 3 and Plot 4?

Click here for hints

The plots both have the y-axis oriented so that the highest pressures, or ground, is at the bottom of the plot. But the scale of the y-axis has changed so that in Plot 3 it is logarithmic.

Question:

  • What does that mean in terms of which part of the atmosphere are emphasized?

Click here for hints

With the logarithmic y-axis more of the upper atmosphere is shown on the figure. If you wanted to emphasize the boundary layer you might want to change the limits of the figure.