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

Skip to content

Commit bf557d4

Browse files
committed
Factor out common limits handling for x/y/z axes.
The change in set_top_view is necessary because super().set_xlim no longer goes to 2D semantics (it's on the Axis.set_view_limits that one would need to do a supercall), but in any case the old behavior was actually more buggy because it would e.g. confuse data-limits sharex and 2D projection sharex; e.g. `subplots(1, 2, sharex=True, subplot_kw={"projection": "3d"})` was previously wrong. As a side effect, also fixes `invert_yaxis`/`invert_zaxis` on 3d axises (because they just go to the base `Axis.set_inverted`).
1 parent eda1404 commit bf557d4

File tree

5 files changed

+122
-331
lines changed

5 files changed

+122
-331
lines changed

lib/matplotlib/axes/_base.py

Lines changed: 10 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -3654,9 +3654,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
36543654
False turns off, None leaves unchanged.
36553655
36563656
xmin, xmax : float, optional
3657-
They are equivalent to left and right respectively,
3658-
and it is an error to pass both *xmin* and *left* or
3659-
*xmax* and *right*.
3657+
They are equivalent to left and right respectively, and it is an
3658+
error to pass both *xmin* and *left* or *xmax* and *right*.
36603659
36613660
Returns
36623661
-------
@@ -3691,76 +3690,10 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
36913690
present is on the right.
36923691
36933692
>>> set_xlim(5000, 0)
3694-
36953693
"""
3696-
if right is None and np.iterable(left):
3697-
left, right = left
3698-
if xmin is not None:
3699-
if left is not None:
3700-
raise TypeError('Cannot pass both `xmin` and `left`')
3701-
left = xmin
3702-
if xmax is not None:
3703-
if right is not None:
3704-
raise TypeError('Cannot pass both `xmax` and `right`')
3705-
right = xmax
3706-
3707-
self._process_unit_info([("x", (left, right))], convert=False)
3708-
left = self._validate_converted_limits(left, self.convert_xunits)
3709-
right = self._validate_converted_limits(right, self.convert_xunits)
3710-
3711-
if left is None or right is None:
3712-
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
3713-
# so only grab the limits if we really need them.
3714-
old_left, old_right = self.get_xlim()
3715-
if left is None:
3716-
left = old_left
3717-
if right is None:
3718-
right = old_right
3719-
3720-
if self.get_xscale() == 'log' and (left <= 0 or right <= 0):
3721-
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
3722-
# so only grab the limits if we really need them.
3723-
old_left, old_right = self.get_xlim()
3724-
if left <= 0:
3725-
_api.warn_external(
3726-
'Attempted to set non-positive left xlim on a '
3727-
'log-scaled axis.\n'
3728-
'Invalid limit will be ignored.')
3729-
left = old_left
3730-
if right <= 0:
3731-
_api.warn_external(
3732-
'Attempted to set non-positive right xlim on a '
3733-
'log-scaled axis.\n'
3734-
'Invalid limit will be ignored.')
3735-
right = old_right
3736-
if left == right:
3737-
_api.warn_external(
3738-
f"Attempting to set identical left == right == {left} results "
3739-
f"in singular transformations; automatically expanding.")
3740-
reverse = left > right
3741-
left, right = self.xaxis.get_major_locator().nonsingular(left, right)
3742-
left, right = self.xaxis.limit_range_for_scale(left, right)
3743-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
3744-
left, right = sorted([left, right], reverse=bool(reverse))
3745-
3746-
self._viewLim.intervalx = (left, right)
3747-
# Mark viewlims as no longer stale without triggering an autoscale.
3748-
for ax in self._shared_axes["x"].get_siblings(self):
3749-
ax._stale_viewlims["x"] = False
3750-
if auto is not None:
3751-
self._autoscaleXon = bool(auto)
3752-
3753-
if emit:
3754-
self.callbacks.process('xlim_changed', self)
3755-
# Call all of the other x-axes that are shared with this one
3756-
for other in self._shared_axes["x"].get_siblings(self):
3757-
if other is not self:
3758-
other.set_xlim(self.viewLim.intervalx,
3759-
emit=False, auto=auto)
3760-
if other.figure != self.figure:
3761-
other.figure.canvas.draw_idle()
3762-
self.stale = True
3763-
return left, right
3694+
return self.xaxis._set_lim(
3695+
left, right, xmin, xmax, emit=emit, auto=auto,
3696+
names=("left", "right"))
37643697

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

@@ -3985,9 +3918,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
39853918
*False* turns off, *None* leaves unchanged.
39863919
39873920
ymin, ymax : float, optional
3988-
They are equivalent to bottom and top respectively,
3989-
and it is an error to pass both *ymin* and *bottom* or
3990-
*ymax* and *top*.
3921+
They are equivalent to bottom and top respectively, and it is an
3922+
error to pass both *ymin* and *bottom* or *ymax* and *top*.
39913923
39923924
Returns
39933925
-------
@@ -4023,75 +3955,9 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
40233955
40243956
>>> set_ylim(5000, 0)
40253957
"""
4026-
if top is None and np.iterable(bottom):
4027-
bottom, top = bottom
4028-
if ymin is not None:
4029-
if bottom is not None:
4030-
raise TypeError('Cannot pass both `ymin` and `bottom`')
4031-
bottom = ymin
4032-
if ymax is not None:
4033-
if top is not None:
4034-
raise TypeError('Cannot pass both `ymax` and `top`')
4035-
top = ymax
4036-
4037-
self._process_unit_info([("y", (bottom, top))], convert=False)
4038-
bottom = self._validate_converted_limits(bottom, self.convert_yunits)
4039-
top = self._validate_converted_limits(top, self.convert_yunits)
4040-
4041-
if bottom is None or top is None:
4042-
# Axes init calls set_ylim(0, 1) before get_ylim() can be called,
4043-
# so only grab the limits if we really need them.
4044-
old_bottom, old_top = self.get_ylim()
4045-
if bottom is None:
4046-
bottom = old_bottom
4047-
if top is None:
4048-
top = old_top
4049-
4050-
if self.get_yscale() == 'log' and (bottom <= 0 or top <= 0):
4051-
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
4052-
# so only grab the limits if we really need them.
4053-
old_bottom, old_top = self.get_ylim()
4054-
if bottom <= 0:
4055-
_api.warn_external(
4056-
'Attempted to set non-positive bottom ylim on a '
4057-
'log-scaled axis.\n'
4058-
'Invalid limit will be ignored.')
4059-
bottom = old_bottom
4060-
if top <= 0:
4061-
_api.warn_external(
4062-
'Attempted to set non-positive top ylim on a '
4063-
'log-scaled axis.\n'
4064-
'Invalid limit will be ignored.')
4065-
top = old_top
4066-
if bottom == top:
4067-
_api.warn_external(
4068-
f"Attempting to set identical bottom == top == {bottom} "
4069-
f"results in singular transformations; automatically "
4070-
f"expanding.")
4071-
reverse = bottom > top
4072-
bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
4073-
bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
4074-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
4075-
bottom, top = sorted([bottom, top], reverse=bool(reverse))
4076-
4077-
self._viewLim.intervaly = (bottom, top)
4078-
# Mark viewlims as no longer stale without triggering an autoscale.
4079-
for ax in self._shared_axes["y"].get_siblings(self):
4080-
ax._stale_viewlims["y"] = False
4081-
if auto is not None:
4082-
self._autoscaleYon = bool(auto)
4083-
4084-
if emit:
4085-
self.callbacks.process('ylim_changed', self)
4086-
# Call all of the other y-axes that are shared with this one
4087-
for other in self._shared_axes["y"].get_siblings(self):
4088-
if other is not self:
4089-
other.set_ylim(self.viewLim.intervaly,
4090-
emit=False, auto=auto)
4091-
if other.figure != self.figure:
4092-
other.figure.canvas.draw_idle()
4093-
self.stale = True
4094-
return bottom, top
3958+
return self.yaxis._set_lim(
3959+
bottom, top, ymin, ymax, emit=emit, auto=auto,
3960+
names=("bottom", "top"))
40953961

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

lib/matplotlib/axis.py

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,10 +1004,10 @@ def set_inverted(self, inverted):
10041004
the top for the y-axis; the "inverse" direction is increasing to the
10051005
left for the x-axis and to the bottom for the y-axis.
10061006
"""
1007-
# Currently, must be implemented in subclasses using set_xlim/set_ylim
1008-
# rather than generically using set_view_interval, so that shared
1009-
# axes get updated as well.
1010-
raise NotImplementedError('Derived must override')
1007+
# docstring inherited
1008+
a, b = self.get_view_interval()
1009+
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
1010+
self._set_lim(*sorted((a, b), reverse=bool(inverted)), auto=None)
10111011

10121012
def set_default_intervals(self):
10131013
"""
@@ -1023,6 +1023,97 @@ def set_default_intervals(self):
10231023
# attribute, and the derived code below will check for that
10241024
# and use it if it's available (else just use 0..1)
10251025

1026+
def _set_lim(self, v0, v1, alt0=None, alt1=None, *,
1027+
emit=True, auto, names=("", "")):
1028+
"""
1029+
Set view limits.
1030+
1031+
This method is a helper for the Axes ``set_xlim``, ``set_ylim``, and
1032+
``set_zlim`` methods. This docstring uses names corresponding to
1033+
``set_xlim`` for simplicity.
1034+
1035+
*names* is the pair of the names of the first two parameters of the
1036+
Axes method (e.g., "left" and "right"). They are only used to generate
1037+
error messages; and can be empty if the limits are known to be valid.
1038+
1039+
Other parameters are directly forwarded from the Axes limits setter:
1040+
*v0*, *v1*, *alt0*, and *alt1* map to *left*, *right*, *xmin*, and
1041+
*xmax* respectively; *emit* and *auto* are used as is.
1042+
"""
1043+
v0name, v1name = names # The value names.
1044+
name, = [name for name, axis in self.axes._get_axis_map().items()
1045+
if axis is self] # The axis name.
1046+
1047+
if v1 is None and np.iterable(v0):
1048+
v0, v1 = v0
1049+
if alt0 is not None:
1050+
if v0 is not None:
1051+
raise TypeError(
1052+
f"Cannot pass both {v0name!r} and '{name}lim'")
1053+
v0 = alt0
1054+
if alt1 is not None:
1055+
if v1 is not None:
1056+
raise TypeError(
1057+
f"Cannot pass both {v1name!r} and '{name}lim'")
1058+
v1 = alt1
1059+
1060+
self.axes._process_unit_info([(name, (v0, v1))], convert=False)
1061+
v0 = self.axes._validate_converted_limits(v0, self.convert_units)
1062+
v1 = self.axes._validate_converted_limits(v1, self.convert_units)
1063+
1064+
if v0 is None or v1 is None:
1065+
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
1066+
# so only grab the limits if we really need them.
1067+
old0, old1 = self.get_view_interval()
1068+
if v0 is None:
1069+
v0 = old0
1070+
if v1 is None:
1071+
v1 = old1
1072+
1073+
if self.get_scale() == 'log' and (v0 <= 0 or v1 <= 0):
1074+
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
1075+
# so only grab the limits if we really need them.
1076+
old0, old1 = self.get_view_interval()
1077+
if v0 <= 0:
1078+
_api.warn_external(
1079+
f"Attempt to set non-positive {v0name} {name}lim on a "
1080+
f"log-scaled axis will be ignored.")
1081+
v0 = old0
1082+
if v1 <= 0:
1083+
_api.warn_external(
1084+
f"Attempt to set non-positive {v1name} {name}lim on a "
1085+
f"log-scaled axis will be ignored.")
1086+
v1 = old1
1087+
if v0 == v1:
1088+
_api.warn_external(
1089+
f"Attempting to set identical {v0name} == {v1name} == {v0} "
1090+
f"makes transformation singular; automatically expanding.")
1091+
reverse = v0 > v1
1092+
v0, v1 = self.get_major_locator().nonsingular(v0, v1)
1093+
v0, v1 = self.limit_range_for_scale(v0, v1)
1094+
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
1095+
v0, v1 = sorted([v0, v1], reverse=bool(reverse))
1096+
1097+
self.set_view_interval(v0, v1, ignore=True)
1098+
# Mark viewlims as no longer stale without triggering an autoscale.
1099+
for ax in self.axes._shared_axes[name].get_siblings(self.axes):
1100+
ax._stale_viewlims[name] = False
1101+
if auto is not None:
1102+
setattr(self.axes, f"_autoscale{name.upper()}on", bool(auto))
1103+
1104+
if emit:
1105+
self.axes.callbacks.process(f"{name}lim_changed", self.axes)
1106+
# Call all of the other axes that are shared with this one
1107+
for other in self.axes._shared_axes[name].get_siblings(self.axes):
1108+
if other is not self.axes:
1109+
other._get_axis_map()[name]._set_lim(
1110+
v0name, v1name, v0, v1, emit=False, auto=auto)
1111+
if other.figure != self.figure:
1112+
other.figure.canvas.draw_idle()
1113+
1114+
self.stale = True
1115+
return v0, v1
1116+
10261117
def _set_artist_props(self, a):
10271118
if a is None:
10281119
return
@@ -2242,12 +2333,6 @@ def get_ticks_position(self):
22422333
def get_minpos(self):
22432334
return self.axes.dataLim.minposx
22442335

2245-
def set_inverted(self, inverted):
2246-
# docstring inherited
2247-
a, b = self.get_view_interval()
2248-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
2249-
self.axes.set_xlim(sorted((a, b), reverse=bool(inverted)), auto=None)
2250-
22512336
def set_default_intervals(self):
22522337
# docstring inherited
22532338
# only change view if dataLim has not changed and user has
@@ -2500,12 +2585,6 @@ def get_ticks_position(self):
25002585
def get_minpos(self):
25012586
return self.axes.dataLim.minposy
25022587

2503-
def set_inverted(self, inverted):
2504-
# docstring inherited
2505-
a, b = self.get_view_interval()
2506-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
2507-
self.axes.set_ylim(sorted((a, b), reverse=bool(inverted)), auto=None)
2508-
25092588
def set_default_intervals(self):
25102589
# docstring inherited
25112590
# only change view if dataLim has not changed and user has

lib/matplotlib/projections/polar.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,7 +1194,7 @@ def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs):
11941194
def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
11951195
*, ymin=None, ymax=None):
11961196
"""
1197-
Set the data limits for the radial axis.
1197+
Set the view limits for the radial axis.
11981198
11991199
Parameters
12001200
----------
@@ -1227,21 +1227,7 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
12271227
bottom, top : (float, float)
12281228
The new y-axis limits in data coordinates.
12291229
"""
1230-
if ymin is not None:
1231-
if bottom is not None:
1232-
raise ValueError('Cannot supply both positional "bottom" '
1233-
'argument and kwarg "ymin"')
1234-
else:
1235-
bottom = ymin
1236-
if ymax is not None:
1237-
if top is not None:
1238-
raise ValueError('Cannot supply both positional "top" '
1239-
'argument and kwarg "ymax"')
1240-
else:
1241-
top = ymax
1242-
if top is None and np.iterable(bottom):
1243-
bottom, top = bottom[0], bottom[1]
1244-
return super().set_ylim(bottom=bottom, top=top, emit=emit, auto=auto)
1230+
return super().set_ylim(bottom, top, emit, auto, ymin=ymin, ymax=ymax)
12451231

12461232
def get_rlabel_position(self):
12471233
"""

lib/matplotlib/tests/test_axes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2591,10 +2591,10 @@ def test_log_scales_no_data():
25912591
def test_log_scales_invalid():
25922592
fig, ax = plt.subplots()
25932593
ax.set_xscale('log')
2594-
with pytest.warns(UserWarning, match='Attempted to set non-positive'):
2594+
with pytest.warns(UserWarning, match='Attempt to set non-positive'):
25952595
ax.set_xlim(-1, 10)
25962596
ax.set_yscale('log')
2597-
with pytest.warns(UserWarning, match='Attempted to set non-positive'):
2597+
with pytest.warns(UserWarning, match='Attempt to set non-positive'):
25982598
ax.set_ylim(-1, 10)
25992599

26002600

0 commit comments

Comments
 (0)