Repository: geopandas/geopandas

Issue:
Pass plot kwargs to matplotlib commands
The plot functions don't pass kwargs to matplotlib.

For example, it would be nice to be able to use `df.plot(figsize=(10, 10))` to change output size, especially in the notebook. Currently, all kwargs to `plot_dataframe()` are assumed to be color arguments that are ultimately only options to `plot_multipolygon()`.



Discussion:
Hi Kelsey,
it seems the color functions do not work properly(?). If one tries `df.plot(facecolor='white')` or any other color it produces an error `TypeError: plot_multipolygon() got multiple values for keyword argument 'facecolor'`. Was this supposed to work? Or is this what you are referring to in this issue?

I was trying to create a map that only showed the geometries without colors (think State borders) in order to plot other data on top. I solved it creating a column of only zeros and using the `colormap='binary'` option. But it seems the `facecolor` option conflicts with the `plot_series` function, which iterates over colors of the colormap and assigns `facecolor` again, thus creating two conflicting values.

FYI, I also tried playing with the `markersize` for points and it does not seem to work.

It seems the problem is that the functions being called by `plot_dataframe` have default values which are not being updated (for markers at least seems to be the case).

Related to this question, is there any way in geopandas to tweak (either before passing to the Descartes plotting machinery, or once it's been appended to the axis) some of the attributes, like the linewidth, the color, etc.?

I've issued a PR to solve the problems raised by @ozak. In the PR, both `plot_series` and `plot_dataframe` can take a fixed `facecolor` argument, and points can also be specified further. All tests pass locally.


Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -3,55 +3,206 @@
 import warnings
 
 import numpy as np
+import pandas as pd
 from six import next
 from six.moves import xrange
-from shapely.geometry import Polygon
 
 
-def plot_polygon(ax, poly, facecolor='red', edgecolor='black', alpha=0.5, linewidth=1.0, **kwargs):
-    """ Plot a single Polygon geometry """
+def _flatten_multi_geoms(geoms, colors):
+    """
+    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.
+
+    "Colors" are treated opaquely and so can actually contain any values.
+
+    Returns
+    -------
+
+    components : list of geometry
+
+    component_colors : list of whatever type `colors` contains
+    """
+    components, component_colors = [], []
+
+    # 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 poly in geom:
+                components.append(poly)
+                # repeat same color for all components
+                component_colors.append(color)
+        else:
+            components.append(geom)
+            component_colors.append(color)
+    return components, component_colors
+
+
+def plot_polygon_collection(ax, geoms, colors_or_values, plot_values,
+                            vmin=None, vmax=None, cmap=None,
+                            edgecolor='black', alpha=0.5, linewidth=1.0, **kwargs):
+    """
+    Plots a collection of Polygon and MultiPolygon geometries to `ax`
+
+    Parameters
+    ----------
+
+    ax : matplotlib.axes.Axes
+        where shapes will be plotted
+
+    geoms : a sequence of `N` Polygons and/or MultiPolygons (can be mixed)
+
+    colors_or_values : a sequence of `N` values or RGBA tuples
+        It should have 1:1 correspondence with the geometries (not their components).
+
+    plot_values : bool
+        If True, `colors_or_values` is interpreted as a list of values, and will
+        be mapped to colors using vmin/vmax/cmap (which become required).
+        Otherwise `colors_or_values` is interpreted as a list of colors.
+
+    Returns
+    -------
+
+    collection : matplotlib.collections.Collection that was plotted
+    """
+
     from descartes.patch import PolygonPatch
-    a = np.asarray(poly.exterior)
-    if poly.has_z:
-        poly = Polygon(zip(*poly.exterior.xy))
+    from matplotlib.collections import PatchCollection
+
+    components, component_colors_or_values = _flatten_multi_geoms(
+        geoms, colors_or_values)
+
+    # PatchCollection does not accept some kwargs.
+    if 'markersize' in kwargs:
+        del kwargs['markersize']
+    collection = PatchCollection([PolygonPatch(poly) for poly in components],
+                                 linewidth=linewidth, edgecolor=edgecolor,
+                                 alpha=alpha, **kwargs)
+
+    if plot_values:
+        collection.set_array(np.array(component_colors_or_values))
+        collection.set_cmap(cmap)
+        collection.set_clim(vmin, vmax)
+    else:
+        # set_color magically sets the correct combination of facecolor and
+        # edgecolor, based on collection type.
+        collection.set_color(component_colors_or_values)
+
+        # If the user set facecolor and/or edgecolor explicitly, the previous
+        # call to set_color might have overridden it (remember, the 'color' may
+        # have come from plot_series, not from the user). The user should be
+        # able to override matplotlib's default behavior, by setting them again
+        # after set_color.
+        if 'facecolor' in kwargs:
+            collection.set_facecolor(kwargs['facecolor'])
+        if edgecolor:
+            collection.set_edgecolor(edgecolor)
+
+    ax.add_collection(collection, autolim=True)
+    ax.autoscale_view()
+    return collection
+
+
+def plot_linestring_collection(ax, geoms, colors_or_values, plot_values,
+                               vmin=None, vmax=None, cmap=None,
+                               linewidth=1.0, **kwargs):
+    """
+    Plots a collection of LineString and MultiLineString geometries to `ax`
+
+    Parameters
+    ----------
+
+    ax : matplotlib.axes.Axes
+        where shapes will be plotted
+
+    geoms : a sequence of `N` LineStrings and/or MultiLineStrings (can be mixed)
+
+    colors_or_values : a sequence of `N` values or RGBA tuples
+        It should have 1:1 correspondence with the geometries (not their components).
 
-    # without Descartes, we could make a Patch of exterior
-    ax.add_patch(PolygonPatch(poly, facecolor=facecolor, linewidth=0, alpha=alpha))  # linewidth=0 because boundaries are drawn separately
-    ax.plot(a[:, 0], a[:, 1], color=edgecolor, linewidth=linewidth, **kwargs)
-    for p in poly.interiors:
-        x, y = zip(*p.coords)
-        ax.plot(x, y, color=edgecolor, linewidth=linewidth)
+    plot_values : bool
+        If True, `colors_or_values` is interpreted as a list of values, and will
+        be mapped to colors using vmin/vmax/cmap (which become required).
+        Otherwise `colors_or_values` is interpreted as a list of colors.
 
+    Returns
+    -------
 
-def plot_multipolygon(ax, geom, facecolor='red', edgecolor='black', alpha=0.5, linewidth=1.0, **kwargs):
-    """ Can safely call with either Polygon or Multipolygon geometry
+    collection : matplotlib.collections.Collection that was plotted
     """
-    if geom.type == 'Polygon':
-        plot_polygon(ax, geom, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, linewidth=linewidth, **kwargs)
-    elif geom.type == 'MultiPolygon':
-        for poly in geom.geoms:
-            plot_polygon(ax, poly, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, linewidth=linewidth, **kwargs)
 
+    from matplotlib.collections import LineCollection
 
-def plot_linestring(ax, geom, color='black', linewidth=1.0, **kwargs):
-    """ Plot a single LineString geometry """
-    a = np.array(geom)
-    ax.plot(a[:, 0], a[:, 1], color=color, linewidth=linewidth, **kwargs)
+    components, component_colors_or_values = _flatten_multi_geoms(
+        geoms, colors_or_values)
 
+    # LineCollection does not accept some kwargs.
+    if 'markersize' in kwargs:
+        del kwargs['markersize']
+    segments = [np.array(linestring)[:, :2] for linestring in components]
+    collection = LineCollection(segments,
+                                linewidth=linewidth, **kwargs)
 
-def plot_multilinestring(ax, geom, color='red', linewidth=1.0, **kwargs):
-    """ Can safely call with either LineString or MultiLineString geometry
+    if plot_values:
+        collection.set_array(np.array(component_colors_or_values))
+        collection.set_cmap(cmap)
+        collection.set_clim(vmin, vmax)
+    else:
+        # set_color magically sets the correct combination of facecolor and
+        # edgecolor, based on collection type.
+        collection.set_color(component_colors_or_values)
+
+        # If the user set facecolor and/or edgecolor explicitly, the previous
+        # call to set_color might have overridden it (remember, the 'color' may
+        # have come from plot_series, not from the user). The user should be
+        # able to override matplotlib's default behavior, by setting them again
+        # after set_color.
+        if 'facecolor' in kwargs:
+            collection.set_facecolor(kwargs['facecolor'])
+
+        if 'edgecolor' in kwargs:
+            collection.set_edgecolor(kwargs['edgecolor'])
+
+    ax.add_collection(collection, autolim=True)
+    ax.autoscale_view()
+    return collection
+
+
+def plot_point_collection(ax, geoms, colors_or_values,
+                          vmin=None, vmax=None, cmap=None,
+                          marker='o', markersize=2, **kwargs):
     """
-    if geom.type == 'LineString':
-        plot_linestring(ax, geom, color=color, linewidth=linewidth, **kwargs)
-    elif geom.type == 'MultiLineString':
-        for line in geom.geoms:
-            plot_linestring(ax, line, color=color, linewidth=linewidth, **kwargs)
+    Plots a collection of Point geometries to `ax`
 
+    Parameters
+    ----------
 
-def plot_point(ax, pt, marker='o', markersize=2, color='black', **kwargs):
-    """ Plot a single Point geometry """
-    ax.plot(pt.x, pt.y, marker=marker, markersize=markersize, color=color, **kwargs)
+    ax : matplotlib.axes.Axes
+        where shapes will be plotted
+
+    geoms : sequence of `N` Points
+
+    colors_or_values : sequence of color or sequence of numbers
+        can be a sequence of color specifications of length `N` or a sequence
+        of `N` numbers to be mapped to colors using vmin, vmax, and cmap.
+
+    Returns
+    -------
+    collection : matplotlib.collections.Collection that was plotted
+    """
+    x = [p.x for p in geoms]
+    y = [p.y for p in geoms]
+
+    # matplotlib ax.scatter requires RGBA color specifications to be a single 2D
+    # array, NOT merely a list of 1D arrays. This reshapes that if necessary,
+    # having no effect on 1D arrays of values.
+    colors_or_values = np.array([element
+                                 for _, element in enumerate(colors_or_values)])
+    collection = ax.scatter(x, y, c=colors_or_values,
+                            vmin=vmin, vmax=vmax, cmap=cmap,
+                            marker=marker, s=markersize, **kwargs)
+    return collection
 
 
 def gencolor(N, colormap='Set1'):
@@ -130,21 +281,52 @@ def plot_series(s, cmap='Set1', color=None, ax=None, linewidth=1.0,
     if ax is None:
         fig, ax = plt.subplots(figsize=figsize)
         ax.set_aspect('equal')
-    color_generator = gencolor(len(s), colormap=cmap)
-    for geom in s:
-        if color is None:
-            col = next(color_generator)
+
+    num_geoms = len(s.index)
+    if color:
+        colors = np.array([color] * num_geoms)
+    else:
+        color_generator = gencolor(len(s), colormap=cmap)
+        colors = np.array([next(color_generator) for _ in xrange(num_geoms)])
+
+    # plot all Polygons and all MultiPolygon components in the same collection
+    poly_idx = np.array(
+        (s.geometry.type == 'Polygon') | (s.geometry.type == 'MultiPolygon'))
+    polys = s.geometry[poly_idx]
+    if not polys.empty:
+        # Legacy behavior applies alpha to fill but not to edges. This requires
+        # plotting them separately (at big performance expense).
+        if linewidth > 0 and color_kwds.get('alpha', 0.5) < 1.0:
+            # Plot the fill with default or user-specified alpha, but do not
+            # draw outlines.
+            plot_polygon_collection(ax, polys, colors[poly_idx], False,
+                                    linewidth=0, **color_kwds)
+            # Draw the edges, fully opaque, but no facecolor.
+            edges_kwds = color_kwds.copy()
+            edges_kwds['alpha'] = 1
+            edges_kwds['facecolor'] = 'none'
+            plot_polygon_collection(ax, polys, colors[poly_idx], False,
+                                    linewidth=linewidth, **edges_kwds)
         else:
-            col = color
-        if geom.type == 'Polygon' or geom.type == 'MultiPolygon':
-            if 'facecolor' in color_kwds:
-                plot_multipolygon(ax, geom, linewidth=linewidth, **color_kwds)
-            else:
-                plot_multipolygon(ax, geom, facecolor=col, linewidth=linewidth, **color_kwds)
-        elif geom.type == 'LineString' or geom.type == 'MultiLineString':
-            plot_multilinestring(ax, geom, color=col, linewidth=linewidth, **color_kwds)
-        elif geom.type == 'Point':
-            plot_point(ax, geom, color=col, **color_kwds)
+            # Optimization: if no alpha on fill, or if no edges, we can plot
+            # everything in one go.
+            plot_polygon_collection(ax, polys, colors[poly_idx], False,
+                                    linewidth=linewidth, **color_kwds)
+
+    # plot all LineStrings and MultiLineString components in same collection
+    line_idx = np.array(
+        (s.geometry.type == 'LineString') |
+        (s.geometry.type == 'MultiLineString'))
+    lines = s.geometry[line_idx]
+    if not lines.empty:
+        plot_linestring_collection(ax, lines, colors[line_idx], False,
+                                   linewidth=linewidth, **color_kwds)
+
+    point_idx = np.array(s.geometry.type == 'Point')
+    points = s.geometry[point_idx]
+    if not points.empty:
+        plot_point_collection(ax, points, colors[point_idx], **color_kwds)
+
     plt.draw()
     return ax
 
@@ -169,7 +351,7 @@ def plot_dataframe(s, column=None, cmap=None, color=None, linewidth=1.0,
             geometries can be plotted.
 
         column : str (default None)
-            The name of the column to be plotted.
+            The name of the column to be plotted. Ignored if `color` is also set.
 
         categorical : bool (default False)
             If False, cmap will reflect numerical values of the
@@ -186,8 +368,7 @@ def plot_dataframe(s, column=None, cmap=None, color=None, linewidth=1.0,
             Line width for geometries.
 
         legend : bool (default False)
-            Plot a legend (Experimental; currently for categorical
-            plots only)
+            Plot a legend. Ignored if no `column` is given, or if `color` is given.
 
         ax : matplotlib.pyplot.Artist (default None)
             axes on which to draw the plot
@@ -228,6 +409,10 @@ def plot_dataframe(s, column=None, cmap=None, color=None, linewidth=1.0,
         warnings.warn("'axes' is deprecated, please use 'ax' instead "
                       "(for consistency with pandas)", FutureWarning)
         ax = color_kwds.pop('axes')
+    if column and color:
+        warnings.warn("Only specify one of 'column' or 'color'. Using 'color'.",
+                      SyntaxWarning)
+        column = None
 
     import matplotlib.pyplot as plt
     from matplotlib.lines import Line2D
@@ -238,52 +423,94 @@ def plot_dataframe(s, column=None, cmap=None, color=None, linewidth=1.0,
         return plot_series(s.geometry, cmap=cmap, color=color,
                            ax=ax, linewidth=linewidth, figsize=figsize,
                            **color_kwds)
+
+    if s[column].dtype is np.dtype('O'):
+        categorical = True
+
+    # Define `values` as a Series
+    if categorical:
+        if cmap is None:
+            cmap = 'Set1'
+        categories = list(set(s[column].values))
+        categories.sort()
+        valuemap = dict([(k, v) for (v, k) in enumerate(categories)])
+        values = np.array([valuemap[k] for k in s[column]])
     else:
-        if s[column].dtype is np.dtype('O'):
-            categorical = True
+        values = s[column]
+    if scheme is not None:
+        binning = __pysal_choro(values, scheme, k=k)
+        values = np.array(binning.yb)
+        # set categorical to True for creating the legend
+        categorical = True
+        binedges = [binning.yb.min()] + binning.bins.tolist()
+        categories = ['{0:.2f} - {1:.2f}'.format(binedges[i], binedges[i+1])
+                      for i in range(len(binedges)-1)]
+    if ax is None:
+        fig, ax = plt.subplots(figsize=figsize)
+        ax.set_aspect('equal')
+
+    mn = values.min() if vmin is None else vmin
+    mx = values.max() if vmax is None else vmax
+
+    # plot all Polygons and all MultiPolygon components in the same collection
+    poly_idx = np.array(
+        (s.geometry.type == 'Polygon') | (s.geometry.type == 'MultiPolygon'))
+    polys = s.geometry[poly_idx]
+    if not polys.empty:
+        # Legacy behavior applies alpha to fill but not to edges. This requires
+        # plotting them separately (at big performance expense).
+        if linewidth > 0 and color_kwds.get('alpha', 0.5) < 1.0:
+            # Plot the fill with default or user-specified alpha, but do not
+            # draw outlines.
+            plot_polygon_collection(ax, polys, values[poly_idx], True,
+                                    vmin=mn, vmax=mx, cmap=cmap,
+                                    linewidth=0, **color_kwds)
+            # Draw the edges, fully opaque, but no facecolor.
+            edges_kwds = color_kwds.copy()
+            edges_kwds['alpha'] = 1
+            edges_kwds['facecolor'] = 'none'
+            # Setting plot_values=False would cause the array values' colors to
+            # override edgecolor. By setting color instead, matplotlib will
+            # respect edgecolor if set.
+            plot_polygon_collection(ax, polys, ['black'] * len(polys), False,
+                                    linewidth=linewidth, **edges_kwds)
+        else:
+            # Optimization: if no alpha on fill, or if no edges, we can plot
+            # everything in one go.
+            plot_polygon_collection(ax, polys, values[poly_idx], True,
+                                    vmin=mn, vmax=mx, cmap=cmap,
+                                    linewidth=linewidth, **color_kwds)
+
+    # plot all LineStrings and MultiLineString components in same collection
+    line_idx = np.array(
+        (s.geometry.type == 'LineString') |
+        (s.geometry.type == 'MultiLineString'))
+    lines = s.geometry[line_idx]
+    if not lines.empty:
+        plot_linestring_collection(ax, lines, values[line_idx], True,
+                                   vmin=mn, vmax=mx, cmap=cmap,
+                                   linewidth=linewidth, **color_kwds)
+
+    point_idx = np.array(s.geometry.type == 'Point')
+    points = s.geometry[point_idx]
+    if not points.empty:
+        plot_point_collection(ax, points, values[point_idx],
+                              vmin=mn, vmax=mx, cmap=cmap, **color_kwds)
+
+    if legend and not color:
+        norm = Normalize(vmin=mn, vmax=mx)
+        n_cmap = cm.ScalarMappable(norm=norm, cmap=cmap)
         if categorical:
-            if cmap is None:
-                cmap = 'Set1'
-            categories = list(set(s[column].values))
-            categories.sort()
-            valuemap = dict([(k, v) for (v, k) in enumerate(categories)])
-            values = [valuemap[k] for k in s[column]]
+            patches = []
+            for value, cat in enumerate(categories):
+                patches.append(Line2D([0], [0], linestyle="none",
+                                      marker="o", alpha=color_kwds.get('alpha', 0.5),
+                                      markersize=10, markerfacecolor=n_cmap.to_rgba(value)))
+            ax.legend(patches, categories, numpoints=1, loc='best')
         else:
-            values = s[column]
-        if scheme is not None:
-            binning = __pysal_choro(values, scheme, k=k)
-            values = binning.yb
-            # set categorical to True for creating the legend
-            categorical = True
-            binedges = [binning.yb.min()] + binning.bins.tolist()
-            categories = ['{0:.2f} - {1:.2f}'.format(binedges[i], binedges[i+1])
-                          for i in range(len(binedges)-1)]
-        cmap = norm_cmap(values, cmap, Normalize, cm, vmin=vmin, vmax=vmax)
-        if ax is None:
-            fig, ax = plt.subplots(figsize=figsize)
-            ax.set_aspect('equal')
-        for geom, value in zip(s.geometry, values):
-            if color is None:
-                col = cmap.to_rgba(value)
-            else:
-                col = color
-            if geom.type == 'Polygon' or geom.type == 'MultiPolygon':
-                plot_multipolygon(ax, geom, facecolor=col, linewidth=linewidth, **color_kwds)
-            elif geom.type == 'LineString' or geom.type == 'MultiLineString':
-                plot_multilinestring(ax, geom, color=col, linewidth=linewidth, **color_kwds)
-            elif geom.type == 'Point':
-                plot_point(ax, geom, color=col, **color_kwds)
-        if legend:
-            if categorical:
-                patches = []
-                for value, cat in enumerate(categories):
-                    patches.append(Line2D([0], [0], linestyle="none",
-                                          marker="o", alpha=color_kwds.get('alpha', 0.5),
-                                          markersize=10, markerfacecolor=cmap.to_rgba(value)))
-                ax.legend(patches, categories, numpoints=1, loc='best')
-            else:
-                # TODO: show a colorbar
-                raise NotImplementedError
+            n_cmap.set_array([])
+            ax.get_figure().colorbar(n_cmap)
+
     plt.draw()
     return ax
 
@@ -332,42 +559,3 @@ class identifier and the bins.
         return binning
     except ImportError:
         raise ImportError("PySAL is required to use the 'scheme' keyword")
-
-
-def norm_cmap(values, cmap, normalize, cm, vmin=None, vmax=None):
-
-    """ Normalize and set colormap
-
-        Parameters
-        ----------
-
-        values
-            Series or array to be normalized
-
-        cmap
-            matplotlib Colormap
-
-        normalize
-            matplotlib.colors.Normalize
-
-        cm
-            matplotlib.cm
-
-        vmin
-            Minimum value of colormap. If None, uses min(values).
-
-        vmax
-            Maximum value of colormap. If None, uses max(values).
-
-        Returns
-        -------
-        n_cmap
-            mapping of normalized values to colormap (cmap)
-
-    """
-
-    mn = min(values) if vmin is None else vmin
-    mx = max(values) if vmax is None else vmax
-    norm = normalize(vmin=mn, vmax=mx)
-    n_cmap = cm.ScalarMappable(norm=norm, cmap=cmap)
-    return n_cmap


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
@@ -1,119 +1,20 @@
 from __future__ import absolute_import, division
 
+import itertools
 import numpy as np
-import os
-import shutil
-import tempfile
-from distutils.version import LooseVersion
+import warnings
 
 import matplotlib
 matplotlib.use('Agg', warn=False)
-from matplotlib.pyplot import Artist, savefig, clf, cm, get_cmap
-from matplotlib.testing.noseclasses import ImageComparisonFailure
-from matplotlib.testing.compare import compare_images
-from numpy import cos, sin, pi
-from shapely.geometry import Polygon, LineString, Point
+from matplotlib.pyplot import get_cmap
+from shapely.affinity import rotate
+from shapely.geometry import MultiPolygon, Polygon, LineString, Point
 from six.moves import xrange
 from .util import unittest
 
 from geopandas import GeoSeries, GeoDataFrame, read_file
 
 
-# If set to True, generate images rather than perform tests (all tests will pass!)
-GENERATE_BASELINE = False
-
-BASELINE_DIR = os.path.join(os.path.dirname(__file__), 'baseline_images', 'test_plotting')
-
-TRAVIS = bool(os.environ.get('TRAVIS', False))
-MPL_DEV = matplotlib.__version__ > LooseVersion('1.5.1')
-
-class TestImageComparisons(unittest.TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        cls.tempdir = tempfile.mkdtemp()
-        return
-
-    @classmethod
-    def tearDownClass(cls):
-        shutil.rmtree(cls.tempdir)
-        return
-
-    def _compare_images(self, ax, filename, tol=10):
-        """ Helper method to do the comparisons """
-        assert isinstance(ax, Artist)
-        if GENERATE_BASELINE:
-            savefig(os.path.join(BASELINE_DIR, filename))
-        savefig(os.path.join(self.tempdir, filename))
-        err = compare_images(os.path.join(BASELINE_DIR, filename),
-                             os.path.join(self.tempdir, filename),
-                             tol, in_decorator=True)
-        if err:
-            raise ImageComparisonFailure('images not close: %(actual)s '
-                                         'vs. %(expected)s '
-                                         '(RMS %(rms).3f)' % err)
-
-    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
-    def test_poly_plot(self):
-        """ Test plotting a simple series of polygons """
-        clf()
-        filename = 'poly_plot.png'
-        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
-        t2 = Polygon([(1, 0), (2, 0), (2, 1)])
-        polys = GeoSeries([t1, t2])
-        ax = polys.plot()
-        self._compare_images(ax=ax, filename=filename)
-
-    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
-    def test_point_plot(self):
-        """ Test plotting a simple series of points """
-        clf()
-        filename = 'points_plot.png'
-        N = 10
-        points = GeoSeries(Point(i, i) for i in xrange(N))
-        ax = points.plot()
-        self._compare_images(ax=ax, filename=filename)
-
-    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
-    def test_line_plot(self):
-        """ Test plotting a simple series of lines """
-        clf()
-        filename = 'lines_plot.png'
-        N = 10
-        lines = GeoSeries([LineString([(0, i), (9, i)]) for i in xrange(N)])
-        ax = lines.plot()
-        self._compare_images(ax=ax, filename=filename)
-
-    @unittest.skipIf(TRAVIS, 'Skip on Travis (fails even though it passes locally)')
-    def test_plot_GeoDataFrame_with_kwargs(self):
-        """
-        Test plotting a simple GeoDataFrame consisting of a series of polygons
-        with increasing values using various extra kwargs.
-        """
-        clf()
-        filename = 'poly_plot_with_kwargs.png'
-        ts = np.linspace(0, 2*pi, 10, endpoint=False)
-
-        # Build GeoDataFrame from a series of triangles wrapping around in a ring
-        # and a second column containing a list of increasing values.
-        r1 = 1.0  # radius of inner ring boundary
-        r2 = 1.5  # radius of outer ring boundary
-
-        def make_triangle(t0, t1):
-            return Polygon([(r1*cos(t0), r1*sin(t0)),
-                            (r2*cos(t0), r2*sin(t0)),
-                            (r1*cos(t1), r1*sin(t1))])
-
-        polys = GeoSeries([make_triangle(t0, t1) for t0, t1 in zip(ts, ts[1:])])
-        values = np.arange(len(polys))
-        df = GeoDataFrame({'geometry': polys, 'values': values})
-
-        # Plot the GeoDataFrame using various keyword arguments to see if they are honoured
-        ax = df.plot(column='values', cmap=cm.RdBu, vmin=+2, vmax=None, figsize=(8, 4))
-        self._compare_images(ax=ax, filename=filename)
-
-
-
 class TestPointPlotting(unittest.TestCase):
 
     def setUp(self):
@@ -123,7 +24,14 @@ def setUp(self):
         values = np.arange(self.N)
         self.df = GeoDataFrame({'geometry': self.points, 'values': values})
 
-    @unittest.skipIf(MPL_DEV, 'Skip for development version of matplotlib')
+    def test_figsize(self):
+
+        ax = self.points.plot(figsize=(1, 1))
+        np.testing.assert_array_equal(ax.figure.get_size_inches(), (1, 1))
+
+        ax = self.df.plot(figsize=(1, 1))
+        np.testing.assert_array_equal(ax.figure.get_size_inches(), (1, 1))
+
     def test_default_colors(self):
 
         ## without specifying values -> max 9 different colors
@@ -132,21 +40,19 @@ def test_default_colors(self):
         ax = self.points.plot()
         cmap = get_cmap('Set1', 9)
         expected_colors = cmap(list(range(9))*2)
-        _check_colors(ax.get_lines(), expected_colors)
+        _check_colors(self.N, ax.collections[0], expected_colors)
 
         # GeoDataFrame -> uses 'jet' instead of 'Set1'
         ax = self.df.plot()
         cmap = get_cmap('jet', 9)
         expected_colors = cmap(list(range(9))*2)
-        _check_colors(ax.get_lines(), expected_colors)
-
-        ## with specifying values
+        _check_colors(self.N, ax.collections[0], expected_colors)
 
+        ## with specifying values -> different colors for all 10 values
         ax = self.df.plot(column='values')
         cmap = get_cmap('jet')
         expected_colors = cmap(np.arange(self.N)/(self.N-1))
-
-        _check_colors(ax.get_lines(), expected_colors)
+        _check_colors(self.N, ax.collections[0], expected_colors)
 
     def test_colormap(self):
 
@@ -156,44 +62,72 @@ def test_colormap(self):
         ax = self.points.plot(cmap='RdYlGn')
         cmap = get_cmap('RdYlGn', 9)
         expected_colors = cmap(list(range(9))*2)
-        _check_colors(ax.get_lines(), expected_colors)
+        _check_colors(self.N, ax.collections[0], expected_colors)
 
         # GeoDataFrame -> same as GeoSeries in this case
         ax = self.df.plot(cmap='RdYlGn')
-        _check_colors(ax.get_lines(), expected_colors)
-
-        ## with specifying values
+        _check_colors(self.N, ax.collections[0], expected_colors)
 
+        ## with specifying values -> different colors for all 10 values
         ax = self.df.plot(column='values', cmap='RdYlGn')
         cmap = get_cmap('RdYlGn')
         expected_colors = cmap(np.arange(self.N)/(self.N-1))
-        _check_colors(ax.get_lines(), expected_colors)
+        _check_colors(self.N, ax.collections[0], expected_colors)
 
     def test_single_color(self):
 
         ax = self.points.plot(color='green')
-        _check_colors(ax.get_lines(), ['green']*self.N)
+        _check_colors(self.N, ax.collections[0], ['green']*self.N)
 
         ax = self.df.plot(color='green')
-        _check_colors(ax.get_lines(), ['green']*self.N)
+        _check_colors(self.N, ax.collections[0], ['green']*self.N)
 
-        ax = self.df.plot(column='values', color='green')
-        _check_colors(ax.get_lines(), ['green']*self.N)
+        with warnings.catch_warnings(record=True) as _:  # don't print warning
+            # 'color' overrides 'column'
+            ax = self.df.plot(column='values', color='green')
+            _check_colors(self.N, ax.collections[0], ['green']*self.N)
 
     def test_style_kwargs(self):
 
         # markersize
         ax = self.points.plot(markersize=10)
-        ms = [l.get_markersize() for l in ax.get_lines()]
-        assert ms == [10] * self.N
+        assert ax.collections[0].get_sizes() == [10]
 
         ax = self.df.plot(markersize=10)
-        ms = [l.get_markersize() for l in ax.get_lines()]
-        assert ms == [10] * self.N
+        assert ax.collections[0].get_sizes() == [10]
 
         ax = self.df.plot(column='values', markersize=10)
-        ms = [l.get_markersize() for l in ax.get_lines()]
-        assert ms == [10] * self.N
+        assert ax.collections[0].get_sizes() == [10]
+
+    def test_legend(self):
+        with warnings.catch_warnings(record=True) as _:  # don't print warning
+            # legend ignored if color is given.
+            ax = self.df.plot(column='values', color='green', legend=True)
+            assert len(ax.get_figure().axes) == 1  # no separate legend axis
+
+        # legend ignored if no column is given.
+        ax = self.df.plot(legend=True)
+        assert len(ax.get_figure().axes) == 1  # no separate legend axis
+
+        # Continuous legend
+        ## the colorbar matches the Point colors
+        ax = self.df.plot(column='values', cmap='RdYlGn', legend=True)
+        point_colors = ax.collections[0].get_facecolors()
+        cbar_colors = ax.get_figure().axes[1].collections[0].get_facecolors()
+        ### first point == bottom of colorbar
+        np.testing.assert_array_equal(point_colors[0], cbar_colors[0])
+        ### last point == top of colorbar
+        np.testing.assert_array_equal(point_colors[-1], cbar_colors[-1])
+
+        # Categorical legend
+        ## the colorbar matches the Point colors
+        ax = self.df.plot(column='values', categorical=True, legend=True)
+        point_colors = ax.collections[0].get_facecolors()
+        cbar_colors = ax.get_legend().axes.collections[0].get_facecolors()
+        ### first point == bottom of colorbar
+        np.testing.assert_array_equal(point_colors[0], cbar_colors[0])
+        ### last point == top of colorbar
+        np.testing.assert_array_equal(point_colors[-1], cbar_colors[-1])
 
 
 class TestLineStringPlotting(unittest.TestCase):
@@ -202,34 +136,52 @@ def setUp(self):
 
         self.N = 10
         values = np.arange(self.N)
-        self.lines = GeoSeries([LineString([(0, i), (9, i)]) for i in xrange(self.N)])
+        self.lines = GeoSeries([LineString([(0, i), (4, i+0.5), (9, i)])
+                                for i in xrange(self.N)],
+                               index=list('ABCDEFGHIJ'))
         self.df = GeoDataFrame({'geometry': self.lines, 'values': values})
 
     def test_single_color(self):
 
         ax = self.lines.plot(color='green')
-        _check_colors(ax.get_lines(), ['green']*self.N)
+        _check_colors(self.N, ax.collections[0], ['green']*self.N)
 
         ax = self.df.plot(color='green')
-        _check_colors(ax.get_lines(), ['green']*self.N)
+        _check_colors(self.N, ax.collections[0], ['green']*self.N)
 
-        ax = self.df.plot(column='values', color='green')
-        _check_colors(ax.get_lines(), ['green']*self.N)
+        with warnings.catch_warnings(record=True) as _:  # don't print warning
+            # 'color' overrides 'column'
+            ax = self.df.plot(column='values', color='green')
+            _check_colors(self.N, ax.collections[0], ['green']*self.N)
 
     def test_style_kwargs(self):
 
+        def linestyle_tuple_to_string(tup):
+            """ Converts a linestyle of the form `(offset, onoffseq)`, as
+                documented in `Collections.set_linestyle`, to a string
+                representation, namely one of:
+                    { 'dashed',  'dotted', 'dashdot', 'solid' }.
+            """
+            from matplotlib.backend_bases import GraphicsContextBase
+            reverse_idx = dict((v, k)
+                               for k, v in GraphicsContextBase.dashd.items())
+            return reverse_idx[tup]
+
         # linestyle
         ax = self.lines.plot(linestyle='dashed')
-        ls = [l.get_linestyle() for l in ax.get_lines()]
-        assert ls == ['--'] * self.N
+        ls = [linestyle_tuple_to_string(l)
+              for l in ax.collections[0].get_linestyles()]
+        assert ls == ['dashed']
 
         ax = self.df.plot(linestyle='dashed')
-        ls = [l.get_linestyle() for l in ax.get_lines()]
-        assert ls == ['--'] * self.N
+        ls = [linestyle_tuple_to_string(l)
+              for l in ax.collections[0].get_linestyles()]
+        assert ls == ['dashed']
 
         ax = self.df.plot(column='values', linestyle='dashed')
-        ls = [l.get_linestyle() for l in ax.get_lines()]
-        assert ls == ['--'] * self.N
+        ls = [linestyle_tuple_to_string(l)
+              for l in ax.collections[0].get_linestyles()]
+        assert ls == ['dashed']
 
 
 class TestPolygonPlotting(unittest.TestCase):
@@ -238,36 +190,121 @@ def setUp(self):
 
         t1 = Polygon([(0, 0), (1, 0), (1, 1)])
         t2 = Polygon([(1, 0), (2, 0), (2, 1)])
-        self.polys = GeoSeries([t1, t2])
+        self.polys = GeoSeries([t1, t2], index=list('AB'))
         self.df = GeoDataFrame({'geometry': self.polys, 'values': [0, 1]})
+
+        multipoly1 = MultiPolygon([t1, t2])
+        multipoly2 = rotate(multipoly1, 180)
+        self.df2 = GeoDataFrame({'geometry': [multipoly1, multipoly2],
+                                 'values': [0, 1]})
         return
 
     def test_single_color(self):
 
         ax = self.polys.plot(color='green')
-        _check_colors(ax.patches, ['green']*2, alpha=0.5)
+        _check_colors(2, ax.collections[0], ['green']*2, alpha=0.5)
 
         ax = self.df.plot(color='green')
-        _check_colors(ax.patches, ['green']*2, alpha=0.5)
+        _check_colors(2, ax.collections[0], ['green']*2, alpha=0.5)
+
+        with warnings.catch_warnings(record=True) as _:  # don't print warning
+            # 'color' overrides 'values'
+            ax = self.df.plot(column='values', color='green')
+            _check_colors(2, ax.collections[0], ['green']*2, alpha=0.5)
+
+    def test_optimization(self):
+
+        # when linewidth=0 or no alpha, we don't have to plot polys twice
+
+        ax = self.polys.plot(linewidth=0)
+        assert len(ax.collections) == 1  # only plotted once
 
-        ax = self.df.plot(column='values', color='green')
-        _check_colors(ax.patches, ['green']*2, alpha=0.5)
+        ax = self.polys.plot(alpha=1)
+        assert len(ax.collections) == 1  # only plotted once
+
+        ax = self.df.plot(linewidth=0)
+        assert len(ax.collections) == 1  # only plotted once
+
+        ax = self.df.plot(alpha=1)
+        assert len(ax.collections) == 1  # only plotted once
 
     def test_vmin_vmax(self):
 
         # when vmin == vmax, all polygons should be the same color
+
+        # non-categorical
+        ax = self.df.plot(column='values', categorical=False, vmin=0, vmax=0)
+        actual_colors = ax.collections[0].get_facecolors()
+        np.testing.assert_array_equal(actual_colors[0], actual_colors[1])
+
+        # categorical
         ax = self.df.plot(column='values', categorical=True, vmin=0, vmax=0)
-        cmap = get_cmap('Set1', 2)
-        self.assertEqual(ax.patches[0].get_facecolor(), ax.patches[1].get_facecolor())
+        actual_colors = ax.collections[0].get_facecolors()
+        np.testing.assert_array_equal(actual_colors[0], actual_colors[1])
 
-    def test_facecolor(self):
-        t1 = Polygon([(0, 0), (1, 0), (1, 1)])
-        t2 = Polygon([(1, 0), (2, 0), (2, 1)])
-        polys = GeoSeries([t1, t2])
-        df = GeoDataFrame({'geometry': polys, 'values': [0, 1]})
+    def test_style_kwargs(self):
+
+        # facecolor overrides default cmap when color is not set
+        ax = self.polys.plot(facecolor='k')
+        _check_colors(2, ax.collections[0], ['k']*2, alpha=0.5)
 
-        ax = polys.plot(facecolor='k')
-        _check_colors(ax.patches, ['k']*2, alpha=0.5)
+        # facecolor overrides more general-purpose color when both are set
+        ax = self.polys.plot(color='red', facecolor='k')
+        _check_colors(2, ax.collections[0], ['k']*2, alpha=0.5)
+
+        # edgecolor
+        ax = self.polys.plot(edgecolor='red')
+        np.testing.assert_array_equal([(1, 0, 0, 0.5)],
+                                      ax.collections[0].get_edgecolors())
+
+        ax = self.df.plot('values', edgecolor='red')
+        np.testing.assert_array_equal([(1, 0, 0, 0.5)],
+                                      ax.collections[0].get_edgecolors())
+
+    def test_multipolygons(self):
+
+        # MultiPolygons
+        ax = self.df2.plot()
+        assert len(ax.collections[0].get_paths()) == 4
+        cmap = get_cmap('jet', 2)
+        ## colors are repeated for all components within a MultiPolygon
+        expected_colors = [cmap(0), cmap(0), cmap(1), cmap(1)]
+        _check_colors(4, ax.collections[0], expected_colors, alpha=0.5)
+
+        ax = self.df2.plot('values')
+        ## specifying values -> same as without values in this case.
+        _check_colors(4, ax.collections[0], expected_colors, alpha=0.5)
+
+
+class TestNonuniformGeometryPlotting(unittest.TestCase):
+
+    def setUp(self):
+
+        poly = Polygon([(1, 0), (2, 0), (2, 1)])
+        line = LineString([(0.5, 0.5), (1, 1), (1, 0.5), (1.5, 1)])
+        point = Point(0.75, 0.25)
+        self.series = GeoSeries([poly, line, point])
+        self.df = GeoDataFrame({'geometry': self.series, 'values': [1, 2, 3]})
+        return
+
+    def test_colormap(self):
+
+        ax = self.series.plot(cmap='RdYlGn')
+        cmap = get_cmap('RdYlGn', 3)
+        # polygon gets extra alpha. See #266
+        _check_colors(1, ax.collections[0], [cmap(0)], alpha=0.5)
+        # N.B. ax.collections[1] contains the edges of the polygon
+        _check_colors(1, ax.collections[2], [cmap(1)], alpha=1)   # line
+        _check_colors(1, ax.collections[3], [cmap(2)], alpha=1)   # point
+
+    def test_style_kwargs(self):
+
+        # markersize -> only the Point gets it
+        ax = self.series.plot(markersize=10)
+        assert ax.collections[3].get_sizes() == [10]
+
+        ax = self.df.plot(markersize=10)
+        assert ax.collections[3].get_sizes() == [10]
 
 
 class TestPySALPlotting(unittest.TestCase):
@@ -284,27 +321,44 @@ def setUpClass(cls):
 
     def test_legend(self):
         ax = self.tracts.plot(column='CRIME', scheme='QUANTILES', k=3,
-                         cmap='OrRd', legend=True)
+                              cmap='OrRd', legend=True)
 
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
         expected = [u'0.00 - 26.07', u'26.07 - 41.97', u'41.97 - 68.89']
         self.assertEqual(labels, expected)
 
 
-def _check_colors(collection, expected_colors, alpha=None):
-
-    from matplotlib.lines import Line2D
+def _check_colors(N, collection, expected_colors, alpha=None):
+    """ Asserts that the members of `collection` match the `expected_colors` (in order)
+
+    Parameters
+    ----------
+    N : int
+        The number of geometries believed to be in collection.
+        matplotlib.collection is implemented such that the number of geoms in
+        `collection` doesn't have to match the number of colors assignments in
+        the collection: the colors will cycle to meet the needs of the geoms.
+        `N` helps us resolve this.
+    collection : matplotlib.collections.Collection
+        The colors of this collection's patches are read from `collection.get_facecolors()`
+    expected_colors : sequence of RGBA tuples
+    alpha : float (optional)
+        If set, this alpha transparency will be applied to the `expected_colors`.
+        (Any transparency on the `collecton` is assumed to be set in its own
+        facecolor RGBA tuples.)
+    """
     import matplotlib.colors as colors
     conv = colors.colorConverter
 
-    for patch, color in zip(collection, expected_colors):
-        if isinstance(patch, Line2D):
-            # points/lines
-            result = patch.get_color()
-        else:
-            # polygons
-            result = patch.get_facecolor()
-        assert conv.to_rgba(result) == conv.to_rgba(color, alpha=alpha)
+    # Convert 2D numpy array to a list of RGBA tuples.
+    actual_colors = list(collection.get_facecolors())
+    actual_colors = map(tuple, actual_colors)
+    all_actual_colors = list(itertools.islice(
+        itertools.cycle(actual_colors), N))
+
+    for actual, expected in zip(all_actual_colors, expected_colors):
+        assert actual == conv.to_rgba(expected, alpha=alpha), \
+            '{} != {}'.format(actual, conv.to_rgba(expected, alpha=alpha))
 
 
 if __name__ == '__main__':