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

Skip to content

Commit 242346d

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 8610965 commit 242346d

File tree

2 files changed

+90
-4
lines changed

2 files changed

+90
-4
lines changed

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def __init__(
9494
self.zz_viewLim = Bbox.unit()
9595
self.xy_dataLim = Bbox.unit()
9696
self.zz_dataLim = Bbox.unit()
97+
self._stale_viewlim_z = False
9798

9899
# inhibit autoscale_view until the axes are defined
99100
# they can't be defined until Axes.__init__ has been called
@@ -232,6 +233,24 @@ def w_zaxis(self):
232233
def _get_axis_list(self):
233234
return super()._get_axis_list() + (self.zaxis, )
234235

236+
def _unstale_viewLim(self):
237+
# We should arrange to store this information once per share-group
238+
# instead of on every axis.
239+
scalex = any(ax._stale_viewlim_x
240+
for ax in self._shared_x_axes.get_siblings(self))
241+
scaley = any(ax._stale_viewlim_y
242+
for ax in self._shared_y_axes.get_siblings(self))
243+
scalez = any(ax._stale_viewlim_z
244+
for ax in self._shared_z_axes.get_siblings(self))
245+
if scalex or scaley or scalez:
246+
for ax in self._shared_x_axes.get_siblings(self):
247+
ax._stale_viewlim_x = False
248+
for ax in self._shared_y_axes.get_siblings(self):
249+
ax._stale_viewlim_y = False
250+
for ax in self._shared_z_axes.get_siblings(self):
251+
ax._stale_viewlim_z = False
252+
self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
253+
235254
def unit_cube(self, vals=None):
236255
minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
237256
return [(minx, miny, minz),
@@ -416,6 +435,8 @@ def apply_aspect(self, position=None):
416435

417436
@artist.allow_rasterization
418437
def draw(self, renderer):
438+
self._unstale_viewLim()
439+
419440
# draw the background patch
420441
self.patch.draw(renderer)
421442
self._frameon = False
@@ -480,7 +501,8 @@ def _on_units_changed(self, scalex=False, scaley=False, scalez=False):
480501
Currently forces updates of data limits and view limits.
481502
"""
482503
self.relim()
483-
self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
504+
self._request_autoscale_view(scalex=scalex, scaley=scaley,
505+
scalez=scalez)
484506

485507
def update_datalim(self, xys, **kwargs):
486508
pass
@@ -529,6 +551,24 @@ def set_autoscalez_on(self, b):
529551
"""
530552
self._autoscaleZon = b
531553

554+
def set_xmargin(self, m):
555+
# docstring inherited
556+
scalez = self._stale_viewlim_z
557+
super().set_xmargin(m)
558+
# Superclass is 2D and will call _request_autoscale_view with defaults
559+
# for unknown Axis, which would be scalez=True, but it shouldn't be for
560+
# this call, so restore it.
561+
self._stale_viewlim_z = scalez
562+
563+
def set_ymargin(self, m):
564+
# docstring inherited
565+
scalez = self._stale_viewlim_z
566+
super().set_ymargin(m)
567+
# Superclass is 2D and will call _request_autoscale_view with defaults
568+
# for unknown Axis, which would be scalez=True, but it shouldn't be for
569+
# this call, so restore it.
570+
self._stale_viewlim_z = scalez
571+
532572
def set_zmargin(self, m):
533573
"""
534574
Set padding of Z data limits prior to autoscaling.
@@ -543,6 +583,7 @@ def set_zmargin(self, m):
543583
if m < 0 or m > 1:
544584
raise ValueError("margin must be in range 0 to 1")
545585
self._zmargin = m
586+
self._request_autoscale_view(scalex=False, scaley=False, scalez=True)
546587
self.stale = True
547588

548589
def margins(self, *margins, x=None, y=None, z=None, tight=True):
@@ -640,8 +681,8 @@ def autoscale(self, enable=True, axis='both', tight=None):
640681
self._autoscaleZon = scalez = bool(enable)
641682
else:
642683
scalez = False
643-
self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
644-
scalez=scalez)
684+
self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
685+
scalez=scalez)
645686

646687
def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
647688
# This updates the bounding boxes as to keep a record as to what the
@@ -657,6 +698,19 @@ def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
657698
# Let autoscale_view figure out how to use this data.
658699
self.autoscale_view()
659700

701+
# API could be better, right now this is just to match the old calls to
702+
# autoscale_view() after each plotting method.
703+
def _request_autoscale_view(self, tight=None, scalex=True, scaley=True,
704+
scalez=True):
705+
if tight is not None:
706+
self._tight = tight
707+
if scalex:
708+
self._stale_viewlim_x = True # Else keep old state.
709+
if scaley:
710+
self._stale_viewlim_y = True
711+
if scalez:
712+
self._stale_viewlim_z = True
713+
660714
def autoscale_view(self, tight=None, scalex=True, scaley=True,
661715
scalez=True):
662716
"""
@@ -767,6 +821,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
767821
left, right = sorted([left, right], reverse=bool(reverse))
768822
self.xy_viewLim.intervalx = (left, right)
769823

824+
# Mark viewlims as no longer stale without triggering an autoscale.
825+
for ax in self._shared_x_axes.get_siblings(self):
826+
ax._stale_viewlim_x = False
770827
if auto is not None:
771828
self._autoscaleXon = bool(auto)
772829

@@ -822,6 +879,9 @@ def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False,
822879
bottom, top = top, bottom
823880
self.xy_viewLim.intervaly = (bottom, top)
824881

882+
# Mark viewlims as no longer stale without triggering an autoscale.
883+
for ax in self._shared_y_axes.get_siblings(self):
884+
ax._stale_viewlim_y = False
825885
if auto is not None:
826886
self._autoscaleYon = bool(auto)
827887

@@ -877,6 +937,9 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False,
877937
bottom, top = top, bottom
878938
self.zz_viewLim.intervalx = (bottom, top)
879939

940+
# Mark viewlims as no longer stale without triggering an autoscale.
941+
for ax in self._shared_z_axes.get_siblings(self):
942+
ax._stale_viewlim_z = False
880943
if auto is not None:
881944
self._autoscaleZon = bool(auto)
882945

@@ -1333,7 +1396,8 @@ def locator_params(self, axis='both', tight=None, **kwargs):
13331396
self.yaxis.get_major_locator().set_params(**kwargs)
13341397
if _z:
13351398
self.zaxis.get_major_locator().set_params(**kwargs)
1336-
self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
1399+
self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y,
1400+
scalez=_z)
13371401

13381402
def tick_params(self, axis='both', **kwargs):
13391403
"""

lib/mpl_toolkits/tests/test_mplot3d.py

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

795795

796+
@pytest.mark.parametrize('axis', ('x', 'y', 'z'))
797+
@pytest.mark.parametrize('auto', (True, False, None))
798+
def test_unautoscale(axis, auto):
799+
fig = plt.figure()
800+
ax = fig.gca(projection='3d')
801+
802+
x = np.arange(100)
803+
y = np.linspace(-0.1, 0.1, 100)
804+
ax.scatter(x, y)
805+
806+
get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
807+
set_lim = getattr(ax, f'set_{axis}lim')
808+
get_lim = getattr(ax, f'get_{axis}lim')
809+
810+
post_auto = get_autoscale_on() if auto is None else auto
811+
812+
set_lim((-0.5, 0.5), auto=auto)
813+
assert post_auto == get_autoscale_on()
814+
fig.canvas.draw()
815+
np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))
816+
817+
796818
@mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False)
797819
def test_axes3d_ortho():
798820
fig = plt.figure()

0 commit comments

Comments
 (0)