Repository: matplotlib/matplotlib

Issue:
[Bug]: Axes.label_outer() does not work when there is a colorbar
### Bug summary

`Axes.label_outer()` fails to hide axis and tick labels for plots that have a colorbar.

### Code for reproduction

```python
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 400)
y = np.sin(x ** 2)

fig, axs = plt.subplots(2, 2)
axs[0, 0].plot(x, y)
cl2c = axs[0, 1].scatter(x, y, c=x)
fig.colorbar(cl2c, ax=axs[0, 1], label='c-label')
axs[1, 0].plot(x, y)
axs[1, 1].plot(x, y)

for ax in axs.flat:
    ax.set(xlabel='x-label', ylabel='y-label')

# try to hide x labels and tick labels for top plots and y ticks for right plots,
# but this fails to hide the labels for axs[0, 1] due to its colorbar
for ax in axs.flat:
    ax.label_outer()
```


### Actual outcome

The output of the code above gives:

![image](https://github.com/matplotlib/matplotlib/assets/7686641/e9a79307-42c5-492f-884a-ede626bc08dc)

### Expected outcome

I would have expected the plot to look like:

![image](https://github.com/matplotlib/matplotlib/assets/7686641/aac412ba-3578-4c7f-aa8b-0dba39d57823)

This expected plot was created by creating the colorbar after the `ax.label_outer()` calls. However, I would not have expected this to have affected whether the labels/ticks are removed.

```python
fig, axs = plt.subplots(2, 2)
axs[0, 0].plot(x, y)
cl2c = axs[0, 1].scatter(x, y, c=x)
axs[1, 0].plot(x, y)
axs[1, 1].plot(x, y)

for ax in axs.flat:
    ax.set(xlabel='x-label', ylabel='y-label')

# Hide x labels and tick labels for top plots and y ticks for right plots.
for ax in axs.flat:
    ax.label_outer()

fig.colorbar(cl2c, ax=axs[0, 1], label='c-label')

fig.tight_layout()
```

### Additional information

_No response_

### Operating system

Ubuntu 20.04.5 LTS

### Matplotlib Version

3.8.0

### Matplotlib Backend

module://matplotlib_inline.backend_inline

### Python version

Python 3.11.6

### Jupyter version

7.0.6

### Installation

conda


Discussion:
Can you try w layout='constrained'.  I think with normal colorbars the parent axes gets shoved down a level in the subplot spec and hence becomes "outer" to the axes/colorbar pair.  This doesn't happen for layout='constrained' or in the colorbar you can also do use_gridspec=False and I think that will fix the issue 
I checked, and my diagnosis above is correct.  Given that there are at least three work arounds for this, I don't know what the appetite is to check if the axes is embedded in a subplotsepc with a colorbar, and that the parent subplotspec is "inner".  I'd recommend using layout='constrained'


The following patch implements the necessary checking of gridspecs for non-constrainedlayout-positioned colorbars, AFAICT:
```patch
diff --git i/lib/matplotlib/axes/_base.py w/lib/matplotlib/axes/_base.py
index b113b0f211..ac991167e1 100644
--- i/lib/matplotlib/axes/_base.py
+++ w/lib/matplotlib/axes/_base.py
@@ -4560,13 +4560,41 @@ class _AxesBase(martist.Artist):
         self._label_outer_yaxis(skip_non_rectangular_axes=False,
                                 remove_inner_ticks=remove_inner_ticks)

+    def _get_subplotspec_with_optional_colorbar(self):
+        """
+        Return the subplotspec for this Axes, except that if this Axes has been
+        moved to a subgridspec to make room for a colorbar, then return the
+        subplotspec that encloses both this Axes and the colorbar Axes.
+        """
+        ss = self.get_subplotspec()
+        if not ss:
+            return
+        gs = ss.get_gridspec()
+        if (isinstance(gs, mpl.gridspec.GridSpecFromSubplotSpec)
+                and gs.nrows * gs.ncols == 2):
+            for ax in self.figure.axes:
+                if (ax is not self
+                        and hasattr(ax, "_colorbar_info")
+                        and ax.get_subplotspec()
+                        and isinstance(ax.get_subplotspec().get_gridspec(),
+                                       mpl.gridspec.GridSpecFromSubplotSpec)
+                        and isinstance(ax.get_subplotspec().get_gridspec()
+                                       ._subplot_spec.get_gridspec(),
+                                       mpl.gridspec.GridSpecFromSubplotSpec)
+                        and (ax.get_subplotspec().get_gridspec()
+                             ._subplot_spec.get_gridspec()._subplot_spec
+                             is gs._subplot_spec)):
+                    ss = gs._subplot_spec
+                    break
+        return ss
+
     def _label_outer_xaxis(self, *, skip_non_rectangular_axes,
                            remove_inner_ticks=False):
         # see documentation in label_outer.
         if skip_non_rectangular_axes and not isinstance(self.patch,
                                                         mpl.patches.Rectangle):
             return
-        ss = self.get_subplotspec()
+        ss = self._get_subplotspec_with_optional_colorbar()
         if not ss:
             return
         label_position = self.xaxis.get_label_position()
@@ -4593,7 +4621,7 @@ class _AxesBase(martist.Artist):
         if skip_non_rectangular_axes and not isinstance(self.patch,
                                                         mpl.patches.Rectangle):
             return
-        ss = self.get_subplotspec()
+        ss = self._get_subplotspec_with_optional_colorbar()
         if not ss:
             return
         label_position = self.yaxis.get_label_position()
```
Ha, cross-post.  I'm still not convinced the above is worth the bother, but wouldn't block if someone wanted to add it.
Will #27306 simplify the patch?
Yes, this patch is what motivated #27306 in fact (you'll only need to dig one gridspec in rather than two).
The simplified patch now that #27306 has been merged:
```patch
diff --git i/lib/matplotlib/axes/_base.py w/lib/matplotlib/axes/_base.py
index b113b0f211..07f767c253 100644
--- i/lib/matplotlib/axes/_base.py
+++ w/lib/matplotlib/axes/_base.py
@@ -4560,13 +4560,37 @@ class _AxesBase(martist.Artist):
         self._label_outer_yaxis(skip_non_rectangular_axes=False,
                                 remove_inner_ticks=remove_inner_ticks)
 
+    def _get_subplotspec_with_optional_colorbar(self):
+        """
+        Return the subplotspec for this Axes, except that if this Axes has been
+        moved to a subgridspec to make room for a colorbar, then return the
+        subplotspec that encloses both this Axes and the colorbar Axes.
+        """
+        ss = self.get_subplotspec()
+        if not ss:
+            return
+        gs = ss.get_gridspec()
+        if (isinstance(gs, mpl.gridspec.GridSpecFromSubplotSpec)
+                and gs.nrows * gs.ncols == 6):
+            for ax in self.figure.axes:
+                if (ax is not self
+                        and hasattr(ax, "_colorbar_info")
+                        and ax.get_subplotspec()
+                        and isinstance(ax.get_subplotspec().get_gridspec(),
+                                       mpl.gridspec.GridSpecFromSubplotSpec)
+                        and (ax.get_subplotspec().get_gridspec()._subplot_spec
+                             is gs._subplot_spec)):
+                    ss = gs._subplot_spec
+                    break
+        return ss
+
     def _label_outer_xaxis(self, *, skip_non_rectangular_axes,
                            remove_inner_ticks=False):
         # see documentation in label_outer.
         if skip_non_rectangular_axes and not isinstance(self.patch,
                                                         mpl.patches.Rectangle):
             return
-        ss = self.get_subplotspec()
+        ss = self._get_subplotspec_with_optional_colorbar()
         if not ss:
             return
         label_position = self.xaxis.get_label_position()
@@ -4593,7 +4617,7 @@ class _AxesBase(martist.Artist):
         if skip_non_rectangular_axes and not isinstance(self.patch,
                                                         mpl.patches.Rectangle):
             return
-        ss = self.get_subplotspec()
+        ss = self._get_subplotspec_with_optional_colorbar()
         if not ss:
             return
         label_position = self.yaxis.get_label_position()
```



Patch:
diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py
index 1ce205ede613..87d42b4d3014 100644
--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -4749,14 +4749,25 @@ def label_outer(self, remove_inner_ticks=False):
         self._label_outer_yaxis(skip_non_rectangular_axes=False,
                                 remove_inner_ticks=remove_inner_ticks)
 
+    def _get_subplotspec_with_optional_colorbar(self):
+        """
+        Return the subplotspec for this Axes, except that if this Axes has been
+        moved to a subgridspec to make room for a colorbar, then return the
+        subplotspec that encloses both this Axes and the colorbar Axes.
+        """
+        ss = self.get_subplotspec()
+        if any(cax.get_subplotspec() for cax in self._colorbars):
+            ss = ss.get_gridspec()._subplot_spec
+        return ss
+
     def _label_outer_xaxis(self, *, skip_non_rectangular_axes,
                            remove_inner_ticks=False):
         # see documentation in label_outer.
         if skip_non_rectangular_axes and not isinstance(self.patch,
                                                         mpl.patches.Rectangle):
             return
-        ss = self.get_subplotspec()
-        if not ss:
+        ss = self._get_subplotspec_with_optional_colorbar()
+        if ss is None:
             return
         label_position = self.xaxis.get_label_position()
         if not ss.is_first_row():  # Remove top label/ticklabels/offsettext.
@@ -4782,8 +4793,8 @@ def _label_outer_yaxis(self, *, skip_non_rectangular_axes,
         if skip_non_rectangular_axes and not isinstance(self.patch,
                                                         mpl.patches.Rectangle):
             return
-        ss = self.get_subplotspec()
-        if not ss:
+        ss = self._get_subplotspec_with_optional_colorbar()
+        if ss is None:
             return
         label_position = self.yaxis.get_label_position()
         if not ss.is_first_col():  # Remove left label/ticklabels/offsettext.
diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py
index 19bdbe605d88..4348f02cfc34 100644
--- a/lib/matplotlib/colorbar.py
+++ b/lib/matplotlib/colorbar.py
@@ -1455,8 +1455,7 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15,
 
     cax = fig.add_axes(pbcb, label="<colorbar>")
     for a in parents:
-        # tell the parent it has a colorbar
-        a._colorbars += [cax]
+        a._colorbars.append(cax)  # tell the parent it has a colorbar
     cax._colorbar_info = dict(
         parents=parents,
         location=location,
@@ -1549,6 +1548,7 @@ def make_axes_gridspec(parent, *, location=None, orientation=None,
 
     fig = parent.get_figure()
     cax = fig.add_subplot(ss_cb, label="<colorbar>")
+    parent._colorbars.append(cax)  # tell the parent it has a colorbar
     cax.set_anchor(anchor)
     cax.set_box_aspect(aspect)
     cax.set_aspect('auto')


Test patch:
diff --git a/lib/matplotlib/tests/test_subplots.py b/lib/matplotlib/tests/test_subplots.py
index a899110ac77a..0f00a88aa72d 100644
--- a/lib/matplotlib/tests/test_subplots.py
+++ b/lib/matplotlib/tests/test_subplots.py
@@ -4,6 +4,7 @@
 import numpy as np
 import pytest
 
+import matplotlib as mpl
 from matplotlib.axes import Axes, SubplotBase
 import matplotlib.pyplot as plt
 from matplotlib.testing.decorators import check_figures_equal, image_comparison
@@ -111,10 +112,15 @@ def test_shared():
 
 
 @pytest.mark.parametrize('remove_ticks', [True, False])
-def test_label_outer(remove_ticks):
-    f, axs = plt.subplots(2, 2, sharex=True, sharey=True)
+@pytest.mark.parametrize('layout_engine', ['none', 'tight', 'constrained'])
+@pytest.mark.parametrize('with_colorbar', [True, False])
+def test_label_outer(remove_ticks, layout_engine, with_colorbar):
+    fig = plt.figure(layout=layout_engine)
+    axs = fig.subplots(2, 2, sharex=True, sharey=True)
     for ax in axs.flat:
         ax.set(xlabel="foo", ylabel="bar")
+        if with_colorbar:
+            fig.colorbar(mpl.cm.ScalarMappable(), ax=ax)
         ax.label_outer(remove_inner_ticks=remove_ticks)
     check_ticklabel_visible(
         axs.flat, [False, False, True, True], [True, False, True, False])