Repository: geopandas/geopandas

Issue:
ENH: Allow autolim keyword argument in plots
#### Is your feature request related to a problem?

I use geopandas to reproject global (e.g. Natural Earth) data and plot it for small regions. This causes axes limits to update to encompass the global (and often badly distorted) data

```python
import geopandas

# open Natural Earth data for the world and Greenland
world = geopandas.read_file(geopandas.datasets.get_path('naturalearth_lowres'))
greenland = world[world.name == 'Greenland']

# suppose we make a map of Greenland
ax = greenland.plot()

# check axes limits
print(ax.get_ylim())  # ca. 60 to 85 degrees North

# add other country borders for context
# (I want to add autolim=False)
ax = world.plot(ax=ax, color='0.75', zorder=-1)

# axes limits have been updated though
print(ax.get_ylim())  # ca. -90 to 90 degrees
```

I wish I could prevent geopandas from updating axes limits when plotting a dataframe.

#### Describe the solution you'd like

`GeoDataFrame.plot` should get a new parameter `autolim`. This is forwarded to matplotlib's `ax.add_collection`. Suggested docstring:

```python
    autolim : bool (default True)
        Update axes data limits to contain the new geometries.
```

#### API breaking implications

None. If `autolim` defaults to `True`, I think this will be fully backward-compatible:

https://github.com/geopandas/geopandas/blob/d5541f8817aac769d628a3108d95f7e1295053c7/geopandas/plotting.py#L182-L186

https://github.com/geopandas/geopandas/blob/d5541f8817aac769d628a3108d95f7e1295053c7/geopandas/plotting.py#L240-L244

#### Describe alternatives you've considered

A workaround is to lock axes limits with `ax.set_autoscale_on(False)`. This works if you do things in this order: plot data of interest, lock axes, plot contextual data. I understand that the `autolim` parameter is superior though, because it allows auto-scaling, but tells matplotlib which layers' limits should be ignored.

#### Additional context

This change will benefit a small library I am developing, which combines plots from xarray and geopandas: https://hyoga.readthedocs.io/en/latest/foreword/startup.html

The current code quietly uses `ax.set_autoscale_on(False)`, which is not ideal:

https://github.com/juseg/hyoga/blob/79d91002fb0c8f0f5df3e773492cd7977b7c9a42/hyoga/plot/datasets.py#L378-L382

Happy to open a PR if this makes sense.


Discussion:
That sounds as a good suggestion to me! A PR would be very welcome
Hey @juseg, any progress on this yet?

I would very much like to see support for setting `"autolim"` as well... running into the same problem when trying to avoid unwanted autoscaling in [EOmaps](https://github.com/raphaelquast/EOmaps)
@raphaelquast feel free to prepare a PR yourself, it is already long enough from the original report.
@raphaelquast @martinfleis Sorry for leaving this stale. I have local changes and will open a PR. Due to the upstream issue matplotlib/matplotlib#24416 this will work for line and polygons but not for scatter plots.
> I have local changes and will open a PR. Due to the upstream issue https://github.com/matplotlib/matplotlib/issues/24416 this will work for line and polygons but not for scatter plots.

Thanks! There seems to be some activity on the matplotlib side to fix this in https://github.com/matplotlib/matplotlib/pull/15595 so let's do the work on our side assuming that scatter plot issue will be solved in upstream.

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -112,7 +112,15 @@ def _PolygonPatch(polygon, **kwargs):
 
 
 def _plot_polygon_collection(
-    ax, geoms, values=None, color=None, cmap=None, vmin=None, vmax=None, **kwargs
+    ax,
+    geoms,
+    values=None,
+    color=None,
+    cmap=None,
+    vmin=None,
+    vmax=None,
+    autolim=True,
+    **kwargs,
 ):
     """
     Plots a collection of Polygon and MultiPolygon geometries to `ax`
@@ -133,6 +141,8 @@ def _plot_polygon_collection(
         Color to fill the polygons. Cannot be used together with `values`.
     color : single color or sequence of `N` colors
         Sets both `edgecolor` and `facecolor`
+    autolim : bool (default True)
+        Update axes data limits to contain the new geometries.
     **kwargs
         Additional keyword arguments passed to the collection
 
@@ -167,13 +177,21 @@ def _plot_polygon_collection(
         if "norm" not in kwargs:
             collection.set_clim(vmin, vmax)
 
-    ax.add_collection(collection, autolim=True)
+    ax.add_collection(collection, autolim=autolim)
     ax.autoscale_view()
     return collection
 
 
 def _plot_linestring_collection(
-    ax, geoms, values=None, color=None, cmap=None, vmin=None, vmax=None, **kwargs
+    ax,
+    geoms,
+    values=None,
+    color=None,
+    cmap=None,
+    vmin=None,
+    vmax=None,
+    autolim=True,
+    **kwargs,
 ):
     """
     Plots a collection of LineString and MultiLineString geometries to `ax`
@@ -189,6 +207,8 @@ def _plot_linestring_collection(
         have 1:1 correspondence with the geometries (not their components).
     color : single color or sequence of `N` colors
         Cannot be used together with `values`.
+    autolim : bool (default True)
+        Update axes data limits to contain the new geometries.
 
     Returns
     -------
@@ -222,7 +242,7 @@ def _plot_linestring_collection(
         if "norm" not in kwargs:
             collection.set_clim(vmin, vmax)
 
-    ax.add_collection(collection, autolim=True)
+    ax.add_collection(collection, autolim=autolim)
     ax.autoscale_view()
     return collection
 
@@ -291,7 +311,14 @@ def _plot_point_collection(
 
 
 def plot_series(
-    s, cmap=None, color=None, ax=None, figsize=None, aspect="auto", **style_kwds
+    s,
+    cmap=None,
+    color=None,
+    ax=None,
+    figsize=None,
+    aspect="auto",
+    autolim=True,
+    **style_kwds,
 ):
     """
     Plot a GeoSeries.
@@ -327,6 +354,8 @@ def plot_series(
         square appears square in the middle of the plot. This implies an
         Equirectangular projection. If None, the aspect of `ax` won't be changed. It can
         also be set manually (float) as the ratio of y-unit to x-unit.
+    autolim : bool (default True)
+        Update axes data limits to contain the new geometries.
     **style_kwds : dict
         Color options to be passed on to the actual plot function, such
         as ``edgecolor``, ``facecolor``, ``linewidth``, ``markersize``,
@@ -421,7 +450,13 @@ def plot_series(
 
         values_ = values[poly_idx] if cmap else None
         _plot_polygon_collection(
-            ax, polys, values_, facecolor=facecolor, cmap=cmap, **style_kwds
+            ax,
+            polys,
+            values_,
+            facecolor=facecolor,
+            cmap=cmap,
+            autolim=autolim,
+            **style_kwds,
         )
 
     # plot all LineStrings and MultiLineString components in same collection
@@ -431,7 +466,7 @@ def plot_series(
         color_ = expl_color[line_idx] if color_given else color
 
         _plot_linestring_collection(
-            ax, lines, values_, color=color_, cmap=cmap, **style_kwds
+            ax, lines, values_, color=color_, cmap=cmap, autolim=autolim, **style_kwds
         )
 
     # plot all Points in the same collection
@@ -468,6 +503,7 @@ def plot_dataframe(
     classification_kwds=None,
     missing_kwds=None,
     aspect="auto",
+    autolim=True,
     **style_kwds,
 ):
     """
@@ -571,7 +607,8 @@ def plot_dataframe(
         square appears square in the middle of the plot. This implies an
         Equirectangular projection. If None, the aspect of `ax` won't be changed. It can
         also be set manually (float) as the ratio of y-unit to x-unit.
-
+    autolim : bool (default True)
+        Update axes data limits to contain the new geometries.
     **style_kwds : dict
         Style options to be passed on to the actual plot function, such
         as ``edgecolor``, ``facecolor``, ``linewidth``, ``markersize``,
@@ -658,6 +695,7 @@ def plot_dataframe(
             figsize=figsize,
             markersize=markersize,
             aspect=aspect,
+            autolim=autolim,
             **style_kwds,
         )
 
@@ -797,7 +835,14 @@ def plot_dataframe(
     subset = values[poly_idx & np.invert(nan_idx)]
     if not polys.empty:
         _plot_polygon_collection(
-            ax, polys, subset, vmin=mn, vmax=mx, cmap=cmap, **style_kwds
+            ax,
+            polys,
+            subset,
+            vmin=mn,
+            vmax=mx,
+            cmap=cmap,
+            autolim=autolim,
+            **style_kwds,
         )
 
     # plot all LineStrings and MultiLineString components in same collection
@@ -805,7 +850,14 @@ def plot_dataframe(
     subset = values[line_idx & np.invert(nan_idx)]
     if not lines.empty:
         _plot_linestring_collection(
-            ax, lines, subset, vmin=mn, vmax=mx, cmap=cmap, **style_kwds
+            ax,
+            lines,
+            subset,
+            vmin=mn,
+            vmax=mx,
+            cmap=cmap,
+            autolim=autolim,
+            **style_kwds,
         )
 
     # plot all Points in the same collection


Test patch:
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -494,6 +494,28 @@ def setup_method(self):
         )
         self.df3 = GeoDataFrame({"geometry": self.linearrings, "values": values})
 
+    def test_autolim_false(self):
+        """Test linestring plot preserving axes limits."""
+        ax = self.lines[: self.N // 2].plot()
+        ylim = ax.get_ylim()
+        self.lines.plot(ax=ax, autolim=False)
+        assert ax.get_ylim() == ylim
+        ax = self.df[: self.N // 2].plot()
+        ylim = ax.get_ylim()
+        self.df.plot(ax=ax, autolim=False)
+        assert ax.get_ylim() == ylim
+
+    def test_autolim_true(self):
+        """Test linestring plot autoscaling axes limits."""
+        ax = self.lines[: self.N // 2].plot()
+        ylim = ax.get_ylim()
+        self.lines.plot(ax=ax, autolim=True)
+        assert ax.get_ylim() != ylim
+        ax = self.df[: self.N // 2].plot()
+        ylim = ax.get_ylim()
+        self.df.plot(ax=ax, autolim=True)
+        assert ax.get_ylim() != ylim
+
     def test_single_color(self):
         ax = self.lines.plot(color="green")
         _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
@@ -637,6 +659,28 @@ def setup_method(self):
         df_nan = GeoDataFrame({"geometry": t3, "values": [np.nan]})
         self.df3 = pd.concat([self.df, df_nan])
 
+    def test_autolim_false(self):
+        """Test polygon plot preserving axes limits."""
+        ax = self.polys[:1].plot()
+        xlim = ax.get_xlim()
+        self.polys.plot(ax=ax, autolim=False)
+        assert ax.get_xlim() == xlim
+        ax = self.df[:1].plot()
+        xlim = ax.get_xlim()
+        self.df.plot(ax=ax, autolim=False)
+        assert ax.get_xlim() == xlim
+
+    def test_autolim_true(self):
+        """Test polygon plot autoscaling axes limits."""
+        ax = self.polys[:1].plot()
+        xlim = ax.get_xlim()
+        self.polys.plot(ax=ax, autolim=True)
+        assert ax.get_xlim() != xlim
+        ax = self.df[:1].plot()
+        xlim = ax.get_xlim()
+        self.df.plot(ax=ax, autolim=True)
+        assert ax.get_xlim() != xlim
+
     def test_single_color(self):
         ax = self.polys.plot(color="green")
         _check_colors(2, ax.collections[0].get_facecolors(), ["green"] * 2)