Skip to content

Commit 86f2d7b

Browse files
authored
Merge pull request #18127 from QuLogic/mplot3d-autoscale
Implement lazy autoscaling in mplot3d.
2 parents 6305e8d + 14d391e commit 86f2d7b

File tree

5 files changed

+114
-26
lines changed

5 files changed

+114
-26
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Autoscaling in Axes3D
2+
~~~~~~~~~~~~~~~~~~~~~
3+
4+
In Matplotlib 3.2.0, autoscaling was made lazier for 2D Axes, i.e., limits
5+
would only be recomputed when actually rendering the canvas, or when the user
6+
queries the Axes limits. This performance improvement is now extended to
7+
`.Axes3D`. This also fixes some issues with autoscaling being triggered
8+
unexpectedly in Axes3D.
9+
10+
Please see :ref:`the API change for 2D Axes <api-changes-3-2-0-autoscaling>`
11+
for further details.

doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ writer, without performing the availability check on subsequent writers, it is
5454
now possible to iterate over the registry, which will yield the names of the
5555
available classes.
5656

57+
.. _api-changes-3-2-0-autoscaling:
58+
5759
Autoscaling
5860
~~~~~~~~~~~
5961

lib/matplotlib/tests/test_axes.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6717,34 +6717,24 @@ def test_ytickcolor_is_not_markercolor():
67176717
assert tick.tick1line.get_markeredgecolor() != 'white'
67186718

67196719

6720+
@pytest.mark.parametrize('axis', ('x', 'y'))
67206721
@pytest.mark.parametrize('auto', (True, False, None))
6721-
def test_unautoscaley(auto):
6722-
fig, ax = plt.subplots()
6723-
x = np.arange(100)
6724-
y = np.linspace(-.1, .1, 100)
6725-
ax.scatter(x, y)
6726-
6727-
post_auto = ax.get_autoscaley_on() if auto is None else auto
6728-
6729-
ax.set_ylim((-.5, .5), auto=auto)
6730-
assert post_auto == ax.get_autoscaley_on()
6731-
fig.canvas.draw()
6732-
assert_array_equal(ax.get_ylim(), (-.5, .5))
6733-
6734-
6735-
@pytest.mark.parametrize('auto', (True, False, None))
6736-
def test_unautoscalex(auto):
6722+
def test_unautoscale(axis, auto):
67376723
fig, ax = plt.subplots()
67386724
x = np.arange(100)
67396725
y = np.linspace(-.1, .1, 100)
67406726
ax.scatter(y, x)
67416727

6742-
post_auto = ax.get_autoscalex_on() if auto is None else auto
6728+
get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
6729+
set_lim = getattr(ax, f'set_{axis}lim')
6730+
get_lim = getattr(ax, f'get_{axis}lim')
6731+
6732+
post_auto = get_autoscale_on() if auto is None else auto
67436733

6744-
ax.set_xlim((-.5, .5), auto=auto)
6745-
assert post_auto == ax.get_autoscalex_on()
6734+
set_lim((-0.5, 0.5), auto=auto)
6735+
assert post_auto == get_autoscale_on()
67466736
fig.canvas.draw()
6747-
assert_array_equal(ax.get_xlim(), (-.5, .5))
6737+
assert_array_equal(get_lim(), (-0.5, 0.5))
67486738

67496739

67506740
@check_figures_equal(extensions=["png"])

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy