From 8af797d3253067b9b929aa9ce88a4ff09662ff19 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 27 Feb 2012 14:58:23 -0500 Subject: [PATCH 01/12] Change cursors and selectors to subclass Widget. --- lib/matplotlib/widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9b4c97c1a814..f49573b6e2dd 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -734,7 +734,7 @@ def funchspace(self, val): if self.drawon: self.targetfig.canvas.draw() -class Cursor: +class Cursor(Widget): """ A horizontal and vertical line span the axes that and move with the pointer. You can turn off the hline or vline spectively with @@ -820,7 +820,7 @@ def _update(self): return False -class MultiCursor: +class MultiCursor(Widget): """ Provide a vertical line cursor shared between multiple axes @@ -895,7 +895,7 @@ def _update(self): self.canvas.draw_idle() -class SpanSelector: +class SpanSelector(Widget): """ Select a min/max range of the x or y axes for a matplotlib Axes @@ -1085,7 +1085,7 @@ def __init__(self, ax, onselect, **kwargs): SpanSelector.__init__(self, ax, onselect, 'horizontal', **kwargs) -class RectangleSelector: +class RectangleSelector(Widget): """ Select a min/max range of the x axes for a matplotlib Axes From 5b35427c4a0e977af8f6dd31c7661ba42deb1dbe Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 27 Feb 2012 15:15:35 -0500 Subject: [PATCH 02/12] Remove call to `new_axes` in SpanSelector. * This change is in preparation for generalizing the base Widget. * It's not clear to me why `new_axes` is necessary, but I left it in (for now). --- lib/matplotlib/widgets.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f49573b6e2dd..1c8c874a793f 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -939,8 +939,8 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, assert direction in ['horizontal', 'vertical'], 'Must choose horizontal or vertical for direction' self.direction = direction - self.ax = None - self.canvas = None + self.ax = ax + self.canvas = ax.figure.canvas self.visible = True self.cids=[] @@ -958,8 +958,24 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.buttonDown = False self.prev = (0, 0) - self.new_axes(ax) + self.cids.append(self.canvas.mpl_connect('motion_notify_event', self.onmove)) + self.cids.append(self.canvas.mpl_connect('button_press_event', self.press)) + self.cids.append(self.canvas.mpl_connect('button_release_event', self.release)) + self.cids.append(self.canvas.mpl_connect('draw_event', self.update_background)) + + if self.direction == 'horizontal': + trans = blended_transform_factory(self.ax.transData, self.ax.transAxes) + w,h = 0,1 + else: + trans = blended_transform_factory(self.ax.transAxes, self.ax.transData) + w,h = 1,0 + self.rect = Rectangle( (0,0), w, h, + transform=trans, + visible=False, + **self.rectprops + ) + if not self.useblit: self.ax.add_patch(self.rect) def new_axes(self,ax): self.ax = ax From e37c135cc515f267318b9528c6670b2e3ab6197c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 27 Feb 2012 15:40:56 -0500 Subject: [PATCH 03/12] Add AxesWidget class and let it initialize `ax` and `canvas` attributes. * Renamed `Lasso.axes` to `Lasso.ax` for consistency. * Remove `Lasso.figure`, which was unused. --- lib/matplotlib/widgets.py | 74 ++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 1c8c874a793f..442bef804a06 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -63,10 +63,23 @@ class Widget(object): drawon = True eventson = True +class AxesWidget(Widget): + """ + Widget that is connected to a single :class:`Axes`. + + Attributes + ---------- + *ax* + The parent :class:`matplotlib.axes.Axes` for the widget + *canvas* + The parent FigureCanvas for the widget + """ + def __init__(self, ax): + self.ax = ax + self.canvas = ax.figure.canvas - -class Button(Widget): +class Button(AxesWidget): """ A GUI neutral button @@ -108,6 +121,8 @@ def __init__(self, ax, label, image=None, *hovercolor* The color of the button when the mouse is over it """ + AxesWidget.__init__(self, ax) + if image is not None: ax.imshow(image) self.label = ax.text(0.5, 0.5, label, @@ -117,8 +132,6 @@ def __init__(self, ax, label, image=None, self.cnt = 0 self.observers = {} - self.ax = ax - ax.figure.canvas.mpl_connect('button_press_event', self._click) ax.figure.canvas.mpl_connect('button_release_event', self._release) @@ -179,7 +192,7 @@ def disconnect(self, cid): -class Slider(Widget): +class Slider(AxesWidget): """ A slider representing a floating point range @@ -240,7 +253,7 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', knob. See the :class:`matplotlib.patches.Rectangle` documentation valid property names (e.g., *facecolor*, *edgecolor*, *alpha*, ...) """ - self.ax = ax + AxesWidget.__init__(self, ax) self.valmin = valmin self.valmax = valmax @@ -351,7 +364,7 @@ def reset(self): -class CheckButtons(Widget): +class CheckButtons(AxesWidget): """ A GUI neutral radio button @@ -385,6 +398,7 @@ def __init__(self, ax, labels, actives): A len(buttons) list of booleans indicating whether the button is active """ + AxesWidget.__init__(self, ax) ax.set_xticks([]) ax.set_yticks([]) @@ -433,8 +447,6 @@ def __init__(self, ax, labels, actives): cnt += 1 ax.figure.canvas.mpl_connect('button_press_event', self._clicked) - self.ax = ax - self.cnt = 0 self.observers = {} @@ -479,7 +491,7 @@ def disconnect(self, cid): except KeyError: pass -class RadioButtons(Widget): +class RadioButtons(AxesWidget): """ A GUI neutral radio button @@ -512,8 +524,9 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): *activecolor* The color of the button when clicked """ - self.activecolor = activecolor + AxesWidget.__init__(self, ax) + self.activecolor = activecolor ax.set_xticks([]) ax.set_yticks([]) @@ -545,8 +558,6 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): cnt += 1 ax.figure.canvas.mpl_connect('button_press_event', self._clicked) - self.ax = ax - self.cnt = 0 self.observers = {} @@ -734,7 +745,7 @@ def funchspace(self, val): if self.drawon: self.targetfig.canvas.draw() -class Cursor(Widget): +class Cursor(AxesWidget): """ A horizontal and vertical line span the axes that and move with the pointer. You can turn off the hline or vline spectively with @@ -757,9 +768,7 @@ def __init__(self, ax, useblit=False, **lineprops): .. plot :: mpl_examples/widgets/cursor.py """ # TODO: Is the GTKAgg limitation still true? - - self.ax = ax - self.canvas = ax.figure.canvas + AxesWidget.__init__(self, ax) self.canvas.mpl_connect('motion_notify_event', self.onmove) self.canvas.mpl_connect('draw_event', self.clear) @@ -895,7 +904,7 @@ def _update(self): self.canvas.draw_idle() -class SpanSelector(Widget): +class SpanSelector(AxesWidget): """ Select a min/max range of the x or y axes for a matplotlib Axes @@ -933,14 +942,14 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, Set the visible attribute to ``False`` if you want to turn off the functionality of the span selector """ + AxesWidget.__init__(self, ax) + if rectprops is None: rectprops = dict(facecolor='red', alpha=0.5) assert direction in ['horizontal', 'vertical'], 'Must choose horizontal or vertical for direction' self.direction = direction - self.ax = ax - self.canvas = ax.figure.canvas self.visible = True self.cids=[] @@ -1101,7 +1110,7 @@ def __init__(self, ax, onselect, **kwargs): SpanSelector.__init__(self, ax, onselect, 'horizontal', **kwargs) -class RectangleSelector(Widget): +class RectangleSelector(AxesWidget): """ Select a min/max range of the x axes for a matplotlib Axes @@ -1181,9 +1190,9 @@ def __init__(self, ax, onselect, drawtype='box', 2 = center mouse button (scroll wheel) 3 = right mouse button """ - self.ax = ax + AxesWidget.__init__(self, ax) + self.visible = True - self.canvas = ax.figure.canvas self.canvas.mpl_connect('motion_notify_event', self.onmove) self.canvas.mpl_connect('button_press_event', self.press) self.canvas.mpl_connect('button_release_event', self.release) @@ -1378,19 +1387,18 @@ def get_active(self): """ Get status of active mode (boolean variable)""" return self.active -class Lasso(Widget): +class Lasso(AxesWidget): def __init__(self, ax, xy, callback=None, useblit=True): - self.axes = ax - self.figure = ax.figure - self.canvas = self.figure.canvas + AxesWidget.__init__(self, ax) + self.useblit = useblit if useblit: - self.background = self.canvas.copy_from_bbox(self.axes.bbox) + self.background = self.canvas.copy_from_bbox(self.ax.bbox) x, y = xy self.verts = [(x,y)] self.line = Line2D([x], [y], linestyle='-', color='black', lw=2) - self.axes.add_line(self.line) + self.ax.add_line(self.line) self.callback = callback self.cids = [] self.cids.append(self.canvas.mpl_connect('button_release_event', self.onrelease)) @@ -1401,14 +1409,14 @@ def onrelease(self, event): self.verts.append((event.xdata, event.ydata)) if len(self.verts)>2: self.callback(self.verts) - self.axes.lines.remove(self.line) + self.ax.lines.remove(self.line) self.verts = None for cid in self.cids: self.canvas.mpl_disconnect(cid) def onmove(self, event): if self.verts is None: return - if event.inaxes != self.axes: return + if event.inaxes != self.ax: return if event.button!=1: return self.verts.append((event.xdata, event.ydata)) @@ -1416,7 +1424,7 @@ def onmove(self, event): if self.useblit: self.canvas.restore_region(self.background) - self.axes.draw_artist(self.line) - self.canvas.blit(self.axes.bbox) + self.ax.draw_artist(self.line) + self.canvas.blit(self.ax.bbox) else: self.canvas.draw_idle() From 21bca27fc3df736561c47fe74c01845f39757ed3 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 27 Feb 2012 16:13:09 -0500 Subject: [PATCH 04/12] Add `connect_event` and `disconnect_events` to AxesWidget. --- lib/matplotlib/widgets.py | 67 +++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 442bef804a06..079b62f62cf3 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -55,7 +55,6 @@ def locked(self): return self._owner is not None - class Widget(object): """ Abstract base class for GUI neutral widgets @@ -63,6 +62,7 @@ class Widget(object): drawon = True eventson = True + class AxesWidget(Widget): """ Widget that is connected to a single :class:`Axes`. @@ -77,6 +77,15 @@ class AxesWidget(Widget): def __init__(self, ax): self.ax = ax self.canvas = ax.figure.canvas + self.cids = [] + + def connect_event(self, event, callback): + self.canvas.mpl_connect(event, callback) + self.cids.append(callback) + + def disconnect_events(self): + for c in self.cids: + self.canvas.mpl_disconnect(c) class Button(AxesWidget): @@ -133,9 +142,9 @@ def __init__(self, ax, label, image=None, self.cnt = 0 self.observers = {} - ax.figure.canvas.mpl_connect('button_press_event', self._click) - ax.figure.canvas.mpl_connect('button_release_event', self._release) - ax.figure.canvas.mpl_connect('motion_notify_event', self._motion) + self.connect_event('button_press_event', self._click) + self.connect_event('button_release_event', self._release) + self.connect_event('motion_notify_event', self._motion) ax.set_navigate(False) ax.set_axis_bgcolor(color) ax.set_xticks([]) @@ -270,10 +279,10 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', ax.set_xticks([]) ax.set_navigate(False) - ax.figure.canvas.mpl_connect('button_press_event', self._update) - ax.figure.canvas.mpl_connect('button_release_event', self._update) + self.connect_event('button_press_event', self._update) + self.connect_event('button_release_event', self._update) if dragging: - ax.figure.canvas.mpl_connect('motion_notify_event', self._update) + self.connect_event('motion_notify_event', self._update) self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes, verticalalignment='center', horizontalalignment='right') @@ -446,7 +455,7 @@ def __init__(self, ax, labels, actives): ax.add_line(l2) cnt += 1 - ax.figure.canvas.mpl_connect('button_press_event', self._clicked) + self.connect_event('button_press_event', self._clicked) self.cnt = 0 self.observers = {} @@ -557,7 +566,7 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): ax.add_patch(p) cnt += 1 - ax.figure.canvas.mpl_connect('button_press_event', self._clicked) + self.connect_event('button_press_event', self._clicked) self.cnt = 0 self.observers = {} @@ -770,8 +779,8 @@ def __init__(self, ax, useblit=False, **lineprops): # TODO: Is the GTKAgg limitation still true? AxesWidget.__init__(self, ax) - self.canvas.mpl_connect('motion_notify_event', self.onmove) - self.canvas.mpl_connect('draw_event', self.clear) + self.connect_event('motion_notify_event', self.onmove) + self.connect_event('draw_event', self.clear) self.visible = True self.horizOn = True @@ -951,7 +960,6 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.direction = direction self.visible = True - self.cids=[] self.rect = None self.background = None @@ -967,10 +975,10 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.buttonDown = False self.prev = (0, 0) - self.cids.append(self.canvas.mpl_connect('motion_notify_event', self.onmove)) - self.cids.append(self.canvas.mpl_connect('button_press_event', self.press)) - self.cids.append(self.canvas.mpl_connect('button_release_event', self.release)) - self.cids.append(self.canvas.mpl_connect('draw_event', self.update_background)) + self.connect_event('motion_notify_event', self.onmove) + self.connect_event('button_press_event', self.press) + self.connect_event('button_release_event', self.release) + self.connect_event('draw_event', self.update_background) if self.direction == 'horizontal': trans = blended_transform_factory(self.ax.transData, self.ax.transAxes) @@ -989,15 +997,14 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, def new_axes(self,ax): self.ax = ax if self.canvas is not ax.figure.canvas: - for cid in self.cids: - self.canvas.mpl_disconnect(cid) + self.disconnect_events() self.canvas = ax.figure.canvas + self.connect_event('motion_notify_event', self.onmove) + self.connect_event('button_press_event', self.press) + self.connect_event('button_release_event', self.release) + self.connect_event('draw_event', self.update_background) - self.cids.append(self.canvas.mpl_connect('motion_notify_event', self.onmove)) - self.cids.append(self.canvas.mpl_connect('button_press_event', self.press)) - self.cids.append(self.canvas.mpl_connect('button_release_event', self.release)) - self.cids.append(self.canvas.mpl_connect('draw_event', self.update_background)) if self.direction == 'horizontal': trans = blended_transform_factory(self.ax.transData, self.ax.transAxes) w,h = 0,1 @@ -1193,10 +1200,10 @@ def __init__(self, ax, onselect, drawtype='box', AxesWidget.__init__(self, ax) self.visible = True - self.canvas.mpl_connect('motion_notify_event', self.onmove) - self.canvas.mpl_connect('button_press_event', self.press) - self.canvas.mpl_connect('button_release_event', self.release) - self.canvas.mpl_connect('draw_event', self.update_background) + self.connect_event('motion_notify_event', self.onmove) + self.connect_event('button_press_event', self.press) + self.connect_event('button_release_event', self.release) + self.connect_event('draw_event', self.update_background) self.active = True # for activation / deactivation self.to_draw = None @@ -1400,9 +1407,8 @@ def __init__(self, ax, xy, callback=None, useblit=True): self.line = Line2D([x], [y], linestyle='-', color='black', lw=2) self.ax.add_line(self.line) self.callback = callback - self.cids = [] - self.cids.append(self.canvas.mpl_connect('button_release_event', self.onrelease)) - self.cids.append(self.canvas.mpl_connect('motion_notify_event', self.onmove)) + self.connect_event('button_release_event', self.onrelease) + self.connect_event('motion_notify_event', self.onmove) def onrelease(self, event): if self.verts is not None: @@ -1411,8 +1417,7 @@ def onrelease(self, event): self.callback(self.verts) self.ax.lines.remove(self.line) self.verts = None - for cid in self.cids: - self.canvas.mpl_disconnect(cid) + self.disconnect_events() def onmove(self, event): if self.verts is None: return From c8f152aafa28b167b02440c5832e759b09d0a558 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 27 Feb 2012 21:47:40 -0500 Subject: [PATCH 05/12] Add default ignore method and check it in callbacks. * Attribute name "active" can be confused with parameter to `RadioButtons`, but `RadioButtons` doesn't save that parameter as an attribute, so there's no name clash. BUT this is still really confusing to users. Consider renaming `AxesWidget.active`. * * * This could have been implemented as a decorator, but some callbacks wouldn't be compatible. For example, `SpanSelector.release` should not be ignored if widget was deactivated during mouse press. * * * `SpanSelector` and `RectangleSelector` already had `ignore` methods, but they do not call it in `update_background`. I don't change the current behavior, although it seems desirable. --- lib/matplotlib/widgets.py | 65 ++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 079b62f62cf3..4929ccf61450 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -10,6 +10,7 @@ """ from __future__ import print_function +import functools import numpy as np from mlab import dist @@ -78,6 +79,7 @@ def __init__(self, ax): self.ax = ax self.canvas = ax.figure.canvas self.cids = [] + self.active = True def connect_event(self, event, callback): self.canvas.mpl_connect(event, callback) @@ -87,6 +89,14 @@ def disconnect_events(self): for c in self.cids: self.canvas.mpl_disconnect(c) + def ignore(self, event): + """Return True if event should be ignored. + + This method (or a version of it) should be called at the beginning + of any event callback. + """ + return not self.active + class Button(AxesWidget): """ @@ -155,6 +165,8 @@ def __init__(self, ax, label, image=None, self._lastcolor = color def _click(self, event): + if self.ignore(event): + return if event.inaxes != self.ax: return if not self.eventson: @@ -163,6 +175,8 @@ def _click(self, event): event.canvas.grab_mouse(self.ax) def _release(self, event): + if self.ignore(event): + return if event.canvas.mouse_grabber != self.ax: return event.canvas.release_mouse(self.ax) @@ -174,6 +188,8 @@ def _release(self, event): func(event) def _motion(self, event): + if self.ignore(event): + return if event.inaxes==self.ax: c = self.hovercolor else: @@ -303,6 +319,9 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f', def _update(self, event): 'update the slider position' + if self.ignore(event): + return + if event.button != 1: return @@ -461,6 +480,8 @@ def __init__(self, ax, labels, actives): self.observers = {} def _clicked(self, event): + if self.ignore(event): + return if event.button !=1 : return if event.inaxes != self.ax: return @@ -572,6 +593,8 @@ def __init__(self, ax, labels, active=0, activecolor='blue'): self.observers = {} def _clicked(self, event): + if self.ignore(event): + return if event.button !=1 : return if event.inaxes != self.ax: return xy = self.ax.transAxes.inverted().transform_point((event.x, event.y)) @@ -795,9 +818,10 @@ def __init__(self, ax, useblit=False, **lineprops): self.background = None self.needclear = False - def clear(self, event): 'clear the cursor' + if self.ignore(event): + return if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) self.linev.set_visible(False) @@ -805,6 +829,8 @@ def clear(self, event): def onmove(self, event): 'on mouse motion draw the cursor if visible' + if self.ignore(event): + return if event.inaxes != self.ax: self.linev.set_visible(False) self.lineh.set_visible(False) @@ -1021,17 +1047,21 @@ def new_axes(self,ax): def update_background(self, event): 'force an update of the background' + # If you add a call to `ignore` here, you'll want to check edge case: + # `release` can call a draw event even when `ignore` is True. if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) - def ignore(self, event): 'return ``True`` if *event* should be ignored' - return event.inaxes!=self.ax or not self.visible or event.button !=1 + widget_off = not self.visible or not self.active + non_event = event.inaxes!=self.ax or event.button !=1 + return widget_off or non_event def press(self, event): 'on button press event' - if self.ignore(event): return + if self.ignore(event): + return self.buttonDown = True self.rect.set_visible(self.visible) @@ -1041,10 +1071,12 @@ def press(self, event): self.pressv = event.ydata return False - def release(self, event): 'on button release event' - if self.pressv is None or (self.ignore(event) and not self.buttonDown): return + if self.ignore(event) and not self.buttonDown: + return + if self.pressv is None: + return self.buttonDown = False self.rect.set_visible(False) @@ -1079,7 +1111,8 @@ def update(self): def onmove(self, event): 'on motion notify event' - if self.pressv is None or self.ignore(event): return + if self.pressv is None or self.ignore(event): + return x, y = event.xdata, event.ydata self.prev = x, y if self.direction == 'horizontal': @@ -1255,7 +1288,6 @@ def update_background(self, event): def ignore(self, event): 'return ``True`` if *event* should be ignored' - # If RectangleSelector is not active : if not self.active: return True @@ -1296,10 +1328,8 @@ def ignore(self, event): def press(self, event): 'on button press event' - # Is the correct button pressed within the correct axes? - if self.ignore(event): return - - + if self.ignore(event): + return # make the drawed box/line visible get the click-coordinates, # button, ... self.to_draw.set_visible(self.visible) @@ -1309,7 +1339,8 @@ def press(self, event): def release(self, event): 'on button release event' - if self.eventpress is None or self.ignore(event): return + if self.eventpress is None or self.ignore(event): + return # make the box/line invisible again self.to_draw.set_visible(False) self.canvas.draw() @@ -1360,10 +1391,10 @@ def update(self): self.canvas.draw_idle() return False - def onmove(self, event): 'on motion notify event if box/line is wanted' - if self.eventpress is None or self.ignore(event): return + if self.eventpress is None or self.ignore(event): + return x,y = event.xdata, event.ydata # actual position (with # (button still pressed) if self.drawtype == 'box': @@ -1411,6 +1442,8 @@ def __init__(self, ax, xy, callback=None, useblit=True): self.connect_event('motion_notify_event', self.onmove) def onrelease(self, event): + if self.ignore(event): + return if self.verts is not None: self.verts.append((event.xdata, event.ydata)) if len(self.verts)>2: @@ -1420,6 +1453,8 @@ def onrelease(self, event): self.disconnect_events() def onmove(self, event): + if self.ignore(event): + return if self.verts is None: return if event.inaxes != self.ax: return if event.button!=1: return From 6baa65a592a47011cfcd593a28a2ec6f512dd7a7 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 28 Feb 2012 13:30:51 -0500 Subject: [PATCH 06/12] Fix: save correct callback ids. Also, remove unused import. --- lib/matplotlib/widgets.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 4929ccf61450..edc4dcb7fffe 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -10,7 +10,6 @@ """ from __future__ import print_function -import functools import numpy as np from mlab import dist @@ -82,8 +81,8 @@ def __init__(self, ax): self.active = True def connect_event(self, event, callback): - self.canvas.mpl_connect(event, callback) - self.cids.append(callback) + cid = self.canvas.mpl_connect(event, callback) + self.cids.append(cid) def disconnect_events(self): for c in self.cids: From 0f0286ea39eaa95282a46c7ae245f0a2fe52a922 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Tue, 28 Feb 2012 13:41:22 -0500 Subject: [PATCH 07/12] Remove duplicate code in `SpanSelector.__init__`. --- lib/matplotlib/widgets.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index edc4dcb7fffe..6c1f493e7219 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1000,24 +1000,9 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, self.buttonDown = False self.prev = (0, 0) - self.connect_event('motion_notify_event', self.onmove) - self.connect_event('button_press_event', self.press) - self.connect_event('button_release_event', self.release) - self.connect_event('draw_event', self.update_background) - - if self.direction == 'horizontal': - trans = blended_transform_factory(self.ax.transData, self.ax.transAxes) - w,h = 0,1 - else: - trans = blended_transform_factory(self.ax.transAxes, self.ax.transData) - w,h = 1,0 - self.rect = Rectangle( (0,0), w, h, - transform=trans, - visible=False, - **self.rectprops - ) - - if not self.useblit: self.ax.add_patch(self.rect) + # Reset canvas so that `new_axes` connects events. + self.canvas = None + self.new_axes(ax) def new_axes(self,ax): self.ax = ax From 8345d4c6afcd8afa9d646d554440de27fcc7d49c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Wed, 29 Feb 2012 22:22:35 -0500 Subject: [PATCH 08/12] Add LassoSelector widget with demo. Note: I put the demo in the "widgets" directory even though the `Lasso` demo is in "event_handling". --- examples/widgets/lasso_selector_demo.py | 81 +++++++++++++++++++++++++ lib/matplotlib/widgets.py | 66 ++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 examples/widgets/lasso_selector_demo.py diff --git a/examples/widgets/lasso_selector_demo.py b/examples/widgets/lasso_selector_demo.py new file mode 100644 index 000000000000..ac0057f20ae4 --- /dev/null +++ b/examples/widgets/lasso_selector_demo.py @@ -0,0 +1,81 @@ +import numpy as np + +from matplotlib.widgets import LassoSelector +from matplotlib.nxutils import points_inside_poly + + +class SelectFromCollection(object): + """Select indices from a matplotlib collection using :class:`LassoSelector`. + + Selected indices are saved in the `ind` attribute. This tool highlights + selected points by fading them out (i.e., reducing their alpha values). + If your collection has alpha < 1, this tool will permanently alter them. + + Note that this tool selects collection objects based on their *origins* + (i.e., `offsets`). + + Parameters + ---------- + ax : :class:`Axes` + Axes to interact with. + + collection : :class:`Collection` + Collection you want to select from. + + alpha_other : 0 <= float <= 1 + To highlight a selection, this tool sets all selected points to an + alpha value of 1 and non-selected points to `alpha_other`. + """ + def __init__(self, ax, collection, alpha_other=0.3): + self.canvas = ax.figure.canvas + self.collection = collection + self.alpha_other = alpha_other + + self.xys = collection.get_offsets() + self.Npts = len(self.xys) + + # Ensure that we have separate colors for each object + self.fc = collection.get_facecolors() + if len(self.fc) == 0: + raise ValueError('Collection must have a facecolor') + elif len(self.fc) == 1: + self.fc = np.tile(self.fc, self.Npts).reshape(self.Npts, -1) + + self.lasso = LassoSelector(ax, onselect=self.onselect) + self.ind = [] + + def onselect(self, verts): + self.ind = np.nonzero(points_inside_poly(self.xys, verts))[0] + self.fc[:, -1] = self.alpha_other + self.fc[self.ind, -1] = 1 + self.collection.set_facecolors(self.fc) + self.canvas.draw_idle() + + def disconnect(self): + self.lasso.disconnect_events() + self.fc[:, -1] = 1 + self.collection.set_facecolors(self.fc) + self.canvas.draw_idle() + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + + plt.ion() + data = np.random.rand(100, 2) + + subplot_kw = dict(xlim=(0,1), ylim=(0,1), autoscale_on=False) + fig, ax = plt.subplots(subplot_kw=subplot_kw) + + pts = ax.scatter(data[:, 0], data[:, 1], s=80) + selector = SelectFromCollection(ax, pts) + + plt.draw() + raw_input('Press any key to accept selected points') + print "Selected points:" + print selector.xys[selector.ind] + selector.disconnect() + + # Block end of script so you can check that lasso is disconnected. + raw_input('Press any key to quit') + diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6c1f493e7219..cba4d2b96d20 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1409,6 +1409,71 @@ def get_active(self): """ Get status of active mode (boolean variable)""" return self.active + +class LassoSelector(AxesWidget): + def __init__(self, ax, onselect=None, useblit=True, lineprops=None): + AxesWidget.__init__(self, ax) + + self.useblit = useblit + self.onselect = onselect + self.verts = None + + if lineprops is None: + lineprops = dict() + self.line = Line2D([], [], **lineprops) + self.line.set_visible(False) + self.ax.add_line(self.line) + + self.connect_event('button_press_event', self.onpress) + self.connect_event('button_release_event', self.onrelease) + self.connect_event('motion_notify_event', self.onmove) + self.connect_event('draw_event', self.update_background) + + def ignore(self, event): + wrong_button = hasattr(event, 'button') and event.button != 1 + return not self.active or wrong_button + + def onpress(self, event): + if self.ignore(event) or event.inaxes != self.ax: + return + self.verts = [(event.xdata, event.ydata)] + self.line.set_visible(True) + + def onrelease(self, event): + if self.ignore(event): + return + if self.verts is not None: + if event.inaxes == self.ax: + self.verts.append((event.xdata, event.ydata)) + self.onselect(self.verts) + self.line.set_data([[], []]) + self.line.set_visible(False) + self.verts = None + + def onmove(self, event): + if self.ignore(event) or event.inaxes != self.ax: + return + if self.verts is None: return + if event.inaxes != self.ax: return + if event.button!=1: return + self.verts.append((event.xdata, event.ydata)) + + self.line.set_data(zip(*self.verts)) + + if self.useblit: + self.canvas.restore_region(self.background) + self.ax.draw_artist(self.line) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + + def update_background(self, event): + if self.ignore(event): + return + if self.useblit: + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + + class Lasso(AxesWidget): def __init__(self, ax, xy, callback=None, useblit=True): AxesWidget.__init__(self, ax) @@ -1452,3 +1517,4 @@ def onmove(self, event): self.canvas.blit(self.ax.bbox) else: self.canvas.draw_idle() + From adb1754db3b2d216eaaf9317945aa4e0d479279b Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 1 Mar 2012 23:54:45 -0500 Subject: [PATCH 09/12] DOC: Fix sphinx class references. --- examples/widgets/lasso_selector_demo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/widgets/lasso_selector_demo.py b/examples/widgets/lasso_selector_demo.py index ac0057f20ae4..d1d9f3baa310 100644 --- a/examples/widgets/lasso_selector_demo.py +++ b/examples/widgets/lasso_selector_demo.py @@ -5,7 +5,7 @@ class SelectFromCollection(object): - """Select indices from a matplotlib collection using :class:`LassoSelector`. + """Select indices from a matplotlib collection using `LassoSelector`. Selected indices are saved in the `ind` attribute. This tool highlights selected points by fading them out (i.e., reducing their alpha values). @@ -16,10 +16,10 @@ class SelectFromCollection(object): Parameters ---------- - ax : :class:`Axes` + ax : :class:`~matplotlib.axes.Axes` Axes to interact with. - collection : :class:`Collection` + collection : :class:`matplotlib.collections.Collection` subclass Collection you want to select from. alpha_other : 0 <= float <= 1 From f169460af338ec51ac08804bec16a626b8481a14 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 2 Mar 2012 00:38:51 -0500 Subject: [PATCH 10/12] Fix and improve docstring. Class links were incorrect. --- lib/matplotlib/widgets.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 6c1f493e7219..f099d0f2e445 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -64,15 +64,16 @@ class Widget(object): class AxesWidget(Widget): - """ - Widget that is connected to a single :class:`Axes`. - - Attributes - ---------- - *ax* - The parent :class:`matplotlib.axes.Axes` for the widget - *canvas* - The parent FigureCanvas for the widget + """Widget that is connected to a single :class:`~matplotlib.axes.Axes`. + + Attributes: + + *ax* : :class:`~matplotlib.axes.Axes` + The parent axes for the widget + *canvas* : :class:`~matplotlib.backend_bases.FigureCanvasBase` subclass + The parent figure canvs for the widget. + *active* : bool + If False, the widget does not respond to events. """ def __init__(self, ax): self.ax = ax @@ -81,10 +82,16 @@ def __init__(self, ax): self.active = True def connect_event(self, event, callback): + """Connect callback with an event. + + This should be used in lieu of `figure.canvas.mpl_connect` since this + function stores call back ids for later clean up. + """ cid = self.canvas.mpl_connect(event, callback) self.cids.append(cid) def disconnect_events(self): + """Disconnect all events created by this widget.""" for c in self.cids: self.canvas.mpl_disconnect(c) From fe3e2b870a6a2634e5267115f83c3500c6372e15 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 12 Mar 2012 00:13:12 -0400 Subject: [PATCH 11/12] Use `Path.contains_point` instead of `nxutils` function. `nxutils` is removed from master. --- examples/widgets/lasso_selector_demo.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/widgets/lasso_selector_demo.py b/examples/widgets/lasso_selector_demo.py index d1d9f3baa310..52ffa45d8ca1 100644 --- a/examples/widgets/lasso_selector_demo.py +++ b/examples/widgets/lasso_selector_demo.py @@ -1,7 +1,7 @@ import numpy as np from matplotlib.widgets import LassoSelector -from matplotlib.nxutils import points_inside_poly +from matplotlib.path import Path class SelectFromCollection(object): @@ -45,7 +45,8 @@ def __init__(self, ax, collection, alpha_other=0.3): self.ind = [] def onselect(self, verts): - self.ind = np.nonzero(points_inside_poly(self.xys, verts))[0] + path = Path(verts) + self.ind = np.nonzero([path.contains_point(xy) for xy in self.xys])[0] self.fc[:, -1] = self.alpha_other self.fc[self.ind, -1] = 1 self.collection.set_facecolors(self.fc) @@ -76,6 +77,6 @@ def disconnect(self): print selector.xys[selector.ind] selector.disconnect() - # Block end of script so you can check that lasso is disconnected. + # Block end of script so you can check that the lasso is disconnected. raw_input('Press any key to quit') From 78798f153f9f285f7c95c41eab711d113ffeb634 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Mon, 12 Mar 2012 00:14:30 -0400 Subject: [PATCH 12/12] Add docstrings for `Lasso` and `LassoSelector`. --- lib/matplotlib/widgets.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 8c8e29017600..b2a2c0c2424c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1418,6 +1418,35 @@ def get_active(self): class LassoSelector(AxesWidget): + """Selection curve of an arbitrary shape. + + The selected path can be used in conjunction with + :function:`~matplotlib.path.Path.contains_point` to select + data points from an image. + + In contrast to :class:`Lasso`, `LassoSelector` is written with an interface + similar to :class:`RectangleSelector` and :class:`SpanSelector` and will + continue to interact with the axes until disconnected. + + Parameters: + + *ax* : :class:`~matplotlib.axes.Axes` + The parent axes for the widget. + *onselect* : function + Whenever the lasso is released, the `onselect` function is called and + passed the vertices of the selected path. + + Example usage:: + + ax = subplot(111) + ax.plot(x,y) + + def onselect(verts): + print verts + lasso = LassoSelector(ax, onselect) + + """ + def __init__(self, ax, onselect=None, useblit=True, lineprops=None): AxesWidget.__init__(self, ax) @@ -1482,6 +1511,27 @@ def update_background(self, event): class Lasso(AxesWidget): + """Selection curve of an arbitrary shape. + + The selected path can be used in conjunction with + :function:`~matplotlib.path.Path.contains_point` to select + data points from an image. + + Unlike :class:`LassoSelector`, this must be initialized with a starting + point `xy`, and the `Lasso` events are destroyed upon release. + + Parameters: + + *ax* : :class:`~matplotlib.axes.Axes` + The parent axes for the widget. + *xy* : array + Coordinates of the start of the lasso. + *callback* : function + Whenever the lasso is released, the `callback` function is called and + passed the vertices of the selected path. + + """ + def __init__(self, ax, xy, callback=None, useblit=True): AxesWidget.__init__(self, ax)