Skip to content

Commit 3200e54

Browse files
First pass at exact 3d plot limits
1 parent e7fd79f commit 3200e54

File tree

8 files changed

+178
-78
lines changed

8 files changed

+178
-78
lines changed

lib/matplotlib/axis.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -829,13 +829,14 @@ def _get_autoscale_on(self):
829829
def _set_autoscale_on(self, b):
830830
"""
831831
Set whether this Axis is autoscaled when drawing or by
832-
`.Axes.autoscale_view`.
832+
`.Axes.autoscale_view`. If b is None, then the value is not changed.
833833
834834
Parameters
835835
----------
836836
b : bool
837837
"""
838-
self._autoscale_on = b
838+
if b is not None:
839+
self._autoscale_on = b
839840

840841
def get_children(self):
841842
return [self.label, self.offsetText,
@@ -1219,8 +1220,7 @@ def _set_lim(self, v0, v1, *, emit=True, auto):
12191220
# Mark viewlims as no longer stale without triggering an autoscale.
12201221
for ax in self._get_shared_axes():
12211222
ax._stale_viewlims[name] = False
1222-
if auto is not None:
1223-
self._set_autoscale_on(bool(auto))
1223+
self._set_autoscale_on(auto)
12241224

12251225
if emit:
12261226
self.axes.callbacks.process(f"{name}lim_changed", self.axes)

lib/matplotlib/mpl-data/matplotlibrc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,9 @@
425425
#axes.autolimit_mode: data # If "data", use axes.xmargin and axes.ymargin as is.
426426
# If "round_numbers", after application of margins, axis
427427
# limits are further expanded to the nearest "round" number.
428-
#polaraxes.grid: True # display grid on polar axes
429-
#axes3d.grid: True # display grid on 3D axes
428+
#polaraxes.grid: True # display grid on polar axes
429+
#axes3d.grid: True # display grid on 3D axes
430+
#axes3d.automargin: False # automatically add margin when manually setting 3D axis limits
430431

431432
#axes3d.xaxis.panecolor: (0.95, 0.95, 0.95, 0.5) # background pane on 3D axes
432433
#axes3d.yaxis.panecolor: (0.90, 0.90, 0.90, 0.5) # background pane on 3D axes

lib/matplotlib/mpl-data/stylelib/classic.mplstyle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ axes.spines.left : True
223223
axes.spines.right : True
224224
axes.spines.top : True
225225
polaraxes.grid : True # display grid on polar axes
226-
axes3d.grid : True # display grid on 3d axes
226+
axes3d.grid : True # display grid on 3D axes
227+
axes3d.automargin : False # automatically add margin when manually setting 3D axis limits
227228

228229
date.autoformatter.year : %Y
229230
date.autoformatter.month : %b %Y

lib/matplotlib/rcsetup.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,8 +1016,10 @@ def _convert_validator_spec(key, conv):
10161016
"axes.ymargin": _range_validators["0 <= x <= 1"], # margin added to yaxis
10171017
'axes.zmargin': _range_validators["0 <= x <= 1"], # margin added to zaxis
10181018

1019-
"polaraxes.grid": validate_bool, # display polar grid or not
1020-
"axes3d.grid": validate_bool, # display 3d grid
1019+
"polaraxes.grid": validate_bool, # display polar grid or not
1020+
"axes3d.grid": validate_bool, # display 3d grid
1021+
"axes3d.automargin": validate_bool, # automatically add margin when
1022+
# manually setting 3D axis limits
10211023

10221024
"axes3d.xaxis.panecolor": validate_color, # 3d background pane
10231025
"axes3d.yaxis.panecolor": validate_color, # 3d background pane

lib/matplotlib/ticker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,8 @@ def _raw_ticks(self, vmin, vmax):
20762076
steps = steps[igood]
20772077

20782078
raw_step = ((_vmax - _vmin) / nbins)
2079+
if hasattr(self.axis, "axes") and self.axis.axes.name == '3d':
2080+
raw_step = raw_step * 24/25 # needed to match mpl3.7 appearance
20792081
large_steps = steps >= raw_step
20802082
if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
20812083
# Classic round_numbers mode may require a larger step.

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 137 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ def __init__(
129129

130130
self.xy_viewLim = Bbox.unit()
131131
self.zz_viewLim = Bbox.unit()
132-
self.xy_dataLim = Bbox.unit()
132+
xymargin = 0.05 * 10/11 # match mpl3.7 appearance
133+
self.xy_dataLim = Bbox([[xymargin, xymargin],
134+
[1 - xymargin, 1 - xymargin]])
133135
# z-limits are encoded in the x-component of the Bbox, y is un-used
134136
self.zz_dataLim = Bbox.unit()
135137

@@ -157,6 +159,9 @@ def __init__(
157159
self.set_axis_on()
158160
self.M = None
159161

162+
self._view_margin = 1/48 # default value to match mpl3.7
163+
self.autoscale_view()
164+
160165
# func used to format z -- fall back on major formatters
161166
self.fmt_zdata = None
162167

@@ -336,7 +341,8 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
336341
self.set_ylim3d,
337342
self.set_zlim3d)):
338343
if i in ax_indices:
339-
set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.)
344+
set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.,
345+
auto=None, view_margin=self._view_margin)
340346
else: # 'box'
341347
# Change the box aspect such that the ratio of the length of
342348
# the unmodified axis to the length of the diagonal
@@ -404,8 +410,8 @@ def set_box_aspect(self, aspect, *, zoom=1):
404410
else:
405411
aspect = np.asarray(aspect, dtype=float)
406412
_api.check_shape((3,), aspect=aspect)
407-
# default scale tuned to match the mpl32 appearance.
408-
aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
413+
# default scale tuned to match the mpl3.2 appearance.
414+
aspect *= 1.8294640721620434 * 25/24 * zoom / np.linalg.norm(aspect)
409415

410416
self._box_aspect = aspect
411417
self.stale = True
@@ -575,17 +581,17 @@ def autoscale(self, enable=True, axis='both', tight=None):
575581
scalez = True
576582
else:
577583
if axis in ['x', 'both']:
578-
self.set_autoscalex_on(bool(enable))
584+
self.set_autoscalex_on(enable)
579585
scalex = self.get_autoscalex_on()
580586
else:
581587
scalex = False
582588
if axis in ['y', 'both']:
583-
self.set_autoscaley_on(bool(enable))
589+
self.set_autoscaley_on(enable)
584590
scaley = self.get_autoscaley_on()
585591
else:
586592
scaley = False
587593
if axis in ['z', 'both']:
588-
self.set_autoscalez_on(bool(enable))
594+
self.set_autoscalez_on(enable)
589595
scalez = self.get_autoscalez_on()
590596
else:
591597
scalez = False
@@ -610,8 +616,8 @@ def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
610616
# Let autoscale_view figure out how to use this data.
611617
self.autoscale_view()
612618

613-
def autoscale_view(self, tight=None, scalex=True, scaley=True,
614-
scalez=True):
619+
def autoscale_view(self, tight=None,
620+
scalex=True, scaley=True, scalez=True):
615621
"""
616622
Autoscale the view limits using the data limits.
617623
@@ -643,7 +649,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True,
643649
x1 += delta
644650
if not _tight:
645651
x0, x1 = xlocator.view_limits(x0, x1)
646-
self.set_xbound(x0, x1)
652+
self.set_xbound(x0, x1, self._view_margin)
647653

648654
if scaley and self.get_autoscaley_on():
649655
y0, y1 = self.xy_dataLim.intervaly
@@ -655,7 +661,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True,
655661
y1 += delta
656662
if not _tight:
657663
y0, y1 = ylocator.view_limits(y0, y1)
658-
self.set_ybound(y0, y1)
664+
self.set_ybound(y0, y1, self._view_margin)
659665

660666
if scalez and self.get_autoscalez_on():
661667
z0, z1 = self.zz_dataLim.intervalx
@@ -667,7 +673,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True,
667673
z1 += delta
668674
if not _tight:
669675
z0, z1 = zlocator.view_limits(z0, z1)
670-
self.set_zbound(z0, z1)
676+
self.set_zbound(z0, z1, self._view_margin)
671677

672678
def get_w_lims(self):
673679
"""Get 3D world limits."""
@@ -676,28 +682,116 @@ def get_w_lims(self):
676682
minz, maxz = self.get_zlim3d()
677683
return minx, maxx, miny, maxy, minz, maxz
678684

679-
# set_xlim, set_ylim are directly inherited from base Axes.
685+
def _set_bound3d(self, get_bound, set_lim, axis_inverted,
686+
lower=None, upper=None, view_margin=None):
687+
"""
688+
Set 3D axis bounds.
689+
"""
690+
if upper is None and np.iterable(lower):
691+
lower, upper = lower
692+
693+
old_lower, old_upper = get_bound()
694+
if lower is None:
695+
lower = old_lower
696+
if upper is None:
697+
upper = old_upper
698+
699+
set_lim(sorted((lower, upper), reverse=bool(axis_inverted())),
700+
auto=None, view_margin=view_margin)
701+
702+
def set_xbound(self, lower=None, upper=None, view_margin=None):
703+
# docstring inherited
704+
self._set_bound3d(self.get_xbound, self.set_xlim, self.xaxis_inverted,
705+
upper, lower, view_margin)
706+
707+
def set_ybound(self, lower=None, upper=None, view_margin=None):
708+
# docstring inherited
709+
self._set_bound3d(self.get_ybound, self.set_ylim, self.yaxis_inverted,
710+
upper, lower, view_margin)
711+
712+
def set_zbound(self, lower=None, upper=None, view_margin=None):
713+
"""
714+
Set the lower and upper numerical bounds of the z-axis.
715+
This method will honor axis inversion regardless of parameter order.
716+
It will not change the autoscaling setting (`.get_autoscaley_on()`).
717+
Parameters
718+
----------
719+
lower, upper : float or None
720+
The lower and upper bounds. If *None*, the respective axis bound
721+
is not modified.
722+
view_margin : float or None
723+
The margin to apply to the bounds. If *None*, the margin is handled
724+
by set_zlim.
725+
See Also
726+
--------
727+
get_zbound
728+
get_zlim, set_zlim
729+
invert_zaxis, zaxis_inverted
730+
"""
731+
self._set_bound3d(self.get_zbound, self.set_zlim, self.zaxis_inverted,
732+
upper, lower, view_margin)
733+
734+
def _set_lim3d(self, axis, lower=None, upper=None, *, emit=True,
735+
auto=False, view_margin=None, axmin=None, axmax=None):
736+
"""
737+
Set 3D axis limits.
738+
See `.Axes.set_ylim` for full documentation
739+
"""
740+
if upper is None:
741+
if np.iterable(lower):
742+
lower, upper = lower
743+
else:
744+
raise ValueError("Must provide an upper bound.")
745+
if lower is None:
746+
raise ValueError("Must provide a lower bound.")
747+
if axmin is not None:
748+
if lower is not None:
749+
raise TypeError("Cannot pass both 'lower' and 'min'")
750+
lower = axmin
751+
if axmax is not None:
752+
if upper is not None:
753+
raise TypeError("Cannot pass both 'upper' and 'max'")
754+
upper = axmax
755+
if view_margin is None:
756+
if mpl.rcParams['axes3d.automargin']:
757+
view_margin = self._view_margin
758+
else:
759+
view_margin = 0
760+
delta = (upper - lower) * view_margin
761+
lower -= delta
762+
upper += delta
763+
return axis._set_lim(lower, upper, emit=emit, auto=auto)
764+
765+
def set_xlim(self, left=None, right=None, *, emit=True, auto=False,
766+
view_margin=None, xmin=None, xmax=None):
767+
"""
768+
Set 3D x limits.
769+
See `.Axes.set_xlim` for full documentation
770+
"""
771+
return self._set_lim3d(self.xaxis, left, right, emit=emit, auto=auto,
772+
view_margin=view_margin, axmin=xmin, axmax=xmax)
773+
774+
def set_ylim(self, bottom=None, top=None, *, emit=True, auto=False,
775+
view_margin=None, ymin=None, ymax=None):
776+
"""
777+
Set 3D y limits.
778+
See `.Axes.set_ylim` for full documentation
779+
"""
780+
return self._set_lim3d(self.yaxis, bottom, top, emit=emit, auto=auto,
781+
view_margin=view_margin, axmin=ymin, axmax=ymax)
782+
680783
def set_zlim(self, bottom=None, top=None, *, emit=True, auto=False,
681-
zmin=None, zmax=None):
784+
view_margin=None, zmin=None, zmax=None):
682785
"""
683786
Set 3D z limits.
684787
685788
See `.Axes.set_ylim` for full documentation
686789
"""
687-
if top is None and np.iterable(bottom):
688-
bottom, top = bottom
689-
if zmin is not None:
690-
if bottom is not None:
691-
raise TypeError("Cannot pass both 'bottom' and 'zmin'")
692-
bottom = zmin
693-
if zmax is not None:
694-
if top is not None:
695-
raise TypeError("Cannot pass both 'top' and 'zmax'")
696-
top = zmax
697-
return self.zaxis._set_lim(bottom, top, emit=emit, auto=auto)
698-
699-
set_xlim3d = maxes.Axes.set_xlim
700-
set_ylim3d = maxes.Axes.set_ylim
790+
return self._set_lim3d(self.zaxis, bottom, top, emit=emit, auto=auto,
791+
view_margin=view_margin, axmin=zmin, axmax=zmax)
792+
793+
set_xlim3d = set_xlim
794+
set_ylim3d = set_ylim
701795
set_zlim3d = set_zlim
702796

703797
def get_xlim(self):
@@ -982,6 +1076,15 @@ def clear(self):
9821076
self._zmargin = mpl.rcParams['axes.zmargin']
9831077
else:
9841078
self._zmargin = 0.
1079+
1080+
xymargin = 0.05 * 10/11 # match mpl3.7 appearance
1081+
self.xy_dataLim = Bbox([[xymargin, xymargin],
1082+
[1 - xymargin, 1 - xymargin]])
1083+
# z-limits are encoded in the x-component of the Bbox, y is un-used
1084+
self.zz_dataLim = Bbox.unit()
1085+
self._view_margin = 1/48 # default value to match mpl3.7
1086+
self.autoscale_view()
1087+
9851088
self.grid(mpl.rcParams['axes3d.grid'])
9861089

9871090
def _button_press(self, event):
@@ -1162,9 +1265,9 @@ def drag_pan(self, button, key, x, y):
11621265
dz = (maxz - minz) * duvw_projected[2]
11631266

11641267
# Set the new axis limits
1165-
self.set_xlim3d(minx + dx, maxx + dx)
1166-
self.set_ylim3d(miny + dy, maxy + dy)
1167-
self.set_zlim3d(minz + dz, maxz + dz)
1268+
self.set_xlim3d(minx + dx, maxx + dx, auto=None)
1269+
self.set_ylim3d(miny + dy, maxy + dy, auto=None)
1270+
self.set_zlim3d(minz + dz, maxz + dz, auto=None)
11681271

11691272
def _calc_view_axes(self, eye):
11701273
"""
@@ -1301,9 +1404,9 @@ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
13011404
dz = (maxz - minz)*scale_z
13021405

13031406
# Set the scaled axis limits
1304-
self.set_xlim3d(cx - dx/2, cx + dx/2)
1305-
self.set_ylim3d(cy - dy/2, cy + dy/2)
1306-
self.set_zlim3d(cz - dz/2, cz + dz/2)
1407+
self.set_xlim3d(cx - dx/2, cx + dx/2, auto=None)
1408+
self.set_ylim3d(cy - dy/2, cy + dy/2, auto=None)
1409+
self.set_zlim3d(cz - dz/2, cz + dz/2, auto=None)
13071410

13081411
def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
13091412
"""
@@ -1392,26 +1495,6 @@ def get_zbound(self):
13921495
else:
13931496
return top, bottom
13941497

1395-
def set_zbound(self, lower=None, upper=None):
1396-
"""
1397-
Set the lower and upper numerical bounds of the z-axis.
1398-
1399-
This method will honor axes inversion regardless of parameter order.
1400-
It will not change the autoscaling setting (`.get_autoscalez_on()`).
1401-
"""
1402-
if upper is None and np.iterable(lower):
1403-
lower, upper = lower
1404-
1405-
old_lower, old_upper = self.get_zbound()
1406-
if lower is None:
1407-
lower = old_lower
1408-
if upper is None:
1409-
upper = old_upper
1410-
1411-
self.set_zlim(sorted((lower, upper),
1412-
reverse=bool(self.zaxis_inverted())),
1413-
auto=None)
1414-
14151498
def text(self, x, y, z, s, zdir=None, **kwargs):
14161499
"""
14171500
Add text to the plot.

0 commit comments

Comments
 (0)