Skip to content

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

Closed

Description

@ondrolexa

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

Activity

  1. oscargus commented on Oct 6, 2022

    @oscargus
    Member

    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...

  2. oscargus commented on Oct 6, 2022

    @oscargus
    Member

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

  3. timhoffm commented on Oct 7, 2022

    @timhoffm
    Member

    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.
  4. rcomer commented on Nov 8, 2023

    @rcomer
    Member

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

    test

  5. rcomer commented on Nov 8, 2023

    @rcomer
    Member

    Bisect points to #25710 as the fix.

  6. added this to the v3.8.0 milestone on Nov 8, 2023
  7. oscargus commented on Nov 8, 2023

    @oscargus
    Member

    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?)

  8. rcomer commented on Nov 8, 2023

    @rcomer
    Member

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions