-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
MNT: simplify valid tick logic #12158
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
157b0e1
to
f49d364
Compare
Ping @efiring |
if tick is None: | ||
continue | ||
# NB: always update labels and position to avoid issues like #9397 | ||
tick.update_position(loc) | ||
tick.set_label1(label) | ||
tick.set_label2(label) | ||
if not mtransforms.interval_contains(interval_expanded, loc): | ||
try: | ||
loct = self.get_transform().transform(loc) |
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.
Not sure if it really makes a performance difference, but AFAICS transform()
works on numpy arrays. So this could be pulled out of the loop and calculated in one go. Well, except for the AssertionError
- when does that occur?
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.
We still have to do the loop, so can probably keep as-is for now.
I think the Assertion Error happened in CI, possibly get_transform returned None for some wacky axis.
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 its from the doc build: gallery/scales/custom_scale.py
.
Exception occurred:
File "/home/circleci/project/lib/matplotlib/transforms.py", line 1436, in transform
assert not np.ma.is_masked(res) # just to be on the safe side
AssertionError
The custom scale uses masks, and transforms.transform doesn't like it.
The previous implementation didn't call the transform, but this one does so that we can do the tolerance checking in log-scale rather than data space.
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.
Are we sure that there are still any ticks on that example when we hit this?
lib/matplotlib/transforms.py
Outdated
@@ -2892,7 +2892,7 @@ def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): | |||
return vmin, vmax | |||
|
|||
|
|||
def interval_contains(interval, val): | |||
def interval_contains(interval, val, rtol=1e-10): |
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 feel like so as to not break downstream users relying on this being exact, the default should be rtol=0
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.
@eric-wieser you probably understand the numerical issues better than me, but is this comparison ever "exact"?
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, comparison is always exact. The danger is when you make assumptions about rounding in operations prior to the comparison, but that's for the caller to decide.
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, done...
e2a1a63
to
61ea269
Compare
lib/matplotlib/axis.py
Outdated
except AssertionError: | ||
loct = None | ||
continue | ||
if ((loct is None) or |
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 transform()
return None
? Otherwise we do not need the loct is None
check here.
98f1560
to
2ee3e6d
Compare
2ee3e6d
to
3d4976b
Compare
lib/matplotlib/transforms.py
Outdated
@@ -2906,10 +2906,39 @@ def interval_contains(interval, val): | |||
Returns | |||
------- | |||
bool | |||
Returns true if given val is within the interval. | |||
Returns true if given val is within the interval (with tolerance) |
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 one says nothing about tolerance
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.
Fixed!
c6df889
to
227bed5
Compare
We should make sure this does not break wcsaxes (attn @astrofrog ) |
lib/matplotlib/axis.py
Outdated
# some scales might make them, so we need this try/except. | ||
loct = None | ||
continue | ||
if not mtransforms.interval_contains_close(inter, loct): |
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 have examples where we need interval_contains_close
and not interval_contains
here? Why couldn't the locators be in charge of making sure whatever they return is in the interval and adjusting the values if needed (except for those outside the interval needed for classic mode)?
I don't really like the idea of hardcoding the 1e-10 tolerance here when it's something that can clearly(?) be avoided.
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.
Hah, we currently have a 1/2-pixel tolerance! Surely 1e-10 * interval
is preferable?
The problem with limits is when people compute them.
On my machine:
3.51-1.51 == 2.0
False
In [17]: abs(3.51-1.51- 2.0) < 1e-10
Out[17]: True
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 guess it doesn't make things worse, but at least let's keep interval_contains_close private?
The point is that the Locator API should say something like the following to implementers (including 3rd party locators):
You may return tick values inside or outside of the axis bounds, but any value outside of the bounds (with no tolerance) will be dropped (except in "classic" mode, where we will keep the first outside value on either side).
and the locator classes are the one which should do something like
xmin, xmax = ...
ticks = ...
if xmin - tol < ticks[0] < xmin: ticks[0] = xmin
(or ticks[1] if that's the one corresponding to xmin).
i.e. the locator displaces the tick to match xmin if it's just below xmin.
But I guess that can wait.
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.
The issue with locators is that some uses want the bracketing ticks, unfortunately, for contour for instance... We could fix/change that as well, but beyond this PR.
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.
Agreed it's beyond this PR.
The design above does not preclude bracketing ticks.
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.
@anntzer did you really want interval_contains_close
to be private? I actually think its useful to take into account floating point slop, but don't feel strongly about it.
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, it feels like a hack while waiting for locators to be fixed, but I'm not going to insist on it.
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.
Okey doke - done!
FIX: make contains_close private
227bed5
to
01d7455
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.
This looks ok by itself.
Not merging yet because of @tacaswell's concern with respect to wcsaxes. Can someone confirm, that this is not breaking them?
I'm checking it and will report back shortly |
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 works fine with WCSAxes (all tests, including image tests, pass), in part because we do all the tick drawing ourselves.
@astrofrog Thanks for testing. |
Yes, thanks all! |
PR Summary
This is a much scaled back first-step PR for #12105.
The logic for deciding whether or not to draw a tick was rather convoluted, and depended on the size of a pixel. This PR simply changes the test for whether a tick is in the view limit to one that depends on a relative tolerance to take up floating point slack (like
np.allclose
, but for an interval). The relevant code is intransforms.inverval_contains
.See also #11004
This passes all previous tests except for the
test_axes/specgram_*
tests.These had a tick at 0 before becuase 0 was within half a pixel of the minimimum view limit of 0.2. Clearly this tick should not be drawn, and the half a pixel slop was far too large. Worse, if the figure was widened in interactive mode, the tick would disappear:
With this PR, the tick is never drawn (because 0<0.2).
Another minimal example (w/o resizing)
Before
100 dpi
200 dpi
After
100 dpi
200 dpi
As above...
PR Checklist