Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Bug]: fig.tight_layout when quiver collection is clipped produces AttributeError #24104

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

Closed
ondrolexa opened this issue Oct 6, 2022 · 7 comments
Milestone

Comments

@ondrolexa
Copy link

ondrolexa commented Oct 6, 2022

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

@oscargus
Copy link
Member

oscargus commented Oct 6, 2022

The cause seems to be that this line returns None and then it is not checked if bbox is None again.

bbox = Bbox.intersection(bbox, clip_box)

Modifying

if clip_path is not None:
to also check for bbox is not None fixes the problem, but I do not know if it is the correct fix...

@oscargus
Copy link
Member

oscargus commented Oct 6, 2022

Maybe better to check for None in intersection and return None if any of the bboxes are None?

@timhoffm
Copy link
Member

timhoffm commented Oct 7, 2022

What is the tightbox expectation 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 None from intersection(). But that ship has sailed. There is no reasonable migration path.

Can the the tightbox code catch None and turn it into a null bbox. API impact to be checked:

  • The above case happens when we have a non-intersecting clip_box and a defined clip_path. This broken code would be replaced by returning a null bbox. --> ok
  • We were returning None without error if there was a non-intersecting clip_box and no clip_path or if there was a non-intersecting clip_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
Copy link
Member

rcomer commented Nov 8, 2023

This seems to have been fixed somehow. With v3.8 I get

test

@rcomer rcomer closed this as completed Nov 8, 2023
@rcomer
Copy link
Member

rcomer commented Nov 8, 2023

Bisect points to #25710 as the fix.

@rcomer rcomer added this to the v3.8.0 milestone Nov 8, 2023
@oscargus
Copy link
Member

oscargus commented 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
Copy link
Member

rcomer commented Nov 8, 2023

x and y run from -1 to 0.9 so the arrows not extending to the top is fine I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants