Repository navigation
[Bug]: fig.tight_layout when quiver collection is clipped produces AttributeError #24104
Copy link
Copy link
Closed
Closed
Copy link
Milestone
Description
Bug summary
When I set a clipping path to quiver object with set_clip_path(), Figure.tight_layout() produces AttributeError: 'NoneType' object has no attribute 'xmin'. Without tight_layout() everything works.
Code for reproduction
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
x, y = np.mgrid[-1:1:0.1, -1:1:0.1]
z = np.sin(x) + np.cos(y)
xg, yg = np.gradient(z)
f, ax = plt.subplots()
h = ax.quiver(x, y, xg, yg, label='Test')
c = Circle((0, 0), radius=1, edgecolor="black", fill=False)
ax.add_patch(c)
h.set_clip_path(c)
ax.set_aspect(1)
f.tight_layout()Actual outcome
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In [42], line 15
13 h.set_clip_path(c)
14 ax.set_aspect(1)
---> 15 f.tight_layout()
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/figure.py:3443, in Figure.tight_layout(self, pad, h_pad, w_pad, rect)
3441 try:
3442 self.set_layout_engine(engine)
-> 3443 engine.execute(self)
3444 finally:
3445 self.set_layout_engine(None)
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/layout_engine.py:180, in TightLayoutEngine.execute(self, fig)
178 renderer = fig._get_renderer()
179 with getattr(renderer, "_draw_disabled", nullcontext)():
--> 180 kwargs = get_tight_layout_figure(
181 fig, fig.axes, subplotspec_list, renderer,
182 pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
183 rect=info['rect'])
184 if kwargs:
185 fig.subplots_adjust(**kwargs)
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/_tight_layout.py:305, in get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, pad, h_pad, w_pad, rect)
300 return {}
301 span_pairs.append((
302 slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
303 slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
--> 305 kwargs = _auto_adjust_subplotpars(fig, renderer,
306 shape=(max_nrows, max_ncols),
307 span_pairs=span_pairs,
308 subplot_list=subplot_list,
309 ax_bbox_list=ax_bbox_list,
310 pad=pad, h_pad=h_pad, w_pad=w_pad)
312 # kwargs can be none if tight_layout fails...
313 if rect is not None and kwargs is not None:
314 # if rect is given, the whole subplots area (including
315 # labels) will fit into the rect instead of the
(...)
319 # auto_adjust_subplotpars twice, where the second run
320 # with adjusted rect parameters.
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/_tight_layout.py:82, in _auto_adjust_subplotpars(fig, renderer, shape, span_pairs, subplot_list, ax_bbox_list, pad, h_pad, w_pad, rect)
80 for ax in subplots:
81 if ax.get_visible():
---> 82 bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
84 tight_bbox_raw = Bbox.union(bb)
85 tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/artist.py:1378, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
1372 """
1373 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
1374 *for_layout_only* kwarg; this helper tries to uses the kwarg but skips it
1375 when encountering third-party subclasses that do not support it.
1376 """
1377 try:
-> 1378 return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
1379 except TypeError:
1380 return obj.get_tightbbox(*args, **kwargs)
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/axes/_base.py:4450, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
4447 bbox_artists = self.get_default_bbox_extra_artists()
4449 for a in bbox_artists:
-> 4450 bbox = a.get_tightbbox(renderer)
4451 if (bbox is not None
4452 and 0 < bbox.width < np.inf
4453 and 0 < bbox.height < np.inf):
4454 bb.append(bbox)
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/artist.py:345, in Artist.get_tightbbox(self, renderer)
343 if clip_path is not None:
344 clip_path = clip_path.get_fully_transformed_path()
--> 345 bbox = Bbox.intersection(bbox, clip_path.get_extents())
346 return bbox
File ~/miniforge3/envs/apsg/lib/python3.10/site-packages/matplotlib/transforms.py:666, in BboxBase.intersection(bbox1, bbox2)
660 @staticmethod
661 def intersection(bbox1, bbox2):
662 """
663 Return the intersection of *bbox1* and *bbox2* if they intersect, or
664 None if they don't.
665 """
--> 666 x0 = np.maximum(bbox1.xmin, bbox2.xmin)
667 x1 = np.minimum(bbox1.xmax, bbox2.xmax)
668 y0 = np.maximum(bbox1.ymin, bbox2.ymin)
AttributeError: 'NoneType' object has no attribute 'xmin'
Expected outcome
No error
Additional information
No response
Operating system
Ubuntu
Matplotlib Version
3.6.0
Matplotlib Backend
module://matplotlib_inline.backend_inline
Python version
3.10.6
Jupyter version
3.4.8
Installation
conda
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
oscargus commentedon Oct 6, 2022
The cause seems to be that this line returns
Noneand then it is not checked ifbboxisNoneagain.matplotlib/lib/matplotlib/artist.py
Line 341 in 7f758c6
Modifying
matplotlib/lib/matplotlib/artist.py
Line 343 in 7f758c6
bbox is not Nonefixes the problem, but I do not know if it is the correct fix...oscargus commentedon Oct 6, 2022
Maybe better to check for
Noneinintersectionand returnNoneif any of thebboxes areNone?timhoffm commentedon Oct 7, 2022
What is the
tightboxexpectation when there is no intersection between the artist and the given clipping box/path? I think we should return a null bbox in this case.It would have been better to return a null bbox object and not
Nonefromintersection(). But that ship has sailed. There is no reasonable migration path.Can the the tightbox code catch
Noneand turn it into a null bbox. API impact to be checked:clip_boxand a definedclip_path. This broken code would be replaced by returning a null bbox. --> okNonewithout error if there was a non-intersectingclip_boxand noclip_pathor if there was a non-intersectingclip_path. Do we internally make use of that? Do we anticipate that a user might check this None? If not, let's return the null bbox.rcomer commentedon Nov 8, 2023
This seems to have been fixed somehow. With v3.8 I get
rcomer commentedon Nov 8, 2023
Bisect points to #25710 as the fix.
oscargus commentedon Nov 8, 2023
Maybe one should add a ( smoke) test for this so that it doesn't reappear?
(Btw, there are no quiver arrows at the top, is that expected?)
rcomer commentedon Nov 8, 2023
xandyrun from -1 to 0.9 so the arrows not extending to the top is fine I think.