Skip to content

Commit a5a704e

Browse files
committed
Speedup ax.get_tightbbox() for nonrectilinear axes
1 parent 0fc7c4b commit a5a704e

File tree

3 files changed

+49
-27
lines changed

3 files changed

+49
-27
lines changed

lib/matplotlib/artist.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -320,22 +320,34 @@ def get_window_extent(self, renderer):
320320
"""
321321
return Bbox([[0, 0], [0, 0]])
322322

323-
def _get_clipping_extent_bbox(self):
324-
"""
325-
Return a bbox with the extents of the intersection of the clip_path
326-
and clip_box for this artist, or None if both of these are
327-
None, or ``get_clip_on`` is False.
328-
"""
329-
bbox = None
330-
if self.get_clip_on():
331-
clip_box = self.get_clip_box()
332-
if clip_box is not None:
333-
bbox = clip_box
334-
clip_path = self.get_clip_path()
335-
if clip_path is not None and bbox is not None:
336-
clip_path = clip_path.get_fully_transformed_path()
337-
bbox = Bbox.intersection(bbox, clip_path.get_extents())
338-
return bbox
323+
def _is_axes_clipped(self):
324+
"""
325+
Return a boolean indicating whether the artist is clipped by the
326+
axes bounding box or patch. True if ``get_clip_on`` is True, one of
327+
`clip_box` or `clip_path` is set, ``clip_box.extents`` is equivalent
328+
to ``axes.bbox.extents`` (if set), and ``clip_path._patch`` is
329+
equivalent to ``axes.patch`` (if set).
330+
"""
331+
# Note that ``clip_path.get_fully_transformed_path().get_extents()``
332+
# cannot be directly compared to ``axes.bbox.extents`` because the
333+
# extents may be undefined (i.e. positive to negative infinity)
334+
# before the associated artist is drawn, and this method is meant
335+
# to determine whether ``axes.get_tightbbox()`` may bypass drawing
336+
ax = self.axes
337+
clip_on = self.get_clip_on()
338+
clip_box = self.get_clip_box()
339+
clip_path = self.get_clip_path()
340+
if ax is None or not clip_on or clip_box is None and clip_path is None:
341+
return False
342+
if clip_box is not None:
343+
if not np.all(clip_box.extents == ax.bbox.extents):
344+
return False
345+
if clip_path is not None:
346+
if not isinstance(clip_path, TransformedPatchPath):
347+
return False
348+
if clip_path._patch is not ax.patch:
349+
return False
350+
return True
339351

340352
def get_tightbbox(self, renderer):
341353
"""

lib/matplotlib/axes/_base.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4626,6 +4626,8 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
46264626
self._update_title_position(renderer)
46274627
axbbox = self.get_window_extent(renderer)
46284628
bb.append(axbbox)
4629+
patchbbox = self.patch.get_window_extent(renderer)
4630+
bb.append(patchbbox)
46294631

46304632
for title in [self.title, self._left_title, self._right_title]:
46314633
if title.get_visible():
@@ -4642,18 +4644,11 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
46424644
if bbox_artists is None:
46434645
bbox_artists = self.get_default_bbox_extra_artists()
46444646

4647+
include_types = (_AxesBase, maxis.Axis, mspines.Spine)
46454648
for a in bbox_artists:
4646-
# Extra check here to quickly see if clipping is on and
4647-
# contained in the Axes. If it is, don't get the tightbbox for
4648-
# this artist because this can be expensive:
4649-
clip_extent = a._get_clipping_extent_bbox()
4650-
if clip_extent is not None:
4651-
clip_extent = mtransforms.Bbox.intersection(
4652-
clip_extent, axbbox)
4653-
if np.all(clip_extent.extents == axbbox.extents):
4654-
# clip extent is inside the Axes bbox so don't check
4655-
# this artist
4656-
continue
4649+
# skip artists clipped by ax.bbox or ax.patch
4650+
if a._is_axes_clipped() and not isinstance(a, include_types):
4651+
continue
46574652
bbox = a.get_tightbbox(renderer)
46584653
if (bbox is not None
46594654
and 0 < bbox.width < np.inf

lib/matplotlib/tests/test_tightlayout.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,18 @@ def test_manual_colorbar():
342342
fig.colorbar(pts, cax=cax)
343343
with pytest.warns(UserWarning, match="This figure includes Axes"):
344344
fig.tight_layout()
345+
346+
347+
def test_axes_clipped():
348+
# Test that artists with default axes clipping are
349+
# detected and skipped by the tight layout algorithm.
350+
arr = np.arange(100).reshape((10, 10))
351+
fig = plt.figure(figsize=(6, 2))
352+
ax1 = fig.add_subplot(projection='rectilinear')
353+
ax2 = fig.add_subplot(projection='mollweide')
354+
ax3 = fig.add_subplot(projection='polar')
355+
for ax in (ax1, ax2, ax3):
356+
h, = ax.plot(arr[:, 0])
357+
m = ax.pcolor(arr)
358+
assert h._is_axes_clipped()
359+
assert m._is_axes_clipped()

0 commit comments

Comments
 (0)