Skip to content

Factor out common limits handling for x/y/z axes. #21442

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

Merged
merged 2 commits into from
Feb 6, 2022
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
138 changes: 10 additions & 128 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3654,9 +3654,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
False turns off, None leaves unchanged.

xmin, xmax : float, optional
They are equivalent to left and right respectively,
and it is an error to pass both *xmin* and *left* or
*xmax* and *right*.
They are equivalent to left and right respectively, and it is an
error to pass both *xmin* and *left* or *xmax* and *right*.

Returns
-------
Expand Down Expand Up @@ -3691,76 +3690,18 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
present is on the right.

>>> set_xlim(5000, 0)

"""
if right is None and np.iterable(left):
left, right = left
if xmin is not None:
if left is not None:
raise TypeError('Cannot pass both `xmin` and `left`')
raise TypeError("Cannot pass both 'left' and 'xmin'")
left = xmin
if xmax is not None:
if right is not None:
raise TypeError('Cannot pass both `xmax` and `right`')
raise TypeError("Cannot pass both 'right' and 'xmax'")
right = xmax

self._process_unit_info([("x", (left, right))], convert=False)
left = self._validate_converted_limits(left, self.convert_xunits)
right = self._validate_converted_limits(right, self.convert_xunits)

if left is None or right is None:
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
# so only grab the limits if we really need them.
old_left, old_right = self.get_xlim()
if left is None:
left = old_left
if right is None:
right = old_right

if self.get_xscale() == 'log' and (left <= 0 or right <= 0):
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
# so only grab the limits if we really need them.
old_left, old_right = self.get_xlim()
if left <= 0:
_api.warn_external(
'Attempted to set non-positive left xlim on a '
'log-scaled axis.\n'
'Invalid limit will be ignored.')
left = old_left
if right <= 0:
_api.warn_external(
'Attempted to set non-positive right xlim on a '
'log-scaled axis.\n'
'Invalid limit will be ignored.')
right = old_right
if left == right:
_api.warn_external(
f"Attempting to set identical left == right == {left} results "
f"in singular transformations; automatically expanding.")
reverse = left > right
left, right = self.xaxis.get_major_locator().nonsingular(left, right)
left, right = self.xaxis.limit_range_for_scale(left, right)
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
left, right = sorted([left, right], reverse=bool(reverse))

self._viewLim.intervalx = (left, right)
# Mark viewlims as no longer stale without triggering an autoscale.
for ax in self._shared_axes["x"].get_siblings(self):
ax._stale_viewlims["x"] = False
if auto is not None:
self._autoscaleXon = bool(auto)

if emit:
self.callbacks.process('xlim_changed', self)
# Call all of the other x-axes that are shared with this one
for other in self._shared_axes["x"].get_siblings(self):
if other is not self:
other.set_xlim(self.viewLim.intervalx,
emit=False, auto=auto)
if other.figure != self.figure:
other.figure.canvas.draw_idle()
self.stale = True
return left, right
return self.xaxis._set_lim(left, right, emit=emit, auto=auto)

get_xscale = _axis_method_wrapper("xaxis", "get_scale")

Expand Down Expand Up @@ -3985,9 +3926,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
*False* turns off, *None* leaves unchanged.

ymin, ymax : float, optional
They are equivalent to bottom and top respectively,
and it is an error to pass both *ymin* and *bottom* or
*ymax* and *top*.
They are equivalent to bottom and top respectively, and it is an
error to pass both *ymin* and *bottom* or *ymax* and *top*.

Returns
-------
Expand Down Expand Up @@ -4027,71 +3967,13 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
bottom, top = bottom
if ymin is not None:
if bottom is not None:
raise TypeError('Cannot pass both `ymin` and `bottom`')
raise TypeError("Cannot pass both 'bottom' and 'ymin'")
bottom = ymin
if ymax is not None:
if top is not None:
raise TypeError('Cannot pass both `ymax` and `top`')
raise TypeError("Cannot pass both 'top' and 'ymax'")
top = ymax

self._process_unit_info([("y", (bottom, top))], convert=False)
bottom = self._validate_converted_limits(bottom, self.convert_yunits)
top = self._validate_converted_limits(top, self.convert_yunits)

if bottom is None or top is None:
# Axes init calls set_ylim(0, 1) before get_ylim() can be called,
# so only grab the limits if we really need them.
old_bottom, old_top = self.get_ylim()
if bottom is None:
bottom = old_bottom
if top is None:
top = old_top

if self.get_yscale() == 'log' and (bottom <= 0 or top <= 0):
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
# so only grab the limits if we really need them.
old_bottom, old_top = self.get_ylim()
if bottom <= 0:
_api.warn_external(
'Attempted to set non-positive bottom ylim on a '
'log-scaled axis.\n'
'Invalid limit will be ignored.')
bottom = old_bottom
if top <= 0:
_api.warn_external(
'Attempted to set non-positive top ylim on a '
'log-scaled axis.\n'
'Invalid limit will be ignored.')
top = old_top
if bottom == top:
_api.warn_external(
f"Attempting to set identical bottom == top == {bottom} "
f"results in singular transformations; automatically "
f"expanding.")
reverse = bottom > top
bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
bottom, top = sorted([bottom, top], reverse=bool(reverse))

self._viewLim.intervaly = (bottom, top)
# Mark viewlims as no longer stale without triggering an autoscale.
for ax in self._shared_axes["y"].get_siblings(self):
ax._stale_viewlims["y"] = False
if auto is not None:
self._autoscaleYon = bool(auto)

if emit:
self.callbacks.process('ylim_changed', self)
# Call all of the other y-axes that are shared with this one
for other in self._shared_axes["y"].get_siblings(self):
if other is not self:
other.set_ylim(self.viewLim.intervaly,
emit=False, auto=auto)
if other.figure != self.figure:
other.figure.canvas.draw_idle()
self.stale = True
return bottom, top
return self.yaxis._set_lim(bottom, top, emit=emit, auto=auto)

get_yscale = _axis_method_wrapper("yaxis", "get_scale")

Expand Down
94 changes: 78 additions & 16 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1004,10 +1004,9 @@ def set_inverted(self, inverted):
the top for the y-axis; the "inverse" direction is increasing to the
left for the x-axis and to the bottom for the y-axis.
"""
# Currently, must be implemented in subclasses using set_xlim/set_ylim
# rather than generically using set_view_interval, so that shared
# axes get updated as well.
raise NotImplementedError('Derived must override')
a, b = self.get_view_interval()
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
self._set_lim(*sorted((a, b), reverse=bool(inverted)), auto=None)

def set_default_intervals(self):
"""
Expand All @@ -1023,6 +1022,81 @@ def set_default_intervals(self):
# attribute, and the derived code below will check for that
# and use it if it's available (else just use 0..1)

def _set_lim(self, v0, v1, *, emit=True, auto):
"""
Set view limits.

This method is a helper for the Axes ``set_xlim``, ``set_ylim``, and
``set_zlim`` methods.

Parameters
----------
v0, v1 : float
The view limits. (Passing *v0* as a (low, high) pair is not
supported; normalization must occur in the Axes setters.)
emit : bool, default: True
Whether to notify observers of limit change.
auto : bool or None, default: False
Whether to turn on autoscaling of the x-axis. True turns on, False
turns off, None leaves unchanged.
"""
name, = [name for name, axis in self.axes._get_axis_map().items()
Copy link
Member

Choose a reason for hiding this comment

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

This seems a really obscure way to get the name to me. Is there some reason an axis can't know its "name" when it is created?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Axises know their "name", but that's e.g. "r"/"theta" for polar axes whereas here we really want "x"/"y" even for polar. Perhaps Axises should have multiple name attributes for the various use cases, but that would be a bit hairy too...

if axis is self] # The axis name.

self.axes._process_unit_info([(name, (v0, v1))], convert=False)
v0 = self.axes._validate_converted_limits(v0, self.convert_units)
v1 = self.axes._validate_converted_limits(v1, self.convert_units)

if v0 is None or v1 is None:
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
# so only grab the limits if we really need them.
old0, old1 = self.get_view_interval()
if v0 is None:
v0 = old0
if v1 is None:
v1 = old1

if self.get_scale() == 'log' and (v0 <= 0 or v1 <= 0):
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
# so only grab the limits if we really need them.
old0, old1 = self.get_view_interval()
if v0 <= 0:
_api.warn_external(f"Attempt to set non-positive {name}lim on "
f"a log-scaled axis will be ignored.")
v0 = old0
if v1 <= 0:
_api.warn_external(f"Attempt to set non-positive {name}lim on "
f"a log-scaled axis will be ignored.")
v1 = old1
if v0 == v1:
_api.warn_external(
f"Attempting to set identical low and high {name}lims "
f"makes transformation singular; automatically expanding.")
reverse = bool(v0 > v1) # explicit cast needed for python3.8+np.bool_.
v0, v1 = self.get_major_locator().nonsingular(v0, v1)
v0, v1 = self.limit_range_for_scale(v0, v1)
v0, v1 = sorted([v0, v1], reverse=bool(reverse))

self.set_view_interval(v0, v1, ignore=True)
# Mark viewlims as no longer stale without triggering an autoscale.
for ax in self.axes._shared_axes[name].get_siblings(self.axes):
ax._stale_viewlims[name] = False
if auto is not None:
setattr(self.axes, f"_autoscale{name.upper()}on", bool(auto))

if emit:
self.axes.callbacks.process(f"{name}lim_changed", self.axes)
# Call all of the other axes that are shared with this one
for other in self.axes._shared_axes[name].get_siblings(self.axes):
if other is not self.axes:
other._get_axis_map()[name]._set_lim(
v0, v1, emit=False, auto=auto)
if other.figure != self.figure:
other.figure.canvas.draw_idle()

self.stale = True
return v0, v1

def _set_artist_props(self, a):
if a is None:
return
Expand Down Expand Up @@ -2242,12 +2316,6 @@ def get_ticks_position(self):
def get_minpos(self):
return self.axes.dataLim.minposx

def set_inverted(self, inverted):
# docstring inherited
a, b = self.get_view_interval()
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
self.axes.set_xlim(sorted((a, b), reverse=bool(inverted)), auto=None)

def set_default_intervals(self):
# docstring inherited
# only change view if dataLim has not changed and user has
Expand Down Expand Up @@ -2500,12 +2568,6 @@ def get_ticks_position(self):
def get_minpos(self):
return self.axes.dataLim.minposy

def set_inverted(self, inverted):
# docstring inherited
a, b = self.get_view_interval()
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
self.axes.set_ylim(sorted((a, b), reverse=bool(inverted)), auto=None)

def set_default_intervals(self):
# docstring inherited
# only change view if dataLim has not changed and user has
Expand Down
18 changes: 2 additions & 16 deletions lib/matplotlib/projections/polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs):
def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
*, ymin=None, ymax=None):
"""
Set the data limits for the radial axis.
Set the view limits for the radial axis.

Parameters
----------
Expand Down Expand Up @@ -1227,21 +1227,7 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
bottom, top : (float, float)
The new y-axis limits in data coordinates.
"""
if ymin is not None:
if bottom is not None:
raise ValueError('Cannot supply both positional "bottom" '
'argument and kwarg "ymin"')
else:
bottom = ymin
if ymax is not None:
if top is not None:
raise ValueError('Cannot supply both positional "top" '
'argument and kwarg "ymax"')
else:
top = ymax
if top is None and np.iterable(bottom):
bottom, top = bottom[0], bottom[1]
return super().set_ylim(bottom=bottom, top=top, emit=emit, auto=auto)
return super().set_ylim(bottom, top, emit, auto, ymin=ymin, ymax=ymax)

def get_rlabel_position(self):
"""
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2591,10 +2591,10 @@ def test_log_scales_no_data():
def test_log_scales_invalid():
fig, ax = plt.subplots()
ax.set_xscale('log')
with pytest.warns(UserWarning, match='Attempted to set non-positive'):
with pytest.warns(UserWarning, match='Attempt to set non-positive'):
ax.set_xlim(-1, 10)
ax.set_yscale('log')
with pytest.warns(UserWarning, match='Attempted to set non-positive'):
with pytest.warns(UserWarning, match='Attempt to set non-positive'):
ax.set_ylim(-1, 10)


Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ def test_too_many_date_ticks(caplog):
with pytest.warns(UserWarning) as rec:
ax.set_xlim((t0, tf), auto=True)
assert len(rec) == 1
assert \
'Attempting to set identical left == right' in str(rec[0].message)
assert ('Attempting to set identical low and high xlims'
in str(rec[0].message))
ax.plot([], [])
ax.xaxis.set_major_locator(mdates.DayLocator())
v = ax.xaxis.get_major_locator()()
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ def test_imshow_bignumbers_real():
def test_empty_imshow(make_norm):
fig, ax = plt.subplots()
with pytest.warns(UserWarning,
match="Attempting to set identical left == right"):
match="Attempting to set identical low and high xlims"):
im = ax.imshow([[]], norm=make_norm())
im.set_extent([-5, 5, -5, 5])
fig.canvas.draw()
Expand Down
Loading