"""Plotting relies on cartopy to ensure that coordinate projections are dealt
with reasonably within matplotlib. The preferred usage for plotting is similar
to the non-pylab interface to matplotlib -- first get a figure and axis object,
then call plotting functions passing in that ax object.
Note that we use the descartes package to plot shapely objects, which is a
simple wrapper to write a shapely polygon as a matplotlib patch.
Note that, for complex plots, it can be useful to manage the ordering of the
layers of objects. In this case, all plotting functions accept matplotlib's
zorder argument, an int which controls the order of drawing, with larger being
later (on top) of smaller values.
"""
import logging
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import collections as pltc
from matplotlib import cm as pcm
import shapely
import rasterio
from mpl_toolkits.mplot3d import Axes3D
import watershed_workflow.utils
import watershed_workflow.crs
import watershed_workflow.colors
def _is_iter(obj):
try:
iter(obj)
except TypeError:
return False
return True
# plot reaches and modify...
#
# This uses the annotated axes
[docs]
class Labeler:
"""A labeling widget that can be attached to matplotlib figures to display info on-click.
When an geometry item (e.g. point, line, or polygon) is clicked on
the figure, that is mapped into the original WW object that
generated the geometry, and then run through a function to
generate a label that is written to the title of the figure.
Parameters
----------
ax : matplotlib.Axes object
The axes to work with.
items : list[tuple[artist, metadata, formatter]]
See documentation of the addItem() method.
"""
def __init__(self, ax, items=None):
self.ax = ax
self.items = []
for item in items:
self.addItem(*item)
self.ax.set_title("None")
self.selected = None
[docs]
def addItem(self, data, artist, formatter):
"""Adds an item to the list of things to label.
Parameters
----------
data : List[*]
A list of objects being labeled. This is likely the
underlying data, with properties, that was passed to
a ww.plot function.
artist : matplotlib collection
A matplotlib Collection, likely the return value of
a ww.plot call or similar.
formatter : function or str
A function that accepts an entry in data and returns a
string to label the item selected. If this is a string,
it is assumed to be a formattable string to which the
item's properties dictionary is passed.
"""
if isinstance(formatter, str):
def format_this(item):
return formatter.format(**dict(item)), list()
formatter = format_this
assert (len(artist) == len(metadata))
self.items.append((artist, metadata, formatter))
self.metadata = metadata
self._format = format
self._selected = []
[docs]
def deselect(self):
"""Clears anything plotted in the last click"""
for artist in self._selected:
artist.clear()
self._selected = []
[docs]
def select(self, i, j, xy):
"""Selects item i, collection index j, with a click at xy"""
data, artist, formatter = self.items[i]
if isinstance(data, list):
dat = data[j]
if isinstance(dat, shapely.geometry.base.BaseGeometry) and hasattr(dat, 'properties'):
dat = dict(geometry=dat, **dat.properties)
title = formatter(dat)
elif isinstance(data, pandas.DataFrame):
title = formatter(data.iloc[j])
self.ax.set_title(title)
# redraw LineStrings with markers
if isinstance(artist, matplotlib.collections.LineCollection):
line = artist.get_data()[i]
color = artist.get_colors()[i]
self._selected.append(self.ax.plot(line[:, 0], line[:, 1], '-x', color=color))
[docs]
def update(self, event):
"""Acts on click."""
print('event loc:', event.mouseevent.x, event.mouseevent.y)
print('event dict:', event.__dict__)
i = next(i for (i, item) in enumerate(self.items) if item[0] is event.artist)
self.select(i, 0, (event.mouseevent.x, event.mouseevent.y))
self.ax.get_figure().canvas.draw_idle()
[docs]
def get_ax(crs,
fig=None,
nrow=1,
ncol=1,
index=None,
window=None,
axgrid=None,
ax_kwargs=None,
**kwargs):
"""Returns an axis with a given projection.
Note this forwards extra kwargs for plt.figure().
Parameters
----------
crs : CRS object, optional
A **projected** CRS for the plot. None can be given for a normal
matplotlib axis (useful for plotting lat/lon or other non-projected
coordinate systems. Note that if you call plotting on an object with an
unprojected CRS, it will project for you, or change coordinates if
needed. This can get a bit dicey, so prefer to plot objects all in the
same CRS. Defaults to None.
fig : matplotlib figure, optional
If you already have a figure, will create the axis on this figure.
Defaults to None, at which point a figure will be created.
nrow, ncol, index : int, optional
Create a grid of axes of this shape. Calls
plt.add_subplot(nrow,ncol,index). Default is 1.
window : [xmin, ymin, width, height], optional
Matplotlib patch arguments for call to fig.add_axes()
figsize : (width, height), optional
Figure size in inches.
dpi : int, optional
Dots per inch for figures.
ax_kwargs : dict
Other arguments provided to the axes creation call.
kwargs : dict
Additional arguments to plt.figure()
Returns
-------
*If fig is provided*
ax : matplotlib axes object
*If fig is not provided*
fig : matplotlib figure object
ax : matplotlib ax object
"""
if ax_kwargs is None:
ax_kwargs = dict()
# make a figure
if fig is None:
fig = plt.figure(**kwargs)
newfig = True
else:
newfig = False
if window is None:
def _get_ax(axargs, ax_kwargs):
if crs is None:
# no crs, just get an ax -- you deal with it.
ax = fig.add_subplot(*axargs, **ax_kwargs)
elif crs == '3d':
# 3d plot
ax_kwargs['projection'] = '3d'
ax = fig.add_subplot(*axargs, **ax_kwargs)
else:
projection = watershed_workflow.crs.to_cartopy(crs)
ax_kwargs['projection'] = projection
ax = fig.add_subplot(*axargs, **ax_kwargs)
return ax
if axgrid is None:
if nrow == 1 and ncol == 1:
index = 1
if index is None:
ax = [[_get_ax([nrow, ncol, i * (ncol) + j + 1], ax_kwargs) for j in range(ncol)]
for i in range(nrow)]
if nrow == 1:
ax = ax[0]
elif ncol == 1:
ax = [a[0] for a in ax]
else:
ax = _get_ax([nrow, ncol, index], ax_kwargs)
else:
ax = _get_ax([axgrid, ], ax_kwargs)
else:
if crs is None:
fig.add_axes(window)
ax = fig.gca()
elif crs == '3d':
ax = Axes3D(fig, rect=window)
else:
projection = watershed_workflow.crs.to_cartopy(crs)
fig.add_axes(window, projection=projection)
ax = fig.gca()
if newfig:
return fig, ax
else:
return ax
[docs]
def huc(huc, crs, color='k', ax=None, **kwargs):
"""Plot a HUC polygon.
A wrapper for plot.shply()
Parameters
----------
huc : shapely polygon
An object to plot.
crs : CRS object
The coordinate system to plot in.
color : str, scalar, or array-like, optional
See https://matplotlib.org/tutorials/colors/colors.html
ax : matplotib axes object, optional
Axes to plot on. Calls get_ax() if not provided.
kwargs : dict
Extra arguments passed to the plotting method, which is likely
descartes.PolygonPatch.
Returns
-------
patches : matplotlib PatchCollection
"""
return shply([huc, ], crs, color, ax, **kwargs)
[docs]
def hucs(hucs,
crs,
color='k',
ax=None,
outlet_marker=None,
outlet_markersize=100,
outlet_markercolor='green',
**kwargs):
"""Plot a SplitHUCs object.
A wrapper for plot.shply()
Parameters
----------
hucs : watershed_workflow.split_hucs.SplitHucs object
The collection of hucs to plot.
crs : CRS object
The coordinate system to plot in.
color : str, scalar, or array-like, optional
See https://matplotlib.org/tutorials/colors/colors.html
ax : matplotib axes object, optional
Axes to plot on. Calls get_ax() if not provided.
outlet_marker : matplotlib marker string, optional
If provided, also plots the actual points that make up the shape.
outlet_markersize : float, optional
Size of the outlet marker.
outlet_markercolor : matplotlib color string, optional
Color of the outlet marker.
kwargs : dict
Extra arguments passed to the plotting method, which is likely
descartes.PolygonPatch.
Returns
-------
patches : matplotib PatchCollection
"""
ps = list(hucs.polygons())
polys = shply(ps, crs, color, ax, **kwargs)
if hucs.polygon_outlets is not None and ax is not None and outlet_marker is not None:
x = np.array([
p.xy[0][0] for p in hucs.polygon_outlets
if not watershed_workflow.utils.is_empty_shapely(p)
])
y = np.array([
p.xy[1][0] for p in hucs.polygon_outlets
if not watershed_workflow.utils.is_empty_shapely(p)
])
c = [
c for (c, p) in zip(outlet_markercolor, hucs.polygon_outlets)
if not watershed_workflow.utils.is_empty_shapely(p)
]
ax.scatter(x, y, s=outlet_markersize, marker=outlet_marker, c=c)
[docs]
def shapes(shps, crs, color='k', ax=None, **kwargs):
"""Plot an itereable collection of fiona shapes.
A wrapper for plot.shply()
Parameters
----------
shapes : list(fiona shape objects)
The collection of fiona shape objects to plot.
crs : CRS object
The coordinate system to plot in.
color : str, scalar, or array-like, optional
See https://matplotlib.org/tutorials/colors/colors.html
ax : matplotib axes object, optional
Axes to plot on. Calls get_ax() if not provided.
kwargs : dict
Extra arguments passed to the plotting method, which is likely
descartes.PolygonPatch.
Returns
-------
patches : matplotib PatchCollection
"""
shplys = [watershed_workflow.utils.create_shply(shp) for shp in shps]
shply(shplys, crs, color, ax, **kwargs)
[docs]
def river(river, crs, color='b', ax=None, **kwargs):
"""Plot an itereable collection of reaches.
A wrapper for plot.shply()
Parameters
----------
river : list(shapely.LineString)
An iterable of shapely LineString reaches.
crs : CRS object
The coordinate system to plot in.
color : str, scalar, or array-like, optional
See https://matplotlib.org/tutorials/colors/colors.html
ax : matplotib axes object, optional
Axes to plot on. Calls get_ax() if not provided.
kwargs : dict
Extra arguments passed to the plotting method, which is likely
matplotlib.collections.LineCollection.
Returns
-------
lines : matplotib LineCollection
"""
shplys(river, crs, color, ax, **kwargs)
[docs]
def rivers(rivers, crs, color=None, ax=None, **kwargs):
"""Plot an itereable collection of river Tree objects.
A wrapper for plot.shply()
Parameters
----------
rivers : list(river_tree.RiverTree)
An iterable of river_tree.RiverTree objects.
crs : CRS object
The coordinate system to plot in.
color : str, scalar, or array-like, optional
See https://matplotlib.org/tutorials/colors/colors.html
ax : matplotib axes object, optional
Axes to plot on. Calls get_ax() if not provided.
kwargs : dict
Extra arguments passed to the plotting method, which is likely
matplotlib.collections.LineCollection.
Returns
-------
lines : matplotib LineCollection
"""
if color is None:
color = watershed_workflow.colors.enumerated_colors(len(rivers))
if type(color) is not str and len(color) == len(rivers):
for r, c in zip(rivers, color):
river(r, crs, c, ax, **kwargs)
else:
for r in rivers:
river(r, crs, color, ax, **kwargs)
[docs]
def shply(shp, *args, **kwargs):
"""Plot a single shapely object. See shplys() for options."""
if type(shp) is list:
return shplys(shp, *args, **kwargs)
else:
return shplys([shp, ], *args, **kwargs)
[docs]
def shplys(shps, crs, color=None, ax=None, marker=None, **kwargs):
"""Plot shapely objects.
Currently this assumes shps is an iterable collection of Points, Lines, or
Polygons. So while a single MultiPolygon is allowed, lists of
MultiPolygons are not currently supported. These can easily be unraveled.
Heterogeneous collections are not supported.
Parameters
----------
shps : shapely shape, list(shapely shape objects), or MultiShape object
An iterable of shapely objects to plot.
crs : CRS object
The coordinate system to plot in.
color : str, scalar, or array-like, optional
See https://matplotlib.org/tutorials/colors/colors.html
ax : matplotib axes object, optional
Axes to plot on. Calls get_ax() if not provided.
marker : matplotlib marker string
If provided, also plots the actual points that make up the shape.
kwargs : dict
Extra arguments passed to the plotting method, which can be:
* pyplot.scatter() (if shps are Point objects)
* matplotlib.collections.LineCollection() (if shps are LineStrings)
* descartes.PolygonPatch() (if shps are Polygons)
Returns
-------
col : collection of matplotlib points or lines or patches
"""
import descartes
try:
if len(shps) == 0:
return
except TypeError:
shps = [shps, ]
# get an axis and projection to work on
if ax is None:
fig, ax = get_ax(crs)
if not hasattr(ax, 'projection') or crs is None:
projection = None
else:
projection = watershed_workflow.crs.to_cartopy(crs)
# set default colors
if color is None:
color = watershed_workflow.colors.enumerated_colors(len(shps))
# update keyword arguments
if 'facecolor' not in kwargs:
kwargs['facecolor'] = 'none'
# markers cannot be used in collections, so we scatter them separately
marker_kwargs = dict()
if marker is not None:
marker_kwargs['marker'] = marker
if 'markersize' in kwargs:
marker_kwargs['s'] = kwargs.pop('markersize')
if type(next(iter(shps))) is shapely.geometry.Point:
# plot points
marker_kwargs.update(kwargs)
if 'marker' not in marker_kwargs:
marker_kwargs['marker'] = 'o'
points = np.array([p.coords for p in shps])[:, 0, :]
if projection is None:
res = ax.scatter(points[:, 0], points[:, 1], c=color, **marker_kwargs)
else:
res = ax.scatter(points[:, 0],
points[:, 1],
c=color,
transform=projection,
**marker_kwargs)
elif type(next(iter(shps))) is shapely.geometry.LineString:
# plot lines
if 'colors' not in kwargs:
kwargs['colors'] = color
if _is_iter(kwargs['colors']) and \
len(kwargs['colors']) == len(shps) and \
not _is_iter(next(iter(kwargs['colors']))):
# colormap!
colors = np.array(kwargs.pop('colors'))
if 'cmap' in kwargs: cmap = kwargs.pop('cmap')
else: cmap = None
if 'vmin' in kwargs: vmin = kwargs.pop('vmin')
else: vmin = np.nanmin(colors)
if 'vmax' in kwargs: vmax = kwargs.pop('vmax')
else: vmax = np.nanmax(colors)
cmapper = watershed_workflow.colors.cm_mapper(vmin, vmax, cmap)
colors = [cmapper(c) for c in colors]
kwargs['colors'] = colors
lines = [np.array(l.coords)[:, 0:2] for l in shps]
res = pltc.LineCollection(lines, **kwargs)
if projection is not None:
res.set_transform(projection)
if type(ax) is Axes3D:
res = ax.add_collection3d(res)
else:
res = ax.add_collection(res)
ax.autoscale()
if marker is not None:
points = np.array([c for l in lines for c in l])
if type(color) is str:
point_colors = color
else:
point_colors = np.array([color[i] for (i, l) in enumerate(lines) for c in l])
if projection is None:
ax.scatter(points[:, 0], points[:, 1], c=point_colors, **marker_kwargs)
else:
ax.scatter(points[:, 0],
points[:, 1],
c=point_colors,
transform=projection,
**marker_kwargs)
elif type(next(iter(shps))) in [shapely.geometry.Polygon, shapely.geometry.MultiPolygon]:
if kwargs['facecolor'] in ['color', 'edge']:
kwargs.pop('facecolor')
face_is_edge = True
else:
face_is_edge = False
if type(color) is str and color == 'elevation':
# compute colors from the mean elevation
color = [np.array(p.exterior.coords)[0:-1, 2].mean() for p in iter(shps)]
elif type(color) is str and color == 'area':
color = [p.area for p in iter(shps)]
elif type(color) is str and color == 'log10area':
color = np.log10(np.array([p.area for p in iter(shps)]))
try:
color_len = len(color)
except (AttributeError, TypeError):
color_len = -1
if color is None or type(color) is str or color_len != len(shps):
# assume this is ONE color, and therefore can add as a multipolygon/polygon collection
if color is None:
color = 'k'
if 'edgecolor' not in kwargs:
kwargs['edgecolor'] = color
if face_is_edge:
kwargs['facecolor'] = color
# first must flatten
def listify(thing):
if type(thing) is shapely.geometry.MultiPolygon:
return list(thing.geoms)
else:
return [thing, ]
multi_poly = shapely.geometry.MultiPolygon([l for shp in shps for l in listify(shp)])
patch = descartes.PolygonPatch(multi_poly, **kwargs)
if projection is not None:
patch.set_transform(projection)
if type(ax) is Axes3D:
res = ax.add_collection3d(patch)
else:
res = ax.add_patch(patch)
if marker is not None:
points = np.array([p for poly in multi_poly.geoms for p in poly.exterior.coords])
if projection is None:
pnts_res = ax.scatter(points[:, 0], points[:, 1], c=color, **marker_kwargs)
else:
pnts_res = ax.scatter(points[:, 0],
points[:, 1],
c=color,
transform=projection,
**marker_kwargs)
elif type(color[0]) is tuple or type(color[0]) is np.ndarray or type(color[0]) is str:
# list of colors
res = []
for shp in shps:
patch = descartes.PolygonPatch(shp, **kwargs)
res.append(patch)
res = pltc.PatchCollection(res, **kwargs)
if face_is_edge:
res.set_facecolor(color)
else:
res.set_edgecolor(color)
ax.add_collection(res)
if marker is not None:
points = np.array([p for poly in shps for p in poly.exterior.coords])
pcolors = np.array(
[color[i] for (i, poly) in enumerate(shps) for p in poly.exterior.coords])
if projection is None:
pnts_res = ax.scatter(points[:, 0], points[:, 1], c=pcolors, **marker_kwargs)
else:
pnts_res = ax.scatter(points[:, 0],
points[:, 1],
c=pcolors,
transform=projection,
**marker_kwargs)
else:
# list of scalars that will be used with cmap to define a color
if 'vmin' in kwargs:
vmin = kwargs.pop('vmin')
else:
vmin = np.nanmin(color)
if 'vmax' in kwargs:
vmax = kwargs.pop('vmax')
else:
vmax = np.nanmax(color)
clim = (vmin, vmax)
res = []
for shp in shps:
patch = descartes.PolygonPatch(shp)
# if projection is not None:
# patch.set_transform(projection)
res.append(patch)
res = pltc.PatchCollection(res, **kwargs)
res.set_array(color)
res.set_clim(clim)
#if face_is_edge:
print('kwargs = ', kwargs)
print('setting face color = ', color)
ax.add_collection(res)
if marker is not None:
points = np.array([p for poly in multi_poly.geoms for c in poly.exterior])
if projection is None:
res = ax.scatter(points[:, 0], points[:, 1], c=color, **marker_kwargs)
else:
res = ax.scatter(points[:, 0],
points[:, 1],
c=color,
transform=projection,
**marker_kwargs)
ax.autoscale()
else:
raise TypeError('Unknown shply type: {}'.format(type(next(iter(shps)))))
#assert res is not None
return res
[docs]
def triangulation(points, tris, crs, color='gray', ax=None, **kwargs):
"""Plots a triangulation.
A wrapper for matplotlib's plot_trisurf() or tripcolor()
Parameters
----------
points : np.ndarray(npoints, 3)
Array of point coordinates, x,y,z.
tris : list, np.ndarray(ntris, 3)
List of lists or ndarray of indices into the points array for defining
the triangle topology.
crs : CRS object
Coordinate system of the points.
color : matplotlib color object or iterable or str, optional
Either a matplotlib color object (for uniform colors), or a list of color
objects (length equal to the length of tris), or 'elevation' to color by
z coordinate.
ax : matplotlib ax object
Axes to plot on. Calls get_ax() if not provided.
kwargs : dict
Extra arguments passed to plot_trisurf() (for 3D axes) or tripcolor()
(for 2D).
Returns
-------
col : matplotlib collection
Collection of patches representing the triangles.
"""
if ax is None:
fig, ax = get_ax(crs)
if type(color) is str and color == 'elevation' and points.shape[1] != 3:
color = 'gray'
def get_color_extents(color):
if 'vmin' not in kwargs:
vmin = np.nanmin(color)
else:
vmin = kwargs.pop('vmin')
if 'vmax' not in kwargs:
vmax = np.nanmax(color)
else:
vmax = kwargs.pop('vmax')
return vmin, vmax
if type(ax) is Axes3D:
if type(color) is str and color == 'elevation':
col = ax.plot_trisurf(points[:, 0], points[:, 1], points[:, 2], tris, points[:, 2],
**kwargs)
elif type(color) != str:
vmin, vmax = get_color_extents(color)
col = ax.plot_trisurf(points[:, 0],
points[:, 1],
points[:, 2],
tris,
color,
vmin=vmin,
vmax=vmax,
**kwargs)
else:
col = ax.plot_trisurf(points[:, 0],
points[:, 1],
points[:, 2],
tris,
color=color,
**kwargs)
else:
if type(color) is str and color == 'elevation':
col = ax.tripcolor(points[:, 0], points[:, 1], tris, points[:, 2], **kwargs)
elif type(color) != str:
vmin, vmax = get_color_extents(color)
col = ax.tripcolor(points[:, 0],
points[:, 1],
tris,
color,
vmin=vmin,
vmax=vmax,
**kwargs)
else:
col = ax.triplot(points[:, 0], points[:, 1], tris, color=color, **kwargs)
return col
[docs]
def mesh(m2, crs, color='gray', ax=None, **kwargs):
"""Plots a watershed_workflow.mesh.Mesh2D object.
Parameters
----------
m2 : Mesh2D
The 2D mesh to plot.
crs : CRS object
Coordinate system of the points.
color : matplotlib color object or iterable or str, optional
Either a matplotlib color object (for uniform colors), or a list of color
objects (length equal to the length of tris), or 'elevation' to color by
z coordinate.
ax : matplotlib ax object
Axes to plot on. Calls get_ax() if not provided.
kwargs : dict
Extra arguments passed to plot_trisurf() (for 3D axes) or tripcolor()
(for 2D).
Returns
-------
col : matplotlib collection
Collection of patches representing the triangles.
"""
shapes = [shapely.geometry.Polygon(m2.coords[c, :]) for c in m2.conn]
return shplys(shapes, crs, color, ax, **kwargs)
[docs]
def raster(profile, data, ax=None, vmin=None, vmax=None, mask=True, **kwargs):
"""Plots a raster.
A wrapper for matplotlib imshow()
Parameters
----------
profile : rasterio profile
Rasterio profile of the input raster.
data : np.ndarray
2D array of data.
ax : matplotlib ax object
Axes to plot on. Calls get_ax() if not provided.
vmin,vmax : float
Min and max value to limit extent of color values.
mask : bool
If true (default), masks out values given as profile['nodata']
kwargs : dict
Dictionary of extra arguments passed to imshow().
Returns
-------
im : matplotlib image object
Return value of imshow()
"""
if ax is None:
fig, ax = get_ax(profile['crs'])
assert (mask)
assert ('nodata' in profile)
if mask and 'nodata' in profile:
nnd = len(np.where(data == profile['nodata'])[0])
data = np.ma.array(data, mask=(data == profile['nodata']))
if vmin is None and 'norm' not in kwargs:
vmin = np.nanmin(data)
if vmax is None and 'norm' not in kwargs:
vmax = np.nanmax(data)
bounds = rasterio.transform.array_bounds(profile['height'], profile['width'],
profile['transform'])
extent = [bounds[0], bounds[2], bounds[1], bounds[3]]
logging.info('BOUNDS: {}'.format(bounds))
return ax.imshow(data, origin='upper', extent=extent, vmin=vmin, vmax=vmax, **kwargs)
[docs]
def dem(profile, data, ax=None, vmin=None, vmax=None, **kwargs):
"""See raster documentation"""
return raster(profile, data, ax, vmin, vmax, **kwargs)
[docs]
def basemap(crs=None,
ax=None,
resolution='50m',
land_kwargs=None,
ocean_kwargs=None,
state_kwargs=None,
country_kwargs=None,
coastline_kwargs=None,
lake_kwargs=None):
"""Add a basemap to the axis.
Uses cartopy to add political and natural boundaries and shapes to the axes
image.
Parameters
----------
crs : CRS object, optional
Coordinate system to plot. May be ignored if ax is provided.
ax : matplotlib ax object, optional
Matplotlib axes to plot on. If not provided, get_ax() is called using
crs.
resolution : str
Resolution of cartopy basemap. One of '10m', '50m', or '110m'.
land_kwargs : dict
Extra arguments passed to cartopy.feature.NaturalEarthFeature call to get
land polygons.
ocean_kwargs : dict
Extra arguments passed to cartopy.feature.NaturalEarthFeature call to get
ocean polygons.
state_kwargs : dict
Extra arguments passed to cartopy.feature.NaturalEarthFeature call to get
political state boundary polygons.
country_kwargs : dict
Extra arguments passed to cartopy.feature.NaturalEarthFeature call to get
political country boundary polygons.
coastline_kwargs : dict
Extra arguments passed to cartopy.feature.NaturalEarthFeature call to get
natural coastline boundary polygons.
"""
import cartopy.feature
if ax is None:
fig, ax = get_ax(crs)
if land_kwargs is not False:
if land_kwargs is None:
land_kwargs = dict()
if 'edgecolor' not in land_kwargs:
land_kwargs['edgecolor'] = 'face'
if 'facecolor' not in land_kwargs:
land_kwargs['facecolor'] = cartopy.feature.COLORS['land']
land = cartopy.feature.NaturalEarthFeature('physical', 'land', resolution, **land_kwargs)
ax.add_feature(land)
if ocean_kwargs is not False:
if ocean_kwargs is None:
ocean_kwargs = dict()
if 'edgecolor' not in ocean_kwargs:
ocean_kwargs['edgecolor'] = 'face'
if 'facecolor' not in ocean_kwargs:
ocean_kwargs['facecolor'] = cartopy.feature.COLORS['water']
ocean = cartopy.feature.NaturalEarthFeature('physical', 'ocean', resolution, **ocean_kwargs)
ax.add_feature(ocean)
if lake_kwargs is not None and lake_kwargs is not False:
if 'edgecolor' not in lake_kwargs:
lake_kwargs['edgecolor'] = 'face'
if 'facecolor' not in lake_kwargs:
lake_kwargs['facecolor'] = cartopy.feature.COLORS['water']
lake = cartopy.feature.NaturalEarthFeature('physical', 'lakes', resolution, **lake_kwargs)
ax.add_feature(lake)
if coastline_kwargs is not None and coastline_kwargs is not False:
kwargs = { 'facecolor': 'none', 'edgecolor': 'k', 'linewidth': 0.5 }
kwargs.update(**coastline_kwargs)
states = cartopy.feature.NaturalEarthFeature('physical', 'coastline', resolution, **kwargs)
ax.add_feature(states)
if state_kwargs is not None and state_kwargs is not False:
kwargs = { 'facecolor': 'none', 'edgecolor': 'k', 'linewidth': 0.5 }
kwargs.update(**state_kwargs)
states = cartopy.feature.NaturalEarthFeature('cultural', 'admin_1_states_provinces_lines',
resolution, **kwargs)
# these seem a bit broken?
if 'fix' in state_kwargs and state_kwargs.pop('fix'):
states = watershed_workflow.utils.flatten(list(states.geometries()))
shplys(states, watershed_workflow.crs.latlon_crs(), ax=ax, **state_kwargs)
else:
ax.add_feature(states)
if country_kwargs is not None and country_kwargs is not False:
kwargs = { 'facecolor': 'none', 'edgecolor': 'k', 'linewidth': 0.5 }
kwargs.update(**country_kwargs)
country = cartopy.feature.NaturalEarthFeature('cultural', 'admin_0_boundary_lines_land',
resolution, **kwargs)
# these seem a bit broken?
if 'fix' in country_kwargs and country_kwargs.pop('fix'):
country = watershed_workflow.utils.flatten(list(country.geometries()))
shplys(country, watershed_workflow.crs.latlon_crs(), ax=ax, **country_kwargs)
else:
ax.add_feature(country)
return
[docs]
def feather_axis_limits(ax, delta=0.02):
"""Adds a small delta to the axis limits to provide a bit of buffer.
Parameters
----------
ax : matplotlib Axis object
The axis to feather.
delta : 2-tuple or double, default=0.02
If a double, equivalent to (delta,delta). Provides the fraction of
the current plot width,height to increase by.
"""
try:
assert (len(delta) == 2)
except AssertionError:
raise RuntimeError("feather_axis_limits expects delta argument of length 2 (dx,dy)")
except ValueError:
delta = (delta, delta)
xlim = ax.get_xlim()
ylim = ax.get_ylim()
dx = delta[0] * (xlim[1] - xlim[0])
dy = delta[1] * (ylim[1] - ylim[0])
ax.set_xlim((xlim[0] - dx, xlim[1] + dx))
ax.set_ylim((ylim[0] - dy, ylim[1] + dy))