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

Skip to content

Commit bce9c8d

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 29ba9a9 commit bce9c8d

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
@@ -92,6 +92,7 @@ def __init__(
9292
self.zz_viewLim = Bbox.unit()
9393
self.xy_dataLim = Bbox.unit()
9494
self.zz_dataLim = Bbox.unit()
95+
self._stale_viewlim_z = False
9596

9697
# inhibit autoscale_view until the axes are defined
9798
# they can't be defined until Axes.__init__ has been called
@@ -230,6 +231,24 @@ def w_zaxis(self):
230231
def _get_axis_list(self):
231232
return super()._get_axis_list() + (self.zaxis, )
232233

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

415434
@artist.allow_rasterization
416435
def draw(self, renderer):
436+
self._unstale_viewLim()
437+
417438
# draw the background patch
418439
self.patch.draw(renderer)
419440
self._frameon = False
@@ -478,7 +499,8 @@ def _on_units_changed(self, scalex=False, scaley=False, scalez=False):
478499
Currently forces updates of data limits and view limits.
479500
"""
480501
self.relim()
481-
self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
502+
self._request_autoscale_view(scalex=scalex, scaley=scaley,
503+
scalez=scalez)
482504

483505
def update_datalim(self, xys, **kwargs):
484506
pass
@@ -527,6 +549,24 @@ def set_autoscalez_on(self, b):
527549
"""
528550
self._autoscaleZon = b
529551

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+
530570
def set_zmargin(self, m):
531571
"""
532572
Set padding of Z data limits prior to autoscaling.
@@ -541,6 +581,7 @@ def set_zmargin(self, m):
541581
if m < 0 or m > 1:
542582
raise ValueError("margin must be in range 0 to 1")
543583
self._zmargin = m
584+
self._request_autoscale_view(scalex=False, scaley=False, scalez=True)
544585
self.stale = True
545586

546587
def margins(self, *margins, x=None, y=None, z=None, tight=True):
@@ -638,8 +679,8 @@ def autoscale(self, enable=True, axis='both', tight=None):
638679
self._autoscaleZon = scalez = bool(enable)
639680
else:
640681
scalez = False
641-
self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
642-
scalez=scalez)
682+
self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
683+
scalez=scalez)
643684

644685
def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
645686
# This updates the bounding boxes as to keep a record as to what the
@@ -655,6 +696,19 @@ def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
655696
# Let autoscale_view figure out how to use this data.
656697
self.autoscale_view()
657698

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+
658712
def autoscale_view(self, tight=None, scalex=True, scaley=True,
659713
scalez=True):
660714
"""
@@ -765,6 +819,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
765819
left, right = sorted([left, right], reverse=bool(reverse))
766820
self.xy_viewLim.intervalx = (left, right)
767821

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
768825
if auto is not None:
769826
self._autoscaleXon = bool(auto)
770827

@@ -820,6 +877,9 @@ def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False,
820877
bottom, top = top, bottom
821878
self.xy_viewLim.intervaly = (bottom, top)
822879

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
823883
if auto is not None:
824884
self._autoscaleYon = bool(auto)
825885

@@ -875,6 +935,9 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False,
875935
bottom, top = top, bottom
876936
self.zz_viewLim.intervalx = (bottom, top)
877937

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
878941
if auto is not None:
879942
self._autoscaleZon = bool(auto)
880943

@@ -1331,7 +1394,8 @@ def locator_params(self, axis='both', tight=None, **kwargs):
13311394
self.yaxis.get_major_locator().set_params(**kwargs)
13321395
if _z:
13331396
self.zaxis.get_major_locator().set_params(**kwargs)
1334-
self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
1397+
self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y,
1398+
scalez=_z)
13351399

13361400
def tick_params(self, axis='both', **kwargs):
13371401
"""

lib/mpl_toolkits/tests/test_mplot3d.py

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

706706

707+
@pytest.mark.parametrize('axis', ('x', 'y', 'z'))
708+
@pytest.mark.parametrize('auto', (True, False, None))
709+
def test_unautoscale(axis, auto):
710+
fig = plt.figure()
711+
ax = fig.gca(projection='3d')
712+
713+
x = np.arange(100)
714+
y = np.linspace(-0.1, 0.1, 100)
715+
ax.scatter(x, y)
716+
717+
get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
718+
set_lim = getattr(ax, f'set_{axis}lim')
719+
get_lim = getattr(ax, f'get_{axis}lim')
720+
721+
post_auto = get_autoscale_on() if auto is None else auto
722+
723+
set_lim((-0.5, 0.5), auto=auto)
724+
assert post_auto == get_autoscale_on()
725+
fig.canvas.draw()
726+
np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))
727+
728+
707729
@mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False)
708730
def test_axes3d_ortho():
709731
fig = plt.figure()

0 commit comments

Comments
 (0)