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

Skip to content
Merged
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
11 changes: 8 additions & 3 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def __init__(self):
self._snap = None
self._sketch = mpl.rcParams['path.sketch']
self._path_effects = mpl.rcParams['path.effects']
self._sticky_edges = _XYPair([], [])
self._sticky_edges = None
self._in_layout = True

def __getstate__(self):
Expand Down Expand Up @@ -1209,6 +1209,8 @@ def sticky_edges(self):
>>> artist.sticky_edges.y[:] = (ymin, ymax)

"""
if self._sticky_edges is None:
self._sticky_edges = _XYPair([], [])
return self._sticky_edges

def update_from(self, other):
Expand All @@ -1223,8 +1225,11 @@ def update_from(self, other):
self._label = other._label
self._sketch = other._sketch
self._path_effects = other._path_effects
self.sticky_edges.x[:] = other.sticky_edges.x.copy()
self.sticky_edges.y[:] = other.sticky_edges.y.copy()
if other._sticky_edges is not None:
self.sticky_edges.x[:] = other._sticky_edges.x.copy()
self.sticky_edges.y[:] = other._sticky_edges.y.copy()
else:
self._sticky_edges = None
self.pchanged()
self.stale = True

Expand Down
69 changes: 46 additions & 23 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3027,20 +3027,29 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True):
if tight is not None:
self._tight = bool(tight)

x_shared_axes = self._shared_axes["x"].get_siblings(self)
y_shared_axes = self._shared_axes["y"].get_siblings(self)

x_stickies = y_stickies = np.array([])
if self.use_sticky_edges:
if self._xmargin and scalex and self.get_autoscalex_on():
edges = []
for ax in self._shared_axes["x"].get_siblings(self):
for artist in ax.get_children():
edges.extend(artist.sticky_edges.x)
x_stickies = np.sort(edges)
x_sticky = []
for ax in x_shared_axes:
for artist in ax._get_data_children():
sticky_edges = artist._sticky_edges
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.

Slighty worried that this is unnecessary micro-optimization.

Image

Do 20ns per artist really make a difference? If not, I'd rather stay within the public API bundaries

Copy link
Copy Markdown
Contributor Author

@scottshambaugh scottshambaugh Mar 6, 2026

Choose a reason for hiding this comment

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

It does show up in the results - just the .sticky_edges property accesses are the circled bits in the baseline profiling here. Because autoscale_view is called for each scatter call in the script, this inner loop is O(n^2) with the number of artists. We could defer autoscaling or be smarter about which artists are on the boundary, but that would be a much bigger change.

image

if sticky_edges is not None and sticky_edges.x:
x_sticky.extend(sticky_edges.x)
if x_sticky:
x_stickies = np.sort(x_sticky)
if self._ymargin and scaley and self.get_autoscaley_on():
edges = []
for ax in self._shared_axes["y"].get_siblings(self):
for artist in ax.get_children():
edges.extend(artist.sticky_edges.y)
y_stickies = np.sort(edges)
y_sticky = []
for ax in y_shared_axes:
for artist in ax._get_data_children():
sticky_edges = artist._sticky_edges
if sticky_edges is not None and sticky_edges.y:
y_sticky.extend(sticky_edges.y)
if y_sticky:
y_stickies = np.sort(y_sticky)
if self.get_xscale() == 'log':
x_stickies = x_stickies[x_stickies > 0]
if self.get_yscale() == 'log':
Expand All @@ -3051,11 +3060,9 @@ def handle_single_axis(

if not (scale and axis._get_autoscale_on()):
return # nothing to do...

shared = shared_axes.get_siblings(self)
# Base autoscaling on finite data limits when there is at least one
# finite data limit among all the shared_axes and intervals.
values = [val for ax in shared
values = [val for ax in shared_axes
for val in getattr(ax.dataLim, f"interval{name}")
if np.isfinite(val)]
if values:
Expand All @@ -3071,20 +3078,22 @@ def handle_single_axis(
x0, x1 = locator.nonsingular(x0, x1)
# Find the minimum minpos for use in the margin calculation.
minimum_minpos = min(
getattr(ax.dataLim, f"minpos{name}") for ax in shared)
getattr(ax.dataLim, f"minpos{name}") for ax in shared_axes)

# Prevent margin addition from crossing a sticky value. A small
# tolerance must be added due to floating point issues with
# streamplot; it is defined relative to x1-x0 but has
# no absolute term (e.g. "+1e-8") to avoid issues when working with
# datasets where all values are tiny (less than 1e-8).
tol = 1e-5 * abs(x1 - x0)
# Index of largest element < x0 + tol, if any.
i0 = stickies.searchsorted(x0 + tol) - 1
x0bound = stickies[i0] if i0 != -1 else None
# Index of smallest element > x1 - tol, if any.
i1 = stickies.searchsorted(x1 - tol)
x1bound = stickies[i1] if i1 != len(stickies) else None
x0bound = x1bound = None
if len(stickies):
tol = 1e-5 * abs(x1 - x0)
# Index of largest element < x0 + tol, if any.
i0 = stickies.searchsorted(x0 + tol) - 1
x0bound = stickies[i0] if i0 != -1 else None
# Index of smallest element > x1 - tol, if any.
i1 = stickies.searchsorted(x1 - tol)
x1bound = stickies[i1] if i1 != len(stickies) else None

# Add the margin in figure space and then transform back, to handle
# non-linear scales.
Expand All @@ -3109,10 +3118,10 @@ def handle_single_axis(
# End of definition of internal function 'handle_single_axis'.

handle_single_axis(
scalex, self._shared_axes["x"], 'x', self.xaxis, self._xmargin,
scalex, x_shared_axes, 'x', self.xaxis, self._xmargin,
x_stickies, self.set_xbound)
handle_single_axis(
scaley, self._shared_axes["y"], 'y', self.yaxis, self._ymargin,
scaley, y_shared_axes, 'y', self.yaxis, self._ymargin,
y_stickies, self.set_ybound)

def _update_title_position(self, renderer):
Expand Down Expand Up @@ -4515,6 +4524,20 @@ def drag_pan(self, button, key, x, y):
self.set_xlim(points[:, 0])
self.set_ylim(points[:, 1])

def _get_data_children(self):
"""
Return artists that represent data (plot lines, collections, images,
patches, etc.) as opposed to auxiliary artists needed to draw the
Axes itself (spines, titles, axis objects, etc.).

Data children are the artists that can contribute to autoscaling
and sticky edges.

Note: This is a preliminary definition and has not been thought
through completely. We may want to revise this later.
"""
Comment thread
scottshambaugh marked this conversation as resolved.
return [*self._children, *self._axis_map.values()]

def get_children(self):
# docstring inherited.
return [
Expand Down
Loading