From 07127b68639825460594a92c364c370f8aedf4bd Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Sun, 19 Apr 2026 12:51:16 +0200 Subject: [PATCH 1/9] BUG: Fix relim() to support Collection artists (scatter, etc.) relim() previously skipped Collection instances entirely. Two issues: 1. add_collection() did not call collection._set_in_autoscale(True), unlike add_line(), add_patch(), and add_image(). This caused relim() to skip collections via the _get_in_autoscale() check. 2. relim() had no handling for Collection instances. Added an elif branch that calls get_datalim() and update_datalim(), mirroring the logic already used in add_collection(). Closes #30859 --- .../next_api_changes/behavior/31530-TZ.rst | 6 ++++ lib/matplotlib/axes/_base.py | 22 ++++++++++--- lib/matplotlib/tests/test_axes.py | 31 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/31530-TZ.rst diff --git a/doc/api/next_api_changes/behavior/31530-TZ.rst b/doc/api/next_api_changes/behavior/31530-TZ.rst new file mode 100644 index 000000000000..470104817a59 --- /dev/null +++ b/doc/api/next_api_changes/behavior/31530-TZ.rst @@ -0,0 +1,6 @@ +``relim()`` now accounts for Collection artists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, `~.axes.Axes.relim` did not recalculate data limits for +`.Collection` artists (e.g. those created by `~.axes.Axes.scatter`). +Calling ``ax.relim()`` followed by ``ax.autoscale_view()`` now correctly +includes scatter plots and other collections in the axes limits. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 1a32af922342..aec1bac79c91 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2434,6 +2434,7 @@ def add_collection(self, collection, autolim=True): self._request_autoscale_view() self.stale = True + collection._set_in_autoscale(True) return collection def add_image(self, image): @@ -2638,15 +2639,11 @@ def relim(self, visible_only=False): """ Recompute the data limits based on current artists. - At present, `.Collection` instances are not supported. - Parameters ---------- visible_only : bool, default: False Whether to exclude invisible artists. """ - # Collections are deliberately not supported (yet); see - # the TODO note in artists.py. self.dataLim.ignore(True) self.dataLim.set_points(mtransforms.Bbox.null().get_points()) self.ignore_existing_data_limits = True @@ -2661,6 +2658,23 @@ def relim(self, visible_only=False): self._update_patch_limits(artist) elif isinstance(artist, mimage.AxesImage): self._update_image_limits(artist) + elif isinstance(artist, mcoll.Collection): + datalim = artist.get_datalim(self.transData) + points = datalim.get_points() + if not np.isinf(datalim.minpos).all(): + points = np.concatenate([points, + [datalim.minpos]]) + x_is_data, y_is_data = ( + artist.get_transform() + .contains_branch_separately(self.transData)) + ox_is_data, oy_is_data = ( + artist.get_offset_transform() + .contains_branch_separately(self.transData)) + self.update_datalim( + points, + updatex=x_is_data or ox_is_data, + updatey=y_is_data or oy_is_data, + ) def update_datalim(self, xys, updatex=True, updatey=True): """ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index f006c624f8d7..7a3f5c76df54 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6511,6 +6511,37 @@ def test_relim_visible_only(): assert ax.get_ylim() == y1 +def test_relim_collection(): + fig, ax = plt.subplots() + sc = ax.scatter([1, 2, 3], [4, 5, 6]) + ax.relim() + ax.autoscale_view() + xlim = ax.get_xlim() + ylim = ax.get_ylim() + assert xlim[0] <= 1 and xlim[1] >= 3 + assert ylim[0] <= 4 and ylim[1] >= 6 + + # After updating offsets, relim should track the new data. + sc.set_offsets([[10, 20], [30, 40]]) + ax.relim() + ax.autoscale_view() + xlim = ax.get_xlim() + ylim = ax.get_ylim() + assert xlim[0] <= 10 and xlim[1] >= 30 + assert ylim[0] <= 20 and ylim[1] >= 40 + + # visible_only=True should ignore hidden collections. + line, = ax.plot([0, 1], [0, 1]) + sc.set_visible(False) + ax.relim(visible_only=True) + ax.autoscale_view() + xlim = ax.get_xlim() + ylim = ax.get_ylim() + # With scatter hidden, limits should be driven by the line only. + assert xlim[1] < 10 + assert ylim[1] < 10 + + def test_text_labelsize(): """ tests for issue #1172 From 1d8b2296cb6877f9520c939f9fdcee1edf909912 Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Mon, 20 Apr 2026 03:01:41 +0200 Subject: [PATCH 2/9] BUG: Add explanatory comments to relim() Collection branch Mirror the comments from add_collection() that explain why minpos is included (log scale support) and why contains_branch_separately is used to conditionally update x/y limits. --- lib/matplotlib/axes/_base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index aec1bac79c91..e5b311c46194 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2662,8 +2662,13 @@ def relim(self, visible_only=False): datalim = artist.get_datalim(self.transData) points = datalim.get_points() if not np.isinf(datalim.minpos).all(): + # As in add_collection: include minpos so that + # self.dataLim updates its own minpos, which ensures + # log scales see the correct minimum. points = np.concatenate([points, [datalim.minpos]]) + # Only update dataLim for x/y if the collection uses + # transData in that direction. x_is_data, y_is_data = ( artist.get_transform() .contains_branch_separately(self.transData)) From 3da28af5d6dcb4b9503be0a5942bdbcc45291c31 Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Mon, 20 Apr 2026 03:47:37 +0200 Subject: [PATCH 3/9] BUG: Preserve autolim=False semantics for Collection relim support _set_in_autoscale(True) was set unconditionally in add_collection(), outside the 'if autolim:' block. This meant that any collection added with autolim=False would still be picked up by relim() later. Fix: move _set_in_autoscale(True) inside the 'if autolim:' block so that relim() only considers collections that explicitly opted in. Add two regression tests: - test_relim_collection_autolim_false: verifies that a collection added with autolim=False does not affect limits after relim(). - test_relim_collection_log_scale: verifies that relim() + autoscale_view() works correctly for a Collection on log-scaled axes (exercises the minpos path). Closes #30859 --- lib/matplotlib/axes/_base.py | 5 ++++- lib/matplotlib/tests/test_axes.py | 34 +++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index e5b311c46194..ca83a4a86248 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2432,9 +2432,12 @@ def add_collection(self, collection, autolim=True): ) if autolim != "_datalim_only": self._request_autoscale_view() + # Mark collection as participating in relim() only when autolim + # is enabled. If autolim=False the caller explicitly opted out, + # so relim() must not pick this collection up later. + collection._set_in_autoscale(True) self.stale = True - collection._set_in_autoscale(True) return collection def add_image(self, image): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 7a3f5c76df54..3ba928c6e759 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6542,6 +6542,40 @@ def test_relim_collection(): assert ylim[1] < 10 +def test_relim_collection_autolim_false(): + # GH#30859 - Collection added with autolim=False must not participate + # in relim() later. + import matplotlib.collections as mcollections + fig, ax = plt.subplots() + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + # Build a collection far outside current limits and add it with autolim=False. + sc = mcollections.PathCollection([]) + sc.set_offsets([[100, 200], [300, 400]]) + ax.add_collection(sc, autolim=False) + ax.relim() + ax.autoscale_view() + # Limits must remain unchanged because autolim=False was requested. + assert ax.get_xlim() == (0, 1) + assert ax.get_ylim() == (0, 1) + + +def test_relim_collection_log_scale(): + # GH#30859 - relim() for Collection on a log-scaled axis should + # correctly pick up minpos so that log scaling works properly. + fig, ax = plt.subplots() + ax.set_xscale('log') + ax.set_yscale('log') + sc = ax.scatter([1e-3, 1e-2, 1e-1], [1e1, 1e2, 1e3]) + sc.set_offsets([[1e1, 1e4], [1e2, 1e5]]) + ax.relim() + ax.autoscale_view() + xlim = ax.get_xlim() + ylim = ax.get_ylim() + assert xlim[0] <= 1e1 and xlim[1] >= 1e2 + assert ylim[0] <= 1e4 and ylim[1] >= 1e5 + + def test_text_labelsize(): """ tests for issue #1172 From 68a6269b973057d7f872eadae9d5fb8bdf20b6bd Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Mon, 20 Apr 2026 21:26:54 +0200 Subject: [PATCH 4/9] BUG: Extract _update_collection_limits helper; call from add_collection and relim() Refactor per reviewer feedback (timhoffm): the Collection data-limit logic in relim() was duplicating code from add_collection(). Extract it into a new private method _update_collection_limits(), following the existing pattern of _update_line_limits(), _update_patch_limits(), and _update_image_limits(). Both add_collection() and relim() now call the helper instead of repeating the get_datalim / minpos-concatenation / contains_branch_separately / update_datalim block inline. --- lib/matplotlib/axes/_base.py | 66 ++++++++++++++---------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ca83a4a86248..89c12485e564 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2410,26 +2410,7 @@ def add_collection(self, collection, autolim=True): # Make sure viewLim is not stale (mostly to match # pre-lazy-autoscale behavior, which is not really better). self._unstale_viewLim() - datalim = collection.get_datalim(self.transData) - points = datalim.get_points() - if not np.isinf(datalim.minpos).all(): - # By definition, if minpos (minimum positive value) is set - # (i.e., non-inf), then min(points) <= minpos <= max(points), - # and minpos would be superfluous. However, we add minpos to - # the call so that self.dataLim will update its own minpos. - # This ensures that log scales see the correct minimum. - points = np.concatenate([points, [datalim.minpos]]) - # only update the dataLim for x/y if the collection uses transData - # in this direction. - x_is_data, y_is_data = (collection.get_transform() - .contains_branch_separately(self.transData)) - ox_is_data, oy_is_data = (collection.get_offset_transform() - .contains_branch_separately(self.transData)) - self.update_datalim( - points, - updatex=x_is_data or ox_is_data, - updatey=y_is_data or oy_is_data, - ) + self._update_collection_limits(collection) if autolim != "_datalim_only": self._request_autoscale_view() # Mark collection as participating in relim() only when autolim @@ -2602,6 +2583,29 @@ def _update_patch_limits(self, patch): xys = trf_to_data.transform(vertices) self.update_datalim(xys, updatex=updatex, updatey=updatey) + def _update_collection_limits(self, collection): + """Update the data limits for the given collection.""" + datalim = collection.get_datalim(self.transData) + points = datalim.get_points() + if not np.isinf(datalim.minpos).all(): + # By definition, if minpos (minimum positive value) is set + # (i.e., non-inf), then min(points) <= minpos <= max(points), + # and minpos would be superfluous. However, we add minpos to + # the call so that self.dataLim will update its own minpos. + # This ensures that log scales see the correct minimum. + points = np.concatenate([points, [datalim.minpos]]) + # only update the dataLim for x/y if the collection uses transData + # in this direction. + x_is_data, y_is_data = (collection.get_transform() + .contains_branch_separately(self.transData)) + ox_is_data, oy_is_data = (collection.get_offset_transform() + .contains_branch_separately(self.transData)) + self.update_datalim( + points, + updatex=x_is_data or ox_is_data, + updatey=y_is_data or oy_is_data, + ) + def add_table(self, tab): """ Add a `.Table` to the Axes; return the table. @@ -2662,27 +2666,7 @@ def relim(self, visible_only=False): elif isinstance(artist, mimage.AxesImage): self._update_image_limits(artist) elif isinstance(artist, mcoll.Collection): - datalim = artist.get_datalim(self.transData) - points = datalim.get_points() - if not np.isinf(datalim.minpos).all(): - # As in add_collection: include minpos so that - # self.dataLim updates its own minpos, which ensures - # log scales see the correct minimum. - points = np.concatenate([points, - [datalim.minpos]]) - # Only update dataLim for x/y if the collection uses - # transData in that direction. - x_is_data, y_is_data = ( - artist.get_transform() - .contains_branch_separately(self.transData)) - ox_is_data, oy_is_data = ( - artist.get_offset_transform() - .contains_branch_separately(self.transData)) - self.update_datalim( - points, - updatex=x_is_data or ox_is_data, - updatey=y_is_data or oy_is_data, - ) + self._update_collection_limits(artist) def update_datalim(self, xys, updatex=True, updatey=True): """ From bfdb6c958064df654941ba3e2091839d0504872f Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Tue, 21 Apr 2026 00:14:58 +0200 Subject: [PATCH 5/9] TST/DOC: align collection relim tests and autolim semantics --- lib/matplotlib/artist.py | 3 --- lib/matplotlib/axes/_base.py | 13 +++++---- lib/matplotlib/tests/test_axes.py | 45 +++++++++++++------------------ 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 1030a6809ceb..a4248bd063e0 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -240,9 +240,6 @@ def remove(self): with `.FigureCanvasBase.draw_idle`. Call `~.axes.Axes.relim` to update the Axes limits if desired. - Note: `~.axes.Axes.relim` will not see collections even if the - collection was added to the Axes with *autolim* = True. - Note: there is no support for removing the artist's legend entry. """ diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 89c12485e564..1b340a62bc03 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2384,7 +2384,10 @@ def add_collection(self, collection, autolim=True): collection : `.Collection` The collection to add. autolim : bool - Whether to update data and view limits. + Whether to update data limits and request autoscaling. + + If *False*, the collection is explicitly excluded from + `~.Axes.relim`. .. versionchanged:: 3.11 @@ -2406,6 +2409,10 @@ def add_collection(self, collection, autolim=True): if collection.get_clip_path() is None: collection.set_clip_path(self.patch) + # Keep relim() participation aligned with the autolim argument. + # autolim can also be the internal sentinel "_datalim_only". + collection._set_in_autoscale(bool(autolim)) + if autolim: # Make sure viewLim is not stale (mostly to match # pre-lazy-autoscale behavior, which is not really better). @@ -2413,10 +2420,6 @@ def add_collection(self, collection, autolim=True): self._update_collection_limits(collection) if autolim != "_datalim_only": self._request_autoscale_view() - # Mark collection as participating in relim() only when autolim - # is enabled. If autolim=False the caller explicitly opted out, - # so relim() must not pick this collection up later. - collection._set_in_autoscale(True) self.stale = True return collection diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 3ba928c6e759..71a9ef62c6d1 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6515,31 +6515,24 @@ def test_relim_collection(): fig, ax = plt.subplots() sc = ax.scatter([1, 2, 3], [4, 5, 6]) ax.relim() - ax.autoscale_view() - xlim = ax.get_xlim() - ylim = ax.get_ylim() - assert xlim[0] <= 1 and xlim[1] >= 3 - assert ylim[0] <= 4 and ylim[1] >= 6 + expected = sc.get_datalim(ax.transData) + assert_allclose(ax.dataLim.get_points(), expected.get_points()) + assert_allclose(ax.dataLim.minpos, expected.minpos) # After updating offsets, relim should track the new data. sc.set_offsets([[10, 20], [30, 40]]) ax.relim() - ax.autoscale_view() - xlim = ax.get_xlim() - ylim = ax.get_ylim() - assert xlim[0] <= 10 and xlim[1] >= 30 - assert ylim[0] <= 20 and ylim[1] >= 40 + expected = sc.get_datalim(ax.transData) + assert_allclose(ax.dataLim.get_points(), expected.get_points()) + assert_allclose(ax.dataLim.minpos, expected.minpos) # visible_only=True should ignore hidden collections. line, = ax.plot([0, 1], [0, 1]) sc.set_visible(False) ax.relim(visible_only=True) - ax.autoscale_view() - xlim = ax.get_xlim() - ylim = ax.get_ylim() # With scatter hidden, limits should be driven by the line only. - assert xlim[1] < 10 - assert ylim[1] < 10 + assert_allclose(ax.dataLim.get_points(), [[0, 0], [1, 1]]) + assert_array_equal(ax.dataLim.minpos, [np.inf, np.inf]) def test_relim_collection_autolim_false(): @@ -6547,33 +6540,31 @@ def test_relim_collection_autolim_false(): # in relim() later. import matplotlib.collections as mcollections fig, ax = plt.subplots() - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) + ax.plot([0, 1], [0, 1]) + ax.relim() + expected = ax.dataLim.frozen() # Build a collection far outside current limits and add it with autolim=False. sc = mcollections.PathCollection([]) sc.set_offsets([[100, 200], [300, 400]]) ax.add_collection(sc, autolim=False) ax.relim() - ax.autoscale_view() - # Limits must remain unchanged because autolim=False was requested. - assert ax.get_xlim() == (0, 1) - assert ax.get_ylim() == (0, 1) + # dataLim must remain unchanged because autolim=False was requested. + assert_allclose(ax.dataLim.get_points(), expected.get_points()) + assert_allclose(ax.dataLim.minpos, expected.minpos) def test_relim_collection_log_scale(): # GH#30859 - relim() for Collection on a log-scaled axis should - # correctly pick up minpos so that log scaling works properly. + # correctly propagate minpos into dataLim. fig, ax = plt.subplots() ax.set_xscale('log') ax.set_yscale('log') sc = ax.scatter([1e-3, 1e-2, 1e-1], [1e1, 1e2, 1e3]) sc.set_offsets([[1e1, 1e4], [1e2, 1e5]]) ax.relim() - ax.autoscale_view() - xlim = ax.get_xlim() - ylim = ax.get_ylim() - assert xlim[0] <= 1e1 and xlim[1] >= 1e2 - assert ylim[0] <= 1e4 and ylim[1] >= 1e5 + expected = sc.get_datalim(ax.transData) + assert_allclose(ax.dataLim.get_points(), expected.get_points()) + assert_allclose(ax.dataLim.minpos, expected.minpos) def test_text_labelsize(): From ee070e3da0bd16ddabb400f978e87187d6ab5414 Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Tue, 21 Apr 2026 00:37:12 +0200 Subject: [PATCH 6/9] TST: fix wrong minpos expectation in test_relim_collection Line data [0, 1] x [0, 1] gives minpos=[1., 1.] (minimum *positive* value), not [inf, inf]. The [inf, inf] sentinel only appears when there are no positive values at all. --- lib/matplotlib/tests/test_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 71a9ef62c6d1..001536062251 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6532,7 +6532,8 @@ def test_relim_collection(): ax.relim(visible_only=True) # With scatter hidden, limits should be driven by the line only. assert_allclose(ax.dataLim.get_points(), [[0, 0], [1, 1]]) - assert_array_equal(ax.dataLim.minpos, [np.inf, np.inf]) + # minpos is the minimum *positive* value; line data [0, 1] gives 1.0. + assert_array_equal(ax.dataLim.minpos, [1., 1.]) def test_relim_collection_autolim_false(): From a1c58ec13809c43393c59142e1068408beed3c9f Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Tue, 21 Apr 2026 06:32:21 +0200 Subject: [PATCH 7/9] DOC/MNT: Address reviewer feedback on add_collection docstring and autoscale semantics - Revert autolim docstring first line to original user-facing wording ("update data and view limits" instead of internal "request autoscaling") - Adopt reviewer suggestion for If-False clause: "does not take part in any limit operations" - Expand versionchanged:: 3.11 note to explain both pre-3.11 behavior and the new relim participation (per reviewer suggestion) - Move _set_in_autoscale(True) inside if autolim: block (simpler and consistent with how add_line/add_patch/add_image do it) - Update stale TODO in Artist.remove: the "collections relim problem" referenced has been fixed; rephrase to reflect the remaining long-term architectural goal --- lib/matplotlib/artist.py | 8 ++++---- lib/matplotlib/axes/_base.py | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index a4248bd063e0..d6db3bb73461 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -268,10 +268,10 @@ def remove(self): else: raise NotImplementedError('cannot remove artist') - # TODO: the fix for the collections relim problem is to move the - # limits calculation into the artist itself, including the property of - # whether or not the artist should affect the limits. Then there will - # be no distinction between axes.add_line, axes.add_patch, etc. + # TODO: move the limits calculation into the artist itself, including + # the property of whether or not the artist should affect the limits. + # Then there will be no distinction between axes.add_line, + # axes.add_patch, etc. # TODO: add legend support def have_units(self): diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 1b340a62bc03..9d049e82e10c 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2384,15 +2384,23 @@ def add_collection(self, collection, autolim=True): collection : `.Collection` The collection to add. autolim : bool - Whether to update data limits and request autoscaling. + Whether to update data and view limits. - If *False*, the collection is explicitly excluded from - `~.Axes.relim`. + If *False*, the collection does not take part in any limit + operations. .. versionchanged:: 3.11 - This now also updates the view limits, making explicit - calls to `~.Axes.autoscale_view` unnecessary. + Since 3.11 `autolim=True` matches the standard behavior + of other ``add_[artist]`` methods: Axes data and view limits + are both updated in the method, and the collection will + be considered in future data limit updates through + `.relim`. + + Prior to matplotlib 3.11 this was only a one-time update + of the data limits. Updating view limits required an + explicit calls to `~.Axes.autoscale_view`, and collections + did not take part in `.relim`. As an implementation detail, the value "_datalim_only" is supported to smooth the internal transition from pre-3.11 @@ -2409,11 +2417,8 @@ def add_collection(self, collection, autolim=True): if collection.get_clip_path() is None: collection.set_clip_path(self.patch) - # Keep relim() participation aligned with the autolim argument. - # autolim can also be the internal sentinel "_datalim_only". - collection._set_in_autoscale(bool(autolim)) - if autolim: + collection._set_in_autoscale(True) # Make sure viewLim is not stale (mostly to match # pre-lazy-autoscale behavior, which is not really better). self._unstale_viewLim() From f90c147dd48650db19b4281c529725500b6a4a5a Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Tue, 21 Apr 2026 06:54:31 +0200 Subject: [PATCH 8/9] TST/BUG: Fix _datalim_only semantics and add user-facing relim test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _set_in_autoscale(True) must not be called for collections added with autolim='_datalim_only' — that sentinel means 'update datalim once but do not enter the autoscale/relim system'. Previously, because the call was outside the if autolim: block, bool('_datalim_only') == True meant 3D collections inadvertently got _in_autoscale=True. Fixed by gating the call on autolim != '_datalim_only', consistent with the existing _request_autoscale_view guard directly below. Also add test_relim_collection_autoscale_view: an end-to-end regression that checks ax.get_xlim()/get_ylim() after relim()+autoscale_view(), matching the exact user-facing scenario from GH#30859. --- lib/matplotlib/axes/_base.py | 3 ++- lib/matplotlib/tests/test_axes.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 9d049e82e10c..efbf2a674ac4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2418,7 +2418,8 @@ def add_collection(self, collection, autolim=True): collection.set_clip_path(self.patch) if autolim: - collection._set_in_autoscale(True) + if autolim != "_datalim_only": + collection._set_in_autoscale(True) # Make sure viewLim is not stale (mostly to match # pre-lazy-autoscale behavior, which is not really better). self._unstale_viewLim() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 001536062251..0f58da4200cc 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6568,6 +6568,22 @@ def test_relim_collection_log_scale(): assert_allclose(ax.dataLim.minpos, expected.minpos) +def test_relim_collection_autoscale_view(): + # GH#30859 - end-to-end: after set_offsets(), relim() + autoscale_view() + # must update the visible axis limits, not just dataLim. + fig, ax = plt.subplots() + sc = ax.scatter([], []) + xs = np.linspace(0, 10, 50) + sc.set_offsets(np.column_stack((xs, np.sin(xs)))) + ax.relim() + ax.autoscale_view() + xlim = ax.get_xlim() + ylim = ax.get_ylim() + # autoscale_view adds a margin, so limits should comfortably contain data + assert xlim[0] <= 0 and xlim[1] >= 10, f"xlim should contain [0, 10], got {xlim}" + assert ylim[0] <= -1 and ylim[1] >= 1, f"ylim should contain [-1, 1], got {ylim}" + + def test_text_labelsize(): """ tests for issue #1172 From 016175145d19f19bee0a9423e51c4a493f083910 Mon Sep 17 00:00:00 2001 From: Tine Zivic Date: Tue, 21 Apr 2026 18:23:23 +0200 Subject: [PATCH 9/9] DOC: Fix Sphinx warning and address reviewer comments - Fix backticks on autolim=True in add_collection docstring (single backtick was interpreted as cross-reference by Sphinx) - 'explicit calls' -> 'explicit call' per reviewer suggestion - Remove out-of-place TODO comments from artist.py remove() --- lib/matplotlib/artist.py | 5 ----- lib/matplotlib/axes/_base.py | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index d6db3bb73461..88e38634b5b1 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -268,11 +268,6 @@ def remove(self): else: raise NotImplementedError('cannot remove artist') - # TODO: move the limits calculation into the artist itself, including - # the property of whether or not the artist should affect the limits. - # Then there will be no distinction between axes.add_line, - # axes.add_patch, etc. - # TODO: add legend support def have_units(self): """Return whether units are set on any axis.""" diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index efbf2a674ac4..ee933ea138ad 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2391,7 +2391,7 @@ def add_collection(self, collection, autolim=True): .. versionchanged:: 3.11 - Since 3.11 `autolim=True` matches the standard behavior + Since 3.11 ``autolim=True`` matches the standard behavior of other ``add_[artist]`` methods: Axes data and view limits are both updated in the method, and the collection will be considered in future data limit updates through @@ -2399,7 +2399,7 @@ def add_collection(self, collection, autolim=True): Prior to matplotlib 3.11 this was only a one-time update of the data limits. Updating view limits required an - explicit calls to `~.Axes.autoscale_view`, and collections + explicit call to `~.Axes.autoscale_view`, and collections did not take part in `.relim`. As an implementation detail, the value "_datalim_only" is