Plotting gridded data in matplotlib#

# initialization
import numpy as np
import pandas as pd
import xarray as xr
import matplotlib.pyplot as plt

Up to this point we’ve plotted only a variable’s dependence on one single dimension (e.g., temperature variation with time; salinity variation with depth). However, as we have seen, many variables can depend on more than one dimensions, and it can be useful to visualize dependency on multiple dimensions at once.

In particular, let’s consider the case where a variable is dependent on two dimensions. Numerically, such data is represented by a 2-dimensional array. Graphically, we may present such data as a false-color plot or a contour plot (we’re not going to discuss perspective 3-dimensional plots, since such plots tend to provide less quantitative information)

For our examples, we will use a coarsened slice of the Global Ocean Physics Analysis and Forecast data for the month of June, 2024. A copy of the dataset we use can be downloaded here:

dset = xr.open_dataset("data/global_ocean_physical_202406.nc")
display(dset)
<xarray.Dataset> Size: 26MB
Dimensions:      (time: 1, depth: 50, latitude: 269, longitude: 121)
Coordinates:
  * time         (time) datetime64[ns] 8B 2024-06-16
  * depth        (depth) float32 200B 0.494 1.541 2.646 ... 5.275e+03 5.728e+03
  * longitude    (longitude) float32 484B -180.0 -179.5 -179.0 ... -120.5 -120.0
  * latitude     (latitude) float64 2kB -67.0 -66.5 -66.0 ... 66.0 66.5 67.0
Data variables:
    temperature  (time, depth, latitude, longitude) float64 13MB ...
    salinity     (time, depth, latitude, longitude) float64 13MB ...
Attributes: (12/13)
    title:                         Monthly mean fields for product GLOBAL_ANA...
    references:                    http://marine.copernicus.eu
    credit:                        E.U. Copernicus Marine Service Information...
    licence:                       http://marine.copernicus.eu/services-portf...
    contact:                       servicedesk.cmems@mercator-ocean.eu
    producer:                      CMEMS - Global Monitoring and Forecasting ...
    ...                            ...
    Conventions:                   CF-1.6
    area:                          GLOBAL
    product:                       GLOBAL_ANALYSISFORECAST_PHY_001_024
    source:                        MERCATOR GLO12
    product_user_manual:           http://marine.copernicus.eu/documents/PUM/...
    quality_information_document:  http://marine.copernicus.eu/documents/QUID...

Input arguments for plotting 2D gridded data#

The 3 plotting methods we introduce below all expect an input of the form X, Y, Z, where X and Y are 1-dimensional arrays representing the horizontal (x-) and vertical (y-) coordinates, respectively, while Z is a 2-dimensional array. The size of X must agree with the column size of Z, and Y must agree with the row size of Z. In a picture:

Alignment of data for 2D plot

For our example dataset, we will take latitude as our horizontal coordinate and depth as our vertical coordinate. Thus, the variables that corresponds to X and Y are:

lat = dset.coords["latitude"].values
depths = dset.coords["depth"].values

For our 2D data Z, we will average over longitude and extract the only time component. We will consider both salinity and temperature. So we define two arrays here:

temp = dset["temperature"].mean("longitude").squeeze().values
sal = dset["salinity"].mean("longitude").squeeze().values

Note that we use .squeeze() to eliminate coordinates of length 1. We can check that both temp and sal are 2-dimensional numpy array and that their shape align with that of lat and depths

print("shape of `lat` = " + str(lat.shape))
print("shape of `depths` = " + str(depths.shape))
print("shape of `temp` = " + str(temp.shape))
print("shape of `sal` = " + str(sal.shape))
shape of `lat` = (269,)
shape of `depths` = (50,)
shape of `temp` = (50, 269)
shape of `sal` = (50, 269)

False-color plot via .pcolormesh()#

We can make a false color plot using the .pcolormesh() method of the Axes instance. For example:

fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot()

# making false color plot
ax.pcolormesh(lat, depths, temp)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/e300a809f4013ddb4f1d3025351bb244a78e88d2bfcd539ca90ab07b17d7cb94.png

Notice that we have focused ourselves to depths in the range (0 m, 2000 m), since that’s the range for which most variations in temperature and salinity occur.

While we can read off latitude and depth quantitatively from the above plot, we can’t read off the temperatures value without some kind of color guide. To generate a color guide, we need to assign the result of .pcolormesh() to a variable and use it to construct a color bar. Concretely, we need to change the example as follows:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

# assign output of .pcolormesh() to a variable
im = ax.pcolormesh(lat, depths, temp)

# making color bar
fig.colorbar(im)

#### MODIFICATION ENDS HERE

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/f23706dc02d3e3a1d8272370f1ad150c88684376c0eb7fd91adf0eb85ec7fa58.png

Note that we now have a color bar but it is unlabeled. To update its title and settings, we assign the colorbar to a variable and use its methods to modify its appearance:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

im = ax.pcolormesh(lat, depths, temp)
cb = fig.colorbar(im)

#### MODIFICATION STARTS HERE

# customize the appearance of the color bar
cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

#### MODIFICATION ENDS HERE

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/5c14772bb1e573094ee00cda42cf4b23385f5bc78fc31302130a3b3095ec9184.png

Sometimes you want to set the range of color to be different from the span of the data. We can do so using the vmin and vmax arguments of .pcolormesh():

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

# setting max and min of color scale
im = ax.pcolormesh(lat, depths, temp, vmin = 0, vmax=30)

#### MODIFICATION ENDS HERE

cb = fig.colorbar(im)

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/1be9d085d66af0194d246ebcd1a6fa6dcab3e2893d33bae426cddc138c03d538.png

Finally, there may be occasions where you can get more information out from the figure if the color is plotted in log scale.

As an example, consider the absolute value of the difference between salinity and its average over the region:

sal_diff_xr = np.fabs((dset["salinity"].mean("longitude") - dset["salinity"].mean()))
sal_diff = sal_diff_xr.squeeze().values

Plotting on the linear color scale we get:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

im = ax.pcolormesh(lat, depths, sal_diff)
cb = fig.colorbar(im)

cb.set_label("Salinity", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Magnitude of salinity variation from mean value", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/49ab86288fc62d1d6201571c405ff7a7a569b6bb6e02d0ad1233319e09e6ea61.png

To plot in log scale we need the LogNorm() function from the matplotlib.color submodule, so we import the submodule below:

import matplotlib.colors as colors

Now we can plot in the log color scale:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

im = ax.pcolormesh(lat, depths, sal_diff, norm=colors.LogNorm())

#### MODIFICATION ENDS HERE

cb = fig.colorbar(im)

cb.set_label("Salinity", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Magnitude of salinity variation from mean value", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/d34f2bedf0ad243bf08ffcfe78fb54e884265d4b6d36fbf2bda88cc9f8c78de7.png

Remarks: choice of color map#

So far we have been making false-color plots using the default color map of matplotlib. This is not the only possible choice. However, recall in the discussion of week 5 that color map choice should take accessibility into account. Helpfully, matplotlib has a page (https://matplotlib.org/stable/users/explain/colors/colormaps.html) that provides good overview of color maps available in matplotlib as well as how accessibility concerns fit into them. Treat this as a required reading and choose your color map wisely!

In short, the sequential color maps should be your default choices. If a middle value (e.g., zero, mean of data, etc.) is important, a diverging color map may be justified.

As an example, here is the plot of temperature variation using the “inferno” color map:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

im = ax.pcolormesh(lat, depths, temp, cmap='inferno')

#### MODIFICATION ENDS HERE

cb = fig.colorbar(im)

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/5e7102f2fe321cf82e10707a2c0fdffbaf2b595805eb3195f0dde6dab7b87536.png

In Addition, the third-party cmocean module also provides a number of good color maps for oceanographic data. A good documentation of cmocean can be found at https://matplotlib.org/cmocean/. For illustration, we will re-plot the temperature data using the thermal colormap below:

import cmocean as cmo
fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

im = ax.pcolormesh(lat, depths, temp, cmap=cmo.cm.thermal)

#### MODIFICATION ENDS HERE

cb = fig.colorbar(im)
cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/80d458c14aac36b245de428f042855e9ef0099478ad117135076683c8d8a8bdc.png

Filled Contour plot via .contourf()#

You may notice that the .pcolormesh() plots we produced are somewhat pixelated. That’s because ultimately our underlying data is discrete. If you want a smoother representation of your data, consider making a filled contour plot using .contourf() instead. In the simplest way, all we have to do is to replace .pcolormesh() with .contourf():

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

# Use `.contourf()` instead of `.pcolormesh()`
im = ax.contourf(lat, depths, temp, cmap=cmo.cm.thermal)

#### MODIFICATION EMDS HERE

cb = fig.colorbar(im)

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/9081d65282dfbfd242e61e5bc30de8de09249c0d1f548d2156c056bff3c7170d.png

However, there is one important argument in .contourf() that’s absent in .pcolormesh(). Namely, the levels argument determines where the contour boundaries are. For example, we can request denser contours than the plot above if we set:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

# Override the automatically chosen contour levels
im = ax.contourf(
    lat, depths, temp, 
    levels = np.arange(-4, 32.5, 2), cmap=cmo.cm.thermal
)

#### MODIFICATION EMDS HERE

cb = fig.colorbar(im)

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/22e246cbb0308e8035535bee1c0fae6994bba9b7a3515d706ccf59c306a3ba3b.png

We can also use logarithmic color scale in filled contour plot:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

im = ax.contourf(
    lat, depths, sal_diff, 
    cmap=cmo.cm.haline, norm=colors.LogNorm(), 
    levels = 10**np.arange(-5, 1, 0.5)
)

#### MODIFICATION ENDS HERE

cb = fig.colorbar(im)

cb.set_label("Salinity", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Magnitude of salinity variation from mean value", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/8bda92ede5d13a4af4295fe22072421631b5bcba874eaf52d01f2fbb53feae96.png

Line contour plot via .contour()#

Finally, we can also make line contour plot via .contour(). Again, with minimum modifcations our plot now looks like:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

# Use `.contour()` instead of `.contourf()`
im = ax.contour(lat, depths, temp, cmap=cmo.cm.thermal)

#### MODIFICATION EMDS HERE

cb = fig.colorbar(im)

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/4910c8d91d0d56f8f58ad072ed3c652aa5cb6adae59a976c4cf8ec40886505be.png

In the case of line contour plot, it is no longer easy to read off the values of the contours via the color bar. So we may instead put labels on the contours themselves. To do so, we need to use the .clabel() method on the contours, e.g.,

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

im = ax.contour(lat, depths, temp, cmap=cmo.cm.thermal)
cb = fig.colorbar(im)

#### MODIFICATION STARTS HERE

# create labels on the contour lines
cl = im.clabel(fontsize=10)

#### MODIFICATION EMDS HERE

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(2000, 0)

plt.show(fig)
../_images/464c6da63fb55de69ad98146e3a4a41b1df77afb9eeb3ab32ebfd17bd4abc846.png

Finally, the line style of contours can also be adjusted per contour:

fig = plt.figure(figsize=(8.5, 4))
ax = fig.add_subplot()

#### MODIFICATION STARTS HERE

# cycle between solid and broken line
im = ax.contour(
    lat, depths, temp, 
    cmap=cmo.cm.thermal, 
    levels = np.arange(0, 32.5, 2), 
    linestyles=["-", "--"]
)

#### MODIFICATION EMDS HERE

cb = fig.colorbar(im)
cl = im.clabel(fontsize=10)

cb.set_label("Temperature (°C)", fontsize=14)
cb.ax.tick_params(labelsize=12)

ax.set_xlabel("latitude (°N)", fontsize=14)
ax.set_ylabel("depth (m)", fontsize=14)
ax.set_title("Temperature variation with latitude and depth", fontsize=16)
ax.tick_params(labelsize=12)

ax.set_ylim(1000, 0)

plt.show(fig)
../_images/d45a798b95fca2805203047a7a61366ae9af68ed7989f4d8106f3ef817378d79.png