Recently, I was working on a multi panel map figure. The figure used tight_layout. The figures produced in Matplotlib 2 have smaller margins than those in Matplotlib 3.
As @tacaswell pointed out on Twitter in a thread with @dopplershift , “More things now participate it [sic] tight_layout.” A warning message received while running suggests that perhaps additional Cartopy “axes decorations” participate in tight_layout in Matplotlib 3.
Below, I include four plots (both MPL versions; with and without Caropy axes) that demonstrate the problem. The upstream changes in Matplotlib 3 apparently result in more than cosmetic layout differences on Cartopy axes for these relatively simple plots.




I created two minimal environments:
conda create -n mpl2 -c conda-forge matplotlib=2 python=3.7 cartopy
conda create -n mpl3 -c conda-forge matplotlib=3 python=3.7 cartopy
Then using the plotting script, I received the following output.
ebruning$ source activate mpl3
(mpl3) ebruning$ python plot.py
Plotting with map
/Users/ebruning/anaconda/envs/mpl3/lib/python3.7/site-packages/matplotlib/tight_layout.py:176: UserWarning: Tight layout not applied. The left and right margins cannot be made large enough to accommodate all axes decorations.
warnings.warn('Tight layout not applied. The left and right margins '
Plotting with no map
(mpl3) ebruning$ source deactivate
ebruning$ source activate mpl2
(mpl2) ebruning$ python plot.py
Plotting with map
Plotting with no map
The plotting script
import numpy as np
import cartopy.crs as ccrs
import matplotlib
matplotlib.use('Agg')
ver = matplotlib.__version__
import matplotlib.pyplot as plt
### This code produces smaller margins in Matplotlib 2 vs. 3.
print("Plotting with map")
fig = plt.figure()
subplots = (2,3)
n_panels = subplots[0] * subplots[1]
lons = np.arange(-120, -80, 1, dtype=float)
lats = np.arange(20, 50, 1, dtype=float)
datashape = lons.shape+lats.shape
fields = [np.random.randn(*datashape) for i in range(n_panels)]
imgextent=(lons.min(), lons.max(), lats.min(), lats.max())
proj = ccrs.PlateCarree()
for fi, f in enumerate(fields):
ax = fig.add_subplot(subplots[0], subplots[1], fi+1, projection=proj)
ax.imshow(f, extent=imgextent)
ax.coastlines()
fig.suptitle('Map, matplotlib={0}'.format(ver))
fig.tight_layout()
fig.savefig(ver+'.png')
### Demonstrate that the issue only happens with Caropy axes.
### This code produces identical figures in Matplotlib 2 and 3.
print("Plotting with no map")
fignomap = plt.figure()
subplots = (2,3)
n_panels = subplots[0] * subplots[1]
lons = np.arange(-120, -80, 1, dtype=float)
lats = np.arange(20, 50, 1, dtype=float)
datashape = lons.shape+lats.shape
fields = [np.random.randn(*datashape) for i in range(n_panels)]
imgextent=(lons.min(), lons.max(), lats.min(), lats.max())
for fi, f in enumerate(fields):
ax = fignomap.add_subplot(subplots[0], subplots[1], fi+1)
ax.imshow(f, extent=imgextent)
fignomap.suptitle('No map, matplotlib={0}'.format(ver))
fignomap.tight_layout()
fignomap.savefig(ver+'_no_map.png')
No errors.
Full environment definition
Mac OS X 10.14 (Mojave). Behavior also observed on CentOS server.
0.17.0
Matplotlib 2
(mpl2) ebruning$ conda list
# packages in environment at /Users/ebruning/anaconda/envs/mpl2:
#
asn1crypto 0.24.0 py37_1003 conda-forge
blas 1.0 mkl
ca-certificates 2018.10.15 ha4d7672_0 conda-forge
cartopy 0.17.0 py37h81b52dc_0 conda-forge
certifi 2018.10.15 py37_1000 conda-forge
cffi 1.11.5 py37h5e8e0c9_1 conda-forge
chardet 3.0.4 py37_1003 conda-forge
cryptography 2.3.1 py37hdffb7b8_0 conda-forge
cycler 0.10.0 py_1 conda-forge
freetype 2.9.1 h6debe1e_4 conda-forge
geos 3.6.2 hfc679d8_3 conda-forge
icu 58.2 hfc679d8_0 conda-forge
idna 2.7 py37_1002 conda-forge
intel-openmp 2019.0 118
jpeg 9c h470a237_1 conda-forge
kiwisolver 1.0.1 py37h2d50403_2 conda-forge
libcxx 7.0.0 h2d50403_2 conda-forge
libffi 3.2.1 hfc679d8_5 conda-forge
libgfortran 3.0.1 h93005f0_2
libiconv 1.15 h470a237_3 conda-forge
libpng 1.6.35 ha92aebf_2 conda-forge
libtiff 4.0.9 he6b73bb_2 conda-forge
libxml2 2.9.8 h422b904_5 conda-forge
libxslt 1.1.32 h88dbc4e_2 conda-forge
llvm-meta 7.0.0 0 conda-forge
lxml 4.2.5 py37hc9114bc_0 conda-forge
matplotlib 2.2.3 py37h0e0179f_0 conda-forge
mkl 2019.0 118
mkl_fft 1.0.6 py37hb8a8100_0
mkl_random 1.0.1 py37h5d10147_1
ncurses 6.1 hfc679d8_1 conda-forge
numpy 1.15.4 py37h6a91979_0
numpy-base 1.15.4 py37h8a80b8c_0
olefile 0.46 py_0 conda-forge
openssl 1.0.2p h470a237_1 conda-forge
owslib 0.17.0 py_0 conda-forge
pillow 5.3.0 py37hc736899_0 conda-forge
pip 18.1 py37_1000 conda-forge
proj4 4.9.3 h470a237_8 conda-forge
pycparser 2.19 py_0 conda-forge
pyepsg 0.3.2 py_1 conda-forge
pykdtree 1.3.1 py37h7eb728f_2 conda-forge
pyopenssl 18.0.0 py37_1000 conda-forge
pyparsing 2.3.0 py_0 conda-forge
pyproj 1.9.5.1 py37h508ed2a_6 conda-forge
pyshp 2.0.0 py_0 conda-forge
pysocks 1.6.8 py37_1002 conda-forge
python 3.7.1 h46c1a51_0 conda-forge
python-dateutil 2.7.5 py_0 conda-forge
pytz 2018.7 py_0 conda-forge
readline 7.0 haf1bffa_1 conda-forge
requests 2.20.1 py37_1000 conda-forge
scipy 1.1.0 py37h28f7352_1
setuptools 40.6.2 py37_0 conda-forge
shapely 1.6.4 py37h164cb2d_0 conda-forge
six 1.11.0 py37_1001 conda-forge
sqlite 3.25.3 hb1c47c0_0 conda-forge
tk 8.6.9 ha92aebf_0 conda-forge
tornado 5.1.1 py37h470a237_0 conda-forge
urllib3 1.23 py37_1001 conda-forge
wheel 0.32.2 py37_0 conda-forge
xz 5.2.4 h470a237_1 conda-forge
zlib 1.2.11 h470a237_3 conda-forge
Matplotlib 3
(mpl3) ebruning$ conda list
# packages in environment at /Users/ebruning/anaconda/envs/mpl3:
#
asn1crypto 0.24.0 py37_1003 conda-forge
blas 1.0 mkl
ca-certificates 2018.10.15 ha4d7672_0 conda-forge
cartopy 0.17.0 py37h81b52dc_0 conda-forge
certifi 2018.10.15 py37_1000 conda-forge
cffi 1.11.5 py37h5e8e0c9_1 conda-forge
chardet 3.0.4 py37_1003 conda-forge
cryptography 2.3.1 py37hdffb7b8_0 conda-forge
cycler 0.10.0 py_1 conda-forge
freetype 2.9.1 h6debe1e_4 conda-forge
geos 3.6.2 hfc679d8_3 conda-forge
icu 58.2 hfc679d8_0 conda-forge
idna 2.7 py37_1002 conda-forge
intel-openmp 2019.0 118
jpeg 9c h470a237_1 conda-forge
kiwisolver 1.0.1 py37h2d50403_2 conda-forge
libcxx 7.0.0 h2d50403_2 conda-forge
libffi 3.2.1 hfc679d8_5 conda-forge
libgfortran 3.0.1 h93005f0_2
libiconv 1.15 h470a237_3 conda-forge
libpng 1.6.35 ha92aebf_2 conda-forge
libtiff 4.0.9 he6b73bb_2 conda-forge
libxml2 2.9.8 h422b904_5 conda-forge
libxslt 1.1.32 h88dbc4e_2 conda-forge
llvm-meta 7.0.0 0 conda-forge
lxml 4.2.5 py37hc9114bc_0 conda-forge
matplotlib 3.0.2 py37_1 conda-forge
matplotlib-base 3.0.2 py37hb2d221d_1 conda-forge
mkl 2019.0 118
mkl_fft 1.0.6 py37hb8a8100_0
mkl_random 1.0.1 py37h5d10147_1
ncurses 6.1 hfc679d8_1 conda-forge
numpy 1.15.4 py37h6a91979_0
numpy-base 1.15.4 py37h8a80b8c_0
olefile 0.46 py_0 conda-forge
openssl 1.0.2p h470a237_1 conda-forge
owslib 0.17.0 py_0 conda-forge
pillow 5.3.0 py37hc736899_0 conda-forge
pip 18.1 py37_1000 conda-forge
proj4 4.9.3 h470a237_8 conda-forge
pycparser 2.19 py_0 conda-forge
pyepsg 0.3.2 py_1 conda-forge
pykdtree 1.3.1 py37h7eb728f_2 conda-forge
pyopenssl 18.0.0 py37_1000 conda-forge
pyparsing 2.3.0 py_0 conda-forge
pyproj 1.9.5.1 py37h508ed2a_6 conda-forge
pyshp 2.0.0 py_0 conda-forge
pysocks 1.6.8 py37_1002 conda-forge
python 3.7.1 h46c1a51_0 conda-forge
python-dateutil 2.7.5 py_0 conda-forge
pytz 2018.7 py_0 conda-forge
readline 7.0 haf1bffa_1 conda-forge
requests 2.20.1 py37_1000 conda-forge
scipy 1.1.0 py37h28f7352_1
setuptools 40.6.2 py37_0 conda-forge
shapely 1.6.4 py37h164cb2d_0 conda-forge
six 1.11.0 py37_1001 conda-forge
sqlite 3.25.3 hb1c47c0_0 conda-forge
tk 8.6.9 ha92aebf_0 conda-forge
tornado 5.1.1 py37h470a237_0 conda-forge
urllib3 1.23 py37_1001 conda-forge
wheel 0.32.2 py37_0 conda-forge
xz 5.2.4 h470a237_1 conda-forge
zlib 1.2.11 h470a237_3 conda-forge
Matplotlib 2
(mpl2) ebruning$ pip list
Package Version
--------------- -----------
asn1crypto 0.24.0
Cartopy 0.17.0
certifi 2018.10.15
cffi 1.11.5
chardet 3.0.4
cryptography 2.3.1
cycler 0.10.0
idna 2.7
kiwisolver 1.0.1
lxml 4.2.5
matplotlib 2.2.3
mkl-fft 1.0.6
mkl-random 1.0.1
numpy 1.15.4
olefile 0.46
OWSLib 0.17.0
Pillow 5.3.0
pip 18.1
pycparser 2.19
pyepsg 0.3.2
pykdtree 1.3.1
pyOpenSSL 18.0.0
pyparsing 2.3.0
pyproj 1.9.5.1
pyshp 2.0.0
PySocks 1.6.8
python-dateutil 2.7.5
pytz 2018.7
requests 2.20.1
scipy 1.1.0
setuptools 40.6.2
Shapely 1.6.4.post2
six 1.11.0
tornado 5.1.1
urllib3 1.23
wheel 0.32.2
Matplotlib 3
(mpl3) ebruning$ pip list
Package Version
--------------- -----------
asn1crypto 0.24.0
Cartopy 0.17.0
certifi 2018.10.15
cffi 1.11.5
chardet 3.0.4
cryptography 2.3.1
cycler 0.10.0
idna 2.7
kiwisolver 1.0.1
lxml 4.2.5
matplotlib 3.0.2
mkl-fft 1.0.6
mkl-random 1.0.1
numpy 1.15.4
olefile 0.46
OWSLib 0.17.0
Pillow 5.3.0
pip 18.1
pycparser 2.19
pyepsg 0.3.2
pykdtree 1.3.1
pyOpenSSL 18.0.0
pyparsing 2.3.0
pyproj 1.9.5.1
pyshp 2.0.0
PySocks 1.6.8
python-dateutil 2.7.5
pytz 2018.7
requests 2.20.1
scipy 1.1.0
setuptools 40.6.2
Shapely 1.6.4.post2
six 1.11.0
tornado 5.1.1
urllib3 1.23
wheel 0.32.2
Yes, presumably something in tha map is not being clipped and Matplotlib is making space for it. Sorry the change in how we check for size of axes is causing problems. As noted above, we now check all objects in an axes for the bounding box calculation. If there is something we can do to fix this, let us know.
As a work-around, inserting fig.canvas.draw() before calling tight_layout works fine.
The bounding box for the GeoAxes object is not correct until the draw is called. I suspect you need to move https://github.com/SciTools/cartopy/blob/75c8296d9cfa552d8a40fde0ee13e9b6d5e68cbf/lib/cartopy/mpl/geoaxes.py#L365-L369
earlier, or duplicate in a local get_window_extent definition. I admit I'm not clear on why those patches are bigger than the axes in the first place - I assume because they are initialized from +/-180 and +/-90?
Thanks for figuring out the root cause @jklymak ! Since the projections can all have vastly different boundary paths/patches, I think the approach taken here just allows for some better flexibility. @pelson might know more. I’m guessing before your work on the layout code, get_window_extent was just never that important before in our users’ normal use cases.
I don’t see any reason why not to refactor that code out to a helper and add an override of get_window_extent to GeoAxes that also can do the update if necessary.
I'm already working to get rid of those two patches; I don't really see how they're any more flexible than Matplotlib's builtin Axes artists.
Any headway on this one? It's one of the few issues keeping our project from switching from basemap to cartopy.
As a work-around, inserting
fig.canvas.draw()before callingtight_layoutworks fine.
sorry for commenting on this old issue. Is there already another fix?
fig.canvas.draw() works for me, but it takes a really long time if you do a lot of plots.
I think this is actually due the axis have x/y limits of (0, 1) upon initialization and those limits not changing until the draw command. The get_extent works immediately upon initialization, but the x/y limits are not set. A simple test for people to use immediately would be to call ax.set_global themselves right after the axis creation.
import cartopy
import matplotlib.pyplot as plt
f, ax1 = plt.subplots(1, 1, constrained_layout=True,
subplot_kw=dict(projection=ccrs.PlateCarree()))
ax1.set_global()
f.savefig('images/temp/mpl-1.png')
f.savefig('images/temp/mpl-2.png')
@QuLogic are you still planning on removing the background_patch? That may also solve the issue, but this was a pretty simple fix for my use case at least.
Most helpful comment
Thanks for figuring out the root cause @jklymak ! Since the projections can all have vastly different boundary paths/patches, I think the approach taken here just allows for some better flexibility. @pelson might know more. I’m guessing before your work on the layout code,
get_window_extentwas just never that important before in our users’ normal use cases.I don’t see any reason why not to refactor that code out to a helper and add an override of
get_window_extenttoGeoAxesthat also can do the update if necessary.