Repository: mne-tools/mne-python

Issue:
Report.add_figure() should not close the passed figures
I think if MNE-report creates a figure, it should auto-close, but if user created the figure (as in `add_figure`), it should not close, by default.

_Originally posted by @jasmainak in https://github.com/mne-tools/mne-python/issues/10164#issuecomment-1004045549_


Discussion:
+1

> Message ID: ***@***.***>
>

@hoechenberger I can do this early next week unless you want to do it
If you've got the time, please go ahead! I believe the fix should be simple – in `add_figure()`, pass `auto_close=False` to the function that converts figures to images

Resolving patch:
diff --git a/mne/report/report.py b/mne/report/report.py
--- a/mne/report/report.py
+++ b/mne/report/report.py
@@ -327,16 +327,20 @@ def _constrain_fig_resolution(fig, *, max_width, max_res):
     fig.set_dpi(dpi)
 
 
-def _fig_to_img(fig, *, image_format='png', auto_close=True):
+def _fig_to_img(fig, *, image_format='png', own_figure=True):
     """Plot figure and create a binary image."""
     # fig can be ndarray, mpl Figure, PyVista Figure
     import matplotlib.pyplot as plt
     from matplotlib.figure import Figure
     if isinstance(fig, np.ndarray):
+        # In this case, we are creating the fig, so we might as well
+        # auto-close in all cases
         fig = _ndarray_to_fig(fig)
-        _constrain_fig_resolution(
-            fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES
-        )
+        if own_figure:
+            _constrain_fig_resolution(
+                fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES
+            )
+        own_figure = True  # close the figure we just created
     elif not isinstance(fig, Figure):
         from ..viz.backends.renderer import backend, MNE_3D_BACKEND_TESTING
         backend._check_3d_figure(figure=fig)
@@ -345,12 +349,14 @@ def _fig_to_img(fig, *, image_format='png', auto_close=True):
         else:  # Testing mode
             img = np.zeros((2, 2, 3))
 
-        if auto_close:
+        if own_figure:
             backend._close_3d_figure(figure=fig)
         fig = _ndarray_to_fig(img)
-        _constrain_fig_resolution(
-            fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES
-        )
+        if own_figure:
+            _constrain_fig_resolution(
+                fig, max_width=MAX_IMG_WIDTH, max_res=MAX_IMG_RES
+            )
+        own_figure = True  # close the fig we just created
 
     output = BytesIO()
     logger.debug(
@@ -366,7 +372,7 @@ def _fig_to_img(fig, *, image_format='png', auto_close=True):
         )
         fig.savefig(output, format=image_format, dpi=fig.get_dpi())
 
-    if auto_close:
+    if own_figure:
         plt.close(fig)
     output = output.getvalue()
     return (output.decode('utf-8') if image_format == 'svg' else
@@ -1976,7 +1982,8 @@ def add_figure(self, fig, title, *, caption=None, image_format=None,
 
         assert figs
         if len(figs) == 1:
-            img = _fig_to_img(fig=figs[0], image_format=image_format)
+            img = _fig_to_img(fig=figs[0], image_format=image_format,
+                              own_figure=False)
             dom_id = self._get_dom_id()
             html = _html_image_element(
                 img=img, div_klass='custom-image', img_klass='custom-image',
@@ -1986,7 +1993,8 @@ def add_figure(self, fig, title, *, caption=None, image_format=None,
         else:
             html, dom_id = self._render_slider(
                 figs=figs, imgs=None, title=title, captions=captions,
-                start_idx=0, image_format=image_format, tags=tags
+                start_idx=0, image_format=image_format, tags=tags,
+                own_figure=False
             )
 
         self._add_or_replace(
@@ -2123,7 +2131,7 @@ def add_bem(self, subject, title, *, subjects_dir=None, decim=2, width=512,
         )
 
     def _render_slider(self, *, figs, imgs, title, captions, start_idx,
-                       image_format, tags, klass=''):
+                       image_format, tags, klass='', own_figure=True):
         if figs is not None and imgs is not None:
             raise ValueError('Must only provide either figs or imgs')
 
@@ -2137,10 +2145,10 @@ def _render_slider(self, *, figs, imgs, title, captions, start_idx,
                 f'Number of captions ({len(captions)}) must be equal to the '
                 f'number of images ({len(imgs)})'
             )
-        elif figs:
-            imgs = [_fig_to_img(fig=fig, image_format=image_format)
+        elif figs:  # figs can be None if imgs is provided
+            imgs = [_fig_to_img(fig=fig, image_format=image_format,
+                                own_figure=own_figure)
                     for fig in figs]
-
         dom_id = self._get_dom_id()
         html = _html_slider_element(
             id=dom_id,


Test patch:
diff --git a/mne/report/tests/test_report.py b/mne/report/tests/test_report.py
--- a/mne/report/tests/test_report.py
+++ b/mne/report/tests/test_report.py
@@ -356,16 +356,22 @@ def test_report_raw_psd_and_date(tmp_path):
 @testing.requires_testing_data
 def test_render_add_sections(renderer, tmp_path):
     """Test adding figures/images to section."""
+    from pyvista.plotting import plotting
     tempdir = str(tmp_path)
     report = Report(subjects_dir=subjects_dir)
     # Check add_figure functionality
+    plt.close('all')
+    assert len(plt.get_fignums()) == 0
     fig = plt.plot([1, 2], [1, 2])[0].figure
+    assert len(plt.get_fignums()) == 1
 
     report.add_figure(fig=fig, title='evoked response', image_format='svg')
     assert 'caption' not in report._content[-1].html
+    assert len(plt.get_fignums()) == 1
 
     report.add_figure(fig=fig, title='evoked with caption', caption='descr')
     assert 'caption' in report._content[-1].html
+    assert len(plt.get_fignums()) == 1
 
     # Check add_image with png
     img_fname = op.join(tempdir, 'testimage.png')
@@ -377,10 +383,14 @@ def test_render_add_sections(renderer, tmp_path):
 
     evoked = read_evokeds(evoked_fname, condition='Left Auditory',
                           baseline=(-0.2, 0.0))
+    n_before = len(plotting._ALL_PLOTTERS)
     fig = plot_alignment(evoked.info, trans_fname, subject='sample',
                          subjects_dir=subjects_dir)
+    n_after = n_before + 1
+    assert n_after == len(plotting._ALL_PLOTTERS)
 
     report.add_figure(fig=fig, title='random image')
+    assert n_after == len(plotting._ALL_PLOTTERS)  # not closed
     assert (repr(report))
     fname = op.join(str(tmp_path), 'test.html')
     report.save(fname, open_browser=False)