diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index f4d005d4e324..b8784f2250f5 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3222,6 +3222,10 @@ def draw(self, renderer): self._update_title_position(renderer) + # Clear axis tick caches for this draw cycle + for _axis in self._axis_map.values(): + _axis._clear_ticks_cache() + if not self.axison: for _axis in self._axis_map.values(): artists.remove(_axis) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 2cd07f869060..c90316e57344 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -673,6 +673,9 @@ def __init__(self, axes, *, pickradius=15, clear=True): self._major_tick_kw = dict() self._minor_tick_kw = dict() + self._cached_ticks_to_draw = None + self._cached_ticklabel_bboxes = None + if clear: self.clear() else: @@ -1302,11 +1305,19 @@ def _tick_group_visible(kw): kw.get('label2On') is not False or kw.get('gridOn') is not False) - def _update_ticks(self): + def _clear_ticks_cache(self): + self._cached_ticks_to_draw = None + self._cached_ticklabel_bboxes = None + + def _update_ticks(self, *, _use_cache=False): """ Update ticks (position and labels) using the current data interval of the axes. Return the list of ticks that will be drawn. """ + # Return cached result if available and requested + if _use_cache and self._cached_ticks_to_draw is not None: + return self._cached_ticks_to_draw + # Check if major ticks should be computed. # Skip if using NullLocator or if all visible components are off. if (self._tick_group_visible(self._major_tick_kw) @@ -1367,16 +1378,28 @@ def _update_ticks(self): if mtransforms._interval_contains_close(interval_t, loc_t): ticks_to_draw.append(tick) + # Only cache the result when called from the draw path + if _use_cache: + self._cached_ticks_to_draw = ticks_to_draw return ticks_to_draw - def _get_ticklabel_bboxes(self, ticks, renderer): + def _get_ticklabel_bboxes(self, ticks, renderer, *, _use_cache=False): """Return lists of bboxes for ticks' label1's and label2's.""" - return ([tick.label1.get_window_extent(renderer) - for tick in ticks - if tick.label1.get_visible() and tick.label1.get_in_layout()], - [tick.label2.get_window_extent(renderer) - for tick in ticks - if tick.label2.get_visible() and tick.label2.get_in_layout()]) + # Return cached result if available and requested + if _use_cache and self._cached_ticklabel_bboxes is not None: + return self._cached_ticklabel_bboxes + + result = ([tick.label1.get_window_extent(renderer) + for tick in ticks + if tick.label1.get_visible() and tick.label1.get_in_layout()], + [tick.label2.get_window_extent(renderer) + for tick in ticks + if tick.label2.get_visible() and tick.label2.get_in_layout()]) + + # Only cache the result when called from the draw path + if _use_cache: + self._cached_ticklabel_bboxes = result + return result def get_tightbbox(self, renderer=None, *, for_layout_only=False): """ @@ -1392,12 +1415,18 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): return if renderer is None: renderer = self.get_figure(root=True)._get_renderer() - ticks_to_draw = self._update_ticks() + # We need to reset the ticks cache here - get_tightbbox() is called + # during layout calculations (e.g., constrained_layout) outside of + # draw(), and must always recalculate to reflect current state. + self._clear_ticks_cache() - self._update_label_position(renderer) + ticks_to_draw = self._update_ticks(_use_cache=True) + + self._update_label_position(renderer, _use_cache=True) # go back to just this axis's tick labels - tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) + tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer, + _use_cache=True) self._update_offset_text_position(tlb1, tlb2) self.offsetText.set_text(self.major.formatter.get_offset()) @@ -1423,6 +1452,8 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): bb.y1 = bb.y0 + 1.0 bboxes.append(bb) bboxes = [b for b in bboxes if b._is_finite()] + self._clear_ticks_cache() + if bboxes: return mtransforms.Bbox.union(bboxes) else: @@ -1444,14 +1475,17 @@ def draw(self, renderer): return renderer.open_group(__name__, gid=self.get_gid()) - ticks_to_draw = self._update_ticks() - tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) + self._clear_ticks_cache() + + ticks_to_draw = self._update_ticks(_use_cache=True) + tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer, + _use_cache=True) for tick in ticks_to_draw: tick.draw(renderer) # Shift label away from axes to avoid overlapping ticklabels. - self._update_label_position(renderer) + self._update_label_position(renderer, _use_cache=True) self.label.draw(renderer) self._update_offset_text_position(tlb1, tlb2) @@ -1459,6 +1493,7 @@ def draw(self, renderer): self.offsetText.draw(renderer) renderer.close_group(__name__) + self._clear_ticks_cache() self.stale = False def get_gridlines(self): @@ -1494,7 +1529,7 @@ def get_pickradius(self): def get_majorticklabels(self): """Return this Axis' major tick labels, as a list of `~.text.Text`.""" - self._update_ticks() + self._update_ticks(_use_cache=False) ticks = self.get_major_ticks() labels1 = [tick.label1 for tick in ticks if tick.label1.get_visible()] labels2 = [tick.label2 for tick in ticks if tick.label2.get_visible()] @@ -1502,7 +1537,7 @@ def get_majorticklabels(self): def get_minorticklabels(self): """Return this Axis' minor tick labels, as a list of `~.text.Text`.""" - self._update_ticks() + self._update_ticks(_use_cache=False) ticks = self.get_minor_ticks() labels1 = [tick.label1 for tick in ticks if tick.label1.get_visible()] labels2 = [tick.label2 for tick in ticks if tick.label2.get_visible()] @@ -2264,7 +2299,7 @@ def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs): self.set_ticklabels(labels, minor=minor, **kwargs) return result - def _get_tick_boxes_siblings(self, renderer): + def _get_tick_boxes_siblings(self, renderer, *, _use_cache=False): """ Get the bounding boxes for this `.axis` and its siblings as set by `.Figure.align_xlabels` or `.Figure.align_ylabels`. @@ -2281,13 +2316,14 @@ def _get_tick_boxes_siblings(self, renderer): # If we want to align labels from other Axes: for ax in grouper.get_siblings(self.axes): axis = ax._axis_map[name] - ticks_to_draw = axis._update_ticks() - tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer) + ticks_to_draw = axis._update_ticks(_use_cache=_use_cache) + tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer, + _use_cache=_use_cache) bboxes.extend(tlb) bboxes2.extend(tlb2) return bboxes, bboxes2 - def _update_label_position(self, renderer): + def _update_label_position(self, renderer, *, _use_cache=False): """ Update the label position based on the bounding box enclosing all the ticklabels and axis spine. @@ -2484,7 +2520,7 @@ def set_label_position(self, position): self.label_position = position self.stale = True - def _update_label_position(self, renderer): + def _update_label_position(self, renderer, *, _use_cache=False): """ Update the label position based on the bounding box enclosing all the ticklabels and axis spine @@ -2494,7 +2530,8 @@ def _update_label_position(self, renderer): # get bounding boxes for this axis and any siblings # that have been set by `fig.align_xlabels()` - bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer) + bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer, + _use_cache=_use_cache) x, y = self.label.get_position() if self.label_position == 'bottom': @@ -2711,7 +2748,7 @@ def set_label_position(self, position): self.label_position = position self.stale = True - def _update_label_position(self, renderer): + def _update_label_position(self, renderer, *, _use_cache=False): """ Update the label position based on the bounding box enclosing all the ticklabels and axis spine @@ -2721,7 +2758,8 @@ def _update_label_position(self, renderer): # get bounding boxes for this axis and any siblings # that have been set by `fig.align_ylabels()` - bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer) + bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer, + _use_cache=_use_cache) x, y = self.label.get_position() if self.label_position == 'left': diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 741491b3dc58..4470664c8b7a 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -155,7 +155,7 @@ def get_window_extent(self, renderer=None): if self.axis is None or not self.axis.get_visible(): return bb bboxes = [bb] - drawn_ticks = self.axis._update_ticks() + drawn_ticks = self.axis._update_ticks(_use_cache=True) major_tick = next(iter({*drawn_ticks} & {*self.axis.majorTicks}), None) minor_tick = next(iter({*drawn_ticks} & {*self.axis.minorTicks}), None) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index b9323897c4d3..40d18391fc20 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -451,6 +451,8 @@ def draw(self, renderer): artist.do_3d_projection() if self._axis3don: + for axis in self._axis_map.values(): + axis._clear_ticks_cache() # Draw panes first for axis in self._axis_map.values(): axis.draw_pane(renderer) diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index fdd22b717f67..0350c639c966 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -435,7 +435,7 @@ def _axmask(self): def _draw_ticks(self, renderer, edgep1, centers, deltas, highs, deltas_per_point, pos): - ticks = self._update_ticks() + ticks = self._update_ticks(_use_cache=True) info = self._axinfo index = info["i"] juggled = info["juggled"] @@ -569,6 +569,7 @@ def draw(self, renderer): self.label._transform = self.axes.transData self.offsetText._transform = self.axes.transData renderer.open_group("axis3d", gid=self.get_gid()) + self._clear_ticks_cache() # Get general axis information: mins, maxs, tc, highs = self._get_coord_info() @@ -627,6 +628,7 @@ def draw(self, renderer): self._draw_labels(renderer, edgep1, edgep2, labeldeltas, centers, dx, dy) renderer.close_group('axis3d') + self._clear_ticks_cache() self.stale = False @artist.allow_rasterization @@ -636,7 +638,7 @@ def draw_grid(self, renderer): renderer.open_group("grid3d", gid=self.get_gid()) - ticks = self._update_ticks() + ticks = self._update_ticks(_use_cache=True) if len(ticks): # Get general axis information: info = self._axinfo @@ -674,6 +676,12 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): # docstring inherited if not self.get_visible(): return + + # We need to reset the ticks cache here - get_tightbbox() is called + # during layout calculations (e.g., constrained_layout) outside of + # draw(), and must always recalculate to reflect current state. + self._clear_ticks_cache() + # We have to directly access the internal data structures # (and hope they are up to date) because at draw time we # shift the ticks and their labels around in (x, y) space @@ -705,7 +713,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): ticks = ticks_to_draw - bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer) + bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer, _use_cache=True) other = [] if self.offsetText.get_visible() and self.offsetText.get_text(): @@ -716,6 +724,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): self.label.get_text()): other.append(self.label.get_window_extent(renderer)) + self._clear_ticks_cache() return mtransforms.Bbox.union([*bb_1, *bb_2, *other]) d_interval = _api.deprecated(