diff --git a/examples/widgets/rectangle_selector.py b/examples/widgets/rectangle_selector.py index 0efdb3635025..eaedc686ea31 100644 --- a/examples/widgets/rectangle_selector.py +++ b/examples/widgets/rectangle_selector.py @@ -46,6 +46,7 @@ def toggle_selector(event): drawtype='box', useblit=True, button=[1, 3], # don't use middle button minspanx=5, minspany=5, - spancoords='pixels') + spancoords='pixels', + interactive=True) plt.connect('key_press_event', toggle_selector) plt.show() diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 64b0e342a21b..c9b100e4d933 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -11,8 +11,18 @@ import matplotlib.pyplot as plt from matplotlib.testing.decorators import cleanup +from numpy.testing import assert_allclose -def get_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): + +def get_ax(): + fig, ax = plt.subplots(1, 1) + ax.plot([0, 200], [0, 200]) + ax.set_aspect(1.0) + ax.figure.canvas.draw() + return ax + + +def do_event(tool, etype, button=1, xdata=0, ydata=0, key=None, step=1): """ *name* the event name @@ -51,6 +61,7 @@ def get_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): """ event = mock.Mock() event.button = button + ax = tool.ax event.x, event.y = ax.transData.transform([(xdata, ydata), (xdata, ydata)])[00] event.xdata, event.ydata = xdata, ydata @@ -60,32 +71,33 @@ def get_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): event.step = step event.guiEvent = None event.name = 'Custom' - return event + + func = getattr(tool, etype) + func(event) @cleanup def check_rectangle(**kwargs): - fig, ax = plt.subplots(1, 1) - ax.plot([0, 200], [0, 200]) - ax.figure.canvas.draw() + ax = get_ax() def onselect(epress, erelease): ax._got_onselect = True assert epress.xdata == 100 assert epress.ydata == 100 - assert erelease.xdata == 200 - assert erelease.ydata == 200 + assert erelease.xdata == 199 + assert erelease.ydata == 199 tool = widgets.RectangleSelector(ax, onselect, **kwargs) - event = get_event(ax, xdata=100, ydata=100, button=1) - tool.press(event) - - event = get_event(ax, xdata=125, ydata=125, button=1) - tool.onmove(event) + do_event(tool, 'press', xdata=100, ydata=100, button=1) + do_event(tool, 'onmove', xdata=199, ydata=199, button=1) # purposely drag outside of axis for release - event = get_event(ax, xdata=250, ydata=250, button=1) - tool.release(event) + do_event(tool, 'release', xdata=250, ydata=250, button=1) + + if kwargs.get('drawtype', None) not in ['line', 'none']: + assert_allclose(tool.geometry, + [[100., 100, 199, 199, 100], [100, 199, 199, 100, 100]], + err_msg=tool.geometry) assert ax._got_onselect @@ -99,11 +111,101 @@ def test_rectangle_selector(): check_rectangle(rectprops=dict(fill=True)) +@cleanup +def test_ellipse(): + """For ellipse, test out the key modifiers""" + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.EllipseSelector(ax, onselect=onselect, + maxdist=10, interactive=True) + tool.extents = (100, 150, 100, 150) + + # drag the rectangle + do_event(tool, 'press', xdata=10, ydata=10, button=1, + key=' ') + do_event(tool, 'onmove', xdata=30, ydata=30, button=1) + do_event(tool, 'release', xdata=30, ydata=30, button=1) + assert tool.extents == (120, 170, 120, 170), tool.extents + + # create from center + do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1, + key='control') + do_event(tool, 'press', xdata=100, ydata=100, button=1) + do_event(tool, 'onmove', xdata=125, ydata=125, button=1) + do_event(tool, 'release', xdata=125, ydata=125, button=1) + do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1, + key='control') + assert tool.extents == (75, 125, 75, 125), tool.extents + + # create a square + do_event(tool, 'on_key_press', xdata=10, ydata=10, button=1, + key='shift') + do_event(tool, 'press', xdata=10, ydata=10, button=1) + do_event(tool, 'onmove', xdata=35, ydata=30, button=1) + do_event(tool, 'release', xdata=35, ydata=30, button=1) + do_event(tool, 'on_key_release', xdata=10, ydata=10, button=1, + key='shift') + extents = [int(e) for e in tool.extents] + assert extents == [10, 35, 10, 34] + + # create a square from center + do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1, + key='ctrl+shift') + do_event(tool, 'press', xdata=100, ydata=100, button=1) + do_event(tool, 'onmove', xdata=125, ydata=130, button=1) + do_event(tool, 'release', xdata=125, ydata=130, button=1) + do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1, + key='ctrl+shift') + extents = [int(e) for e in tool.extents] + assert extents == [70, 129, 70, 130], extents + + assert tool.geometry.shape == (2, 74) + assert_allclose(tool.geometry[:, 0], [70., 100]) + + +@cleanup +def test_rectangle_handles(): + ax = get_ax() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect=onselect, + maxdist=10, interactive=True) + tool.extents = (100, 150, 100, 150) + + assert tool.corners == ( + (100, 150, 150, 100), (100, 100, 150, 150)) + assert tool.extents == (100, 150, 100, 150) + assert tool.edge_centers == ( + (100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150)) + assert tool.extents == (100, 150, 100, 150) + + # grab a corner and move it + do_event(tool, 'press', xdata=100, ydata=100) + do_event(tool, 'onmove', xdata=120, ydata=120) + do_event(tool, 'release', xdata=120, ydata=120) + assert tool.extents == (120, 150, 120, 150) + + # grab the center and move it + do_event(tool, 'press', xdata=132, ydata=132) + do_event(tool, 'onmove', xdata=120, ydata=120) + do_event(tool, 'release', xdata=120, ydata=120) + assert tool.extents == (108, 138, 108, 138) + + # create a new rectangle + do_event(tool, 'press', xdata=10, ydata=10) + do_event(tool, 'onmove', xdata=100, ydata=100) + do_event(tool, 'release', xdata=100, ydata=100) + assert tool.extents == (10, 100, 10, 100) + + @cleanup def check_span(*args, **kwargs): - fig, ax = plt.subplots(1, 1) - ax.plot([0, 200], [0, 200]) - ax.figure.canvas.draw() + ax = get_ax() def onselect(vmin, vmax): ax._got_onselect = True @@ -119,14 +221,9 @@ def onmove(vmin, vmax): kwargs['onmove_callback'] = onmove tool = widgets.SpanSelector(ax, onselect, *args, **kwargs) - event = get_event(ax, xdata=100, ydata=100, button=1) - tool.press(event) - - event = get_event(ax, xdata=125, ydata=125, button=1) - tool.onmove(event) - - event = get_event(ax, xdata=150, ydata=150, button=1) - tool.release(event) + do_event(tool, 'press', xdata=100, ydata=100, button=1) + do_event(tool, 'onmove', xdata=125, ydata=125, button=1) + do_event(tool, 'release', xdata=150, ydata=150, button=1) assert ax._got_onselect @@ -142,24 +239,16 @@ def test_span_selector(): @cleanup def check_lasso_selector(**kwargs): - fig, ax = plt.subplots(1, 1) - ax = plt.gca() - ax.plot([0, 200], [0, 200]) - ax.figure.canvas.draw() + ax = get_ax() def onselect(verts): ax._got_onselect = True assert verts == [(100, 100), (125, 125), (150, 150)] tool = widgets.LassoSelector(ax, onselect, **kwargs) - event = get_event(ax, xdata=100, ydata=100, button=1) - tool.press(event) - - event = get_event(ax, xdata=125, ydata=125, button=1) - tool.onmove(event) - - event = get_event(ax, xdata=150, ydata=150, button=1) - tool.release(event) + do_event(tool, 'press', xdata=100, ydata=100, button=1) + do_event(tool, 'onmove', xdata=125, ydata=125, button=1) + do_event(tool, 'release', xdata=150, ydata=150, button=1) assert ax._got_onselect diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5880ae9de30a..ab2331e0b0e4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -19,7 +19,7 @@ import numpy as np from .mlab import dist -from .patches import Circle, Rectangle +from .patches import Circle, Rectangle, Ellipse from .lines import Line2D from .transforms import blended_transform_factory @@ -83,7 +83,7 @@ def get_active(self): # set_active is overriden by SelectorWidgets. active = property(get_active, lambda self, active: self.set_active(active), doc="Is the widget active?") - + def ignore(self, event): """Return True if event should be ignored. @@ -642,7 +642,7 @@ class RadioButtons(AxesWidget): *circles* A list of :class:`matplotlib.patches.Circle` instances - + *value_selected* A string listing the current value selected @@ -1091,7 +1091,7 @@ def disconnect(self): def clear(self, event): """clear the cursor""" if self.ignore(event): - return + return if self.useblit: self.background = ( self.canvas.copy_from_bbox(self.canvas.figure.bbox)) @@ -1100,7 +1100,7 @@ def clear(self, event): def onmove(self, event): if self.ignore(event): - return + return if event.inaxes is None: return if not self.canvas.widgetlock.available(self): @@ -1135,16 +1135,21 @@ def _update(self): class _SelectorWidget(AxesWidget): - def __init__(self, ax, onselect, useblit=False, button=None): + def __init__(self, ax, onselect, useblit=False, button=None, + state_modifier_keys=None): AxesWidget.__init__(self, ax) self.visible = True self.onselect = onselect + self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() + self.state_modifier_keys = dict(move=' ', clear='escape', + square='shift', center='control') + self.state_modifier_keys.update(state_modifier_keys or {}) + self.background = None self.artists = [] - self.useblit = useblit and self.canvas.supports_blit if isinstance(button, int): self.validButtons = [button] @@ -1155,6 +1160,8 @@ def __init__(self, ax, onselect, useblit=False, button=None): self.eventpress = None # will save the data (pos. at mouserelease) self.eventrelease = None + self._prev_event = None + self.state = set() def set_active(self, active): AxesWidget.set_active(self, active) @@ -1175,11 +1182,12 @@ def connect_default_events(self): self.connect_event('button_release_event', self.release) self.connect_event('draw_event', self.update_background) self.connect_event('key_press_event', self.on_key_press) + self.connect_event('key_release_event', self.on_key_release) self.connect_event('scroll_event', self.on_scroll) def ignore(self, event): """return *True* if *event* should be ignored""" - if not self.active: + if not self.active or not self.ax.get_visible(): return True # If canvas was locked @@ -1215,6 +1223,9 @@ def update(self): useblit """ + if not self.ax.get_visible(): + return False + if self.useblit: if self.background is not None: self.canvas.restore_region(self.background) @@ -1228,7 +1239,9 @@ def update(self): return False def _get_data(self, event): - """Limit the xdata and ydata to the axes limits""" + """Get the xdata and ydata for event, with limits""" + if event.xdata is None: + return None, None x0, x1 = self.ax.get_xbound() y0, y1 = self.ax.get_ybound() xdata = max(x0, event.xdata) @@ -1237,35 +1250,109 @@ def _get_data(self, event): ydata = min(y1, ydata) return xdata, ydata + def _clean_event(self, event): + """Clean up an event + + Use prev event if there is no xdata + Limit the xdata and ydata to the axes limits + Set the prev event + """ + if event.xdata is None: + event = self._prev_event + else: + event = copy.copy(event) + event.xdata, event.ydata = self._get_data(event) + + self._prev_event = event + return event + def press(self, event): - """Button press handler""" + """Button press handler and validator""" if not self.ignore(event): - self.eventpress = copy.copy(event) - self.eventpress.xdata, self.eventpress.ydata = ( - self._get_data(event)) + event = self._clean_event(event) + self.eventpress = event + self._prev_event = event + key = event.key or '' + key = key.replace('ctrl', 'control') + # move state is locked in on a button press + if key == self.state_modifier_keys['move']: + self.state.add('move') + self._press(event) return True return False + def _press(self, event): + """Button press handler""" + pass + def release(self, event): - """Button release event""" - if not self.ignore(event) and self.eventpress is not None: - self.eventrelease = copy.copy(event) - self.eventrelease.xdata, self.eventrelease.ydata = ( - self._get_data(event)) + """Button release event handler and validator""" + if not self.ignore(event) and self.eventpress: + event = self._clean_event(event) + self.eventrelease = event + self._release(event) + self.eventpress = None + self.eventrelease = None + self.state.discard('move') return True - else: - return False + return False + + def _release(self, event): + """Button release event handler""" + pass def onmove(self, event): - """Cursor move event""" + """Cursor move event handler and validator""" + if not self.ignore(event) and self.eventpress: + event = self._clean_event(event) + self._onmove(event) + return True + return False + + def _onmove(self, event): + """Cursor move event handler""" pass def on_scroll(self, event): - """Mouse scroll event""" + """Mouse scroll event handler and validator""" + if not self.ignore(event): + self._on_scroll(event) + + def _on_scroll(self, event): + """Mouse scroll event handler""" pass def on_key_press(self, event): - """Key press event""" + """Key press event handler and validator for all selection widgets""" + if self.active: + key = event.key or '' + key = key.replace('ctrl', 'control') + if key == self.state_modifier_keys['clear']: + for artist in self.artists: + artist.set_visible(False) + self.update() + return + for (state, modifier) in self.state_modifier_keys.items(): + if modifier in key: + self.state.add(state) + self._on_key_press(event) + + def _on_key_press(self, event): + """Key press event handler - use for widget-specific key press actions. + """ + pass + + def on_key_release(self, event): + """Key release event handler and validator""" + if self.active: + key = event.key or '' + for (state, modifier) in self.state_modifier_keys.items(): + if modifier in key: + self.state.discard(state) + self._on_key_release(event) + + def _on_key_release(self, event): + """Key release event handler""" pass def set_visible(self, visible): @@ -1312,7 +1399,7 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, If *minspan* is not *None*, ignore events smaller than *minspan* The span rectangle is drawn with *rectprops*; default:: - + rectprops = dict(facecolor='red', alpha=0.5) Set the visible attribute to *False* if you want to turn off @@ -1333,11 +1420,13 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, """ _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, - button=button) + button=button) if rectprops is None: rectprops = dict(facecolor='red', alpha=0.5) + rectprops['animated'] = self.useblit + if direction not in ['horizontal', 'vertical']: msg = "direction must be in [ 'horizontal' | 'vertical' ]" raise ValueError(msg) @@ -1386,18 +1475,15 @@ def new_axes(self, ax): **self.rectprops) self.ax.add_patch(self.stay_rect) - if not self.useblit: - self.ax.add_patch(self.rect) - self.artists = [self.rect] + self.ax.add_patch(self.rect) + self.artists = [self.rect] def ignore(self, event): """return *True* if *event* should be ignored""" return _SelectorWidget.ignore(self, event) or not self.visible - def press(self, event): + def _press(self, event): """on button press event""" - if not _SelectorWidget.press(self, event): - return False self.rect.set_visible(self.visible) if self.span_stays: self.stay_rect.set_visible(False) @@ -1409,9 +1495,9 @@ def press(self, event): self.pressv = ydata return False - def release(self, event): + def _release(self, event): """on button release event""" - if not _SelectorWidget.release(self, event) or self.pressv is None: + if self.pressv is None: return self.buttonDown = False @@ -1441,11 +1527,14 @@ def release(self, event): self.pressv = None return False - def onmove(self, event): + def _onmove(self, event): """on motion notify event""" - if self.pressv is None or self.ignore(event): + if self.pressv is None: return x, y = self._get_data(event) + if x is None: + return + self.prev = x, y if self.direction == 'horizontal': v = x @@ -1478,6 +1567,65 @@ def onmove(self, event): return False +class ToolHandles(object): + """Control handles for canvas tools. + + Parameters + ---------- + ax : :class:`matplotlib.axes.Axes` + Matplotlib axes where tool handles are displayed. + x, y : 1D arrays + Coordinates of control handles. + marker : str + Shape of marker used to display handle. See `matplotlib.pyplot.plot`. + marker_props : dict + Additional marker properties. See :class:`matplotlib.lines.Line2D`. + """ + + def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True): + self.ax = ax + + props = dict(marker=marker, markersize=7, mfc='w', ls='none', + alpha=0.5, visible=False) + props.update(marker_props if marker_props is not None else {}) + self._markers = Line2D(x, y, animated=useblit, **props) + self.ax.add_line(self._markers) + self.artist = self._markers + + @property + def x(self): + return self._markers.get_xdata() + + @property + def y(self): + return self._markers.get_ydata() + + def set_data(self, pts, y=None): + """Set x and y positions of handles""" + if y is not None: + x = pts + pts = np.array([x, y]) + self._markers.set_data(pts) + + def set_visible(self, val): + self._markers.set_visible(val) + + def set_animated(self, val): + self._markers.set_animated(val) + + def closest(self, x, y): + """Return index and pixel distance to closest index.""" + pts = np.transpose((self.x, self.y)) + # Transform data coordinates to pixel coordinates. + pts = self.ax.transData.transform(pts) + diff = pts - ((x, y)) + if diff.ndim == 2: + dist = np.sqrt(np.sum(diff ** 2, axis=1)) + return np.argmin(dist), np.min(dist) + else: + return 0, np.sqrt(np.sum(diff ** 2)) + + class RectangleSelector(_SelectorWidget): """ Select a rectangular region of an axes. @@ -1492,17 +1640,17 @@ class RectangleSelector(_SelectorWidget): def onselect(eclick, erelease): 'eclick and erelease are matplotlib events at press and release' - print ' startposition : (%f, %f)' % (eclick.xdata, eclick.ydata) - print ' endposition : (%f, %f)' % (erelease.xdata, erelease.ydata) - print ' used button : ', eclick.button + print(' startposition : (%f, %f)' % (eclick.xdata, eclick.ydata)) + print(' endposition : (%f, %f)' % (erelease.xdata, erelease.ydata)) + print(' used button : ', eclick.button) def toggle_selector(event): - print ' Key pressed.' + print(' Key pressed.') if event.key in ['Q', 'q'] and toggle_selector.RS.active: - print ' RectangleSelector deactivated.' + print(' RectangleSelector deactivated.') toggle_selector.RS.set_active(False) if event.key in ['A', 'a'] and not toggle_selector.RS.active: - print ' RectangleSelector activated.' + print(' RectangleSelector activated.') toggle_selector.RS.set_active(True) x = arange(100)/(99.0) @@ -1515,10 +1663,14 @@ def toggle_selector(event): connect('key_press_event', toggle_selector) show() """ + + _shape_klass = Rectangle + def __init__(self, ax, onselect, drawtype='box', minspanx=None, minspany=None, useblit=False, lineprops=None, rectprops=None, spancoords='data', - button=None): + button=None, maxdist=10, marker_props=None, + interactive=False, state_modifier_keys=None): """ Create a selector in *ax*. When a selection is made, clear @@ -1535,7 +1687,7 @@ def __init__(self, ax, onselect, drawtype='box', The rectangle is drawn with *rectprops*; default:: rectprops = dict(facecolor='red', edgecolor = 'black', - alpha=0.5, fill=False) + alpha=0.2, fill=True) The line is drawn with *lineprops*; default:: @@ -1560,11 +1712,30 @@ def __init__(self, ax, onselect, drawtype='box', 1 = left mouse button 2 = center mouse button (scroll wheel) 3 = right mouse button + + *interactive* will draw a set of handles and allow you interact + with the widget after it is drawn. + + *state_modifier_keys* are keyboard modifiers that affect the behavior + of the widget. + + The defaults are: + dict(move=' ', clear='escape', square='shift', center='ctrl') + + Keyboard modifiers, which: + 'move': Move the existing shape. + 'clear': Clear the current shape. + 'square': Makes the shape square. + 'center': Make the initial point the center of the shape. + 'square' and 'center' can be combined. """ _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, - button=button) + button=button, + state_modifier_keys=state_modifier_keys) self.to_draw = None + self.visible = True + self.interactive = interactive if drawtype == 'none': drawtype = 'line' # draw a line but make it @@ -1572,19 +1743,19 @@ def __init__(self, ax, onselect, drawtype='box', if drawtype == 'box': if rectprops is None: - rectprops = dict(facecolor='white', edgecolor='black', - alpha=0.5, fill=False) + rectprops = dict(facecolor='red', edgecolor='black', + alpha=0.2, fill=True) + rectprops['animated'] = self.useblit self.rectprops = rectprops - self.to_draw = Rectangle((0, 0), + self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False, **self.rectprops) self.ax.add_patch(self.to_draw) if drawtype == 'line': if lineprops is None: lineprops = dict(color='black', linestyle='-', linewidth=2, alpha=0.5) + lineprops['animated'] = self.useblit self.lineprops = lineprops - if self.useblit: - self.lineprops['animated'] = True self.to_draw = Line2D([0, 0], [0, 0], visible=False, **self.lineprops) self.ax.add_line(self.to_draw) @@ -1598,24 +1769,57 @@ def __init__(self, ax, onselect, drawtype='box', self.spancoords = spancoords self.drawtype = drawtype - self.artists = [self.to_draw] - def press(self, event): + self.maxdist = maxdist + + if rectprops is None: + props = dict(mec='r') + else: + props = dict(mec=rectprops.get('edgecolor', 'r')) + self._corner_order = ['NW', 'NE', 'SE', 'SW'] + xc, yc = self.corners + self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props, + useblit=self.useblit) + + self._edge_order = ['W', 'N', 'E', 'S'] + xe, ye = self.edge_centers + self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', + marker_props=props, useblit=self.useblit) + + xc, yc = self.center + self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', + marker_props=props, useblit=self.useblit) + + self.active_handle = None + + self.artists = [self.to_draw, self._center_handle.artist, + self._corner_handles.artist, + self._edge_handles.artist] + + if not self.interactive: + self.artists = [self.to_draw] + + self._extents_on_press = None + + def _press(self, event): """on button press event""" - if not _SelectorWidget.press(self, event): - return True - # make the drawed box/line visible - self.to_draw.set_visible(self.visible) - return False + # make the drawed box/line visible get the click-coordinates, + # button, ... + if self.interactive and self.to_draw.get_visible(): + self._set_active_handle(event) + else: + self.active_handle = None - def release(self, event): - """on button release event""" - if not _SelectorWidget.release(self, event): - return True + if self.active_handle is None or not self.interactive: + # Clear previous rectangle before drawing new rectangle. + self.update() - # make the box/line invisible again - self.to_draw.set_visible(False) - self.canvas.draw() + self.set_visible(self.visible) + + def _release(self, event): + """on button release event""" + if not self.interactive: + self.to_draw.set_visible(False) if self.spancoords == 'data': xmin, ymin = self.eventpress.xdata, self.eventpress.ydata @@ -1642,38 +1846,270 @@ def release(self, event): (xproblems or yproblems)): # check if drawn distance (if it exists) is not too small in # neither x nor y-direction + self.extents = [0, 0, 0, 0] return + # update the eventpress and eventrelease with the resulting extents + x1, x2, y1, y2 = self.extents + self.eventpress.xdata = x1 + self.eventpress.ydata = y1 + xy1 = self.ax.transData.transform_point([x1, y1]) + self.eventpress.x, self.eventpress.y = xy1 + + self.eventrelease.xdata = x2 + self.eventrelease.ydata = y2 + xy2 = self.ax.transData.transform_point([x2, y2]) + self.eventrelease.x, self.eventrelease.y = xy2 + self.onselect(self.eventpress, self.eventrelease) # call desired function - self.eventpress = None + self.update() + return False - def onmove(self, event): + def _onmove(self, event): """on motion notify event if box/line is wanted""" - if self.eventpress is None or self.ignore(event): + # resize an existing shape + if self.active_handle and not self.active_handle == 'C': + x1, x2, y1, y2 = self._extents_on_press + if self.active_handle in ['E', 'W'] + self._corner_order: + x2 = event.xdata + if self.active_handle in ['N', 'S'] + self._corner_order: + y2 = event.ydata + + # move existing shape + elif (('move' in self.state or self.active_handle == 'C') + and self._extents_on_press is not None): + x1, x2, y1, y2 = self._extents_on_press + dx = event.xdata - self.eventpress.xdata + dy = event.ydata - self.eventpress.ydata + x1 += dx + x2 += dx + y1 += dy + y2 += dy + + # new shape + else: + center = [self.eventpress.xdata, self.eventpress.ydata] + center_pix = [self.eventpress.x, self.eventpress.y] + dx = (event.xdata - center[0]) / 2. + dy = (event.ydata - center[1]) / 2. + + # square shape + if 'square' in self.state: + dx_pix = abs(event.x - center_pix[0]) + dy_pix = abs(event.y - center_pix[1]) + if not dx_pix: + return + maxd = max(abs(dx_pix), abs(dy_pix)) + if abs(dx_pix) < maxd: + dx *= maxd / (abs(dx_pix) + 1e-6) + if abs(dy_pix) < maxd: + dy *= maxd / (abs(dy_pix) + 1e-6) + + # from center + if 'center' in self.state: + dx *= 2 + dy *= 2 + + # from corner + else: + center[0] += dx + center[1] += dy + + x1, x2, y1, y2 = (center[0] - dx, center[0] + dx, + center[1] - dy, center[1] + dy) + + self.extents = x1, x2, y1, y2 + + @property + def _rect_bbox(self): + if self.drawtype == 'box': + x0 = self.to_draw.get_x() + y0 = self.to_draw.get_y() + width = self.to_draw.get_width() + height = self.to_draw.get_height() + return x0, y0, width, height + else: + x, y = self.to_draw.get_data() + x0, x1 = min(x), max(x) + y0, y1 = min(y), max(y) + return x0, y0, x1 - x0, y1 - y0 + + @property + def corners(self): + """Corners of rectangle from lower left, moving clockwise.""" + x0, y0, width, height = self._rect_bbox + xc = x0, x0 + width, x0 + width, x0 + yc = y0, y0, y0 + height, y0 + height + return xc, yc + + @property + def edge_centers(self): + """Midpoint of rectangle edges from left, moving clockwise.""" + x0, y0, width, height = self._rect_bbox + w = width / 2. + h = height / 2. + xe = x0, x0 + w, x0 + width, x0 + w + ye = y0 + h, y0, y0 + h, y0 + height + return xe, ye + + @property + def center(self): + """Center of rectangle""" + x0, y0, width, height = self._rect_bbox + return x0 + width / 2., y0 + height / 2. + + @property + def extents(self): + """Return (xmin, xmax, ymin, ymax).""" + x0, y0, width, height = self._rect_bbox + xmin, xmax = sorted([x0, x0 + width]) + ymin, ymax = sorted([y0, y0 + height]) + return xmin, xmax, ymin, ymax + + @extents.setter + def extents(self, extents): + # Update displayed shape + self.draw_shape(extents) + # Update displayed handles + self._corner_handles.set_data(*self.corners) + self._edge_handles.set_data(*self.edge_centers) + self._center_handle.set_data(*self.center) + self.set_visible(self.visible) + self.update() + + def draw_shape(self, extents): + x0, x1, y0, y1 = extents + xmin, xmax = sorted([x0, x1]) + ymin, ymax = sorted([y0, y1]) + xlim = sorted(self.ax.get_xlim()) + ylim = sorted(self.ax.get_ylim()) + + xmin = max(xlim[0], xmin) + ymin = max(ylim[0], ymin) + xmax = min(xmax, xlim[1]) + ymax = min(ymax, ylim[1]) + + if self.drawtype == 'box': + self.to_draw.set_x(xmin) + self.to_draw.set_y(ymin) + self.to_draw.set_width(xmax - xmin) + self.to_draw.set_height(ymax - ymin) + + elif self.drawtype == 'line': + self.to_draw.set_data([xmin, xmax], [ymin, ymax]) + + def _set_active_handle(self, event): + """Set active handle based on the location of the mouse event""" + # Note: event.xdata/ydata in data coordinates, event.x/y in pixels + c_idx, c_dist = self._corner_handles.closest(event.x, event.y) + e_idx, e_dist = self._edge_handles.closest(event.x, event.y) + m_idx, m_dist = self._center_handle.closest(event.x, event.y) + + if 'move' in self.state: + self.active_handle = 'C' + self._extents_on_press = self.extents + + # Set active handle as closest handle, if mouse click is close enough. + elif m_dist < self.maxdist * 2: + self.active_handle = 'C' + elif c_dist > self.maxdist and e_dist > self.maxdist: + self.active_handle = None return + elif c_dist < e_dist: + self.active_handle = self._corner_order[c_idx] + else: + self.active_handle = self._edge_order[e_idx] + + # Save coordinates of rectangle at the start of handle movement. + x1, x2, y1, y2 = self.extents + # Switch variables so that only x2 and/or y2 are updated on move. + if self.active_handle in ['W', 'SW', 'NW']: + x1, x2 = x2, event.xdata + if self.active_handle in ['N', 'NW', 'NE']: + y1, y2 = y2, event.ydata + self._extents_on_press = x1, x2, y1, y2 + + @property + def geometry(self): + if hasattr(self.to_draw, 'get_verts'): + xfm = self.ax.transData.inverted() + y, x = xfm.transform(self.to_draw.get_verts()).T + return np.array([x[:-1], y[:-1]]) + else: + return np.array(self.to_draw.get_data()) + + +class EllipseSelector(RectangleSelector): + """ + Select an elliptical region of an axes. + + For the cursor to remain responsive you much keep a reference to + it. + + Example usage:: + + from matplotlib.widgets import EllipseSelector + from pylab import * + + def onselect(eclick, erelease): + 'eclick and erelease are matplotlib events at press and release' + print(' startposition : (%f, %f)' % (eclick.xdata, eclick.ydata)) + print(' endposition : (%f, %f)' % (erelease.xdata, erelease.ydata)) + print(' used button : ', eclick.button) + + def toggle_selector(event): + print(' Key pressed.') + if event.key in ['Q', 'q'] and toggle_selector.ES.active: + print(' EllipseSelector deactivated.') + toggle_selector.RS.set_active(False) + if event.key in ['A', 'a'] and not toggle_selector.ES.active: + print(' EllipseSelector activated.') + toggle_selector.ES.set_active(True) + + x = arange(100)/(99.0) + y = sin(x) + fig = figure + ax = subplot(111) + ax.plot(x,y) + + toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line') + connect('key_press_event', toggle_selector) + show() + """ + _shape_klass = Ellipse + + def draw_shape(self, extents): + x1, x2, y1, y2 = extents + xmin, xmax = sorted([x1, x2]) + ymin, ymax = sorted([y1, y2]) + center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.] + a = (xmax - xmin) / 2. + b = (ymax - ymin) / 2. - x, y = self._get_data(event) # actual position (with - # (button still pressed) if self.drawtype == 'box': - minx, maxx = self.eventpress.xdata, x # click-x and actual mouse-x - miny, maxy = self.eventpress.ydata, y # click-y and actual mouse-y - if minx > maxx: - minx, maxx = maxx, minx # get them in the right order - if miny > maxy: - miny, maxy = maxy, miny - self.to_draw.set_x(minx) # set lower left of box - self.to_draw.set_y(miny) - self.to_draw.set_width(maxx - minx) # set width and height of box - self.to_draw.set_height(maxy - miny) - self.update() - return False - if self.drawtype == 'line': - self.to_draw.set_data([self.eventpress.xdata, x], - [self.eventpress.ydata, y]) - self.update() - return False + self.to_draw.center = center + self.to_draw.width = 2 * a + self.to_draw.height = 2 * b + else: + rad = np.arange(31) * 12 * np.pi / 180 + x = a * np.cos(rad) + center[0] + y = b * np.sin(rad) + center[1] + self.to_draw.set_data(x, y) + + @property + def _rect_bbox(self): + if self.drawtype == 'box': + x, y = self.to_draw.center + width = self.to_draw.width + height = self.to_draw.height + return x - width / 2., y - height / 2., width, height + else: + x, y = self.to_draw.get_data() + x0, x1 = min(x), max(x) + y0, y1 = min(y), max(y) + return x0, y0, x1 - x0, y1 - y0 class LassoSelector(_SelectorWidget): @@ -1737,18 +2173,14 @@ def __init__(self, ax, onselect=None, useblit=True, lineprops=None, def onpress(self, event): self.press(event) - def press(self, event): - if not _SelectorWidget.press(self, event): - return + def _press(self, event): self.verts = [self._get_data(event)] self.line.set_visible(True) def onrelease(self, event): self.release(event) - def release(self, event): - if not _SelectorWidget.release(self, event): - return + def _release(self, event): if self.verts is not None: self.verts.append(self._get_data(event)) self.onselect(self.verts) @@ -1756,8 +2188,8 @@ def release(self, event): self.line.set_visible(False) self.verts = None - def onmove(self, event): - if self.ignore(event) or self.verts is None: + def _onmove(self, event): + if self.verts is None: return self.verts.append(self._get_data(event))