-
-
Notifications
You must be signed in to change notification settings - Fork 8.1k
ENH: Only create ticks if required #27027
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
base: main
Are you sure you want to change the base?
Conversation
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.
Speeding this up would be great! OTOH this seems to do it by carving out specially named axises?
| axis.axis_name == 'y' and (mpl.rcParams['ytick.left'] or mpl.rcParams['ytick.right'])): | ||
| axis.set_major_locator(AutoLocator()) | ||
| else: | ||
| axis.set_major_locator(NullLocator()) |
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 breaking on polar where I think these axes are named something else (r and theta?)
But I'm curious how this is helping you? Are your axes not named x and 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.
But I'm curious how this is helping you? Are your axes not named x and y?
This logic is copied directly from below (109-113 on main), only adapting major in place of minor. Below for the minor ticks if the axis is x (or y), and the user has x.minor.visible set to False, it uses NullLocator for minorTicks. So this was the equivalent logic (I think) for major where if the axis is x (or y), and the user has xtick.visible set to False, it uses NullLocator for majorTicks.
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 breaking on polar where I think these axes are named something else (r and theta?)
I think I see something else. Assuming Polar uses a Linear scaling (I assume it does), Polar could be making an assumption that the locator is AutoLocator or whatever the default used to be before my change. Then you NullLocator.set_params(...) which is not allowed (safely at least). At least that's what I'm thinking from this traceback:
/home/runner/work/matplotlib/matplotlib/lib/matplotlib/projections/polar.py:421: in clear
super().clear()
/home/runner/work/matplotlib/matplotlib/lib/matplotlib/axis.py:897: in clear
self._set_scale('linear')
/home/runner/work/matplotlib/matplotlib/lib/matplotlib/projections/polar.py:433: in _set_scale
self.get_major_locator().set_params(steps=[1, 1.5, 3, 4.5, 9, 10])
/home/runner/work/matplotlib/matplotlib/lib/matplotlib/ticker.py:1605: in set_params
_api.warn_external(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
message = "'set_params()' not defined for locator of type <class 'matplotlib.ticker.NullLocator'>"
One easy fix/workaround is to just make clear or maybe better Polar._set_scale set the formatter to AutoFormatter since it almost immediately thereafter assumes it can set_params 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.
OK, I see - this is just not setting the locators/formatters if the ticks are turned off. That seems a reasonable short path, if a rare use case.
|
Handling each tick individually is costly, and almost always all ticks have the same style. I have a rough idea for migrating from individual ticks to tick collections. These insert an abstraction layer, replacing the current tick lists and can be more efficient internally. Though, we need to ensure backward compatibility and still allow styling individual ticks. The strategy here is to allow access to individual ticks, and if the user does that the internal representation of the tick collection falls back to a list of individual ticks. Within our own library code, we don’t use that interface (in standard plots, there is no need to access individual ticks) and thus have the more efficient internal representation. |
|
It wouldn’t necessarily block this PR, but it might be that the PR logic can be implemented more consistently behind such an abstraction layer, or that it’s not needed at all afterwards. |
It's possible. I'm not totally sure, but maybe I could add some tests here. For example if you request "no ticks" (like in my example above) currently in I'm also unsure if using a collection with no properties will be as fast (not knowledgable enough to know) -- you'd still have to iterate over |
PR summary
Consider this a proposal-by-draft-PR since I couldn't see how tractable a solution would be until I actually fixed/improved the behavior, and this very well might not be a good solution or work fully correctly yet! #6664 being closed got me to look again at whether in MNE-Python we should plot traces ourselves with fake mini-axes or try creating hundreds of axes (below I used 720 axes):
Benchmarking code
Running this script on
main, the first time is the time it takes to plot all 720 of the 1000-sample traces within a single Axes (positioning them with offsets) versus using 720 axes, each with a single trace:On this PR the timings are:
So we cut the time down from ~2.8s to ~0.9s. The code here was developed using
kernprof/line-profilerto see that the bulk of time was spent formatting ticks that would never be used. To prevent tick creation (and thus reformatting) both of the changes in this PR were necessary, namely:NullLocatorcan be used inLinearScale, andNullLocatoris in usemajorTicks[0]without checkinglenfirst in one place (could be others!)It really doesn't seem like (2) should be required in principle, but I couldn't figure out how to avoid the tick creation with the
_LazyTickListand how it gets accessed/used -- I couldn't wrap my head around it, and no matter what I did, 2 ticks were always created per axis. And that means 4 per plot, i.e., 2880 ticks with text and lines and such that need to be processed (hence the time savings by avoiding it in this PR).If this seems like a reasonable or workable approach, it looked like there was some very similar code elsewhere in
scale.pythat could use a similar treatment.Profiling with
py-spy record -f speedscope --subprocesses --nonblocking --rate 1000 python ~/Desktop/topo_bench.pyI'm not sure there are any more big gains to be made here, maybe another 50 ms from avoidingspinesor 50 ms from avoiding text resetting but those seem like much more challenging targets:cc @drammock and @ruuskas who I discussed this with a bit recently
PR checklist
[N/A] "closes #0000" is in the body of the PR description to link the related issue
[N/A] new and changed code is tested
.majorTickshas two entries even whenNullLocatoris in use.[N/A] Plotting related features are demonstrated in an example
[N/A] New Features and API Changes are noted with a directive and release note
[N/A] Documentation complies with general and docstring guidelines