-
Notifications
You must be signed in to change notification settings - Fork 387
Make FeatureArtist
a Collection
#2323
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
Conversation
@@ -510,7 +510,6 @@ def test_gridliner_remove(): | |||
gl.remove() | |||
|
|||
assert gl not in ax.artists | |||
assert not ax.collections |
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.
Broke this by making coastlines a collection. However, this line was redundant since the PR that added it also stopped GridLiner
adding the LineCollection
s to the axes.
It does seem like most of the features are closer to collections, whether it be PolyCollection (LAND), LineCollection (COASTLINES). So this seems reasonable to me. Are there any issues or benefits with exposing Should the name be updated and called |
Thanks @greglucas! I did spot #1789 and wondered if this would help there. I don't really know anything about I think the point about I also wondered about the name. Having |
Yes, they all get passed to the renderer at once within collections rather than one-by-one as Python objects. That is a good point about not necessarily knowing the geometries and what is used. Do we actually have all the geometries, it is just not drawing the ones that are out of the view? I'm wondering if we could do some fancy thing with only sending the specific paths we're interested in to the renderer, even if the others still exist. That definitely seems like a lot more work and effort to keep track of all the potential split paths and whats in/out of view! Naming things is hard :) All this to say, this seems like a good idea and enables some other interesting possibilities as follow-ups as well. |
Hmmm... import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import shapely.geometry as sgeom
square1 = sgeom.Polygon([(-10, -10), (10, -10), (10, 10), (-10, 10), (-10, 10) ])
square2 = sgeom.Polygon([(30, 0), (50, 0), (50, 20), (30, 20), (30, 0)])
ax = plt.figure().add_subplot(111, projection=ccrs.PlateCarree())
ax.coastlines()
ax.add_geometries([square1, square2], crs=ccrs.PlateCarree(),
facecolors=['r', 'b'])
plt.show() If we pan/zoom so that the red square is out of the picture, the blue square turns red! Also, ax = plt.figure().add_subplot(111, projection=ccrs.PlateCarree(central_longitude=180))
ax.coastlines()
ax.add_geometries([square1, square2], crs=ccrs.PlateCarree(),
facecolors=['r', 'b']) This is also true with v0.22, so I haven't actually broken anything here. Edit: #2325 addresses the second of these. |
Currently draw only accesses the geometries that intersect with the map cartopy/lib/cartopy/mpl/feature_artist.py Line 151 in 25a2154
If you call |
I think this might be similar to what we are doing with the GeoQuadMesh, where a user has all the data and would call |
be78352
to
f8458b6
Compare
FeatureArtist
a Collection
FeatureArtist
a Collection
f8458b6
to
9b48049
Compare
I think I am pretty happy with this one now. I should squash the commits before merge but the history may be useful for review. In particular, I found some weird behaviour when I made the change within The doc failure appears to be because of WMTS shenanigans. I think this now also closes #1789. |
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.
I decided to draw one path per geometry for the ShapelyFeature
case rather than trying to line up the various style parameters (which may or may not be lists) with the geometries that get drawn. Collection
has some logic for cycling the properties if there are fewer than the number of paths and also some special handling if you set different numbers of linestyles and linewidths. I think it would be a headache to try to account for all of that!
@@ -189,8 +219,7 @@ def draw(self, renderer, *args, **kwargs): | |||
projected_geom = geom | |||
|
|||
geom_paths = cpatch.geos_to_path(projected_geom) | |||
if not geom_paths: | |||
continue |
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.
If we call make_compound_path
with zero paths then it returns an empty path. Keeping hold of this helps us preserve one-path-per-geometry.
# `array` or a list of facecolors then we should keep the colours | ||
# consistent after pan/zoom. Do this by creating a Path for every | ||
# geometry regardless of whether they are currently in view. | ||
geoms = self._feature.geometries() |
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.
I thought about inserting empty paths for geometries that are not in view, which should be more efficient than transforming all of them. However I'm not sure that the extra code complexity is justified: since the mapping from geometry to path is cached, it will only make a difference at the first draw. Also if someone is passing a specific list of geometries then I would think they usually want to draw most/all of them at least once.
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.
Do you think this will this cause any confusion with the difference between classes of whether geometries is all of the geometries versus only some of them? I'm not sure and as you say it might be a lot more code complexity to justify something that isn't needed.
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.
It's a definite possibility that that would cause confusion! But I suspect that the vast majority of users using Natural Earth or the other downloads would only pass one style for all geometries so will never notice the difference.
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.
🎉 I like this update and think it brings a lot of nice capabilities for color-mapping geometries!
self.set(**feature.kwargs) | ||
self.set(**self._kwargs) |
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.
I'm not sure if set()
emits any callbacks, but we might want to merge the dictionaries before setting to avoid setting properties twice. Could even bring in a zorder dictionary first too {"zorder": 1.5} | ...
self.set(**feature.kwargs) | |
self.set(**self._kwargs) | |
self.set(**(feature.kwargs | self._kwargs)) |
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.
My thinking for calling twice rather than merging the dictionaries is that this way we don't have to sort out the aliases ourselves. So if one has "facecolor" and the other has "fc", then the second will win. Before this PR we use style.merge for that. So we could go back to that, though I think we should add in the plurals (facecolors, etc) which the comment at the top of the style module says are deliberately omitted but does not state why.
|
||
self._feature = feature | ||
|
||
def set_facecolor(self, c): |
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.
I'm not entirely clear why there is a deviation here from Matplotlib with adding "never" other than that previously the facecolor and edgecolor weren't available for a user to set so they had to go through color directly.
Now that we have inherited set_facecolor()
I'm wondering if we should deprecate the "never" option? Users can now explicitly update the color they want: facecolor -> only face
, edgecolor -> only edge
, color -> both
. Maybe I'm missing something else though and this would be really annoying to get rid of?
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.
"never" is used by BORDERS and COASTLINES so my suspicion is it was done to make things more intuitive for the basic user. If I don't know anything about how artists work I would be surprised that setting color
on a coastline also colours the land in.
I think you could previously pass edgecolor and facecolor because ultimately they were just passed in to PathCollection
.
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.
Another solution to that might be to have a flag on the feature that says "these are edges" and override set_color
to only set the edgecolor in that case. This would make us more consistent with LineCollection which will still allow you to explicitly set facecolor if you really want. This might also work as a solution for #1782 - though there we probably need to account for some geometries being edges and some not.
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.
That does seem like a nice way to go. But, I think what you have here is good and consistent with what we had before, so lets not stall this.
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.
Yeah, I was thinking more for a follow-up PR as I'm sure there are pitfalls I haven't thought of!
Thanks for the merge!
# `array` or a list of facecolors then we should keep the colours | ||
# consistent after pan/zoom. Do this by creating a Path for every | ||
# geometry regardless of whether they are currently in view. | ||
geoms = self._feature.geometries() |
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.
Do you think this will this cause any confusion with the difference between classes of whether geometries is all of the geometries versus only some of them? I'm not sure and as you say it might be a lot more code complexity to justify something that isn't needed.
f2941b3
to
bf33716
Compare
Rationale
If we make
FeatureArtist
inherit fromCollection
instead ofArtist
this wouldset_*
methods for free fromCollection
.style.merge
less and instead relying on mpl'sset
method to rationalise those for us.This is currently a pretty rough draft as I wanted to get feedback on the idea before spending time polishing it. Two tests are very broken because they are built around the assumption that we draw via
PathCollection
instances.I have eyeballed the output from all the examples, and they look fine.
Implications