Repository: geopandas/geopandas

Issue:
DOC: add explanation of using zorder to control which layers are on top
See eg https://stackoverflow.com/questions/59052898/why-are-line-plots-in-geopandas-ignoring-plot-order-when-it-comes-to-display-lay (@martinfleis was faster in answering ;))

We should add some explanation to the mapping docs on this.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1228?src=pr&el=h1) Report
> Merging [#1228](https://codecov.io/gh/geopandas/geopandas/pull/1228?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/22b5a0dc23be8876a3270d0c009db5bd765fabf3?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/geopandas/geopandas/pull/1228/graphs/tree.svg?width=650&token=6TOPMcCBhB&height=150&src=pr)](https://codecov.io/gh/geopandas/geopandas/pull/1228?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1228   +/-   ##
=======================================
  Coverage   89.89%   89.89%           
=======================================
  Files          21       21           
  Lines        2088     2088           
=======================================
  Hits         1877     1877           
  Misses        211      211
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1228?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1228?src=pr&el=footer). Last update [22b5a0d...a75a3c3](https://codecov.io/gh/geopandas/geopandas/pull/1228?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/doc/source/mapping.rst b/doc/source/mapping.rst
index 9fa4baf8..c278db20 100644
--- a/doc/source/mapping.rst
+++ b/doc/source/mapping.rst
@@ -200,6 +200,27 @@ Before combining maps, however, remember to always ensure they share a common CR
     @savefig capitals_over_countries_2.png
     plt.show();
 
+Control the order of multiple layers in a plot
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When plotting multiple layers, use ``zorder`` to take control of the order of layers being plotted.
+The lower the ``zorder`` is, the lower the layer is on the map and vice versa.
+
+Without specified ``zorder``, cities (Points) gets plotted below world (Polygons), following the default order based on geometry types.
+
+.. ipython:: python
+
+    ax = cities.plot(color='k')
+    @savefig zorder_default.png
+    world.plot(ax=ax);
+
+We can set the ``zorder`` for cities higher than for world to move it of top.
+
+.. ipython:: python
+
+    ax = cities.plot(color='k', zorder=2)
+    @savefig zorder_set.png
+    world.plot(ax=ax, zorder=1);
 
 Other Resources
 -----------------
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index f7e63120..8fefd478 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -123,10 +123,10 @@ def to_file(df, filename, driver="ESRI Shapefile", schema=None, **kwargs):
 
 
 def infer_schema(df):
-    try:
-        from collections import OrderedDict
-    except ImportError:
-        from ordereddict import OrderedDict
+    from collections import OrderedDict
+
+    # TODO: test pandas string type and boolean type once released
+    types = {"Int64": "int", "string": "str", "boolean": "bool"}
 
     def convert_type(column, in_type):
         if in_type == object:
@@ -134,7 +134,10 @@ def infer_schema(df):
         if in_type.name.startswith("datetime64"):
             # numpy datetime type regardless of frequency
             return "datetime"
-        out_type = type(np.zeros(1, in_type).item()).__name__
+        if str(in_type) in types:
+            out_type = types[str(in_type)]
+        else:
+            out_type = type(np.zeros(1, in_type).item()).__name__
         if out_type == "long":
             out_type = "int"
         if not _FIONA18 and out_type == "bool":
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index c86509aa..f915a569 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -3,45 +3,41 @@ import warnings
 import numpy as np
 import pandas as pd
 
+import geopandas
 
-def _flatten_multi_geoms(geoms, colors=None):
+
+def _flatten_multi_geoms(geoms, prefix="Multi"):
     """
-    Returns Series like geoms and colors, except that any Multi geometries
-    are split into their components and colors are repeated for all component
-    in the same Multi geometry.  Maintains 1:1 matching of geometry to color.
-    Passing `color` is optional, and when no `color` is passed a list of None
-    values is returned as `component_colors`.
+    Returns Series like geoms and index, except that any Multi geometries
+    are split into their components and indices are repeated for all component
+    in the same Multi geometry.  Maintains 1:1 matching of geometry to value.
 
-    "Colors" are treated opaquely and so can actually contain any values.
+    Prefix specifies type of geometry to be flatten. 'Multi' for MultiPoint and similar,
+    "Geom" for GeometryCollection.
 
     Returns
     -------
 
     components : list of geometry
 
-    component_colors : list of whatever type `colors` contains
+    component_index : index array
+        indices are repeated for all components in the same Multi geometry
     """
-    if colors is None:
-        colors = [None] * len(geoms)
-
-    components, component_colors = [], []
+    components, component_index = [], []
 
-    if not geoms.geom_type.str.startswith("Multi").any():
-        return geoms, colors
+    if not geoms.geom_type.str.startswith(prefix).any():
+        return geoms, np.arange(len(geoms))
 
-    # precondition, so zip can't short-circuit
-    assert len(geoms) == len(colors)
-    for geom, color in zip(geoms, colors):
-        if geom.type.startswith("Multi"):
+    for ix, geom in enumerate(geoms):
+        if geom.type.startswith(prefix):
             for poly in geom:
                 components.append(poly)
-                # repeat same color for all components
-                component_colors.append(color)
+                component_index.append(ix)
         else:
             components.append(geom)
-            component_colors.append(color)
+            component_index.append(ix)
 
-    return components, component_colors
+    return components, np.array(component_index)
 
 
 def plot_polygon_collection(
@@ -92,7 +88,7 @@ def plot_polygon_collection(
     from matplotlib.collections import PatchCollection
     from matplotlib.colors import is_color_like
 
-    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
@@ -165,7 +161,7 @@ def plot_linestring_collection(
     from matplotlib.collections import LineCollection
     from matplotlib.colors import is_color_like
 
-    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
@@ -236,7 +232,7 @@ def plot_point_collection(
     if values is not None and color is not None:
         raise ValueError("Can only specify one of 'values' and 'color' kwargs")
 
-    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
@@ -349,16 +345,22 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         style_kwds["vmin"] = style_kwds.get("vmin", values.min())
         style_kwds["vmax"] = style_kwds.get("vmax", values.max())
 
-    geom_types = s.geometry.type
+    # decompose GeometryCollections
+    geoms, multiindex = _flatten_multi_geoms(s.geometry, prefix="Geom")
+    values = np.take(values, multiindex, axis=0) if cmap else None
+    expl_series = geopandas.GeoSeries(geoms)
+
+    geom_types = expl_series.type
     poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
     line_idx = np.asarray(
-        (geom_types == "LineString") | (geom_types == "MultiLineString")
+        (geom_types == "LineString")
+        | (geom_types == "MultiLineString")
+        | (geom_types == "LinearRing")
     )
     point_idx = np.asarray((geom_types == "Point") | (geom_types == "MultiPoint"))
 
     # plot all Polygons and all MultiPolygon components in the same collection
-    polys = s.geometry[poly_idx]
-
+    polys = expl_series[poly_idx]
     if not polys.empty:
         # color overrides both face and edgecolor. As we want people to be
         # able to use edgecolor as well, pass color to facecolor
@@ -372,7 +374,7 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         )
 
     # plot all LineStrings and MultiLineString components in same collection
-    lines = s.geometry[line_idx]
+    lines = expl_series[line_idx]
     if not lines.empty:
         values_ = values[line_idx] if cmap else None
         plot_linestring_collection(
@@ -380,7 +382,7 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         )
 
     # plot all Points in the same collection
-    points = s.geometry[point_idx]
+    points = expl_series[point_idx]
     if not points.empty:
         values_ = values[point_idx] if cmap else None
         plot_point_collection(ax, points, values_, color=color, cmap=cmap, **style_kwds)
@@ -595,15 +597,23 @@ def plot_dataframe(
     mn = values[~np.isnan(values)].min() if vmin is None else vmin
     mx = values[~np.isnan(values)].max() if vmax is None else vmax
 
-    geom_types = df.geometry.type
+    # decompose GeometryCollections
+    geoms, multiindex = _flatten_multi_geoms(df.geometry, prefix="Geom")
+    values = np.take(values, multiindex, axis=0)
+    nan_idx = np.take(nan_idx, multiindex, axis=0)
+    expl_series = geopandas.GeoSeries(geoms)
+
+    geom_types = expl_series.type
     poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
     line_idx = np.asarray(
-        (geom_types == "LineString") | (geom_types == "MultiLineString")
+        (geom_types == "LineString")
+        | (geom_types == "MultiLineString")
+        | (geom_types == "LinearRing")
     )
     point_idx = np.asarray((geom_types == "Point") | (geom_types == "MultiPoint"))
 
     # plot all Polygons and all MultiPolygon components in the same collection
-    polys = df.geometry[poly_idx & np.invert(nan_idx)]
+    polys = expl_series[poly_idx & np.invert(nan_idx)]
     subset = values[poly_idx & np.invert(nan_idx)]
     if not polys.empty:
         plot_polygon_collection(
@@ -611,7 +621,7 @@ def plot_dataframe(
         )
 
     # plot all LineStrings and MultiLineString components in same collection
-    lines = df.geometry[line_idx & np.invert(nan_idx)]
+    lines = expl_series[line_idx & np.invert(nan_idx)]
     subset = values[line_idx & np.invert(nan_idx)]
     if not lines.empty:
         plot_linestring_collection(
@@ -619,10 +629,11 @@ def plot_dataframe(
         )
 
     # plot all Points in the same collection
-    points = df.geometry[point_idx & np.invert(nan_idx)]
+    points = expl_series[point_idx & np.invert(nan_idx)]
     subset = values[point_idx & np.invert(nan_idx)]
     if not points.empty:
         if isinstance(markersize, np.ndarray):
+            markersize = np.take(markersize, multiindex, axis=0)
             markersize = markersize[point_idx & np.invert(nan_idx)]
         plot_point_collection(
             ax,
@@ -643,7 +654,7 @@ def plot_dataframe(
         merged_kwds = style_kwds.copy()
         merged_kwds.update(missing_kwds)
 
-        plot_series(df.geometry[nan_idx], ax=ax, **merged_kwds)
+        plot_series(expl_series[nan_idx], ax=ax, **merged_kwds)
 
     if legend and not color:
 


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index 6074519d..fd53efc8 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -7,11 +7,13 @@ import sys
 
 import fiona
 import numpy as np
+import pandas as pd
 from shapely.geometry import Point, Polygon, box
 
 import geopandas
 from geopandas import GeoDataFrame, read_file
 from geopandas.io.file import fiona_env, _FIONA18
+from geopandas._compat import PANDAS_GE_024
 
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 from geopandas.tests.util import PACKAGE_DIR, validate_boro_df
@@ -183,6 +185,17 @@ def test_to_file_types(tmpdir, df_points):
     df.to_file(tempfilename)
 
 
+@pytest.mark.skipif(not PANDAS_GE_024, reason="pandas >= 0.24 needed")
+def test_to_file_int64(tmpdir, df_points):
+    tempfilename = os.path.join(str(tmpdir), "int64.shp")
+    geometry = df_points.geometry
+    df = GeoDataFrame(geometry=geometry)
+    df["data"] = pd.array([1, np.nan] * 5, dtype=pd.Int64Dtype())
+    df.to_file(tempfilename)
+    df_read = GeoDataFrame.from_file(tempfilename)
+    assert_geodataframe_equal(df_read, df, check_dtype=False, check_like=True)
+
+
 def test_to_file_empty(tmpdir):
     input_empty_df = GeoDataFrame()
     tempfilename = os.path.join(str(tmpdir), "test.shp")
diff --git a/geopandas/io/tests/test_file_geom_types_drivers.py b/geopandas/io/tests/test_file_geom_types_drivers.py
index f5143c79..a581953d 100644
--- a/geopandas/io/tests/test_file_geom_types_drivers.py
+++ b/geopandas/io/tests/test_file_geom_types_drivers.py
@@ -287,19 +287,7 @@ def geodataframe(request):
     return request.param
 
 
-@pytest.fixture(
-    params=[
-        "GeoJSON",
-        "ESRI Shapefile",
-        pytest.param(
-            "GPKG",
-            marks=pytest.mark.skipif(
-                (sys.version_info < (3, 0)) and sys.platform.startswith("win"),
-                reason="GPKG tests failing on AppVeyor for Python 2.7",
-            ),
-        ),
-    ]
-)
+@pytest.fixture(params=["GeoJSON", "ESRI Shapefile", "GPKG"])
 def ogr_driver(request):
     return request.param
 
diff --git a/geopandas/io/tests/test_infer_schema.py b/geopandas/io/tests/test_infer_schema.py
index ac58353a..c077254e 100644
--- a/geopandas/io/tests/test_infer_schema.py
+++ b/geopandas/io/tests/test_infer_schema.py
@@ -9,8 +9,12 @@ from shapely.geometry import (
     Polygon,
 )
 
+import pandas as pd
+import numpy as np
+import pytest
 from geopandas import GeoDataFrame
 from geopandas.io.file import _FIONA18, infer_schema
+from geopandas._compat import PANDAS_GE_024
 
 # Credit: Polygons below come from Montreal city Open Data portal
 # http://donnees.ville.montreal.qc.ca/dataset/unites-evaluation-fonciere
@@ -313,3 +317,15 @@ def test_infer_schema_null_geometry_all():
     # None geometry type in then replaced by 'Unknown'
     # (default geometry type supported by Fiona)
     assert infer_schema(df) == {"geometry": "Unknown", "properties": OrderedDict()}
+
+
+@pytest.mark.skipif(not PANDAS_GE_024, reason="pandas >= 0.24 needed")
+def test_infer_schema_int64():
+    int64col = pd.array([1, np.nan], dtype=pd.Int64Dtype())
+    df = GeoDataFrame(geometry=[city_hall_entrance, city_hall_balcony])
+    df["int64"] = int64col
+
+    assert infer_schema(df) == {
+        "geometry": "Point",
+        "properties": OrderedDict([("int64", "int")]),
+    }
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index a6b66201..306e3b6c 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -8,9 +8,11 @@ from shapely.geometry import (
     MultiPolygon,
     Polygon,
     LineString,
+    LinearRing,
     Point,
     MultiPoint,
     MultiLineString,
+    GeometryCollection,
 )
 
 
@@ -294,6 +296,12 @@ class TestLineStringPlotting:
             {"geometry": [multiline1, multiline2], "values": [0, 1]}
         )
 
+        self.linearrings = GeoSeries(
+            [LinearRing([(0, i), (4, i + 0.5), (9, i)]) for i in range(self.N)],
+            index=list("ABCDEFGHIJ"),
+        )
+        self.df3 = GeoDataFrame({"geometry": self.linearrings, "values": values})
+
     def test_single_color(self):
 
         ax = self.lines.plot(color="green")
@@ -302,6 +310,12 @@ class TestLineStringPlotting:
         ax = self.df.plot(color="green")
         _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
 
+        ax = self.linearrings.plot(color="green")
+        _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
+
+        ax = self.df3.plot(color="green")
+        _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
+
         # check rgba tuple GH1178
         ax = self.df.plot(color=(0.5, 0.5, 0.5, 0.5))
         _check_colors(
@@ -552,6 +566,37 @@ class TestPolygonZPlotting:
         self.df.plot()
 
 
+class TestGeometryCollectionPlotting:
+    def setup_method(self):
+        coll1 = GeometryCollection(
+            [
+                Polygon([(1, 0), (2, 0), (2, 1)]),
+                MultiLineString([((0.5, 0.5), (1, 1)), ((1, 0.5), (1.5, 1))]),
+            ]
+        )
+        coll2 = GeometryCollection(
+            [Point(0.75, 0.25), Polygon([(2, 2), (3, 2), (2, 3)])]
+        )
+
+        self.series = GeoSeries([coll1, coll2])
+        self.df = GeoDataFrame({"geometry": self.series, "values": [1, 2]})
+
+    def test_colors(self):
+        # default uniform color
+        ax = self.series.plot()
+        _check_colors(1, ax.collections[0].get_facecolors(), [MPL_DFT_COLOR])  # poly
+        _check_colors(2, ax.collections[1].get_edgecolors(), [MPL_DFT_COLOR])  # line
+        _check_colors(2, ax.collections[2].get_facecolors(), [MPL_DFT_COLOR])  # point
+
+    def test_values(self):
+        ax = self.df.plot("values")
+        cmap = plt.get_cmap()
+        exp_colors = cmap(np.arange(2) / 1)
+        _check_colors(1, ax.collections[0].get_facecolors(), exp_colors)  # poly
+        _check_colors(2, ax.collections[1].get_edgecolors(), [exp_colors[0]])  # line
+        _check_colors(2, ax.collections[2].get_facecolors(), [exp_colors[1]])  # point
+
+
 class TestNonuniformGeometryPlotting:
     def setup_method(self):
         pytest.importorskip("matplotlib", "1.5.0")