Skip to content

FIX: fix bbox_inches=tight and constrained layout bad interaction #19342

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 2 commits into from
Jan 25, 2021

Conversation

jklymak
Copy link
Member

@jklymak jklymak commented Jan 22, 2021

PR Summary

Closes #19339

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(constrained_layout=True)
ax.set_aspect(1.0)
fig.savefig('/tmp/New.png', bbox_inches='tight')

Old

Bad interaction between width and current size of the axes:

Old

New:

Suppress constrained layout after the first draw in the bbox_inches=tight logic

New

Sorry, this obviously needs a new image test unless someone knows how to mock up bbox_inches='tight' behaviour without actually saving the figure.

PR Checklist

  • Has pytest style unit tests (and pytest passes).
  • Is Flake 8 compliant (run flake8 on changed files to check).
  • New features are documented, with examples if plot related.
  • Documentation is sphinx and numpydoc compliant (the docs should build without error).
  • Conforms to Matplotlib style conventions (install flake8-docstrings and run flake8 --docstring-convention=all).
  • New features have an entry in doc/users/next_whats_new/ (follow instructions in README.rst there).
  • API changes documented in doc/api/next_api_changes/ (follow instructions in README.rst there).

@jklymak jklymak added Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. topic: geometry manager LayoutEngine, Constrained layout, Tight layout labels Jan 22, 2021
@jklymak jklymak added this to the v3.4.0 milestone Jan 22, 2021
Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

I'm not claiming I've fully dug into the code, but the structure looks a bit odd:

  • There's a lot of distance between saving cl_state and deactivating the layout.
  • The save/restore always happens, but deactivating only happens if bbox_inches == "tight"

Can't this be pulled closer together? Ideally, if we had a with figure._constrained_layout_deactivated() context manager could this be used here?

@jklymak
Copy link
Member Author

jklymak commented Jan 22, 2021

The issue is that bbox_inches=tight makes an extra draw, and then changes the size of the figure. You only want to turn CL off after that extra draw. Otherwise you want to leave it as the user set it because the print_method call calls a draw (which retriggers CL). I don't think there is anything that can be wrapped in a context manager.

@jklymak
Copy link
Member Author

jklymak commented Jan 23, 2021

I guess the other way of expressing this is that I think the original issue is that the way we do the bbox_inches='tight' is not terribly intuitive. In an ideal world, we would just make the figure normally, and crop it afterwards in a backend-appropriate way. Instead, we make a smaller canvas, and temporarily transform all the artists a dx and dy, and then draw the figure. CL balks at this because it then tries to resize on the smaller trimmed figure using margins etc that were based on the bigger figure. Its a case of two types of semi-fragile magics not getting along.

@jklymak
Copy link
Member Author

jklymak commented Jan 23, 2021

Though thinking about it some more, I forgot to check if bbox_inches=Bbox([[1, 0], [3, 2]]) and indeed that was incorrect as well for the same reason. So the new commit unconditionally does a draw if either constrained layout is needed or bbox_inches="tight", and then turns CL off when the printed version is rendered. and then turns it back on after.

Comment on lines 2235 to 2237
# we have already done CL above, so turn it off:
cl_state = self.figure.get_constrained_layout()
self.figure.set_constrained_layout(False)
Copy link
Member

Choose a reason for hiding this comment

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

I suspect this should not be part of try. We're not expecting it to fail.

Also, does it make sense to

cl_state = self.figure.get_constrained_layout()
if cl_state:
    self.figure.set_constrained_layout(False)

[...]

if cl_state:
    self.figure.set_constrained_layout(cl_state)

to prevent any overhead if constrained is not used?

Note that this can easily be refactored to a context manager now:

with self.figure._constrained_layout_deactivated():
    try:
        [...]
    finally:
        [...]
return result

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, the set can be outside the try.

@anntzer
Copy link
Contributor

anntzer commented Jan 23, 2021

In an ideal world, we would just make the figure normally, and crop it afterwards in a backend-appropriate way.

Actually, now that you write this, I think that's probably doable (not in the 3.4 time frame of course)... I'll keep that in the back of my mind for the future.

@jklymak
Copy link
Member Author

jklymak commented Jan 23, 2021

Actually, now that you write this, I think that's probably doable (not in the 3.4 time frame of course)... I'll keep that in the back of my mind for the future.

For raster backends, it is pretty trivial - just crop the canvas before write.

For the vector backends, I'm not sure what the incantation needs to be to do that robustly....

@anntzer
Copy link
Contributor

anntzer commented Jan 23, 2021

For svg we can probably handle with https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox.
For ps/pdf I guess something like /PageSize or %BoundingBox or setting some global translation that gets applied to everything.

@jklymak
Copy link
Member Author

jklymak commented Jan 23, 2021

Yeah postscript (and I assume friends) has a translate command. Something like this would be so much more straightforward than the song and dance we do now. But as you say, not for 3.4.

@jklymak
Copy link
Member Author

jklymak commented Jan 23, 2021

I thought about the context manager, but I am not sure if its worth creating a context manager in figure.py just for this. If we were doing this pattern more, I'd agree, but...

I am happy to include the if-statements if you like, but I think set_constrained_layout is pretty lightweight compared to anything else that is happening during a figure save.

@timhoffm
Copy link
Member

I'm slightly in favor of a context manager because it makes the code more readable, even if the context manager is only used once.

But I'm also ok without. That's why I've already approved.

@jklymak jklymak requested a review from anntzer January 25, 2021 17:50
@jklymak
Copy link
Member Author

jklymak commented Jan 25, 2021

@anntzer can you confirm if this fixes the issue for you?

Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

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

Works for me. Thanks for the quick fix :)

@anntzer
Copy link
Contributor

anntzer commented Jan 25, 2021

I think the context manager, if any, should be setprop_cm (similar to setattr_cm). But that would be another PR.

@anntzer anntzer merged commit b6294ff into matplotlib:master Jan 25, 2021
@jklymak jklymak deleted the fix-CL-tight branch January 25, 2021 18:52
@jklymak
Copy link
Member Author

jklymak commented Jan 25, 2021

Thanks @timhoffm and @anntzer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. topic: geometry manager LayoutEngine, Constrained layout, Tight layout
Projects
None yet
Development

Successfully merging this pull request may close these issues.

constrained_layout + fixed-aspect axes + bbox_inches="tight"
3 participants