-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
PERF: Defer tick materialization during Axes init/clear #31525
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?
Changes from 4 commits
d0bb8ff
3ac8224
ceef40c
fe563e3
1b72b8b
e80c08e
d0ed04c
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 | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -550,24 +550,48 @@ def __init__(self, major): | |||||||
| def __get__(self, instance, owner): | ||||||||
| if instance is None: | ||||||||
|
Member
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. Since the logic complexity grows, we should afford a description.
Suggested change
|
||||||||
| return self | ||||||||
| # instance._get_tick() can itself try to access the majorTicks | ||||||||
| # attribute (e.g. in certain projection classes which override | ||||||||
| # e.g. get_xaxis_text1_transform). In order to avoid infinite | ||||||||
| # recursion, first set the majorTicks on the instance temporarily | ||||||||
| # to an empty list. Then create the tick; note that _get_tick() | ||||||||
| # may call reset_ticks(). Therefore, the final tick list is | ||||||||
| # created and assigned afterwards. | ||||||||
| attr = 'majorTicks' if self._major else 'minorTicks' | ||||||||
| setattr(instance, attr, []) | ||||||||
|
Member
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. Can we keep one named instance around in the function?
IMHO this is logically clearer than an empty placeholder list that get's replaced later and going back and forward through the instance using multiple |
||||||||
| # Build the Tick (and its sub-artists) under the rcParams captured | ||||||||
| # at the last ``Axis.clear`` so that a lazily-materialized Tick | ||||||||
| # matches an eager (pre-lazy) Tick (see ``Axis._tick_rcParams``). | ||||||||
| # We avoid ``rc_context`` here because it re-applies rcParams via | ||||||||
| # ``RcParams.__setitem__``, whose validators emit warnings on every | ||||||||
| # assignment for keys like ``toolbar='toolmanager'`` -- re-setting | ||||||||
| # the snapshot to its own (identical) values would spuriously | ||||||||
| # re-trigger those warnings. ``_update_raw`` bypasses the validators | ||||||||
| # on both entry and exit. | ||||||||
| if instance._tick_rcParams is not None: | ||||||||
| rc = mpl.rcParams | ||||||||
| orig = dict(rc) | ||||||||
| rc._update_raw(instance._tick_rcParams) | ||||||||
| try: | ||||||||
| tick = instance._get_tick(major=self._major) | ||||||||
| finally: | ||||||||
| rc._update_raw(orig) | ||||||||
|
Member
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. Can rc_context be used here?
Member
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. I suggest to still define a private |
||||||||
| else: | ||||||||
| # instance._get_tick() can itself try to access the majorTicks | ||||||||
| # attribute (e.g. in certain projection classes which override | ||||||||
| # e.g. get_xaxis_text1_transform). In order to avoid infinite | ||||||||
| # recursion, first set the majorTicks on the instance temporarily | ||||||||
| # to an empty list. Then create the tick; note that _get_tick() | ||||||||
| # may call reset_ticks(). Therefore, the final tick list is | ||||||||
| # created and assigned afterwards. | ||||||||
| if self._major: | ||||||||
| instance.majorTicks = [] | ||||||||
| tick = instance._get_tick(major=True) | ||||||||
| instance.majorTicks = [tick] | ||||||||
| return instance.majorTicks | ||||||||
| else: | ||||||||
| instance.minorTicks = [] | ||||||||
| tick = instance._get_tick(major=False) | ||||||||
| instance.minorTicks = [tick] | ||||||||
| return instance.minorTicks | ||||||||
| tick = instance._get_tick(major=self._major) | ||||||||
| # Re-apply any ``set_tick_params`` overrides to the fresh Tick. | ||||||||
| # Subclasses of ``Axis`` (e.g. the ``SkewXAxis`` in the skewt | ||||||||
| # gallery example) sometimes override ``_get_tick`` without | ||||||||
| # forwarding ``_{major,minor}_tick_kw``; calling ``_apply_params`` | ||||||||
| # here guarantees those overrides still take effect, matching the | ||||||||
| # pre-lazy behaviour where the first tick was materialized eagerly | ||||||||
| # and updated in place by ``set_tick_params``. | ||||||||
| tick_kw = (instance._major_tick_kw if self._major | ||||||||
| else instance._minor_tick_kw) | ||||||||
| if tick_kw: | ||||||||
| tick._apply_params(**tick_kw) | ||||||||
| instance._propagate_axis_state_to_tick(tick) | ||||||||
| setattr(instance, attr, [tick]) | ||||||||
| return getattr(instance, attr) | ||||||||
|
|
||||||||
|
|
||||||||
| class Axis(martist.Artist): | ||||||||
|
|
@@ -672,6 +696,15 @@ def __init__(self, axes, *, pickradius=15, clear=True): | |||||||
| # Initialize here for testing; later add API | ||||||||
| self._major_tick_kw = dict() | ||||||||
| self._minor_tick_kw = dict() | ||||||||
| # Snapshot of rcParams at the time of the last ``Axis.clear`` (or | ||||||||
|
Member
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. Optional, but comments are never rendered, so ReST formatting is a bit of overkill here. Alternatives:
|
||||||||
| # ``set_tick_params(reset=True)``). ``_LazyTickList`` re-applies | ||||||||
| # these when it lazily creates a Tick so that the Tick and its | ||||||||
| # sub-artists see the same rcParams an eager (pre-lazy) | ||||||||
| # materialization would have seen. Kept separate from | ||||||||
| # ``_major_tick_kw``/``_minor_tick_kw``, which hold user-provided | ||||||||
| # ``set_tick_params`` overrides rather than ambient rcParams. See | ||||||||
| # ``_propagate_axis_state_to_tick`` for the clip-state counterpart. | ||||||||
| self._tick_rcParams = None | ||||||||
|
|
||||||||
| if clear: | ||||||||
| self.clear() | ||||||||
|
|
@@ -846,17 +879,36 @@ def get_children(self): | |||||||
| return [self.label, self.offsetText, | ||||||||
| *self.get_major_ticks(), *self.get_minor_ticks()] | ||||||||
|
|
||||||||
| def _propagate_axis_state_to_tick(self, tick): | ||||||||
|
Member
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 could be moved to a method |
||||||||
| """ | ||||||||
| Copy Axis clip state onto a lazily-created Tick. | ||||||||
|
|
||||||||
| `Axis.set_clip_path` runs during ``Axes.__clear`` before any Tick | ||||||||
| exists under the lazy-init refactor, so the clip stored on the | ||||||||
|
Member
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. "lazy-init refactor" is a development activity/artifact, not an architectual concept. Also, I'm not exactly sure what "lazy-init refactor" is. Is it the introduction of |
||||||||
| Axis must be re-stamped onto the first Tick when it materializes | ||||||||
| (only the Tick itself and its gridline are clipped, matching | ||||||||
| `Tick.set_clip_path`). Per-Artist rcParams like ``path.sketch`` / | ||||||||
| ``path.effects`` are handled by the ``rc_context`` wrapping in | ||||||||
| `_LazyTickList.__get__`, not here. | ||||||||
| """ | ||||||||
| for artist in (tick, tick.gridline): | ||||||||
| artist.clipbox = self.clipbox | ||||||||
| artist._clippath = self._clippath | ||||||||
| artist._clipon = self._clipon | ||||||||
|
|
||||||||
| def _reset_major_tick_kw(self): | ||||||||
| self._major_tick_kw.clear() | ||||||||
| self._major_tick_kw['gridOn'] = ( | ||||||||
| mpl.rcParams['axes.grid'] and | ||||||||
| mpl.rcParams['axes.grid.which'] in ('both', 'major')) | ||||||||
| self._tick_rcParams = dict(mpl.rcParams) | ||||||||
|
|
||||||||
| def _reset_minor_tick_kw(self): | ||||||||
| self._minor_tick_kw.clear() | ||||||||
| self._minor_tick_kw['gridOn'] = ( | ||||||||
| mpl.rcParams['axes.grid'] and | ||||||||
| mpl.rcParams['axes.grid.which'] in ('both', 'minor')) | ||||||||
| self._tick_rcParams = dict(mpl.rcParams) | ||||||||
|
|
||||||||
| def clear(self): | ||||||||
| """ | ||||||||
|
|
@@ -887,6 +939,11 @@ def clear(self): | |||||||
| # Clear the callback registry for this axis, or it may "leak" | ||||||||
| self.callbacks = cbook.CallbackRegistry(signals=["units"]) | ||||||||
|
|
||||||||
| # Snapshot current rcParams so that a Tick materialized later by | ||||||||
| # ``_LazyTickList`` (possibly outside any ``rc_context`` active | ||||||||
| # now) sees the same rcParams an eager pre-lazy tick would have. | ||||||||
| self._tick_rcParams = dict(mpl.rcParams) | ||||||||
|
|
||||||||
| # whether the grids are on | ||||||||
| self._major_tick_kw['gridOn'] = ( | ||||||||
| mpl.rcParams['axes.grid'] and | ||||||||
|
|
@@ -907,19 +964,46 @@ def reset_ticks(self): | |||||||
|
|
||||||||
| Each list starts with a single fresh Tick. | ||||||||
| """ | ||||||||
| # Restore the lazy tick lists. | ||||||||
| try: | ||||||||
| del self.majorTicks | ||||||||
| except AttributeError: | ||||||||
| pass | ||||||||
| try: | ||||||||
| del self.minorTicks | ||||||||
| except AttributeError: | ||||||||
| pass | ||||||||
| try: | ||||||||
| self.set_clip_path(self.axes.patch) | ||||||||
| except AttributeError: | ||||||||
| pass | ||||||||
| # Drop any materialized tick lists so the _LazyTickList descriptor is | ||||||||
| # reactivated on next access. If ticks were already materialized, | ||||||||
| # re-apply the axes-patch clip path; otherwise skip. | ||||||||
| had_major = bool(self.__dict__.pop('majorTicks', None)) | ||||||||
| had_minor = bool(self.__dict__.pop('minorTicks', None)) | ||||||||
| if had_major or had_minor: | ||||||||
| try: | ||||||||
| self.set_clip_path(self.axes.patch) | ||||||||
| except AttributeError: | ||||||||
| pass | ||||||||
|
Comment on lines
+975
to
+981
Member
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. Is this a logical change or just rewriting? AFAICS we can impove the comment to something like
Contributor
Author
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 is a logical change as |
||||||||
|
|
||||||||
| def _existing_ticks(self, major=None): | ||||||||
| """ | ||||||||
| Yield already-materialized ticks without triggering the lazy descriptor. | ||||||||
|
|
||||||||
| `majorTicks` and `minorTicks` are `_LazyTickList` descriptors that | ||||||||
| create a fresh `.Tick` on first access. Several internal methods | ||||||||
| (`set_clip_path`, `set_tick_params`) need to touch every | ||||||||
| *already-materialized* tick without forcing materialization, because | ||||||||
| doing so would | ||||||||
|
|
||||||||
| (a) create throwaway Tick objects during ``Axes.__init__`` and | ||||||||
| ``Axes.__clear`` | ||||||||
| (b) risk re-entering the | ||||||||
| ``Spine.set_position -> Axis.reset_ticks -> Axis.set_clip_path | ||||||||
| -> _LazyTickList.__get__ -> Tick.__init__ -> Spine.set_position`` | ||||||||
| cascade. | ||||||||
|
|
||||||||
| Reading the instance ``__dict__`` directly bypasses the descriptor. | ||||||||
|
|
||||||||
| Parameters | ||||||||
| ---------- | ||||||||
| major : bool, optional | ||||||||
| If True, yield only major ticks; if False, only minor ticks; | ||||||||
| if None (default), yield major followed by minor. | ||||||||
| """ | ||||||||
| if major is None or major: | ||||||||
| yield from self.__dict__.get('majorTicks', ()) | ||||||||
| if major is None or not major: | ||||||||
| yield from self.__dict__.get('minorTicks', ()) | ||||||||
|
|
||||||||
| def minorticks_on(self): | ||||||||
| """ | ||||||||
|
|
@@ -988,11 +1072,11 @@ def set_tick_params(self, which='major', reset=False, **kwargs): | |||||||
| else: | ||||||||
| if which in ['major', 'both']: | ||||||||
| self._major_tick_kw.update(kwtrans) | ||||||||
| for tick in self.majorTicks: | ||||||||
| for tick in self._existing_ticks(major=True): | ||||||||
| tick._apply_params(**kwtrans) | ||||||||
| if which in ['minor', 'both']: | ||||||||
| self._minor_tick_kw.update(kwtrans) | ||||||||
| for tick in self.minorTicks: | ||||||||
| for tick in self._existing_ticks(major=False): | ||||||||
| tick._apply_params(**kwtrans) | ||||||||
| # labelOn and labelcolor also apply to the offset text. | ||||||||
| if 'label1On' in kwtrans or 'label2On' in kwtrans: | ||||||||
|
|
@@ -1131,7 +1215,7 @@ def _translate_tick_params(cls, kw, reverse=False): | |||||||
|
|
||||||||
| def set_clip_path(self, path, transform=None): | ||||||||
| super().set_clip_path(path, transform) | ||||||||
| for child in self.majorTicks + self.minorTicks: | ||||||||
| for child in self._existing_ticks(): | ||||||||
| child.set_clip_path(path, transform) | ||||||||
| self.stale = 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.
If we're accessing private information like this, is there a reason we don't just call
_ensure_position_is_setlike in the comment?