Repository: matplotlib/matplotlib

Issue:
# [Bug]: ax.relim() ignores scatter artist

### Bug summary

**ax.relim()** completely ignores changes to scatterplots with **.set_offsets()**.

### Code for reproduction

```Python
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import numpy as np

class App:
    def __init__(self, root):
        self.root = root
        fig = Figure(figsize=(5, 5), dpi=100)
        self.ax = fig.add_subplot(111)

        self.canvas = FigureCanvasTkAgg(fig, root)
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        self.toolbar = NavigationToolbar2Tk(self.canvas, root)

        # Create artists once
        self.scatter = self.ax.scatter([], [])
        self.line, = self.ax.plot([], [])

        root.after(1000, self.update_plot)

    def update_plot(self):
        # New data every update
        xs = np.linspace(0, 10, 100)
        ys = np.sin(xs) + np.random.normal(scale=0.1, size=100)

        # Update artists
        self.scatter.set_offsets(np.column_stack((xs, ys)))
        #self.line.set_data(xs, ys) #Uncomment this to see the expected behaviour

        # Autoscale (not working)
        self.ax.relim()
        self.ax.autoscale_view()

        self.canvas.draw_idle()

        # Keep updating
        self.root.after(1000, self.update_plot)

root = tk.Tk()
App(root)
root.mainloop()
```

### Actual outcome

<img width="502" height="574" alt="Image" src="https://github.com/user-attachments/assets/320471a2-82d4-4263-aaa8-913feaf154e7" />

### Expected outcome

<img width="502" height="574" alt="Image" src="https://github.com/user-attachments/assets/9e3c9f7f-0506-4725-94d7-31baa68088ae" />

### Additional information

_No response_

### Operating system

Windows

### Matplotlib Version

3.9.1

### Matplotlib Backend

tkAgg

### Python version

3.11.9

### Jupyter version

None

### Installation

pip

Implementation patch:
diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py
index f89c231815dc..9e515a7a8aa0 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__)
 
@@ -2415,6 +2416,12 @@ def _update_image_limits(self, image):
         xmin, xmax, ymin, ymax = image.get_extent()
         self.axes.update_datalim(((xmin, ymin), (xmax, ymax)))
 
+    def _update_collection_limits(self, collection):
+     offsets = collection.get_offsets()
+     if offsets is not None and len(offsets):
+        self.update_datalim(offsets)
+
+
     def add_line(self, line):
         """
         Add a `.Line2D` to the Axes; return the line.
@@ -2605,6 +2612,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):
         """


Test patch:
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index 46843841fe93..6e6614090311 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -10159,3 +10159,27 @@ def test_animated_artists_not_drawn_by_default():
 
     mocked_im_draw.assert_not_called()
     mocked_ln_draw.assert_not_called()
+
+
+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