Merge pull request #267 from IamJeffG/259-plotting-collections

Implement plotting using matplotlib Collections
This commit is contained in:
Kelsey Jordahl
2016-12-28 14:25:51 -06:00
committed by GitHub
2 changed files with 537 additions and 295 deletions
+321 -133
View File
@@ -3,55 +3,206 @@ from __future__ import print_function
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
# 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)
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_multipolygon(ax, geom, facecolor='red', edgecolor='black', alpha=0.5, linewidth=1.0, **kwargs):
""" Can safely call with either Polygon or Multipolygon geometry
def plot_linestring_collection(ax, geoms, colors_or_values, plot_values,
vmin=None, vmax=None, cmap=None,
linewidth=1.0, **kwargs):
"""
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)
Plots a collection of LineString and MultiLineString geometries to `ax`
Parameters
----------
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)
ax : matplotlib.axes.Axes
where shapes will be plotted
geoms : a sequence of `N` LineStrings and/or MultiLineStrings (can be mixed)
def plot_multilinestring(ax, geom, color='red', linewidth=1.0, **kwargs):
""" Can safely call with either LineString or MultiLineString geometry
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
"""
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)
from matplotlib.collections import LineCollection
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)
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(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)
def plot_point_collection(ax, geoms, colors_or_values,
vmin=None, vmax=None, cmap=None,
marker='o', markersize=2, **kwargs):
"""
Plots a collection of Point geometries to `ax`
Parameters
----------
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
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]]
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:
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
# 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:
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:
n_cmap.set_array([])
ax.get_figure().colorbar(n_cmap)
plt.draw()
return ax
@@ -325,42 +552,3 @@ def __pysal_choro(values, scheme, k=5):
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
+216 -162
View File
@@ -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 @@ class TestPointPlotting(unittest.TestCase):
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 @@ class TestPointPlotting(unittest.TestCase):
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 @@ class TestPointPlotting(unittest.TestCase):
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 @@ class TestLineStringPlotting(unittest.TestCase):
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 @@ class TestPolygonPlotting(unittest.TestCase):
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)
ax = self.df.plot(column='values', color='green')
_check_colors(ax.patches, ['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.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):
ax = polys.plot(facecolor='k')
_check_colors(ax.patches, ['k']*2, alpha=0.5)
# 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)
# 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,7 +321,7 @@ class TestPySALPlotting(unittest.TestCase):
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']
@@ -297,20 +334,37 @@ class TestPySALPlotting(unittest.TestCase):
cmap='OrRd', legend=True)
def _check_colors(collection, expected_colors, alpha=None):
def _check_colors(N, collection, expected_colors, alpha=None):
""" Asserts that the members of `collection` match the `expected_colors` (in order)
from matplotlib.lines import Line2D
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__':