Repository: geopandas/geopandas

Issue:
Multi-part Lines getting same color but dissimilar width
Probably related to [1141](https://github.com/geopandas/geopandas/issues/1141), but this issue is still present in 0.7. I'll let the example speak for itself:

```
import geopandas as gpd
import shapely.geometry as sg

gdf = gpd.GeoDataFrame([{'geometry':sg.MultiLineString([[[0,0],[0,1]],[[0,1],[1,1]]]),
                         'linewidth':8, 'color':'red'},
                        {'geometry':sg.LineString([[0,0],[2,1]]),
                         'linewidth':2, 'color':'blue'}])
gdf.plot(linewidth=gdf['linewidth'], color=gdf['color'])
```

output: 
![image](https://user-images.githubusercontent.com/4106013/79636312-0567c700-8177-11ea-8b47-a8871d982945.png)

As you can see, the `MultiLine`, which is correctly colored red in the image, is drawn with an incorrect linewidth for one of its two lines. The `Line`, which is correctly colored blue, also has an incorrect linewidth. That is, unless I'm using it wrong of course.


Discussion:
Thank you! We should make sure that all style kwds are properly replicated for all parts. We do that now for `color` only. This should be generalised for all applicable kwds:

https://github.com/geopandas/geopandas/blob/f70a66918df086bd0fca37f03e160224f400fe14/geopandas/plotting.py#L101-L102
Ah, great. I was looking through the trace to see where this happens. If I have time later I'll see if I can fix it and make a PR

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -54,6 +54,36 @@ def _flatten_multi_geoms(geoms, prefix="Multi"):
     return components, np.array(component_index)
 
 
+def _expand_kwargs(kwargs, multiindex):
+    """
+    Most arguments to the plot functions must be a (single) value, or a sequence
+    of values. This function checks each key-value pair in 'kwargs' and expands
+    it (in place) to the correct length/formats with help of 'multiindex', unless
+    the value appears to already be a valid (single) value for the key.
+    """
+    from matplotlib.colors import is_color_like
+    from typing import Iterable
+
+    for att, value in kwargs.items():
+        if "color" in att:  # color(s), edgecolor(s), facecolor(s)
+            if is_color_like(value):
+                continue
+        elif "linestyle" in att:  # linestyle(s)
+            # A single linestyle can be 2-tuple of a number and an iterable.
+            if (
+                isinstance(value, tuple)
+                and len(value) == 2
+                and isinstance(value[1], Iterable)
+            ):
+                continue
+        elif att in ["marker", "alpha"]:
+            # For these attributes, only a single value is allowed, so never expand.
+            continue
+
+        if pd.api.types.is_list_like(value):
+            kwargs[att] = np.take(value, multiindex, axis=0)
+
+
 def _plot_polygon_collection(
     ax, geoms, values=None, color=None, cmap=None, vmin=None, vmax=None, **kwargs
 ):
@@ -93,36 +123,23 @@ def _plot_polygon_collection(
             "'pip install descartes'."
         )
     from matplotlib.collections import PatchCollection
-    from matplotlib.colors import is_color_like
 
     geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
     # PatchCollection does not accept some kwargs.
-    if "markersize" in kwargs:
-        del kwargs["markersize"]
+    kwargs = {
+        att: value
+        for att, value in kwargs.items()
+        if att not in ["markersize", "marker"]
+    }
+
+    # Add to kwargs for easier checking below.
     if color is not None:
-        if is_color_like(color):
-            kwargs["color"] = color
-        elif pd.api.types.is_list_like(color):
-            kwargs["color"] = np.take(color, multiindex, axis=0)
-        else:
-            raise TypeError(
-                "Color attribute has to be a single color or sequence of colors."
-            )
+        kwargs["color"] = color
 
-    else:
-        for att in ["facecolor", "edgecolor"]:
-            if att in kwargs:
-                if not is_color_like(kwargs[att]):
-                    if pd.api.types.is_list_like(kwargs[att]):
-                        kwargs[att] = np.take(kwargs[att], multiindex, axis=0)
-                    elif kwargs[att] is not None:
-                        raise TypeError(
-                            "Color attribute has to be a single color or sequence "
-                            "of colors."
-                        )
+    _expand_kwargs(kwargs, multiindex)
 
     collection = PatchCollection([PolygonPatch(poly) for poly in geoms], **kwargs)
 
@@ -163,26 +180,23 @@ def _plot_linestring_collection(
     collection : matplotlib.collections.Collection that was plotted
     """
     from matplotlib.collections import LineCollection
-    from matplotlib.colors import is_color_like
 
     geoms, multiindex = _flatten_multi_geoms(geoms)
     if values is not None:
         values = np.take(values, multiindex, axis=0)
 
     # LineCollection does not accept some kwargs.
-    if "markersize" in kwargs:
-        del kwargs["markersize"]
+    kwargs = {
+        att: value
+        for att, value in kwargs.items()
+        if att not in ["markersize", "marker"]
+    }
 
-    # color=None gives black instead of default color cycle
+    # Add to kwargs for easier checking below.
     if color is not None:
-        if is_color_like(color):
-            kwargs["color"] = color
-        elif pd.api.types.is_list_like(color):
-            kwargs["color"] = np.take(color, multiindex, axis=0)
-        else:
-            raise TypeError(
-                "Color attribute has to be a single color or sequence of colors."
-            )
+        kwargs["color"] = color
+
+    _expand_kwargs(kwargs, multiindex)
 
     segments = [np.array(linestring)[:, :2] for linestring in geoms]
     collection = LineCollection(segments, **kwargs)
@@ -234,8 +248,6 @@ def _plot_point_collection(
     -------
     collection : matplotlib.collections.Collection that was plotted
     """
-    from matplotlib.colors import is_color_like
-
     if values is not None and color is not None:
         raise ValueError("Can only specify one of 'values' and 'color' kwargs")
 
@@ -252,21 +264,17 @@ def _plot_point_collection(
     if markersize is not None:
         kwargs["s"] = markersize
 
+    # Add to kwargs for easier checking below.
     if color is not None:
-        if not is_color_like(color):
-            if pd.api.types.is_list_like(color):
-                color = np.take(color, multiindex, axis=0)
-            else:
-                raise TypeError(
-                    "Color attribute has to be a single color or sequence of colors."
-                )
+        kwargs["color"] = color
+    if marker is not None:
+        kwargs["marker"] = marker
+    _expand_kwargs(kwargs, multiindex)
 
     if "norm" not in kwargs:
-        collection = ax.scatter(
-            x, y, color=color, vmin=vmin, vmax=vmax, cmap=cmap, marker=marker, **kwargs
-        )
+        collection = ax.scatter(x, y, vmin=vmin, vmax=vmax, cmap=cmap, **kwargs)
     else:
-        collection = ax.scatter(x, y, color=color, cmap=cmap, marker=marker, **kwargs)
+        collection = ax.scatter(x, y, cmap=cmap, **kwargs)
 
     return collection
 
@@ -493,7 +501,7 @@ def plot_dataframe(
         values are not plotted.
 
     **style_kwds : dict
-        Color options to be passed on to the actual plot function, such
+        Style options to be passed on to the actual plot function, such
         as ``edgecolor``, ``facecolor``, ``linewidth``, ``markersize``,
         ``alpha``.
 


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
@@ -128,7 +128,7 @@ def test_single_color(self):
         _check_colors(
             self.N, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5, 0.5)] * self.N
         )
-        with pytest.raises(TypeError):
+        with pytest.raises((ValueError, TypeError)):
             self.df.plot(color="not color")
 
         with warnings.catch_warnings(record=True) as _:  # don't print warning
@@ -155,11 +155,24 @@ def test_markersize(self):
         ax = self.df.plot(column="values", markersize="values")
         assert (ax.collections[0].get_sizes() == self.df["values"]).all()
 
+    def test_markerstyle(self):
+        ax = self.df2.plot(marker="+")
+        expected = _style_to_vertices("+")
+        np.testing.assert_array_equal(
+            expected, ax.collections[0].get_paths()[0].vertices
+        )
+
     def test_style_kwargs(self):
 
         ax = self.points.plot(edgecolors="k")
         assert (ax.collections[0].get_edgecolor() == [0, 0, 0, 1]).all()
 
+    def test_style_kwargs_alpha(self):
+        ax = self.df.plot(alpha=0.7)
+        np.testing.assert_array_equal([0.7], ax.collections[0].get_alpha())
+        with pytest.raises(TypeError):  # no list allowed for alpha
+            ax = self.df.plot(alpha=[0.7, 0.2])
+
     def test_legend(self):
         with warnings.catch_warnings(record=True) as _:  # don't print warning
             # legend ignored if color is given.
@@ -243,6 +256,12 @@ def test_multipoints(self):
         # colors are repeated for all components within a MultiPolygon
         _check_colors(2, ax.collections[0].get_facecolors(), ["r"] * 10 + ["b"] * 10)
 
+    def test_multipoints_alpha(self):
+        ax = self.df2.plot(alpha=0.7)
+        np.testing.assert_array_equal([0.7], ax.collections[0].get_alpha())
+        with pytest.raises(TypeError):  # no list allowed for alpha
+            ax = self.df2.plot(alpha=[0.7, 0.2])
+
     def test_misssing(self):
         self.df.loc[0, "values"] = np.nan
         ax = self.df.plot("values")
@@ -325,7 +344,7 @@ def test_single_color(self):
         _check_colors(
             self.N, ax.collections[0].get_colors(), [(0.5, 0.5, 0.5, 0.5)] * self.N
         )
-        with pytest.raises(TypeError):
+        with pytest.raises((TypeError, ValueError)):
             self.df.plot(color="not color")
 
         with warnings.catch_warnings(record=True) as _:  # don't print warning
@@ -333,26 +352,54 @@ def test_single_color(self):
             ax = self.df.plot(column="values", color="green")
             _check_colors(self.N, ax.collections[0].get_colors(), ["green"] * self.N)
 
-    def test_style_kwargs(self):
-        # linestyle (style patterns depend on linewidth, therefore pin to 1)
-        linestyle = "dashed"
-        linewidth = 1
-
-        ax = self.lines.plot(linestyle=linestyle, linewidth=linewidth)
-        exp_ls = _style_to_linestring_onoffseq(linestyle, linewidth)
-        for ls in ax.collections[0].get_linestyles():
-            assert ls[0] == exp_ls[0]
-            assert ls[1] == exp_ls[1]
-
-        ax = self.df.plot(linestyle=linestyle, linewidth=linewidth)
-        for ls in ax.collections[0].get_linestyles():
-            assert ls[0] == exp_ls[0]
-            assert ls[1] == exp_ls[1]
-
-        ax = self.df.plot(column="values", linestyle=linestyle, linewidth=linewidth)
-        for ls in ax.collections[0].get_linestyles():
-            assert ls[0] == exp_ls[0]
-            assert ls[1] == exp_ls[1]
+    def test_style_kwargs_linestyle(self):
+        # single
+        for ax in [
+            self.lines.plot(linestyle=":", linewidth=1),
+            self.df.plot(linestyle=":", linewidth=1),
+            self.df.plot(column="values", linestyle=":", linewidth=1),
+        ]:
+            assert [(0.0, [1.0, 1.65])] == ax.collections[0].get_linestyle()
+
+        # tuple
+        ax = self.lines.plot(linestyle=(0, (3, 10, 1, 15)), linewidth=1)
+        assert [(0, [3, 10, 1, 15])] == ax.collections[0].get_linestyle()
+
+        # multiple
+        ls = [("dashed", "dotted", "dashdot", "solid")[k % 4] for k in range(self.N)]
+        exp_ls = [_style_to_linestring_onoffseq(st, 1) for st in ls]
+        for ax in [
+            self.lines.plot(linestyle=ls, linewidth=1),
+            self.lines.plot(linestyles=ls, linewidth=1),
+            self.df.plot(linestyle=ls, linewidth=1),
+            self.df.plot(column="values", linestyle=ls, linewidth=1),
+        ]:
+            np.testing.assert_array_equal(exp_ls, ax.collections[0].get_linestyle())
+
+    def test_style_kwargs_linewidth(self):
+        # single
+        for ax in [
+            self.lines.plot(linewidth=2),
+            self.df.plot(linewidth=2),
+            self.df.plot(column="values", linewidth=2),
+        ]:
+            np.testing.assert_array_equal([2], ax.collections[0].get_linewidths())
+
+        # multiple
+        lw = [(0, 1, 2, 5.5, 10)[k % 5] for k in range(self.N)]
+        for ax in [
+            self.lines.plot(linewidth=lw),
+            self.lines.plot(linewidths=lw),
+            self.df.plot(linewidth=lw),
+            self.df.plot(column="values", linewidth=lw),
+        ]:
+            np.testing.assert_array_equal(lw, ax.collections[0].get_linewidths())
+
+    def test_style_kwargs_alpha(self):
+        ax = self.df.plot(alpha=0.7)
+        np.testing.assert_array_equal([0.7], ax.collections[0].get_alpha())
+        with pytest.raises(TypeError):  # no list allowed for alpha
+            ax = self.df.plot(alpha=[0.7, 0.2])
 
     def test_subplots_norm(self):
         # colors of subplots are the same as for plot (norm is applied)
@@ -419,7 +466,7 @@ def test_single_color(self):
         _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5)] * 2)
         ax = self.df.plot(color=(0.5, 0.5, 0.5, 0.5))
         _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5, 0.5)] * 2)
-        with pytest.raises(TypeError):
+        with pytest.raises((TypeError, ValueError)):
             self.df.plot(color="not color")
 
         with warnings.catch_warnings(record=True) as _:  # don't print warning
@@ -445,7 +492,7 @@ def test_vmin_vmax(self):
         actual_colors = ax.collections[0].get_facecolors()
         assert np.any(np.not_equal(actual_colors[0], actual_colors[1]))
 
-    def test_style_kwargs(self):
+    def test_style_kwargs_color(self):
 
         # facecolor overrides default cmap when color is not set
         ax = self.polys.plot(facecolor="k")
@@ -483,6 +530,38 @@ def test_style_kwargs(self):
         _check_colors(2, ax.collections[0].get_facecolors(), [(0.5, 0.5, 0.5, 0.5)] * 2)
         _check_colors(2, ax.collections[0].get_edgecolors(), [(0.4, 0.5, 0.6, 0.5)] * 2)
 
+    def test_style_kwargs_linestyle(self):
+        #   single
+        ax = self.df.plot(linestyle=":", linewidth=1)
+        assert [(0.0, [1.0, 1.65])] == ax.collections[0].get_linestyle()
+
+        # tuple
+        ax = self.df.plot(linestyle=(0, (3, 10, 1, 15)), linewidth=1)
+        assert [(0, [3, 10, 1, 15])] == ax.collections[0].get_linestyle()
+
+        #   multiple
+        ls = ["dashed", "dotted"]
+        exp_ls = [_style_to_linestring_onoffseq(st, 1) for st in ls]
+        for ax in [
+            self.df.plot(linestyle=ls, linewidth=1),
+            self.df.plot(linestyles=ls, linewidth=1),
+        ]:
+            assert exp_ls == ax.collections[0].get_linestyle()
+
+    def test_style_kwargs_linewidth(self):
+        #   single
+        ax = self.df.plot(linewidth=2)
+        np.testing.assert_array_equal([2], ax.collections[0].get_linewidths())
+        #   multiple
+        for ax in [self.df.plot(linewidth=[2, 4]), self.df.plot(linewidths=[2, 4])]:
+            np.testing.assert_array_equal([2, 4], ax.collections[0].get_linewidths())
+
+        # alpha
+        ax = self.df.plot(alpha=0.7)
+        np.testing.assert_array_equal([0.7], ax.collections[0].get_alpha())
+        with pytest.raises(TypeError):  # no list allowed for alpha
+            ax = self.df.plot(alpha=[0.7, 0.2])
+
     def test_legend_kwargs(self):
 
         ax = self.df.plot(
@@ -516,7 +595,7 @@ def test_colorbar_kwargs(self):
 
         assert ax.get_figure().axes[1].get_xlabel() == label_txt
 
-    def test_multipolygons(self):
+    def test_multipolygons_color(self):
 
         # MultiPolygons
         ax = self.df2.plot()
@@ -533,6 +612,41 @@ def test_multipolygons(self):
         # colors are repeated for all components within a MultiPolygon
         _check_colors(4, ax.collections[0].get_facecolors(), ["r", "r", "b", "b"])
 
+    def test_multipolygons_linestyle(self):
+        # single
+        ax = self.df2.plot(linestyle=":", linewidth=1)
+        assert [(0.0, [1.0, 1.65])] == ax.collections[0].get_linestyle()
+
+        # tuple
+        ax = self.df2.plot(linestyle=(0, (3, 10, 1, 15)), linewidth=1)
+        assert [(0, [3, 10, 1, 15])] == ax.collections[0].get_linestyle()
+
+        # multiple
+        ls = ["dashed", "dotted"]
+        exp_ls = [_style_to_linestring_onoffseq(st, 1) for st in ls for i in range(2)]
+        for ax in [
+            self.df2.plot(linestyle=ls, linewidth=1),
+            self.df2.plot(linestyles=ls, linewidth=1),
+        ]:
+            assert exp_ls == ax.collections[0].get_linestyle()
+
+    def test_multipolygons_linewidth(self):
+        # single
+        ax = self.df2.plot(linewidth=2)
+        np.testing.assert_array_equal([2], ax.collections[0].get_linewidths())
+
+        # multiple
+        for ax in [self.df2.plot(linewidth=[2, 4]), self.df2.plot(linewidths=[2, 4])]:
+            np.testing.assert_array_equal(
+                [2, 2, 4, 4], ax.collections[0].get_linewidths()
+            )
+
+    def test_multipolygons_alpha(self):
+        ax = self.df2.plot(alpha=0.7)
+        np.testing.assert_array_equal([0.7], ax.collections[0].get_alpha())
+        with pytest.raises(TypeError):  # no list allowed for alpha
+            ax = self.df2.plot(alpha=[0.7, 0.2])
+
     def test_subplots_norm(self):
         # colors of subplots are the same as for plot (norm is applied)
         cmap = matplotlib.cm.viridis_r
@@ -628,6 +742,57 @@ def test_style_kwargs(self):
         ax = self.df.plot(markersize=10)
         assert ax.collections[2].get_sizes() == [10]
 
+    def test_style_kwargs_linestyle(self):
+        # single
+        for ax in [
+            self.series.plot(linestyle=":", linewidth=1),
+            self.df.plot(linestyle=":", linewidth=1),
+        ]:
+            assert [(0.0, [1.0, 1.65])] == ax.collections[0].get_linestyle()
+
+        # tuple
+        ax = self.series.plot(linestyle=(0, (3, 10, 1, 15)), linewidth=1)
+        assert [(0, [3, 10, 1, 15])] == ax.collections[0].get_linestyle()
+
+    @pytest.mark.skip(
+        reason="array-like style_kwds not supported for mixed geometry types (#1379)"
+    )
+    def test_style_kwargs_linestyle_listlike(self):
+        # multiple
+        ls = ["solid", "dotted", "dashdot"]
+        exp_ls = [_style_to_linestring_onoffseq(style, 1) for style in ls]
+        for ax in [
+            self.series.plot(linestyle=ls, linewidth=1),
+            self.series.plot(linestyles=ls, linewidth=1),
+            self.df.plot(linestyles=ls, linewidth=1),
+        ]:
+            np.testing.assert_array_equal(exp_ls, ax.collections[0].get_linestyle())
+
+    def test_style_kwargs_linewidth(self):
+        # single
+        ax = self.df.plot(linewidth=2)
+        np.testing.assert_array_equal([2], ax.collections[0].get_linewidths())
+
+    @pytest.mark.skip(
+        reason="array-like style_kwds not supported for mixed geometry types (#1379)"
+    )
+    def test_style_kwargs_linewidth_listlike(self):
+        # multiple
+        for ax in [
+            self.series.plot(linewidths=[2, 4, 5.5]),
+            self.series.plot(linewidths=[2, 4, 5.5]),
+            self.df.plot(linewidths=[2, 4, 5.5]),
+        ]:
+            np.testing.assert_array_equal(
+                [2, 4, 5.5], ax.collections[0].get_linewidths()
+            )
+
+    def test_style_kwargs_alpha(self):
+        ax = self.df.plot(alpha=0.7)
+        np.testing.assert_array_equal([0.7], ax.collections[0].get_alpha())
+        with pytest.raises(TypeError):  # no list allowed for alpha
+            ax = self.df.plot(alpha=[0.7, 0.2, 0.9])
+
 
 class TestMapclassifyPlotting:
     @classmethod
@@ -795,7 +960,7 @@ def test_points(self):
         ax.cla()
 
         # not a color
-        with pytest.raises(TypeError):
+        with pytest.raises((TypeError, ValueError)):
             _plot_point_collection(ax, self.points, color="not color")
 
         # check DeprecationWarning
@@ -869,9 +1034,8 @@ def test_linestrings(self):
         ax.cla()
 
         # not a color
-        with pytest.raises(TypeError):
+        with pytest.raises((TypeError, ValueError)):
             _plot_linestring_collection(ax, self.lines, color="not color")
-
         # check DeprecationWarning
         with pytest.warns(DeprecationWarning):
             plot_linestring_collection(ax, self.lines)
@@ -962,7 +1126,7 @@ def test_polygons(self):
         ax.cla()
 
         # not a color
-        with pytest.raises(TypeError):
+        with pytest.raises((TypeError, ValueError)):
             _plot_polygon_collection(ax, self.polygons, color="not color")
         # check DeprecationWarning
         with pytest.warns(DeprecationWarning):
@@ -1089,3 +1253,10 @@ def _style_to_linestring_onoffseq(linestyle, linewidth):
     """
     offset, dashes = matplotlib.lines._get_dash_pattern(linestyle)
     return matplotlib.lines._scale_dashes(offset, dashes, linewidth)
+
+
+def _style_to_vertices(markerstyle):
+    """ Converts a markerstyle string to a path. """
+    # TODO: Vertices values are twice the actual path; unclear, why.
+    path = matplotlib.markers.MarkerStyle(markerstyle).get_path()
+    return path.vertices / 2