Skip to content

Commit 31272f2

Browse files
authored
enable set_map_layout for AxesGrid (#116)
* enable set_map_layout for AxesGrid * changelog * Apply suggestions from code review
1 parent 3ae5be7 commit 31272f2

9 files changed

+453
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
| ----------- | ---- | ------- |
1111
| matplotlib* | 3.6 | 3.7 |
1212

13+
### Enhancements
1314

15+
- Enable passing `AxesGrid` (from `mpl_toolkits.axes_grid1`) to `set_map_layout` ([#116](https://github.com/mathause/mplotutils/pull/116)).
1416

1517
## v0.5.0 (27.03.2024)
1618

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ Without mplotutils | With mplotutils
1111

1212
The code to create the examples can be found in [docs/example.py](docs/example.py).
1313

14+
15+
**axes_grid**
16+
17+
Matplotlib's [axes_grid](https://matplotlib.org/stable/users/explain/toolkits/axes_grid.html) can also display data with a fixed aspect ratio. However, the size of the figure will not be correct. mplotutils (from version 0.6) can also help with this
18+
19+
| Axes grid - without mplotutils | Axes grid - with mplotutils |
20+
| :--------------------------------: | :-------------------------: |
21+
| <img src="docs/example_axes_grid_no_mpu.png" alt="Without mplotutils" width="250"/> | <img src="docs/example_axes_grid_mpu.png" alt="With mplotutils" width="250"/> |
22+
23+
The code to create the example can be found in [docs/example_axes_grid.py](docs/example_axes_grid.py).
24+
25+
1426
## Installation
1527

1628
See [docs/installation.md](docs/installation.md).

docs/example_axes_grid.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import cartopy.crs as ccrs
2+
import matplotlib.pyplot as plt
3+
from cartopy.mpl.geoaxes import GeoAxes
4+
from mpl_toolkits.axes_grid1 import AxesGrid
5+
6+
import mplotutils as mpu
7+
8+
9+
def plot_map_axes_grid():
10+
11+
# create example data
12+
da = mpu.sample_dataarray(30, 30)
13+
14+
axes_class = (GeoAxes, {"projection": ccrs.Robinson()})
15+
16+
f = plt.figure()
17+
18+
axgr = AxesGrid(
19+
f,
20+
111,
21+
axes_class=axes_class,
22+
nrows_ncols=(1, 2),
23+
axes_pad=0.5 / 2.54,
24+
cbar_mode="single",
25+
cbar_location="bottom",
26+
cbar_size="10%",
27+
# see https://github.com/matplotlib/matplotlib/issues/28343
28+
cbar_pad=0.0 / 2.54,
29+
)
30+
31+
for ax in axgr.axes_all:
32+
mappable = da.plot(ax=ax, transform=ccrs.PlateCarree(), add_colorbar=False)
33+
ax.coastlines()
34+
35+
cbax = axgr.cbar_axes[0]
36+
37+
cbax.colorbar(mappable)
38+
39+
f.subplots_adjust(left=0.05, right=0.95) # , bottom=0, top=1)
40+
41+
return f, axgr
42+
43+
44+
def plot_map_no_mpu():
45+
"""plot 2 x 2 global maps _not_ using mplotutils"""
46+
47+
f, __ = plot_map_axes_grid()
48+
49+
print(f.get_size_inches()) # * 2.54)
50+
51+
f.suptitle("AxesGrid - without mplotutils")
52+
53+
54+
def plot_map_mpu():
55+
"""plot 2 x 2 global maps using mplotutils"""
56+
57+
f, axgr = plot_map_axes_grid()
58+
59+
# ensure the figure has the correct size
60+
mpu.set_map_layout(axgr, width=6.4 * 2.54)
61+
62+
print(f.get_size_inches()) # * 2.54)
63+
64+
f.suptitle("AxesGrid - with mplotutils")
65+
66+
67+
if __name__ == "__main__":
68+
opt = {"dpi": 200, "facecolor": "0.9", "transparent": False}
69+
70+
plot_map_no_mpu()
71+
plt.savefig("example_axes_grid_no_mpu.png", **opt)
72+
73+
plot_map_mpu()
74+
plt.savefig("example_axes_grid_mpu.png", **opt)

docs/example_axes_grid_mpu.png

142 KB
Loading

docs/example_axes_grid_no_mpu.png

148 KB
Loading

mplotutils/map_layout.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1+
import warnings
2+
13
import matplotlib.pyplot as plt
24
import numpy as np
5+
from mpl_toolkits.axes_grid1 import AxesGrid
36

47
from mplotutils._deprecate import _deprecate_positional_args
8+
from mplotutils.mpl import _get_renderer
59

610

711
@_deprecate_positional_args("0.3")
8-
def set_map_layout(axes, width=17.0, *, nrow=None, ncol=None):
12+
def set_map_layout(obj=None, width=17.0, *, nrow=None, ncol=None, axes=None):
913
"""set figure height, given width, taking axes' aspect ratio into account
1014
1115
Needs to be called after all plotting is done.
1216
1317
Parameters
1418
----------
15-
axes : (Geo)Axes | iterable of (Geo)Axes
16-
Array with all axes of the figure.
19+
obj : (Geo)Axes | iterable of (Geo)Axes | AxesGrid
20+
Array with all axes of the figure or mpl_toolkits.axes_grid1 AxesGrid.
1721
width : float, default: 17
1822
Width of the full figure in cm.
1923
nrow : integer, default: None
@@ -28,6 +32,26 @@ def set_map_layout(axes, width=17.0, *, nrow=None, ncol=None):
2832
Only works if all the axes have the same aspect ratio.
2933
"""
3034

35+
if axes is not None and obj is not None:
36+
raise TypeError("Cannot pass 'obj' and 'axes'")
37+
38+
if axes is not None:
39+
warnings.warn("The 'axes' keyword has been renamed to 'obj'", FutureWarning)
40+
obj = axes
41+
42+
if obj is None:
43+
raise TypeError(
44+
"set_map_layout() missing 1 required positional argument: 'obj'"
45+
)
46+
47+
if isinstance(obj, AxesGrid):
48+
_set_map_layout_axes_grid(obj, width, nrow, ncol)
49+
else:
50+
_set_map_layout_axes(obj, width, nrow, ncol)
51+
52+
53+
def _set_map_layout_axes(axes, width, nrow, ncol):
54+
3155
if (nrow is None and ncol is not None) or (nrow is not None and ncol is None):
3256
raise ValueError("Must set none or both of 'nrow' and 'ncol'")
3357

@@ -69,3 +93,51 @@ def set_map_layout(axes, width=17.0, *, nrow=None, ncol=None):
6993

7094
f.set_figwidth(width / 2.54)
7195
f.set_figheight(height / 2.54)
96+
97+
98+
def _set_map_layout_axes_grid(axgr, width, nrow, ncol):
99+
100+
if nrow is not None or ncol is not None:
101+
raise TypeError("Cannot pass 'nrow' or 'ncol' for and 'AxesGrid'")
102+
103+
# assumes the first of the axes is representative for all
104+
ax = axgr.axes_all[0]
105+
106+
f = ax.get_figure()
107+
108+
# getting the correct data ratio of geoaxes requires draw
109+
f.canvas.draw()
110+
111+
bottom = f.subplotpars.bottom
112+
top = f.subplotpars.top
113+
left = f.subplotpars.left
114+
right = f.subplotpars.right
115+
116+
width_fraction = right - left
117+
height_fraction = top - bottom
118+
119+
inner_width = width_fraction * width
120+
121+
renderer = _get_renderer(f)
122+
123+
# divider get_*_sizes contains the relative and absolute sizes of all plot elements
124+
# (subplots, colorbars, pad & colorbar pad)
125+
126+
divider = axgr.get_divider()
127+
128+
vertical_sizes = divider.get_vertical_sizes(renderer)
129+
vs_rel = vertical_sizes[:, 0].sum()
130+
vs_abs = vertical_sizes[:, 1].sum() * 2.54
131+
132+
horizontal_sizes = divider.get_horizontal_sizes(renderer)
133+
hs_rel = horizontal_sizes[:, 0].sum()
134+
hs_abs = horizontal_sizes[:, 1].sum() * 2.54
135+
136+
inner_height = (inner_width - hs_abs) / hs_rel * vs_rel + vs_abs
137+
138+
if inner_height <= 0:
139+
raise ValueError("Not enough space on figure")
140+
141+
height = inner_height / height_fraction
142+
143+
f.set_size_inches(width / 2.54, height / 2.54)

mplotutils/tests/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,13 @@ def _set_backend(backend):
5050
matplotlib.use(backend)
5151
except ImportError:
5252
pytest.skip(backend)
53+
54+
55+
def get_rtol(f):
56+
# macosx is only exact up to 1 / dpi
57+
58+
if plt.get_backend().lower() != "macosx":
59+
rtol = 1e-07
60+
else:
61+
rtol = 1 / f.get_dpi()
62+
return rtol

mplotutils/tests/test_set_map_layout.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,25 @@
44

55
from mplotutils import set_map_layout
66

7-
from . import figure_context, subplots_context
7+
from . import figure_context, get_rtol, subplots_context
88

99

10-
def get_rtol(f):
11-
# macosx is only exact up to 1 / dpi
10+
def test_set_map_layout_deprecated_kwarg():
1211

13-
if plt.get_backend().lower() != "macosx":
14-
rtol = 1e-07
15-
else:
16-
rtol = 1 / f.get_dpi()
17-
return rtol
12+
with subplots_context() as (__, ax):
13+
with pytest.warns(
14+
FutureWarning, match="The 'axes' keyword has been renamed to 'obj'"
15+
):
16+
set_map_layout(axes=ax)
17+
18+
with pytest.raises(
19+
TypeError,
20+
match=r"set_map_layout\(\) missing 1 required positional argument: 'obj'",
21+
):
22+
set_map_layout()
23+
24+
with pytest.raises(TypeError, match="Cannot pass 'obj' and 'axes'"):
25+
set_map_layout(object, axes=object)
1826

1927

2028
def test_set_map_layout_default_width():
@@ -195,10 +203,10 @@ def test_set_map_layout_two_axes_horz():
195203

196204
def test_set_map_layout_nrow_ncol_only_one_raises():
197205
with pytest.raises(ValueError, match="Must set none or both of 'nrow' and 'ncol'"):
198-
set_map_layout(None, width=17.0, nrow=1, ncol=None)
206+
set_map_layout(object, width=17.0, nrow=1, ncol=None)
199207

200208
with pytest.raises(ValueError, match="Must set none or both of 'nrow' and 'ncol'"):
201-
set_map_layout(None, width=17.0, nrow=None, ncol=1)
209+
set_map_layout(object, width=17.0, nrow=None, ncol=1)
202210

203211

204212
def test_set_map_layout_cartopy_2_2():

0 commit comments

Comments
 (0)