mirror of
https://github.com/wassname/geopandas.git
synced 2026-09-21 13:00:14 +08:00
ENH: Add GeoPlot accessor (#1465)
Co-authored-by: Martin Fleischmann <martin@martinfleischmann.net> Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
This commit is contained in:
co-authored by
Martin Fleischmann
Joris Van den Bossche
parent
6e8f6f91cb
commit
e0981ab14e
@@ -34,6 +34,7 @@ nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
result_images
|
||||
|
||||
# Sphinx documentation
|
||||
doc/_build/
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies:
|
||||
- rtree
|
||||
- matplotlib
|
||||
- mapclassify
|
||||
- scipy
|
||||
- geopy
|
||||
- SQLalchemy
|
||||
- libspatialite
|
||||
|
||||
@@ -18,6 +18,7 @@ dependencies:
|
||||
- rtree
|
||||
- matplotlib
|
||||
- mapclassify
|
||||
- scipy
|
||||
- geopy
|
||||
# installed in tests.yaml, because not available on windows
|
||||
# - postgis
|
||||
|
||||
@@ -19,6 +19,7 @@ dependencies:
|
||||
- matplotlib
|
||||
- descartes
|
||||
- mapclassify
|
||||
- scipy
|
||||
- geopy
|
||||
# installed in tests.yaml, because not available on windows
|
||||
# - postgis
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
+18
-14
@@ -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:
|
||||
|
||||
+44
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user