Skip to content

ENH: pad_inches='layout' for savefig #24981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/users/next_whats_new/savefig_bbox_layout.rst
Original file line number Diff line number Diff line change
@@ -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.
24 changes: 17 additions & 7 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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"]
Comment on lines +2350 to +2351
Copy link
Member

Choose a reason for hiding this comment

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

the layout_engine pads are in figure-normalized units (or at least that is what the docs say, if that isn't the case, then we should fix the docs), whereas bbox_inches='tight' pads are supposed to be in inches...

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh yeah, the docs do say that (I missed that). But internally the pad gets divided by the figsize

def execute(self, fig):
"""
Perform constrained_layout and move and resize axes accordingly.
Parameters
----------
fig : `.Figure` to perform layout on.
"""
width, height = fig.get_size_inches()
# pads are relative to the current state of the figure...
w_pad = self._params['w_pad'] / width
h_pad = self._params['h_pad'] / height
return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad,
wspace=self._params['wspace'],
hspace=self._params['hspace'],
rect=self._params['rect'],
compress=self._compress)

so I think the docs may indeed be wrong.

Copy link
Member

Choose a reason for hiding this comment

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

Oooops, thats not good. I guess padding in inches (or points) makes more sense to me (ahem, and probably did when I wrote this) and the docs need to be fixed.

Copy link
Member Author

@rcomer rcomer Jan 16, 2023

Choose a reason for hiding this comment

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

The matplotlibrc is OK though

#figure.constrained_layout.h_pad:  0.04167  # Padding around axes objects. Float representing
#figure.constrained_layout.w_pad:  0.04167  # inches. Default is 3/72 inches (3 points)

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(
Expand Down
7 changes: 5 additions & 2 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/layout_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions lib/matplotlib/tests/test_bbox_tight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
19 changes: 16 additions & 3 deletions lib/matplotlib/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*."""
Expand Down