-
Notifications
You must be signed in to change notification settings - Fork 367
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
Make GridLiner into an Artist #2249
Conversation
@@ -436,10 +441,6 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, | |||
self._drawn = False | |||
self._auto_update = auto_update | |||
|
|||
# Check visibility of labels at each draw event | |||
# (or once drawn, only at resize event ?) | |||
self.axes.figure.canvas.mpl_connect('draw_event', self._draw_event) |
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 don't understand why this was here. I think it's telling Matplotlib to call the method on each draw, but that was also happening by calling it within GeoAxes.draw
. So was this redundant or am I misunderstanding what this is for?
If I just remove this line from main
, the tests pass. Also just removing this seems to account for the improvement to #2247.
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.
What you're saying intuitively makes sense.
OK I've now realised that the axes' tightbbox incorporates all of its artists, so it does make sense to call |
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.
This seems like a great approach.
We do have this open issue with another idea for gridlines as well if you wanted to look more into that approach: #1773
But I think it would be more effort than this and not exclusive of this either.
For a test you could add a mock draw method and count the number of draws you get for the gridlines.
# We do not want the labels clipped to axes. | ||
self.set_clip_on(False) |
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 want to set the clipping differently for the lines vs the text? Allow the text to be outside, but the lines to be clipped?
Edit: I looked at the code below and I think you are accounting for this after all!
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.
Yes, the lines need clipping to the axes patch, which happens by default within add_collection
https://github.com/matplotlib/matplotlib/blob/ff552f45f2d30c76120fe0355306afac9a9dd2da/lib/matplotlib/axes/_base.py#L2255-L2256
I got a lot of image test failures before I realised that!
@@ -436,10 +441,6 @@ def __init__(self, axes, crs, draw_labels=False, xlocator=None, | |||
self._drawn = False | |||
self._auto_update = auto_update | |||
|
|||
# Check visibility of labels at each draw event | |||
# (or once drawn, only at resize event ?) | |||
self.axes.figure.canvas.mpl_connect('draw_event', self._draw_event) |
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.
What you're saying intuitively makes sense.
# Clear lists of artists | ||
for lines in [*self.xline_artists, *self.yline_artists]: | ||
lines.remove() | ||
# Clear lists of child artists |
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 would be nice if we could get rid of this as well and just update the artists that are already there instead. Something to look into the future I think.
8528071
to
cda79ad
Compare
OK, I've added some tests. Against
I also
|
Test failures all seem to be something to do with MODIS. |
mocked.assert_called_once() | ||
|
||
|
||
def test_gridliner_remove(): |
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 wanted a check_figures_equal
for this one to show that adding and removing the gridliner gives the same as not adding it, but that doesn't seem to exist [yet] in pytest-mpl. Another option would be an image test against an empty rectangle...
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.
Can you test against an already existing image?
https://github.com/SciTools/cartopy/blob/main/lib/cartopy/tests/mpl/baseline_images/mpl/test_mpl_integration/simple_global.png
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 can try - can you specify a different directory in the image test decorator? Or should I just create a symlink from the gridliner baseline image directory to the target image?
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 have no idea... I was just pointing you at an empty figure to test against :)
FWIW, I think your current version with the asserts is fine.
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.
Hmmm. I can report that the decorator takes a baseline_dir keyword, but it doesn't appear to have any effect.
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.
Ah, it's possible if we modify conftest.py
. I've put that in a separate commit in case we decide that's not a good idea...
cda79ad
to
42cd8a0
Compare
This issue has been bumped recently at #geopython/OWSLib#769 |
This does seem like a good approach. |
lib/cartopy/mpl/gridliner.py
Outdated
def get_window_extent(self, renderer=None): | ||
self._draw_gridliner(renderer=renderer) | ||
visible_text_extents = [c.get_window_extent(renderer=renderer) | ||
for c in self.label_artists if c.get_visible()] |
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.
Although this works for all tested cases, it feels a bit wrong to be excluding the lines from the bbox calculation here. Would it be better to define get_tightbbox
directly as a union of the tightbboxes of all the child artists?
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 think that makes sense to get the union of all child artists as well because they are a part of this artist.
One of the other things I noticed with this when testing it out is if you pan up, the title moves locations based on the axis labels box on the y-axis, which doesn't happen in plain MPL. If the text for the axis extends beyond the spine, then the title gets pushed up with it. Then when the label disappears the title jumps back down again. So I'm wondering if this also needs some adjustment on that aspect as well... (example: #2246)
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 can reproduce the oscillating title with main
and with v0.22. The culprit is this method:
cartopy/lib/cartopy/mpl/geoaxes.py
Lines 533 to 566 in f2bb81d
def _update_title_position(self, renderer): | |
super()._update_title_position(renderer) | |
if not self._gridliners: | |
return | |
if self._autotitlepos is not None and not self._autotitlepos: | |
return | |
# Get the max ymax of all top labels | |
top = -1 | |
for gl in self._gridliners: | |
if gl.has_labels(): | |
for label in (gl.top_label_artists + | |
gl.left_label_artists + | |
gl.right_label_artists): | |
# we skip bottom labels because they are usually | |
# not at the top | |
bb = label.get_tightbbox(renderer) | |
top = max(top, bb.ymax) | |
if top < 0: | |
# nothing to do if no label found | |
return | |
yn = self.transAxes.inverted().transform((0., top))[1] | |
if yn <= 1: | |
# nothing to do if the upper bounds of labels is below | |
# the top of the axes | |
return | |
# Loop on titles to adjust | |
titles = (self.title, self._left_title, self._right_title) | |
for title in titles: | |
x, y0 = title.get_position() | |
y = max(1.0, yn) | |
title.set_position((x, y)) |
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 added a commit that addresses the case above: we check whether the horizontal extents of the left/right labels overlap the horizontal extent of the axes. If not, the labels do not affect the title position. I'm not sure whether this is too simplistic or too projection-specific though.
We do not seem to have any existing test coverage for this title positioning method. So I tried this example from when the code went in: #1117 (comment) The good news is that I get identical results with my branch and with main
. The bad news is I don't get what was shown in that comment. Note the titles for AlbersEqualArea
and LambertConformal
are lower here than they were then. A closer look at the AlbersEqualArea
example shows that it has no "left", "right" or "top" labels, only "bottom" and "geo". So it seems like "geo" needs to be incorporated into the method somehow. I haven't really got my head around what the "geo" labels do though.
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.
OK, I think I get it now: the left and right labels refer to anything on the vertical spines of the standard rectangular axes. The "geo" labels are everywhere where the spine curves round (I think). This was presumably not the case when the method was originally written. If I’ve understood correctly, we now only need to consider the “top” and “geo” labels when updating the title position. That example now gives me the below image, and I have pulled some of it out into a new image test.
c1d042b
to
1367d8b
Compare
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 think this is a good improvement.
lib/cartopy/mpl/geoaxes.py
Outdated
if self._autotitlepos is not None and not self._autotitlepos: | ||
return |
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.
Minor, but possibly put this check first to short-circuit out of it before iterating through artists?
for artist in ax.artists: | ||
if isinstance(artist, Gridliner): | ||
assert hasattr(artist, '_drawn') and artist._drawn |
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 realize this was not your doing, but this seems fragile if something changes types and we don't hit this code path. Could we change it to a count assertion num_gridliners_drawn == expected_number
to make sure we are hitting the code path we expect?
1367d8b
to
d3a3249
Compare
Thanks @greglucas - I have addressed those two points and also updated a comment that I missed before. |
All test failures are due to external WMTS server failures and are unrelated to this PR. |
Rationale
Thinking about #2246 (comment), it is not intuitive to me that
Gridliner._draw_gridliner
should be called as part of finding theGeoAxes
's bbox, since this method does not seem to update theGeoAxes
's position. I tried moving the call so it's only used at draw and not for finding the bbox, and the only test that failed usesfig.tight_layout()
before the first draw. Clearlytight_layout
cannot create space for artists that do not yet exist.I wonder if a better approach is to have the
GridLiner
itself be an artist, then it can interact with the Matplotlib layouts and draw machinery on its own terms. This PR is my attempt at that. I went looking at the container artists in Matplotlib's offsetbox.py for ideas about how to do it. TheLineCollection
andText
objects are no longer added to the axes directly, but are children of theGridliner
.Fixes #2246. Also closes #1658 as we can now remove the
GridLiner
using the standardArtist.remove
method. I can add tests for those but I wanted to get some feedback on this first in case I have gone completely off piste!This also seems to help #2247 - I only see the busy kernel when I interact with the plot.
Implications
If user code is relying on the fact that the
LineCollection
andText
objects were added directly to the axes, I guess this will break that.