Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,20 @@ def __clear(self):
self.xaxis.set_clip_path(self.patch)
self.yaxis.set_clip_path(self.patch)

# Ensure spines have the correct transform before any subsequent
# layout or draw. Spine.__init__ installs ``self.axes.transData`` as
# a placeholder; the real blended transform is set by
# Spine.set_position (via ``_ensure_position_is_set``). Historically
# this fired as a side effect of tick materialization during clear;
# with lazy tick lists that cascade no longer runs, so nudge it here
# for spines that still carry the placeholder (projections like
# polar or secondary axes install custom transforms and are
# skipped).
for spine in self.spines.values():
if spine._position is None and spine._transform is self.transData:
spine._position = ('outward', 0.0)
spine.set_transform(spine.get_spine_transform())
Copy link
Copy Markdown
Member

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_set like in the comment?


if self._sharex is not None:
self.xaxis.set_visible(xaxis_visible)
self.patch.set_visible(patch_visible)
Expand Down
150 changes: 117 additions & 33 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,24 +550,48 @@ def __init__(self, major):
def __get__(self, instance, owner):
if instance is None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the logic complexity grows, we should afford a description.

Suggested change
if instance is None:
"""Materialize the desciptor to a list containing one properly configured tick."""
if instance is None:

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, [])
Copy link
Copy Markdown
Member

@timhoffm timhoffm Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep one named instance around in the function?

  • create an empty list
  • assign it to the instance
  • create and configure the tick
  • add the tick to the list
  • return the list
        tick_list = []
        setattr(instance, attr, tick_list)  # must be defined before instance._get_tick()
        
        # create and configure the tick

        tick_list.append(tick)
        return tick_list

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 getattr/setattr` (which are needed to have one execution path for majorTicks/minorTicks, which in turn is needed because the logical complexity of tick configuring rises and we don't want to duplicate that in two branches for major/minor

# 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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can rc_context be used here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to still define a private _rc_context_raw() context manager to simplify the code. The rcParams handling is very much distracting from the non-trivial tick handling code.

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):
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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:

  • No quotes but parentheses

    [...] time of the last Axis.clear() (or

  • Single ticks as used in CPython error messages

    [...] time of the last 'Axis.clear'

# ``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()
Expand Down Expand Up @@ -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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be moved to a method tick._configure_for_axis(axis). Conceptually the tick should know how it needs to be configured; e.g. the Axis should not have to know that the tick itself and its gridline need to be clipped.

"""
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 _LazyTickList or this PR? Please rephrase.

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):
"""
Expand Down Expand Up @@ -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
Expand All @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Delete the majorTicks / minorTicks instances so that the _LazyTickList descriptor is reactivated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a logical change as set_clip_path is no longer always executed. I updated the docstring


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):
"""
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
Loading