Skip to content
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

Artist autolim #28071

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@

# Python files #
################

# virtual environment
env/

# meson-python working directory
build
.mesonpy*
Expand Down
2 changes: 2 additions & 0 deletions doc/api/artist_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ Miscellaneous
:nosignatures:

Artist.sticky_edges
Artist.set_in_autoscale
Artist.get_in_autoscale
Artist.set_in_layout
Artist.get_in_layout
Artist.stale
Expand Down
9 changes: 9 additions & 0 deletions doc/users/next_whats_new/artist_in_autoscale.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
``Artist`` gained setter and getter for new ``_in_autoscale`` flag
-------------------------------------------------------------------

The ``_in_autoscale`` flag determines whether the instance is used
in the autoscale calculation. The flag can be a bool, or tuple[bool] for 2D/3D.
Expansion to a tuple is done in the setter.

The purpose is to put auto-limit logic inside respective Artists.
This allows ``Collection`` objects to be used in ``relim`` axis calculations.
27 changes: 27 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def __init__(self):
self._path_effects = mpl.rcParams['path.effects']
self._sticky_edges = _XYPair([], [])
self._in_layout = True
self._in_autoscale = True

def __getstate__(self):
d = self.__dict__.copy()
Expand Down Expand Up @@ -881,6 +882,14 @@ def _fully_clipped_to_axes(self):
or isinstance(clip_path, TransformedPatchPath)
and clip_path._patch is self.axes.patch))

def get_in_autoscale(self):
"""
Return bool or tuple[bool] flag, with as many entries as
dimensions in the plot. When True, the artist is included
in autoscale calculations along that axis.
"""
return self._in_autoscale

def get_clip_on(self):
"""Return whether the artist uses clipping."""
return self._clipon
Expand Down Expand Up @@ -1086,6 +1095,17 @@ def set_in_layout(self, in_layout):
"""
self._in_layout = in_layout

def set_in_autoscale(self, in_autoscale):
"""
Set if artist is to be included in autoscale calculations
along certain axes.

Parameters
----------
in_autoscale : bool or tuple[bool]
"""
self._in_autoscale = in_autoscale

def get_label(self):
"""Return the label used for this artist in the legend."""
return self._label
Expand Down Expand Up @@ -1220,6 +1240,13 @@ def _internal_update(self, kwargs):
kwargs, "{cls.__name__}.set() got an unexpected keyword argument "
"{prop_name!r}")

def _update_limits(self, axes_base):
"""
Defaults to failure in base Artist. Will be overridden in
Line2D, Patch, AxesImage, and Collection classes.
"""
raise NotImplementedError('artist child does not have _update_limits function')

def set(self, **kwargs):
# docstring and signature are auto-generated via
# Artist._update_set_signature_and_docstring() at the end of the
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/artist.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class Artist:
def get_visible(self) -> bool: ...
def get_animated(self) -> bool: ...
def get_in_layout(self) -> bool: ...
def get_in_autoscale(self) -> bool | tuple[bool]: ...
def get_clip_on(self) -> bool: ...
def get_clip_box(self) -> Bbox | None: ...
def get_clip_path(
Expand All @@ -117,6 +118,7 @@ class Artist:
def set_visible(self, b: bool) -> None: ...
def set_animated(self, b: bool) -> None: ...
def set_in_layout(self, in_layout: bool) -> None: ...
def set_in_autoscale(self, in_autoscale: bool | tuple[bool]) -> None: ...
def get_label(self) -> object: ...
def set_label(self, s: object) -> None: ...
def get_zorder(self) -> float: ...
Expand Down
139 changes: 27 additions & 112 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2265,19 +2265,9 @@
collection.set_clip_path(self.patch)

if autolim:
# Make sure viewLim is not stale (mostly to match
# pre-lazy-autoscale behavior, which is not really better).
self._unstale_viewLim()
datalim = collection.get_datalim(self.transData)
points = datalim.get_points()
if not np.isinf(datalim.minpos).all():
# By definition, if minpos (minimum positive value) is set
# (i.e., non-inf), then min(points) <= minpos <= max(points),
# and minpos would be superfluous. However, we add minpos to
# the call so that self.dataLim will update its own minpos.
# This ensures that log scales see the correct minimum.
points = np.concatenate([points, [datalim.minpos]])
self.update_datalim(points)
collection._update_limits(self)
else:
collection.set_in_autoscale(False)

self.stale = True
return collection
Expand All @@ -2295,10 +2285,6 @@
self.stale = True
return image

def _update_image_limits(self, image):
xmin, xmax, ymin, ymax = image.get_extent()
self.axes.update_datalim(((xmin, ymin), (xmax, ymax)))

def add_line(self, line):
"""
Add a `.Line2D` to the Axes; return the line.
Expand Down Expand Up @@ -2327,54 +2313,6 @@
self.stale = True
return txt

def _update_line_limits(self, line):
"""
Figures out the data limit of the given line, updating self.dataLim.
"""
path = line.get_path()
if path.vertices.size == 0:
return

line_trf = line.get_transform()

if line_trf == self.transData:
data_path = path
elif any(line_trf.contains_branch_seperately(self.transData)):
# Compute the transform from line coordinates to data coordinates.
trf_to_data = line_trf - self.transData
# If transData is affine we can use the cached non-affine component
# of line's path (since the non-affine part of line_trf is
# entirely encapsulated in trf_to_data).
if self.transData.is_affine:
line_trans_path = line._get_transformed_path()
na_path, _ = line_trans_path.get_transformed_path_and_affine()
data_path = trf_to_data.transform_path_affine(na_path)
else:
data_path = trf_to_data.transform_path(path)
else:
# For backwards compatibility we update the dataLim with the
# coordinate range of the given path, even though the coordinate
# systems are completely different. This may occur in situations
# such as when ax.transAxes is passed through for absolute
# positioning.
data_path = path

if not data_path.vertices.size:
return

updatex, updatey = line_trf.contains_branch_seperately(self.transData)
if self.name != "rectilinear":
# This block is mostly intended to handle axvline in polar plots,
# for which updatey would otherwise be True.
if updatex and line_trf == self.get_yaxis_transform():
updatex = False
if updatey and line_trf == self.get_xaxis_transform():
updatey = False
self.dataLim.update_from_path(data_path,
self.ignore_existing_data_limits,
updatex=updatex, updatey=updatey)
self.ignore_existing_data_limits = False

def add_patch(self, p):
"""
Add a `.Patch` to the Axes; return the patch.
Expand All @@ -2388,46 +2326,6 @@
p._remove_method = self._children.remove
return p

def _update_patch_limits(self, patch):
"""Update the data limits for the given patch."""
# hist can add zero height Rectangles, which is useful to keep
# the bins, counts and patches lined up, but it throws off log
# scaling. We'll ignore rects with zero height or width in
# the auto-scaling

# cannot check for '==0' since unitized data may not compare to zero
# issue #2150 - we update the limits if patch has non zero width
# or height.
if (isinstance(patch, mpatches.Rectangle) and
((not patch.get_width()) and (not patch.get_height()))):
return
p = patch.get_path()
# Get all vertices on the path
# Loop through each segment to get extrema for Bezier curve sections
vertices = []
for curve, code in p.iter_bezier(simplify=False):
# Get distance along the curve of any extrema
_, dzeros = curve.axis_aligned_extrema()
# Calculate vertices of start, end and any extrema in between
vertices.append(curve([0, *dzeros, 1]))

if len(vertices):
vertices = np.vstack(vertices)

patch_trf = patch.get_transform()
updatex, updatey = patch_trf.contains_branch_seperately(self.transData)
if not (updatex or updatey):
return
if self.name != "rectilinear":
# As in _update_line_limits, but for axvspan.
if updatex and patch_trf == self.get_yaxis_transform():
updatex = False
if updatey and patch_trf == self.get_xaxis_transform():
updatey = False
trf_to_data = patch_trf - self.transData
xys = trf_to_data.transform(vertices)
self.update_datalim(xys, updatex=updatex, updatey=updatey)

def add_table(self, tab):
"""
Add a `.Table` to the Axes; return the table.
Expand Down Expand Up @@ -2481,14 +2379,31 @@
self.dataLim.set_points(mtransforms.Bbox.null().get_points())
self.ignore_existing_data_limits = True

for artist in self._children:
for artist in self._children: # can be Collection now
if not visible_only or artist.get_visible():
if isinstance(artist, mlines.Line2D):
self._update_line_limits(artist)
elif isinstance(artist, mpatches.Patch):
self._update_patch_limits(artist)
elif isinstance(artist, mimage.AxesImage):
self._update_image_limits(artist)
if artist.get_in_autoscale():
artist._update_limits(self)

def _update_line_limits(self, line):
"""
These 3 functions keep the interface the same for integration tests.
_update_limits() is now inside the respective Artists.
"""
line._update_limits(self)

def _update_patch_limits(self, patch):
"""
These 3 functions keep the interface the same for integration tests.
_update_limits() is now inside the respective Artists.
"""
patch._update_limits(self)

def _update_image_limits(self, image):
"""
These 3 functions keep the interface the same for integration tests.
_update_limits() is now inside the respective Artists.
"""
image._update_limits(self)

Check warning on line 2406 in lib/matplotlib/axes/_base.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_base.py#L2406

Added line #L2406 was not covered by tests

def update_datalim(self, xys, updatex=True, updatey=True):
"""
Expand Down
22 changes: 22 additions & 0 deletions lib/matplotlib/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,28 @@
self.cmap = other.cmap
self.stale = True

def _update_limits(self, axes_base):
"""
Figures out the data limit of a Collection, updating
axes_base.dataLim.
"""
if not self._in_autoscale:
return

Check warning on line 966 in lib/matplotlib/collections.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/collections.py#L966

Added line #L966 was not covered by tests

# Make sure viewLim is not stale (mostly to match
# pre-lazy-autoscale behavior, which is not really better).
axes_base._unstale_viewLim()
datalim = self.get_datalim(axes_base.transData)
points = datalim.get_points()
if not np.isinf(datalim.minpos).all():
# By definition, if minpos (minimum positive value) is set
# (i.e., non-inf), then min(points) <= minpos <= max(points),
# and minpos would be superfluous. However, we add minpos to
# the call so that axes_base.dataLim will update its own minpos.
# This ensures that log scales see the correct minimum.
points = np.concatenate([points, [datalim.minpos]])
axes_base.update_datalim(points)


class _CollectionWithSizes(Collection):
"""
Expand Down
7 changes: 7 additions & 0 deletions lib/matplotlib/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,13 @@
else:
return arr[i, j]

def _update_limits(self, axes_base):
if not self._in_autoscale:
return

Check warning on line 1048 in lib/matplotlib/image.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/image.py#L1048

Added line #L1048 was not covered by tests

xmin, xmax, ymin, ymax = self.get_extent()
axes_base.axes.update_datalim(((xmin, ymin), (xmax, ymax)))


class NonUniformImage(AxesImage):

Expand Down
51 changes: 51 additions & 0 deletions lib/matplotlib/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,57 @@
"""
return self._linestyle in ('--', '-.', ':')

def _update_limits(self, axes_base):
"""
Figures out the data limit of the given line, updating axes_base.dataLim.
"""
if not self._in_autoscale:
return

path = self.get_path()
if path.vertices.size == 0:
return

line_trf = self.get_transform()

if line_trf == axes_base.transData:
data_path = path
elif any(line_trf.contains_branch_seperately(axes_base.transData)):
# Compute the transform from line coordinates to data coordinates.
trf_to_data = line_trf - axes_base.transData
# If transData is affine we can use the cached non-affine component
# of line's path (since the non-affine part of line_trf is
# entirely encapsulated in trf_to_data).
if axes_base.transData.is_affine:
line_trans_path = self._get_transformed_path()
na_path, _ = line_trans_path.get_transformed_path_and_affine()
data_path = trf_to_data.transform_path_affine(na_path)
else:
data_path = trf_to_data.transform_path(path)
else:
# For backwards compatibility we update the dataLim with the
# coordinate range of the given path, even though the coordinate
# systems are completely different. This may occur in situations
# such as when ax.transAxes is passed through for absolute
# positioning.
data_path = path

if not data_path.vertices.size:
return

Check warning on line 1503 in lib/matplotlib/lines.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/lines.py#L1503

Added line #L1503 was not covered by tests

updatex, updatey = line_trf.contains_branch_seperately(axes_base.transData)
if axes_base.name != "rectilinear":
# This block is mostly intended to handle axvline in polar plots,
# for which updatey would otherwise be True.
if updatex and line_trf == axes_base.get_yaxis_transform():
updatex = False
if updatey and line_trf == axes_base.get_xaxis_transform():
updatey = False
axes_base.dataLim.update_from_path(data_path,
axes_base.ignore_existing_data_limits,
updatex=updatex, updatey=updatey)
axes_base.ignore_existing_data_limits = False


class AxLine(Line2D):
"""
Expand Down