From 0def4c50f2c6c84d62c4b2c6629c3bb2c2ec6eab Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 7 Aug 2015 00:07:12 -0400 Subject: [PATCH 1/5] PRF: only check some artists on mousemove Instead of walking the full hitlist of the Axes, only check artists that ask to be part of the mouseover events. This is still missing logic to remove the artist from the mouseover_set when removing the artist from the Axes. --- lib/matplotlib/artist.py | 17 ++++++++++++++++- lib/matplotlib/axes/_base.py | 3 +++ lib/matplotlib/backend_bases.py | 4 +++- lib/matplotlib/image.py | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index e6d428fba997..7bbb3d29d920 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -103,7 +103,7 @@ def __init__(self): self._contains = None self._rasterized = None self._agg_filter = None - + self._mouseover = False self.eventson = False # fire events only if eventson self._oid = 0 # an observer id self._propobservers = {} # a dict from oids to funcs @@ -961,6 +961,21 @@ def format_cursor_data(self, data): data = [data] return ', '.join('{:0.3g}'.format(item) for item in data) + @property + def mouseover(self): + return self._mouseover + + @mouseover.setter + def mouseover(self, val): + val = bool(val) + self._mouseover = val + ax = self.axes + if ax: + if val: + ax.mouseover_set.add(self) + else: + ax.mouseover_set.discard(self) + class ArtistInspector(object): """ diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0b3f8b20a738..9d6d892d5373 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -786,6 +786,8 @@ def _set_artist_props(self, a): a.set_transform(self.transData) a.axes = self + if a.mouseover: + self.mouseover_set.add(a) def _gen_axes_patch(self): """ @@ -916,6 +918,7 @@ def cla(self): self.tables = [] self.artists = [] self.images = [] + self.mouseover_set = set() self._current_image = None # strictly for pyplot via _sci, _gci self.legend_ = None self.collections = [] # collection.Collection instances diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 675cbcaa777c..cc05a59eca16 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2815,9 +2815,11 @@ def mouse_move(self, event): except (ValueError, OverflowError): pass else: - artists = event.inaxes.hitlist(event) + artists = [a for a in event.inaxes.mouseover_set + if a.contains(event)] if artists: + a = max(enumerate(artists), key=lambda x: x[1].zorder)[1] if a is not event.inaxes.patch: data = a.get_cursor_data(event) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index cef36bdca578..7fcf0f6ded80 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -99,7 +99,7 @@ def __init__(self, ax, """ martist.Artist.__init__(self) cm.ScalarMappable.__init__(self, norm, cmap) - + self._mouseover = True if origin is None: origin = rcParams['image.origin'] self.origin = origin From 11332145945f710b08750afbbef502514fa9b323 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 13 Aug 2015 21:19:40 -0400 Subject: [PATCH 2/5] API: allow setting axes back to None This is useful when removing artists from an `Axes` --- lib/matplotlib/artist.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 7bbb3d29d920..3380d893b2ac 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -214,7 +214,9 @@ def axes(self): @axes.setter def axes(self, new_axes): - if self._axes is not None and new_axes != self._axes: + + if (new_axes is not None and + (self._axes is not None and new_axes != self._axes)): raise ValueError("Can not reset the axes. You are " "probably trying to re-use an artist " "in more than one Axes which is not " From 01c4b90f28330ab9fe76456bf706e3f9b02fdc81 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 13 Aug 2015 21:20:11 -0400 Subject: [PATCH 3/5] ENH: pull apart `Artist` state when removing - disconnect the stale_callback (this assumes that #4738 goes in) - remove the artist from the Axes.mousover_set - marks the parent axes/figure as stale - resets the artist's axes/figure to None --- lib/matplotlib/artist.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 3380d893b2ac..e78b59883417 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -147,6 +147,23 @@ def remove(self): # callback has one parameter, which is the child to be removed. if self._remove_method is not None: self._remove_method(self) + # clear stale callback + self.stale_callback = None + _ax_flag = False + if hasattr(self, 'axes') and self.axes: + # remove from the mouse hit list + self.axes.mouseover_set.discard(self) + # mark the axes as stale + self.axes.stale = True + # decouple the artist from the axes + self.axes = None + _ax_flag = True + + if self.figure: + self.figure = None + if not _ax_flag: + self.figure = True + else: raise NotImplementedError('cannot remove artist') # TODO: the fix for the collections relim problem is to move the From 090b6d66b853bd154889b7fac41d066fb4ff6cd5 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 13 Aug 2015 22:26:27 -0400 Subject: [PATCH 4/5] TST: added test of remove functionality --- lib/matplotlib/tests/test_artist.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index 9c92beb02e46..cd57fbb7d381 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -14,6 +14,9 @@ import matplotlib.collections as mcollections from matplotlib.testing.decorators import image_comparison, cleanup +from nose.tools import (assert_true, assert_false, assert_is, assert_in, + assert_not_in) + @cleanup def test_patch_transform_of_none(): @@ -144,6 +147,30 @@ def test_cull_markers(): assert len(svg.getvalue()) < 20000 +@cleanup +def test_remove(): + fig, ax = plt.subplots() + im = ax.imshow(np.arange(36).reshape(6, 6)) + + assert_true(fig.stale) + assert_true(ax.stale) + + fig.canvas.draw() + assert_false(fig.stale) + assert_false(ax.stale) + + assert_in(im, ax.mouseover_set) + assert_is(im.axes, ax) + + im.remove() + + assert_is(im.axes, None) + assert_is(im.figure, None) + assert_not_in(im, ax.mouseover_set) + assert_true(fig.stale) + assert_true(ax.stale) + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False) From ded655a0fda48f23cacd15ac7dc15b121680a3be Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 14 Aug 2015 10:26:51 -0400 Subject: [PATCH 5/5] MNT: not all assert methods available in 2.6 Apparently `assert_is`, `assert_in`, and `assert_not_in` are not in the 2.6 version of nose, but are in all others. --- lib/matplotlib/tests/test_artist.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index cd57fbb7d381..967a4c014e19 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -14,8 +14,7 @@ import matplotlib.collections as mcollections from matplotlib.testing.decorators import image_comparison, cleanup -from nose.tools import (assert_true, assert_false, assert_is, assert_in, - assert_not_in) +from nose.tools import (assert_true, assert_false) @cleanup @@ -159,14 +158,14 @@ def test_remove(): assert_false(fig.stale) assert_false(ax.stale) - assert_in(im, ax.mouseover_set) - assert_is(im.axes, ax) + assert_true(im in ax.mouseover_set) + assert_true(im.axes is ax) im.remove() - assert_is(im.axes, None) - assert_is(im.figure, None) - assert_not_in(im, ax.mouseover_set) + assert_true(im.axes is None) + assert_true(im.figure is None) + assert_true(im not in ax.mouseover_set) assert_true(fig.stale) assert_true(ax.stale)