diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 1f161732cabb..39520b2e2e37 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -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): @@ -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): @@ -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 diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 268d781038a7..53061b338aaf 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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 + 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': @@ -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: @@ -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. @@ -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): @@ -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. + """ + return [*self._children, *self._axis_map.values()] + def get_children(self): # docstring inherited. return [