Skip to content

Commit f840d22

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 f4d83d0 commit f840d22

File tree

5 files changed

+117
-331
lines changed

5 files changed

+117
-331
lines changed

lib/matplotlib/axes/_base.py

+8-144
Original file line numberDiff line numberDiff line change
@@ -3630,9 +3630,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
36303630
False turns off, None leaves unchanged.
36313631
36323632
xmin, xmax : float, optional
3633-
They are equivalent to left and right respectively,
3634-
and it is an error to pass both *xmin* and *left* or
3635-
*xmax* and *right*.
3633+
They are equivalent to left and right respectively, and it is an
3634+
error to pass both *xmin* and *left* or *xmax* and *right*.
36363635
36373636
Returns
36383637
-------
@@ -3667,76 +3666,9 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False,
36673666
present is on the right.
36683667
36693668
>>> set_xlim(5000, 0)
3670-
36713669
"""
3672-
if right is None and np.iterable(left):
3673-
left, right = left
3674-
if xmin is not None:
3675-
if left is not None:
3676-
raise TypeError('Cannot pass both `xmin` and `left`')
3677-
left = xmin
3678-
if xmax is not None:
3679-
if right is not None:
3680-
raise TypeError('Cannot pass both `xmax` and `right`')
3681-
right = xmax
3682-
3683-
self._process_unit_info([("x", (left, right))], convert=False)
3684-
left = self._validate_converted_limits(left, self.convert_xunits)
3685-
right = self._validate_converted_limits(right, self.convert_xunits)
3686-
3687-
if left is None or right is None:
3688-
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
3689-
# so only grab the limits if we really need them.
3690-
old_left, old_right = self.get_xlim()
3691-
if left is None:
3692-
left = old_left
3693-
if right is None:
3694-
right = old_right
3695-
3696-
if self.get_xscale() == 'log' and (left <= 0 or right <= 0):
3697-
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
3698-
# so only grab the limits if we really need them.
3699-
old_left, old_right = self.get_xlim()
3700-
if left <= 0:
3701-
_api.warn_external(
3702-
'Attempted to set non-positive left xlim on a '
3703-
'log-scaled axis.\n'
3704-
'Invalid limit will be ignored.')
3705-
left = old_left
3706-
if right <= 0:
3707-
_api.warn_external(
3708-
'Attempted to set non-positive right xlim on a '
3709-
'log-scaled axis.\n'
3710-
'Invalid limit will be ignored.')
3711-
right = old_right
3712-
if left == right:
3713-
_api.warn_external(
3714-
f"Attempting to set identical left == right == {left} results "
3715-
f"in singular transformations; automatically expanding.")
3716-
reverse = left > right
3717-
left, right = self.xaxis.get_major_locator().nonsingular(left, right)
3718-
left, right = self.xaxis.limit_range_for_scale(left, right)
3719-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
3720-
left, right = sorted([left, right], reverse=bool(reverse))
3721-
3722-
self._viewLim.intervalx = (left, right)
3723-
# Mark viewlims as no longer stale without triggering an autoscale.
3724-
for ax in self._shared_axes["x"].get_siblings(self):
3725-
ax._stale_viewlims["x"] = False
3726-
if auto is not None:
3727-
self._autoscaleXon = bool(auto)
3728-
3729-
if emit:
3730-
self.callbacks.process('xlim_changed', self)
3731-
# Call all of the other x-axes that are shared with this one
3732-
for other in self._shared_axes["x"].get_siblings(self):
3733-
if other is not self:
3734-
other.set_xlim(self.viewLim.intervalx,
3735-
emit=False, auto=auto)
3736-
if other.figure != self.figure:
3737-
other.figure.canvas.draw_idle()
3738-
self.stale = True
3739-
return left, right
3670+
return self.xaxis._set_lim(
3671+
"left", "right", left, right, xmin, xmax, emit=emit, auto=auto)
37403672

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

@@ -3957,9 +3889,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
39573889
*False* turns off, *None* leaves unchanged.
39583890
39593891
ymin, ymax : float, optional
3960-
They are equivalent to bottom and top respectively,
3961-
and it is an error to pass both *ymin* and *bottom* or
3962-
*ymax* and *top*.
3892+
They are equivalent to bottom and top respectively, and it is an
3893+
error to pass both *ymin* and *bottom* or *ymax* and *top*.
39633894
39643895
Returns
39653896
-------
@@ -3995,75 +3926,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False,
39953926
39963927
>>> set_ylim(5000, 0)
39973928
"""
3998-
if top is None and np.iterable(bottom):
3999-
bottom, top = bottom
4000-
if ymin is not None:
4001-
if bottom is not None:
4002-
raise TypeError('Cannot pass both `ymin` and `bottom`')
4003-
bottom = ymin
4004-
if ymax is not None:
4005-
if top is not None:
4006-
raise TypeError('Cannot pass both `ymax` and `top`')
4007-
top = ymax
4008-
4009-
self._process_unit_info([("y", (bottom, top))], convert=False)
4010-
bottom = self._validate_converted_limits(bottom, self.convert_yunits)
4011-
top = self._validate_converted_limits(top, self.convert_yunits)
4012-
4013-
if bottom is None or top is None:
4014-
# Axes init calls set_ylim(0, 1) before get_ylim() can be called,
4015-
# so only grab the limits if we really need them.
4016-
old_bottom, old_top = self.get_ylim()
4017-
if bottom is None:
4018-
bottom = old_bottom
4019-
if top is None:
4020-
top = old_top
4021-
4022-
if self.get_yscale() == 'log' and (bottom <= 0 or top <= 0):
4023-
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
4024-
# so only grab the limits if we really need them.
4025-
old_bottom, old_top = self.get_ylim()
4026-
if bottom <= 0:
4027-
_api.warn_external(
4028-
'Attempted to set non-positive bottom ylim on a '
4029-
'log-scaled axis.\n'
4030-
'Invalid limit will be ignored.')
4031-
bottom = old_bottom
4032-
if top <= 0:
4033-
_api.warn_external(
4034-
'Attempted to set non-positive top ylim on a '
4035-
'log-scaled axis.\n'
4036-
'Invalid limit will be ignored.')
4037-
top = old_top
4038-
if bottom == top:
4039-
_api.warn_external(
4040-
f"Attempting to set identical bottom == top == {bottom} "
4041-
f"results in singular transformations; automatically "
4042-
f"expanding.")
4043-
reverse = bottom > top
4044-
bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
4045-
bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
4046-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
4047-
bottom, top = sorted([bottom, top], reverse=bool(reverse))
4048-
4049-
self._viewLim.intervaly = (bottom, top)
4050-
# Mark viewlims as no longer stale without triggering an autoscale.
4051-
for ax in self._shared_axes["y"].get_siblings(self):
4052-
ax._stale_viewlims["y"] = False
4053-
if auto is not None:
4054-
self._autoscaleYon = bool(auto)
4055-
4056-
if emit:
4057-
self.callbacks.process('ylim_changed', self)
4058-
# Call all of the other y-axes that are shared with this one
4059-
for other in self._shared_axes["y"].get_siblings(self):
4060-
if other is not self:
4061-
other.set_ylim(self.viewLim.intervaly,
4062-
emit=False, auto=auto)
4063-
if other.figure != self.figure:
4064-
other.figure.canvas.draw_idle()
4065-
self.stale = True
4066-
return bottom, top
3929+
return self.yaxis._set_lim(
3930+
"bottom", "top", bottom, top, ymin, ymax, emit=emit, auto=auto)
40673931

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

lib/matplotlib/axis.py

+95-16
Original file line numberDiff line numberDiff line change
@@ -993,10 +993,11 @@ def set_inverted(self, inverted):
993993
the top for the y-axis; the "inverse" direction is increasing to the
994994
left for the x-axis and to the bottom for the y-axis.
995995
"""
996-
# Currently, must be implemented in subclasses using set_xlim/set_ylim
997-
# rather than generically using set_view_interval, so that shared
998-
# axes get updated as well.
999-
raise NotImplementedError('Derived must override')
996+
# docstring inherited
997+
a, b = self.get_view_interval()
998+
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
999+
self._set_lim("", "", *sorted((a, b), reverse=bool(inverted)),
1000+
auto=None)
10001001

10011002
def set_default_intervals(self):
10021003
"""
@@ -1012,6 +1013,96 @@ def set_default_intervals(self):
10121013
# attribute, and the derived code below will check for that
10131014
# and use it if it's available (else just use 0..1)
10141015

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

2232-
def set_inverted(self, inverted):
2233-
# docstring inherited
2234-
a, b = self.get_view_interval()
2235-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
2236-
self.axes.set_xlim(sorted((a, b), reverse=bool(inverted)), auto=None)
2237-
22382323
def set_default_intervals(self):
22392324
# docstring inherited
22402325
# only change view if dataLim has not changed and user has
@@ -2490,12 +2575,6 @@ def get_ticks_position(self):
24902575
def get_minpos(self):
24912576
return self.axes.dataLim.minposy
24922577

2493-
def set_inverted(self, inverted):
2494-
# docstring inherited
2495-
a, b = self.get_view_interval()
2496-
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
2497-
self.axes.set_ylim(sorted((a, b), reverse=bool(inverted)), auto=None)
2498-
24992578
def set_default_intervals(self):
25002579
# docstring inherited
25012580
# only change view if dataLim has not changed and user has

lib/matplotlib/projections/polar.py

+2-16
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

+2-2
Original file line numberDiff line numberDiff line change
@@ -2539,10 +2539,10 @@ def test_log_scales_no_data():
25392539
def test_log_scales_invalid():
25402540
fig, ax = plt.subplots()
25412541
ax.set_xscale('log')
2542-
with pytest.warns(UserWarning, match='Attempted to set non-positive'):
2542+
with pytest.warns(UserWarning, match='Attempt to set non-positive'):
25432543
ax.set_xlim(-1, 10)
25442544
ax.set_yscale('log')
2545-
with pytest.warns(UserWarning, match='Attempted to set non-positive'):
2545+
with pytest.warns(UserWarning, match='Attempt to set non-positive'):
25462546
ax.set_ylim(-1, 10)
25472547

25482548

0 commit comments

Comments
 (0)