Repository: geopandas/geopandas

Issue:
BUG: CRS lost after GeoSeries.shift()
- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest version of geopandas.

- [ ] (optional) I have confirmed this bug exists on the master branch of geopandas.

---

**Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) detailing how to provide the necessary information for us to reproduce your bug.

#### Code Sample, a copy-pastable example

```python
import pandas as pd
import geopandas as gpd
print(gpd.__version__)
df = pd.DataFrame(
    {'City': ['Buenos Aires', 'Brasilia', 'Santiago', 'Bogota', 'Caracas'],
     'Country': ['Argentina', 'Brazil', 'Chile', 'Colombia', 'Venezuela'],
     'Latitude': [-34.58, -15.78, -33.45, 4.60, 10.48],
     'Longitude': [-58.66, -47.91, -70.66, -74.08, -66.86]})
gdf = gpd.GeoDataFrame(
    df, geometry=gpd.points_from_xy(df.Longitude, df.Latitude),
    crs='epsg:4326')
gdf.geometry.distance(gdf.geometry.shift())
```

#### Problem description

The pandas `shift()` method should be supported by geopandas to also copy the CRS - thus, avoiding the missing CRS warning when computing distance.

#### Expected Output

Not missing CRS warning.

#### Output of ``geopandas.show_versions()``

<details>
SYSTEM INFO
-----------
python     : 3.8.2 (default, Mar 26 2020, 15:53:00)  [GCC 7.3.0]
executable : /home/fogh/miniconda3/envs/py38_v3/bin/python
machine    : Linux-5.4.0-48-generic-x86_64-with-glibc2.10

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.8.0
GEOS lib   : /home/fogh/miniconda3/envs/py38_v3/lib/libgeos_c.so
GDAL       : 3.0.2
GDAL data dir: /home/fogh/miniconda3/envs/py38_v3/share/gdal
PROJ       : 6.2.1
PROJ data dir: /home/fogh/miniconda3/envs/py38_v3/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.8.1
pandas     : 1.1.1
fiona      : 1.8.13.post1
numpy      : 1.19.1
shapely    : 1.7.1
rtree      : 0.9.4
pyproj     : 2.6.1.post1
matplotlib : 3.3.1
mapclassify: 2.3.0
geopy      : 2.0.0
psycopg2   : None
geoalchemy2: None
pyarrow    : 1.0.1
</details>


Discussion:
jorisvandenbossche: > On the longer term, we should probably check other methods as well / potentially think about a more general solution. For example, also unique looses the CRS.

Actually, taking a closer look, `unique` might be one of the only ones that also has this problem (it's basically everything that uses `_from_sequence` that would have this problem, because the CRS is not part of the dtype)
johnflavin: > > On the longer term, we should probably check other methods as well / potentially think about a more general solution. For example, also unique looses the CRS.
> 
> Actually, taking a closer look, `unique` might be one of the only ones that also has this problem (it's basically everything that uses `_from_sequence` that would have this problem, because the CRS is not part of the dtype)

Two questions:

1. I discussed an idea for this in the PR description:
    > give `_from_sequence` a class method implementation and an instance method implementation

    What do you think about that?
2. Whether we do a general fix in `_from_sequence` or just overriding `shift` and `unique`, should that also be part of this PR?
martinfleis: > give _from_sequence a class method implementation and an instance method implementation

I like the idea of fixing the issue directly where it comes from. Is this (easily) doable (haven't checked the actual pandas implementation)?

> Whether we do a general fix in _from_sequence or just overriding shift and unique, should that also be part of this PR?

If we go for `_from_sequence` fix that could replace the existing in this PR. If we just want to override `shift` and `unique` (which is also fine with me considering that these two seems to be the only problematic) then we can leave `unique` for a separate PR or do it in here. That's up to you.
jorisvandenbossche: > give _from_sequence a class method implementation and an instance method implementation


Hmm, yes, that could be possible (at least if python allows to define both). Checking in pandas, inside the base ExtensionArray implementation, we seem to always use `self._from_sequence(..)` (so calling on an instance) and not `type(self)._from_sequence(..)`. So having the special case instance should help for those cases. 

johnflavin: > > give _from_sequence a class method implementation and an instance method implementation
> 
> Hmm, yes, that could be possible (at least if python allows to define both).

My plan was to follow the approach in this SO answer: [Same name for classmethod and instancemethod?](https://stackoverflow.com/a/28238047). (Well, more specifically the first approach, since he does  give two.) I would replace the `@classmethod` decorator on `_from_sequence` with a custom `@class_or_instancemethod` decorator. Then inside `_from_sequence` I would check if it is being called from the class or an instance. If the latter, I would set the `crs` on the newly created array to be the `crs` of the instance. 

I’ve never done anything like that before. Hope it works!
johnflavin: Ok, I did the thing I outlined above, and it seems to work.

In `_from_sequence`, I have decorated it with `@class_or_instancemethod`. Now the first argument can be a class if the method is called on the class or an instance if called on the instance. If the latter, we get the `crs` from the instance and use it to construct the new array. This lets `shift` keep the crs without overriding it.

But `unique` was different. I tracked through the code and didn't see that it was using `_from_sequence`. Instead it uses `_from_factorized`. (`GeometryArray.unique` -> `pandas.core.algorithms:factorize` -> `pandas.core.arrays.base:factorize` -> `ExtensionArray._from_factorized` / `GeometryArray._from_factorized`). And I initially thought I would need to do the same `@class_or_instancemethod` thing there that I did with `_from_sequence`. But, actually, `_from_factorized` already has a parameter that is used as a reference to `self`, which it calls `original`. So I just got the crs from that, and `unique` is also fixed.

UPDATE
Except that [this failed test](https://github.com/geopandas/geopandas/pull/1744/checks?check_run_id=1710780187#step:6:2674) tells me that even with my `_from_factorized` modification `unique` does not keep its crs on pandas 0.25. I have looked through the code and can't figure out why.

FURTHER UPDATE
Ok, now I see what is happening. The crs for update is being lost when `pandas.core.algorithms:factorize` calls `_reconstruct_data`. In the [current implementation](https://github.com/pandas-dev/pandas/blob/master/pandas/core/algorithms.py#L214-L219) that will pass through the data when it is already an array with the right dtype, and if not it will call the ExtensionArray `_from_sequence` (as a class method, not an instance method). But in the [pandas 0.25 implementation](https://github.com/pandas-dev/pandas/blob/62a87bf4a2af02a8d3bc271ad26e5994292b8e6a/pandas/core/algorithms.py#L184) it always calls the classmethod `_from_sequence`. In fact, that change was only made [relatively recently](https://github.com/pandas-dev/pandas/pull/38120/files#diff-c8f3ad29eaf121537b999e88e9117f3e3702d0b818a67516da25093fe2890ce8L203) and released in pandas 1.1.5.

So I think I need to skip `test_unique_has_crs` for pandas < 1.1.5.
jorisvandenbossche: Thanks for tracking down the `unique` fix! 

For `shift` / `_from_sequence`, seeing the code you pushed now, I get the feeling that maybe the simple override of the ``shift`` method as you had before is a bit cleaner / easier to follow?
johnflavin: > I get the feeling that maybe the simple override of the `shift` method as you had before is a bit cleaner / easier to follow?

I agree, that was easier to follow. I can revert the `class_or_instancemethod` decorator changes. (Even though it was fun to figure out how to do it.)

Patch:
diff --git a/.gitignore b/.gitignore
index 8eefebdf..5e9c00e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,6 +34,7 @@ nosetests.xml
 coverage.xml
 *.cover
 .hypothesis/
+result_images
 
 # Sphinx documentation
 doc/_build/
diff --git a/doc/source/docs/user_guide.rst b/doc/source/docs/user_guide.rst
index 02b0210e..1086f58b 100644
--- a/doc/source/docs/user_guide.rst
+++ b/doc/source/docs/user_guide.rst
@@ -13,7 +13,7 @@ Advanced topics can be found in the :doc:`Advanced Guide <advanced_guide>` and f
   Data Structures <user_guide/data_structures>
   Reading and Writing Files <user_guide/io>
   Indexing and Selecting Data <user_guide/indexing>
-  Making Maps <user_guide/mapping>
+  Making Maps and plots <user_guide/mapping>
   Managing Projections <user_guide/projections>
   Geometric Manipulations <user_guide/geometric_manipulations>
   Set Operations with overlay <user_guide/set_operations>
diff --git a/doc/source/docs/user_guide/mapping.rst b/doc/source/docs/user_guide/mapping.rst
index dbad330f..f2a96b5a 100644
--- a/doc/source/docs/user_guide/mapping.rst
+++ b/doc/source/docs/user_guide/mapping.rst
@@ -11,7 +11,7 @@
    plt.close('all')
 
 
-Mapping Tools
+Mapping and Plotting Tools
 =========================================
 
 
@@ -222,6 +222,41 @@ We can set the ``zorder`` for cities higher than for world to move it of top.
     @savefig zorder_set.png
     world.plot(ax=ax, zorder=1);
 
+
+Pandas Plots
+-----------------
+
+Plotting methods also allow for different plot styles from pandas
+along with the default ``geo`` plot. These methods can be accessed using
+the ``kind`` keyword argument in :meth:`~GeoDataFrame.plot`, and include:
+
+* ``geo`` for mapping
+* ``line`` for line plots
+* ``bar`` or ``barh`` for bar plots
+* ``hist`` for histogram
+* ``box`` for boxplot
+* ``kde`` or ``density`` for density plots
+* ``area``  for area plots
+* ``scatter`` for scatter plots
+* ``hexbin`` for hexagonal bin plots
+* ``pie`` for pie plots
+
+.. ipython:: python
+
+    gdf = world.head(10)
+    @savefig pandas_line_plot.png
+    gdf.plot(kind='scatter', x="pop_est", y="gdp_md_est")
+
+You can also create these other plots using the ``GeoDataFrame.plot.<kind>`` accessor methods instead of providing the ``kind`` keyword argument.
+
+.. ipython:: python
+
+    @savefig pandas_bar_plot.png
+    gdf.plot.bar()
+
+For more information check out the `pandas documentation <https://pandas.pydata.org/pandas-docs/stable/user_guide/visualization.html>`_.
+
+
 Other Resources
 -----------------
 Links to jupyter Notebooks for different mapping tasks:
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index 2be985b3..635aeccb 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -17,6 +17,7 @@ import shapely.geos
 PANDAS_GE_025 = str(pd.__version__) >= LooseVersion("0.25.0")
 PANDAS_GE_10 = str(pd.__version__) >= LooseVersion("1.0.0")
 PANDAS_GE_11 = str(pd.__version__) >= LooseVersion("1.1.0")
+PANDAS_GE_115 = str(pd.__version__) >= LooseVersion("1.1.5")
 PANDAS_GE_12 = str(pd.__version__) >= LooseVersion("1.2.0")
 
 
diff --git a/geopandas/array.py b/geopandas/array.py
index c0a67971..a25e01ba 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1077,6 +1077,41 @@ class GeometryArray(ExtensionArray):
     def nbytes(self):
         return self.data.nbytes
 
+    def shift(self, periods=1, fill_value=None):
+        """
+        Shift values by desired number.
+
+        Newly introduced missing values are filled with
+        ``self.dtype.na_value``.
+
+        Parameters
+        ----------
+        periods : int, default 1
+            The number of periods to shift. Negative values are allowed
+            for shifting backwards.
+
+        fill_value : object, optional (default None)
+            The scalar value to use for newly introduced missing values.
+            The default is ``self.dtype.na_value``.
+
+        Returns
+        -------
+        GeometryArray
+            Shifted.
+
+        Notes
+        -----
+        If ``self`` is empty or ``periods`` is 0, a copy of ``self`` is
+        returned.
+
+        If ``periods > len(self)``, then an array of size
+        len(self) is returned, with all values filled with
+        ``self.dtype.na_value``.
+        """
+        shifted = super(GeometryArray, self).shift(periods, fill_value)
+        shifted.crs = self.crs
+        return shifted
+
     # -------------------------------------------------------------------------
     # ExtensionArray specific
     # -------------------------------------------------------------------------
@@ -1142,7 +1177,7 @@ class GeometryArray(ExtensionArray):
         pandas.factorize
         ExtensionArray.factorize
         """
-        return from_wkb(values)
+        return from_wkb(values, crs=original.crs)
 
     def _values_for_argsort(self):
         # type: () -> np.ndarray
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 78fc3a34..d3d18d77 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1312,20 +1312,6 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
 
         return self
 
-    def plot(self, *args, **kwargs):
-        """Generate a plot of the geometries in the ``GeoDataFrame``.
-
-        If the ``column`` parameter is given, colors plot according to values
-        in that column, otherwise calls ``GeoSeries.plot()`` on the
-        ``geometry`` column.
-
-        Wraps the ``plot_dataframe()`` function, and documentation is copied
-        from there.
-        """
-        return plot_dataframe(self, *args, **kwargs)
-
-    plot.__doc__ = plot_dataframe.__doc__
-
     def dissolve(self, by=None, aggfunc="first", as_index=True):
         """
         Dissolve geometries within `groupby` into single observation.
@@ -1617,6 +1603,24 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
         )
         return self.geometry.difference(other)
 
+    if compat.PANDAS_GE_025:
+        from pandas.core.accessor import CachedAccessor
+
+        plot = CachedAccessor("plot", geopandas.plotting.GeoplotAccessor)
+    else:
+
+        def plot(self, *args, **kwargs):
+            """Generate a plot of the geometries in the ``GeoDataFrame``.
+            If the ``column`` parameter is given, colors plot according to values
+            in that column, otherwise calls ``GeoSeries.plot()`` on the
+            ``geometry`` column.
+            Wraps the ``plot_dataframe()`` function, and documentation is copied
+            from there.
+            """
+            return plot_dataframe(self, *args, **kwargs)
+
+    plot.__doc__ = plot_dataframe.__doc__
+
 
 def _dataframe_set_geometry(self, col, drop=False, inplace=False, crs=None):
     if inplace:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 9c395685..7159aa2d 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -115,7 +115,7 @@ def _PolygonPatch(polygon, **kwargs):
 
     path = Path.make_compound_path(
         Path(np.asarray(polygon.exterior.coords)[:, :2]),
-        *[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors]
+        *[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors],
     )
     return PathPatch(path, **kwargs)
 
@@ -254,7 +254,7 @@ def _plot_point_collection(
     vmax=None,
     marker="o",
     markersize=None,
-    **kwargs
+    **kwargs,
 ):
     """
     Plots a collection of Point and MultiPoint geometries to `ax`
@@ -488,7 +488,7 @@ def plot_dataframe(
     classification_kwds=None,
     missing_kwds=None,
     aspect="auto",
-    **style_kwds
+    **style_kwds,
 ):
     """
     Plot a GeoDataFrame.
@@ -508,6 +508,21 @@ def plot_dataframe(
         If np.array or pd.Series are used then it must have same length as
         dataframe. Values are used to color the plot. Ignored if `color` is
         also set.
+    kind: str
+        The kind of plots to produce:
+         - 'geo': Map (default)
+         Pandas Kinds
+         - 'line' : line plot
+         - 'bar' : vertical bar plot
+         - 'barh' : horizontal bar plot
+         - 'hist' : histogram
+         - 'box' : BoxPlot
+         - 'kde' : Kernel Density Estimation plot
+         - 'density' : same as 'kde'
+         - 'area' : area plot
+         - 'pie' : pie plot
+         - 'scatter' : scatter plot
+         - 'hexbin' : hexbin plot.
     cmap : str (default None)
         The name of a colormap recognized by matplotlib.
     color : str (default None)
@@ -683,7 +698,7 @@ GON (((-122.84000 49.00000, -120.0000...
             figsize=figsize,
             markersize=markersize,
             aspect=aspect,
-            **style_kwds
+            **style_kwds,
         )
 
     # To accept pd.Series and np.arrays as column
@@ -820,7 +835,7 @@ GON (((-122.84000 49.00000, -120.0000...
             vmax=mx,
             markersize=markersize,
             cmap=cmap,
-            **style_kwds
+            **style_kwds,
         )
 
     if missing_kwds is not None and not expl_series[nan_idx].empty:
@@ -899,6 +914,30 @@ GON (((-122.84000 49.00000, -120.0000...
     return ax
 
 
+if geopandas._compat.PANDAS_GE_025:
+    from pandas.plotting import PlotAccessor
+
+    class GeoplotAccessor(PlotAccessor):
+
+        __doc__ = plot_dataframe.__doc__
+        _pandas_kinds = PlotAccessor._all_kinds
+
+        def __call__(self, *args, **kwargs):
+            data = self._parent.copy()
+            kind = kwargs.pop("kind", "geo")
+            if kind == "geo":
+                return plot_dataframe(data, *args, **kwargs)
+            if kind in self._pandas_kinds:
+                # Access pandas plots
+                return PlotAccessor(data)(kind=kind, **kwargs)
+            else:
+                # raise error
+                raise ValueError(f"{kind} is not a valid plot kind")
+
+        def geo(self, *args, **kwargs):
+            return self(kind="geo", *args, **kwargs)
+
+
 def _mapclassify_choro(values, scheme, **classification_kwds):
     """
     Wrapper for choropleth schemes from mapclassify for use with plot_dataframe


Test patch:
diff --git a/ci/envs/37-latest-conda-forge.yaml b/ci/envs/37-latest-conda-forge.yaml
index a0d148ac..1ca12938 100644
--- a/ci/envs/37-latest-conda-forge.yaml
+++ b/ci/envs/37-latest-conda-forge.yaml
@@ -18,6 +18,7 @@ dependencies:
   - rtree
   - matplotlib
   - mapclassify
+  - scipy
   - geopy
   - SQLalchemy
   - libspatialite
diff --git a/ci/envs/38-latest-conda-forge.yaml b/ci/envs/38-latest-conda-forge.yaml
index 5ebdc26a..a9ecb787 100644
--- a/ci/envs/38-latest-conda-forge.yaml
+++ b/ci/envs/38-latest-conda-forge.yaml
@@ -18,6 +18,7 @@ dependencies:
   - rtree
   - matplotlib
   - mapclassify
+  - scipy
   - geopy
   # installed in tests.yaml, because not available on windows
   # - postgis
diff --git a/ci/envs/39-latest-conda-forge.yaml b/ci/envs/39-latest-conda-forge.yaml
index 1f856524..bb8c8f35 100644
--- a/ci/envs/39-latest-conda-forge.yaml
+++ b/ci/envs/39-latest-conda-forge.yaml
@@ -19,6 +19,7 @@ dependencies:
   - matplotlib
   - descartes
   - mapclassify
+  - scipy
   - geopy
   # installed in tests.yaml, because not available on windows
   # - postgis
diff --git a/geopandas/tests/test_array.py b/geopandas/tests/test_array.py
index fcc5ec15..ef3cd143 100644
--- a/geopandas/tests/test_array.py
+++ b/geopandas/tests/test_array.py
@@ -886,6 +886,23 @@ def test_isna_pdNA():
     assert t1[0] is None
 
 
+def test_shift_has_crs():
+    t = T.copy()
+    t.crs = 4326
+    assert t.shift(1).crs == t.crs
+    assert t.shift(0).crs == t.crs
+    assert t.shift(-1).crs == t.crs
+
+
+@pytest.mark.skipif(
+    not compat.PANDAS_GE_115, reason="crs only preserved in unique after pandas 1.1.5"
+)
+def test_unique_has_crs():
+    t = T.copy()
+    t.crs = 4326
+    assert t.unique().crs == t.crs
+
+
 class TestEstimateUtmCrs:
     def setup_method(self):
         self.esb = shapely.geometry.Point(-73.9847, 40.7484)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 2f59958f..884d64ee 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -29,6 +29,13 @@ matplotlib = pytest.importorskip("matplotlib")
 matplotlib.use("Agg")
 import matplotlib.pyplot as plt  # noqa
 
+try:  # skipif and importorskip do not work for decorators
+    from matplotlib.testing.decorators import check_figures_equal
+
+    MPL_DECORATORS = True
+except ImportError:
+    MPL_DECORATORS = False
+
 
 @pytest.fixture(autouse=True)
 def close_figures(request):
@@ -42,6 +49,8 @@ try:
 except KeyError:
     MPL_DFT_COLOR = matplotlib.rcParams["axes.color_cycle"][0]
 
+plt.rcParams.update({"figure.max_open_warning": 0})
+
 
 class TestPointPlotting:
     def setup_method(self):
@@ -1466,6 +1475,79 @@ class TestPlotCollections:
         ax.cla()
 
 
+@pytest.mark.skipif(not compat.PANDAS_GE_025, reason="requires pandas > 0.24")
+class TestGeoplotAccessor:
+    def setup_method(self):
+        geometries = [Polygon([(0, 0), (1, 0), (1, 1)]), Point(1, 3)]
+        x = [1, 2]
+        y = [10, 20]
+        self.gdf = GeoDataFrame({"geometry": geometries, "x": x, "y": y})
+        self.df = pd.DataFrame({"x": x, "y": y})
+
+    def compare_figures(self, kind, fig_test, fig_ref, kwargs):
+        """Compare Figures."""
+        ax_pandas_1 = fig_test.subplots()
+        self.df.plot(kind=kind, ax=ax_pandas_1, **kwargs)
+        ax_geopandas_1 = fig_ref.subplots()
+        self.gdf.plot(kind=kind, ax=ax_geopandas_1, **kwargs)
+
+        ax_pandas_2 = fig_test.subplots()
+        getattr(self.df.plot, kind)(ax=ax_pandas_2, **kwargs)
+        ax_geopandas_2 = fig_ref.subplots()
+        getattr(self.gdf.plot, kind)(ax=ax_geopandas_2, **kwargs)
+
+    _pandas_kinds = []
+    if compat.PANDAS_GE_025:
+        from geopandas.plotting import GeoplotAccessor
+
+        _pandas_kinds = GeoplotAccessor._pandas_kinds
+
+    if MPL_DECORATORS:
+
+        @pytest.mark.parametrize("kind", _pandas_kinds)
+        @check_figures_equal(extensions=["png", "pdf"])
+        def test_pandas_kind(self, kind, fig_test, fig_ref):
+            """Test Pandas kind."""
+            import importlib
+
+            _scipy_dependent_kinds = ["kde", "density"]  # Needs scipy
+            _y_kinds = ["pie"]  # Needs y
+            _xy_kinds = ["scatter", "hexbin"]  # Needs x & y
+            kwargs = {}
+            if kind in _scipy_dependent_kinds:
+                if not importlib.util.find_spec("scipy"):
+                    with pytest.raises(
+                        ModuleNotFoundError, match="No module named 'scipy'"
+                    ):
+                        self.gdf.plot(kind=kind)
+            elif kind in _y_kinds:
+                kwargs = {"y": "y"}
+            elif kind in _xy_kinds:
+                kwargs = {"x": "x", "y": "y"}
+
+            self.compare_figures(kind, fig_test, fig_ref, kwargs)
+            plt.close("all")
+
+        @check_figures_equal(extensions=["png", "pdf"])
+        def test_geo_kind(self, fig_test, fig_ref):
+            """Test Geo kind."""
+            ax1 = fig_test.subplots()
+            self.gdf.plot(ax=ax1)
+            ax2 = fig_ref.subplots()
+            getattr(self.gdf.plot, "geo")(ax=ax2)
+            plt.close("all")
+
+    def test_invalid_kind(self):
+        """Test invalid kinds."""
+        with pytest.raises(ValueError, match="error is not a valid plot kind"):
+            self.gdf.plot(kind="error")
+        with pytest.raises(
+            AttributeError,
+            match="'GeoplotAccessor' object has no attribute 'error'",
+        ):
+            self.gdf.plot.error()
+
+
 def test_column_values():
     """
     Check that the dataframe plot method returns same values with an