From 99fee3280f80115670f70c64a99aa58969fc6553 Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Wed, 11 Mar 2026 10:56:09 +0530 Subject: [PATCH 1/7] Use Artist autoscale participation flag for conditional autoscaling of Collections --- lib/matplotlib/axes/_base.py | 9 +++++++++ lib/matplotlib/tests/test_axes.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index f4d005d4e324..b98f8d607755 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -29,6 +29,7 @@ import matplotlib.text as mtext import matplotlib.ticker as mticker import matplotlib.transforms as mtransforms +import matplotlib.collections as mcollections _log = logging.getLogger(__name__) @@ -1133,6 +1134,11 @@ def _update_transScale(self): mtransforms.blended_transform_factory( self.xaxis.get_transform(), self.yaxis.get_transform())) + def _update_collection_limits(self, collection): + offsets = collection.get_offsets() + if offsets is not None and len(offsets): + self.update_datalim(offsets) + def get_position(self, original=False): """ Return the position of the Axes within the figure as a `.Bbox`. @@ -2412,6 +2418,7 @@ def add_collection(self, collection, autolim=True): self._request_autoscale_view() self.stale = True + collection._set_in_autoscale(autolim) return collection def add_image(self, image): @@ -2626,6 +2633,8 @@ def relim(self, visible_only=False): self._update_patch_limits(artist) elif isinstance(artist, mimage.AxesImage): self._update_image_limits(artist) + elif isinstance(artist, mcollections.Collection): + self._update_collection_limits(artist) 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 74d48a89d0c0..e8ea4ed80dad 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10174,3 +10174,27 @@ def test_errorbar_uses_rcparams(): assert_allclose([cap.get_markeredgewidth() for cap in caplines], 2.5) for barcol in barlinecols: assert_allclose(barcol.get_linewidths(), 1.75) + + +def test_relim_updates_scatter_offsets(): + import numpy as np + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + + xs = np.linspace(0, 10, 100) + ys = np.sin(xs) + scatter = ax.scatter(xs, ys) + + # Shift scatter upward + new_ys = np.sin(xs) + 5 + scatter.set_offsets(np.column_stack((xs, new_ys))) + + ax.relim() + ax.autoscale_view() + + ymin, ymax = ax.get_ylim() + + # New limits should reflect shifted data + assert ymin > 3 + assert ymax > 5 From be151e1408e7e902c8404105b6dbc7773290d3ea Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Wed, 11 Mar 2026 22:52:05 +0530 Subject: [PATCH 2/7] fixing requested reviews --- lib/matplotlib/axes/_base.py | 9 ++++----- lib/matplotlib/tests/test_axes.py | 3 --- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 4e556c68bfec..4fed02e57207 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -29,7 +29,6 @@ import matplotlib.text as mtext import matplotlib.ticker as mticker import matplotlib.transforms as mtransforms -import matplotlib.collections as mcollections _log = logging.getLogger(__name__) @@ -1135,9 +1134,9 @@ def _update_transScale(self): self.xaxis.get_transform(), self.yaxis.get_transform())) def _update_collection_limits(self, collection): - offsets = collection.get_offsets() - if offsets is not None and len(offsets): - self.update_datalim(offsets) + offsets = collection.get_offsets() + if offsets is not None and len(offsets): + self.update_datalim(offsets) def get_position(self, original=False): """ @@ -2656,7 +2655,7 @@ def relim(self, visible_only=False): self._update_patch_limits(artist) elif isinstance(artist, mimage.AxesImage): self._update_image_limits(artist) - elif isinstance(artist, mcollections.Collection): + elif isinstance(artist, mcoll.Collection): self._update_collection_limits(artist) 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 5f1f780460ee..624396ade49f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10192,9 +10192,6 @@ def test_errorbar_uses_rcparams(): def test_relim_updates_scatter_offsets(): - import numpy as np - import matplotlib.pyplot as plt - fig, ax = plt.subplots() xs = np.linspace(0, 10, 100) From 3e6c3c0fe1e37fe5a1a80f7958a2f05d038f4ba1 Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Fri, 13 Mar 2026 16:16:04 +0530 Subject: [PATCH 3/7] Refactor add_collection autoscale handling into _update_collection_limits --- lib/matplotlib/axes/_base.py | 59 +++++++++++++++++------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 4fed02e57207..8957dede42fc 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1133,10 +1133,30 @@ def _update_transScale(self): mtransforms.blended_transform_factory( self.xaxis.get_transform(), self.yaxis.get_transform())) - def _update_collection_limits(self, collection): - offsets = collection.get_offsets() - if offsets is not None and len(offsets): - self.update_datalim(offsets) + def _update_collection_limits(self, collection, autolim): + self._unstale_viewLim() + + datalim = collection.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 = ( + 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, + ) + + if autolim != "_datalim_only": + self._request_autoscale_view() def get_position(self, original=False): """ @@ -2398,35 +2418,12 @@ def add_collection(self, collection, autolim=True): if collection.get_clip_path() is None: collection.set_clip_path(self.patch) + collection._set_in_autoscale(autolim) + if autolim: - # 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, - ) - if autolim != "_datalim_only": - self._request_autoscale_view() + self._update_collection_limits(collection, autolim) self.stale = True - collection._set_in_autoscale(autolim) return collection def add_image(self, image): @@ -2656,7 +2653,7 @@ def relim(self, visible_only=False): elif isinstance(artist, mimage.AxesImage): self._update_image_limits(artist) elif isinstance(artist, mcoll.Collection): - self._update_collection_limits(artist) + self._update_collection_limits(artist, autolim="_datalim_only") def update_datalim(self, xys, updatex=True, updatey=True): """ From 9b9a23eb45f0ce6a1146e37809ff9601fbd6669d Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Sun, 15 Mar 2026 00:51:28 +0530 Subject: [PATCH 4/7] Added docstring and refine _update_collection_limits --- lib/matplotlib/axes/_base.py | 63 ++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 8957dede42fc..90f6691f0483 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1133,31 +1133,6 @@ def _update_transScale(self): mtransforms.blended_transform_factory( self.xaxis.get_transform(), self.yaxis.get_transform())) - def _update_collection_limits(self, collection, autolim): - self._unstale_viewLim() - - datalim = collection.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 = ( - 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, - ) - - if autolim != "_datalim_only": - self._request_autoscale_view() - def get_position(self, original=False): """ Return the position of the Axes within the figure as a `.Bbox`. @@ -2426,6 +2401,44 @@ def add_collection(self, collection, autolim=True): self.stale = True return collection + def _update_collection_limits(self, collection, autolim): + """ + Update Axes data limits using the data from a Collection. + + This helper extracts the limit update logic previously embedded + in `Axes.add_collection`. It ensures that collections participating + in autoscaling correctly contribute their data limits to the Axes. + + Parameters + ---------- + collection : matplotlib.collections.Collection + The collection whose data limits should be incorporated into + the Axes data limits. + """ + self._unstale_viewLim() + + datalim = collection.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 = ( + 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, + ) + + if autolim != "_datalim_only": + self._request_autoscale_view() + def add_image(self, image): """ Add an `.AxesImage` to the Axes; return the image. From 50f42471edd8d56971d710568bc27e9a82faadbe Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Mon, 23 Mar 2026 19:56:01 +0530 Subject: [PATCH 5/7] Align autoscale handling with existing design by removing autolim from helper --- lib/matplotlib/axes/_base.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 90f6691f0483..ca8d7f7245ca 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2393,15 +2393,16 @@ def add_collection(self, collection, autolim=True): if collection.get_clip_path() is None: collection.set_clip_path(self.patch) - collection._set_in_autoscale(autolim) - if autolim: - self._update_collection_limits(collection, autolim) + self._update_collection_limits(collection) + + if autolim != "_datalim_only": + self._request_autoscale_view() self.stale = True return collection - def _update_collection_limits(self, collection, autolim): + def _update_collection_limits(self, collection): """ Update Axes data limits using the data from a Collection. @@ -2436,9 +2437,6 @@ def _update_collection_limits(self, collection, autolim): updatey=y_is_data or oy_is_data, ) - if autolim != "_datalim_only": - self._request_autoscale_view() - def add_image(self, image): """ Add an `.AxesImage` to the Axes; return the image. From 424c7806188ebd5ca3e797084a956cdfebd351c4 Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Wed, 25 Mar 2026 19:05:55 +0530 Subject: [PATCH 6/7] Fix autoscale logic: ensure _request_autoscale_view only runs when autolim is enabled --- lib/matplotlib/axes/_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ca8d7f7245ca..ceb5d53d671f 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2396,8 +2396,8 @@ def add_collection(self, collection, autolim=True): if autolim: self._update_collection_limits(collection) - if autolim != "_datalim_only": - self._request_autoscale_view() + if autolim != "_datalim_only": + self._request_autoscale_view() self.stale = True return collection From eb2ebe80af3b507e633d2515d1486ccb8ed7037c Mon Sep 17 00:00:00 2001 From: Archil Jain Date: Wed, 25 Mar 2026 23:56:55 +0530 Subject: [PATCH 7/7] Fix collection autoscale integration in relim and align with autoscale flag design --- lib/matplotlib/axes/_base.py | 39 +++++++++++------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ceb5d53d671f..31aadff5bd9f 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2403,19 +2403,6 @@ def add_collection(self, collection, autolim=True): return collection def _update_collection_limits(self, collection): - """ - Update Axes data limits using the data from a Collection. - - This helper extracts the limit update logic previously embedded - in `Axes.add_collection`. It ensures that collections participating - in autoscaling correctly contribute their data limits to the Axes. - - Parameters - ---------- - collection : matplotlib.collections.Collection - The collection whose data limits should be incorporated into - the Axes data limits. - """ self._unstale_viewLim() datalim = collection.get_datalim(self.transData) @@ -2424,18 +2411,16 @@ def _update_collection_limits(self, collection): if not np.isinf(datalim.minpos).all(): points = np.concatenate([points, [datalim.minpos]]) - 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) - ) + transform = collection.get_transform() + offset_trf = collection.get_offset_transform() + + x_is_data, y_is_data = transform.contains_branch_separately(self.transData) + ox_is_data, oy_is_data = offset_trf.contains_branch_separately(self.transData) + + updatex = x_is_data or ox_is_data + updatey = y_is_data or oy_is_data - self.update_datalim( - points, - updatex=x_is_data or ox_is_data, - updatey=y_is_data or oy_is_data, - ) + self.update_datalim(points, updatex=updatex, updatey=updatey) def add_image(self, image): """ @@ -2655,8 +2640,8 @@ def relim(self, visible_only=False): for artist in self._children: if not visible_only or artist.get_visible(): - if not artist._get_in_autoscale(): - continue + if hasattr(artist, "get_in_autoscale") and not artist.get_in_autoscale(): # noqa: E501 + continue if isinstance(artist, mlines.Line2D): self._update_line_limits(artist) elif isinstance(artist, mpatches.Patch): @@ -2664,7 +2649,7 @@ def relim(self, visible_only=False): elif isinstance(artist, mimage.AxesImage): self._update_image_limits(artist) elif isinstance(artist, mcoll.Collection): - self._update_collection_limits(artist, autolim="_datalim_only") + self._update_collection_limits(artist) def update_datalim(self, xys, updatex=True, updatey=True): """