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

Skip to content

Implement lazy autoscaling in mplot3d. #18127

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/api/next_api_changes/behavior/18127-ES.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Autoscaling in Axes3D
~~~~~~~~~~~~~~~~~~~~~

In Matplotlib 3.2.0, autoscaling was made lazier for 2D Axes, i.e., limits
would only be recomputed when actually rendering the canvas, or when the user
queries the Axes limits. This performance improvement is now extended to
`.Axes3D`. This also fixes some issues with autoscaling being triggered
unexpectedly in Axes3D.

Please see :ref:`the API change for 2D Axes <api-changes-3-2-0-autoscaling>`
for further details.
2 changes: 2 additions & 0 deletions doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ writer, without performing the availability check on subsequent writers, it is
now possible to iterate over the registry, which will yield the names of the
available classes.

.. _api-changes-3-2-0-autoscaling:

Autoscaling
~~~~~~~~~~~

Expand Down
30 changes: 10 additions & 20 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6717,34 +6717,24 @@ def test_ytickcolor_is_not_markercolor():
assert tick.tick1line.get_markeredgecolor() != 'white'


@pytest.mark.parametrize('axis', ('x', 'y'))
@pytest.mark.parametrize('auto', (True, False, None))
def test_unautoscaley(auto):
fig, ax = plt.subplots()
x = np.arange(100)
y = np.linspace(-.1, .1, 100)
ax.scatter(x, y)

post_auto = ax.get_autoscaley_on() if auto is None else auto

ax.set_ylim((-.5, .5), auto=auto)
assert post_auto == ax.get_autoscaley_on()
fig.canvas.draw()
assert_array_equal(ax.get_ylim(), (-.5, .5))


@pytest.mark.parametrize('auto', (True, False, None))
def test_unautoscalex(auto):
def test_unautoscale(axis, auto):
fig, ax = plt.subplots()
x = np.arange(100)
y = np.linspace(-.1, .1, 100)
ax.scatter(y, x)

post_auto = ax.get_autoscalex_on() if auto is None else auto
get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
set_lim = getattr(ax, f'set_{axis}lim')
get_lim = getattr(ax, f'get_{axis}lim')

post_auto = get_autoscale_on() if auto is None else auto

ax.set_xlim((-.5, .5), auto=auto)
assert post_auto == ax.get_autoscalex_on()
set_lim((-0.5, 0.5), auto=auto)
assert post_auto == get_autoscale_on()
fig.canvas.draw()
assert_array_equal(ax.get_xlim(), (-.5, .5))
assert_array_equal(get_lim(), (-0.5, 0.5))


@check_figures_equal(extensions=["png"])
Expand Down
75 changes: 69 additions & 6 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def __init__(
self.zz_viewLim = Bbox.unit()
self.xy_dataLim = Bbox.unit()
self.zz_dataLim = Bbox.unit()
self._stale_viewlim_z = False

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

def _unstale_viewLim(self):
# We should arrange to store this information once per share-group
# instead of on every axis.
scalex = any(ax._stale_viewlim_x
for ax in self._shared_x_axes.get_siblings(self))
scaley = any(ax._stale_viewlim_y
for ax in self._shared_y_axes.get_siblings(self))
scalez = any(ax._stale_viewlim_z
for ax in self._shared_z_axes.get_siblings(self))
if scalex or scaley or scalez:
for ax in self._shared_x_axes.get_siblings(self):
ax._stale_viewlim_x = False
for ax in self._shared_y_axes.get_siblings(self):
ax._stale_viewlim_y = False
for ax in self._shared_z_axes.get_siblings(self):
ax._stale_viewlim_z = False
self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)

def unit_cube(self, vals=None):
minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
return [(minx, miny, minz),
Expand Down Expand Up @@ -378,6 +397,8 @@ def apply_aspect(self, position=None):

@artist.allow_rasterization
def draw(self, renderer):
self._unstale_viewLim()

# draw the background patch
self.patch.draw(renderer)
self._frameon = False
Expand Down Expand Up @@ -477,9 +498,9 @@ def _unit_change_handler(self, axis_name, event=None):
self._unit_change_handler, axis_name, event=object())
_api.check_in_list(self._get_axis_map(), axis_name=axis_name)
self.relim()
self.autoscale_view(scalex=(axis_name == "x"),
scaley=(axis_name == "y"),
scalez=(axis_name == "z"))
self._request_autoscale_view(scalex=(axis_name == "x"),
scaley=(axis_name == "y"),
scalez=(axis_name == "z"))

def update_datalim(self, xys, **kwargs):
pass
Expand Down Expand Up @@ -528,6 +549,24 @@ def set_autoscalez_on(self, b):
"""
self._autoscaleZon = b

def set_xmargin(self, m):
# docstring inherited
scalez = self._stale_viewlim_z
super().set_xmargin(m)
# Superclass is 2D and will call _request_autoscale_view with defaults
# for unknown Axis, which would be scalez=True, but it shouldn't be for
# this call, so restore it.
self._stale_viewlim_z = scalez

def set_ymargin(self, m):
# docstring inherited
scalez = self._stale_viewlim_z
super().set_ymargin(m)
# Superclass is 2D and will call _request_autoscale_view with defaults
# for unknown Axis, which would be scalez=True, but it shouldn't be for
# this call, so restore it.
self._stale_viewlim_z = scalez

def set_zmargin(self, m):
"""
Set padding of Z data limits prior to autoscaling.
Expand All @@ -542,6 +581,7 @@ def set_zmargin(self, m):
if m < 0 or m > 1:
raise ValueError("margin must be in range 0 to 1")
self._zmargin = m
self._request_autoscale_view(scalex=False, scaley=False, scalez=True)
self.stale = True

def margins(self, *margins, x=None, y=None, z=None, tight=True):
Expand Down Expand Up @@ -639,8 +679,8 @@ def autoscale(self, enable=True, axis='both', tight=None):
self._autoscaleZon = scalez = bool(enable)
else:
scalez = False
self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
scalez=scalez)
self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
scalez=scalez)

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

# API could be better, right now this is just to match the old calls to
# autoscale_view() after each plotting method.
def _request_autoscale_view(self, tight=None, scalex=True, scaley=True,
scalez=True):
if tight is not None:
self._tight = tight
if scalex:
self._stale_viewlim_x = True # Else keep old state.
if scaley:
self._stale_viewlim_y = True
if scalez:
self._stale_viewlim_z = True

def autoscale_view(self, tight=None, scalex=True, scaley=True,
scalez=True):
"""
Expand Down Expand Up @@ -766,6 +819,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
left, right = sorted([left, right], reverse=bool(reverse))
self.xy_viewLim.intervalx = (left, right)

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

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

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

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

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

Expand Down Expand Up @@ -1346,7 +1408,8 @@ def locator_params(self, axis='both', tight=None, **kwargs):
self.yaxis.get_major_locator().set_params(**kwargs)
if _z:
self.zaxis.get_major_locator().set_params(**kwargs)
self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y,
scalez=_z)

def tick_params(self, axis='both', **kwargs):
"""
Expand Down
22 changes: 22 additions & 0 deletions lib/mpl_toolkits/tests/test_mplot3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,28 @@ def test_autoscale():
assert ax.get_w_lims() == (0, 1, -.1, 1.1, -.4, 2.4)


@pytest.mark.parametrize('axis', ('x', 'y', 'z'))
@pytest.mark.parametrize('auto', (True, False, None))
def test_unautoscale(axis, auto):
fig = plt.figure()
ax = fig.gca(projection='3d')

x = np.arange(100)
y = np.linspace(-0.1, 0.1, 100)
ax.scatter(x, y)

get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
set_lim = getattr(ax, f'set_{axis}lim')
get_lim = getattr(ax, f'get_{axis}lim')

post_auto = get_autoscale_on() if auto is None else auto

set_lim((-0.5, 0.5), auto=auto)
assert post_auto == get_autoscale_on()
fig.canvas.draw()
np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))


@mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False)
def test_axes3d_ortho():
fig = plt.figure()
Expand Down