From 2fd2dd004ff565f470ffbb1b5c11d72934fbc3c8 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Jun 2017 15:40:10 +0100 Subject: [PATCH 1/4] ENH: add two new modes for 'adjustable' option The new modes are 'xlim' and 'ylim' - these behave like the 'datalim' option except that only the x or y limits are adjusted, in a deterministic way. --- lib/matplotlib/axes/_base.py | 32 ++++++++--- lib/matplotlib/tests/test_axes.py | 90 +++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 7948c14de8e9..4bfd78f3eeac 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..2be34364bd3d 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5149,3 +5149,93 @@ 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.]) From 4a15e9ab5b533df33e4a7e768842b8f678cc0a6a Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Jun 2017 17:18:16 +0100 Subject: [PATCH 2/4] Added 'What's new' entry --- doc/users/whats_new/adjustable_xlim_ylim.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 doc/users/whats_new/adjustable_xlim_ylim.rst 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. From 5365621a5564d2bf7f7e44f2268e8f96feaafb66 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 1 Jun 2017 17:29:10 +0100 Subject: [PATCH 3/4] Fix PEP8 failures --- lib/matplotlib/tests/test_axes.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 2be34364bd3d..9303c494cb94 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5171,11 +5171,11 @@ def test_adjustable_limits(): 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. + # 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]) @@ -5183,7 +5183,8 @@ def test_adjustable_limits(): # 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. + # 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]) @@ -5214,7 +5215,8 @@ def test_adjustable_limits(): 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' + # 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') From c37354c51ace5b715b9f3f638646ea1a4531fd27 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Sat, 10 Jun 2017 12:52:34 +0100 Subject: [PATCH 4/4] Add trailing \ in docstring --- lib/matplotlib/axes/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 4bfd78f3eeac..d6459afd7162 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -438,7 +438,7 @@ def __init__(self, fig, rect, ================ ========================================= Keyword Description ================ ========================================= - *adjustable* [ 'box' | 'datalim' | 'xlim' | 'ylim' | + *adjustable* [ 'box' | 'datalim' | 'xlim' | 'ylim' | \ 'box-forced' ] *alpha* float: the alpha transparency (can be None) *anchor* [ 'C', 'SW', 'S', 'SE', 'E', 'NE', 'N',