Repository: SciTools/iris (language: python)

Issue:
Zonal mean gallery example
## 🚀 Pull Request

### Description
<!-- Provide a clear description about your awesome pull request -->
<!-- Tell us all about your new feature, improvement, or bug fix -->

This example demonstrates aligning a linear plot and a cartographic plot using Matplotlib.

---
[Consult Iris pull request check list]( https://scitools-iris.readthedocs.io/en/latest/developers_guide/contributing_pull_request_checklist.html)

Add a Zonal Mean gallery example
This came out of supporting a user at the UK Met Office. We were able to produce code that aligns a linear plot with one of the axes of a cartographic plot - ideal for zonal means - based on [this Stack Overflow post](https://stackoverflow.com/questions/56571020/cartopy-aspect-ratio-match-line-subplots-to-geographic-map).

An example plot & script is below, although the data loading steps have been removed. We should be able to adapt this to work with something from [iris-sample-data](https://github.com/SciTools/iris-sample-data). A previous attempt stalled due to lack of contributor time - #4871 - so that could be used as a template.

---

<details><summary>Zonal mean plot example</summary>

![sal_ano](https://user-images.githubusercontent.com/40734014/183623963-32202f30-8b91-4637-b893-8df8f4302c84.png)

```python
from cartopy.crs import PlateCarree
import iris
from iris.analysis import MEAN
import iris.plot as iplt
import matplotlib.pyplot as plt
import matplotlib.cm as mpl_cm
from mpl_toolkits.axes_grid1 import make_axes_locatable
import seaborn as sns

sns.set(style='whitegrid')
cmap_BrBG_r = mpl_cm.get_cmap('BrBG_r')

fig = plt.figure(figsize=[12, 4])
ax1 = fig.add_subplot(111, projection=PlateCarree())
ax1.set_extent([-90, 0, -10, 70], crs=ccrs.PlateCarree())
plt.sca(ax1)
im=ax1.contourf(lon_n216,lat_n216,tmptmp,levels=levels,cmap=cmap_BrBG_r,extend='both')
ax1.coastlines()
ax1.gridlines()
ax1.set_xticks([-100, -80, -60, -40, -20, 0], crs=ccrs.PlateCarree())
ax1.set_yticks([-10, 0, 10, 20, 30, 40, 50 , 60 , 70], crs=ccrs.PlateCarree())
ax1.set_title('Upper ocean salinity anomaly')
ax1.set_ylabel('latitude')
ax1.set_xlabel('longitude')

divider = make_axes_locatable(ax1)

ax2 = divider.new_vertical(size="5%", pad=0.5, axes_class=plt.Axes, pack_start=True)
fig.add_axes(ax2)
plt.sca(ax2)
cbar=plt.colorbar(im, cax=ax2,orientation='horizontal')
cbar.ax.set_xlabel('salinity [PSU]')

ax3 = divider.new_horizontal(size="30%", pad=0.2, axes_class=plt.Axes)
fig.add_axes(ax3)
plt.sca(ax3)
ax3.plot(avg_sa_arr[0,:],lat_n216,color='k')
ax3.axvline(0,color='k',linewidth=0.5)
ax3.set_ylim(-10,70)
ax3.set_title('Zonal mean')
ax3.set_ylabel('latitude')
ax3.set_xlabel('salinity [PSU]')
ax3.yaxis.set_label_position("right")
ax3.yaxis.tick_right()

outfile='sal_ano.png'
plt.savefig(outfile,bbox_inches='tight', pad_inches=0.1, dpi=300)
plt.close()

```

</details>



Discussion / hints:
Closing as @TTV-Intrepid is no longer available to complete this work.


Fix patch:
diff --git a/docs/gallery_code/general/plot_zonal_means.py b/docs/gallery_code/general/plot_zonal_means.py
new file mode 100644
index 0000000000..08a9578e63
--- /dev/null
+++ b/docs/gallery_code/general/plot_zonal_means.py
@@ -0,0 +1,89 @@
+"""
+Zonal Mean Diagram of Air Temperature
+=====================================
+This example demonstrates aligning a linear plot and a cartographic plot using Matplotlib.
+"""
+
+import cartopy.crs as ccrs
+import matplotlib.pyplot as plt
+from mpl_toolkits.axes_grid1 import make_axes_locatable
+import numpy as np
+
+import iris
+from iris.analysis import MEAN
+import iris.plot as iplt
+import iris.quickplot as qplt
+
+
+def main():
+
+    # Loads air_temp.pp and "collapses" longitude into a single, average value.
+    fname = iris.sample_data_path("air_temp.pp")
+    temperature = iris.load_cube(fname)
+    collapsed_temp = temperature.collapsed("longitude", MEAN)
+
+    # Set y-axes with -90 and 90 limits and steps of 15 per tick.
+    start, stop, step = -90, 90, 15
+    yticks = np.arange(start, stop + step, step)
+    ylim = [start, stop]
+
+    # Plot "temperature" on a cartographic plot and set the ticks and titles
+    # on the axes.
+    fig = plt.figure(figsize=[12, 4])
+
+    ax1 = fig.add_subplot(111, projection=ccrs.PlateCarree())
+    im = iplt.contourf(temperature, cmap="RdYlBu_r")
+    ax1.coastlines()
+    ax1.gridlines()
+    ax1.set_xticks([-180, -90, 0, 90, 180])
+    ax1.set_yticks(yticks)
+    ax1.set_title("Air Temperature")
+    ax1.set_ylabel(f"Latitude / {temperature.coord('latitude').units}")
+    ax1.set_xlabel(f"Longitude / {temperature.coord('longitude').units}")
+    ax1.set_ylim(*ylim)
+
+    # Create a Matplotlib AxesDivider object to allow alignment of other
+    # Axes objects.
+    divider = make_axes_locatable(ax1)
+
+    # Gives the air temperature bar size, colour and a title.
+    ax2 = divider.new_vertical(
+        size="5%", pad=0.5, axes_class=plt.Axes, pack_start=True
+    )  # creates 2nd axis
+    fig.add_axes(ax2)
+    cbar = plt.colorbar(
+        im, cax=ax2, orientation="horizontal"
+    )  # puts colour bar on second axis
+    cbar.ax.set_xlabel(f"{temperature.units}")  # labels colour bar
+
+    # Plot "collapsed_temp" on the mean graph and set the ticks and titles
+    # on the axes.
+    ax3 = divider.new_horizontal(
+        size="30%", pad=0.4, axes_class=plt.Axes
+    )  # create 3rd axis
+    fig.add_axes(ax3)
+    qplt.plot(
+        collapsed_temp, collapsed_temp.coord("latitude")
+    )  # plots temperature collapsed over longitude against latitude
+    ax3.axhline(0, color="k", linewidth=0.5)
+
+    # Creates zonal mean details
+    ax3.set_title("Zonal Mean")
+    ax3.yaxis.set_label_position("right")
+    ax3.yaxis.tick_right()
+    ax3.set_yticks(yticks)
+    ax3.grid()
+
+    # Round each tick for the third ax to the nearest 20 (ready for use).
+    data_max = collapsed_temp.data.max()
+    x_max = data_max - data_max % -20
+    data_min = collapsed_temp.data.min()
+    x_min = data_min - data_min % 20
+    ax3.set_xlim(x_min, x_max)
+    ax3.set_ylim(*ylim)
+
+    plt.show()
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index aae1ce2970..43bacd3ec5 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -46,7 +46,7 @@ jobs:
             session: "tests"
 
     env:
-      IRIS_TEST_DATA_VERSION: "2.15"
+      IRIS_TEST_DATA_VERSION: "2.16"
       ENV_NAME: "ci-tests"
 
     steps:
diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst
index d0b03d9304..a42a5a64be 100644
--- a/docs/src/whatsnew/latest.rst
+++ b/docs/src/whatsnew/latest.rst
@@ -25,7 +25,7 @@ This document explains the changes made to Iris for this release
 📢 Announcements
 ================
 
-#. Welcome to `@ESadek-MO`_ who made their first contribution to Iris 🎉
+#. Welcome to `@ESadek-MO`_ and `@TTV-Intrepid`_  who made their first contributions to Iris 🎉
 
 
 ✨ Features
@@ -78,7 +78,8 @@ This document explains the changes made to Iris for this release
 📚 Documentation
 ================
 
-#. N/A
+#. `@ESadek-MO`_, `@TTV-Intrepid`_ and `@trexfeathers`_ added a gallery example for zonal
+   means plotted parallel to a cartographic plot. (:pull:`4871`)
 
 
 💼 Internal
@@ -92,6 +93,8 @@ This document explains the changes made to Iris for this release
     Whatsnew author names (@github name) in alphabetical order. Note that,
     core dev names are automatically included by the common_links.inc:
 
+.. _@TTV-Intrepid: https://github.com/TTV-Intrepid
+
 
 
 
diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json
index 342428ca67..e5c2ad863a 100644
--- a/lib/iris/tests/results/imagerepo.json
+++ b/lib/iris/tests/results/imagerepo.json
@@ -34,6 +34,7 @@
     "gallery_tests.test_plot_wind_barbs.0": "e9e161e996169316c1fe9e96c29e36739e13c07c3d61c07f39813929c07f3f01",
     "gallery_tests.test_plot_wind_speed.0": "e9e960e996169306c1fe9e96c29e36739e03c06c3d61c07f3da139e1c07f3f01",
     "gallery_tests.test_plot_wind_speed.1": "e9e960e996169306c1ee9f96c29e36739653c06c3d61c07f39a139e1c07f3f01",
+    "gallery_tests.test_plot_zonal_means.0": "b45b3071c9a4c9a6c69c363cc327cbb3cb9634d8c9e63cf336738c6634d8c384",
     "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.0": "fe81c17e817e3e81817e3e81857e7a817e81c17e7e81c17e7a81817e817e8c2e",
     "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.1": "fe81857e817e7a85817e7a81857e7e817e81917a7e81817e7a81817e817e843e",
     "iris.tests.experimental.test_animate.IntegrationTest.test_cube_animation.2": "be81817ec17e7a81c17e7e81857e3e803e81817a3e81c17e7a81c17ec97e2c2f",