diff --git a/doc/users/whats_new/adjustable_xlim_ylim.rst b/doc/users/whats_new/adjustable_xlim_ylim.rst new file mode 100644 index 000000000000..3b85aad4ae43 --- /dev/null +++ b/doc/users/whats_new/adjustable_xlim_ylim.rst @@ -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. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 7948c14de8e9..d6459afd7162 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -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' ] @@ -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 ============ ===================================== @@ -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( @@ -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: @@ -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 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 86b6f2a3ddc2..9303c494cb94 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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.])