From d099f93084bb75d9bfb8ff7724bdd17a97e993f2 Mon Sep 17 00:00:00 2001 From: lbdreyer Date: Fri, 17 Mar 2023 15:05:55 +0000 Subject: [PATCH] Add histogram convenience for passing Iris objects to plt.hist (#5189) * Add histogram convenience for passing Iris objects to plt.hist * Fix orientation * Review actions --- .github/workflows/benchmark.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- docs/src/whatsnew/latest.rst | 3 ++ lib/iris/plot.py | 30 ++++++++++++++++ lib/iris/quickplot.py | 25 +++++++++++++ lib/iris/tests/results/imagerepo.json | 3 ++ lib/iris/tests/test_plot.py | 9 +++++ lib/iris/tests/test_quickplot.py | 19 ++++++++++ lib/iris/tests/unit/plot/test_hist.py | 51 +++++++++++++++++++++++++++ 9 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 lib/iris/tests/unit/plot/test_hist.py diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9ae3534c76..5dfbc81b8f 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -21,7 +21,7 @@ jobs: env: IRIS_TEST_DATA_LOC_PATH: benchmarks IRIS_TEST_DATA_PATH: benchmarks/iris-test-data - IRIS_TEST_DATA_VERSION: "2.18" + IRIS_TEST_DATA_VERSION: "2.19" # Lets us manually bump the cache to rebuild ENV_CACHE_BUILD: "0" TEST_DATA_CACHE_BUILD: "2" diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 81f5132ccf..46cc319c49 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -50,7 +50,7 @@ jobs: session: "tests" env: - IRIS_TEST_DATA_VERSION: "2.18" + IRIS_TEST_DATA_VERSION: "2.19" ENV_NAME: "ci-tests" steps: diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index bb7efb9aa9..d92711d285 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -45,6 +45,9 @@ This document explains the changes made to Iris for this release :meth:`iris.cube.Cube.rolling_window`). This automatically adapts cube units if necessary. (:pull:`5084`) +#. `@lbdreyer`_ and `@trexfeathers`_ (reviewer) added :func:`iris.plot.hist` + and :func:`iris.quickplot.hist`. (:pull:`5189`) + 🐛 Bugs Fixed ============= diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 8cd849b716..f87d74b020 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -1704,6 +1704,36 @@ def fill_between(x, y1, y2, *args, **kwargs): ) +def hist(x, *args, **kwargs): + """ + Compute and plot a histogram. + + Args: + + * x: + A :class:`~iris.cube.Cube`, :class:`~iris.coords.Coord`, + :class:`~iris.coords.CellMeasure`, or :class:`~iris.coords.AncillaryVariable` + that will be used as the values that will be used to create the + histogram. + Note that if a coordinate is given, the points are used, ignoring the + bounds. + + See :func:`matplotlib.pyplot.hist` for details of additional valid + keyword arguments. + + """ + if isinstance(x, iris.cube.Cube): + data = x.data + elif isinstance(x, iris.coords._DimensionalMetadata): + data = x._values + else: + raise TypeError( + "x must be a cube, coordinate, cell measure or " + "ancillary variable." + ) + return plt.hist(data, *args, **kwargs) + + # Provide convenience show method from pyplot show = plt.show diff --git a/lib/iris/quickplot.py b/lib/iris/quickplot.py index 6006314265..c992cfdbf0 100644 --- a/lib/iris/quickplot.py +++ b/lib/iris/quickplot.py @@ -324,5 +324,30 @@ def fill_between(x, y1, y2, *args, **kwargs): return result +def hist(x, *args, **kwargs): + """ + Compute and plot a labelled histogram. + + See :func:`iris.plot.hist` for details of valid arguments and + keyword arguments. + """ + axes = kwargs.get("axes") + result = iplt.hist(x, *args, **kwargs) + title = _title(x, with_units=False) + label = _title(x, with_units=True) + + if axes is None: + axes = plt.gca() + + orientation = kwargs.get("orientation") + if orientation == "horizontal": + axes.set_ylabel(label) + else: + axes.set_xlabel(label) + axes.set_title(title) + + return result + + # Provide a convenience show method from pyplot. show = plt.show diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index 92f0d8fc20..2313c25270 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -195,6 +195,7 @@ "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_default.0": "b87830b0c786cf269ec766c99399cce998d3b3166f2530d3658c692d30ec6735", "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_yx_order.0": "fa85978e837e68f094d3673089626ad792073985659a9b1a7a15b52869f19f56", "iris.tests.test_plot.TestPlotDimAndAuxCoordsKwarg.test_yx_order.1": "ea95969c874a63d39ca3ad2a231cdbc9c4973631cd6336c633182cbc61c3d3f2", + "iris.tests.test_plot.TestPlotHist.test_cube.0": "b59cc3dadb433c24c4f16603943a793591a7c3dcb4dcbccc68c697a93b139131", "iris.tests.test_plot.TestPlotOtherCoordSystems.test_plot_tmerc.0": "e665326d999ecc92b399b32466269326b369cccccccd64d96199631364f33333", "iris.tests.test_plot.TestQuickplotPlot.test_t.0": "83ffb59a7f00e59a2205d9d6e4619a74d9388c8e884e8da799d30b6dddb47e00", "iris.tests.test_plot.TestQuickplotPlot.test_t_dates.0": "82fe958b7e046f89a0033bd4d9632c74d8799d3e8d8d826789e487b348dc2f69", @@ -216,6 +217,8 @@ "iris.tests.test_quickplot.TestLabels.test_pcolor.0": "eea16affc05ab500956e974ac53f3d80925ac03f2f81c07e3fa12da1c2fe3f80", "iris.tests.test_quickplot.TestLabels.test_pcolormesh.0": "eea16affc05ab500956e974ac53f3d80925ac03f2f81c07e3fa12da1c2fe3f80", "iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol.0": "eea16affc05ab500956e974ac53f3d80925ac03f3f80c07e3fa12d21c2ff3f80", + "iris.tests.test_quickplot.TestPlotHist.test_horizontal.0": "b59cc3dadb433c24c4f166039438793591a7dbdcbcdc9ccc68c697a91b139131", + "iris.tests.test_quickplot.TestPlotHist.test_vertical.0": "bf80c7c6c07d7959647e343a33364b699589c6c64ec0312b9e227ad681ffcc68", "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_non_cube_coordinate.0": "fe816a85857a957ac07f957ac07f3e80956ac07f3e80c07f3e813e85c07e3f80", "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.0": "ea856a95955a956ac17f950a807e3f4e951ac07e3f81c0ff3ea16aa1c0bd3e81", "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_tx.1": "ea856a85957a957ac17e954ac17e1ea2950bc07e3e80c07f3e807a85c1ff3f81", diff --git a/lib/iris/tests/test_plot.py b/lib/iris/tests/test_plot.py index c9eba31e58..f4f41f6aa4 100644 --- a/lib/iris/tests/test_plot.py +++ b/lib/iris/tests/test_plot.py @@ -1001,6 +1001,15 @@ def test_non_cube_coordinate(self): self.draw("contourf", cube, coords=["grid_latitude", x]) +@tests.skip_data +@tests.skip_plot +class TestPlotHist(tests.GraphicsTest): + def test_cube(self): + cube = simple_cube()[0] + iplt.hist(cube, bins=np.linspace(287.7, 288.2, 11)) + self.check_graphic() + + @tests.skip_data @tests.skip_plot class TestPlotDimAndAuxCoordsKwarg(tests.GraphicsTest): diff --git a/lib/iris/tests/test_quickplot.py b/lib/iris/tests/test_quickplot.py index 06f170c666..df2db12de6 100644 --- a/lib/iris/tests/test_quickplot.py +++ b/lib/iris/tests/test_quickplot.py @@ -10,6 +10,9 @@ # import iris tests first so that some things can be initialised before importing anything else import iris.tests as tests # isort:skip + +import numpy as np + import iris import iris.tests.test_plot as test_plot @@ -281,5 +284,21 @@ def test_without_axes__default(self): self._check(mappable, self.figure2, self.axes2) +@tests.skip_data +@tests.skip_plot +class TestPlotHist(tests.GraphicsTest): + def test_horizontal(self): + cube = test_plot.simple_cube()[0] + qplt.hist(cube, bins=np.linspace(287.7, 288.2, 11)) + self.check_graphic() + + def test_vertical(self): + cube = test_plot.simple_cube()[0] + qplt.hist( + cube, bins=np.linspace(287.7, 288.2, 11), orientation="horizontal" + ) + self.check_graphic() + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/plot/test_hist.py b/lib/iris/tests/unit/plot/test_hist.py new file mode 100644 index 0000000000..8a74ff8701 --- /dev/null +++ b/lib/iris/tests/unit/plot/test_hist.py @@ -0,0 +1,51 @@ +# 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. +"""Unit tests for the `iris.plot.hist` function.""" +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +from unittest import mock + +import numpy as np +import pytest + +from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord +from iris.cube import Cube + +if tests.MPL_AVAILABLE: + import iris.plot as iplt + + +@tests.skip_plot +class Test: + @pytest.fixture(autouse=True) + def create_data(self): + self.data = np.array([0, 100, 110, 120, 200, 320]) + + @pytest.mark.parametrize( + "x", [AuxCoord, Cube, DimCoord, CellMeasure, AncillaryVariable] + ) + def test_simple(self, x): + with mock.patch("matplotlib.pyplot.hist") as mocker: + iplt.hist(x(self.data)) + # mocker.assert_called_once_with is not working as expected with + # _DimensionalMetadata objects so we use np.testing array equality + # checks instead. + args, kwargs = mocker.call_args + assert len(args) == 1 + np.testing.assert_array_equal(args[0], self.data) + + def test_kwargs(self): + cube = Cube(self.data) + bins = [0, 150, 250, 350] + with mock.patch("matplotlib.pyplot.hist") as mocker: + iplt.hist(cube, bins=bins) + mocker.assert_called_once_with(self.data, bins=bins) + + def test_unsupported_input(self): + with pytest.raises(TypeError, match="x must be a"): + iplt.hist(self.data)