Repository: matplotlib/matplotlib

Issue:
[Bug]: axvspan no longer participating in limit calculations
### Bug summary

Since upgrading to matplotlib 3.9, axvspan plots no longer seem to be included in limit calculations.  I suspect this is due to the change from Polygon to Rectangle, but it does seem to have some unintended consequences.

### Code for reproduction

```Python
In [2]: fig, ax = plt.subplots()

In [3]: ax.axvspan(0.5, 1.0)
Out[3]: <matplotlib.patches.Rectangle object at 0x7080118bbf80>

In [4]: ax.set(title='Matplotlib 3.9.0')
Out[4]: [Text(0.5, 1.0, 'Matplotlib 3.9.0')]
```


### Actual outcome

![image](https://github.com/matplotlib/matplotlib/assets/1190540/417455cd-c184-4c3a-a446-24994c989284)

Note that the axis limits are still centered at 0, and the vspan is out of frame.

### Expected outcome

![image](https://github.com/matplotlib/matplotlib/assets/1190540/498e2ff9-43bd-45b0-9757-7da51357fbb2)

In 3.8.4, the limits would adapt to the location of the generated artist.

### Additional information

This is causing us some problems in mir_eval, where we have high-level plot constructors that are built entirely from axvspans for showing time-series segmentation labels.

### Operating system

all

### Matplotlib Version

3.9.0

### Matplotlib Backend

tkagg, but shouldn't matter

### Python version

3.12

### Jupyter version

n/a

### Installation

pip


Discussion:
This may be duplicate of #28341, which has a patch, but has not been put into a PR yet, I believe.

That said, the discussion there was about `axhspan` and indicated that `axvspan` was unaffected, so may not be exactly the same...
> That said, the discussion there was about `axhspan` and indicated that `axvspan` was unaffected, so may not be exactly the same...

I took a look at the proposed patch in #28341, and I don't think it addresses the issue.

In axhspan, the proposed fix was to .copy() the horizontal interval when updating the datalim:
https://github.com/matplotlib/matplotlib/blob/54729dbcb59c6f46c44241fc3193a2e67d5983e8/lib/matplotlib/axes/_axes.py#L1028-L1031
and the post in thread indicates that this is unnecessary for vspan, which already has the .copy():
https://github.com/matplotlib/matplotlib/blob/54729dbcb59c6f46c44241fc3193a2e67d5983e8/lib/matplotlib/axes/_axes.py#L1091-L1094
However, that's only applied to the y-axis, and it's the x-axis that is causing the issue here.

So I think these are two related, but distinct problems.
So the problem here is that for patches, we ask for whether it contains the data transform:
https://github.com/matplotlib/matplotlib/blob/d347c3227f8de8a99aa327390fee619310452a96/lib/matplotlib/axes/_base.py#L2445-L2448
and for `Rectangle` this returns `False` for both x&y. This is likely because a `Rectangle` is a unit rectangle, with a scaling transform + the data transform.

Since `Patch.get_transform()` == patch transform + data transform, it's possible that we may want to check data transform, and then use patch transform directly for the datalim update. Something like:
```patch
diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py
index 1cf56c90cc..5f37710ee6 100644
--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -2415,17 +2415,17 @@ class _AxesBase(martist.Artist):
         if len(vertices):
             vertices = np.vstack(vertices)
 
-        patch_trf = patch.get_transform()
-        updatex, updatey = patch_trf.contains_branch_seperately(self.transData)
+        data_trf = patch.get_data_transform()
+        updatex, updatey = data_trf.contains_branch_seperately(self.transData)
         if not (updatex or updatey):
             return
         if self.name != "rectilinear":
             # As in _update_line_limits, but for axvspan.
-            if updatex and patch_trf == self.get_yaxis_transform():
+            if updatex and data_trf == self.get_yaxis_transform():
                 updatex = False
-            if updatey and patch_trf == self.get_xaxis_transform():
+            if updatey and data_trf == self.get_xaxis_transform():
                 updatey = False
-        trf_to_data = patch_trf - self.transData
+        trf_to_data = patch.get_patch_transform()
         xys = trf_to_data.transform(vertices)
         self.update_datalim(xys, updatex=updatex, updatey=updatey)
 ```



Patch:
diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py
index 5003e2113930..3575bd1fc14d 100644
--- a/lib/matplotlib/transforms.py
+++ b/lib/matplotlib/transforms.py
@@ -1423,7 +1423,7 @@ def contains_branch_seperately(self, other_transform):
                              'transforms with 2 output dimensions')
         # for a non-blended transform each separate dimension is the same, so
         # just return the appropriate shape.
-        return [self.contains_branch(other_transform)] * 2
+        return (self.contains_branch(other_transform), ) * 2
 
     def __sub__(self, other):
         """
@@ -2404,6 +2404,15 @@ def _iter_break_from_left_to_right(self):
         for left, right in self._b._iter_break_from_left_to_right():
             yield self._a + left, right
 
+    def contains_branch_seperately(self, other_transform):
+        # docstring inherited
+        if self.output_dims != 2:
+            raise ValueError('contains_branch_seperately only supports '
+                             'transforms with 2 output dimensions')
+        if self == other_transform:
+            return (True, True)
+        return self._b.contains_branch_seperately(other_transform)
+
     depth = property(lambda self: self._a.depth + self._b.depth)
     is_affine = property(lambda self: self._a.is_affine and self._b.is_affine)
     is_separable = property(


Test patch:
diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py
index 959814de82db..3d12b90d5210 100644
--- a/lib/matplotlib/tests/test_transforms.py
+++ b/lib/matplotlib/tests/test_transforms.py
@@ -667,6 +667,13 @@ def test_contains_branch(self):
 
         assert not self.stack1.contains_branch(self.tn1 + self.ta2)
 
+        blend = mtransforms.BlendedGenericTransform(self.tn2, self.stack2)
+        x, y = blend.contains_branch_seperately(self.stack2_subset)
+        stack_blend = self.tn3 + blend
+        sx, sy = stack_blend.contains_branch_seperately(self.stack2_subset)
+        assert x is sx is False
+        assert y is sy is True
+
     def test_affine_simplification(self):
         # tests that a transform stack only calls as much is absolutely
         # necessary "non-affine" allowing the best possible optimization with