Cartopy: tight_layout and Matplotlib 3

Created on 18 Nov 2018  Â·  8Comments  Â·  Source: SciTools/cartopy

Description

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.

2 2 3
3 0 2
2 2 3_no_map
3 0 2_no_map

Code to reproduce

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')

Traceback

No errors.


Full environment definition

Operating system

Mac OS X 10.14 (Mojave). Behavior also observed on CentOS server.

Cartopy version

0.17.0

conda list

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

pip list

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     

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_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.

All 8 comments

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 calling tight_layout works 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.

Was this page helpful?
0 / 5 - 0 ratings