Skip to content
Open
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
28 changes: 28 additions & 0 deletions test/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,31 @@ def test_to_raster_pixel_ratio(gridpath, r1, r2):
f = r2 / r1
d = np.array(raster2.shape) - f * np.array(raster1.shape)
assert (d >= 0).all() and (d <= f - 1).all()


def test_matplotlib_backend_restored_after_switch(monkeypatch):
"""Regression test for #1537: switching HoloViews to matplotlib must restore
the Matplotlib backend captured beforehand.

A bare pytest can't reproduce the Jupyter inline-hook flip, so we simulate
hv.extension flipping the backend and assert assign() restores it.
"""
import holoviews as hv

from uxarray.plot.utils import HoloviewsBackend

original = matplotlib.get_backend()
try:
matplotlib.use("svg") # the "user's" backend before plotting

# Simulate hv.extension("matplotlib") flipping the active backend to agg.
monkeypatch.setattr(hv.Store, "current_backend", "bokeh", raising=False)
monkeypatch.setattr(hv, "extension", lambda *a, **k: matplotlib.use("agg"))

be = HoloviewsBackend()
be.assign("matplotlib")

# assign() must restore the backend that was active before the switch.
assert matplotlib.get_backend() == "svg"
finally:
matplotlib.use(original)
35 changes: 18 additions & 17 deletions uxarray/plot/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,37 @@


class HoloviewsBackend:
"""Utility class to compare and set a HoloViews plotting backend for
visualization."""
"""Compare and set the HoloViews plotting backend."""

def __init__(self):
self.matplotlib_backend = None

def assign(self, backend: str):
"""Assigns a backend for use with HoloViews visualization.

Parameters
----------
backend : str
Plotting backend to use, one of 'matplotlib', 'bokeh'
"""

if self.matplotlib_backend is None:
import matplotlib as mpl

self.matplotlib_backend = mpl.get_backend()

"""Assign a HoloViews backend, one of 'matplotlib', 'bokeh'."""
if backend not in ["bokeh", "matplotlib", None]:
raise ValueError(
f"Unsupported backend. Expected one of ['bokeh', 'matplotlib'], but received {backend}"
)
if backend is not None and backend != hv.Store.current_backend:
# only call hv.extension if it needs to be changed
import matplotlib as mpl

# Capture the live backend now (not once at init) so a backend the
# user set later is what we restore.
self.matplotlib_backend = mpl.get_backend()
hv.extension(backend)

if backend == "matplotlib":
# hv.extension("matplotlib") switches the active Matplotlib
# backend (e.g. to agg in Jupyter), breaking subsequent native
# matplotlib/xarray .plot() calls. HoloViews renders through
# hv.Store.current_backend, so restoring is safe. See #1537.
self.reset_mpl_backend()

def reset_mpl_backend(self):
"""Resets the default backend for the ``matplotlib`` module."""
"""Switch Matplotlib back to the backend captured before the last switch."""
if self.matplotlib_backend is None:
return

import matplotlib as mpl

mpl.use(self.matplotlib_backend)
Comment thread
rajeeja marked this conversation as resolved.
Expand Down
Loading