From 4ffb8d354a06f8b65d44d5a0e7cb988e4d8bd7a3 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Mon, 2 Feb 2026 16:12:56 -0700 Subject: [PATCH 1/5] Fix text appearing outside axis scale range --- lib/matplotlib/artist.py | 16 ++++++++++++++++ lib/matplotlib/tests/test_text.py | 12 ++++++++++++ lib/matplotlib/text.py | 8 ++++++++ 3 files changed, 36 insertions(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 5c7d68557e0f..d4a0464dbe2e 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -375,6 +375,22 @@ def get_window_extent(self, renderer=None): """ return Bbox([[0, 0], [0, 0]]) + def _in_axes_domain(self, x, y): + """ + Check if the data point (x, y) is within the valid domain of the axes + scales. + + Returns True if no axes or if the point is in the valid domain. + """ + ax = self.axes + if ax is None: + return True + for val, axis in [(x, ax.xaxis), (y, ax.yaxis)]: + vmin, vmax = axis.limit_range_for_scale(val, val) + if vmin != val or vmax != val: + return False + return True + def get_tightbbox(self, renderer=None): """ Get the artist's bounding box in display space, taking clipping into account. diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 551adbedbc61..46231dd70908 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1226,3 +1226,15 @@ def test_ytick_rotation_mode(): tick.set_rotation(angle) plt.subplots_adjust(left=0.4, right=0.6, top=.99, bottom=.01) + + +def test_text_tightbbox_outside_scale_domain(): + # Test that text at positions outside the valid domain of axes scales + # (e.g., negative coordinates with log scale) returns a null bbox. + fig, ax = plt.subplots() + ax.set_yscale('log') + ax.set_ylim(1, 100) + + invalid_text = ax.text(0, -5, 'invalid') + invalid_bbox = invalid_text.get_tightbbox(fig.canvas.get_renderer()) + assert not np.isfinite(invalid_bbox.width) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index d794cab1339b..b3d66a5c21f1 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1060,6 +1060,14 @@ def get_window_extent(self, renderer=None, dpi=None): bbox = bbox.translated(x, y) return bbox + def get_tightbbox(self, renderer=None): + # Exclude text at data coordinates outside the valid domain of the axes + # scales (e.g., negative coordinates with a log scale). + if (self.axes is not None and self.get_transform() == self.axes.transData + and not self._in_axes_domain(*self.get_unitless_position())): + return Bbox.null() + return super().get_tightbbox(renderer) + def set_backgroundcolor(self, color): """ Set the background color of the text. From 20103e791dba407587f263dc46c2e6da709e8179 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Tue, 3 Feb 2026 09:42:16 -0700 Subject: [PATCH 2/5] Rename to _outside_axes_domain --- lib/matplotlib/artist.py | 13 +++++++------ lib/matplotlib/text.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index d4a0464dbe2e..6e64d36c5765 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -375,21 +375,22 @@ def get_window_extent(self, renderer=None): """ return Bbox([[0, 0], [0, 0]]) - def _in_axes_domain(self, x, y): + def _outside_axes_domain(self, x, y): """ - Check if the data point (x, y) is within the valid domain of the axes + Check if the data point (x, y) is outside the valid domain of the axes scales. - Returns True if no axes or if the point is in the valid domain. + Returns True if the artist is in an Axes but the point is outside its + data range (eg. negative coordinates with a log scale). """ ax = self.axes if ax is None: - return True + return False for val, axis in [(x, ax.xaxis), (y, ax.yaxis)]: vmin, vmax = axis.limit_range_for_scale(val, val) if vmin != val or vmax != val: - return False - return True + return True + return False def get_tightbbox(self, renderer=None): """ diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index b3d66a5c21f1..c4c960757215 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1063,8 +1063,8 @@ def get_window_extent(self, renderer=None, dpi=None): def get_tightbbox(self, renderer=None): # Exclude text at data coordinates outside the valid domain of the axes # scales (e.g., negative coordinates with a log scale). - if (self.axes is not None and self.get_transform() == self.axes.transData - and not self._in_axes_domain(*self.get_unitless_position())): + if (self._outside_axes_domain(*self.get_unitless_position()) + and self.get_transform() == self.axes.transData): return Bbox.null() return super().get_tightbbox(renderer) From 371fcb29cd7a8ac71f613af30f0c7fcecc9dde68 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:50:15 -0700 Subject: [PATCH 3/5] Update lib/matplotlib/text.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/text.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index c4c960757215..02c9e5d08449 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1063,8 +1063,8 @@ def get_window_extent(self, renderer=None, dpi=None): def get_tightbbox(self, renderer=None): # Exclude text at data coordinates outside the valid domain of the axes # scales (e.g., negative coordinates with a log scale). - if (self._outside_axes_domain(*self.get_unitless_position()) - and self.get_transform() == self.axes.transData): + if (self.axes and self.get_transform() == self.axes.transData # text is a data Artist + and self._outside_axes_domain(*self.get_unitless_position())): return Bbox.null() return super().get_tightbbox(renderer) From d1bd978aa28cd21352e888016c913f457766559a Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Wed, 25 Feb 2026 15:34:36 -0700 Subject: [PATCH 4/5] Move domain check to axes --- lib/matplotlib/artist.py | 17 ----------------- lib/matplotlib/axes/_base.py | 14 ++++++++++++++ lib/matplotlib/text.py | 5 +++-- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 6e64d36c5765..5c7d68557e0f 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -375,23 +375,6 @@ def get_window_extent(self, renderer=None): """ return Bbox([[0, 0], [0, 0]]) - def _outside_axes_domain(self, x, y): - """ - Check if the data point (x, y) is outside the valid domain of the axes - scales. - - Returns True if the artist is in an Axes but the point is outside its - data range (eg. negative coordinates with a log scale). - """ - ax = self.axes - if ax is None: - return False - for val, axis in [(x, ax.xaxis), (y, ax.yaxis)]: - vmin, vmax = axis.limit_range_for_scale(val, val) - if vmin != val or vmax != val: - return True - return False - def get_tightbbox(self, renderer=None): """ Get the artist's bounding box in display space, taking clipping into account. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ecff24540690..b8d6d74403f0 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2443,6 +2443,20 @@ def _add_text(self, txt): self.stale = True return txt + def _point_in_data_domain(self, x, y): + """ + Check if the data point (x, y) is within the valid domain of the axes + scales. + + Returns False if the point is outside the data range + (e.g. negative coordinates with a log scale). + """ + for val, axis in [(x, self.xaxis), (y, self.yaxis)]: + vmin, vmax = axis.limit_range_for_scale(val, val) + if vmin != val or vmax != val: + return False + return True + def _update_line_limits(self, line): """ Figures out the data limit of the given line, updating `.Axes.dataLim`. diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 02c9e5d08449..7422affab2cb 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1063,8 +1063,9 @@ def get_window_extent(self, renderer=None, dpi=None): def get_tightbbox(self, renderer=None): # Exclude text at data coordinates outside the valid domain of the axes # scales (e.g., negative coordinates with a log scale). - if (self.axes and self.get_transform() == self.axes.transData # text is a data Artist - and self._outside_axes_domain(*self.get_unitless_position())): + if (self.axes + and self.get_transform() == self.axes.transData + and not self.axes._point_in_data_domain(*self.get_unitless_position())): return Bbox.null() return super().get_tightbbox(renderer) From bb9daeeef1a8dd2b03e0128116c12772eee53e61 Mon Sep 17 00:00:00 2001 From: Scott Shambaugh Date: Mon, 9 Mar 2026 21:06:23 -0600 Subject: [PATCH 5/5] Use _axis_map --- lib/matplotlib/axes/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index b8d6d74403f0..5a1ee06d845a 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2451,7 +2451,7 @@ def _point_in_data_domain(self, x, y): Returns False if the point is outside the data range (e.g. negative coordinates with a log scale). """ - for val, axis in [(x, self.xaxis), (y, self.yaxis)]: + for val, axis in zip([x, y], self._axis_map.values()): vmin, vmax = axis.limit_range_for_scale(val, val) if vmin != val or vmax != val: return False