Creating a Figure Module

This section discusses the elements of a nowcast.figures module. We’ll use nowcast.figures.research.tracer_thalweg_and_surface as the basis of the example. The example focuses on the structure of the module and the functions it contains, as well as the interfaces between those and the rest of the SalishSeaNowcastPackage.

There are some very strong opinions in this section about what function names to use, and how to break the code that creates a figure up into functions. They are here because we have learned the hard way that figure generation code quickly evolves into hard to read and maintain globs with fragile interconnections. Please follow the methodology in this section, but do feel free to discuss it with the group so that we can try to improve.

The DevelopTracerThalwegAndSurfaceModule notebook in notebooks/figures/research/ was used to develop our example figure module’s functions. You can take that approach if you wish, or you can develop directly in a module.

Of course, the ultimate goal is to produce a module. Once you’ve got a code module, you should create a notebook that tests it in the nowcast context. The TestTracerThalwegAndSurfaceModule notebook in notebooks/figures/research/ is an example for the nowcast.figures.research.tracer_thalweg_and_surface module.

Example Module

First we’ll show the tracer_thalweg_and_surface module structure as a whole, and then we’ll look at each section in detail.

  1 # Copyright 2013 – present by the SalishSeaCast Project contributors
  2 # and The University of British Columbia
  3
  4 # Licensed under the Apache License, Version 2.0 (the "License");
  5 # you may not use this file except in compliance with the License.
  6 # You may obtain a copy of the License at
  7
  8 #    http://www.apache.org/licenses/LICENSE-2.0
  9
 10 # Unless required by applicable law or agreed to in writing, software
 11 # distributed under the License is distributed on an "AS IS" BASIS,
 12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 # See the License for the specific language governing permissions and
 14 # limitations under the License.
 15
 16 # SPDX-License-Identifier: Apache-2.0
 17
 18
 19 """Produce a figure that shows colour contours of a tracer on a vertical slice
 20 along a section of the domain thalweg,
 21 and on the surface for a section of the domain that excludes Puget Sound
 22 in the south and Johnstone Strait in the north.
 23 """
 24 from types import SimpleNamespace
 25
 26 import matplotlib.pyplot as plt
 27 from matplotlib import gridspec
 28 import numpy as np
 29
 30 from salishsea_tools import visualisations as vis
 31 from salishsea_tools import viz_tools
 32
 33 import nowcast.figures.website_theme
 34
 35
 36 def make_figure(
 37     tracer_var, bathy, mesh_mask, cmap, depth_integrated,
 38     figsize=(16, 9), theme=nowcast.figures.website_theme
 39 ):
 40     """Plot colour contours of tracer on a vertical slice along a section of
 41     the domain thalweg,
 42     and on the surface for the Strait of Georgia and Juan de Fuca Strait
 43     regions of the domain.
 44
 45     :param tracer_var: Hourly average tracer results from NEMO run.
 46     :type tracer_var: :py:class:`netCDF4.Variable`
 47
 48     :param bathy: SalishSeaCast NEMO model bathymetry data.
 49     :type bathy: :class:`netCDF4.Dataset`
 50
 51     :param mesh_mask: NEMO-generated mesh mask for run that produced tracer_var.
 52     :type mesh_mask: :class:`netCDF4.Dataset`
 53
 54     :param cmap: Colour map to use for tracer_var contour plots.
 55     :type cmap: :py:class:`matplotlib.colors.LinearSegmentedColormap`
 56
 57     :param boolean depth_integrated: Integrate the tracer over the water column
 58                                      depth when :py:obj:`True`.
 59
 60     :param 2-tuple figsize: Figure size (width, height) in inches.
 61
 62     :param theme: Module-like object that defines the style elements for the
 63                 figure. See :py:mod:`nowcast.figures.website_theme` for an
 64                 example.
 65
 66     :returns: :py:class:`matplotlib.figure.Figure`
 67     """
 68     plot_data = _prep_plot_data(tracer_var, mesh_mask, depth_integrated)
 69     fig, (ax_thalweg, ax_surface) = _prep_fig_axes(figsize, theme)
 70
 71     clevels_thalweg, clevels_surface, show_thalweg_cbar = _calc_clevels(
 72         plot_data)
 73
 74     cbar_thalweg = _plot_tracer_thalweg(
 75         ax_thalweg, plot_data, bathy, mesh_mask, cmap, clevels_thalweg)
 76     _thalweg_axes_labels(
 77         ax_thalweg, plot_data, show_thalweg_cbar, clevels_thalweg,
 78         cbar_thalweg, theme)
 79
 80     cbar_surface = _plot_tracer_surface(
 81         ax_surface, plot_data, cmap, clevels_surface)
 82     _surface_axes_labels(
 83         ax_surface, tracer_var, depth_integrated, clevels_surface, cbar_surface,
 84         theme)
 85     return fig
 86
 87
 88 def _prep_plot_data(tracer_var, mesh_mask, depth_integrated):
 89     hr = 19
 90     sj, ej = 200, 800
 91     si, ei = 20, 395
 92
 93     tracer_hr = tracer_var[hr]
 94     masked_tracer_hr = np.ma.masked_where(
 95         mesh_mask['tmask'][0, ...] == 0, tracer_hr)
 96     surface_hr = masked_tracer_hr[0, sj:ej, si:ei]
 97
 98     if depth_integrated:
 99         grid_heights = mesh_mask.variables['e3t_1d'][:][0].reshape(
100             tracer_hr.shape[0], 1, 1)
101         height_weighted = masked_tracer_hr[:, sj:ej, si:ei] * grid_heights
102         surface_hr = height_weighted.sum(axis=0)
103
104     return SimpleNamespace(
105         tracer_var=tracer_var,
106         tracer_hr=tracer_hr,
107         surface_hr=surface_hr,
108         surface_j_limits=(sj, ej),
109         surface_i_limits=(si, ei),
110         thalweg_depth_limits=(0, 450),
111         thalweg_length_limits=(0, 632),
112     )
113
114
115 def _prep_fig_axes(figsize, theme):
116     fig = plt.figure(
117         figsize=figsize, facecolor=theme.COLOURS['figure']['facecolor'])
118
119     gs = gridspec.GridSpec(1, 2, width_ratios=[1.618, 1])
120
121     ax_thalweg = fig.add_subplot(gs[0])
122     ax_thalweg.set_axis_bgcolor(theme.COLOURS['axes']['background'])
123
124     ax_surface = fig.add_subplot(gs[1])
125     ax_surface.set_axis_bgcolor(theme.COLOURS['axes']['background'])
126
127     return fig, (ax_thalweg, ax_surface)
128
129
130 def _calc_clevels(plot_data):
131     """Calculates contour levels for the two axes and decides whether whether
132     the levels are similar enough that one colour bar is sufficient for the
133     figure, or if each axes requires one.
134     """
135     percent_98_surf = np.percentile(plot_data.surface_hr.compressed(), 98)
136     percent_2_surf = np.percentile(plot_data.surface_hr.compressed(), 2)
137
138     percent_98_grid = np.percentile(
139         np.ma.masked_values(plot_data.tracer_hr, 0).compressed(), 98)
140     percent_2_grid = np.percentile(
141         np.ma.masked_values(plot_data.tracer_hr, 0).compressed(), 2)
142
143     overlap = (
144         max(0, min(percent_98_surf, percent_98_grid)
145             - max(percent_2_surf, percent_2_grid)))
146     magnitude = (
147         (percent_98_surf - percent_2_surf) + (percent_98_grid - percent_2_grid))
148     if 2 * overlap / magnitude > 0.5:
149         max_clevel = max(percent_98_surf, percent_98_grid)
150         min_clevel = min(percent_2_surf, percent_2_grid)
151         clevels_thalweg = np.arange(
152             min_clevel, max_clevel, (max_clevel - min_clevel) / 20.0)
153         clevels_surface = clevels_thalweg
154         show_thalweg_cbar = False
155     else:
156         clevels_thalweg = np.arange(
157             percent_2_grid, percent_98_grid,
158             (percent_98_grid - percent_2_grid) / 20.0)
159         clevels_surface = np.arange(
160             percent_2_surf, percent_98_surf,
161             (percent_98_surf - percent_2_surf) / 20.0)
162         show_thalweg_cbar = True
163     return clevels_thalweg, clevels_surface, show_thalweg_cbar
164
165
166 def _plot_tracer_thalweg(ax, plot_data, bathy, mesh_mask, cmap, clevels):
167     cbar = vis.contour_thalweg(
168         ax, plot_data.tracer_hr, bathy, mesh_mask, clevels=clevels, cmap=cmap,
169         thalweg_file='/SalishSeaCast/tools/bathymetry/thalweg_working.txt',
170         cbar_args={'fraction': 0.030, 'pad': 0.04, 'aspect': 45}
171     )
172     return cbar
173
174
175 def _thalweg_axes_labels(
176     ax, plot_data, show_thalweg_cbar, clevels, cbar, theme
177 ):
178     ax.set_xlim(plot_data.thalweg_length_limits)
179     ax.set_ylim(
180         plot_data.thalweg_depth_limits[1], plot_data.thalweg_depth_limits[0])
181     if show_thalweg_cbar:
182         label = (
183             f'{plot_data.tracer_var.long_name} [{plot_data.tracer_var.units}]')
184         _cbar_labels(cbar, clevels[::2], theme, label)
185     else:
186         cbar.remove()
187     ax.set_xlabel(
188         'Distance along thalweg [km]', color=theme.COLOURS['text']['axis'],
189         fontproperties=theme.FONTS['axis'])
190     ax.set_ylabel(
191         'Depth [m]', color=theme.COLOURS['text']['axis'],
192         fontproperties=theme.FONTS['axis'])
193     theme.set_axis_colors(ax)
194
195
196 def _cbar_labels(cbar, contour_intervals, theme, label):
197     cbar.set_ticks(contour_intervals)
198     cbar.ax.axes.tick_params(labelcolor=theme.COLOURS['cbar']['tick labels'])
199     cbar.set_label(
200         label,
201         fontproperties=theme.FONTS['axis'],
202         color=theme.COLOURS['text']['axis'])
203
204
205 def _plot_tracer_surface(ax, plot_data, cmap, clevels):
206     x, y = np.meshgrid(
207         np.arange(*plot_data.surface_i_limits, dtype=int),
208         np.arange(*plot_data.surface_j_limits, dtype=int))
209     mesh = ax.contourf(
210         x, y, plot_data.surface_hr, levels=clevels, cmap=cmap, extend='both')
211     cbar = plt.colorbar(mesh, ax=ax, fraction=0.034, pad=0.04, aspect=45)
212     return cbar
213
214
215 def _surface_axes_labels(
216     ax, tracer_var, depth_integrated, clevels, cbar, theme
217 ):
218     cbar_units = (
219         f'{tracer_var.units}*m' if depth_integrated
220         else f'{tracer_var.units}')
221     cbar_label = f'{tracer_var.long_name} [{cbar_units}]'
222     _cbar_labels(cbar, clevels[::2], theme, cbar_label)
223     ax.set_xlabel(
224         'Grid x', color=theme.COLOURS['text']['axis'],
225         fontproperties=theme.FONTS['axis'])
226     ax.set_ylabel(
227         'Grid y', color=theme.COLOURS['text']['axis'],
228         fontproperties=theme.FONTS['axis'])
229     ax.set_axis_bgcolor('burlywood')
230     viz_tools.set_aspect(ax)
231     theme.set_axis_colors(ax)

Summary of Functions in a Figure Module

The function that the nowcast.workers.make_plots worker will call is named make_figure(). More details in make_figure() Function section.

make_figure() starts by calling 2 other functions:

  1. _prep_plot_data() to do all of the extraction and preparatory processing of the data that will be plotted in the figure’s axes objects.

    All of the slicing of the plot data from the dataset objects passed into the :make_figure(), and any calculations that are required should be done in _prep_plot_data() so that the variables it returns are ready to be passed into plotting methods. More details in the _prep_plot_data() Function section.

  2. _prep_fig_axes() creates the figure and axes objects that the variables will be plotted on. More details in the _prep_fig_axes() Function section.

make_figure() then calls a function whose name starts with _plot_() for each of the axes objects returned by _prep_fig_axes().

If the processing in the _prep_plot_data(), _prep_fig_axes(), or _plot_*() functions is long or complicated, it may be broken up into additional functions that those functions call. Examples include:

  • Code that is used to prepare several variables like the nowcast.figures.comparison.compare_venus_ctd._calc_results_time_series() function

  • Axis labeling and prettifying code like nowcast.figures.research.tracer_thalweg_and_surface._thalweg_axes_labels()

  • Code to calculate contour levels like nowcast.figures.research.tracer_thalweg_and_surface._calc_clevels()

The following sub-sections go through the example module above section by section to discuss its details.

Module Docstring

The module docstring will appear at top of the automatically generated module documentation (nowcast.figures.research.tracer_thalweg_and_surface in this case).

"""Produce a figure that shows colour contours of a tracer on a vertical slice
along a section of the domain thalweg,
and on the surface for a section of the domain that excludes Puget Sound
in the south and Johnstone Strait in the north.
"""

Imports

Next come the imports:

from types import SimpleNamespace

import matplotlib.pyplot as plt
from matplotlib import gridspec
import numpy as np

from salishsea_tools import visualisations as vis
from salishsea_tools import viz_tools

import nowcast.figures.website_theme

The Python standard library imports, those from 3rd party libraries like matplotlib, numpy, etc., and imports from the SalishSeaTools Package will vary from one figure module to another. However, the

import nowcast.figures.website_theme

import must be present in every figure module. nowcast.figures.website_theme provides the definition of colours and fonts that figure modules must use in order to ensure consistency from one to the next, and with the salishsea.eos.ubc.ca site NEMO results section styling.

See nowcast.figures.website_theme Module for more details about the website_theme module.

See library code Imports section for notes on organizing imports, coding style, and other guidelines.

make_figure() Function

The first function in the module is the function that will be called by the nowcast.workers.make_plots worker to return a matplotlib.figure.Figure object. This function is always named make_figure(). It is also the module’s only public function.

def make_figure(
    tracer_var, bathy, mesh_mask, cmap, depth_integrated,
    figsize=(16, 9), theme=nowcast.figures.website_theme
):
    """Plot colour contours of tracer on a vertical slice along a section of
    the domain thalweg,
    and on the surface for the Strait of Georgia and Juan de Fuca Strait
    regions of the domain.

    :param tracer_var: Hourly average tracer results from NEMO run.
    :type tracer_var: :py:class:`netCDF4.Variable`

    :param bathy: SalishSeaCast NEMO model bathymetry data.
    :type bathy: :class:`netCDF4.Dataset`

    :param mesh_mask: NEMO-generated mesh mask for run that produced tracer_var.
    :type mesh_mask: :class:`netCDF4.Dataset`

    :param cmap: Colour map to use for tracer_var contour plots.
    :type cmap: :py:class:`matplotlib.colors.LinearSegmentedColormap`

    :param boolean depth_integrated: Integrate the tracer over the water column
                                     depth when :py:obj:`True`.

    :param 2-tuple figsize: Figure size (width, height) in inches.

    :param theme: Module-like object that defines the style elements for the
                figure. See :py:mod:`nowcast.figures.website_theme` for an
                example.

    :returns: :py:class:`matplotlib.figure.Figure`
    """
    plot_data = _prep_plot_data(tracer_var, mesh_mask, depth_integrated)
    fig, (ax_thalweg, ax_surface) = _prep_fig_axes(figsize, theme)

    clevels_thalweg, clevels_surface, show_thalweg_cbar = _calc_clevels(
        plot_data)

    cbar_thalweg = _plot_tracer_thalweg(
        ax_thalweg, plot_data, bathy, mesh_mask, cmap, clevels_thalweg)
    _thalweg_axes_labels(
        ax_thalweg, plot_data, show_thalweg_cbar, clevels_thalweg,
        cbar_thalweg, theme)

    cbar_surface = _plot_tracer_surface(
        ax_surface, plot_data, cmap, clevels_surface)
    _surface_axes_labels(
        ax_surface, tracer_var, depth_integrated, clevels_surface, cbar_surface,
        theme)
    return fig

Function Signature

The function signature

def make_figure(
    tracer_var, bathy, mesh_mask, cmap, depth_integrated,
    figsize=(16, 9), theme=nowcast.figures.website_theme
):

should use model results dataset objects rather than file names so that the datasets are loaded once by the nowcast.workers.make_plots worker and references to them passed into the figure creation functions.

The signature ends with the default-values keyword arguments figsize and theme.

The figsize 2-tuple give the width and height of the figure, but more importantly its aspect ratio. Choose values that are appropriate to the information presented in the figure. If you don’t have a good reason to choose something else, use figsize=(16, 9) because that matches the aspect ration of wide displays that most people use to view web sites (even phones in landscape orientation).

theme should be defaulted to nowcast.figures.website_theme, a module that provides colours and font specifications that fit with the salishsea site colour scheme and provide consistency among the figures.

Function Docstring

The function docstring

"""Plot colour contours of tracer on a vertical slice along a section of
the domain thalweg,
and on the surface for the Strait of Georgia and Juan de Fuca Strait
regions of the domain.

:param tracer_var: Hourly average tracer results from NEMO run.
:type tracer_var: :py:class:`netCDF4.Variable`

:param bathy: SalishSeaCast NEMO model bathymetry data.
:type bathy: :class:`netCDF4.Dataset`

:param mesh_mask: NEMO-generated mesh mask for run that produced tracer_var.
:type mesh_mask: :class:`netCDF4.Dataset`

:param cmap: Colour map to use for tracer_var contour plots.
:type cmap: :py:class:`matplotlib.colors.LinearSegmentedColormap`

:param boolean depth_integrated: Integrate the tracer over the water column
                                 depth when :py:obj:`True`.

:param 2-tuple figsize: Figure size (width, height) in inches.

:param theme: Module-like object that defines the style elements for the
            figure. See :py:mod:`nowcast.figures.website_theme` for an
            example.

:returns: :py:class:`matplotlib.figure.Figure`
"""

includes description and type information for each of the function arguments. Those are written using Sphinx Info Field List markup so that they render nicely in the automatically generated module documentation.

Simple, 1-word type information can be included in the :param ...: role, for example:

:param boolean depth_integrated: Integrate the tracer over the water column
                                 depth when :py:obj:`True`.

More complicated type information should go in a separate :type ...: role like:

:param tracer_var: Hourly average tracer results from NEMO run.
:type tracer_var: :py:class:`netCDF4.Variable`

Function Code

The function code does 4 things:

  1. Call a module-private function _prep_plot_data() to prepare the collection of objects that contain the data that will be plotted in the figure:

    plot_data = _prep_plot_data(tracer_var, mesh_mask, depth_integrated)
    
  2. Call a module-private function _prep_fig_axes():

    fig, (ax_thalweg, ax_surface) = _prep_fig_axes(figsize, theme)
    

    That function returns:

    • a matplotlib.figure.Figure object

    • a tuple of matplotlib.axes.Axes objects, one for each axes in the figure

    The _prep_fig_axes() function accept arguments named figsize and theme. figsize provides the size and shape of the figure area. theme provides the nowcast.figures.website_theme Website Theme module which defines things like the figure and axes background colours.

    The tuple of axes objects returned by _prep_fig_axes() should be given meaningful names as shown above rather than:

    fig, (ax1, ax2, ax2, ax4) = _prep_fig_axes(figsize, theme)
    
  3. For each axes object returned by _prep_fig_axes(), call a module-private function whose name starts with _plot_() is called to draw all the things on the axes:

    clevels_thalweg, clevels_surface, show_thalweg_cbar = _calc_clevels(
        plot_data)
    
    cbar_thalweg = _plot_tracer_thalweg(
        ax_thalweg, plot_data, bathy, mesh_mask, cmap, clevels_thalweg)
    _thalweg_axes_labels(
        ax_thalweg, plot_data, show_thalweg_cbar, clevels_thalweg,
        cbar_thalweg, theme)
    
    cbar_surface = _plot_tracer_surface(
        ax_surface, plot_data, cmap, clevels_surface)
    _surface_axes_labels(
        ax_surface, tracer_var, depth_integrated, clevels_surface, cbar_surface,
        theme)
    

    In tracer_thalweg_and_surface we have an extra _calc_clevels() function that calculates contour levels for the two axes and decides whether whether the levels are similar enough that one colour bar is sufficient for the figure, or if each axes requires one.

    We have also separated the axes labeling and prettifying code into separate functions, _thalweg_axes_labels(), and _surface_axes_labels().

  4. Return the matplotlib.figure.Figure object to the nowcast.workers.make_plots worker:

    return fig
    

_prep_plot_data() Function

The _prep_plot_data() function is responsible for all of the extraction and preparatory processing of the data that will be plotted in the figure’s axes objects. All of the slicing of the plot data from the dataset objects passed into the make_figure() Function, and any calculations that are required should be done in _prep_plot_data() so that the variables it returns are ready to be passed into plotting methods.

def _prep_plot_data(tracer_var, mesh_mask, depth_integrated):
    hr = 19
    sj, ej = 200, 800
    si, ei = 20, 395

    tracer_hr = tracer_var[hr]
    masked_tracer_hr = np.ma.masked_where(
        mesh_mask['tmask'][0, ...] == 0, tracer_hr)
    surface_hr = masked_tracer_hr[0, sj:ej, si:ei]

    if depth_integrated:
        grid_heights = mesh_mask.variables['e3t_1d'][:][0].reshape(
            tracer_hr.shape[0], 1, 1)
        height_weighted = masked_tracer_hr[:, sj:ej, si:ei] * grid_heights
        surface_hr = height_weighted.sum(axis=0)

    return SimpleNamespace(
        tracer_var=tracer_var,
        tracer_hr=tracer_hr,
        surface_hr=surface_hr,
        surface_j_limits=(sj, ej),
        surface_i_limits=(si, ei),
        thalweg_depth_limits=(0, 450),
        thalweg_length_limits=(0, 632),
    )

_prep_plot_data() should return a types.SimpleNamespace so that the various data objects to be plotted can be easily accessed using dotted notation; e.g. plot_data.tracer_hr. Please see Return SimpleNamespace from Functions for more details.

In figure modules that use the salishsea_tools.places module, _prep_plot_data() is probably the best place to catch undefined place key errors Please see salishsea_tools.places for more details.

_prep_fig_axes() Function

The _prep_fig_axes() function accepts arguments named figsize and theme. figsize provides the size and shape of the figure theme provides the nowcast.figures.website_theme Website Theme module which defines things like the figure and axes background colours.

def _prep_fig_axes(figsize, theme):
    fig = plt.figure(
        figsize=figsize, facecolor=theme.COLOURS['figure']['facecolor'])

    gs = gridspec.GridSpec(1, 2, width_ratios=[1.618, 1])

    ax_thalweg = fig.add_subplot(gs[0])
    ax_thalweg.set_axis_bgcolor(theme.COLOURS['axes']['background'])

    ax_surface = fig.add_subplot(gs[1])
    ax_surface.set_axis_bgcolor(theme.COLOURS['axes']['background'])

    return fig, (ax_thalweg, ax_surface)

The nowcast.figures.website_theme module provides:

  • a colour to match the web page background colour that is used as the figure facecolor: theme.COLOURS['figure']['facecolor']

  • a background colour for the axes objects that is set using the set_axis_bgcolor() method: theme.COLOURS['axes']['background']

The function returns

  • a matplotlib.figure.Figure object

  • a tuple of matplotlib.axes.Axes objects, one for each axes in the figure

Axes Plotting Functions

After preparing the plot data, and setting up the figure and axes objects, our example make_figure() Function calls 2 axes plotting functions:

  1. _plot_tracer_thalweg() Function

  2. _plot_tracer_surface() Function

one for each matplotlib.axes.Axes object returned by _prep_fig_axes().

Those functions generally accept:

  • a matplotlib.axes.Axes object as their 1st argument, called ax by convention

  • the SimpleNamespace object that was returned by the _prep_plot_data() function, called plot_data by convention

  • the nowcast.figures.website_theme module as their last argument, theme by convention

They may accept other arguments as necessary.

The job of the _plot_*() functions is to act on the matplotlib.axes.Axes object (ax) so they may or may not return anything.

_plot_tracer_thalweg() Function

The _plot_tracer_thalweg() function in our example plots colour contours of a tracer on a vertical slice along a section of the domain thalweg.

def _plot_tracer_thalweg(ax, plot_data, bathy, mesh_mask, cmap, clevels):
    cbar = vis.contour_thalweg(
        ax, plot_data.tracer_hr, bathy, mesh_mask, clevels=clevels, cmap=cmap,
        thalweg_file='/SalishSeaCast/tools/bathymetry/thalweg_working.txt',
        cbar_args={'fraction': 0.030, 'pad': 0.04, 'aspect': 45}
    )
    return cbar

This function is a thin wrapper around the salishsea_tools.visualisations.contour_thalweg() function. It returns the cbar colour bar object for a separate _thalweg_axes_labels() function to operate on to handle “making the axes pretty”:

def _thalweg_axes_labels(
    ax, plot_data, show_thalweg_cbar, clevels, cbar, theme
):
    ax.set_xlim(plot_data.thalweg_length_limits)
    ax.set_ylim(
        plot_data.thalweg_depth_limits[1], plot_data.thalweg_depth_limits[0])
    if show_thalweg_cbar:
        label = (
            f'{plot_data.tracer_var.long_name} [{plot_data.tracer_var.units}]')
        _cbar_labels(cbar, clevels[::2], theme, label)
    else:
        cbar.remove()
    ax.set_xlabel(
        'Distance along thalweg [km]', color=theme.COLOURS['text']['axis'],
        fontproperties=theme.FONTS['axis'])
    ax.set_ylabel(
        'Depth [m]', color=theme.COLOURS['text']['axis'],
        fontproperties=theme.FONTS['axis'])
    theme.set_axis_colors(ax)

This function shows how text colours and fonts are obtained from theme. It finishes with a call to the theme.set_axis_colors() convenience function to set the colours of axis labels, ticks, and spines so that they are consistent with the web site theme.

The code format the colour bar labels is in separate _cbar_labels() function so that it can be used by both _thalweg_axes_labels() and _surface_axes_labels().

def _cbar_labels(cbar, contour_intervals, theme, label):
    cbar.set_ticks(contour_intervals)
    cbar.ax.axes.tick_params(labelcolor=theme.COLOURS['cbar']['tick labels'])
    cbar.set_label(
        label,
        fontproperties=theme.FONTS['axis'],
        color=theme.COLOURS['text']['axis'])

The colour of the tick labels on the colorbar is set by calling the axes.tick_params() method on the axes object with a colour provided by theme.

_plot_tracer_surface() Function

The _plot_tracer_surface() function is an example of horizontal layer contour plotting on an axes object.

def _plot_tracer_surface(ax, plot_data, cmap, clevels):
    x, y = np.meshgrid(
        np.arange(*plot_data.surface_i_limits, dtype=int),
        np.arange(*plot_data.surface_j_limits, dtype=int))
    mesh = ax.contourf(
        x, y, plot_data.surface_hr, levels=clevels, cmap=cmap, extend='both')
    cbar = plt.colorbar(mesh, ax=ax, fraction=0.034, pad=0.04, aspect=45)
    return cbar

This function constructs a mesh grid of x-y grid points and uses it and to plot colour contours. It illustrates how to access the surface tracer field that we returned in the plot_data namespace from the _prep_plot_data() Function.

An important consideration when plotting model results as maps for the web site is that the resulting images size must be kept as small as possible so that the page loading time does not become so large that the site is unusable, especially on slower or mobile networks. Using the contourf() method rather than pcolormesh() is one very effective way of limit the resulting figure image size. The contour() method is used to overlay contour lines on the contour map.

The method to add a colorbar to a axes that shows contoured data is not available on the matplotlib.axes.Axes object. Here we use the colorbar() convenience method provided by matplotlib.pyplot (which we aliases to plt on import).

The need to plot colour contours of horizontal data surfaces is general enough that code like this should be refactored into a salishsea_tools.visualisations.contour_layer() function so that _plot_tracer_surface() can become a wrapper like the _plot_tracer_thalweg() Function.

Similar to the _plot_tracer_thalweg() Function, this function returns the cbar colour bar object for a separate _surface_axes_labels() function to operate on to handle “making the axes pretty”.

Automatic Module Documentation Generation

When you create a new figure module don’t forget to add it to the SalishSeaNowcast/docs/workers.rst file so that documentation will be generated for it. For our example, the content added to SalishSeaNowcast/docs/workers.rst is:

.. _nowcast.figures.research.tracer_thalweg_and_surface:

:py:mod:`nowcast.figures.research.tracer_thalweg_and_surface` Module
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. automodule:: nowcast.figures.research.tracer_thalweg_and_surface
    :members:

Automatic Code Formatting

The SalishSeaNowcast package uses the black code formatting tool to maintain a coding style that is very close to PEP 8.

black is installed as part of the Nowcast Figures Development Environment setup.

Before each commit of your figure module please run black to automatically format your code. For our example tracer_thalweg_and_surface module the command would be:

$ pixi run -e fig-dev black nowcast/figures/research/tracer_thalweg_and_surface.py