diff --git a/examples/gallery/bokeh/orthographic_vectorfield.ipynb b/examples/gallery/bokeh/orthographic_vectorfield.ipynb index 791f4d61..4401a1f2 100644 --- a/examples/gallery/bokeh/orthographic_vectorfield.ipynb +++ b/examples/gallery/bokeh/orthographic_vectorfield.ipynb @@ -48,7 +48,7 @@ "\n", "xs, ys, U, V, crs = sample_data()\n", "mag = np.sqrt(U**2 + V**2)\n", - "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "angle = np.pi / 2 - np.arctan2(-V, -U)\n", "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" ] }, diff --git a/examples/gallery/bokeh/vectorfield_example.ipynb b/examples/gallery/bokeh/vectorfield_example.ipynb index b7eea1aa..7e942d31 100644 --- a/examples/gallery/bokeh/vectorfield_example.ipynb +++ b/examples/gallery/bokeh/vectorfield_example.ipynb @@ -47,7 +47,7 @@ "\n", "xs, ys, U, V, crs = sample_data()\n", "mag = np.sqrt(U**2 + V**2)\n", - "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "angle = np.pi / 2 - np.arctan2(-V, -U)\n", "tiles = gv.tile_sources.OSM\n", "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" ] diff --git a/examples/gallery/matplotlib/orthographic_vectorfield.ipynb b/examples/gallery/matplotlib/orthographic_vectorfield.ipynb index b091a760..ab86b4e7 100644 --- a/examples/gallery/matplotlib/orthographic_vectorfield.ipynb +++ b/examples/gallery/matplotlib/orthographic_vectorfield.ipynb @@ -50,7 +50,7 @@ "\n", "xs, ys, U, V, crs = sample_data()\n", "mag = np.sqrt(U**2 + V**2)\n", - "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "angle = np.pi / 2 - np.arctan2(-V, -U)\n", "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" ] }, diff --git a/examples/gallery/matplotlib/vectorfield_example.ipynb b/examples/gallery/matplotlib/vectorfield_example.ipynb index dad5ad23..e5a94422 100644 --- a/examples/gallery/matplotlib/vectorfield_example.ipynb +++ b/examples/gallery/matplotlib/vectorfield_example.ipynb @@ -49,7 +49,7 @@ "\n", "xs, ys, U, V, crs = sample_data()\n", "mag = np.sqrt(U**2 + V**2)\n", - "angle = (np.pi/2.) - np.arctan2(U/mag, V/mag)\n", + "angle = np.pi / 2 - np.arctan2(-V, -U)\n", "tiles = gv.tile_sources.OSM\n", "vectorfield = gv.VectorField((xs, ys, angle, mag), crs=crs)" ] diff --git a/examples/gallery/matplotlib/wind_barbs_example.ipynb b/examples/gallery/matplotlib/wind_barbs_example.ipynb new file mode 100644 index 00000000..27dd40c1 --- /dev/null +++ b/examples/gallery/matplotlib/wind_barbs_example.ipynb @@ -0,0 +1,90 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import geoviews as gv\n", + "\n", + "gv.extension('matplotlib')\n", + "\n", + "gv.output(fig='svg', size=200)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Define data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "lat = np.arange(60, 37.5, -2.5)\n", + "lon = np.arange(270, 292.5, 2.5)\n", + "uwnd = np.array(\n", + " [\n", + " [2, 0, -2, -2, -3, -3, -3, -2, -1],\n", + " [2, 0, -2, -2, -2, -2, -2, -1, 1],\n", + " [2, -1, -2, -2, -2, -1, 0, 1, 3],\n", + " [3, 0, -3, -5, -5, -4, -4, -2, 0],\n", + " [8, 4, 0, -3, -5, -6, -6, -6, -5],\n", + " [12, 10, 8, 5, 3, 0, -2, -2, -2],\n", + " [13, 14, 16, 16, 14, 12, 10, 9, 10],\n", + " [13, 18, 22, 24, 25, 24, 23, 22, 23],\n", + " [20, 25, 29, 32, 33, 32, 32, 33, 34],\n", + " ]\n", + ")\n", + "vwwnd = np.array(\n", + " [\n", + " [3, 1, 0, -1, -1, 0, 1, 3, 4],\n", + " [-2, -3, -3, -2, 0, 2, 4, 6, 8],\n", + " [-6, -6, -4, -1, 2, 5, 7, 10, 12],\n", + " [-12, -10, -6, -1, 4, 7, 10, 12, 14],\n", + " [-17, -15, -10, -4, 2, 6, 9, 12, 16],\n", + " [-20, -18, -14, -8, -2, 2, 5, 10, 16],\n", + " [-17, -16, -13, -9, -6, -3, 1, 7, 15],\n", + " [-11, -10, -8, -6, -6, -5, -2, 6, 15],\n", + " [-5, -3, -2, -2, -4, -5, -2, 6, 15],\n", + " ]\n", + ")\n", + "\n", + "wind_barbs = gv.WindBarbs.from_uv((lon, lat, uwnd, vwwnd)).opts(\n", + " fig_size=250, length=6.5, padding=1\n", + ")\n", + "coastline = gv.feature.coastline()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "wind_barbs * coastline" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/geoviews/__init__.py b/geoviews/__init__.py index 427abffc..c8a46be5 100644 --- a/geoviews/__init__.py +++ b/geoviews/__init__.py @@ -11,7 +11,7 @@ _Element, Feature, Tiles, WMTS, LineContours, FilledContours, Text, Image, Points, Path, Polygons, Shape, Dataset, RGB, Contours, Graph, TriMesh, Nodes, EdgePaths, QuadMesh, VectorField, - HexTiles, Labels, Rectangles, Segments + HexTiles, Labels, Rectangles, Segments, WindBarbs ) from .util import load_tiff, from_xarray # noqa (API import) from .operation import project # noqa (API import) diff --git a/geoviews/element/__init__.py b/geoviews/element/__init__.py index 8c0a3abd..6114b8a6 100644 --- a/geoviews/element/__init__.py +++ b/geoviews/element/__init__.py @@ -7,7 +7,7 @@ WMTS, Points, Image, Text, LineContours, RGB, FilledContours, Path, Polygons, Shape, Dataset, Contours, TriMesh, Graph, Nodes, EdgePaths, QuadMesh, - VectorField, Labels, HexTiles, Rectangles, Segments) + VectorField, Labels, HexTiles, Rectangles, Segments, WindBarbs) class GeoConversion(ElementConversion): diff --git a/geoviews/element/comparison.py b/geoviews/element/comparison.py index 1e9650ee..8b6d148c 100644 --- a/geoviews/element/comparison.py +++ b/geoviews/element/comparison.py @@ -2,7 +2,7 @@ from holoviews.element.comparison import Comparison as HvComparison -from .geo import Image, Points, LineContours, FilledContours +from .geo import Image, Points, LineContours, FilledContours, WindBarbs class Comparison(HvComparison): @@ -13,6 +13,7 @@ def register(cls): cls.equality_type_funcs[Points] = cls.compare_dataset cls.equality_type_funcs[LineContours] = cls.compare_dataset cls.equality_type_funcs[FilledContours] = cls.compare_dataset + cls.equality_type_funcs[WindBarbs] = cls.compare_dataset return cls.equality_type_funcs diff --git a/geoviews/element/geo.py b/geoviews/element/geo.py index 8a88daab..f055ce49 100644 --- a/geoviews/element/geo.py +++ b/geoviews/element/geo.py @@ -15,8 +15,10 @@ QuadMesh as HvQuadMesh, Points as HvPoints, VectorField as HvVectorField, HexTiles as HvHexTiles, Labels as HvLabels, Rectangles as HvRectangles, - Segments as HvSegments + Segments as HvSegments, Geometry as HvGeometry, ) +from holoviews.element.selection import Selection2DExpr + from shapely.geometry.base import BaseGeometry from shapely.geometry import ( @@ -340,6 +342,42 @@ class VectorField(_Element, HvVectorField): Dimension('Magnitude')], bounds=(1, None)) +class WindBarbs(_Element, Selection2DExpr, HvGeometry): + """ + """ + + group = param.String(default='WindBarbs', constant=True) + + vdims = param.List(default=[Dimension('Angle', cyclic=True, range=(0,2*np.pi)), + Dimension('Magnitude')], bounds=(2, None)) + + @classmethod + def from_uv(cls, data, kdims=None, vdims=None, **params): + if isinstance(data, tuple): + xs, ys, us, vs = data + else: + us = data[vdims[0]] + vs = data[vdims[1]] + + uv_magnitudes = np.hypot(us, vs) # unscaled + radians = np.pi / 2 - np.arctan2(-vs, -us) + + if isinstance(data, tuple): + reorganized_data = (xs, ys, radians, uv_magnitudes) + else: + # calculations on this data could mutate the original data + # here we do not do any calculations; we only store the data + reorganized_data = {} + for kdim in kdims: + reorganized_data[kdim] = data[kdim] + reorganized_data["Angle"] = radians + reorganized_data["Magnitude"] = uv_magnitudes + for vdim in vdims[2:]: + reorganized_data[vdim] = data[vdim] + vdims = ["Angle", "Magnitude"] + vdims[2:] + return cls(reorganized_data, kdims=kdims, vdims=vdims, **params) + + class Image(_Element, HvImage): """ Image represents a 2D array of some quantity with diff --git a/geoviews/plotting/mpl/__init__.py b/geoviews/plotting/mpl/__init__.py index 90440a58..b2144c7b 100644 --- a/geoviews/plotting/mpl/__init__.py +++ b/geoviews/plotting/mpl/__init__.py @@ -30,7 +30,7 @@ Image, Points, Feature, WMTS, Tiles, Text, LineContours, FilledContours, is_geographic, Path, Polygons, Shape, RGB, Contours, Nodes, EdgePaths, Graph, TriMesh, QuadMesh, VectorField, - HexTiles, Labels, Rectangles, Segments + HexTiles, Labels, Rectangles, Segments, WindBarbs ) from ...util import geo_mesh, poly_types from ..plot import ProjectionPlot @@ -39,6 +39,7 @@ project_points, project_path, project_graph, project_quadmesh, project_geom ) +from .chart import WindBarbsPlot @@ -327,6 +328,16 @@ class GeoVectorFieldPlot(GeoPlot, VectorFieldPlot): _project_operation = project_points +class GeoWindBarbsPlot(GeoPlot, WindBarbsPlot): + """ + Draws a wind barbs plot from the data in a WindBarbs Element. + """ + + apply_ranges = param.Boolean(default=True) + + _project_operation = project_points + + class GeometryPlot(GeoPlot): def init_artists(self, ax, plot_args, plot_kwargs): @@ -567,6 +578,7 @@ def draw_annotation(self, axis, data, crs, opts): Points: GeoPointPlot, Labels: GeoLabelsPlot, VectorField: GeoVectorFieldPlot, + WindBarbs: GeoWindBarbsPlot, Text: GeoTextPlot, Layout: LayoutPlot, NdLayout: LayoutPlot, diff --git a/geoviews/plotting/mpl/chart.py b/geoviews/plotting/mpl/chart.py new file mode 100644 index 00000000..a90f5a53 --- /dev/null +++ b/geoviews/plotting/mpl/chart.py @@ -0,0 +1,144 @@ +import param +import numpy as np +from holoviews.plotting.mpl.element import ColorbarPlot +from holoviews.util.transform import dim +from holoviews.core.options import abbreviated_exception + + +class WindBarbsPlot(ColorbarPlot): + """ + Barbs are traditionally used in meteorology as a way to plot the speed and + direction of wind observations, but can technically be used to plot any two + dimensional vector quantity. As opposed to arrows, which give vector + magnitude by the length of the arrow, the barbs give more quantitative + information about the vector magnitude by putting slanted lines or a + triangle for various increments in magnitude. + + The largest increment is given by a triangle (or "flag"). After those come full lines (barbs). + The smallest increment is a half line. There is only, of course, ever at most 1 half line. + If the magnitude is small and only needs a single half-line and no full lines or + triangles, the half-line is offset from the end of the barb so that it can be + easily distinguished from barbs with a single full line. The magnitude for the barb + shown above would nominally be 65, using the standard increments of 50, 10, and 5. + """ + + padding = param.ClassSelector(default=0.05, class_=(int, float, tuple)) + + convention = param.ObjectSelector(objects=["from", "to"], doc=""" + Convention to return direction; 'from' returns the direction the wind is coming from + (meteorological convention), 'to' returns the direction the wind is going towards + (oceanographic convention).""") + + style_opts = [ + "alpha", + "color", + "edgecolors", + "facecolors", + "linewidth", + "marker", + "visible", + "cmap", + "norm", + # barb specific + "length", + "barbcolor", + "flagcolor", + "fill_empty", + "rounding", + "barb_increments", + "flip_barb", + "pivot", + "sizes", + "width", + ] + + _nonvectorized_styles = [ + "alpha", + "marker", + "cmap", + "visible", + "norm", + # TODO: clarify whether these are vectorized or not + "length", + "barbcolor", + "flagcolor", + "fill_empty", + "rounding", + "barb_increments", + "flip_barb", + "pivot", + "sizes", + "width", + ] + + _plot_methods = dict(single="barbs") + + def _get_us_vs(self, element): + radians = element.dimension_values(2) if len(element.data) else [] + + mag_dim = element.get_dimension(3) + if isinstance(mag_dim, dim): + magnitudes = mag_dim.apply(element, flat=True) + else: + magnitudes = element.dimension_values(mag_dim) + + if self.convention == "to": + radians -= np.pi + + if self.invert_axes: + radians -= 0.5 * np.pi + + us = -magnitudes * np.sin(radians.flatten()) + vs = -magnitudes * np.cos(radians.flatten()) + + return us, vs + + def get_data(self, element, ranges, style): + # Compute coordinates + xidx, yidx = (1, 0) if self.invert_axes else (0, 1) + xs = element.dimension_values(xidx) if len(element.data) else [] + ys = element.dimension_values(yidx) if len(element.data) else [] + + # Compute U and V components as required by matplotlib plt.barbs + us, vs = self._get_us_vs(element) + args = (xs, ys, us, vs) + + color = style.get('color', None) # must do before apply transform + flagcolor = style.get('flagcolor', None) + barbcolor = style.get('barbcolor', None) + + # Process style + with abbreviated_exception(): + style = self._apply_transforms(element, ranges, style) + uses_color = ((isinstance(color, str) and color in element) or isinstance(color, dim)) + if uses_color and (flagcolor is not None or barbcolor is not None): + self.param.warning( + "Cannot declare style mapping for 'color' option and either " + "'flagcolor' and 'barbcolor'; ignoring 'flagcolor' and 'barbcolor'.") + style.pop('flagcolor', None) + style.pop('barbcolor', None) + if "vmin" in style: + style["clim"] = (style.pop("vmin"), style.pop("vmax")) + if "c" in style: + style["array"] = style.pop("c") + if "pivot" not in style: + style["pivot"] = "tip" + return args, style, {} + + def update_handles(self, key, axis, element, ranges, style): + args, style, axis_kwargs = self.get_data(element, ranges, style) + + barbs = self.handles["artist"] + barbs.set_offsets(np.column_stack(args[:2])) + if "color" in style: + if "flagcolor" not in style: + barbs.set_facecolors(style["color"]) + if "barbcolor" not in style: + barbs.set_edgecolors(style["color"]) + if "array" in style: + barbs.set_array(style["array"]) + if "clim" in style: + barbs.set_clim(style["clim"]) + if "linewidth" in style: + barbs.set_linewidths(style["linewidth"]) + return axis_kwargs diff --git a/geoviews/tests/plotting/mpl/test_chart.py b/geoviews/tests/plotting/mpl/test_chart.py new file mode 100644 index 00000000..ed48fc63 --- /dev/null +++ b/geoviews/tests/plotting/mpl/test_chart.py @@ -0,0 +1,197 @@ +import numpy as np +import xarray as xr +import geoviews as gv + +from geoviews.element import WindBarbs + +from holoviews.tests.plotting.utils import ParamLogStream + +from geoviews import Store + +from test_plot import TestMPLPlot + +mpl_renderer = Store.renderers["matplotlib"] + + +class TestWindBarbsPlot(TestMPLPlot): + def test_windbarbs(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 5 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + + gv_barbs = WindBarbs((X, Y, angle, mag)) + + fig = gv.render(gv_barbs) + mpl_barbs = fig.axes[0].get_children()[0] + np.testing.assert_almost_equal(mpl_barbs.u.data, U.T.flatten()) + np.testing.assert_almost_equal(mpl_barbs.v.data, V.T.flatten()) + + def test_windbarbs_dataset(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 1 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + ds = xr.Dataset( + { + "u": (["y", "x"], U), + "v": (["y", "x"], V), + "a": (["y", "x"], angle), + "m": (["y", "x"], mag), + }, + coords={"x": x, "y": x}, + ) + + gv_barbs = gv.WindBarbs(ds, ["x", "y"], ["a", "m"]) + + fig = gv.render(gv_barbs) + mpl_barbs = fig.axes[0].get_children()[0] + np.testing.assert_almost_equal(mpl_barbs.u.data, U.T.flatten()) + np.testing.assert_almost_equal(mpl_barbs.v.data, V.T.flatten()) + + def test_windbarbs_from_uv(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 2 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + + gv_barbs = WindBarbs((X, Y, angle, mag)) + gv_barbs_uv = WindBarbs.from_uv((X, Y, U, V)) + + np.testing.assert_almost_equal(gv_barbs.data["Angle"], gv_barbs_uv.data["Angle"]) + np.testing.assert_almost_equal(gv_barbs.data["Magnitude"], gv_barbs_uv.data["Magnitude"]) + + def test_windbarbs_dataset_from_uv_other_dim(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 3 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + ds = xr.Dataset( + { + "u": (["y", "x"], U), + "v": (["y", "x"], V), + "a": (["y", "x"], angle), + "m": (["y", "x"], mag), + "other": (["y", "x"], np.ones_like(mag)), + }, + coords={"x": x, "y": -x}, + ) + + gv_barbs = WindBarbs.from_uv(ds, ["x", "y"], ["u", "v", "other"]) + assert "other" in gv_barbs.data + + def test_windbarbs_color_op(self): + barbs = WindBarbs( + [(0, 0, 0, 1, "#000000"), (0, 1, 0, 1, "#FF0000"), (0, 2, 0, 1, "#00FF00")], + vdims=["A", "M", "color"], + ).opts(color="color") + plot = mpl_renderer.get_plot(barbs) + artist = plot.handles["artist"] + self.assertEqual( + artist.get_facecolors(), + np.array([[0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1]]), + ) + + def test_windbarbs_both_flagcolor_barbcolor(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 4 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + + barbs = gv.WindBarbs((X, Y, angle, mag)).opts( + colorbar=True, clim=(0, 50), flagcolor="red", barbcolor="blue" + ) + plot = mpl_renderer.get_plot(barbs) + artist = plot.handles["artist"] + np.testing.assert_almost_equal( + # red + artist.get_facecolor(), + np.array([[1.0, 0.0, 0.0, 1.0]]), + ) + np.testing.assert_almost_equal( + # blue + artist.get_edgecolor(), + np.array([[0.0, 0.0, 1.0, 1.0]]), + ) + + def test_windbarbs_flagcolor(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 5 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + + barbs = gv.WindBarbs((X, Y, angle, mag)).opts( + colorbar=True, clim=(0, 50), flagcolor="red" + ) + plot = mpl_renderer.get_plot(barbs) + artist = plot.handles["artist"] + np.testing.assert_almost_equal( + # red (RGBA) + artist.get_facecolor(), + np.array([[1.0, 0.0, 0.0, 1.0]]), + ) + np.testing.assert_almost_equal( + # red + artist.get_edgecolor(), + np.array([[1.0, 0.0, 0.0, 1.0]]), + ) + + def test_windbarbs_barbcolor(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 6 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + + barbs = gv.WindBarbs((X, Y, angle, mag)).opts( + colorbar=True, clim=(0, 50), barbcolor="red" + ) + plot = mpl_renderer.get_plot(barbs) + artist = plot.handles["artist"] + np.testing.assert_almost_equal( + # red (RGBA) + artist.get_facecolor(), + np.array([[1.0, 0.0, 0.0, 1.0]]), + ) + np.testing.assert_almost_equal( + # red + artist.get_edgecolor(), + np.array([[1.0, 0.0, 0.0, 1.0]]), + ) + + def test_windbarbs_color_warning(self): + x = np.linspace(-1, 1, 4) + X, Y = np.meshgrid(x, x) + U, V = 10 * X, 7 * Y + + angle = np.pi / 2 - np.arctan2(-V, -U) + mag = np.hypot(U, V) + + barbs = gv.WindBarbs((X, Y, angle, mag)).opts( + colorbar=True, + clim=(0, 50), + color="Magnitude", + flagcolor="black", + barbcolor="black", + ) + with ParamLogStream() as log: + mpl_renderer.get_plot(barbs) + log_msg = log.stream.read() + warning = ( + "Cannot declare style mapping for 'color' option and either " + "'flagcolor' and 'barbcolor'; ignoring 'flagcolor' and 'barbcolor'.\n" + ) + self.assertEqual(log_msg, warning) diff --git a/geoviews/tests/plotting/mpl/test_plot.py b/geoviews/tests/plotting/mpl/test_plot.py new file mode 100644 index 00000000..8322f118 --- /dev/null +++ b/geoviews/tests/plotting/mpl/test_plot.py @@ -0,0 +1,29 @@ +import matplotlib.pyplot as plt +import pyviz_comms as comms + +from geoviews import Store +from geoviews.element.comparison import ComparisonTestCase +from geoviews.plotting.mpl import ElementPlot +from param import concrete_descendents + +mpl_renderer = Store.renderers['matplotlib'] + + +class TestMPLPlot(ComparisonTestCase): + + def setUp(self): + self.previous_backend = Store.current_backend + self.comm_manager = mpl_renderer.comm_manager + mpl_renderer.comm_manager = comms.CommManager + Store.set_current_backend('matplotlib') + self._padding = {} + for plot in concrete_descendents(ElementPlot).values(): + self._padding[plot] = plot.padding + plot.padding = 0 + + def tearDown(self): + Store.current_backend = self.previous_backend + mpl_renderer.comm_manager = self.comm_manager + plt.close(plt.gcf()) + for plot, padding in self._padding.items(): + plot.padding = padding