diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index f8aa99d48a09..441ec703d9cc 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -35,8 +35,10 @@ from matplotlib.axes import Axes, SubplotBase, subplot_class_factory from matplotlib.gridspec import GridSpec -from matplotlib.layout_engine import (ConstrainedLayoutEngine, - TightLayoutEngine, LayoutEngine) +from matplotlib.layout_engine import ( + ConstrainedLayoutEngine, TightLayoutEngine, LayoutEngine, + PlaceHolderLayoutEngine +) import matplotlib.legend as mlegend from matplotlib.patches import Rectangle from matplotlib.text import Text @@ -2382,7 +2384,9 @@ def _check_layout_engines_compat(self, old, new): If the figure has used the old engine and added a colorbar then the value of colorbar_gridspec must be the same on the new engine. """ - if old is None or old.colorbar_gridspec == new.colorbar_gridspec: + if old is None or new is None: + return True + if old.colorbar_gridspec == new.colorbar_gridspec: return True # colorbar layout different, so check if any colorbars are on the # figure... @@ -2398,15 +2402,29 @@ def set_layout_engine(self, layout=None, **kwargs): Parameters ---------- - layout: {'constrained', 'compressed', 'tight'} or `~.LayoutEngine` - 'constrained' will use `~.ConstrainedLayoutEngine`, - 'compressed' will also use ConstrainedLayoutEngine, but with a - correction that attempts to make a good layout for fixed-aspect - ratio Axes. 'tight' uses `~.TightLayoutEngine`. Users and - libraries can define their own layout engines as well. + layout: {'constrained', 'compressed', 'tight', 'none'} or \ +`LayoutEngine` or None + + - 'constrained' will use `~.ConstrainedLayoutEngine` + - 'compressed' will also use `~.ConstrainedLayoutEngine`, but with + a correction that attempts to make a good layout for fixed-aspect + ratio Axes. + - 'tight' uses `~.TightLayoutEngine` + - 'none' removes layout engine. + + If `None`, the behavior is controlled by :rc:`figure.autolayout` + (which if `True` behaves as if 'tight' were passed) and + :rc:`figure.constrained_layout.use` (which if `True` behaves as if + 'constrained' were passed). If both are `True`, + :rc:`figure.autolayout` takes priority. + + Users and libraries can define their own layout engines and pass + the instance directly as well. + kwargs: dict The keyword arguments are passed to the layout engine to set things like padding and margin sizes. Only used if *layout* is a string. + """ if layout is None: if mpl.rcParams['figure.autolayout']: @@ -2423,6 +2441,14 @@ def set_layout_engine(self, layout=None, **kwargs): elif layout == 'compressed': new_layout_engine = ConstrainedLayoutEngine(compress=True, **kwargs) + elif layout == 'none': + if self._layout_engine is not None: + new_layout_engine = PlaceHolderLayoutEngine( + self._layout_engine.adjust_compatible, + self._layout_engine.colorbar_gridspec + ) + else: + new_layout_engine = None elif isinstance(layout, LayoutEngine): new_layout_engine = layout else: diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py index e0b058e601dd..ee04afef19c7 100644 --- a/lib/matplotlib/layout_engine.py +++ b/lib/matplotlib/layout_engine.py @@ -100,6 +100,30 @@ def execute(self, fig): raise NotImplementedError +class PlaceHolderLayoutEngine(LayoutEngine): + """ + This layout engine does not adjust the figure layout at all. + + The purpose of this `.LayoutEngine` is to act as a place holder when the + user removes a layout engine to ensure an incompatible `.LayoutEngine` can + not be set later. + + Parameters + ---------- + adjust_compatible, colorbar_gridspec : bool + Allow the PlaceHolderLayoutEngine to mirror the behavior of whatever + layout engine it is replacing. + + """ + def __init__(self, adjust_compatible, colorbar_gridspec, **kwargs): + self._adjust_compatible = adjust_compatible + self._colorbar_gridspec = colorbar_gridspec + super().__init__(**kwargs) + + def execute(self, fig): + return + + class TightLayoutEngine(LayoutEngine): """ Implements the ``tight_layout`` geometry management. See diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index ec1a814ebc59..b2da459d6cf9 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -17,7 +17,8 @@ from matplotlib.axes import Axes from matplotlib.figure import Figure, FigureBase from matplotlib.layout_engine import (ConstrainedLayoutEngine, - TightLayoutEngine) + TightLayoutEngine, + PlaceHolderLayoutEngine) from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter import matplotlib.pyplot as plt import matplotlib.dates as mdates @@ -578,6 +579,9 @@ def test_invalid_layouts(): fig, ax = plt.subplots(layout="constrained") pc = ax.pcolormesh(np.random.randn(2, 2)) fig.colorbar(pc) + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("tight") + fig.set_layout_engine("none") with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): fig.set_layout_engine("tight") @@ -586,6 +590,11 @@ def test_invalid_layouts(): fig.colorbar(pc) with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): fig.set_layout_engine("constrained") + fig.set_layout_engine("none") + assert isinstance(fig.get_layout_engine(), PlaceHolderLayoutEngine) + + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("constrained") @check_figures_equal(extensions=["png", "pdf"])