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

Skip to content

Commit cfb046e

Browse files
committed
Implement lazy autoscaling in mplot3d.
Since `Axes3D` derives from 2D `Axes`, this lazy autoscale was partially implemented. This might cause extraneous re-scaling since the 3D case did not always turn it off. It might also cause re-scaling to be missed since 2D `Axes` does not check z. Fixes #18112.
1 parent 09ab2c2 commit cfb046e

File tree

2 files changed

+91
-6
lines changed

2 files changed

+91
-6
lines changed

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def __init__(
9090
self.zz_viewLim = Bbox.unit()
9191
self.xy_dataLim = Bbox.unit()
9292
self.zz_dataLim = Bbox.unit()
93+
self._stale_viewlim_z = False
9394

9495
# inhibit autoscale_view until the axes are defined
9596
# they can't be defined until Axes.__init__ has been called
@@ -191,6 +192,24 @@ def get_zaxis(self):
191192
def _get_axis_list(self):
192193
return super()._get_axis_list() + (self.zaxis, )
193194

195+
def _unstale_viewLim(self):
196+
# We should arrange to store this information once per share-group
197+
# instead of on every axis.
198+
scalex = any(ax._stale_viewlim_x
199+
for ax in self._shared_x_axes.get_siblings(self))
200+
scaley = any(ax._stale_viewlim_y
201+
for ax in self._shared_y_axes.get_siblings(self))
202+
scalez = any(ax._stale_viewlim_z
203+
for ax in self._shared_z_axes.get_siblings(self))
204+
if scalex or scaley or scalez:
205+
for ax in self._shared_x_axes.get_siblings(self):
206+
ax._stale_viewlim_x = False
207+
for ax in self._shared_y_axes.get_siblings(self):
208+
ax._stale_viewlim_y = False
209+
for ax in self._shared_z_axes.get_siblings(self):
210+
ax._stale_viewlim_z = False
211+
self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
212+
194213
def unit_cube(self, vals=None):
195214
minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
196215
return [(minx, miny, minz),
@@ -378,6 +397,8 @@ def apply_aspect(self, position=None):
378397

379398
@artist.allow_rasterization
380399
def draw(self, renderer):
400+
self._unstale_viewLim()
401+
381402
# draw the background patch
382403
self.patch.draw(renderer)
383404
self._frameon = False
@@ -477,9 +498,9 @@ def _unit_change_handler(self, axis_name, event=None):
477498
self._unit_change_handler, axis_name, event=object())
478499
_api.check_in_list(self._get_axis_map(), axis_name=axis_name)
479500
self.relim()
480-
self.autoscale_view(scalex=(axis_name == "x"),
481-
scaley=(axis_name == "y"),
482-
scalez=(axis_name == "z"))
501+
self._request_autoscale_view(scalex=(axis_name == "x"),
502+
scaley=(axis_name == "y"),
503+
scalez=(axis_name == "z"))
483504

484505
def update_datalim(self, xys, **kwargs):
485506
pass
@@ -528,6 +549,24 @@ def set_autoscalez_on(self, b):
528549
"""
529550
self._autoscaleZon = b
530551

552+
def set_xmargin(self, m):
553+
# docstring inherited
554+
scalez = self._stale_viewlim_z
555+
super().set_xmargin(m)
556+
# Superclass is 2D and will call _request_autoscale_view with defaults
557+
# for unknown Axis, which would be scalez=True, but it shouldn't be for
558+
# this call, so restore it.
559+
self._stale_viewlim_z = scalez
560+
561+
def set_ymargin(self, m):
562+
# docstring inherited
563+
scalez = self._stale_viewlim_z
564+
super().set_ymargin(m)
565+
# Superclass is 2D and will call _request_autoscale_view with defaults
566+
# for unknown Axis, which would be scalez=True, but it shouldn't be for
567+
# this call, so restore it.
568+
self._stale_viewlim_z = scalez
569+
531570
def set_zmargin(self, m):
532571
"""
533572
Set padding of Z data limits prior to autoscaling.
@@ -542,6 +581,7 @@ def set_zmargin(self, m):
542581
if m < 0 or m > 1:
543582
raise ValueError("margin must be in range 0 to 1")
544583
self._zmargin = m
584+
self._request_autoscale_view(scalex=False, scaley=False, scalez=True)
545585
self.stale = True
546586

547587
def margins(self, *margins, x=None, y=None, z=None, tight=True):
@@ -639,8 +679,8 @@ def autoscale(self, enable=True, axis='both', tight=None):
639679
self._autoscaleZon = scalez = bool(enable)
640680
else:
641681
scalez = False
642-
self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
643-
scalez=scalez)
682+
self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
683+
scalez=scalez)
644684

645685
def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
646686
# This updates the bounding boxes as to keep a record as to what the
@@ -656,6 +696,19 @@ def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
656696
# Let autoscale_view figure out how to use this data.
657697
self.autoscale_view()
658698

699+
# API could be better, right now this is just to match the old calls to
700+
# autoscale_view() after each plotting method.
701+
def _request_autoscale_view(self, tight=None, scalex=True, scaley=True,
702+
scalez=True):
703+
if tight is not None:
704+
self._tight = tight
705+
if scalex:
706+
self._stale_viewlim_x = True # Else keep old state.
707+
if scaley:
708+
self._stale_viewlim_y = True
709+
if scalez:
710+
self._stale_viewlim_z = True
711+
659712
def autoscale_view(self, tight=None, scalex=True, scaley=True,
660713
scalez=True):
661714
"""
@@ -766,6 +819,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
766819
left, right = sorted([left, right], reverse=bool(reverse))
767820
self.xy_viewLim.intervalx = (left, right)
768821

822+
# Mark viewlims as no longer stale without triggering an autoscale.
823+
for ax in self._shared_x_axes.get_siblings(self):
824+
ax._stale_viewlim_x = False
769825
if auto is not None:
770826
self._autoscaleXon = bool(auto)
771827

@@ -821,6 +877,9 @@ def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False,
821877
bottom, top = top, bottom
822878
self.xy_viewLim.intervaly = (bottom, top)
823879

880+
# Mark viewlims as no longer stale without triggering an autoscale.
881+
for ax in self._shared_y_axes.get_siblings(self):
882+
ax._stale_viewlim_y = False
824883
if auto is not None:
825884
self._autoscaleYon = bool(auto)
826885

@@ -876,6 +935,9 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False,
876935
bottom, top = top, bottom
877936
self.zz_viewLim.intervalx = (bottom, top)
878937

938+
# Mark viewlims as no longer stale without triggering an autoscale.
939+
for ax in self._shared_z_axes.get_siblings(self):
940+
ax._stale_viewlim_z = False
879941
if auto is not None:
880942
self._autoscaleZon = bool(auto)
881943

@@ -1346,7 +1408,8 @@ def locator_params(self, axis='both', tight=None, **kwargs):
13461408
self.yaxis.get_major_locator().set_params(**kwargs)
13471409
if _z:
13481410
self.zaxis.get_major_locator().set_params(**kwargs)
1349-
self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
1411+
self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y,
1412+
scalez=_z)
13501413

13511414
def tick_params(self, axis='both', **kwargs):
13521415
"""

lib/mpl_toolkits/tests/test_mplot3d.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,28 @@ def test_autoscale():
895895
assert ax.get_w_lims() == (0, 1, -.1, 1.1, -.4, 2.4)
896896

897897

898+
@pytest.mark.parametrize('axis', ('x', 'y', 'z'))
899+
@pytest.mark.parametrize('auto', (True, False, None))
900+
def test_unautoscale(axis, auto):
901+
fig = plt.figure()
902+
ax = fig.gca(projection='3d')
903+
904+
x = np.arange(100)
905+
y = np.linspace(-0.1, 0.1, 100)
906+
ax.scatter(x, y)
907+
908+
get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
909+
set_lim = getattr(ax, f'set_{axis}lim')
910+
get_lim = getattr(ax, f'get_{axis}lim')
911+
912+
post_auto = get_autoscale_on() if auto is None else auto
913+
914+
set_lim((-0.5, 0.5), auto=auto)
915+
assert post_auto == get_autoscale_on()
916+
fig.canvas.draw()
917+
np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))
918+
919+
898920
@mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False)
899921
def test_axes3d_ortho():
900922
fig = plt.figure()

0 commit comments

Comments
 (0)