diff --git a/docs/gallery_code/meteorology/plot_wind_barbs.py b/docs/gallery_code/meteorology/plot_wind_barbs.py new file mode 100644 index 0000000000..c3c056eb4a --- /dev/null +++ b/docs/gallery_code/meteorology/plot_wind_barbs.py @@ -0,0 +1,62 @@ +""" +Plotting Wind Direction Using Barbs +=================================== + +This example demonstrates using barbs to plot wind speed contours and wind +direction barbs from wind vector component input data. The vector components +are co-located in space in this case. + +The magnitude of the wind in the original data is low and so doesn't illustrate +the full range of barbs. The wind is scaled to simulate a storm that better +illustrates the range of barbs that are available. +""" + +import matplotlib.pyplot as plt + +import iris +import iris.plot as iplt +import iris.quickplot as qplt + + +def main(): + # Load the u and v components of wind from a pp file + infile = iris.sample_data_path("wind_speed_lake_victoria.pp") + + uwind = iris.load_cube(infile, "x_wind") + vwind = iris.load_cube(infile, "y_wind") + + uwind.convert_units("knot") + vwind.convert_units("knot") + + # To illustrate the full range of barbs, scale the wind speed up to pretend + # that a storm is passing over + magnitude = (uwind ** 2 + vwind ** 2) ** 0.5 + magnitude.convert_units("knot") + max_speed = magnitude.collapsed( + ("latitude", "longitude"), iris.analysis.MAX + ).data + max_desired = 65 + + uwind = uwind / max_speed * max_desired + vwind = vwind / max_speed * max_desired + + # Create a cube containing the wind speed + windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 + windspeed.rename("windspeed") + windspeed.convert_units("knot") + + plt.figure() + + # Plot the wind speed as a contour plot + qplt.contourf(windspeed) + + # Add wind barbs except for the outermost values which overhang the edge + # of the plot if left + iplt.barbs(uwind[1:-1, 1:-1], vwind[1:-1, 1:-1], pivot="middle", length=6) + + plt.title("Wind speed during a simulated storm") + qplt.show() + + +if __name__ == "__main__": + main() diff --git a/docs/gallery_tests/test_plot_wind_barbs.py b/docs/gallery_tests/test_plot_wind_barbs.py new file mode 100644 index 0000000000..6003860a5e --- /dev/null +++ b/docs/gallery_tests/test_plot_wind_barbs.py @@ -0,0 +1,30 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. + +# Import Iris tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +from .gallerytest_util import ( + add_gallery_to_path, + fail_any_deprecation_warnings, + show_replaced_by_check_graphic, +) + + +class TestWindBarbs(tests.GraphicsTest): + """Test the wind_barbs example code.""" + + def test_wind_barbs(self): + with fail_any_deprecation_warnings(): + with add_gallery_to_path(): + import plot_wind_barbs + with show_replaced_by_check_graphic(self): + plot_wind_barbs.main() + + +if __name__ == "__main__": + tests.main() diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 4daff5a2d1..09e78b3256 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -42,6 +42,9 @@ This document explains the changes made to Iris for this release the primary coordinate being plotted against is a vertical coordinate. E.g. ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before it would have produced a phenomenon-vs-z plot. (:pull:`3906`) +#. `@jonseddon`_ added :meth:`iris.plot.barbs` to provide a convenient way to + use :func:`matplotlib.pyplot.barbs` with Iris cubes. A gallery example was + included to illustrate the new method's use. (:pull:`3710`) #. `@bjlittle`_ introduced :func:`iris.common.metadata.hexdigest` to the public API. Previously it was a private function introduced in ``v3.0.0``. diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 0086a05ead..1b1c4dc774 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -1419,6 +1419,55 @@ def _vector_component_args(x_points, y_points, u_data, *args, **kwargs): return ((x_points, y_points, u_data, v_data), kwargs) +def barbs(u_cube, v_cube, *args, **kwargs): + """ + Draws a barb plot from two vector component cubes. Triangles, full-lines + and half-lines represent increments of 50, 10 and 5 respectively. + + Args: + + * u_cube, v_cube : (:class:`~iris.cube.Cube`) + u and v vector components. Must have same shape and units. + If the cubes have geographic coordinates, the values are treated as + true distance differentials, e.g. windspeeds, and *not* map coordinate + vectors. The components are aligned with the North and East of the + cube coordinate system. + + .. Note:: + + At present, if u_cube and v_cube have geographic coordinates, then they + must be in a lat-lon coordinate system, though it may be a rotated one. + To transform wind values between coordinate systems, use + :func:`iris.analysis.cartography.rotate_grid_vectors`. + To transform coordinate grid points, you will need to create + 2-dimensional arrays of x and y values. These can be transformed with + :meth:`cartopy.crs.CRS.transform_points`. + + Kwargs: + + * coords: (list of :class:`~iris.coords.Coord` or string) + Coordinates or coordinate names. Use the given coordinates as the axes + for the plot. The order of the given coordinates indicates which axis + to use for each, where the first element is the horizontal + axis of the plot and the second element is the vertical axis + of the plot. + + * axes: the :class:`matplotlib.axes.Axes` to use for drawing. + Defaults to the current axes if none provided. + + See :func:`matplotlib.pyplot.barbs` for details of other valid + keyword arguments. + + """ + # + # TODO: check u + v cubes for compatibility. + # + kwargs["_v_data"] = v_cube.data + return _draw_2d_from_points( + "barbs", _vector_component_args, u_cube, *args, **kwargs + ) + + def quiver(u_cube, v_cube, *args, **kwargs): """ Draws an arrow plot from two vector component cubes. @@ -1432,12 +1481,12 @@ def quiver(u_cube, v_cube, *args, **kwargs): vectors. The components are aligned with the North and East of the cube coordinate system. - .. Note: + .. Note:: At present, if u_cube and v_cube have geographic coordinates, then they must be in a lat-lon coordinate system, though it may be a rotated one. To transform wind values between coordinate systems, use - :func:`iris.analysis.cartography.rotate_vectors`. + :func:`iris.analysis.cartography.rotate_grid_vectors`. To transform coordinate grid points, you will need to create 2-dimensional arrays of x and y values. These can be transformed with :meth:`cartopy.crs.CRS.transform_points`. diff --git a/lib/iris/tests/integration/plot/test_vector_plots.py b/lib/iris/tests/integration/plot/test_vector_plots.py index 7ffbce6eff..9dad0ddd3b 100644 --- a/lib/iris/tests/integration/plot/test_vector_plots.py +++ b/lib/iris/tests/integration/plot/test_vector_plots.py @@ -24,16 +24,16 @@ if tests.MPL_AVAILABLE: import matplotlib.pyplot as plt - from iris.plot import quiver + from iris.plot import barbs, quiver @tests.skip_plot class MixinVectorPlotCases: """ - Test examples mixin, used by separate quiver + streamplot classes. + Test examples mixin, used by separate barb, quiver + streamplot classes. - NOTE: at present for quiver only, as streamplot does not support arbitrary - coordinates. + NOTE: at present for barb and quiver only, as streamplot does not support + arbitrary coordinates. """ @@ -193,6 +193,34 @@ def test_circular_longitude(self): self.plot("circular", u_cube, v_cube, coords=("longitude", "latitude")) +class TestBarbs(MixinVectorPlotCases, tests.GraphicsTest): + def setUp(self): + super().setUp() + + @staticmethod + def _nonlatlon_xyuv(): + # Increase the range of wind speeds used in the barbs test to test more + # barbs shapes than just circles + x, y, u, v = MixinVectorPlotCases._nonlatlon_xyuv() + scale_factor = 50 + u *= scale_factor + v *= scale_factor + return x, y, u, v + + @staticmethod + def _latlon_uv_cubes(grid_cube): + # Increase the range of wind speeds used in the barbs test to test all + # barbs shapes + u_cube, v_cube = MixinVectorPlotCases._latlon_uv_cubes(grid_cube) + scale_factor = 30 + u_cube.data *= scale_factor + v_cube.data *= scale_factor + return u_cube, v_cube + + def plot_function_to_test(self): + return barbs + + class TestQuiver(MixinVectorPlotCases, tests.GraphicsTest): def setUp(self): super().setUp() diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index 8c6f622991..35ef4d00d4 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -143,6 +143,10 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172d0847ecd2bc913939c36846c714933799cc3cc8727e67639f939996a58.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fa8172c6857ecd38cb3392ce36c564311931d85ec64e9787719a39993c316e66.png" ], + "gallery_tests.test_plot_wind_barbs.TestWindBarbs.test_wind_barbs.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169316c1fe9e96c29e36739e13c07c3d61c07f39a13921c07f3e21.png", + "https://scitools.github.io/test-iris-imagehash/images/v4/e9e161e996169316c1fe9e96c29e36739e13c07c3d61c07f39813929c07f3f01.png" + ], "gallery_tests.test_plot_wind_speed.TestWindSpeed.test_plot_wind_speed.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/bcf924fb9306930ce12ccf97c73236b28ecec4cd3e29847b18e639e6c14f1a09.png", "https://scitools.github.io/test-iris-imagehash/images/v4/e9e960e996169306c1fe9e96c29e36739e13c06c3d61c07f39a139e1c07f3f01.png" @@ -173,6 +177,21 @@ "iris.tests.integration.plot.test_plot_2d_coords.Test2dContour.test_2d_coords_contour.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/b4b2643ecb05cb43b0f23d80c53c4e1d3e5990eb1f81c19f2f983cb1c4ff3e42.png" ], + "iris.tests.integration.plot.test_vector_plots.TestBarbs.test_2d_plain_latlon.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/eb036726c47c9273918e6e2c6f216336787590eb969a165890ee6c676925b3b3.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestBarbs.test_2d_plain_latlon_on_polar_map.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/e66d673c999031cd6667663398dc332c676364e798959336636660d933998666.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestBarbs.test_2d_rotated_latlon.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/eba037a4c479c273b2963f2c6f6126966865d86f969e33c9b1706c26692793b0.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestBarbs.test_non_latlon_1d_coords.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/a7ac334934d2e65c72596325b343338cb41c92d9c5b36f65330d379692ca6d6c.png" + ], + "iris.tests.integration.plot.test_vector_plots.TestBarbs.test_non_latlon_2d_coords.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/a7acb36134d2e676627963259343330cb43e92d9c5336e67330d379292ca6d6c.png" + ], "iris.tests.integration.plot.test_vector_plots.TestQuiver.test_2d_plain_latlon.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fb8d4f21c472b27e919d2e216f216b3178e69c7e961ab39a84696c616d245b94.png" ],