Repository: SciTools/cartopy

Issue:
Calling tight_layout between gridlines and set_extent screws up
### Description
Since cartopy 0.19.0 (this does not apply to 0.18.0), calling `plt.tight_layout()` after `gridlines`, but before `set_extent` screws up the map extent and the grid lines are drawn for the whole valid extent of the selected projection instead of only the selected extent. In a Jupyter notebook, with `%matplotlib widget`, this will lead to no gridline labels and too few lines. With `%matplotlib inline` the plotted area is truncated without truncating the gridline labels (see output below).

#### Code to reproduce

In a jupyter notebook:
```
%matplotlib inline
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
ax = plt.axes(projection=ccrs.PlateCarree())
ax.gridlines(draw_labels=True)
plt.tight_layout()
ax.set_extent((-13, 32, 35, 72), crs=ccrs.PlateCarree())
ax.coastlines()
```
Output:
![image](https://user-images.githubusercontent.com/57705593/118772157-b44b6a80-b883-11eb-86dc-fff0a013b72f.png)

Note that there is no problem when the call to `plt.tight_layout()` is moved either before `ax.gridlines` or after `ax.set_extent`. In that case, the result looks like this:
![image](https://user-images.githubusercontent.com/57705593/118772218-c7f6d100-b883-11eb-97b8-a23917bc4375.png)

<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->

### Operating system

Ubuntu 20.04

### Cartopy version

cartopy 0.19.0.post1 (py38hc9c980b_0) from conda-forge (does not apply to 0.18.0)

### conda list

```
# packages in environment at /home/tovogt/.local/share/miniconda3/envs/cartopy:
#
# Name                    Version                   Build  Channel
_libgcc_mutex             0.1                 conda_forge    conda-forge
_openmp_mutex             4.5                       1_gnu    conda-forge
anyio                     3.0.1            py39hf3d152e_0    conda-forge
argon2-cffi               20.1.0           py39h3811e60_2    conda-forge
async_generator           1.10                       py_0    conda-forge
attrs                     21.2.0             pyhd8ed1ab_0    conda-forge
babel                     2.9.1              pyh44b312d_0    conda-forge
backcall                  0.2.0              pyh9f0ad1d_0    conda-forge
backports                 1.0                        py_2    conda-forge
backports.functools_lru_cache 1.6.4              pyhd8ed1ab_0    conda-forge
bleach                    3.3.0              pyh44b312d_0    conda-forge
brotlipy                  0.7.0           py39h3811e60_1001    conda-forge
c-ares                    1.17.1               h7f98852_1    conda-forge
ca-certificates           2020.12.5            ha878542_0    conda-forge
cartopy                   0.19.0.post1     py39h3b23250_0    conda-forge
certifi                   2020.12.5        py39hf3d152e_1    conda-forge
cffi                      1.14.5           py39he32792d_0    conda-forge
chardet                   4.0.0            py39hf3d152e_1    conda-forge
cryptography              3.4.7            py39hbca0aa6_0    conda-forge
cycler                    0.10.0                     py_2    conda-forge
decorator                 5.0.9              pyhd8ed1ab_0    conda-forge
defusedxml                0.7.1              pyhd8ed1ab_0    conda-forge
entrypoints               0.3             pyhd8ed1ab_1003    conda-forge
freetype                  2.10.4               h0708190_1    conda-forge
geos                      3.9.1                h9c3ff4c_2    conda-forge
idna                      2.10               pyh9f0ad1d_0    conda-forge
importlib-metadata        4.0.1            py39hf3d152e_0    conda-forge
ipykernel                 5.5.5            py39hef51801_0    conda-forge
ipython                   7.23.1           py39hef51801_0    conda-forge
ipython_genutils          0.2.0                      py_1    conda-forge
jedi                      0.18.0           py39hf3d152e_2    conda-forge
jinja2                    3.0.0              pyhd8ed1ab_0    conda-forge
jpeg                      9d                   h36c2ea0_0    conda-forge
json5                     0.9.5              pyh9f0ad1d_0    conda-forge
jsonschema                3.2.0              pyhd8ed1ab_3    conda-forge
jupyter_client            6.1.12             pyhd8ed1ab_0    conda-forge
jupyter_core              4.7.1            py39hf3d152e_0    conda-forge
jupyter_server            1.7.0            py39hf3d152e_1    conda-forge
jupyterlab                3.0.16             pyhd8ed1ab_0    conda-forge
jupyterlab_pygments       0.1.2              pyh9f0ad1d_0    conda-forge
jupyterlab_server         2.5.2              pyhd8ed1ab_0    conda-forge
kiwisolver                1.3.1            py39h1a9c180_1    conda-forge
krb5                      1.19.1               hcc1bbae_0    conda-forge
lcms2                     2.12                 hddcbb42_0    conda-forge
ld_impl_linux-64          2.35.1               hea4e1c9_2    conda-forge
libblas                   3.9.0                9_openblas    conda-forge
libcblas                  3.9.0                9_openblas    conda-forge
libcurl                   7.76.1               h2574ce0_2    conda-forge
libedit                   3.1.20191231         he28a2e2_2    conda-forge
libev                     4.33                 h516909a_1    conda-forge
libffi                    3.3                  h58526e2_2    conda-forge
libgcc-ng                 9.3.0               h2828fa1_19    conda-forge
libgfortran-ng            9.3.0               hff62375_19    conda-forge
libgfortran5              9.3.0               hff62375_19    conda-forge
libgomp                   9.3.0               h2828fa1_19    conda-forge
liblapack                 3.9.0                9_openblas    conda-forge
libnghttp2                1.43.0               h812cca2_0    conda-forge
libopenblas               0.3.15          pthreads_h8fe5266_1    conda-forge
libpng                    1.6.37               h21135ba_2    conda-forge
libsodium                 1.0.18               h36c2ea0_1    conda-forge
libssh2                   1.9.0                ha56f1ee_6    conda-forge
libstdcxx-ng              9.3.0               h6de172a_19    conda-forge
libtiff                   4.2.0                hf544144_3    conda-forge
libwebp-base              1.2.0                h7f98852_2    conda-forge
lz4-c                     1.9.3                h9c3ff4c_0    conda-forge
markupsafe                2.0.0            py39h3811e60_0    conda-forge
matplotlib-base           3.4.2            py39h2fa2bec_0    conda-forge
matplotlib-inline         0.1.2              pyhd8ed1ab_2    conda-forge
mistune                   0.8.4           py39h3811e60_1003    conda-forge
nbclassic                 0.2.8              pyhd8ed1ab_0    conda-forge
nbclient                  0.5.3              pyhd8ed1ab_0    conda-forge
nbconvert                 6.0.7            py39hf3d152e_3    conda-forge
nbformat                  5.1.3              pyhd8ed1ab_0    conda-forge
ncurses                   6.2                  h58526e2_4    conda-forge
nest-asyncio              1.5.1              pyhd8ed1ab_0    conda-forge
notebook                  6.4.0              pyha770c72_0    conda-forge
numpy                     1.20.2           py39hdbf815f_0    conda-forge
olefile                   0.46               pyh9f0ad1d_1    conda-forge
openjpeg                  2.4.0                hb52868f_1    conda-forge
openssl                   1.1.1k               h7f98852_0    conda-forge
packaging                 20.9               pyh44b312d_0    conda-forge
pandoc                    2.13                 h7f98852_0    conda-forge
pandocfilters             1.4.2                      py_1    conda-forge
parso                     0.8.2              pyhd8ed1ab_0    conda-forge
pexpect                   4.8.0              pyh9f0ad1d_2    conda-forge
pickleshare               0.7.5                   py_1003    conda-forge
pillow                    8.2.0            py39hf95b381_1    conda-forge
pip                       21.1.1             pyhd8ed1ab_0    conda-forge
proj                      7.2.0                h277dcde_2    conda-forge
prometheus_client         0.10.1             pyhd8ed1ab_0    conda-forge
prompt-toolkit            3.0.18             pyha770c72_0    conda-forge
ptyprocess                0.7.0              pyhd3deb0d_0    conda-forge
pycparser                 2.20               pyh9f0ad1d_2    conda-forge
pygments                  2.9.0              pyhd8ed1ab_0    conda-forge
pyopenssl                 20.0.1             pyhd8ed1ab_0    conda-forge
pyparsing                 2.4.7              pyh9f0ad1d_0    conda-forge
pyrsistent                0.17.3           py39h3811e60_2    conda-forge
pyshp                     2.1.3              pyh44b312d_0    conda-forge
pysocks                   1.7.1            py39hf3d152e_3    conda-forge
python                    3.9.4           hffdb5ce_0_cpython    conda-forge
python-dateutil           2.8.1                      py_0    conda-forge
python_abi                3.9                      1_cp39    conda-forge
pytz                      2021.1             pyhd8ed1ab_0    conda-forge
pyzmq                     22.0.3           py39h37b5a0c_1    conda-forge
readline                  8.1                  h46c0cb4_0    conda-forge
requests                  2.25.1             pyhd3deb0d_0    conda-forge
scipy                     1.6.3            py39hee8e79c_0    conda-forge
send2trash                1.5.0                      py_0    conda-forge
setuptools                49.6.0           py39hf3d152e_3    conda-forge
shapely                   1.7.1            py39ha61afbd_4    conda-forge
six                       1.16.0             pyh6c4a22f_0    conda-forge
sniffio                   1.2.0            py39hf3d152e_1    conda-forge
sqlite                    3.35.5               h74cdb3f_0    conda-forge
terminado                 0.9.4            py39h06a4308_0  
testpath                  0.5.0              pyhd8ed1ab_0    conda-forge
tk                        8.6.10               h21135ba_1    conda-forge
tornado                   6.1              py39h3811e60_1    conda-forge
traitlets                 5.0.5                      py_0    conda-forge
tzdata                    2021a                he74cb21_0    conda-forge
urllib3                   1.26.4             pyhd8ed1ab_0    conda-forge
wcwidth                   0.2.5              pyh9f0ad1d_2    conda-forge
webencodings              0.5.1                      py_1    conda-forge
websocket-client          0.57.0           py39hf3d152e_4    conda-forge
wheel                     0.36.2             pyhd3deb0d_0    conda-forge
xz                        5.2.5                h516909a_1    conda-forge
zeromq                    4.3.4                h9c3ff4c_0    conda-forge
zipp                      3.4.1              pyhd8ed1ab_0    conda-forge
zlib                      1.2.11            h516909a_1010    conda-forge
zstd                      1.5.0                ha95c52a_0    conda-forge
```

### pip list

```
Package                       Version
----------------------------- -------------------
anyio                         3.0.1
argon2-cffi                   20.1.0
async-generator               1.10
attrs                         21.2.0
Babel                         2.9.1
backcall                      0.2.0
backports.functools-lru-cache 1.6.4
bleach                        3.3.0
brotlipy                      0.7.0
Cartopy                       0.19.0.post1
certifi                       2020.12.5
cffi                          1.14.5
chardet                       4.0.0
cryptography                  3.4.7
cycler                        0.10.0
decorator                     5.0.9
defusedxml                    0.7.1
entrypoints                   0.3
idna                          2.10
importlib-metadata            4.0.1
ipykernel                     5.5.5
ipython                       7.23.1
ipython-genutils              0.2.0
jedi                          0.18.0
Jinja2                        3.0.0
json5                         0.9.5
jsonschema                    3.2.0
jupyter-client                6.1.12
jupyter-core                  4.7.1
jupyter-server                1.7.0
jupyterlab                    3.0.16
jupyterlab-pygments           0.1.2
jupyterlab-server             2.5.2
kiwisolver                    1.3.1
MarkupSafe                    2.0.0
matplotlib                    3.4.2
matplotlib-inline             0.1.2
mistune                       0.8.4
nbclassic                     0.2.8
nbclient                      0.5.3
nbconvert                     6.0.7
nbformat                      5.1.3
nest-asyncio                  1.5.1
notebook                      6.4.0
numpy                         1.20.2
olefile                       0.46
packaging                     20.9
pandocfilters                 1.4.2
parso                         0.8.2
pexpect                       4.8.0
pickleshare                   0.7.5
Pillow                        8.2.0
pip                           21.1.1
prometheus-client             0.10.1
prompt-toolkit                3.0.18
ptyprocess                    0.7.0
pycparser                     2.20
Pygments                      2.9.0
pyOpenSSL                     20.0.1
pyparsing                     2.4.7
pyrsistent                    0.17.3
pyshp                         2.1.3
PySocks                       1.7.1
python-dateutil               2.8.1
pytz                          2021.1
pyzmq                         22.0.3
requests                      2.25.1
scipy                         1.6.3
Send2Trash                    1.5.0
setuptools                    49.6.0.post20210108
Shapely                       1.7.1
six                           1.16.0
sniffio                       1.2.0
terminado                     0.9.4
testpath                      0.5.0
tornado                       6.1
traitlets                     5.0.5
urllib3                       1.26.4
wcwidth                       0.2.5
webencodings                  0.5.1
websocket-client              0.57.0
wheel                         0.36.2
zipp                          3.4.1
```
</details>



Discussion:
Just wanted to mention that this is still an issue with cartopy 0.20.2. I haven't tested with 0.21 though.
Thanks for testing with 0.21! That means the issue is unchanged for 0.21.
Sorry I deleted my comment because I had missed that you said it depended where the call to tight_layout came.  For posterity, here is what I got with 0.21:

![gridlines_setextent_tight](https://user-images.githubusercontent.com/10599679/211024837-b966e0bb-4e5b-40c8-8120-c685a76e2be6.png)

I have learned quite a lot about the `GridLiner` in the last year!  I think the problem here is that, unlike most Matplotlib artists, the gridline and label positions only get calculated once by default.  In this case they are calculated in the call to `tight_layout` but then not updated when the figure is finally drawn.  With a one line change we get the expected plot:

`ax.gridlines(draw_labels=True, auto_update=True)`

Since #2249 and #2252 made the updating more efficient, I wonder if we should consider making `auto_update=True` the default?
> I wonder if we should consider making auto_update=True the default?

That would make sense to me and call it a bugfix even. People can still turn it off if they want by setting that directly, but it doesn't seem necessary anymore and maybe we should take it a step further and deprecate that option altogether?

Resolving patch:
diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py
--- a/lib/cartopy/mpl/geoaxes.py
+++ b/lib/cartopy/mpl/geoaxes.py
@@ -1359,7 +1359,7 @@ def gridlines(self, crs=None, draw_labels=False,
                   xformatter=None, yformatter=None, xlim=None, ylim=None,
                   rotate_labels=None, xlabel_style=None, ylabel_style=None,
                   labels_bbox_style=None, xpadding=5, ypadding=5,
-                  offset_angle=25, auto_update=False, formatter_kwargs=None,
+                  offset_angle=25, auto_update=None, formatter_kwargs=None,
                   **kwargs):
         """
         Automatically add gridlines to the axes, in the given coordinate
@@ -1464,9 +1464,13 @@ def gridlines(self, crs=None, draw_labels=False,
             a label must be flipped to be more readable.
             For example, a value of 10 makes a vertical top label to be
             flipped only at 100 degrees.
-        auto_update: bool
-            Whether to update the grilines and labels when the plot is
+        auto_update: bool, default=True
+            Whether to update the gridlines and labels when the plot is
             refreshed.
+
+            .. deprecated:: 0.23
+               In future the gridlines and labels will always be updated.
+
         formatter_kwargs: dict, optional
             Options passed to the default formatters.
             See :class:`~cartopy.mpl.ticker.LongitudeFormatter` and
diff --git a/lib/cartopy/mpl/gridliner.py b/lib/cartopy/mpl/gridliner.py
--- a/lib/cartopy/mpl/gridliner.py
+++ b/lib/cartopy/mpl/gridliner.py
@@ -114,7 +114,7 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None,
                  xlim=None, ylim=None, rotate_labels=None,
                  xlabel_style=None, ylabel_style=None, labels_bbox_style=None,
                  xpadding=5, ypadding=5, offset_angle=25,
-                 auto_update=False, formatter_kwargs=None):
+                 auto_update=None, formatter_kwargs=None):
         """
         Artist used by :meth:`cartopy.mpl.geoaxes.GeoAxes.gridlines`
         to add gridlines and tick labels to a map.
@@ -218,9 +218,13 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None,
             a label must be flipped to be more readable.
             For example, a value of 10 makes a vertical top label to be
             flipped only at 100 degrees.
-        auto_update: bool
+        auto_update: bool, default=True
             Whether to redraw the gridlines and labels when the figure is
             updated.
+
+            .. deprecated:: 0.23
+               In future the gridlines and labels will always be redrawn.
+
         formatter_kwargs: dict, optional
             Options passed to the default formatters.
             See :class:`~cartopy.mpl.ticker.LongitudeFormatter` and
@@ -444,6 +448,13 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None,
 
         # Draw status
         self._drawn = False
+        if auto_update is None:
+            auto_update = True
+        else:
+            warnings.warn(
+                "The auto_update parameter was deprecated at Cartopy 0.23.  In future "
+                "the gridlines and labels will always be updated.",
+                DeprecationWarning)
         self._auto_update = auto_update
 
     def has_labels(self):


Test patch:
diff --git a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png
Binary files a/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png and b/lib/cartopy/tests/mpl/baseline_images/mpl/test_gridliner/gridliner_labels_tight.png differ
diff --git a/lib/cartopy/tests/mpl/test_gridliner.py b/lib/cartopy/tests/mpl/test_gridliner.py
--- a/lib/cartopy/tests/mpl/test_gridliner.py
+++ b/lib/cartopy/tests/mpl/test_gridliner.py
@@ -216,7 +216,7 @@ def test_grid_labels():
 @pytest.mark.skipif(geos_version == (3, 9, 0), reason="GEOS intersection bug")
 @pytest.mark.natural_earth
 @pytest.mark.mpl_image_compare(filename='gridliner_labels_tight.png',
-                               tolerance=2.92)
+                               tolerance=2.9)
 def test_grid_labels_tight():
     # Ensure tight layout accounts for gridlines
     fig = plt.figure(figsize=(7, 5))
@@ -290,8 +290,7 @@ def test_gridliner_constrained_adjust_datalim():
     ax.autoscale()
 
     # Add some gridlines
-    ax.gridlines(draw_labels=["bottom", "left"], auto_update=True,
-                 linestyle="-")
+    ax.gridlines(draw_labels=["bottom", "left"], linestyle="-")
 
     return fig
 
@@ -515,11 +514,11 @@ def test_gridliner_remove():
 
 
 def test_gridliner_save_tight_bbox():
-    # Smoke test for save with auto_update=True and bbox_inches=Tight (gh2246).
+    # Smoke test for save with bbox_inches=Tight (gh2246).
     fig = plt.figure()
     ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
     ax.set_global()
-    ax.gridlines(draw_labels=True, auto_update=True)
+    ax.gridlines(draw_labels=True)
     fig.savefig(io.BytesIO(), bbox_inches='tight')
 
 
@@ -569,7 +568,7 @@ def test_gridliner_labels_zoom():
 
     # Start with a global map.
     ax.set_global()
-    gl = ax.gridlines(draw_labels=True, auto_update=True)
+    gl = ax.gridlines(draw_labels=True)
 
     fig.draw_without_rendering()  # Generate child artists
     labels = [a.get_text() for a in gl.bottom_label_artists if a.get_visible()]