From 162e25b2ff1765b28f6ec3a675a729ffc7748917 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 14 Dec 2015 20:56:32 -0500 Subject: [PATCH 1/5] MNT: change to immutable default value --- lib/matplotlib/cbook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 18ef6b793c2b..840570e9d3a0 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -1669,7 +1669,7 @@ class Grouper(object): False """ - def __init__(self, init=[]): + def __init__(self, init=()): mapping = self._mapping = {} for x in init: mapping[ref(x)] = [ref(x)] From 2dfb60b852375df5a8b1421f06881db4c8ead3a4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 14 Dec 2015 23:45:09 -0500 Subject: [PATCH 2/5] ENH: add remove method to cbook.Grouper - also add tests --- lib/matplotlib/cbook.py | 8 ++++++++ lib/matplotlib/tests/test_cbook.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 840570e9d3a0..da870a7f421e 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -1721,6 +1721,14 @@ def joined(self, a, b): except KeyError: return False + def remove(self, a): + self.clean() + + mapping = self._mapping + seta = mapping.pop(ref(a), None) + if seta is not None: + seta.remove(ref(a)) + def __iter__(self): """ Iterate over each of the disjoint sets as a list. diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 2b916b08566f..84ca8d3431d7 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -1,5 +1,6 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +import itertools from matplotlib.externals import six @@ -376,3 +377,23 @@ def test_step_fails(): np.arange(12)) assert_raises(ValueError, cbook._step_validation, np.arange(12), np.arange(3)) + + +def test_grouper(): + class dummy(): + pass + a, b, c, d, e = objs = [dummy() for j in range(5)] + g = cbook.Grouper() + g.join(*objs) + assert set(list(g)[0]) == set(objs) + assert set(g.get_siblings(a)) == set(objs) + + for other in objs[1:]: + assert g.joined(a, other) + + g.remove(a) + for other in objs[1:]: + assert not g.joined(a, other) + + for A, B in itertools.product(objs[1:], objs[1:]): + assert g.joined(A, B) From db34cb7f66ab8f80d889ea9e8e86a0e83c0c4daf Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 14 Dec 2015 23:52:10 -0500 Subject: [PATCH 3/5] TST: more tests of Grouper This test touches the internals, if Grouper gets refactored this test should be removed. --- lib/matplotlib/tests/test_cbook.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 84ca8d3431d7..1b11fe026120 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -1,6 +1,7 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) import itertools +from weakref import ref from matplotlib.externals import six @@ -397,3 +398,20 @@ class dummy(): for A, B in itertools.product(objs[1:], objs[1:]): assert g.joined(A, B) + + +def test_grouper_private(): + class dummy(): + pass + objs = [dummy() for j in range(5)] + g = cbook.Grouper() + g.join(*objs) + # reach in and touch the internals ! + mapping = g._mapping + + for o in objs: + assert ref(o) in mapping + + base_set = mapping[ref(objs[0])] + for o in objs[1:]: + assert mapping[ref(o)] is base_set From 0245db95ee5fb23271f1a68e979f7893070fd0ab Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 14 Dec 2015 23:53:36 -0500 Subject: [PATCH 4/5] FIX/ENH: do more clean up when removing axes This adds clean up of the shared axes `Grouper` objects as part of removing an axes. `_reset_loc_form` is required because the locator/formatters end up bound to the axis objects of the last axes object (all of the Axes in the group share the same formatter/locator objects but have different Axis objects so that all but one of the Axis objects can be made not visible to not over-draw). If these are not re-bound to a still visible axes then changing the limits will change the view limits, but not the tick locations. closes #5663 --- lib/matplotlib/figure.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 0de748cf7a78..547df4e3ca17 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -916,7 +916,7 @@ def add_axes(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) - a._remove_method = lambda ax: self.delaxes(ax) + a._remove_method = self.__remove_ax self.stale = True a.stale_callback = _stale_figure_callback return a @@ -1006,11 +1006,37 @@ def add_subplot(self, *args, **kwargs): self._axstack.add(key, a) self.sca(a) - a._remove_method = lambda ax: self.delaxes(ax) + a._remove_method = self.__remove_ax self.stale = True a.stale_callback = _stale_figure_callback return a + def __remove_ax(self, ax): + def _reset_loc_form(axis): + axis.set_major_formatter(axis.get_major_formatter()) + axis.set_major_locator(axis.get_major_locator()) + axis.set_minor_formatter(axis.get_minor_formatter()) + axis.set_minor_locator(axis.get_minor_locator()) + + def _break_share_link(ax, grouper): + siblings = grouper.get_siblings(ax) + if len(siblings) > 1: + grouper.remove(ax) + for last_ax in siblings: + if ax is last_ax: + continue + return last_ax + return None + + self.delaxes(ax) + last_ax = _break_share_link(ax, ax._shared_y_axes) + if last_ax is not None: + _reset_loc_form(last_ax.yaxis) + + last_ax = _break_share_link(ax, ax._shared_x_axes) + if last_ax is not None: + _reset_loc_form(last_ax.xaxis) + def clf(self, keep_observers=False): """ Clear the figure. From f0a32b33dcb4cd4c901846d7e1f539086a87cf37 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 15 Dec 2015 00:18:21 -0500 Subject: [PATCH 5/5] TST: add test for removing axes with shared axis --- lib/matplotlib/tests/test_axes.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index a3a161d39f80..de2f51bf3a14 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4086,11 +4086,57 @@ def test_shared_scale(): assert_equal(ax.get_yscale(), 'linear') assert_equal(ax.get_xscale(), 'linear') + @cleanup def test_violin_point_mass(): """Violin plot should handle point mass pdf gracefully.""" plt.violinplot(np.array([0, 0])) + +@cleanup +def test_remove_shared_axes(): + + def _helper_x(ax): + ax2 = ax.twinx() + ax2.remove() + ax.set_xlim(0, 15) + r = ax.xaxis.get_major_locator()() + assert r[-1] > 14 + + def _helper_y(ax): + ax2 = ax.twiny() + ax2.remove() + ax.set_ylim(0, 15) + r = ax.yaxis.get_major_locator()() + assert r[-1] > 14 + + # test all of the ways to get fig/ax sets + fig = plt.figure() + ax = fig.gca() + yield _helper_x, ax + yield _helper_y, ax + + fig, ax = plt.subplots() + yield _helper_x, ax + yield _helper_y, ax + + fig, ax_lst = plt.subplots(2, 2, sharex='all', sharey='all') + ax = ax_lst[0][0] + yield _helper_x, ax + yield _helper_y, ax + + fig = plt.figure() + ax = fig.add_axes([.1, .1, .8, .8]) + yield _helper_x, ax + yield _helper_y, ax + + fig, ax_lst = plt.subplots(2, 2, sharex='all', sharey='all') + ax = ax_lst[0][0] + orig_xlim = ax_lst[0][1].get_xlim() + ax.remove() + ax.set_xlim(0, 5) + assert assert_array_equal(ax_lst[0][1].get_xlim(), orig_xlim) + if __name__ == '__main__': import nose import sys