Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
env:
IRIS_TEST_DATA_LOC_PATH: benchmarks
IRIS_TEST_DATA_PATH: benchmarks/iris-test-data
IRIS_TEST_DATA_VERSION: "2.13"
IRIS_TEST_DATA_VERSION: "2.14"
# Lets us manually bump the cache to rebuild
ENV_CACHE_BUILD: "0"
TEST_DATA_CACHE_BUILD: "2"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-docs-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
session: ["doctest", "gallery", "linkcheck"]

env:
IRIS_TEST_DATA_VERSION: "2.13"
IRIS_TEST_DATA_VERSION: "2.14"
ENV_NAME: "ci-docs-tests"

steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
session: ["tests"]

env:
IRIS_TEST_DATA_VERSION: "2.13"
IRIS_TEST_DATA_VERSION: "2.14"
ENV_NAME: "ci-tests"

steps:
Expand Down
4 changes: 4 additions & 0 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ This document explains the changes made to Iris for this release
:func:`numpy.percentile` keywords through the :obj:`~iris.analysis.PERCENTILE`
aggregator. (:pull:`4791`)

#. `@wjbenfold`_ and `@bjlittle`_ (reviewer) implemented
:func:`iris.plot.fill_between` and :func:`iris.quickplot.fill_between`.
(:issue:`3493`, :pull:`4647`)


馃悰 Bugs Fixed
=============
Expand Down
110 changes: 109 additions & 1 deletion lib/iris/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,30 @@ def _u_object_from_v_object(v_object):


def _get_plot_objects(args):
if len(args) > 1 and isinstance(
if len(args) > 2 and isinstance(
args[2], (iris.cube.Cube, iris.coords.Coord)
):
# three arguments
u_object, v_object1, v_object2 = args[:3]
u1, v1 = _uv_from_u_object_v_object(u_object, v_object1)
_, v2 = _uv_from_u_object_v_object(u_object, v_object2)
args = args[3:]
if u1.size != v1.size or u1.size != v2.size:
msg = "The x and y-axis objects are not all compatible. They should have equal sizes but got ({}: {}), ({}: {}) and ({}: {})"
raise ValueError(
msg.format(
u_object.name(),
u1.size,
v_object1.name(),
v1.size,
v_object2.name(),
v2.size,
)
)
u = u1
v = (v1, v2)
v_object = (v_object1, v_object2)
elif len(args) > 1 and isinstance(
args[1], (iris.cube.Cube, iris.coords.Coord)
):
# two arguments
Expand Down Expand Up @@ -823,6 +846,52 @@ def _draw_1d_from_points(draw_method_name, arg_func, *args, **kwargs):
return result


def _draw_two_1d_from_points(draw_method_name, arg_func, *args, **kwargs):
"""
This function is equivalend to _draw_two_1d_from_points but expects two
y-axis variables rather than one (such as is required for .fill_between). It
can't be used where the y-axis variables are string coordinates. The y-axis
variable provided first has precedence where the two differ on whether the
axis should be inverted or whether a map should be drawn.
"""
# NB. In the interests of clarity we use "u" to refer to the horizontal
# axes on the matplotlib plot and "v" for the vertical axes.

# retrieve the objects that are plotted on the horizontal and vertical
# axes (cubes or coordinates) and their respective values, along with the
# argument tuple with these objects removed
u_object, v_objects, u, vs, args = _get_plot_objects(args)

v_object1, _ = v_objects
v1, v2 = vs

# if both u_object and v_object are coordinates then check if a map
# should be drawn
if (
isinstance(u_object, iris.coords.Coord)
and isinstance(v_object1, iris.coords.Coord)
and _can_draw_map([v_object1, u_object])
):
# Replace non-cartopy subplot/axes with a cartopy alternative and set
# the transform keyword.
kwargs = _ensure_cartopy_axes_and_determine_kwargs(
u_object, v_object1, kwargs
)

axes = kwargs.pop("axes", None)
draw_method = getattr(axes if axes else plt, draw_method_name)
if arg_func is not None:
args, kwargs = arg_func(u, v1, v2, *args, **kwargs)
result = draw_method(*args, **kwargs)
else:
result = draw_method(u, v1, v2, *args, **kwargs)

# Invert y-axis if necessary.
_invert_yaxis(v_object1, axes)

return result


def _replace_axes_with_cartopy_axes(cartopy_proj):
"""
Replace non-cartopy subplot/axes with a cartopy alternative
Expand Down Expand Up @@ -1599,6 +1668,45 @@ def scatter(x, y, *args, **kwargs):
return _draw_1d_from_points("scatter", _plot_args, *args, **kwargs)


def fill_between(x, y1, y2, *args, **kwargs):
"""
Plots y1 and y2 against x, and fills the space between them.

Args:

* x: :class:`~iris.cube.Cube` or :class:`~iris.coords.Coord`
A cube or a coordinate to plot on the x-axis.

* y1: :class:`~iris.cube.Cube` or :class:`~iris.coords.Coord`
First cube or a coordinate to plot on the y-axis.

* y2: :class:`~iris.cube.Cube` or :class:`~iris.coords.Coord`
Second cube or a coordinate to plot on the y-axis.

Kwargs:

* axes: :class:`matplotlib.axes.Axes`
The axes to use for drawing. Defaults to the current axes if none
provided.

See :func:`matplotlib.pyplot.fill_between` for details of additional valid
keyword arguments.

"""
# here we are more specific about argument types than generic 1d plotting
if not isinstance(x, (iris.cube.Cube, iris.coords.Coord)):
raise TypeError("x must be a cube or a coordinate.")
if not isinstance(y1, (iris.cube.Cube, iris.coords.Coord)):
raise TypeError("y1 must be a cube or a coordinate.")
if not isinstance(y1, (iris.cube.Cube, iris.coords.Coord)):
raise TypeError("y2 must be a cube or a coordinate.")
args = (x, y1, y2) + args
_plot_args = None
return _draw_two_1d_from_points(
"fill_between", _plot_args, *args, **kwargs
)


# Provide convenience show method from pyplot
show = plt.show

Expand Down
14 changes: 14 additions & 0 deletions lib/iris/quickplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,5 +311,19 @@ def scatter(x, y, *args, **kwargs):
return result


def fill_between(x, y1, y2, *args, **kwargs):
"""
Draws a labelled fill_between plot based on the given cubes or coordinates.

See :func:`iris.plot.fill_between` for details of valid arguments and
keyword arguments.

"""
axes = kwargs.get("axes")
result = iplt.fill_between(x, y1, y2, *args, **kwargs)
_label_1d_plot(x, y1, axes=axes)
return result


# Provide a convenience show method from pyplot.
show = plt.show
8 changes: 8 additions & 0 deletions lib/iris/tests/results/imagerepo.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,21 @@
"iris.tests.test_mapping.TestLowLevel.test_simple.0": "faa0e558855f9de7857a1ab16a85a51d36a1e55a854e58a5c13837096e8fe17a",
"iris.tests.test_mapping.TestMappingSubRegion.test_simple.0": "b9913d90c66eca6ec66ec2f3689195aecf5b2f00392cb3496495e21da4db6c92",
"iris.tests.test_mapping.TestUnmappable.test_simple.0": "fa81b54a817eca37817ec701857e3e64943e7bb41b806f996e817e006ee1b19b",
"iris.tests.test_plot.Test1dFillBetween.test_coord_coord.0": "f31432798cebcd87723835b4a5c5c2dbcf139c6c8cf4730bf3c36d801e380378",
"iris.tests.test_plot.Test1dFillBetween.test_coord_cube.0": "ea17352b92f0cbd42d6c8d25e59d36dc3a538d2bb2e42d26c6d2c2c8e4a1ce99",
"iris.tests.test_plot.Test1dFillBetween.test_cube_coord.0": "aff8e44af2019b3d3d03e0d1865e272cc1643de292db4b98c53c7ce5b0c37b2c",
"iris.tests.test_plot.Test1dFillBetween.test_cube_cube.0": "ea1761f695a09c0b70cc938d334b4e4f4c3671f2cd8b7996973c2c68e1c39e26",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_coord.0": "8bfec2577e01a5a5ed013b4ac4521c94817d4e6d91ff63369c6d61991e3278cc",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_coord.0": "8fff941e7e01e1c2f801c878a41e5b0d85cf36e1837e2d9992c62f21769e6a4d",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_coord_map.0": "bbe0c214cd979dc3b05e4b68db0771b48698961b7962d2446e8ca5bb36716c6e",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_coord_cube.0": "8ff897066a01f0f2f818ee1eb007ca41853e3b81c57e36a991fe2ca9725e29ed",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_cube.0": "8fffc1dc7e019c70f001b70ee4386de1814e7938837b6a7f84d07c9f15b02f21",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_coord.0": "8fffc1dc7e019c70f001b70ee4386de1814e7938837b6a7f84d07c9f15b02f21",
"iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_cube.0": "8ff8c0567a01b296e4019d2ff10b464bd4da6391943678e5879f7e3903e63f1c",
"iris.tests.test_plot.Test1dQuickplotFillBetween.test_coord_coord.0": "f314b2798ce3cd87723835a4a5c5c2dbcf139c6c8cf4730bd3c36d801c3c6378",
"iris.tests.test_plot.Test1dQuickplotFillBetween.test_coord_cube.0": "ea17352bd2f0cbd4256c8da5e59c36dc1a538d2b92e41d26ced2c2c8eca1ce99",
"iris.tests.test_plot.Test1dQuickplotFillBetween.test_cube_coord.0": "a3ffe44af6009b3d2907c8f1f6588f2cc96619e290fb4b88cd2c3ce590e3770c",
"iris.tests.test_plot.Test1dQuickplotFillBetween.test_cube_cube.0": "ea17e1f695a09c0b60cc938d334b4e4f4c3671f2cd8b7996973c2c69e1c31e26",
"iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord.0": "83fec2777e002427e801bb4ae65a1c94813dcec999db4bbc9ccd79991f3238cc",
"iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_coord.0": "83ff9d9f7e01e1c2b001c8f8f63e1b1d81cf36e1837e258982ce6f215c9a626c",
"iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_coord_coord_map.0": "bbe0c214cd979dc3b05e4b68db0771b48698961b7962d2446e8ca5bb36716c6e",
Expand Down
10 changes: 5 additions & 5 deletions lib/iris/tests/test_image_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ def test_json(self):
missing_from_json = test_data_name_set - repo_name_set
if missing_from_json:
amsg = (
"Missing images: Image names are referenced in "
"imagerepo.json, that are not present in the iris-test-data "
"repo"
"Missing images: Images are present in the iris-test-data "
"repo, that are not referenced in imagerepo.json"
)
# Always fails when we get here: report the problem.
self.assertEqual(missing_from_json, set(), msg=amsg)
missing_from_test_data = repo_name_set - test_data_name_set
if missing_from_test_data:
amsg = (
"Missing images: Images are present in the iris-test-data "
"repo, that are not referenced in imagerepo.json"
"Missing images: Image names are referenced in "
"imagerepo.json, that are not present in the iris-test-data "
"repo"
)
# Always fails when we get here: report the problem.
self.assertEqual(missing_from_test_data, set(), msg=amsg)
Expand Down
103 changes: 103 additions & 0 deletions lib/iris/tests/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import numpy as np

import iris
import iris.analysis
import iris.coords as coords
import iris.tests.stock

Expand Down Expand Up @@ -341,6 +342,108 @@ def test_circular_changes(self):
self.check_graphic()


class Test1dFillBetween(tests.GraphicsTest):
def setUp(self):
super().setUp()
self.cube = iris.load_cube(
tests.get_data_path(
("NetCDF", "testing", "small_theta_colpex.nc")
),
"air_potential_temperature",
)[0, 0]
self.draw_method = iplt.fill_between

def test_coord_coord(self):
x = self.cube.coord("grid_latitude")
y1 = self.cube.coord("surface_altitude")[:, 0]
y2 = self.cube.coord("surface_altitude")[:, 1]
self.draw_method(x, y1, y2)
self.check_graphic()

def test_coord_cube(self):
x = self.cube.coord("grid_latitude")
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)
self.draw_method(x, y1, y2)
self.check_graphic()

def test_cube_coord(self):
x = self.cube.collapsed("grid_longitude", iris.analysis.MEAN)
y1 = self.cube.coord("surface_altitude")[:, 0]
y2 = y1 + 10
self.draw_method(x, y1, y2)
self.check_graphic()

def test_cube_cube(self):
x = self.cube.collapsed("grid_longitude", iris.analysis.MEAN)
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)
self.draw_method(x, y1, y2)
self.check_graphic()

def test_incompatible_objects_x_odd(self):
# cubes/coordinates of different sizes cannot be plotted
x = self.cube.coord("grid_latitude")[:-1]
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)
with self.assertRaises(ValueError):
self.draw_method(x, y1, y2)

def test_incompatible_objects_y1_odd(self):
# cubes/coordinates of different sizes cannot be plotted
x = self.cube.coord("grid_latitude")
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)[:-1]
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)
with self.assertRaises(ValueError):
self.draw_method(x, y1, y2)

def test_incompatible_objects_y2_odd(self):
# cubes/coordinates of different sizes cannot be plotted
x = self.cube.coord("grid_latitude")
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)[:-1]
with self.assertRaises(ValueError):
self.draw_method(x, y1, y2)

def test_incompatible_objects_all_odd(self):
# cubes/coordinates of different sizes cannot be plotted
x = self.cube.coord("grid_latitude")
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)[:-1]
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)[:-2]
with self.assertRaises(ValueError):
self.draw_method(x, y1, y2)

def test_multidimensional(self):
# multidimensional cubes/coordinates are not allowed
x = self.cube.coord("grid_latitude")
y1 = self.cube
y2 = self.cube
with self.assertRaises(ValueError):
self.draw_method(x, y1, y2)

def test_not_cube_or_coord(self):
# inputs must be cubes or coordinates
x = np.arange(self.cube.shape[0])
y1 = self.cube.collapsed("grid_longitude", iris.analysis.MIN)
y2 = self.cube.collapsed("grid_longitude", iris.analysis.MAX)
with self.assertRaises(TypeError):
self.draw_method(x, y1, y2)


@tests.skip_data
@tests.skip_plot
class Test1dQuickplotFillBetween(Test1dFillBetween):
def setUp(self):
tests.GraphicsTest.setUp(self)
self.cube = iris.load_cube(
tests.get_data_path(
("NetCDF", "testing", "small_theta_colpex.nc")
),
"air_potential_temperature",
)[0, 0]
self.draw_method = qplt.fill_between


@tests.skip_data
@tests.skip_plot
class TestAttributePositive(tests.GraphicsTest):
Expand Down