From 64fef474e52213b0962e57e2b1ce917146396c27 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 21 Jan 2026 17:35:09 -0700 Subject: [PATCH 1/8] Cache ticks each draw cycle Fix test --- lib/matplotlib/axis.py | 22 ++++++++++++++++++---- lib/matplotlib/spines.py | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 2cd07f869060..386ee14fb68b 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -673,6 +673,8 @@ def __init__(self, axes, *, pickradius=15, clear=True): self._major_tick_kw = dict() self._minor_tick_kw = dict() + self._cached_ticks_to_draw = None + if clear: self.clear() else: @@ -1302,11 +1304,15 @@ def _tick_group_visible(kw): kw.get('label2On') is not False or kw.get('gridOn') is not False) - def _update_ticks(self): + def _update_ticks(self, *, _use_cached=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_cached 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,6 +1373,8 @@ def _update_ticks(self): if mtransforms._interval_contains_close(interval_t, loc_t): ticks_to_draw.append(tick) + # Cache the result before returning + self._cached_ticks_to_draw = ticks_to_draw return ticks_to_draw def _get_ticklabel_bboxes(self, ticks, renderer): @@ -1392,7 +1400,10 @@ 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() + # Don't use cached values here - get_tightbbox() is called during + # layout calculations (e.g., constrained_layout) outside of draw(), + # and must always recalculate to reflect current state. + ticks_to_draw = self._update_ticks(_use_cached=False) self._update_label_position(renderer) @@ -1444,7 +1455,10 @@ def draw(self, renderer): return renderer.open_group(__name__, gid=self.get_gid()) - ticks_to_draw = self._update_ticks() + # Clear tick cache for this draw cycle + self._cached_ticks_to_draw = None + + ticks_to_draw = self._update_ticks(_use_cached=True) tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) for tick in ticks_to_draw: @@ -2281,7 +2295,7 @@ 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() + ticks_to_draw = axis._update_ticks(_use_cached=True) tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer) bboxes.extend(tlb) bboxes2.extend(tlb2) diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 741491b3dc58..d2f141a2f79d 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_cached=True) major_tick = next(iter({*drawn_ticks} & {*self.axis.majorTicks}), None) minor_tick = next(iter({*drawn_ticks} & {*self.axis.minorTicks}), None) From 3fe6b24e6e2f7658ec6d96e34d8ca4e1446797bd Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 21 Jan 2026 17:46:06 -0700 Subject: [PATCH 2/8] Cache ticklabel bboxes each draw cycle --- lib/matplotlib/axis.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 386ee14fb68b..c286dc4ffd3c 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -674,6 +674,7 @@ def __init__(self, axes, *, pickradius=15, clear=True): self._minor_tick_kw = dict() self._cached_ticks_to_draw = None + self._cached_ticklabel_bboxes = None if clear: self.clear() @@ -1377,14 +1378,22 @@ def _update_ticks(self, *, _use_cached=False): 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_cached=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_cached 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()]) + + # Cache the result before returning + self._cached_ticklabel_bboxes = result + return result def get_tightbbox(self, renderer=None, *, for_layout_only=False): """ @@ -1408,7 +1417,8 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): self._update_label_position(renderer) # 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_cached=False) self._update_offset_text_position(tlb1, tlb2) self.offsetText.set_text(self.major.formatter.get_offset()) @@ -1455,11 +1465,13 @@ def draw(self, renderer): return renderer.open_group(__name__, gid=self.get_gid()) - # Clear tick cache for this draw cycle + # Clear caches for this draw cycle self._cached_ticks_to_draw = None + self._cached_ticklabel_bboxes = None ticks_to_draw = self._update_ticks(_use_cached=True) - tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) + tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer, + _use_cached=True) for tick in ticks_to_draw: tick.draw(renderer) @@ -2296,7 +2308,8 @@ def _get_tick_boxes_siblings(self, renderer): for ax in grouper.get_siblings(self.axes): axis = ax._axis_map[name] ticks_to_draw = axis._update_ticks(_use_cached=True) - tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer) + tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer, + _use_cached=True) bboxes.extend(tlb) bboxes2.extend(tlb2) return bboxes, bboxes2 From deada8128f808dc0ddcfd165f56df40af0f477d5 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 21 Jan 2026 18:37:15 -0700 Subject: [PATCH 3/8] Cache ticks and ticklabel bboxes for 3d axes --- lib/mpl_toolkits/mplot3d/axes3d.py | 3 +++ lib/mpl_toolkits/mplot3d/axis3d.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index b9323897c4d3..5c8ce10b06d5 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -451,6 +451,9 @@ def draw(self, renderer): artist.do_3d_projection() if self._axis3don: + for axis in self._axis_map.values(): + axis._cached_ticks_to_draw = None + axis._cached_ticklabel_bboxes = None # 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..c8922933e63b 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_cached=True) info = self._axinfo index = info["i"] juggled = info["juggled"] @@ -629,6 +629,10 @@ def draw(self, renderer): renderer.close_group('axis3d') self.stale = False + # Reset cached values for next draw cycle, in case not called by Axes3D.draw() + self._cached_ticks_to_draw = None + self._cached_ticklabel_bboxes = None + @artist.allow_rasterization def draw_grid(self, renderer): if not self.axes._draw_grid: @@ -636,7 +640,7 @@ def draw_grid(self, renderer): renderer.open_group("grid3d", gid=self.get_gid()) - ticks = self._update_ticks() + ticks = self._update_ticks(_use_cached=True) if len(ticks): # Get general axis information: info = self._axinfo @@ -705,7 +709,10 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): ticks = ticks_to_draw - bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer) + # Don't use cached values here - get_tightbbox() is called during + # layout calculations (e.g., constrained_layout) outside of draw(), + # and must always recalculate to reflect current state. + bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer, _use_cached=False) other = [] if self.offsetText.get_visible() and self.offsetText.get_text(): From 274b6a8cefb7508ef1bf64e25b5628368b29c151 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 21 Jan 2026 19:01:05 -0700 Subject: [PATCH 4/8] Fix tests --- lib/matplotlib/axes/_base.py | 5 +++ lib/matplotlib/axis.py | 56 ++++++++++++++++-------------- lib/matplotlib/spines.py | 2 +- lib/mpl_toolkits/mplot3d/axis3d.py | 6 ++-- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index f4d005d4e324..d95cd1c53f02 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3222,6 +3222,11 @@ 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._cached_ticks_to_draw = None + _axis._cached_ticklabel_bboxes = None + 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 c286dc4ffd3c..340779dc6970 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1305,13 +1305,13 @@ def _tick_group_visible(kw): kw.get('label2On') is not False or kw.get('gridOn') is not False) - def _update_ticks(self, *, _use_cached=False): + 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_cached and self._cached_ticks_to_draw is not None: + 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. @@ -1374,14 +1374,15 @@ def _update_ticks(self, *, _use_cached=False): if mtransforms._interval_contains_close(interval_t, loc_t): ticks_to_draw.append(tick) - # Cache the result before returning - self._cached_ticks_to_draw = ticks_to_draw + # 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, *, _use_cached=False): + def _get_ticklabel_bboxes(self, ticks, renderer, *, _use_cache=False): """Return lists of bboxes for ticks' label1's and label2's.""" # Return cached result if available and requested - if _use_cached and self._cached_ticklabel_bboxes is not None: + if _use_cache and self._cached_ticklabel_bboxes is not None: return self._cached_ticklabel_bboxes result = ([tick.label1.get_window_extent(renderer) @@ -1391,8 +1392,9 @@ def _get_ticklabel_bboxes(self, ticks, renderer, *, _use_cached=False): for tick in ticks if tick.label2.get_visible() and tick.label2.get_in_layout()]) - # Cache the result before returning - self._cached_ticklabel_bboxes = result + # 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): @@ -1412,13 +1414,13 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): # Don't use cached values here - get_tightbbox() is called during # layout calculations (e.g., constrained_layout) outside of draw(), # and must always recalculate to reflect current state. - ticks_to_draw = self._update_ticks(_use_cached=False) + ticks_to_draw = self._update_ticks(_use_cache=False) - self._update_label_position(renderer) + self._update_label_position(renderer, _use_cache=False) # go back to just this axis's tick labels tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer, - _use_cached=False) + _use_cache=False) self._update_offset_text_position(tlb1, tlb2) self.offsetText.set_text(self.major.formatter.get_offset()) @@ -1465,19 +1467,15 @@ def draw(self, renderer): return renderer.open_group(__name__, gid=self.get_gid()) - # Clear caches for this draw cycle - self._cached_ticks_to_draw = None - self._cached_ticklabel_bboxes = None - - ticks_to_draw = self._update_ticks(_use_cached=True) + ticks_to_draw = self._update_ticks(_use_cache=True) tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer, - _use_cached=True) + _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) @@ -1487,6 +1485,10 @@ def draw(self, renderer): renderer.close_group(__name__) self.stale = False + # Reset cached values for next draw cycle, in case not called by Axes.draw() + self._cached_ticks_to_draw = None + self._cached_ticklabel_bboxes = None + def get_gridlines(self): r"""Return this Axis' grid lines as a list of `.Line2D`\s.""" ticks = self.get_major_ticks() @@ -2290,7 +2292,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`. @@ -2307,14 +2309,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(_use_cached=True) + ticks_to_draw = axis._update_ticks(_use_cache=_use_cache) tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer, - _use_cached=True) + _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. @@ -2511,7 +2513,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 @@ -2521,7 +2523,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': @@ -2738,7 +2741,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 @@ -2748,7 +2751,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 d2f141a2f79d..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(_use_cached=True) + 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/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index c8922933e63b..05461d23c9b7 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(_use_cached=True) + ticks = self._update_ticks(_use_cache=True) info = self._axinfo index = info["i"] juggled = info["juggled"] @@ -640,7 +640,7 @@ def draw_grid(self, renderer): renderer.open_group("grid3d", gid=self.get_gid()) - ticks = self._update_ticks(_use_cached=True) + ticks = self._update_ticks(_use_cache=True) if len(ticks): # Get general axis information: info = self._axinfo @@ -712,7 +712,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): # Don't use cached values here - get_tightbbox() is called during # layout calculations (e.g., constrained_layout) outside of draw(), # and must always recalculate to reflect current state. - bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer, _use_cached=False) + bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer, _use_cache=False) other = [] if self.offsetText.get_visible() and self.offsetText.get_text(): From 62d8bfcaa29fb3ffb2783b128aa350ade577b97a Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Thu, 22 Jan 2026 14:16:36 -0700 Subject: [PATCH 5/8] Extra ticks cache guard for get_tightbbox --- lib/matplotlib/axes/_base.py | 3 +-- lib/matplotlib/axis.py | 8 ++++++-- lib/mpl_toolkits/mplot3d/axes3d.py | 3 +-- lib/mpl_toolkits/mplot3d/axis3d.py | 3 +-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index d95cd1c53f02..b8784f2250f5 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3224,8 +3224,7 @@ def draw(self, renderer): # Clear axis tick caches for this draw cycle for _axis in self._axis_map.values(): - _axis._cached_ticks_to_draw = None - _axis._cached_ticklabel_bboxes = None + _axis._clear_ticks_cache() if not self.axison: for _axis in self._axis_map.values(): diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 340779dc6970..129028e701dc 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1305,6 +1305,10 @@ def _tick_group_visible(kw): kw.get('label2On') is not False or kw.get('gridOn') is not False) + 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 @@ -1414,6 +1418,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): # Don't use cached values 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() ticks_to_draw = self._update_ticks(_use_cache=False) self._update_label_position(renderer, _use_cache=False) @@ -1486,8 +1491,7 @@ def draw(self, renderer): self.stale = False # Reset cached values for next draw cycle, in case not called by Axes.draw() - self._cached_ticks_to_draw = None - self._cached_ticklabel_bboxes = None + self._clear_ticks_cache() def get_gridlines(self): r"""Return this Axis' grid lines as a list of `.Line2D`\s.""" diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5c8ce10b06d5..40d18391fc20 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -452,8 +452,7 @@ def draw(self, renderer): if self._axis3don: for axis in self._axis_map.values(): - axis._cached_ticks_to_draw = None - axis._cached_ticklabel_bboxes = None + 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 05461d23c9b7..4586fcee6911 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -630,8 +630,7 @@ def draw(self, renderer): self.stale = False # Reset cached values for next draw cycle, in case not called by Axes3D.draw() - self._cached_ticks_to_draw = None - self._cached_ticklabel_bboxes = None + self._clear_ticks_cache() @artist.allow_rasterization def draw_grid(self, renderer): From 771922a64a5181acaf01965a8c679acf77d0a98d Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Thu, 22 Jan 2026 14:52:32 -0700 Subject: [PATCH 6/8] Cache ticks within get_tightbbox --- lib/matplotlib/axis.py | 21 ++++++++++++--------- lib/mpl_toolkits/mplot3d/axis3d.py | 2 +- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 129028e701dc..7156cc62c81e 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1415,17 +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() - # Don't use cached values here - get_tightbbox() is called during - # layout calculations (e.g., constrained_layout) outside of draw(), - # and must always recalculate to reflect current state. + # 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() - ticks_to_draw = self._update_ticks(_use_cache=False) - self._update_label_position(renderer, _use_cache=False) + 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, - _use_cache=False) + _use_cache=True) self._update_offset_text_position(tlb1, tlb2) self.offsetText.set_text(self.major.formatter.get_offset()) @@ -1451,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: @@ -1472,6 +1475,8 @@ def draw(self, renderer): return renderer.open_group(__name__, gid=self.get_gid()) + 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) @@ -1488,10 +1493,8 @@ def draw(self, renderer): self.offsetText.draw(renderer) renderer.close_group(__name__) - self.stale = False - - # Reset cached values for next draw cycle, in case not called by Axes.draw() self._clear_ticks_cache() + self.stale = False def get_gridlines(self): r"""Return this Axis' grid lines as a list of `.Line2D`\s.""" diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 4586fcee6911..a86406daf30d 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -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() @@ -629,7 +630,6 @@ def draw(self, renderer): renderer.close_group('axis3d') self.stale = False - # Reset cached values for next draw cycle, in case not called by Axes3D.draw() self._clear_ticks_cache() @artist.allow_rasterization From 123700dd5cd7e1730a8decb530611be1e024548f Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Thu, 22 Jan 2026 15:25:49 -0700 Subject: [PATCH 7/8] Same get_tightbbox cache logic for axes3d --- lib/mpl_toolkits/mplot3d/axis3d.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index a86406daf30d..0350c639c966 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -628,9 +628,8 @@ def draw(self, renderer): self._draw_labels(renderer, edgep1, edgep2, labeldeltas, centers, dx, dy) renderer.close_group('axis3d') - self.stale = False - self._clear_ticks_cache() + self.stale = False @artist.allow_rasterization def draw_grid(self, renderer): @@ -677,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 @@ -708,10 +713,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): ticks = ticks_to_draw - # Don't use cached values here - get_tightbbox() is called during - # layout calculations (e.g., constrained_layout) outside of draw(), - # and must always recalculate to reflect current state. - bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer, _use_cache=False) + bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer, _use_cache=True) other = [] if self.offsetText.get_visible() and self.offsetText.get_text(): @@ -722,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( From 7991b9bed67bf39b930b706bd6769abefd0d11a2 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Thu, 22 Jan 2026 16:04:20 -0700 Subject: [PATCH 8/8] Be explicit about _use_cache=False flag --- lib/matplotlib/axis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 7156cc62c81e..c90316e57344 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1529,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()] @@ -1537,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()]