-
Notifications
You must be signed in to change notification settings - Fork 371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Legends for feature artists #1500
base: main
Are you sure you want to change the base?
Changes from all commits
2e4e34b
5fef68d
b146558
5a8bd09
a4abc72
bd6a831
049d018
cb3de4f
288a8f0
495d96d
3d58461
0976f53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,10 @@ | |
import matplotlib.artist | ||
import matplotlib.collections | ||
|
||
from shapely.geometry import Polygon, LineString, LinearRing | ||
|
||
import cartopy.mpl.patch as cpatch | ||
import cartopy.crs as ccrs | ||
from .style import merge as style_merge, finalize as style_finalize | ||
|
||
|
||
|
@@ -144,16 +147,29 @@ def draw(self, renderer, *args, **kwargs): | |
return | ||
|
||
ax = self.axes | ||
feature_crs = self._feature.crs | ||
|
||
# Get geometries that we need to draw. | ||
extent = None | ||
try: | ||
extent = ax.get_extent(feature_crs) | ||
except ValueError: | ||
warnings.warn('Unable to determine extent. Defaulting to global.') | ||
geoms = self._feature.intersecting_geometries(extent) | ||
geoms, feature_crs, transform = self.get_geometry() | ||
projection = ax.projection | ||
stylised_paths = self.get_stylised_paths( | ||
geoms, feature_crs, projection, **kwargs) | ||
|
||
# Draw one PathCollection per style. We could instead pass an array | ||
# of style items through to a single PathCollection, but that | ||
# complexity does not yet justify the effort. | ||
for style, paths in stylised_paths.items(): | ||
style = style_finalize(dict(style)) | ||
# Build path collection and draw it. | ||
c = matplotlib.collections.PathCollection(paths, | ||
transform=transform, | ||
**style) | ||
c.set_clip_path(ax.patch) | ||
c.set_figure(ax.figure) | ||
c.draw(renderer) | ||
|
||
# n.b. matplotlib.collection.Collection.draw returns None | ||
return None | ||
|
||
def get_stylised_paths(self, geoms, feature_crs, projection, **kwargs): | ||
# Combine all the keyword args in priority order. | ||
prepared_kwargs = style_merge(self._feature.kwargs, | ||
self._kwargs, | ||
|
@@ -165,7 +181,6 @@ def draw(self, renderer, *args, **kwargs): | |
|
||
# Project (if necessary) and convert geometries to matplotlib paths. | ||
stylised_paths = OrderedDict() | ||
key = ax.projection | ||
for geom in geoms: | ||
# As Shapely geometries cannot be relied upon to be | ||
# hashable, we have to use a WeakValueDictionary to manage | ||
|
@@ -183,15 +198,15 @@ def draw(self, renderer, *args, **kwargs): | |
geom_key, geom) | ||
mapping = FeatureArtist._geom_key_to_path_cache.setdefault( | ||
geom_key, {}) | ||
geom_paths = mapping.get(key) | ||
geom_paths = mapping.get(projection) | ||
if geom_paths is None: | ||
if ax.projection != feature_crs: | ||
projected_geom = ax.projection.project_geometry( | ||
if projection != feature_crs: | ||
projected_geom = projection.project_geometry( | ||
geom, feature_crs) | ||
else: | ||
projected_geom = geom | ||
geom_paths = cpatch.geos_to_path(projected_geom) | ||
mapping[key] = geom_paths | ||
mapping[projection] = geom_paths | ||
|
||
if not self._styler: | ||
style = prepared_kwargs | ||
|
@@ -202,20 +217,67 @@ def draw(self, renderer, *args, **kwargs): | |
|
||
stylised_paths.setdefault(style, []).extend(geom_paths) | ||
|
||
transform = ax.projection._as_mpl_transform(ax) | ||
return stylised_paths | ||
|
||
# Draw one PathCollection per style. We could instead pass an array | ||
# of style items through to a single PathCollection, but that | ||
# complexity does not yet justify the effort. | ||
for style, paths in stylised_paths.items(): | ||
style = style_finalize(dict(style)) | ||
# Build path collection and draw it. | ||
c = matplotlib.collections.PathCollection(paths, | ||
transform=transform, | ||
**style) | ||
c.set_clip_path(ax.patch) | ||
c.set_figure(ax.figure) | ||
c.draw(renderer) | ||
def get_geometry(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this too. |
||
ax = self.axes | ||
extent = None | ||
if ax is not None: | ||
transform = ax.projection._as_mpl_transform(ax) | ||
feature_crs = self._feature.crs | ||
# Get geometries that we need to draw. | ||
try: | ||
extent = ax.get_extent(feature_crs) | ||
except ValueError: | ||
warnings.warn('''Unable to determine extent. | ||
Defaulting to global.''') | ||
Comment on lines
+232
to
+233
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This warning should definitely not have a newline and a bunch of whitespace in it. Use plain strings, not a multiline string. |
||
else: | ||
transform = None | ||
feature_crs = ccrs.PlateCarree() | ||
|
||
# n.b. matplotlib.collection.Collection.draw returns None | ||
return None | ||
geoms = self._feature.intersecting_geometries(extent) | ||
|
||
return geoms, feature_crs, transform | ||
poplarShift marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class HandlerFeature(matplotlib.legend_handler.HandlerPathCollection): | ||
poplarShift marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def create_artists(self, legend, orig_handle, | ||
xdescent, ydescent, width, height, fontsize, trans): | ||
# Use first geometry object to determine shapely geometry type | ||
geom = next(orig_handle._feature.geometries()) | ||
poplarShift marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Get paths and associated styles | ||
geoms, feature_crs, _ = orig_handle.get_geometry() | ||
projection = ccrs.PlateCarree() | ||
stylised_paths = orig_handle.get_stylised_paths(geoms, feature_crs, | ||
projection) | ||
|
||
artists = [] | ||
for style in stylised_paths.keys(): | ||
style = dict(style) | ||
facecolor = style.get('facecolor', 'none') | ||
if facecolor not in ('none', 'never') or type(geom) is Polygon: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
p = matplotlib.patches.Rectangle( | ||
xy=(-xdescent, -ydescent), | ||
width=width, height=height, | ||
**style | ||
) | ||
elif type(geom) in (LineString, LinearRing): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
# color handling | ||
style.pop('facecolor') | ||
val = style.pop('edgecolor', 'none') | ||
if val != 'none' and style.get('color', 'none') == 'none': | ||
style['color'] = val | ||
|
||
xdata, _ = self.get_xdata(legend, xdescent, ydescent, | ||
width, height, fontsize) | ||
ydata = np.full_like(xdata, (height - ydescent) / 2) | ||
p = matplotlib.lines.Line2D(xdata, ydata, **style) | ||
|
||
artists.append(p) | ||
|
||
return artists | ||
|
||
|
||
matplotlib.legend.Legend.update_default_handler_map({ | ||
poplarShift marked this conversation as resolved.
Show resolved
Hide resolved
poplarShift marked this conversation as resolved.
Show resolved
Hide resolved
|
||
FeatureArtist: HandlerFeature()}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this private.