Repository: geopandas/geopandas

Issue:
Colors provided to plot color argument do not match patches
A geopandas dataframe has the option to plot the frame. This also has a color argument which can be provided a list of colors.

The colors do however not match the patches in each row, even though the list of colors provided is the same length as the number of rows in the geopandas data frame. 

Running the following code using the attached geojson file in [bug_df.zip](https://github.com/geopandas/geopandas/files/3451023/bug_df.zip) shows this :
```
import geopandas as gpd
import matplotlib.pyplot as plt
plot_df = gpd.read_file('bug_df.geojson')
plot_df['coords'] = plot_df['geometry'].apply(lambda x: \
                                    x.representative_point().coords[:][0])

fig, ax = plt.subplots(figsize=[15,15])
plot_df.plot(ax=ax, color=plot_df['color'])

for _, row in plot_df.iterrows():
    ax.annotate(s=row['color'], xy=row['coords'],
                horizontalalignment='center',
                color=row['color'])
plt.title('Wrong coloring')

fig, ax = plt.subplots(figsize=[15,15])

for col in plot_df['color'].unique():
    plot_df[plot_df['color']==col].plot(ax=ax, color=col)
plt.title('Correct coloring');
```

The output of this code shows that the colors for the patches do not match the expected color:
![image](https://user-images.githubusercontent.com/7702207/62202810-49693080-b38a-11e9-941f-c54472218887.png)

As a workaround (also in the code above) one can plot each of the patches separately with the right color. This gives the correct plot:
![image](https://user-images.githubusercontent.com/7702207/62202884-6c93e000-b38a-11e9-949b-42f6893078cc.png)




Comments:

Patch:
diff --git a/doc/source/mergingdata.rst b/doc/source/mergingdata.rst
index 8c97242..3bfddeb 100644
--- a/doc/source/mergingdata.rst
+++ b/doc/source/mergingdata.rst
@@ -96,3 +96,25 @@ The `how` argument specifies the type of join that will occur and which geometry
 * ``inner``: use intersection of index values from both geodataframes; retain only the `left_df` geometry column
 
 Note more complicated spatial relationships can be studied by combining geometric operations with spatial join. To find all polygons within a given distance of a point, for example, one can first use the ``buffer`` method to expand each point into a circle of appropriate radius, then intersect those buffered circles with the polygons in question.
+
+
+Sjoin Performance
+~~~~~~~~~~~~~~~~~~
+
+Existing spatial indexes on either `left_df` or `right_df` will be reused when performing an ``sjoin``. If neither df has a spatial index, a spatial index will be generated for the longer df. If both have a spatial index, the `right_df`'s index will be used preferentially. Performance of multiple sjoins in a row involving a common GeoDataFrame may be improved by pre-generating the spatial index of the common GeoDataFrame prior to performing sjoins using ``df1.sindex``.
+
+.. code-block:: python
+
+    df1 = # a GeoDataFrame with data
+    df2 = # a second GeoDataFrame
+    df3 = # a third GeoDataFrame
+
+    # pre-generate sindex on df1 if it doesn't already exist
+    df1.sindex
+
+    sjoin(df1, df2, ...)
+    # sindex for df1 is reused
+    sjoin(df1, df3, ...)
+    # sindex for df1 is reused again
+
+
diff --git a/geopandas/geoseries.py b/geopandas/geoseries.py
index 1fc67bd..5782406 100644
--- a/geopandas/geoseries.py
+++ b/geopandas/geoseries.py
@@ -18,7 +18,8 @@ from geopandas.plotting import plot_series
 from .array import GeometryDtype, from_shapely
 from .base import is_geometry_type
 
-_PYPROJ2 = LooseVersion(pyproj.__version__) >= LooseVersion("2.1.0")
+
+_PYPROJ_VERSION = LooseVersion(pyproj.__version__)
 
 
 _SERIES_WARNING_MSG = """\
@@ -389,22 +390,38 @@ class GeoSeries(GeoPandasBase, Series):
         """
         from fiona.crs import from_epsg
 
+        if crs is None and epsg is None:
+            raise TypeError("Must set either crs or epsg for output.")
+
         if self.crs is None:
             raise ValueError(
                 "Cannot transform naive geometries.  "
                 "Please set a crs on the object first."
             )
+
         if crs is None:
             try:
                 crs = from_epsg(epsg)
-            except TypeError:
-                raise TypeError("Must set either crs or epsg for output.")
-        proj_in = pyproj.Proj(self.crs, preserve_units=True)
-        proj_out = pyproj.Proj(crs, preserve_units=True)
-        if _PYPROJ2:
-            transformer = pyproj.Transformer.from_proj(proj_in, proj_out)
+            except (TypeError, ValueError):
+                raise ValueError("Invalid epsg: {}".format(epsg))
+
+        # skip transformation if the input CRS and output CRS are the exact same
+        if _PYPROJ_VERSION >= LooseVersion("2.1.2") and pyproj.CRS.from_user_input(
+            self.crs
+        ).is_exact_same(pyproj.CRS.from_user_input(crs)):
+            return self
+
+        if _PYPROJ_VERSION >= LooseVersion("2.2.0"):
+            # if availale, use always_xy=True to preserve GIS axis order
+            transformer = pyproj.Transformer.from_crs(self.crs, crs, always_xy=True)
+            project = transformer.transform
+        elif _PYPROJ_VERSION >= LooseVersion("2.1.0"):
+            # use transformer for repeated transformations
+            transformer = pyproj.Transformer.from_crs(self.crs, crs)
             project = transformer.transform
         else:
+            proj_in = pyproj.Proj(self.crs, preserve_units=True)
+            proj_out = pyproj.Proj(crs, preserve_units=True)
             project = partial(pyproj.transform, proj_in, proj_out)
         result = self.apply(lambda geom: transform(project, geom))
         result.__class__ = GeoSeries
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 517c647..1d7b194 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -91,17 +91,24 @@ def plot_polygon_collection(
         )
     from matplotlib.collections import PatchCollection
 
-    geoms, values = _flatten_multi_geoms(geoms, values)
-    if None in values:
-        values = None
+    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    if values is not None:
+        values = np.take(values, multiindex)
 
     # PatchCollection does not accept some kwargs.
     if "markersize" in kwargs:
         del kwargs["markersize"]
-
-    # color=None overwrites specified facecolor/edgecolor with default color
     if color is not None:
         kwargs["color"] = color
+        if pd.api.types.is_list_like(color):
+            kwargs["color"] = np.take(color, multiindex)
+        else:
+            kwargs["color"] = color
+    else:
+        for att in ["facecolor", "edgecolor"]:
+            if att in kwargs:
+                if pd.api.types.is_list_like(kwargs[att]):
+                    kwargs[att] = np.take(kwargs[att], multiindex)
 
     collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
 
@@ -145,9 +152,9 @@ def plot_linestring_collection(
     """
     from matplotlib.collections import LineCollection
 
-    geoms, values = _flatten_multi_geoms(geoms, values)
-    if None in values:
-        values = None
+    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    if values is not None:
+        values = np.take(values, multiindex)
 
     # LineCollection does not accept some kwargs.
     if "markersize" in kwargs:
@@ -155,7 +162,10 @@ def plot_linestring_collection(
 
     # color=None gives black instead of default color cycle
     if color is not None:
-        kwargs["color"] = color
+        if pd.api.types.is_list_like(color):
+            kwargs["color"] = np.take(color, multiindex)
+        else:
+            kwargs["color"] = color
 
     segments = [np.array(linestring)[:, :2] for linestring in geoms]
     collection = LineCollection(segments, **kwargs)
@@ -206,9 +216,10 @@ 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, values = _flatten_multi_geoms(geoms, values)
-    if None in values:
-        values = None
+    geoms, multiindex = _flatten_multi_geoms(geoms, range(len(geoms)))
+    if values is not None:
+        values = np.take(values, multiindex)
+
     x = [p.x for p in geoms]
     y = [p.y for p in geoms]
 
@@ -218,9 +229,14 @@ def plot_point_collection(
     if markersize is not None:
         kwargs["s"] = markersize
 
+    if color is not None:
+        if pd.api.types.is_list_like(color):
+            color = np.take(color, multiindex)
+
     collection = ax.scatter(
         x, y, color=color, vmin=vmin, vmax=vmax, cmap=cmap, marker=marker, **kwargs
     )
+
     return collection
 
 
@@ -314,6 +330,7 @@ def plot_series(s, cmap=None, color=None, ax=None, figsize=None, **style_kwds):
         facecolor = style_kwds.pop("facecolor", None)
         if color is not None:
             facecolor = color
+
         values_ = values[poly_idx] if cmap else None
         plot_polygon_collection(
             ax, polys, values_, facecolor=facecolor, cmap=cmap, **style_kwds
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
index 3421431..52290a9 100644
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -32,8 +32,6 @@ def sjoin(
         Suffix to apply to overlapping column names (right GeoDataFrame).
 
     """
-    import rtree
-
     if not isinstance(left_df, GeoDataFrame):
         raise ValueError(
             "'left_df' should be GeoDataFrame, got {}".format(type(left_df))
@@ -76,6 +74,17 @@ def sjoin(
             " joined".format(index_left, index_right)
         )
 
+    # Attempt to re-use spatial indexes, otherwise generate the spatial index
+    # for the longer dataframe
+    if right_df._sindex_generated or (
+        not left_df._sindex_generated and right_df.shape[0] > left_df.shape[0]
+    ):
+        tree_idx = right_df.sindex
+        tree_idx_right = True
+    else:
+        tree_idx = left_df.sindex
+        tree_idx_right = False
+
     # the rtree spatial index only allows limited (numeric) index types, but an
     # index in geopandas may be any arbitrary dtype. so reset both indices now
     # and store references to the original indices, to be reaffixed later.
@@ -90,22 +99,32 @@ def sjoin(
     if op == "within":
         # within implemented as the inverse of contains; swap names
         left_df, right_df = right_df, left_df
+        tree_idx_right = not tree_idx_right
+
+    r_idx = np.empty((0, 0))
+    l_idx = np.empty((0, 0))
+    # get rtree spatial index
+    if tree_idx_right:
+        idxmatch = left_df.geometry.apply(lambda x: x.bounds).apply(
+            lambda x: list(tree_idx.intersection(x))
+        )
+        idxmatch = idxmatch[idxmatch.apply(len) > 0]
+        # indexes of overlapping boundaries
+        if idxmatch.shape[0] > 0:
+            r_idx = np.concatenate(idxmatch.values)
+            l_idx = np.concatenate([[i] * len(v) for i, v in idxmatch.iteritems()])
+    else:
+        # tree_idx_df == 'left'
+        idxmatch = right_df.geometry.apply(lambda x: x.bounds).apply(
+            lambda x: list(tree_idx.intersection(x))
+        )
+        idxmatch = idxmatch[idxmatch.apply(len) > 0]
+        if idxmatch.shape[0] > 0:
+            # indexes of overlapping boundaries
+            l_idx = np.concatenate(idxmatch.values)
+            r_idx = np.concatenate([[i] * len(v) for i, v in idxmatch.iteritems()])
 
-    # insert the bounds in the rtree spatial index
-    right_df_bounds = right_df.geometry.apply(lambda x: x.bounds)
-    stream = ((i, b, None) for i, b in enumerate(right_df_bounds))
-    tree_idx = rtree.index.Index(stream)
-
-    idxmatch = left_df.geometry.apply(lambda x: x.bounds).apply(
-        lambda x: list(tree_idx.intersection(x))
-    )
-    idxmatch = idxmatch[idxmatch.apply(len) > 0]
-
-    if idxmatch.shape[0] > 0:
-        # if output from join has overlapping geometries
-        r_idx = np.concatenate(idxmatch.values)
-        l_idx = np.concatenate([[i] * len(v) for i, v in idxmatch.iteritems()])
-
+    if len(r_idx) > 0 and len(l_idx) > 0:
         # Vectorize predicate operations
         def find_intersects(a1, a2):
             return a1.intersects(a2)


Test patch:
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
index f32cb23..e08ca88 100644
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -1,6 +1,11 @@
+from distutils.version import LooseVersion
+
 import numpy as np
 
+from shapely.geometry import Point
+
 from geopandas import GeoDataFrame, points_from_xy
+from geopandas.geoseries import _PYPROJ_VERSION
 
 from geopandas.testing import assert_geodataframe_equal
 import pytest
@@ -94,3 +99,22 @@ def test_transform2(epsg4326, epsg26918):
     # can't check for CRS equality, as the formats differ although representing
     # the same CRS
     assert_geodataframe_equal(df, utm, check_less_precise=True, check_crs=False)
+
+
+@pytest.mark.skipif(
+    _PYPROJ_VERSION < LooseVersion("2.2.0"),
+    reason="EPSG strings without +init= won't work on previous versions of pyproj.",
+)
+def test_crs_axis_order__always_xy():
+    df = GeoDataFrame(geometry=[Point(-1683723, 6689139)], crs="epsg:26918")
+    lonlat = df.to_crs("epsg:4326")
+    test_lonlat = GeoDataFrame(
+        geometry=[Point(-110.1399901, 55.1350011)], crs="epsg:4326"
+    )
+    assert_geodataframe_equal(lonlat, test_lonlat, check_less_precise=True)
+
+
+def test_skip_exact_same():
+    df = df_epsg26918()
+    utm = df.to_crs(df.crs)
+    assert_geodataframe_equal(df, utm, check_less_precise=True)
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index b3c5839..223a486 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -6,7 +6,15 @@ import warnings
 import numpy as np
 
 from shapely.affinity import rotate
-from shapely.geometry import LineString, MultiPoint, MultiPolygon, Point, Polygon
+from shapely.geometry import (
+    MultiPolygon,
+    Polygon,
+    LineString,
+    Point,
+    MultiPoint,
+    MultiLineString,
+)
+
 
 from geopandas import GeoDataFrame, GeoSeries, read_file
 from geopandas.datasets import get_path
@@ -189,6 +197,10 @@ class TestPointPlotting:
         expected_colors = [cmap(0)] * self.N + [cmap(1)] * self.N
         _check_colors(2, ax.collections[0].get_facecolors(), expected_colors)
 
+        ax = self.df2.plot(color=["r", "b"])
+        # colors are repeated for all components within a MultiPolygon
+        _check_colors(2, ax.collections[0].get_facecolors(), ["r"] * 10 + ["b"] * 10)
+
 
 class TestPointZPlotting:
     def setup_method(self):
@@ -212,6 +224,12 @@ class TestLineStringPlotting:
         )
         self.df = GeoDataFrame({"geometry": self.lines, "values": values})
 
+        multiline1 = MultiLineString(self.lines.loc["A":"B"].values)
+        multiline2 = MultiLineString(self.lines.loc["C":"D"].values)
+        self.df2 = GeoDataFrame(
+            {"geometry": [multiline1, multiline2], "values": [0, 1]}
+        )
+
     def test_single_color(self):
 
         ax = self.lines.plot(color="green")
@@ -246,6 +264,23 @@ class TestLineStringPlotting:
             assert ls[0] == exp_ls[0]
             assert ls[1] == exp_ls[1]
 
+    def test_multilinestrings(self):
+
+        # MultiLineStrings
+        ax = self.df2.plot()
+        assert len(ax.collections[0].get_paths()) == 4
+        _check_colors(4, ax.collections[0].get_facecolors(), [MPL_DFT_COLOR] * 4)
+
+        ax = self.df2.plot("values")
+        cmap = plt.get_cmap(lut=2)
+        # colors are repeated for all components within a MultiLineString
+        expected_colors = [cmap(0), cmap(0), cmap(1), cmap(1)]
+        _check_colors(4, ax.collections[0].get_facecolors(), expected_colors)
+
+        ax = self.df2.plot(color=["r", "b"])
+        # colors are repeated for all components within a MultiLineString
+        _check_colors(4, ax.collections[0].get_facecolors(), ["r", "r", "b", "b"])
+
 
 class TestPolygonPlotting:
     def setup_method(self):
@@ -372,6 +407,10 @@ class TestPolygonPlotting:
         expected_colors = [cmap(0), cmap(0), cmap(1), cmap(1)]
         _check_colors(4, ax.collections[0].get_facecolors(), expected_colors)
 
+        ax = self.df2.plot(color=["r", "b"])
+        # colors are repeated for all components within a MultiPolygon
+        _check_colors(4, ax.collections[0].get_facecolors(), ["r", "r", "b", "b"])
+
 
 class TestPolygonZPlotting:
     def setup_method(self):
@@ -673,6 +712,12 @@ class TestPlotCollections:
         _check_colors(self.N, coll.get_edgecolor(), ["g"] * self.N)
         ax.cla()
 
+        # default: color can be passed as a list
+        coll = plot_polygon_collection(ax, self.polygons, color=["g", "b", "r"])
+        _check_colors(self.N, coll.get_facecolor(), ["g", "b", "r"])
+        _check_colors(self.N, coll.get_edgecolor(), ["g", "b", "r"])
+        ax.cla()
+
         # only setting facecolor keeps default for edgecolor
         coll = plot_polygon_collection(ax, self.polygons, facecolor="g")
         _check_colors(self.N, coll.get_facecolor(), ["g"] * self.N)