Skip to content

ENH: add two new modes for 'adjustable' option #8700

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

Closed
wants to merge 4 commits into from
Closed
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
13 changes: 13 additions & 0 deletions doc/users/whats_new/adjustable_xlim_ylim.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
New ``'xlim'`` and ``'ylim'`` options for ``adjustable`` argument
-----------------------------------------------------------------

The ``adjustable`` argument to the :meth:`~matplotlib.axes.Axes.set_aspect`
method (and the same argument which can be specified when initializing
:meth:`~matplotlib.axes.Axes`) can take two new values: ``'xlim'`` and
``'ylim'``. Previously, users could pass the ``'datalim'`` value to indicate
that Matplotlib should adjust the limits as needed so as to be able to avoid
modifying the position and aspect ratio of the axes, but it was impossible to
know deterministically whether Matplotlib would modify the x or y limits. The
new ``'xlim'`` and ``'ylim'`` options behave like ``'datalim'`` except that
``'xlim'`` causes only the x limits to be adjusted, and ``'ylim'`` causes only
the y limits to be adjusted.
32 changes: 26 additions & 6 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,8 @@ def __init__(self, fig, rect,
================ =========================================
Keyword Description
================ =========================================
*adjustable* [ 'box' | 'datalim' | 'box-forced']
*adjustable* [ 'box' | 'datalim' | 'xlim' | 'ylim' | \
'box-forced' ]
*alpha* float: the alpha transparency (can be None)
*anchor* [ 'C', 'SW', 'S', 'SE', 'E', 'NE', 'N',
'NW', 'W' ]
Expand Down Expand Up @@ -1271,6 +1272,8 @@ def set_aspect(self, aspect, adjustable=None, anchor=None):
============ =====================================
'box' change physical size of axes
'datalim' change xlim or ylim
'xlim' change xlim
'ylim' change ylim
'box-forced' same as 'box', but axes can be shared
============ =====================================

Expand Down Expand Up @@ -1307,9 +1310,9 @@ def get_adjustable(self):

def set_adjustable(self, adjustable):
"""
ACCEPTS: [ 'box' | 'datalim' | 'box-forced']
ACCEPTS: [ 'box' | 'datalim' | 'xlim' | 'ylim' | 'box-forced']
"""
if adjustable in ('box', 'datalim', 'box-forced'):
if adjustable in ('box', 'datalim', 'xlim', 'ylim', 'box-forced'):
if self in self._shared_x_axes or self in self._shared_y_axes:
if adjustable == 'box':
raise ValueError(
Expand Down Expand Up @@ -1482,11 +1485,27 @@ def apply_aspect(self, position=None):
self not in self._shared_x_axes)
changey = (self in self._shared_x_axes and
self not in self._shared_y_axes)

if changex and changey:
warnings.warn("adjustable='datalim' cannot work with shared "
"x and y axes")
warnings.warn("adjustable='{0}' cannot work with shared "
"x and y axes".format(self._adjustable))
return

if self._adjustable == 'xlim' and changey:
warnings.warn("adjustable='xlim' cannot work with shared "
"x axes".format(self._adjustable))
return

if self._adjustable == 'ylim' and changex:
warnings.warn("adjustable='ylim' cannot work with shared "
"y axes".format(self._adjustable))
return
if changex:

if self._adjustable == 'xlim':
adjust_y = False
elif self._adjustable == 'ylim':
adjust_y = True
elif changex:
adjust_y = False
else:
if xmarg > xm and ymarg > ym:
Expand All @@ -1495,6 +1514,7 @@ def apply_aspect(self, position=None):
else:
adjy = y_expander > 0
adjust_y = changey or adjy # (Ymarg > xmarg)

if adjust_y:
yc = 0.5 * (ymin + ymax)
y0 = yc - Ysize / 2.0
Expand Down
92 changes: 92 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5149,3 +5149,95 @@ def test_twinx_knows_limits():
ax2.plot([0, 0.5], [1, 2])

assert((xtwin.viewLim.intervalx == ax2.viewLim.intervalx).all())


def test_adjustable_limits():

# Test the 'datalim', 'xlim', and 'ylim' options

image = np.ones((5, 5))

# First, we test adjustable='datalim'. Here, whether xlim or ylim are
# changed is up to Matplotlib.

fig = plt.figure(figsize=(6, 4))
ax = fig.add_axes([0, 0, 1, 1], aspect='equal')
ax.imshow(image, origin='lower')

# Since the axes has a landscape aspect ratio, the image should fit
# vertically and have white padding horizontally:
ax.set_adjustable('datalim')
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [-1.75, 5.75])
assert_allclose(ax.get_ylim(), [-0.5, 4.5])

# Because of the way Matplotlib computes the aspect internally, it turns
# out that in this scenario ylim is the adjustable, so even if we change
# ylim, xlim will stay the same and ylim will get adjusted. This could be
# improved in future by checking in set_xlim and set_ylim whether
# adjustable='datalim' and try and make sure this value is respected.
ax.set_ylim(4, 5)
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [-1.75, 5.75])
assert_allclose(ax.get_ylim(), [2, 7])

# Similarly, if xlim is changed, the values are not necessarily respected
# and in fact ylim is the one that stays constant. This behavior is the
# reason for adding explicit adjustable='xlim' and adjustable='ylim'
# options.
ax.set_xlim(1, 4)
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [-1.25, 6.25])
assert_allclose(ax.get_ylim(), [2, 7])

# We now test adjustable='xlim', which should behave in a much more
# predictable way.

fig = plt.figure(figsize=(6, 4))
ax = fig.add_axes([0, 0, 1, 1], aspect='equal')
ax.imshow(image, origin='lower')

ax.set_adjustable('xlim')
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [-1.75, 5.75])
assert_allclose(ax.get_ylim(), [-0.5, 4.5])

# Changing ylim results in ylim changing predictably and xlim getting
# adjusted
ax.set_ylim(4, 6)
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [0.5, 3.5])
assert_allclose(ax.get_ylim(), [4, 6])

# Changing xlim results in xlim adjusting itself and ylim staying the same
ax.set_xlim(2, 4)
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [1.5, 4.5])
assert_allclose(ax.get_ylim(), [4, 6])

# Finally we test adjustable='ylim', which should behave similarly to
# 'xlim'

fig = plt.figure(figsize=(6, 4))
ax = fig.add_axes([0, 0, 1, 1], aspect='equal')
ax.imshow(image, origin='lower')

# In the case where ylim is the adjustable, the image will fill the axes
# horizontally.
ax.set_adjustable('ylim')
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [-0.5, 4.5])
assert_allclose(ax.get_ylim(), [1 / 3., 11 / 3.])

# Changing xlim results in xlim changing predictably and ylim getting
# adjusted
ax.set_xlim(4, 6)
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [4, 6])
assert_allclose(ax.get_ylim(), [4 / 3., 8 / 3.])

# Changing ylim results in ylim adjusting itself and xlim staying the same
ax.set_ylim(2, 4)
ax.apply_aspect()
assert_allclose(ax.get_xlim(), [4, 6])
assert_allclose(ax.get_ylim(), [7 / 3., 11 / 3.])