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

Skip to content

Commit af76ddf

Browse files
authored
Merge pull request #21935 from lukelbd/tightbbox-speedup
Significantly improve tight layout performance for cartopy axes
2 parents 959e76f + 9ef9d01 commit af76ddf

File tree

3 files changed

+66
-47
lines changed

3 files changed

+66
-47
lines changed

lib/matplotlib/artist.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -320,23 +320,6 @@ 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
339-
340323
def get_tightbbox(self, renderer):
341324
"""
342325
Like `.Artist.get_window_extent`, but includes any clipping.
@@ -358,7 +341,7 @@ def get_tightbbox(self, renderer):
358341
if clip_box is not None:
359342
bbox = Bbox.intersection(bbox, clip_box)
360343
clip_path = self.get_clip_path()
361-
if clip_path is not None and bbox is not None:
344+
if clip_path is not None:
362345
clip_path = clip_path.get_fully_transformed_path()
363346
bbox = Bbox.intersection(bbox, clip_path.get_extents())
364347
return bbox
@@ -844,6 +827,30 @@ def get_in_layout(self):
844827
"""
845828
return self._in_layout
846829

830+
def _fully_clipped_to_axes(self):
831+
"""
832+
Return a boolean flag, ``True`` if the artist is clipped to the axes
833+
and can thus be skipped in layout calculations. Requires `get_clip_on`
834+
is True, one of `clip_box` or `clip_path` is set, ``clip_box.extents``
835+
is equivalent to ``ax.bbox.extents`` (if set), and ``clip_path._patch``
836+
is equivalent to ``ax.patch`` (if set).
837+
"""
838+
# Note that ``clip_path.get_fully_transformed_path().get_extents()``
839+
# cannot be directly compared to ``axes.bbox.extents`` because the
840+
# extents may be undefined (i.e. equivalent to ``Bbox.null()``)
841+
# before the associated artist is drawn, and this method is meant
842+
# to determine whether ``axes.get_tightbbox()`` may bypass drawing
843+
clip_box = self.get_clip_box()
844+
clip_path = self.get_clip_path()
845+
return (self.axes is not None
846+
and self.get_clip_on()
847+
and (clip_box is not None or clip_path is not None)
848+
and (clip_box is None
849+
or np.all(clip_box.extents == self.axes.bbox.extents))
850+
and (clip_path is None
851+
or isinstance(clip_path, TransformedPatchPath)
852+
and clip_path._patch is self.axes.patch))
853+
847854
def get_clip_on(self):
848855
"""Return whether the artist uses clipping."""
849856
return self._clipon

lib/matplotlib/axes/_base.py

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import numpy as np
1313

1414
import matplotlib as mpl
15-
from matplotlib import _api, cbook, docstring
15+
from matplotlib import _api, cbook, docstring, offsetbox
1616
import matplotlib.artist as martist
1717
import matplotlib.axis as maxis
1818
from matplotlib.cbook import _OrderedSet, _check_1d, index_of
@@ -4541,21 +4541,26 @@ def get_default_bbox_extra_artists(self):
45414541

45424542
artists = self.get_children()
45434543

4544+
for _axis in self._get_axis_list():
4545+
# axis tight bboxes are calculated separately inside
4546+
# Axes.get_tightbbox() using for_layout_only=True
4547+
artists.remove(_axis)
45444548
if not (self.axison and self._frameon):
45454549
# don't do bbox on spines if frame not on.
45464550
for spine in self.spines.values():
45474551
artists.remove(spine)
45484552

4549-
if not self.axison:
4550-
for _axis in self._get_axis_list():
4551-
artists.remove(_axis)
4552-
45534553
artists.remove(self.title)
45544554
artists.remove(self._left_title)
45554555
artists.remove(self._right_title)
45564556

4557-
return [artist for artist in artists
4558-
if (artist.get_visible() and artist.get_in_layout())]
4557+
# always include types that do not internally implement clipping
4558+
# to axes. may have clip_on set to True and clip_box equivalent
4559+
# to ax.bbox but then ignore these properties during draws.
4560+
noclip = (_AxesBase, maxis.Axis,
4561+
offsetbox.AnnotationBbox, offsetbox.OffsetBox)
4562+
return [a for a in artists if a.get_visible() and a.get_in_layout()
4563+
and (isinstance(a, noclip) or not a._fully_clipped_to_axes())]
45594564

45604565
def get_tightbbox(self, renderer, call_axes_locator=True,
45614566
bbox_extra_artists=None, *, for_layout_only=False):
@@ -4612,17 +4617,11 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
46124617
else:
46134618
self.apply_aspect()
46144619

4615-
if self.axison:
4616-
if self.xaxis.get_visible():
4617-
bb_xaxis = martist._get_tightbbox_for_layout_only(
4618-
self.xaxis, renderer)
4619-
if bb_xaxis:
4620-
bb.append(bb_xaxis)
4621-
if self.yaxis.get_visible():
4622-
bb_yaxis = martist._get_tightbbox_for_layout_only(
4623-
self.yaxis, renderer)
4624-
if bb_yaxis:
4625-
bb.append(bb_yaxis)
4620+
for axis in self._get_axis_list():
4621+
if self.axison and axis.get_visible():
4622+
ba = martist._get_tightbbox_for_layout_only(axis, renderer)
4623+
if ba:
4624+
bb.append(ba)
46264625
self._update_title_position(renderer)
46274626
axbbox = self.get_window_extent(renderer)
46284627
bb.append(axbbox)
@@ -4643,17 +4642,6 @@ def get_tightbbox(self, renderer, call_axes_locator=True,
46434642
bbox_artists = self.get_default_bbox_extra_artists()
46444643

46454644
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
46574645
bbox = a.get_tightbbox(renderer)
46584646
if (bbox is not None
46594647
and 0 < bbox.width < np.inf

lib/matplotlib/tests/test_tightlayout.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,27 @@ 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_clipped_to_axes():
348+
# Ensure that _fully_clipped_to_axes() returns True under default
349+
# conditions for all projection types. Axes.get_tightbbox()
350+
# uses this to skip artists in layout calculations.
351+
arr = np.arange(100).reshape((10, 10))
352+
fig = plt.figure(figsize=(6, 2))
353+
ax1 = fig.add_subplot(131, projection='rectilinear')
354+
ax2 = fig.add_subplot(132, projection='mollweide')
355+
ax3 = fig.add_subplot(133, projection='polar')
356+
for ax in (ax1, ax2, ax3):
357+
# Default conditions (clipped by ax.bbox or ax.patch)
358+
ax.grid(False)
359+
h, = ax.plot(arr[:, 0])
360+
m = ax.pcolor(arr)
361+
assert h._fully_clipped_to_axes()
362+
assert m._fully_clipped_to_axes()
363+
# Non-default conditions (not clipped by ax.patch)
364+
rect = Rectangle((0, 0), 0.5, 0.5, transform=ax.transAxes)
365+
h.set_clip_path(rect)
366+
m.set_clip_path(rect.get_path(), rect.get_transform())
367+
assert not h._fully_clipped_to_axes()
368+
assert not m._fully_clipped_to_axes()

0 commit comments

Comments
 (0)