Skip to content

Restore matplotlib backend after HoloViews matplotlib plot#1538

Open
rajeeja wants to merge 6 commits into
mainfrom
rajeeja/fix_matplotlib_backend
Open

Restore matplotlib backend after HoloViews matplotlib plot#1538
rajeeja wants to merge 6 commits into
mainfrom
rajeeja/fix_matplotlib_backend

Conversation

@rajeeja

@rajeeja rajeeja commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Calling plot(backend="matplotlib") runs hv.extension("matplotlib"), which switches the active matplotlib backend and clobbers the IPython inline display hook, silently breaking any subsequent native matplotlib/xarray .plot() calls. This restores the original matplotlib backend right after the HoloViews extension switch, which is safe because HoloViews objects display through Store.current_backend rather than the active matplotlib backend. Verified in real Jupyter kernels that the reported sequence now works, with a new regression test and all plotting tests/relevant docs notebooks passing; closes #1537.

plot(backend='matplotlib') calls hv.extension('matplotlib'), which switches
the active matplotlib backend and clobbers the IPython inline display hook,
silently breaking subsequent native matplotlib/xarray .plot() calls. Restore
the original matplotlib backend right after the HoloViews extension switch;
HoloViews objects still display via Store.current_backend, so this is safe.

Closes #1537

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Jupyter/IPython plotting regression where uxarray.plot(..., backend="matplotlib") triggers hv.extension("matplotlib"), which can alter Matplotlib’s active backend and break subsequent native matplotlib/xarray plotting in the same session.

Changes:

  • Restore the Matplotlib backend immediately after switching HoloViews to the matplotlib backend.
  • Update reset_mpl_backend() documentation to describe intended behavior.
  • Add a regression test covering backend restoration after a UXarray matplotlib-backed plot.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
uxarray/plot/utils.py Adds post-hv.extension("matplotlib") backend restoration and updates backend reset docstring.
test/test_plot.py Adds a regression test asserting Matplotlib backend state and subsequent xarray plotting still works.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread uxarray/plot/utils.py
Comment thread uxarray/plot/utils.py
Comment thread test/test_plot.py Outdated
@cmdupuis3 cmdupuis3 added the run-benchmark Run ASV benchmark workflow label Jul 2, 2026
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [a7509c3] After [1cd2a86] Ratio Benchmark (Parameter)
- 578M 390M 0.68 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
- 699M 390M 0.56 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
- 496M 384M 0.77 mpas_ocean.Gradient.peakmem_gradient('480km')
- 4.73±0.02ms 4.29±0.02ms 0.91 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping

Benchmarks that have stayed the same:

Change Before [a7509c3] After [1cd2a86] Ratio Benchmark (Parameter)
10.5±0.03μs 10.7±0.1μs 1.02 bench_connectivity.Connectivity.time_edge_face('120km')
10.8±0.1μs 10.8±0.2μs 1 bench_connectivity.Connectivity.time_edge_face('480km')
10.6±0.02μs 10.6±0.1μs 1 bench_connectivity.Connectivity.time_edge_node('120km')
11.0±0.2μs 11.1±0.1μs 1.01 bench_connectivity.Connectivity.time_edge_node('480km')
10.5±0.08μs 10.5±0.03μs 1 bench_connectivity.Connectivity.time_face_edge('120km')
11.0±0.1μs 11.0±0.1μs 1 bench_connectivity.Connectivity.time_face_edge('480km')
10.4±0.2μs 10.7±0.2μs 1.03 bench_connectivity.Connectivity.time_face_face('120km')
11.2±0.07μs 11.1±0.2μs 0.99 bench_connectivity.Connectivity.time_face_face('480km')
22.1±0.1μs 21.5±0.2μs 0.97 bench_connectivity.Connectivity.time_face_node('120km')
22.1±0.4μs 22.1±0.3μs 1 bench_connectivity.Connectivity.time_face_node('480km')
10.6±0.04μs 11.1±0.5μs 1.05 bench_connectivity.Connectivity.time_node_edge('120km')
11.2±0.2μs 11.2±0.06μs 1 bench_connectivity.Connectivity.time_node_edge('480km')
10.5±0.07μs 10.7±0.08μs 1.02 bench_connectivity.Connectivity.time_node_face('120km')
11.3±0.2μs 11.1±0.1μs 0.98 bench_connectivity.Connectivity.time_node_face('480km')
389M 389M 1 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
421M 419M 0.99 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
14.2±0.2ms 14.4±0.09ms 1.01 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
3.68±0.05ms 3.70±0.02ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
18.9±0.01ms 18.8±0.08ms 1 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.21±0.03ms 2.18±0.02ms 0.98 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.05±0.03s 1.04±0.02s 1 import.Imports.timeraw_import_uxarray
930±10ns 919±5ns 0.99 mpas_ocean.CheckNorm.time_check_norm('120km')
899±3ns 904±20ns 1.01 mpas_ocean.CheckNorm.time_check_norm('480km')
825±10ms 819±20ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
54.0±0.4ms 54.7±1ms 1.01 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
653±20μs 652±9μs 1 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
575±7μs 589±20μs 1.02 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.40±0.04ms 5.38±0.05ms 1 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.90±0.05ms 3.90±0.03ms 1 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.48±0.02s 3.45±0s 0.99 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
226±0.9ms 219±1ms 0.97 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.1±0.02ms 18.1±0.04ms 1 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
1.01±0.02ms 1.03±0.02ms 1.02 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.6±0.03ms 10.5±0.02ms 1 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
691±30μs 707±8μs 1.02 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
709±9ms 705±3ms 0.99 mpas_ocean.CrossSections.time_const_lat('120km', 1)
353±5ms 355±0.4ms 1.01 mpas_ocean.CrossSections.time_const_lat('120km', 2)
184±1ms 184±1ms 1 mpas_ocean.CrossSections.time_const_lat('120km', 4)
543±5ms 546±9ms 1.01 mpas_ocean.CrossSections.time_const_lat('480km', 1)
274±2ms 274±3ms 1 mpas_ocean.CrossSections.time_const_lat('480km', 2)
141±0.7ms 141±0.3ms 1 mpas_ocean.CrossSections.time_const_lat('480km', 4)
24.5±0.08ms 24.4±0.1ms 1 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
3.18±0.06ms 3.19±0.07ms 1 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
951±5ms 941±8ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
53.3±1ms 53.0±2ms 0.99 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
83.8±0.9ms 82.5±0.3ms 0.98 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.84±0.3ms 5.49±0.2ms 0.94 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
404M 404M 1 mpas_ocean.Gradient.peakmem_gradient('120km')
175±0.3ms 176±0.6ms 1.01 mpas_ocean.Gradient.time_gradient('120km')
12.1±0.09ms 12.4±0.1ms 1.02 mpas_ocean.Gradient.time_gradient('480km')
221±0.8μs 223±0.7μs 1.01 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
131±0.5μs 134±1μs 1.02 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
353M 351M 0.99 mpas_ocean.Integrate.peakmem_integrate('120km')
330M 331M 1 mpas_ocean.Integrate.peakmem_integrate('480km')
217±1μs 213±1μs 0.98 mpas_ocean.Integrate.time_integrate('120km')
199±1μs 200±0.9μs 1.01 mpas_ocean.Integrate.time_integrate('480km')
130±1ms 130±0.9ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
132±3ms 131±1ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
132±1ms 132±0.3ms 1 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
10.4±0.09ms 10.3±0.1ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
10.6±0.1ms 10.5±0.04ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
10.5±0.2ms 10.3±0.1ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
351±2μs 358±2μs 1.02 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
352±5μs 358±3μs 1.02 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
335±4μs 337±0.7μs 1.01 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
336±1μs 336±3μs 1 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
243±0.7ms 245±2ms 1.01 mpas_ocean.RemapDownsample.time_bilinear_remapping
301±4ms 296±3ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
1.44±0s 1.43±0s 0.99 mpas_ocean.RemapUpsample.time_bilinear_remapping
36.6±0.4ms 35.9±0.4ms 0.98 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
9.33±0.07ms 9.35±0.07ms 1 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
28.5±0.2ms 28.3±0.3ms 0.99 mpas_ocean.ZonalAverage.time_zonal_average('120km')
6.91±0.3ms 6.39±0.01ms 0.93 mpas_ocean.ZonalAverage.time_zonal_average('480km')
326M 328M 1.01 quad_hexagon.QuadHexagon.peakmem_open_dataset
326M 325M 1 quad_hexagon.QuadHexagon.peakmem_open_grid
7.21±0.2ms 6.92±0.07ms 0.96 quad_hexagon.QuadHexagon.time_open_dataset
5.99±0.1ms 5.95±0.06ms 0.99 quad_hexagon.QuadHexagon.time_open_grid

@Sevans711

Copy link
Copy Markdown
Collaborator

I was taking a quick look to see if I could review and get this merged to main, but I ended up with a couple quick questions.

(1) Can you provide a small example that definitely causes the unexpected behavior mentioned in #1537 before this fix, and does not cause it anymore after this fix?

On main currently (i.e. without this fix yet), in a Jupyter notebook, I ran the following, putting each plot() call in its own cell. I tried in Python 3.10 and Python 3.13. In both cases, every plot call displays a plot; I can't reproduce the failure to make plots. Each of these calls successfully makes a plot.

import uxarray as ux
uxds = ux.tutorial.open_dataset("outCSne30-vortex")
uxds["psi"].plot(backend='matplotlib')  # makes a matplotlib-style plot
uxds["psi"].plot()  # makes a matplotlib-style plot
# try switching to bokeh just in case it is related to switching back and forth:
uxds["psi"].plot(backend='bokeh')  # makes a bokeh-style plot
uxds["psi"].plot()  # makes a bokeh-style plot
# switch back again:
uxds["psi"].plot(backend='matplotlib')  # makes a matplotlib-style plot
uxds["psi"].plot()  # makes a matplotlib-style plot

(2) Is it desirable for the backend to be changed "permanently" instead of just a temporary change for the current plot? I naively would expect a kwarg being passed to plot() to only affect behavior for that one plot, not to affect global state. E.g., I would expect the following:

import holoviews as hv
hv.extension('bokeh')
uxds["psi"].plot()  # makes a bokeh-style plot
uxds["psi"].plot(backend='matplotlib')  # makes a matplotlib-style plot
uxds["psi"].plot()  # I would expect a bokeh-style plot, but actually it is matplotlib-style.

Please let me know if this second point belongs in a separate issue instead.

@rajeeja

rajeeja commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. I’ll keep this separate from #1541. Test added.

@Sevans711

Copy link
Copy Markdown
Collaborator

I agree this should stay separate from #1541. I thought both of my points were unrelated to 1541, though.

For (1), I see you added regression tests, with good comments detailing how they aren't actually producing a real Jupyter notebook to see the issue occur (that would be challenging to set up with pytest). Were you able to reproduce the original issue on a real Jupyter notebook before this fix?

Ideally, I would like to run a small piece of code which can reproduce the issue when in main in a real Jupyter notebook; then change to this branch, rerun the code, and see that the issue does not occur anymore.

@rajeeja rajeeja moved this to 👀 In review in UXarray Development Jul 8, 2026
@Sevans711

Copy link
Copy Markdown
Collaborator

Ah, I misread the original bug. It isn't saying that uxarray.UxDataArray.plot() fails, it's saying that xarray.DataArray.plot() fails. I believe your changes so far have not fixed this bug. From within this branch, I still see the bug:

Screenshot 2026-07-09 at 11 15 13 AM

What I would expect, once the bug has been fixed, would be for the xr.DataArray([1,2,3]).plot() cell to show a plot, like it does if it is run before calling any uxarray plotting routines:

Screenshot 2026-07-09 at 11 11 51 AM

@rajeeja rajeeja requested a review from Sevans711 July 9, 2026 15:52
@rajeeja

rajeeja commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Yes — I reproduced this in notebook-style execution.

Minimal check:

%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
import uxarray as ux

uxds = ux.tutorial.open_dataset("outCSne30-vortex")
print("initial:", mpl.get_backend())
uxds["psi"].plot(backend="matplotlib")
print("after uxarray:", mpl.get_backend())

plt.figure()
plt.plot([0, 1], [0, 1])
plt.show()

On main, the UXarray plot changes Matplotlib from inline to agg, and the later native Matplotlib plot warns that FigureCanvasAgg is non-interactive.

On this branch, the backend is restored to inline, and the later native Matplotlib plot displays normally.

@Sevans711

Copy link
Copy Markdown
Collaborator

Ah, I can confirm, if I explicitly include %matplotlib inline then things work on this branch and not on main.

However, when I don't include it, even on this branch, the results are that no plot gets shown:
Screenshot 2026-07-09 at 12 54 42 PM

I basically never explicitly write out %matplotlib inline in my notebooks. Can we make a fix that works even when you don't include that line?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-benchmark Run ASV benchmark workflow

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

simple uxarray.plot(backend="matplotlib") seems to break subsequent xarray.plot functionality

5 participants