-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
[Bug]: Nondeterministic behavior with subplot spacing and constrained layout #28574
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
Comments
I think this is expected behaviour and I'm not sure if we really can do anything about it. This happens because when the constraints are first computed the xtick on the right hand side of the axis horizontally overspills the axis bounding box, so constrained layout adds a bit of space. The axes gets redrawn, the axis is larger, and now the tick label fits. I don't think there is anything to directly be done about this. I think you could take the tick label out of the layout, but that is a bit fiddily. If you want the axes to always exactly touch, then I'm not sure I'd bother using constrained layout. |
Btw it maybe could be more clear, but this is the case covered by caveat 2 here https://matplotlib.org/stable/users/explain/axes/constrainedlayout_guide.html#other-caveats |
I'm not sure I follow why this should be nondeterministic between identical executions. I do think the zero-space result is the correct one (following from what I stated under "additional information"), but even if it isn't I don't see why the result should be nondeterministic.
As an aside, I would like to know how to do this (using |
This is a complicated interplay of our tick system being too customizable, being too lazy, and IMO, some weird cross-ownership of the calculation of tight bounding boxes. In order to calculate the bounding box of the matplotlib/lib/matplotlib/spines.py Lines 160 to 178 in 9b8a8c7
In doing so, it picks a major tick, and a minor tick, and adds padding based on the direction in the tick's _tickdir attribute. However, it does this by passing the tick lists through some sets, meaning it will pick a random one depending on the set.
Ticks are generated lazily, and copy properties from the initial tick: matplotlib/lib/matplotlib/axis.py Lines 1611 to 1619 in 9b8a8c7
This copies the properties from sub-Artist to sub-Artist, but not the _tickdir attribute (or any others for that matter). So it will be whatever the default rcParam is at the time of the copy. If you run a draw in the context, the copies happen inside, and are all _tickdir='in' . If you don't, then some ticks will have the right value, and some won't.
This means that the |
Indeed, setting the rcParams in my example globally rather than as a context appears to ensure determinism. Also helps explain the inconsistent behavior I was observing more generally (since I was using rc contexts to facilitate testing, whereas I normally set a style globally). Many thanks for digging into it (and addressing it so quickly)! |
@QuLogic, would your observations explain why calling I'm actually struggling to get %matplotlib inline
import matplotlib.pyplot as plt
rc = {
"xtick.direction": "in",
"ytick.direction": "in",
"figure.constrained_layout.use": True,
"figure.constrained_layout.h_pad": 0,
"figure.constrained_layout.w_pad": 0,
"figure.constrained_layout.wspace": 0,
"figure.constrained_layout.hspace": 0,
}
plt.style.use(rc)
fig, axes = plt.subplots(2, 2, figsize=(6, 4), sharey=True, sharex="col")
for ax in axes.flat:
ax.set_title("title")
for ax in axes.flat:
for artist in ax.get_children():
artist.set_in_layout(False) Let me know whether to open a new issue. I'm really struggling to study the issue in more detail because, e.g., I can't figure out how to properly draw bboxes on figures. Copying this import matplotlib.pyplot as plt
fig, ax = plt.subplots()
bbaxis, bbspines, bbax, bbtb = color_boxes(fig, ax) |
The bad bboxes are because you are drawing in pixel co-ordinates, but Matplotlib doubles the pixels for retina displays. I wouldn't dig code out of the tests and expect it to work on arbitrary code. In particular As for taking everything out of layout - that may not work as not everything checks the in_layout setting when calculating the tightbboxes. One would have to track all the elements down and then decide if it makes sense to allow them to be excluded. It is possible that the axes elements have never been excluded. |
Argh, thanks, I set that in my jupyter configuration so many years ago I completely forgot about it.
Do the tests set some global configuration that's not evident in the files themselves, or is just my own config to worry about? I scoured the repo, issues, discourse, and google for other up-to-date solutions before realizing the tests should be as authoritative as it gets.
Does the docstring for get_tightbbox (and this line in |
Someone would have to check the code paths for getting bounding boxes, which are somewhat convoluted. |
Uh oh!
There was an error while loading. Please reload this page.
Bug summary
I'm trying to get constrained layout to remove all space between subplots when no artists should be preventing it. In this example I've set all necessary rc params accordingly and changed the axes limits to ensure the x-axis tick labels don't spill beyond the axis extent. The resulting space between the axes is nondeterministic.
Code for reproduction
Actual outcome
Example output (ran a handful of times until one of each result showed):

Expected outcome
Zero spacing between the axes, deterministically.
Additional information
I noticed that the results became deterministic (with zero space between panels) when saving the figure (that is, the result that displayed in the notebook, not just the saved figure). Inserting a
fig.canvas.draw()
at the end likewise ensures zero space deterministically.Operating system
Rocky Linux
Matplotlib Version
3.9.1
Matplotlib Backend
inline
Python version
3.11.9
Jupyter version
4.2.3
Installation
conda
The text was updated successfully, but these errors were encountered: