diff --git a/doc/users/next_whats_new/savefig_bbox_layout.rst b/doc/users/next_whats_new/savefig_bbox_layout.rst new file mode 100644 index 000000000000..bf2d2bb72f90 --- /dev/null +++ b/doc/users/next_whats_new/savefig_bbox_layout.rst @@ -0,0 +1,10 @@ +pad_inches="layout" for savefig +------------------------------- + +When using constrained or compressed layout, + +.. code-block:: python + + savefig(filename, bbox_inches="tight", pad_inches="layout") + +will now use the padding sizes defined on the layout engine. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index c573c369e9e3..b54f6fed167b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -48,6 +48,7 @@ from matplotlib._pylab_helpers import Gcf from matplotlib.backend_managers import ToolManager from matplotlib.cbook import _setattr_cm +from matplotlib.layout_engine import ConstrainedLayoutEngine from matplotlib.path import Path from matplotlib.texmanager import TexManager from matplotlib.transforms import Affine2D @@ -2273,8 +2274,11 @@ def print_figure( Bounding box in inches: only the given portion of the figure is saved. If 'tight', try to figure out the tight bbox of the figure. - pad_inches : float, default: :rc:`savefig.pad_inches` - Amount of padding around the figure when *bbox_inches* is 'tight'. + pad_inches : float or 'layout', default: :rc:`savefig.pad_inches` + Amount of padding in inches around the figure when bbox_inches is + 'tight'. If 'layout' use the padding from the constrained or + compressed layout engine; ignored if one of those engines is not in + use. bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional A list of extra artists that will be considered when the @@ -2324,8 +2328,8 @@ def print_figure( if bbox_inches is None: bbox_inches = rcParams['savefig.bbox'] - if (self.figure.get_layout_engine() is not None or - bbox_inches == "tight"): + layout_engine = self.figure.get_layout_engine() + if layout_engine is not None or bbox_inches == "tight": # we need to trigger a draw before printing to make sure # CL works. "tight" also needs a draw to get the right # locations: @@ -2341,9 +2345,15 @@ def print_figure( if bbox_inches == "tight": bbox_inches = self.figure.get_tightbbox( renderer, bbox_extra_artists=bbox_extra_artists) - if pad_inches is None: - pad_inches = rcParams['savefig.pad_inches'] - bbox_inches = bbox_inches.padded(pad_inches) + if (isinstance(layout_engine, ConstrainedLayoutEngine) and + pad_inches == "layout"): + h_pad = layout_engine.get()["h_pad"] + w_pad = layout_engine.get()["w_pad"] + else: + if pad_inches in [None, "layout"]: + pad_inches = rcParams['savefig.pad_inches'] + h_pad = w_pad = pad_inches + bbox_inches = bbox_inches.padded(w_pad, h_pad) # call adjust_bbox to save only the given area restore_bbox = _tight_bbox.adjust_bbox( diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 7b273cf9fb81..e911a037d818 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -3257,8 +3257,11 @@ def savefig(self, fname, *, transparent=None, **kwargs): Bounding box in inches: only the given portion of the figure is saved. If 'tight', try to figure out the tight bbox of the figure. - pad_inches : float, default: :rc:`savefig.pad_inches` - Amount of padding around the figure when bbox_inches is 'tight'. + pad_inches : float or 'layout', default: :rc:`savefig.pad_inches` + Amount of padding in inches around the figure when bbox_inches is + 'tight'. If 'layout' use the padding from the constrained or + compressed layout engine; ignored if one of those engines is not in + use. facecolor : color or 'auto', default: :rc:`savefig.facecolor` The facecolor of the figure. If 'auto', use the current figure diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py index 49b3f1dd125f..185be9857abb 100644 --- a/lib/matplotlib/layout_engine.py +++ b/lib/matplotlib/layout_engine.py @@ -203,7 +203,7 @@ def __init__(self, *, h_pad=None, w_pad=None, Parameters ---------- h_pad, w_pad : float - Padding around the axes elements in figure-normalized units. + Padding around the axes elements in inches. Default to :rc:`figure.constrained_layout.h_pad` and :rc:`figure.constrained_layout.w_pad`. hspace, wspace : float @@ -261,7 +261,7 @@ def set(self, *, h_pad=None, w_pad=None, Parameters ---------- h_pad, w_pad : float - Padding around the axes elements in figure-normalized units. + Padding around the axes elements in inches. Default to :rc:`figure.constrained_layout.h_pad` and :rc:`figure.constrained_layout.w_pad`. hspace, wspace : float diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_layout.png b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_layout.png new file mode 100644 index 000000000000..657eaed42267 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_layout.png differ diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 91ff7fe20963..7e0ad945b335 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -69,6 +69,22 @@ def test_bbox_inches_tight_suptitle_non_default(): fig.suptitle('Booo', x=0.5, y=1.1) +@image_comparison(['bbox_inches_tight_layout.png'], remove_text=True, + style='mpl20', + savefig_kwarg=dict(bbox_inches='tight', pad_inches='layout')) +def test_bbox_inches_tight_layout_constrained(): + fig, ax = plt.subplots(layout='constrained') + fig.get_layout_engine().set(h_pad=0.5) + ax.set_aspect('equal') + + +def test_bbox_inches_tight_layout_notconstrained(tmp_path): + # pad_inches='layout' should be ignored when not using constrained/ + # compressed layout. Smoke test that savefig doesn't error in this case. + fig, ax = plt.subplots() + fig.savefig(tmp_path / 'foo.png', bbox_inches='tight', pad_inches='layout') + + @image_comparison(['bbox_inches_tight_clipping'], remove_text=True, savefig_kwarg={'bbox_inches': 'tight'}) def test_bbox_inches_tight_clipping(): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 0ab2b0db3596..64bdcaff6ac8 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -616,10 +616,23 @@ def expanded(self, sw, sh): a = np.array([[-deltaw, -deltah], [deltaw, deltah]]) return Bbox(self._points + a) - def padded(self, p): - """Construct a `Bbox` by padding this one on all four sides by *p*.""" + @_api.rename_parameter("3.8", "p", "w_pad") + def padded(self, w_pad, h_pad=None): + """ + Construct a `Bbox` by padding this one on all four sides. + + Parameters + ---------- + w_pad : float + Width pad + h_pad: float, optional + Height pad. Defaults to *w_pad*. + + """ points = self.get_points() - return Bbox(points + [[-p, -p], [p, p]]) + if h_pad is None: + h_pad = w_pad + return Bbox(points + [[-w_pad, -h_pad], [w_pad, h_pad]]) def translated(self, tx, ty): """Construct a `Bbox` by translating this one by *tx* and *ty*."""