Repository: matplotlib/matplotlib
Issue category: Bug Report

Issue:
[Bug]: Figure.align_labels() confused by GridSpecFromSubplotSpec
### Bug summary

In a composite figure with nested gridspecs, `Figure.align_labels()` (and `align_xlabels()`, `align_ylabels()`) can end up aligning labels that should not intuitively be. Likewise with `align_titles()`.

### Code for reproduction

```Python
fig = plt.figure(figsize=(6, 4))

gs0 = gridspec.GridSpec(nrows=1, ncols=2, figure=fig)
gs00 = gs0[0].subgridspec(nrows=2, ncols=1, height_ratios=[8, 8])
gs01 = gs0[1].subgridspec(nrows=2, ncols=1, height_ratios=[9, 6])

left_axs = gs00.subplots()
right_axs = gs01.subplots()

left_axs[0].set_ylim(0, 0.02)  # to force nontrivial alignment

left_axs[0].set_ylabel('foo')
left_axs[1].set_ylabel('bar')
right_axs[0].set_ylabel('baz')
right_axs[1].set_ylabel('qux')

left_axs[1].set_title('title')
right_axs[1].set_title('title')

fig.align_labels()
fig.align_titles()
```


### Actual outcome

All labels are aligned. Titles are aligned as well.

![image](https://github.com/user-attachments/assets/b78defcf-e742-4643-b8b9-1f66ce375997)

### Expected outcome

Labels in separate columns are aligned, but labels in different columns should not be. Titles are not aligned:

![image](https://github.com/user-attachments/assets/795a7730-b2b3-442b-896f-cdde31b7ff97)


### Additional information

Right now, the ylabel (xlabel) alignment code seems to attempt to align labels on Axes with the same column index (resp. row index) without checking if those indexes are for the same gridspec. To fix this, we should probably add a check that two Axes share the same gridspec (in addition to being in the same row/col) before we align their labels. (This would not allow label alignment across gridspecs, but if a user wants to align labels between two Axes, it seems reasonable to expect them to put the Axes in the same gridspec.)

The same thing happens with align_titles().

For now, a workaround for labels is to call `Figure.align_labels()` separately for each sub-gridspec with the `axs` kwarg (as done for the expected outcome figure above).

### Operating system

macOS 14.1.1

### Matplotlib Version

3.9.2

### Matplotlib Backend

module://matplotlib_inline.backend_inline

### Python version

3.12.2

### Jupyter version

_No response_

### Installation

conda


Hints:
This is definitely an issue, but not sure we would prioritize or accept a complicated fix for this.  Note the docs say 

> Align the xlabels of subplots in the same subplot row if label alignment is being done automatically (i.e. the label position is not manually set).

This issue with subgridspecs not having a clear hierarchy is why we introduced [subfigures](https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subfigures.html) in v3.4.  Your code would look like:

```
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure(figsize=(6, 4), layout='constrained')

sfigs = fig.subfigures(1, 2)

left_axs = sfigs[0].subplots(2, 1, height_ratios=[8, 8])
right_axs = sfigs[1].subplots(2, 1, height_ratios=[9, 6])

left_axs[0].set_ylim(0, 0.02)  # to force nontrivial alignment

left_axs[0].set_ylabel('foo')
left_axs[1].set_ylabel('bar')
right_axs[0].set_ylabel('baz')
right_axs[1].set_ylabel('qux')

left_axs[1].set_title('title')
right_axs[1].set_title('title')

for sfig in sfigs:
    sfig.align_labels()
    sfig.align_titles()
plt.show()
```
![subplots](https://github.com/user-attachments/assets/d560a556-1699-4082-90ee-2c2f8814e749)

I suppose one change we could entertain is `align_labels` and friends accepting a list of subplots to align.  
This works with `subplot_mosaic`, which would be my recommended approach
```python
fig, axd = plt.subplot_mosaic("""
    AC
    AC
    BC
    BD
    BD
    """, layout="constrained")

axd["A"].set_ylim(0, 0.02)  # to force nontrivial alignment

axd["A"].set_ylabel('foo')
axd["B"].set_ylabel('bar')
axd["C"].set_ylabel('baz')
axd["D"].set_ylabel('qux')

axd["B"].set_title('title')
axd["D"].set_title('title')

fig.align_labels()
fig.align_titles()
```

![grafik](https://github.com/user-attachments/assets/16fa2680-9b88-436e-9bed-aa64ead1b7f6)

I suggest we simply declare that `align_labels` and `align_titles` do not work with subgridspece.
For sure you could use subplot_mosaic for a similar layout as well, though note that it is very hard to use it to get the height ratios exactly as requested.  Depends on what your constraints actually are.  

Being slightly more specific in the docstring would be fine.  
Actually,

https://matplotlib.org/devdocs/api/_as_gen/matplotlib.figure.Figure.align_titles.html

https://matplotlib.org/devdocs/api/_as_gen/matplotlib.figure.Figure.align_xlabels.html

https://matplotlib.org/devdocs/api/_as_gen/matplotlib.figure.Figure.align_ylabels.html

all have a note that they assume all Axes are from the same GridSpec.
That note is missing in https://matplotlib.org/devdocs/api/_as_gen/matplotlib.figure.Figure.align_labels.html and should be copied there.

Patch:
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index e5cf88131178..3d6f9a7f4c16 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -1382,8 +1382,8 @@ def align_xlabels(self, axs=None):
 
         Notes
         -----
-        This assumes that ``axs`` are from the same `.GridSpec`, so that
-        their `.SubplotSpec` positions correspond to figure positions.
+        This assumes that all Axes in ``axs`` are from the same `.GridSpec`,
+        so that their `.SubplotSpec` positions correspond to figure positions.
 
         Examples
         --------
@@ -1444,8 +1444,8 @@ def align_ylabels(self, axs=None):
 
         Notes
         -----
-        This assumes that ``axs`` are from the same `.GridSpec`, so that
-        their `.SubplotSpec` positions correspond to figure positions.
+        This assumes that all Axes in ``axs`` are from the same `.GridSpec`,
+        so that their `.SubplotSpec` positions correspond to figure positions.
 
         Examples
         --------
@@ -1500,8 +1500,8 @@ def align_titles(self, axs=None):
 
         Notes
         -----
-        This assumes that ``axs`` are from the same `.GridSpec`, so that
-        their `.SubplotSpec` positions correspond to figure positions.
+        This assumes that all Axes in ``axs`` are from the same `.GridSpec`,
+        so that their `.SubplotSpec` positions correspond to figure positions.
 
         Examples
         --------
@@ -1544,6 +1544,11 @@ def align_labels(self, axs=None):
         matplotlib.figure.Figure.align_xlabels
         matplotlib.figure.Figure.align_ylabels
         matplotlib.figure.Figure.align_titles
+
+        Notes
+        -----
+        This assumes that all Axes in ``axs`` are from the same `.GridSpec`,
+        so that their `.SubplotSpec` positions correspond to figure positions.
         """
         self.align_xlabels(axs=axs)
         self.align_ylabels(axs=axs)


Test patch: