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

Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,19 @@ 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._ensure_position_is_set()

if self._sharex is not None:
self.xaxis.set_visible(xaxis_visible)
self.patch.set_visible(patch_visible)
Expand Down
157 changes: 123 additions & 34 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Classes for the ticks and x- and y-axis.
"""

import contextlib
import datetime
import functools
import logging
Expand Down Expand Up @@ -243,6 +244,18 @@ def set_clip_path(self, path, transform=None):
self.gridline.set_clip_path(path, transform)
self.stale = True

def _configure_for_axis(self, axis):
"""
Copy the Axis clip state onto this Tick and its gridline.

Used by `_LazyTickList` to stamp clip state set via
`Axis.set_clip_path` onto a lazily-materialized Tick.
"""
for artist in (self, self.gridline):
artist.clipbox = axis.clipbox
artist._clippath = axis._clippath
artist._clipon = axis._clipon

def contains(self, mouseevent):
"""
Test whether the mouse event occurred in the Tick marks.
Expand Down Expand Up @@ -536,6 +549,26 @@ def formatter(self, formatter):
self._formatter = formatter


@contextlib.contextmanager
def _rc_context_raw(snapshot):
"""
Like ``mpl.rc_context(snapshot)`` but bypasses ``RcParams`` validators
on entry and exit; re-applying a snapshot to its own values must not
re-trigger one-shot validator warnings (e.g. ``toolbar='toolmanager'``).
``snapshot=None`` is a no-op.
"""
if snapshot is None:
yield
return
rc = mpl.rcParams
orig = dict(rc)
rc._update_raw(snapshot)
try:
yield
finally:
rc._update_raw(orig)


class _LazyTickList:
"""
A descriptor for lazy instantiation of tick lists.
Expand All @@ -548,26 +581,39 @@ def __init__(self, major):
self._major = major

def __get__(self, instance, owner):
"""Materialize the descriptor to a list with one configured tick."""
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
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
# 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). To avoid infinite recursion,
# bind the attribute to an empty list before calling _get_tick().
# _get_tick() may also call reset_ticks(), which pops the attribute
# from the instance dict; the final setattr below re-binds the
# (now non-empty) list so subsequent accesses skip the descriptor.
attr = 'majorTicks' if self._major else 'minorTicks'
tick_list = []
setattr(instance, attr, tick_list)
# 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).
with _rc_context_raw(instance._tick_rcParams):
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)
tick._configure_for_axis(instance)
tick_list.append(tick)
setattr(instance, attr, tick_list)
return tick_list


class Axis(martist.Artist):
Expand Down Expand Up @@ -672,6 +718,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
# 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
# Tick._configure_for_axis() for the clip-state counterpart.
self._tick_rcParams = None

if clear:
self.clear()
Expand Down Expand Up @@ -851,12 +906,14 @@ def _reset_major_tick_kw(self):
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 +944,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 +969,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 +1077,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 +1220,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