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

Conversation

rcomer
Copy link
Member

@rcomer rcomer commented Jan 14, 2023

PR Summary

When using constrained or compressed layout,

savefig(filename, bbox_inches="tight", pad_inches="layout")`

will use the padding defined on the layout engine.

Since the layout engine has configurable padding information on it, it makes sense to me that there should be a simple way to tell savefig to use that. This option means that either the height or width of the output file will match what was set at figsize. When preparing figures for a paper, a user might be particularly fussy about the width. Note that the default pad for bbox_inches='tight' is larger than the default for the layout engine, so currently a little fiddling is required to achieve that (I told myself that I shouldn't really care about 3mm but...).

PR Checklist

Documentation and Tests

  • Has pytest style unit tests (and pytest passes)
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • New plotting related features are documented with examples.

Release Notes

  • New features are marked with a .. versionadded:: directive in the docstring and documented in doc/users/next_whats_new/
  • [N/A] API changes are marked with a .. versionchanged:: directive in the docstring and documented in doc/api/next_api_changes/
  • Release notes conform with instructions in next_whats_new/README.rst or next_api_changes/README.rst

@rcomer rcomer added New feature topic: geometry manager LayoutEngine, Constrained layout, Tight layout labels Jan 14, 2023
@@ -616,10 +616,22 @@ 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*."""
def padded(self, pw, ph=None):
Copy link
Member

Choose a reason for hiding this comment

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

After just expiring a number of deprecations which involved name changes of non-optional arguments, I would suggest adding a @_api.rename_parameter("3.8", "p", "pw").

In addition, may one should go all in and spell out the parameter name?

Copy link
Member Author

Choose a reason for hiding this comment

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

I picked very short names for consistency with (some of) the other methods on this class, but maybe w_pad and h_pad would be better? That would make them consistent with those for the relatively recently added constrained layout engine.

Thanks for the pointer on _api.rename_parameter. I tend to forget that the difference between positional and keyword args in python is only pretend!

pw : float
Width pad
ph: float, optional
Height pad. Defaults to pw.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Height pad. Defaults to pw.
Height pad. Defaults to *pw*.

@jklymak
Copy link
Member

jklymak commented Jan 14, 2023

I'm not following this feature, but didn't look too closely. Your example calls compressed layout on a single axes (??) and then trims the saved figure by a pad. However, I'm not following why you don't just do

fig, axs = plt.subplots(2, 2, layout='compressed')
for ax in axs.flat:
    ax.set_aspect(1)

fig.savefig('boo.png', bbox_inches='tight', pad_inches=0.05)

I also didn't understand the motivation vis-a-vis a certain width figure? If the user wants a compressed layout and a figure of a certain size they need only do fig, axs = plt.subplot(2, 2, layout='compressed', figsize=(4.2367, 20)) where 20 is big enough that the compression always happens in the vertical.

Overall, I'd be very leery of a feature that changes the visual appearance of a figure for just savefig. With a name like bbox_inches='compressed' I'd expect compressed layout to be used on saving, whether or not it was used previously. I don't see that you are doing that (?), but...

If the motivation is to have different vertical and horizontal padding, why not make that an option to pad_inches?

@rcomer
Copy link
Member Author

rcomer commented Jan 15, 2023

My test is not very representative of my target use-case because I was just going for "what's the simplest test I can make that verifies the outer padding looks how I expect". I set the h_pad because I wanted to check what happens if h_pad and w_pad are different.

In my real use-case I am more than happy with the layout engine's default pad, so currently I can get what I want with something like

fig, axs = plt.subplots(2, 2, figsize=(my_chosen_width, some_large_height), layout='compressed')
for ax in axs.flat:
    ax.set_aspect(1)

fig.savefig('boo.png', bbox_inches='tight',
            pad_inches=plt.rcParams["figure.constrained_layout.w_pad"])

which was all easy to figure out except what exactly to use for pad_inches. In general the layout engine Just Works in the background so I'd previously never even needed to learn what class of object does the work. So although it was obvious that compressed layout must have a pad stored somewhere, it took me some searching in the docs to figure out where. Even once you know, passing in the rcParam feels a little cumbersome (and if I just pass 0.04167 I will definitely need to look it up every time!) So I think it would be nice to provide some sort of shortcut that says "the padding on save should match the padding on the layout engine".

I picked bbox_inches="compressed" because I was thinking it only makes sense to use this with compressed layout. Thought process was essentially "this is needed when you have fixed aspect ratios, and if you have fixed aspect ratios you'll be using compressed layout". But thinking again that's clearly not true. So yes, this would need some more thought on the applicability and therefore on the name.

Thinking again this morning, I think my ideal solution would be to change the pad_inches default. So if you are using a ConstrainedLayoutEngine it automatically gets the pad off that, and if not it falls back to the current default of rcParams["savefig.pad_inches"].

@jklymak
Copy link
Member

jklymak commented Jan 15, 2023

Thinking again this morning, I think my ideal solution would be to change the pad_inches default. So if you are using a ConstrainedLayoutEngine it automatically gets the pad off that, and if not it falls back to the current default of rcParams["savefig.pad_inches"].

I think modifying pad_inches is a good path to take. Whether the default should change is another issue, but something like pad_inches='layout' might be good for using the layout engine defaults.

@rcomer rcomer force-pushed the savefig-compressed branch from 6e9781e to 6557bdf Compare January 15, 2023 20:06
@rcomer rcomer changed the title ENH: bbox_inches='compressed' for savefig ENH: pad_inches='layout' for savefig Jan 15, 2023
@rcomer
Copy link
Member Author

rcomer commented Jan 15, 2023

OK I've now implemented pad_inches="layout". Currently it only works if a ConstrainedLayoutEngine is in use. If pad_inches="layout" is passed when ConstrainedLayoutEngine is not in use it silently falls back to using the default pad. I'm not sure whether it's better to do that or raise an exception. It could also be implemented for TightLayoutEngine, but I do not know how useful that would be: I would not envisage using it with tight layout myself.

@rcomer rcomer added this to the v3.8.0 milestone Jan 15, 2023
@jklymak jklymak self-assigned this Jan 15, 2023
Copy link
Member

@jklymak jklymak left a comment

Choose a reason for hiding this comment

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

I don't think this is quite right, though I didn't upload to debug yet.

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 around the figure when bbox_inches is 'tight'. If
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Amount of padding around the figure when bbox_inches is 'tight'. If
Amount of padding in inches around the figure when bbox_inches is 'tight'. If

Comment on lines +2349 to +2351
h_pad = layout_engine.get()["h_pad"]
w_pad = layout_engine.get()["w_pad"]
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)

@rcomer rcomer force-pushed the savefig-compressed branch 2 times, most recently from ded0868 to a6b6dd9 Compare January 16, 2023 19:26
@oscargus
Copy link
Member

Maybe a bit too early to approve as I think there should go some .. versionadded:: and maybe a user release note in. But other than that, looks good!

@jklymak
Copy link
Member

jklymak commented Jan 17, 2023

Should this get a what's new entry?

(ooops, cross post)

@rcomer
Copy link
Member Author

rcomer commented Jan 17, 2023

Looks like the criteria for a versionadded directive and a what's new entry are the same:
https://matplotlib.org/devdocs/devel/coding_guide.html#new-features-and-api-changes

I'm not sure I would class this as "major"?

If it should get a what's new entry, do we need to wait till after the 3.7 release - since this is milestoned for 3.8 and the whatsnew directory is currently full of 3.7 stuff?

@QuLogic
Copy link
Member

QuLogic commented Jan 18, 2023

No, the branch is made; anything for 3.8 is fine to put in main.

@jklymak
Copy link
Member

jklymak commented Jan 18, 2023

I wouldn't add a version added note, because it's just a new argument to the keyword. But I'd add to the what's-new.

@rcomer rcomer force-pushed the savefig-compressed branch 2 times, most recently from 8c51c42 to 59cc1b2 Compare January 18, 2023 20:58
@rcomer rcomer force-pushed the savefig-compressed branch from 59cc1b2 to 1eb1323 Compare January 19, 2023 09:25
@jklymak jklymak merged commit ba7a45a into matplotlib:main Jan 19, 2023
@rcomer
Copy link
Member Author

rcomer commented Jan 19, 2023

Thanks @jklymak, @oscargus

@rcomer rcomer mentioned this pull request Jan 19, 2023
6 tasks
@rcomer rcomer deleted the savefig-compressed branch January 19, 2023 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
New feature topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants