diff --git a/doc/users/next_whats_new/fig_draw_no_output.rst b/doc/users/next_whats_new/fig_draw_no_output.rst new file mode 100644 index 000000000000..293c6590b8c9 --- /dev/null +++ b/doc/users/next_whats_new/fig_draw_no_output.rst @@ -0,0 +1,10 @@ +Figure now has draw_no_output method +------------------------------------ + +Rarely, the user will want to trigger a draw without making output to +either the screen or a file. This is useful for determining the final +position of artists on the figure that require a draw, like text artists. +This could be accomplished via ``fig.canvas.draw()`` but has side effects, +sometimes requires an open file, and is documented on an object most users +do not need to access. The `.Figure.draw_no_output` is provided to trigger +a draw without pushing to the final output, and with fewer side effects. \ No newline at end of file diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 59f8e7856bbc..974846f63995 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1584,9 +1584,9 @@ def _draw(renderer): raise Done(renderer) def _no_output_draw(figure): - renderer = _get_renderer(figure) - with renderer._draw_disabled(): - figure.draw(renderer) + # _no_output_draw was promoted to the figure level, but + # keep this here in case someone was calling it... + figure.draw_no_output() def _is_non_interactive_terminal_ipython(ip): diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index c63aaca2024b..530df4b12dd2 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -29,7 +29,7 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase, _no_output_draw) + GraphicsContextBase, RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure from matplotlib.font_manager import findfont, get_font @@ -2718,7 +2718,7 @@ def print_pdf(self, filename, *, file.close() def draw(self): - _no_output_draw(self.figure) + self.figure.draw_no_output() return super().draw() diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 69b32236ad5d..c6db4931e370 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -19,7 +19,7 @@ from matplotlib import _api, cbook, font_manager as fm from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase, _no_output_draw + GraphicsContextBase, RendererBase ) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.backends.backend_pdf import ( @@ -889,7 +889,7 @@ def get_renderer(self): return RendererPgf(self.figure, None) def draw(self): - _no_output_draw(self.figure) + self.figure.draw_no_output() return super().draw() diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 9da5063cea56..24a483a54906 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -23,7 +23,7 @@ from matplotlib.afm import AFM from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase, _no_output_draw) + GraphicsContextBase, RendererBase) from matplotlib.cbook import is_writable_file_like, file_requires_unicode from matplotlib.font_manager import get_font from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE @@ -1112,7 +1112,7 @@ def _print_figure_tex( _move_path_to_path_or_stream(tmpfile, outfile) def draw(self): - _no_output_draw(self.figure) + self.figure.draw_no_output() return super().draw() diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index a1311e614dd1..51c0c17b8089 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -17,7 +17,7 @@ from matplotlib import _api, cbook from matplotlib.backend_bases import ( _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - RendererBase, _no_output_draw) + RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.colors import rgb2hex from matplotlib.dates import UTC @@ -1343,7 +1343,7 @@ def get_default_filetype(self): return 'svg' def draw(self): - _no_output_draw(self.figure) + self.figure.draw_no_output() return super().draw() diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 6691e91713a7..59850a5a1640 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -26,7 +26,7 @@ from matplotlib.artist import ( Artist, allow_rasterization, _finalize_rasterization) from matplotlib.backend_bases import ( - FigureCanvasBase, NonGuiException, MouseButton) + FigureCanvasBase, NonGuiException, MouseButton, _get_renderer) import matplotlib._api as _api import matplotlib.cbook as cbook import matplotlib.colorbar as cbar @@ -2739,6 +2739,15 @@ def draw(self, renderer): self.canvas.draw_event(renderer) + def draw_no_output(self): + """ + Draw the figure with no output. Useful to get the final size of + artists that require a draw before their size is known (e.g. text). + """ + renderer = _get_renderer(self) + with renderer._draw_disabled(): + self.draw(renderer) + def draw_artist(self, a): """ Draw `.Artist` *a* only. @@ -3015,7 +3024,6 @@ def execute_constrained_layout(self, renderer=None): """ from matplotlib._constrained_layout import do_constrained_layout - from matplotlib.tight_layout import get_renderer _log.debug('Executing constrainedlayout') if self._layoutgrid is None: @@ -3033,7 +3041,7 @@ def execute_constrained_layout(self, renderer=None): w_pad = w_pad / width h_pad = h_pad / height if renderer is None: - renderer = get_renderer(fig) + renderer = _get_renderer(fig) do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace) def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): @@ -3063,7 +3071,7 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): """ from .tight_layout import ( - get_renderer, get_subplotspec_list, get_tight_layout_figure) + get_subplotspec_list, get_tight_layout_figure) from contextlib import suppress subplotspec_list = get_subplotspec_list(self.axes) if None in subplotspec_list: @@ -3071,7 +3079,7 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): "compatible with tight_layout, so results " "might be incorrect.") - renderer = get_renderer(self) + renderer = _get_renderer(self) ctx = (renderer._draw_disabled() if hasattr(renderer, '_draw_disabled') else suppress()) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 67474628e7bf..d8bd4cffde94 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -134,7 +134,7 @@ def test_constrained_layout7(): for gs in gsl: fig.add_subplot(gs) # need to trigger a draw to get warning - fig.draw(fig.canvas.get_renderer()) + fig.draw_no_output() @image_comparison(['constrained_layout8.png']) @@ -327,7 +327,7 @@ def test_constrained_layout18(): ax2 = ax.twinx() example_plot(ax) example_plot(ax2, fontsize=24) - fig.canvas.draw() + fig.draw_no_output() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -339,7 +339,7 @@ def test_constrained_layout19(): example_plot(ax2, fontsize=24) ax2.set_title('') ax.set_title('') - fig.canvas.draw() + fig.draw_no_output() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -359,11 +359,11 @@ def test_constrained_layout21(): fig, ax = plt.subplots(constrained_layout=True) fig.suptitle("Suptitle0") - fig.canvas.draw() + fig.draw_no_output() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle1") - fig.canvas.draw() + fig.draw_no_output() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -373,11 +373,11 @@ def test_constrained_layout22(): """#11035: suptitle should not be include in CL if manually positioned""" fig, ax = plt.subplots(constrained_layout=True) - fig.canvas.draw() + fig.draw_no_output() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle", y=0.5) - fig.canvas.draw() + fig.draw_no_output() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -425,7 +425,7 @@ def test_hidden_axes(): # (as does a gridspec slot that is empty) fig, axs = plt.subplots(2, 2, constrained_layout=True) axs[0, 1].set_visible(False) - fig.canvas.draw() + fig.draw_no_output() extents1 = np.copy(axs[0, 0].get_position().extents) np.testing.assert_allclose( @@ -451,7 +451,7 @@ def test_colorbar_align(): fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.1, wspace=0.1) - fig.canvas.draw() + fig.draw_no_output() if location in ['left', 'right']: np.testing.assert_allclose(cbs[0].ax.get_position().x0, cbs[2].ax.get_position().x0) @@ -493,7 +493,7 @@ def test_colorbars_no_overlapH(): def test_manually_set_position(): fig, axs = plt.subplots(1, 2, constrained_layout=True) axs[0].set_position([0.2, 0.2, 0.3, 0.3]) - fig.canvas.draw() + fig.draw_no_output() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]]) @@ -501,7 +501,7 @@ def test_manually_set_position(): axs[0].set_position([0.2, 0.2, 0.3, 0.3]) pc = axs[0].pcolormesh(np.random.rand(20, 20)) fig.colorbar(pc, ax=axs[0]) - fig.canvas.draw() + fig.draw_no_output() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.44, 0.5]]) @@ -546,7 +546,7 @@ def test_align_labels(): fig.align_ylabels(axs=(ax3, ax1, ax2)) - fig.canvas.draw() + fig.draw_no_output() after_align = [ax1.yaxis.label.get_window_extent(), ax2.yaxis.label.get_window_extent(), ax3.yaxis.label.get_window_extent()] diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 635037c43442..8d589fc5890c 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -415,11 +415,11 @@ def test_autofmt_xdate(which): @pytest.mark.style('default') def test_change_dpi(): fig = plt.figure(figsize=(4, 4)) - fig.canvas.draw() + fig.draw_no_output() assert fig.canvas.renderer.height == 400 assert fig.canvas.renderer.width == 400 fig.dpi = 50 - fig.canvas.draw() + fig.draw_no_output() assert fig.canvas.renderer.height == 200 assert fig.canvas.renderer.width == 200