Repository: mathause/mplotutils

Issue:
fix compatibility of mpu.colorbar with mpl >= 3.4
mpu.colorbar - in combination with `aspect` and `bbox_inches="tight"` no longer works properly (the resulting bbox is too large) for matplotlib >= 3.4.

```python
import cartopy
import cartopy.crs as ccrs
import matplotlib
import matplotlib.pyplot as plt

import mplotutils as mpu

print(f"{matplotlib.__version__=}")
print(f"{cartopy.__version__=}")

lon, lat, data = mpu.sample_data_map(30, 30)


f, axs = plt.subplots(1, 2, subplot_kw=dict(projection=ccrs.PlateCarree()))
f.subplots_adjust(left=0.01, right=0.99, bottom=0., top=1, wspace=0)

h = axs[0].pcolormesh(lon, lat, data, transform=ccrs.PlateCarree())

cbar = mpu.colorbar(h, axs[0], axs[1], orientation="horizontal", aspect=1)

for ax in axs:
    ax.coastlines()
    
plt.savefig("colorbar_issue.png", bbox_inches="tight")
```




Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/mathause/mplotutils/pull/26?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser) Report
Base: **65.50**% // Head: **66.66**% // Increases project coverage by **`+1.16%`** :tada:
> Coverage data is based on head [(`dfb8686`)](https://codecov.io/gh/mathause/mplotutils/pull/26?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser) compared to base [(`b9048b8`)](https://codecov.io/gh/mathause/mplotutils/commit/b9048b86b8758a31b4462c589b2921899ae45fc4?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser).
> Patch coverage: 93.10% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #26      +/-   ##
==========================================
+ Coverage   65.50%   66.66%   +1.16%     
==========================================
  Files           4        4              
  Lines         316      327      +11     
==========================================
+ Hits          207      218      +11     
  Misses        109      109              
```

| Flag | Coverage Δ | |
|---|---|---|
| min-all-deps | `66.66% <93.10%> (+1.16%)` | :arrow_up: |
| unittests | `66.66% <93.10%> (+1.16%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/mathause/mplotutils/pull/26?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser) | Coverage Δ | |
|---|---|---|
| [mplotutils/colorbar\_utils.py](https://codecov.io/gh/mathause/mplotutils/pull/26/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser#diff-bXBsb3R1dGlscy9jb2xvcmJhcl91dGlscy5weQ==) | `81.25% <93.10%> (+2.04%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/mathause/mplotutils/pull/26?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mathias+Hauser).


Patch:
diff --git a/README.md b/README.md
index b87dccb..18642fa 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,13 @@ helper functions for cartopy and matplotlib
 
 ### 0.3.0 (unreleased)
 
+#### Bug fixes
+
+ * Fixed compatibility of `mpu.colorbar` with `bbox_inches="tight"` for matplotlib 3.4 and
+   newer [#26](https://github.com/mathause/mplotutils/pull/26).
+
+#### Internal changes
+
  * Added ``nrow`` and ``ncol`` parameters to ``set_map_layout`` for use with a
    gridspec.
  * Replaced `ax.get_subplotspec().get_geometry()` with `ax.get_subplotspec().get_geometry()`
diff --git a/mplotutils/colorbar_utils.py b/mplotutils/colorbar_utils.py
index 55bc79e..88f2f9c 100644
--- a/mplotutils/colorbar_utils.py
+++ b/mplotutils/colorbar_utils.py
@@ -1,4 +1,5 @@
 import matplotlib.pyplot as plt
+import numpy as np
 
 
 def colorbar(
@@ -21,29 +22,26 @@ def colorbar(
     Parameters
     ----------
     mappable : handle
-        The return value of 'ax.contourf', 'ax.pcolormesh' etc to
-        which the colorbar applies.
-    ax1 : Axes
-        Axes to adjust the colorbar to.
-    ax2 : Axes, optional
-        If the colorbar should span more than one Axes. Default: None.
-    orientation : 'vertical' | 'horizontal', optional
-        Orientation of the colorbar. Default: 'vertical'.
-    aspect : float, optional
-        The ratio of long to short dimensions of the colorbar. Default 20.
-        Mutually exclusive with 'size'.
-    size : float
+        The `matplotlib.cm.ScalarMappable` described by this colorbar.
+    ax1 : `matplotlib.axes.Axes`
+        The axes to adjust the colorbar to.
+    ax2 : `~matplotlib.axes.Axes`, default: None.
+        If the colorbar should span more than one axes.
+    orientation : 'vertical' | 'horizontal'. Default: 'vertical'.
+        Orientation of the colorbar.
+    aspect : float, default 20.
+        The ratio of long to short dimensions of the colorbar Mutually exclusive with
+        `size`.
+    size : float, default: None
         Width of the colorbar as fraction of the axes width (vertical) or
-        height (horizontal). Mutually exclusive with 'aspect'. Default: None.
-    pad : float
+        height (horizontal). Mutually exclusive with `aspect`.
+    pad : float, default: None.
         Distance of the colorbar to the axes in Figure coordinates.
          Default: 0.05 (vertical) or 0.15 (horizontal).
-    shift : 'symmetric' or float in 0..1
-        Fraction of the total height that the colorbar is shifted upwards.
-        See Note. Default: 'symmetric'
-    shrink : None or float in 0..1
-        Fraction of the total height that the colorbar is shrunk.
-        See Note. Default: None.
+    shift : 'symmetric' or float in 0..1, default: 'symmetric'
+        Fraction of the total height that the colorbar is shifted upwards. See Note.
+    shrink : None or float in 0..1, default: None.
+        Fraction of the total height that the colorbar is shrunk. See Note.
     **kwargs : keyword arguments
         colorbar properties
         ============  ====================================================
@@ -99,36 +97,30 @@ def colorbar(
 
     # example with 1 axes
 
-    f = plt.figure()
-    ax = plt.axes(projection=ccrs.PlateCarree())
+    f, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()})
     h = ax.pcolormesh([[0, 1]])
-
     ax.coastlines()
-
     mpu.colorbar(h, ax)
-
     ax.set_global()
 
     # =========================
     # example with 2 axes
 
-    f, axes = plt.subplots(2, 1, subplot_kw=dict(projection=ccrs.Robinson()))
+    f, axs = plt.subplots(2, 1, subplot_kw={"projection": ccrs.PlateCarree()})
 
-    for ax in axes:
+    for ax in axs:
         ax.coastlines()
         ax.set_global()
         h = ax.pcolormesh([[0, 1]])
 
-    cbar = mpu.colorbar(h, axes[0], axes[1])
-
-    cbar.set_label('[°C]', labelpad=10)
+    cbar = mpu.colorbar(h, axs[0], axs[1])
 
     # =========================
     # example with 3 axes & 2 colorbars
 
-    f, axes = plt.subplots(3, 1, subplot_kw=dict(projection=ccrs.Robinson()))
+    f, axs = plt.subplots(3, 1, subplot_kw={"projection": ccrs.PlateCarree()})
 
-    for ax in axes:
+    for ax in axs:
         ax.coastlines()
         ax.set_global()
 
@@ -136,24 +128,21 @@ def colorbar(
     h1 = ax.pcolormesh([[0, 1]])
     h2 = ax.pcolormesh([[0, 1]], cmap='Blues')
 
-    cbar = mpu.colorbar(h, axes[0], axes[1], size=0.05)
-
-    cbar = mpu.colorbar(h, axes[2], size=0.05)
+    cbar = mpu.colorbar(h, axs[0], axs[1], size=0.05)
+    cbar = mpu.colorbar(h, axs[2], size=0.05)
 
     plt.draw()
 
     Notes
     -----
-    shift='symmetric', shrink=None  -> colorbar extends over the whole height
-    shift='symmetric', shrink=0.1   -> colorbar is 10 % smaller, and centered
-    shift=0., shrink=0.1            -> colorbar is 10 % smaller, and aligned
-                                       with the bottom
-    shift=0.1, shrink=None          -> colorbar is 10 % smaller, and aligned
-                                       with the top
+    - ``shift='symmetric', shrink=None`` -> colorbar extends over the whole height
+    - ``shift='symmetric', shrink=0.1`` -> colorbar is 10 % smaller, and centered
+    - ``shift=0., shrink=0.1`` -> colorbar is 10 % smaller, and aligned with the bottom
+    - ``shift=0.1, shrink=None`` -> colorbar is 10 % smaller, and aligned with the top
 
     See Also
     --------
-    _resize_colorbar_horz
+    plt.colorbar
     """
 
     orientations = ("vertical", "horizontal")
@@ -269,6 +258,8 @@ def _resize_colorbar_vert(
 
     size, aspect, pad = _parse_size_aspect_pad(size, aspect, pad, "vertical")
 
+    f = ax1.get_figure()
+
     # swap axes if ax1 is above ax2
     if ax2 is not None:
         posn1 = ax1.get_position()
@@ -278,29 +269,34 @@ def _resize_colorbar_vert(
 
     if aspect is not None:
         anchor = (0, 0.5)
-        cbax.set_aspect(aspect, anchor=anchor, adjustable="box")
+        cbax.set_anchor(anchor)
+        cbax.set_box_aspect(aspect)
 
     # inner function is called by event handler
     def inner(event=None):
 
-        posn1 = ax1.get_position()
+        pos1 = ax1.get_position()
 
         # determine total height of all axes
-        if ax2 is not None:
-            posn2 = ax2.get_position()
-            full_height = posn2.y0 - posn1.y0 + posn2.height
+        if ax2 is None:
+            full_height = pos1.height
         else:
-            full_height = posn1.height
+            pos2 = ax2.get_position()
+            full_height = pos2.y0 - pos1.y0 + pos2.height
 
-        pad_scaled = pad * posn1.width
-        size_scaled = size * posn1.width
+        pad_scaled = pad * pos1.width
 
-        # calculate position
-        left = posn1.x0 + posn1.width + pad_scaled
-        bottom = posn1.y0 + shift * full_height
-        height = full_height - shrink * full_height
-        # ignored if aspect is set
-        width = size_scaled
+        # calculate position of cbax
+        left = pos1.x0 + pos1.width + pad_scaled
+        bottom = pos1.y0 + shift * full_height
+        height = (1 - shrink) * full_height
+
+        if aspect is None:
+            size_scaled = size * pos1.width
+            width = size_scaled
+        else:
+            figure_aspect = np.divide(*f.get_size_inches())
+            width = height / (aspect * figure_aspect)
 
         pos = [left, bottom, width, height]
 
@@ -357,6 +353,8 @@ def _resize_colorbar_horz(
 
     size, aspect, pad = _parse_size_aspect_pad(size, aspect, pad, "horizontal")
 
+    f = ax1.get_figure()
+
     if ax2 is not None:
         posn1 = ax1.get_position()
         posn2 = ax2.get_position()
@@ -367,27 +365,32 @@ def _resize_colorbar_horz(
     if aspect is not None:
         aspect = 1 / aspect
         anchor = (0.5, 1.0)
-        cbax.set_aspect(aspect, anchor=anchor, adjustable="box")
+        cbax.set_anchor(anchor)
+        cbax.set_box_aspect(aspect)
 
     def inner(event=None):
 
         posn1 = ax1.get_position()
 
-        if ax2 is not None:
+        if ax2 is None:
+            full_width = posn1.width
+        else:
             posn2 = ax2.get_position()
             full_width = posn2.x0 - posn1.x0 + posn2.width
-        else:
-            full_width = posn1.width
 
         pad_scaled = pad * posn1.height
         size_scaled = size * posn1.height
 
-        left = posn1.x0 + shift * full_width
-        bottom = posn1.y0 - (pad_scaled + size_scaled)
         width = full_width - shrink * full_width
 
-        # ignored if aspect is set
-        height = size_scaled
+        if aspect is None:
+            height = size_scaled
+        else:
+            figure_aspect = np.divide(*f.get_size_inches())
+            height = (width * figure_aspect) / (aspect)
+
+        left = posn1.x0 + shift * full_width
+        bottom = posn1.y0 - (pad_scaled + height)
 
         pos = [left, bottom, width, height]
 


Test patch:
diff --git a/mplotutils/tests/test_colorbar_utils.py b/mplotutils/tests/test_colorbar_utils.py
index 8d6758e..599d724 100644
--- a/mplotutils/tests/test_colorbar_utils.py
+++ b/mplotutils/tests/test_colorbar_utils.py
@@ -104,7 +104,7 @@ def test_resize_colorbar_vert():
     res = [pos.x0, pos.y0, pos.width, pos.height]
     exp = [0.8, 0, 0.2 * 0.8, 1.0]
 
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
     # -----------------------------------------------------------
 
@@ -115,7 +115,7 @@ def test_resize_colorbar_vert():
     res = [pos.x0, pos.y0, pos.width, pos.height]
     exp = [0.8, 0.1, 0.2 * 0.8, 0.8]
 
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
     plt.close()
 
@@ -129,9 +129,9 @@ def test_resize_colorbar_vert():
     # don't test width if aspect is given, but also test aspect
     res = [pos.x0, pos.y0, pos.height]
     exp = [0.8, 0, 1.0]
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
-    assert cbar.ax.get_aspect() == 5
+    assert cbar.ax.get_box_aspect() == 5
 
     plt.close()
 
@@ -145,9 +145,9 @@ def test_resize_colorbar_vert():
     # don't test width if aspect is given, but also test aspect
     res = [pos.x0, pos.y0, pos.height]
     exp = [0.8, 0, 1.0]
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
-    assert cbar.ax.get_aspect() == 20
+    assert cbar.ax.get_box_aspect() == 20
 
     plt.close()
 
@@ -161,7 +161,7 @@ def test_resize_colorbar_vert():
     res = [pos.x0, pos.y0, pos.width, pos.height]
     exp = [0.8 + 0.8 * 0.05, 0, 0.8 * 0.1, 1.0]
 
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
     plt.close()
 
@@ -176,7 +176,7 @@ def test_resize_colorbar_vert():
     res = [pos.x0, pos.y0, pos.width, pos.height]
     exp = [0.8, 0.05, 0.2 * 0.8, 0.9]
 
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
     plt.close()
 
@@ -191,7 +191,7 @@ def test_resize_colorbar_vert():
     res = [pos.x0, pos.y0, pos.width, pos.height]
     exp = [0.8, 0.0, 0.2 * 0.8, 0.9]
 
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
     plt.close()
 
@@ -206,7 +206,7 @@ def test_resize_colorbar_vert():
     res = [pos.x0, pos.y0, pos.width, pos.height]
     exp = [0.8, 0.1, 0.2 * 0.8, 0.9]
 
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
     plt.close()
 
@@ -272,9 +272,9 @@ def test_resize_colorbar_horz():
     # don't test width if aspect is given, but also test aspect
     res = [pos.x0, pos.y0 + pos.height, pos.width]
     exp = [0.0, 0.2, 1.0]
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
-    assert cbar.ax.get_aspect() == 1.0 / 5
+    assert cbar.ax.get_box_aspect() == 1.0 / 5
 
     plt.close()
 
@@ -288,9 +288,9 @@ def test_resize_colorbar_horz():
     # don't test width if aspect is given, but also test aspect
     res = [pos.x0, pos.y0 + pos.height, pos.width]
     exp = [0.0, 0.2, 1.0]
-    np.testing.assert_allclose(res, exp)
+    np.testing.assert_allclose(res, exp, atol=1e-08)
 
-    assert cbar.ax.get_aspect() == 1.0 / 20
+    assert cbar.ax.get_box_aspect() == 1.0 / 20
 
     plt.close()