From 2ed0d496ad0ae91d5b6e022ca887699a5dc4df3a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 09:21:16 -0600 Subject: [PATCH 01/48] Store prev event to handle out of bounds selections --- lib/matplotlib/widgets.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5880ae9de30a..24ce70fa6f7a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1155,6 +1155,7 @@ 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 def set_active(self, active): AxesWidget.set_active(self, active) @@ -1241,6 +1242,7 @@ def press(self, event): """Button press handler""" if not self.ignore(event): self.eventpress = copy.copy(event) + self._prev_event = copy.copy(event) self.eventpress.xdata, self.eventpress.ydata = ( self._get_data(event)) return True @@ -1249,16 +1251,25 @@ def press(self, event): def release(self, event): """Button release event""" if not self.ignore(event) and self.eventpress is not None: + if event.xdata is None: + event = copy.copy(self._prev_event) self.eventrelease = copy.copy(event) self.eventrelease.xdata, self.eventrelease.ydata = ( self._get_data(event)) - return True + return event else: - return False + return None def onmove(self, event): """Cursor move event""" - pass + if not self.ignore(event) and self.eventpress is not None: + if event.xdata is None: + event = copy.copy(self._prev_event) + else: + self._prev_event = copy.copy(event) + return event + else: + return False def on_scroll(self, event): """Mouse scroll event""" From f8eefe6138f819b8335cc803f668115a82c2faa4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 09:23:42 -0600 Subject: [PATCH 02/48] Upgrade RectangleSelector with ToolHandles and add Ellipse --- lib/matplotlib/widgets.py | 306 +++++++++++++++++++++++++++++++++----- 1 file changed, 265 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 24ce70fa6f7a..352732fd1f39 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 @@ -1526,10 +1526,13 @@ def toggle_selector(event): connect('key_press_event', toggle_selector) show() """ - def __init__(self, ax, onselect, drawtype='box', + + _shape_klass = Rectangle + + def __init__(self, ax, onselect, drawtype='patch', minspanx=None, minspany=None, useblit=False, lineprops=None, rectprops=None, spancoords='data', - button=None): + button=None, maxdist=10, marker_props=None): """ Create a selector in *ax*. When a selection is made, clear @@ -1573,20 +1576,24 @@ def __init__(self, ax, onselect, drawtype='box', 3 = right mouse button """ _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, - button=button) + button=button) self.to_draw = None + self.visible = True + + if drawtype == 'box': # backwards compatibility + drawtype = 'patch' if drawtype == 'none': drawtype = 'line' # draw a line but make it self.visible = False # invisible - if drawtype == 'box': + if drawtype == 'patch': 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) 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': @@ -1594,9 +1601,7 @@ def __init__(self, ax, onselect, drawtype='box', lineprops = dict(color='black', linestyle='-', linewidth=2, alpha=0.5) self.lineprops = lineprops - if self.useblit: - self.lineprops['animated'] = True - self.to_draw = Line2D([0, 0], [0, 0], visible=False, + self.to_draw = Line2D([0, 0, 0, 0, 0], [0, 0, 0, 0, 0], visible=False, **self.lineprops) self.ax.add_line(self.to_draw) @@ -1609,25 +1614,51 @@ def __init__(self, ax, onselect, drawtype='box', self.spancoords = spancoords self.drawtype = drawtype - self.artists = [self.to_draw] + + self.maxdist = maxdist + + if rectprops is None: + props = dict(mec='r') + else: + props = dict(mec=rectprops['edgecolor']) + 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.artists = [self.to_draw, self._center_handle.artist, + self._corner_handles.artist, + self._edge_handles.artist] 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, ... + self.set_visible(self.visible) + self._set_active_handle(event) + + if self.active_handle is None: + # Clear previous rectangle before drawing new rectangle. + self.update() + + self.set_visible(self.visible) def release(self, event): """on button release event""" if not _SelectorWidget.release(self, event): return True - # make the box/line invisible again - self.to_draw.set_visible(False) - self.canvas.draw() - if self.spancoords == 'data': xmin, ymin = self.eventpress.xdata, self.eventpress.ydata xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata @@ -1655,36 +1686,229 @@ def release(self, event): # neither x nor y-direction 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): """on motion notify event if box/line is wanted""" - if self.eventpress is None or self.ignore(event): + event = _SelectorWidget.onmove(self, event) + if not event: + return True + + key = self.eventpress.key or '' + + # 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 self.active_handle == 'C': + 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 'shift' in key: + 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) + if abs(dy_pix) < maxd: + dy *= maxd / abs(dy_pix) + + # from center + if key == 'control' or key == 'ctrl+shift': + 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 == 'patch': + 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.canvas.draw_idle() + + def draw_shape(self, extents): + x0, x1, y0, y1 = extents + xmin, xmax = sorted([x0, x1]) + ymin, ymax = sorted([y0, y1]) + + if self.drawtype == 'patch': + 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, xmin, xmax, xmax, xmin], + [ymin, ymax, ymax, ymin, ymin]) + + 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 event.key in ['alt', ' ']: + 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 + + +class EllipseSelector(RectangleSelector): + + _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. + + if self.drawtype == 'patch': + 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 == 'patch': + 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 + + @property + def geometry(self): + x0, y0, width, height = self._rect_bbox + return x0 + width / 2., y0 + width / 2., width, height - 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 class LassoSelector(_SelectorWidget): From 7f044cff4f83dc5facc07c6e2f980b3d7ca927ff Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 09:24:36 -0600 Subject: [PATCH 03/48] Add toolhandles class --- lib/matplotlib/widgets.py | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 352732fd1f39..25f64d117739 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1489,6 +1489,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. From f450df639b9f40f4cc339d7126a6e1245e568e92 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 09:41:27 -0600 Subject: [PATCH 04/48] Add test using tool handles --- lib/matplotlib/tests/test_widgets.py | 50 ++++++++++++++++++++++++++-- lib/matplotlib/widgets.py | 9 ++--- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 64b0e342a21b..43f139261677 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -73,14 +73,14 @@ 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) + event = get_event(ax, xdata=199, ydata=199, button=1) tool.onmove(event) # purposely drag outside of axis for release @@ -99,6 +99,50 @@ def test_rectangle_selector(): check_rectangle(rectprops=dict(fill=True)) +def test_rectangle_modifiers(): + # TODO: add tests for center, square, center, shift+drag + pass + + +def test_rectangle_handles(): + fig, ax = plt.subplots(1, 1) + ax.plot([0, 200], [0, 200]) + ax.figure.canvas.draw() + + def onselect(epress, erelease): + pass + + tool = widgets.RectangleSelector(ax, onselect=onselect, + maxdist=10) + event = get_event(ax, xdata=100, ydata=100, button=1) + tool.press(event) + + 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 + event = get_event(ax, xdata=100, ydata=100) + tool.press(event) + event = get_event(ax, xdata=120, ydata=120) + tool.onmove(event) + tool.release(event) + assert tool.extents == (120, 150, 120, 150) + + # create a new rectangle + event = get_event(ax, xdata=10, ydata=10) + tool.press(event) + event = get_event(ax, xdata=100, ydata=100) + tool.onmove(event) + tool.release(event) + assert tool.extents == (10, 100, 10, 100) + + @cleanup def check_span(*args, **kwargs): fig, ax = plt.subplots(1, 1) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 25f64d117739..9d00e1b63a39 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1548,6 +1548,7 @@ def closest(self, x, y): else: return 0, np.sqrt(np.sum(diff ** 2)) + class RectangleSelector(_SelectorWidget): """ Select a rectangular region of an axes. @@ -1679,7 +1680,7 @@ def __init__(self, ax, onselect, drawtype='patch', if rectprops is None: props = dict(mec='r') else: - props = dict(mec=rectprops['edgecolor']) + props = dict(mec=rectprops.get('edgecolor', 'k')) self._corner_order = ['NW', 'NE', 'SE', 'SW'] xc, yc = self.corners self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props, @@ -1963,12 +1964,6 @@ def _rect_bbox(self): y0, y1 = min(y), max(y) return x0, y0, x1 - x0, y1 - y0 - @property - def geometry(self): - x0, y0, width, height = self._rect_bbox - return x0 + width / 2., y0 + width / 2., width, height - - class LassoSelector(_SelectorWidget): """Selection curve of an arbitrary shape. From 79294e05e695e32eef64a00d250e0d670b60eed9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 09:43:27 -0600 Subject: [PATCH 05/48] Add a helper function to get axes --- lib/matplotlib/tests/test_widgets.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 43f139261677..1b10abd6a290 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -12,6 +12,14 @@ from matplotlib.testing.decorators import cleanup + +def get_ax(): + fig, ax = plt.subplots(1, 1) + ax.plot([0, 200], [0, 200]) + ax.figure.canvas.draw() + return ax + + def get_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): """ *name* @@ -65,9 +73,7 @@ def get_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): @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 @@ -105,9 +111,7 @@ def test_rectangle_modifiers(): def test_rectangle_handles(): - fig, ax = plt.subplots(1, 1) - ax.plot([0, 200], [0, 200]) - ax.figure.canvas.draw() + ax = get_ax() def onselect(epress, erelease): pass @@ -145,9 +149,7 @@ def onselect(epress, erelease): @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 @@ -186,10 +188,7 @@ 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 From fd264a227e14401f27a8444762d6a4e284f769ef Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 15:51:49 -0600 Subject: [PATCH 06/48] Do not allow extents outside the image and add a path property --- lib/matplotlib/widgets.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9d00e1b63a39..525a3f3494a1 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1886,6 +1886,13 @@ def draw_shape(self, extents): x0, x1, y0, y1 = extents xmin, xmax = sorted([x0, x1]) ymin, ymax = sorted([y0, y1]) + xlim = self.ax.get_xlim() + ylim = 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 == 'patch': self.to_draw.set_x(xmin) @@ -1928,6 +1935,10 @@ def _set_active_handle(self, event): y1, y2 = y2, event.ydata self._extents_on_press = x1, x2, y1, y2 + @property + def path(self): + return self.to_draw.get_path() + class EllipseSelector(RectangleSelector): From 438715f931192fd6302942191643145ed2a560c4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 15:52:14 -0600 Subject: [PATCH 07/48] Finish rectangle handle tests and add ellipse test with key modifiers --- lib/matplotlib/tests/test_widgets.py | 63 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 1b10abd6a290..8738e3697462 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -16,6 +16,7 @@ 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 @@ -105,9 +106,54 @@ def test_rectangle_selector(): check_rectangle(rectprops=dict(fill=True)) -def test_rectangle_modifiers(): - # TODO: add tests for center, square, center, shift+drag - pass +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) + tool.extents = (100, 150, 100, 150) + + # drag the rectangle + event = get_event(ax, xdata=10, ydata=10, button=1, + key='alt') + tool.press(event) + event = get_event(ax, xdata=30, ydata=30, button=1) + tool.onmove(event) + tool.release(event) + assert tool.extents == (120, 170, 120, 170) + + # create from center + event = get_event(ax, xdata=100, ydata=100, button=1, + key='control') + tool.press(event) + event = get_event(ax, xdata=125, ydata=125, button=1) + tool.onmove(event) + tool.release(event) + assert tool.extents == (75, 125, 75, 125) + + # create a square + event = get_event(ax, xdata=10, ydata=10, button=1, + key='shift') + tool.press(event) + event = get_event(ax, xdata=35, ydata=30, button=1) + tool.onmove(event) + tool.release(event) + extents = [int(e) for e in tool.extents] + assert extents == [10, 35, 10, 35] + + # create a square from center + event = get_event(ax, xdata=100, ydata=100, button=1, + key='ctrl+shift') + tool.press(event) + event = get_event(ax, xdata=125, ydata=130, button=1) + tool.onmove(event) + tool.release(event) + extents = [int(e) for e in tool.extents] + assert extents == [70, 130, 70, 130], extents def test_rectangle_handles(): @@ -118,9 +164,6 @@ def onselect(epress, erelease): tool = widgets.RectangleSelector(ax, onselect=onselect, maxdist=10) - event = get_event(ax, xdata=100, ydata=100, button=1) - tool.press(event) - tool.extents = (100, 150, 100, 150) assert tool.corners == ( @@ -138,6 +181,14 @@ def onselect(epress, erelease): tool.release(event) assert tool.extents == (120, 150, 120, 150) + # grab the center and move it + event = get_event(ax, xdata=132, ydata=132) + tool.press(event) + event = get_event(ax, xdata=120, ydata=120) + tool.onmove(event) + tool.release(event) + assert tool.extents == (108, 138, 108, 138) + # create a new rectangle event = get_event(ax, xdata=10, ydata=10) tool.press(event) From e5607136255696558e9d4580ba478cf506755ad9 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 21:17:21 -0600 Subject: [PATCH 08/48] Rename path property to geometry and return consistent results. --- lib/matplotlib/widgets.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 525a3f3494a1..54625036bcc1 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1936,8 +1936,14 @@ def _set_active_handle(self, event): self._extents_on_press = x1, x2, y1, y2 @property - def path(self): - return self.to_draw.get_path() + 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): From 5025ba2aeec3b1c99309e728c757815462479990 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 21:17:57 -0600 Subject: [PATCH 09/48] Add geometry tests. --- lib/matplotlib/tests/test_widgets.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 8738e3697462..f45d5542ea29 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -11,6 +11,7 @@ import matplotlib.pyplot as plt from matplotlib.testing.decorators import cleanup +from numpy.testing import assert_allclose def get_ax(): @@ -94,6 +95,10 @@ def onselect(epress, erelease): event = get_event(ax, xdata=250, ydata=250, button=1) tool.release(event) + assert_allclose(tool.geometry, + [[100., 100, 199, 199, 100], [100, 199, 199, 100, 100]], + err_msg=tool.geometry) + assert ax._got_onselect @@ -155,6 +160,8 @@ def onselect(epress, erelease): extents = [int(e) for e in tool.extents] assert extents == [70, 130, 70, 130], extents + assert tool.geometry.shape == (2, 74) + assert_allclose(tool.geometry[:, 0], [70., 100]) def test_rectangle_handles(): ax = get_ax() From b118294094dc476d28bd9566667839a76f98628d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 19 Dec 2014 21:23:10 -0600 Subject: [PATCH 10/48] Remove extra line. --- lib/matplotlib/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 54625036bcc1..66325cde572f 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1945,7 +1945,6 @@ def geometry(self): return np.array(self.to_draw.get_data()) - class EllipseSelector(RectangleSelector): _shape_klass = Ellipse From b15cf12ebe8ae7d1977aaaec7ccb8868857da52f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 09:53:25 -0600 Subject: [PATCH 11/48] Refactor the event handling methods --- lib/matplotlib/widgets.py | 88 ++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 66325cde572f..8be664e0be75 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1171,12 +1171,12 @@ def update_background(self, event): def connect_default_events(self): """Connect the major canvas events to methods.""" - 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('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.connect_event('key_press_event', self.on_key_press) - self.connect_event('scroll_event', self.on_scroll) + self.connect_event('key_press_event', self._on_key_press) + self.connect_event('scroll_event', self._on_scroll) def ignore(self, event): """return *True* if *event* should be ignored""" @@ -1238,45 +1238,64 @@ def _get_data(self, event): ydata = min(y1, ydata) return xdata, ydata - def press(self, event): - """Button press handler""" + def _press(self, event): + """Button press handler and validator""" if not self.ignore(event): self.eventpress = copy.copy(event) - self._prev_event = copy.copy(event) + self._prev_event = event self.eventpress.xdata, self.eventpress.ydata = ( self._get_data(event)) - return True - return False + if event.key in ['alt', ' ']: + self._moving = True + self.press(event) - def release(self, event): - """Button release event""" + def press(self, event): + """Button press handler""" + pass + + def _release(self, event): + """Button release event handler and validator""" if not self.ignore(event) and self.eventpress is not None: if event.xdata is None: - event = copy.copy(self._prev_event) + event = self._prev_event self.eventrelease = copy.copy(event) self.eventrelease.xdata, self.eventrelease.ydata = ( self._get_data(event)) - return event - else: - return None + self.release(event) - def onmove(self, event): - """Cursor move event""" + def release(self, event): + """Button release event handler""" + pass + + def _onmove(self, event): + """Cursor move event handler and validator""" if not self.ignore(event) and self.eventpress is not None: if event.xdata is None: event = copy.copy(self._prev_event) else: - self._prev_event = copy.copy(event) - return event - else: - return False + self._prev_event = event + self.onmove(event) + + def onmove(self, event): + """Cursor move event handler""" + pass + + def _on_scroll(self, event): + """Mouse scroll event handler and validator""" + if not self.ignore(event): + self.on_scroll(event) def on_scroll(self, event): - """Mouse scroll event""" + """Mouse scroll event handler""" pass + def _on_key_press(self, event): + """Key press event handler and validator""" + if not self.ignore(event): + self.on_key_press(event) + def on_key_press(self, event): - """Key press event""" + """Key press event handler""" pass def set_visible(self, visible): @@ -1407,8 +1426,6 @@ def ignore(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) @@ -1422,7 +1439,7 @@ def press(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 @@ -1454,7 +1471,7 @@ def release(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) self.prev = x, y @@ -1701,8 +1718,6 @@ def __init__(self, ax, onselect, drawtype='patch', def press(self, event): """on button press event""" - if not _SelectorWidget.press(self, event): - return True # make the drawed box/line visible get the click-coordinates, # button, ... self.set_visible(self.visible) @@ -1716,9 +1731,6 @@ def press(self, event): def release(self, event): """on button release event""" - if not _SelectorWidget.release(self, event): - return True - if self.spancoords == 'data': xmin, ymin = self.eventpress.xdata, self.eventpress.ydata xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata @@ -1766,10 +1778,6 @@ def release(self, event): def onmove(self, event): """on motion notify event if box/line is wanted""" - event = _SelectorWidget.onmove(self, event) - if not event: - return True - key = self.eventpress.key or '' # resize an existing shape @@ -2043,8 +2051,6 @@ def onpress(self, event): self.press(event) def press(self, event): - if not _SelectorWidget.press(self, event): - return self.verts = [self._get_data(event)] self.line.set_visible(True) @@ -2052,8 +2058,6 @@ def onrelease(self, event): self.release(event) def release(self, event): - if not _SelectorWidget.release(self, event): - return if self.verts is not None: self.verts.append(self._get_data(event)) self.onselect(self.verts) @@ -2062,7 +2066,7 @@ def release(self, event): self.verts = None def onmove(self, event): - if self.ignore(event) or self.verts is None: + if self.verts is None: return self.verts.append(self._get_data(event)) From e96fe811be1081cfb0e70741459ceea5ea9f5552 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 09:57:06 -0600 Subject: [PATCH 12/48] Update rectangle logic to include '_moving' --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 8be664e0be75..c4ecdf4cce9d 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1919,7 +1919,7 @@ def _set_active_handle(self, event): 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 event.key in ['alt', ' ']: + if self._moving: self.active_handle = 'C' self._extents_on_press = self.extents From 240cfa2d699e95c7dfe2d4cf7e7ee5977329e7de Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 10:37:06 -0600 Subject: [PATCH 13/48] Clean up event handling and add a state attribute --- lib/matplotlib/widgets.py | 62 +++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index c4ecdf4cce9d..23565cd5ee05 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1228,26 +1228,43 @@ def update(self): self.canvas.draw_idle() return False - def _get_data(self, event): - """Limit the xdata and ydata to the axes limits""" + 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) + x0, x1 = self.ax.get_xbound() y0, y1 = self.ax.get_ybound() xdata = max(x0, event.xdata) - xdata = min(x1, xdata) + event.xdata = min(x1, xdata) ydata = max(y0, event.ydata) - ydata = min(y1, ydata) - return xdata, ydata + event.ydata = min(y1, ydata) + + self._prev_event = event + return event def _press(self, event): """Button press handler and validator""" if not self.ignore(event): - self.eventpress = copy.copy(event) + event = self._clean_event(event) + self.eventpress = event self._prev_event = event - self.eventpress.xdata, self.eventpress.ydata = ( - self._get_data(event)) - if event.key in ['alt', ' ']: - self._moving = True - self.press(event) + + self.state = [] + if 'alt' in event.key or event.key == ' ': + self.state.append('move') + if 'shift' in event.key: + self.state.append('square') + if 'ctrl' in event.key or 'control' in event.key: + self.state.append('center') + return event def press(self, event): """Button press handler""" @@ -1255,12 +1272,9 @@ def press(self, event): def _release(self, event): """Button release event handler and validator""" - if not self.ignore(event) and self.eventpress is not None: - if event.xdata is None: - event = self._prev_event - self.eventrelease = copy.copy(event) - self.eventrelease.xdata, self.eventrelease.ydata = ( - self._get_data(event)) + if not self.ignore(event): + event = self._clean_event(event) + self.eventrelease = event self.release(event) def release(self, event): @@ -1269,11 +1283,8 @@ def release(self, event): def _onmove(self, event): """Cursor move event handler and validator""" - if not self.ignore(event) and self.eventpress is not None: - if event.xdata is None: - event = copy.copy(self._prev_event) - else: - self._prev_event = event + if not self.ignore(event): + event = self._clean_event(event) self.onmove(event) def onmove(self, event): @@ -1778,7 +1789,6 @@ def release(self, event): def onmove(self, event): """on motion notify event if box/line is wanted""" - key = self.eventpress.key or '' # resize an existing shape if self.active_handle and not self.active_handle == 'C': @@ -1789,7 +1799,7 @@ def onmove(self, event): y2 = event.ydata # move existing shape - elif self.active_handle == 'C': + elif self.active_handle == 'C' or 'move' in self.state: x1, x2, y1, y2 = self._extents_on_press dx = event.xdata - self.eventpress.xdata dy = event.ydata - self.eventpress.ydata @@ -1806,7 +1816,7 @@ def onmove(self, event): dy = (event.ydata - center[1]) / 2. # square shape - if 'shift' in key: + 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: @@ -1818,7 +1828,7 @@ def onmove(self, event): dy *= maxd / abs(dy_pix) # from center - if key == 'control' or key == 'ctrl+shift': + if 'center' in self.state: dx *= 2 dy *= 2 From 7915db8c28b0544606f444fcf2077ab85c56d6d2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 11:00:24 -0600 Subject: [PATCH 14/48] Add state handling to onmove --- lib/matplotlib/widgets.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 23565cd5ee05..82184d13a24a 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1285,6 +1285,15 @@ def _onmove(self, event): """Cursor move event handler and validator""" if not self.ignore(event): event = self._clean_event(event) + # update the state + if 'move' in self.state: + self.state = ['move'] + else: + self.state = [] + if 'shift' in event.key: + self.state.append('square') + if 'ctrl' in event.key or 'control' in event.key: + self.state.append('center') self.onmove(event) def onmove(self, event): From f7a9222f2e5b65dbb9d690096fd4ed30d8771d6b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 13:27:38 -0600 Subject: [PATCH 15/48] More "state" refactoring --- lib/matplotlib/widgets.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 82184d13a24a..20cbecd32498 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1156,6 +1156,7 @@ def __init__(self, ax, onselect, useblit=False, button=None): # will save the data (pos. at mouserelease) self.eventrelease = None self._prev_event = None + self.state = [] def set_active(self, active): AxesWidget.set_active(self, active) @@ -1258,13 +1259,14 @@ def _press(self, event): self._prev_event = event self.state = [] - if 'alt' in event.key or event.key == ' ': + key = event.key or '' + if 'alt' in key or key == ' ': self.state.append('move') - if 'shift' in event.key: + if 'shift' in key: self.state.append('square') - if 'ctrl' in event.key or 'control' in event.key: + if 'ctrl' in key or 'control' in key: self.state.append('center') - return event + self.press(event) def press(self, event): """Button press handler""" @@ -1283,16 +1285,17 @@ def release(self, event): def _onmove(self, event): """Cursor move event handler and validator""" - if not self.ignore(event): + if not self.ignore(event) and self.eventpress: event = self._clean_event(event) # update the state if 'move' in self.state: self.state = ['move'] else: self.state = [] - if 'shift' in event.key: + key = event.key or '' + if 'shift' in key: self.state.append('square') - if 'ctrl' in event.key or 'control' in event.key: + if 'ctrl' in key or 'control' in key: self.state.append('center') self.onmove(event) @@ -1732,6 +1735,8 @@ def __init__(self, ax, onselect, drawtype='patch', 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] @@ -1797,8 +1802,7 @@ def release(self, event): return False def onmove(self, event): - """on motion notify event if box/line is wanted""" - + """on motion notify event if box/line is wanted""" # resize an existing shape if self.active_handle and not self.active_handle == 'C': x1, x2, y1, y2 = self._extents_on_press @@ -1913,8 +1917,8 @@ def draw_shape(self, extents): x0, x1, y0, y1 = extents xmin, xmax = sorted([x0, x1]) ymin, ymax = sorted([y0, y1]) - xlim = self.ax.get_xlim() - ylim = self.ax.get_ylim() + xlim = sorted(self.ax.get_xlim()) + ylim = sorted(self.ax.get_ylim()) xmin = max(xlim[0], xmin) ymin = max(ylim[0], ymin) @@ -1937,10 +1941,10 @@ def _set_active_handle(self, event): 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 self._moving: + + if 'move' in self.state: self.active_handle = 'C' - self._extents_on_press = self.extents + self._extents_on_press = self.extents # Set active handle as closest handle, if mouse click is close enough. elif m_dist < self.maxdist * 2: From 7c04e257a0b5539c6984ecf646b42274057268a8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 14:13:32 -0600 Subject: [PATCH 16/48] Add key release and continue refactoring state handling --- lib/matplotlib/widgets.py | 42 +++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 20cbecd32498..5f6da11b8b01 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1156,7 +1156,7 @@ def __init__(self, ax, onselect, useblit=False, button=None): # will save the data (pos. at mouserelease) self.eventrelease = None self._prev_event = None - self.state = [] + self.state = set() def set_active(self, active): AxesWidget.set_active(self, active) @@ -1177,6 +1177,7 @@ 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): @@ -1257,15 +1258,9 @@ def _press(self, event): event = self._clean_event(event) self.eventpress = event self._prev_event = event - - self.state = [] key = event.key or '' if 'alt' in key or key == ' ': - self.state.append('move') - if 'shift' in key: - self.state.append('square') - if 'ctrl' in key or 'control' in key: - self.state.append('center') + self.state.add('move') self.press(event) def press(self, event): @@ -1287,16 +1282,6 @@ def _onmove(self, event): """Cursor move event handler and validator""" if not self.ignore(event) and self.eventpress: event = self._clean_event(event) - # update the state - if 'move' in self.state: - self.state = ['move'] - else: - self.state = [] - key = event.key or '' - if 'shift' in key: - self.state.append('square') - if 'ctrl' in key or 'control' in key: - self.state.append('center') self.onmove(event) def onmove(self, event): @@ -1314,13 +1299,32 @@ def on_scroll(self, event): def _on_key_press(self, event): """Key press event handler and validator""" - if not self.ignore(event): + if self.active: + key = event.key or '' + if 'shift' in key: + self.state.add('square') + if 'ctrl' in key or 'control' in key: + self.state.add('center') self.on_key_press(event) def on_key_press(self, event): """Key press event handler""" pass + def _on_key_release(self, event): + """Key release event handler and validator""" + if self.active: + key = event.key or '' + if 'shift' in key: + self.state.discard('square') + if 'ctrl' in key or 'control' in key: + self.state.discard('center') + self.on_key_release(event) + + def on_key_release(self, event): + """Key release event handler""" + pass + def set_visible(self, visible): """ Set the visibility of our artists """ self.visible = visible From cf491db131d19d0f7caad79334851528d783d39e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 14:38:22 -0600 Subject: [PATCH 17/48] Recreate the _get_data method and more event refactoring --- lib/matplotlib/widgets.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 5f6da11b8b01..14d954a5e517 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1230,6 +1230,18 @@ def update(self): self.canvas.draw_idle() return False + def _get_data(self, event): + """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) + xdata = min(x1, xdata) + ydata = max(y0, event.ydata) + ydata = min(y1, ydata) + return xdata, ydata + def _clean_event(self, event): """Clean up an event @@ -1241,14 +1253,8 @@ def _clean_event(self, event): event = self._prev_event else: event = copy.copy(event) - - x0, x1 = self.ax.get_xbound() - y0, y1 = self.ax.get_ybound() - xdata = max(x0, event.xdata) - event.xdata = min(x1, xdata) - ydata = max(y0, event.ydata) - event.ydata = min(y1, ydata) - + event.xdata, event.ydata = self._get_data(event) + self._prev_event = event return event @@ -1269,10 +1275,11 @@ def press(self, event): def _release(self, event): """Button release event handler and validator""" - if not self.ignore(event): + if not self.ignore(event) and self.eventpress: event = self._clean_event(event) self.eventrelease = event self.release(event) + self.state.discard('move') def release(self, event): """Button release event handler""" @@ -1840,9 +1847,9 @@ def onmove(self, event): return maxd = max(abs(dx_pix), abs(dy_pix)) if abs(dx_pix) < maxd: - dx *= maxd / abs(dx_pix) + dx *= maxd / (abs(dx_pix) + 1e-6) if abs(dy_pix) < maxd: - dy *= maxd / abs(dy_pix) + dy *= maxd / (abs(dy_pix) + 1e-6) # from center if 'center' in self.state: From 7ece2fa1475011ec6f5e5c978545e1042700c621 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 20 Dec 2014 14:38:53 -0600 Subject: [PATCH 18/48] Refactor the event creator to call the private methods --- lib/matplotlib/tests/test_widgets.py | 110 ++++++++++++--------------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index f45d5542ea29..532cd56f0e5b 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -22,7 +22,7 @@ def get_ax(): return ax -def get_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): +def do_event(tool, etype, button=1, xdata=0, ydata=0, key=None, step=1): """ *name* the event name @@ -61,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 @@ -70,7 +71,9 @@ 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, '_%s' % etype) + func(event) @cleanup @@ -85,15 +88,11 @@ def onselect(epress, erelease): 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=199, ydata=199, 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) assert_allclose(tool.geometry, [[100., 100, 199, 199, 100], [100, 199, 199, 100, 100]], @@ -123,42 +122,43 @@ def onselect(epress, erelease): tool.extents = (100, 150, 100, 150) # drag the rectangle - event = get_event(ax, xdata=10, ydata=10, button=1, + do_event(tool, 'press', xdata=10, ydata=10, button=1, key='alt') - tool.press(event) - event = get_event(ax, xdata=30, ydata=30, button=1) - tool.onmove(event) - tool.release(event) + 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) # create from center - event = get_event(ax, xdata=100, ydata=100, button=1, + 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') - tool.press(event) - event = get_event(ax, xdata=125, ydata=125, button=1) - tool.onmove(event) - tool.release(event) - assert tool.extents == (75, 125, 75, 125) + assert tool.extents == (75, 125, 75, 125), tool.extents # create a square - event = get_event(ax, xdata=10, ydata=10, button=1, + 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') - tool.press(event) - event = get_event(ax, xdata=35, ydata=30, button=1) - tool.onmove(event) - tool.release(event) extents = [int(e) for e in tool.extents] - assert extents == [10, 35, 10, 35] + assert extents == [10, 35, 10, 34] # create a square from center - event = get_event(ax, xdata=100, ydata=100, button=1, + 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') - tool.press(event) - event = get_event(ax, xdata=125, ydata=130, button=1) - tool.onmove(event) - tool.release(event) extents = [int(e) for e in tool.extents] - assert extents == [70, 130, 70, 130], extents + assert extents == [70, 129, 70, 130], extents assert tool.geometry.shape == (2, 74) assert_allclose(tool.geometry[:, 0], [70., 100]) @@ -181,27 +181,21 @@ def onselect(epress, erelease): assert tool.extents == (100, 150, 100, 150) # grab a corner and move it - event = get_event(ax, xdata=100, ydata=100) - tool.press(event) - event = get_event(ax, xdata=120, ydata=120) - tool.onmove(event) - tool.release(event) + 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 - event = get_event(ax, xdata=132, ydata=132) - tool.press(event) - event = get_event(ax, xdata=120, ydata=120) - tool.onmove(event) - tool.release(event) + 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 - event = get_event(ax, xdata=10, ydata=10) - tool.press(event) - event = get_event(ax, xdata=100, ydata=100) - tool.onmove(event) - tool.release(event) + 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) @@ -223,14 +217,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 @@ -253,14 +242,9 @@ def onselect(verts): 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 From 59ccd8170b0fc23fb94dfc47621c15109a2cfc9b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 21 Dec 2014 17:31:43 -0600 Subject: [PATCH 19/48] Preserve existing api for event handler functions --- lib/matplotlib/tests/test_widgets.py | 2 +- lib/matplotlib/widgets.py | 68 ++++++++++++++-------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 532cd56f0e5b..e16cd5d359ea 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -72,7 +72,7 @@ def do_event(tool, etype, button=1, xdata=0, ydata=0, key=None, step=1): event.guiEvent = None event.name = 'Custom' - func = getattr(tool, '_%s' % etype) + func = getattr(tool, etype) func(event) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 14d954a5e517..73dcb33fe382 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1140,11 +1140,11 @@ def __init__(self, ax, onselect, useblit=False, button=None): self.visible = True self.onselect = onselect + self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() self.background = None self.artists = [] - self.useblit = useblit and self.canvas.supports_blit if isinstance(button, int): self.validButtons = [button] @@ -1172,13 +1172,13 @@ def update_background(self, event): def connect_default_events(self): """Connect the major canvas events to methods.""" - 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('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.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) + 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""" @@ -1258,7 +1258,7 @@ def _clean_event(self, event): self._prev_event = event return event - def _press(self, event): + def press(self, event): """Button press handler and validator""" if not self.ignore(event): event = self._clean_event(event) @@ -1267,44 +1267,44 @@ def _press(self, event): key = event.key or '' if 'alt' in key or key == ' ': self.state.add('move') - self.press(event) + self._press(event) - def press(self, event): + def _press(self, event): """Button press handler""" pass - def _release(self, event): + def release(self, 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._release(event) self.state.discard('move') - def release(self, event): + def _release(self, event): """Button release event handler""" pass - def _onmove(self, event): + def onmove(self, event): """Cursor move event handler and validator""" if not self.ignore(event) and self.eventpress: event = self._clean_event(event) - self.onmove(event) + self._onmove(event) - def onmove(self, event): + def _onmove(self, event): """Cursor move event handler""" pass - def _on_scroll(self, event): + def on_scroll(self, event): """Mouse scroll event handler and validator""" if not self.ignore(event): - self.on_scroll(event) + self._on_scroll(event) - def on_scroll(self, event): + def _on_scroll(self, event): """Mouse scroll event handler""" pass - def _on_key_press(self, event): + def on_key_press(self, event): """Key press event handler and validator""" if self.active: key = event.key or '' @@ -1312,13 +1312,13 @@ def _on_key_press(self, event): self.state.add('square') if 'ctrl' in key or 'control' in key: self.state.add('center') - self.on_key_press(event) + self._on_key_press(event) - def on_key_press(self, event): + def _on_key_press(self, event): """Key press event handler""" pass - def _on_key_release(self, event): + def on_key_release(self, event): """Key release event handler and validator""" if self.active: key = event.key or '' @@ -1326,9 +1326,9 @@ def _on_key_release(self, event): self.state.discard('square') if 'ctrl' in key or 'control' in key: self.state.discard('center') - self.on_key_release(event) + self._on_key_release(event) - def on_key_release(self, event): + def _on_key_release(self, event): """Key release event handler""" pass @@ -1458,7 +1458,7 @@ 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""" self.rect.set_visible(self.visible) if self.span_stays: @@ -1471,7 +1471,7 @@ def press(self, event): self.pressv = ydata return False - def release(self, event): + def _release(self, event): """on button release event""" if self.pressv is None: return @@ -1503,7 +1503,7 @@ 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: return @@ -1752,7 +1752,7 @@ def __init__(self, ax, onselect, drawtype='patch', self._corner_handles.artist, self._edge_handles.artist] - def press(self, event): + def _press(self, event): """on button press event""" # make the drawed box/line visible get the click-coordinates, # button, ... @@ -1765,7 +1765,7 @@ def press(self, event): self.set_visible(self.visible) - def release(self, event): + def _release(self, event): """on button release event""" if self.spancoords == 'data': xmin, ymin = self.eventpress.xdata, self.eventpress.ydata @@ -1812,7 +1812,7 @@ def release(self, event): return False - def onmove(self, event): + def _onmove(self, event): """on motion notify event if box/line is wanted""" # resize an existing shape if self.active_handle and not self.active_handle == 'C': @@ -2084,14 +2084,14 @@ def __init__(self, ax, onselect=None, useblit=True, lineprops=None, def onpress(self, event): self.press(event) - def press(self, event): + 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): + def _release(self, event): if self.verts is not None: self.verts.append(self._get_data(event)) self.onselect(self.verts) @@ -2099,7 +2099,7 @@ def release(self, event): self.line.set_visible(False) self.verts = None - def onmove(self, event): + def _onmove(self, event): if self.verts is None: return self.verts.append(self._get_data(event)) From a033e8e078c07030eba63ea2e9ee22a1e4080503 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 23 Dec 2014 21:28:27 -0600 Subject: [PATCH 20/48] Preserve event return values --- lib/matplotlib/widgets.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 73dcb33fe382..e2790d4f664f 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1268,6 +1268,8 @@ def press(self, event): if 'alt' in key or key == ' ': self.state.add('move') self._press(event) + return True + return False def _press(self, event): """Button press handler""" @@ -1280,6 +1282,8 @@ def release(self, event): self.eventrelease = event self._release(event) self.state.discard('move') + return True + return False def _release(self, event): """Button release event handler""" @@ -1290,6 +1294,8 @@ def onmove(self, event): 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""" From 2923f97c3bb23a64d0a2cd4204d17a9c2f66bb16 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 23 Dec 2014 21:45:34 -0600 Subject: [PATCH 21/48] Add cleanup to tests and fix default rect prop --- lib/matplotlib/tests/test_widgets.py | 3 +++ lib/matplotlib/widgets.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index e16cd5d359ea..989a43b1cf6a 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -110,6 +110,7 @@ def test_rectangle_selector(): check_rectangle(rectprops=dict(fill=True)) +@cleanup def test_ellipse(): """For ellipse, test out the key modifiers""" ax = get_ax() @@ -163,6 +164,8 @@ def onselect(epress, erelease): assert tool.geometry.shape == (2, 74) assert_allclose(tool.geometry[:, 0], [70., 100]) + +@cleanup def test_rectangle_handles(): ax = get_ax() diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e2790d4f664f..ab522c82bd68 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1737,7 +1737,7 @@ def __init__(self, ax, onselect, drawtype='patch', if rectprops is None: props = dict(mec='r') else: - props = dict(mec=rectprops.get('edgecolor', 'k')) + 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, From 6549538a40e35d0f9a953ea0b8261b3987546762 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 24 Dec 2014 05:59:32 -0600 Subject: [PATCH 22/48] Pep8 fixes --- lib/matplotlib/tests/test_widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 989a43b1cf6a..47e45c89391a 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -187,13 +187,13 @@ def onselect(epress, erelease): 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) + 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) + assert tool.extents == (108, 138, 108, 138) # create a new rectangle do_event(tool, 'press', xdata=10, ydata=10) From 491276eecec4d6d04fe17cd96de1a9d3d52c8062 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 17 Jan 2015 16:07:08 -0600 Subject: [PATCH 23/48] Fix bug in SpanSelector when blit=True --- 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 ab522c82bd68..412995498f2c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1456,9 +1456,8 @@ 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""" From 744308cad3a1e74b086885e0bf2918a20357378c Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 17 Jan 2015 19:06:42 -0600 Subject: [PATCH 24/48] Clear events after valid release --- lib/matplotlib/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 412995498f2c..153f30d183ba 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1281,6 +1281,8 @@ def release(self, event): event = self._clean_event(event) self.eventrelease = event self._release(event) + self.eventpress = None + self.eventrelease = None self.state.discard('move') return True return False From a7518b32d06f034bd1e375ab21660ca1574cc81e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 17 Jan 2015 19:38:29 -0600 Subject: [PATCH 25/48] Do not attempt to draw artists if ax is invisible --- lib/matplotlib/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 153f30d183ba..cf498f2afb16 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1218,6 +1218,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) From 77dc52bd4d3bc56f63a711d52f06cc8e6de8a311 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 17 Jan 2015 19:43:36 -0600 Subject: [PATCH 26/48] Ignore events when axes are invisible --- lib/matplotlib/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index cf498f2afb16..d3b316384628 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1182,7 +1182,7 @@ def connect_default_events(self): 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 From 48ebf30bd36014eb5912112ba61b756fc91a82af Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 20 Feb 2015 21:14:53 -0600 Subject: [PATCH 27/48] Fix span selector onmove when we exit axes --- lib/matplotlib/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index d3b316384628..d49e1f27e237 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1518,6 +1518,9 @@ def _onmove(self, 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 From 3ffd7c01900d9307b40656595c32a667e9acd5b7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 18 Aug 2015 22:40:30 -0500 Subject: [PATCH 28/48] Restore previous behaviour and allow escape to clear the current selection --- lib/matplotlib/widgets.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index d49e1f27e237..a9931d22c5fc 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1319,6 +1319,11 @@ def on_key_press(self, event): """Key press event handler and validator""" if self.active: key = event.key or '' + if key == 'escape': + for artist in self.artists: + artist.set_visible(False) + self.update() + return if 'shift' in key: self.state.add('square') if 'ctrl' in key or 'control' in key: @@ -1656,7 +1661,8 @@ def toggle_selector(event): def __init__(self, ax, onselect, drawtype='patch', minspanx=None, minspany=None, useblit=False, lineprops=None, rectprops=None, spancoords='data', - button=None, maxdist=10, marker_props=None): + button=None, maxdist=10, marker_props=None, + interactive=False): """ Create a selector in *ax*. When a selection is made, clear @@ -1683,7 +1689,7 @@ def __init__(self, ax, onselect, drawtype='patch', Use *drawtype* if you want the mouse to draw a line, a box or nothing between click and actual position by setting - ``drawtype = 'line'``, ``drawtype='box'`` or ``drawtype = 'none'``. + ``drawtype = 'line'``, ``drawtype='patch'`` or ``drawtype = 'none'``. *spancoords* is one of 'data' or 'pixels'. If 'data', *minspanx* and *minspanx* will be interpreted in the same coordinates as @@ -1698,12 +1704,16 @@ def __init__(self, ax, onselect, drawtype='patch', 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. """ _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, - button=button) + button=button) self.to_draw = None self.visible = True + self.interactive = interactive if drawtype == 'box': # backwards compatibility drawtype = 'patch' @@ -1765,14 +1775,18 @@ def __init__(self, ax, onselect, drawtype='patch', self._corner_handles.artist, self._edge_handles.artist] + if not self.interactive: + self.artists = [self.to_draw] + def _press(self, event): """on button press event""" # make the drawed box/line visible get the click-coordinates, # button, ... - self.set_visible(self.visible) - self._set_active_handle(event) + if self.interactive and self.to_draw.get_visible(): + self._set_active_handle(event) - if self.active_handle is None: + self.set_visible(self.visible) + if self.active_handle is None or not self.interactive: # Clear previous rectangle before drawing new rectangle. self.update() @@ -1780,6 +1794,9 @@ def _press(self, event): 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 xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata From e8029913cca52bc53d909fe2428d66fa5af9bb10 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 19 Aug 2015 14:38:35 -0500 Subject: [PATCH 29/48] Fix failing tests --- lib/matplotlib/tests/test_widgets.py | 2 +- lib/matplotlib/widgets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 47e45c89391a..660da9182cae 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -173,7 +173,7 @@ def onselect(epress, erelease): pass tool = widgets.RectangleSelector(ax, onselect=onselect, - maxdist=10) + maxdist=10, interactive=True) tool.extents = (100, 150, 100, 150) assert tool.corners == ( diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index a9931d22c5fc..b38b1e61db8d 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1853,7 +1853,7 @@ def _onmove(self, event): y2 = event.ydata # move existing shape - elif self.active_handle == 'C' or 'move' in self.state: + elif 'move' in self.state: x1, x2, y1, y2 = self._extents_on_press dx = event.xdata - self.eventpress.xdata dy = event.ydata - self.eventpress.ydata From af6e1b46e888dc8aafba7f76a599a52f137b105e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 20 Aug 2015 09:28:55 -0500 Subject: [PATCH 30/48] Fix handling of center handle --- lib/matplotlib/tests/test_widgets.py | 4 ++-- lib/matplotlib/widgets.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 660da9182cae..866626859f91 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -119,7 +119,7 @@ def onselect(epress, erelease): pass tool = widgets.EllipseSelector(ax, onselect=onselect, - maxdist=10) + maxdist=10, interactive=True) tool.extents = (100, 150, 100, 150) # drag the rectangle @@ -193,7 +193,7 @@ def onselect(epress, erelease): 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) + assert tool.extents == (108, 138, 108, 138), tool.extents # create a new rectangle do_event(tool, 'press', xdata=10, ydata=10) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index b38b1e61db8d..f0d76e391e3c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1778,6 +1778,8 @@ def __init__(self, ax, onselect, drawtype='patch', if not self.interactive: self.artists = [self.to_draw] + self._extents_on_press = None + def _press(self, event): """on button press event""" # make the drawed box/line visible get the click-coordinates, @@ -1853,7 +1855,8 @@ def _onmove(self, event): y2 = event.ydata # move existing shape - elif 'move' in self.state: + 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 From f5666a4714142bc44dff3ec5f909276c4afe11db Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 20 Aug 2015 09:32:15 -0500 Subject: [PATCH 31/48] Remove debug print --- lib/matplotlib/tests/test_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 866626859f91..ee0497e61c3b 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -193,7 +193,7 @@ def onselect(epress, erelease): 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), tool.extents + assert tool.extents == (108, 138, 108, 138) # create a new rectangle do_event(tool, 'press', xdata=10, ydata=10) From 545a727d41cf062bacedab8bf49162f7107b8145 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 22 Aug 2015 15:49:33 -0400 Subject: [PATCH 32/48] STY: PEP8 Mostly trailing whitespace --- lib/matplotlib/widgets.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index f0d76e391e3c..cd62fe3924f4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -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): @@ -1257,7 +1257,7 @@ def _clean_event(self, event): else: event = copy.copy(event) event.xdata, event.ydata = self._get_data(event) - + self._prev_event = event return event @@ -1293,7 +1293,7 @@ def release(self, event): def _release(self, event): """Button release event handler""" pass - + def onmove(self, event): """Cursor move event handler and validator""" if not self.ignore(event) and self.eventpress: @@ -1392,7 +1392,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 @@ -1413,7 +1413,7 @@ 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) @@ -1845,7 +1845,7 @@ def _release(self, event): return False def _onmove(self, event): - """on motion notify event if box/line is wanted""" + """on motion notify event if box/line is wanted""" # resize an existing shape if self.active_handle and not self.active_handle == 'C': x1, x2, y1, y2 = self._extents_on_press @@ -1985,10 +1985,10 @@ def _set_active_handle(self, event): 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 + self._extents_on_press = self.extents # Set active handle as closest handle, if mouse click is close enough. elif m_dist < self.maxdist * 2: From feb58fef166916ee702a32069bdd304dd5a8e815 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 22 Aug 2015 15:50:49 -0400 Subject: [PATCH 33/48] FIX: do not call draw_idle if blitting - make sure all blitted artists are marked as 'animated' - use `self.update` instead of `canvas.draw_idle` in extent setter --- lib/matplotlib/widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index cd62fe3924f4..e21633e48060 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1418,6 +1418,8 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, if rectprops is None: rectprops = dict(facecolor='red', alpha=0.5) + rectprops['animated'] = useblit + if direction not in ['horizontal', 'vertical']: msg = "direction must be in [ 'horizontal' | 'vertical' ]" raise ValueError(msg) @@ -1726,6 +1728,7 @@ def __init__(self, ax, onselect, drawtype='patch', if rectprops is None: rectprops = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) + rectprops['animated'] = useblit self.rectprops = rectprops self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False, **self.rectprops) @@ -1734,6 +1737,7 @@ def __init__(self, ax, onselect, drawtype='patch', if lineprops is None: lineprops = dict(color='black', linestyle='-', linewidth=2, alpha=0.5) + lineprops['animated'] = useblit self.lineprops = lineprops self.to_draw = Line2D([0, 0, 0, 0, 0], [0, 0, 0, 0, 0], visible=False, **self.lineprops) @@ -1954,8 +1958,7 @@ def extents(self, extents): self._edge_handles.set_data(*self.edge_centers) self._center_handle.set_data(*self.center) self.set_visible(self.visible) - - self.canvas.draw_idle() + self.update() def draw_shape(self, extents): x0, x1, y0, y1 = extents From 7bd67d2f1468e5f918ead97dbef5146cf31ffaf5 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 22 Aug 2015 15:52:22 -0400 Subject: [PATCH 34/48] DOC: make rectangle demo use handles --- examples/widgets/rectangle_selector.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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() From b60a84d960363a4f7afa0a531adae3cd01449b97 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 22 Aug 2015 16:49:12 -0500 Subject: [PATCH 35/48] Add docstring for EllipseSelector and use print() statements --- lib/matplotlib/widgets.py | 47 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e21633e48060..d43e9dfa93d8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1634,17 +1634,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) @@ -2024,7 +2024,42 @@ def geometry(self): 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): From 840a671435d3afcb834cdd2e60d327288279ef90 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 23 Aug 2015 17:25:27 -0500 Subject: [PATCH 36/48] Revert to old draw types and add docs about key modifiers --- examples/widgets/rectangle_selector.py | 2 +- lib/matplotlib/widgets.py | 32 +++++++++++++++----------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/examples/widgets/rectangle_selector.py b/examples/widgets/rectangle_selector.py index eaedc686ea31..287c3c848e01 100644 --- a/examples/widgets/rectangle_selector.py +++ b/examples/widgets/rectangle_selector.py @@ -43,7 +43,7 @@ def toggle_selector(event): # drawtype is 'box' or 'line' or 'none' toggle_selector.RS = RectangleSelector(current_ax, line_select_callback, - drawtype='box', useblit=True, + drawtype='line', useblit=True, button=[1, 3], # don't use middle button minspanx=5, minspany=5, spancoords='pixels', diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index d43e9dfa93d8..9aa6d16effe6 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1660,7 +1660,7 @@ def toggle_selector(event): _shape_klass = Rectangle - def __init__(self, ax, onselect, drawtype='patch', + def __init__(self, ax, onselect, drawtype='box', minspanx=None, minspany=None, useblit=False, lineprops=None, rectprops=None, spancoords='data', button=None, maxdist=10, marker_props=None, @@ -1691,7 +1691,7 @@ def __init__(self, ax, onselect, drawtype='patch', Use *drawtype* if you want the mouse to draw a line, a box or nothing between click and actual position by setting - ``drawtype = 'line'``, ``drawtype='patch'`` or ``drawtype = 'none'``. + ``drawtype = 'line'``, ``drawtype='box'`` or ``drawtype = 'none'``. *spancoords* is one of 'data' or 'pixels'. If 'data', *minspanx* and *minspanx* will be interpreted in the same coordinates as @@ -1708,7 +1708,15 @@ def __init__(self, ax, onselect, drawtype='patch', 3 = right mouse button *interactive* will draw a set of handles and allow you interact - with the widget after it is drawn. + with the widget after it is drawn. Holding the 'space' while dragging + the mouse will move the object. + + Modifier keys + ------------- + One can use modifier keys to affect the way the shape is drawn. + Hold the 'Ctrl' key to center the rectangle on the initial position. + Hold the 'Shift' key to force the shape to be square. + These can be combined. """ _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, button=button) @@ -1717,14 +1725,11 @@ def __init__(self, ax, onselect, drawtype='patch', self.visible = True self.interactive = interactive - if drawtype == 'box': # backwards compatibility - drawtype = 'patch' - if drawtype == 'none': drawtype = 'line' # draw a line but make it self.visible = False # invisible - if drawtype == 'patch': + if drawtype == 'box': if rectprops is None: rectprops = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) @@ -1739,7 +1744,7 @@ def __init__(self, ax, onselect, drawtype='patch', linewidth=2, alpha=0.5) lineprops['animated'] = useblit self.lineprops = lineprops - self.to_draw = Line2D([0, 0, 0, 0, 0], [0, 0, 0, 0, 0], visible=False, + self.to_draw = Line2D([0, 0], [0, 0], visible=False, **self.lineprops) self.ax.add_line(self.to_draw) @@ -1905,7 +1910,7 @@ def _onmove(self, event): @property def _rect_bbox(self): - if self.drawtype == 'patch': + if self.drawtype == 'box': x0 = self.to_draw.get_x() y0 = self.to_draw.get_y() width = self.to_draw.get_width() @@ -1972,15 +1977,14 @@ def draw_shape(self, extents): xmax = min(xmax, xlim[1]) ymax = min(ymax, ylim[1]) - if self.drawtype == 'patch': + 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, xmin, xmax, xmax, xmin], - [ymin, ymax, ymax, ymin, ymin]) + 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""" @@ -2070,7 +2074,7 @@ def draw_shape(self, extents): a = (xmax - xmin) / 2. b = (ymax - ymin) / 2. - if self.drawtype == 'patch': + if self.drawtype == 'box': self.to_draw.center = center self.to_draw.width = 2 * a self.to_draw.height = 2 * b @@ -2082,7 +2086,7 @@ def draw_shape(self, extents): @property def _rect_bbox(self): - if self.drawtype == 'patch': + if self.drawtype == 'box': x, y = self.to_draw.center width = self.to_draw.width height = self.to_draw.height From 2a11f5f4bc40ef2ddf6e744564526e43aa595573 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 23 Aug 2015 17:26:11 -0500 Subject: [PATCH 37/48] Revert change to the example --- examples/widgets/rectangle_selector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/widgets/rectangle_selector.py b/examples/widgets/rectangle_selector.py index 287c3c848e01..eaedc686ea31 100644 --- a/examples/widgets/rectangle_selector.py +++ b/examples/widgets/rectangle_selector.py @@ -43,7 +43,7 @@ def toggle_selector(event): # drawtype is 'box' or 'line' or 'none' toggle_selector.RS = RectangleSelector(current_ax, line_select_callback, - drawtype='line', useblit=True, + drawtype='box', useblit=True, button=[1, 3], # don't use middle button minspanx=5, minspany=5, spancoords='pixels', From 8da7985553585af349decbfb76ad6b9146f5ce71 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 23 Aug 2015 17:29:04 -0500 Subject: [PATCH 38/48] Remove extra draw_idle trigger --- lib/matplotlib/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9aa6d16effe6..e0403f725883 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1796,7 +1796,6 @@ def _press(self, event): if self.interactive and self.to_draw.get_visible(): self._set_active_handle(event) - self.set_visible(self.visible) if self.active_handle is None or not self.interactive: # Clear previous rectangle before drawing new rectangle. self.update() From cfe4df18ed0e73f533d2458efbe6965693aeea26 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 23 Aug 2015 17:31:11 -0500 Subject: [PATCH 39/48] Update the docstring --- lib/matplotlib/widgets.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index e0403f725883..974a5eadcd60 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1711,12 +1711,11 @@ def __init__(self, ax, onselect, drawtype='box', with the widget after it is drawn. Holding the 'space' while dragging the mouse will move the object. - Modifier keys - ------------- - One can use modifier keys to affect the way the shape is drawn. - Hold the 'Ctrl' key to center the rectangle on the initial position. - Hold the 'Shift' key to force the shape to be square. - These can be combined. + Keyboard modifiers: + Alt or Space moves the existing shape. + Shift makes the shape square. + Ctrl makes the initial point the center of the shape. + Ctrl and shift can be combined. """ _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, button=button) From bc225b69419bbb49f7da239762e7afa26ae7bf95 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 23 Aug 2015 17:32:38 -0500 Subject: [PATCH 40/48] Remove extraneous doc --- lib/matplotlib/widgets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 974a5eadcd60..d6226c4dee87 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1708,8 +1708,7 @@ def __init__(self, ax, onselect, drawtype='box', 3 = right mouse button *interactive* will draw a set of handles and allow you interact - with the widget after it is drawn. Holding the 'space' while dragging - the mouse will move the object. + with the widget after it is drawn. Keyboard modifiers: Alt or Space moves the existing shape. From 59145aff79d93f0093975ee04246f8760cffdf19 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 23 Aug 2015 18:04:36 -0500 Subject: [PATCH 41/48] Fix failing test --- lib/matplotlib/tests/test_widgets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index ee0497e61c3b..e71c55036a7e 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -94,9 +94,10 @@ def onselect(epress, erelease): # purposely drag outside of axis for release do_event(tool, 'release', xdata=250, ydata=250, button=1) - assert_allclose(tool.geometry, - [[100., 100, 199, 199, 100], [100, 199, 199, 100, 100]], - err_msg=tool.geometry) + 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 From d491ac91ca541c988980688935790ebc821555e2 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 24 Aug 2015 19:28:42 -0500 Subject: [PATCH 42/48] Update docstring, use self.useblit, use space for move --- 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 d6226c4dee87..bbb780132256 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1268,7 +1268,7 @@ def press(self, event): self.eventpress = event self._prev_event = event key = event.key or '' - if 'alt' in key or key == ' ': + if key == ' ': self.state.add('move') self._press(event) return True @@ -1681,7 +1681,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:: @@ -1731,7 +1731,7 @@ def __init__(self, ax, onselect, drawtype='box', if rectprops is None: rectprops = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) - rectprops['animated'] = useblit + rectprops['animated'] = self.useblit self.rectprops = rectprops self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False, **self.rectprops) @@ -1740,7 +1740,7 @@ def __init__(self, ax, onselect, drawtype='box', if lineprops is None: lineprops = dict(color='black', linestyle='-', linewidth=2, alpha=0.5) - lineprops['animated'] = useblit + lineprops['animated'] = self.useblit self.lineprops = lineprops self.to_draw = Line2D([0, 0], [0, 0], visible=False, **self.lineprops) From 8e8e473259c91dab2a031dcf7e9e3957d05e9030 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 26 Aug 2015 08:12:13 -0500 Subject: [PATCH 43/48] Use new state_modifier_keys dictionary --- lib/matplotlib/widgets.py | 42 ++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index bbb780132256..902aed421745 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1135,7 +1135,8 @@ 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 @@ -1143,6 +1144,10 @@ def __init__(self, ax, onselect, useblit=False, button=None): self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() + self.state_modifier_keys = dict(move=' ', clear='escape', + square='shift', center='ctrl') + self.state_modifier_keys.update(state_modifier_keys or {}) + self.background = None self.artists = [] @@ -1268,7 +1273,8 @@ def press(self, event): self.eventpress = event self._prev_event = event key = event.key or '' - if key == ' ': + key = key.replace('control', 'ctrl') + if key == self.state_modifier_keys['move']: self.state.add('move') self._press(event) return True @@ -1319,15 +1325,15 @@ def on_key_press(self, event): """Key press event handler and validator""" if self.active: key = event.key or '' - if key == 'escape': + key = key.replace('control', 'ctrl') + if key == self.state_modifier_keys['clear']: for artist in self.artists: artist.set_visible(False) self.update() return - if 'shift' in key: - self.state.add('square') - if 'ctrl' in key or 'control' in key: - self.state.add('center') + 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): @@ -1664,7 +1670,7 @@ def __init__(self, ax, onselect, drawtype='box', minspanx=None, minspany=None, useblit=False, lineprops=None, rectprops=None, spancoords='data', button=None, maxdist=10, marker_props=None, - interactive=False): + interactive=False, state_modifier_keys=None): """ Create a selector in *ax*. When a selection is made, clear @@ -1710,14 +1716,22 @@ def __init__(self, ax, onselect, drawtype='box', *interactive* will draw a set of handles and allow you interact with the widget after it is drawn. - Keyboard modifiers: - Alt or Space moves the existing shape. - Shift makes the shape square. - Ctrl makes the initial point the center of the shape. - Ctrl and shift can be combined. + *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 From 2ed0419d1da46c2411eef5a64da6560a8af3bb87 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 26 Aug 2015 08:14:00 -0500 Subject: [PATCH 44/48] Clear the active handle when invisible --- lib/matplotlib/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 902aed421745..833c549f383b 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1807,6 +1807,8 @@ def _press(self, event): # button, ... if self.interactive and self.to_draw.get_visible(): self._set_active_handle(event) + else: + self.active_handle = None if self.active_handle is None or not self.interactive: # Clear previous rectangle before drawing new rectangle. From d5b5d86bac7fb77163312df8a2d769d0e2fc8c74 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 26 Aug 2015 08:20:00 -0500 Subject: [PATCH 45/48] Collapse the shape on improper draw to prevent showing the previous shape --- lib/matplotlib/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 833c549f383b..4c4e96b6c8bc 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1846,6 +1846,7 @@ 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 From 87ab1994530d26130cb6e7d0b6af4abe7f322585 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 26 Aug 2015 08:38:55 -0500 Subject: [PATCH 46/48] Fix test using 'alt' key --- lib/matplotlib/tests/test_widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index e71c55036a7e..c9b100e4d933 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -125,10 +125,10 @@ def onselect(epress, erelease): # drag the rectangle do_event(tool, 'press', xdata=10, ydata=10, button=1, - key='alt') + 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) + assert tool.extents == (120, 170, 120, 170), tool.extents # create from center do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1, From 9eaae3230e6f3f98d8833aecc924072b6791af55 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 1 Sep 2015 08:07:55 -0500 Subject: [PATCH 47/48] Clean up and add more docs --- lib/matplotlib/widgets.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 4c4e96b6c8bc..2410d2f69380 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1145,7 +1145,7 @@ def __init__(self, ax, onselect, useblit=False, button=None, self.connect_default_events() self.state_modifier_keys = dict(move=' ', clear='escape', - square='shift', center='ctrl') + square='shift', center='control') self.state_modifier_keys.update(state_modifier_keys or {}) self.background = None @@ -1273,7 +1273,8 @@ def press(self, event): self.eventpress = event self._prev_event = event key = event.key or '' - key = key.replace('control', 'ctrl') + 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) @@ -1322,10 +1323,10 @@ def _on_scroll(self, event): pass def on_key_press(self, event): - """Key press event handler and validator""" + """Key press event handler and validator for all selection widgets""" if self.active: key = event.key or '' - key = key.replace('control', 'ctrl') + key = key.replace('ctrl', 'control') if key == self.state_modifier_keys['clear']: for artist in self.artists: artist.set_visible(False) @@ -1337,7 +1338,8 @@ def on_key_press(self, event): self._on_key_press(event) def _on_key_press(self, event): - """Key press event handler""" + """Key press event handler - use for widget-specific key press actions. + """ pass def on_key_release(self, event): @@ -1424,7 +1426,7 @@ def __init__(self, ax, onselect, direction, minspan=None, useblit=False, if rectprops is None: rectprops = dict(facecolor='red', alpha=0.5) - rectprops['animated'] = useblit + rectprops['animated'] = self.useblit if direction not in ['horizontal', 'vertical']: msg = "direction must be in [ 'horizontal' | 'vertical' ]" @@ -1567,7 +1569,6 @@ def _onmove(self, event): class ToolHandles(object): - """Control handles for canvas tools. Parameters From 819804fda0bc11c36e370b95a3b6b3e1a3368b48 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 1 Sep 2015 08:13:13 -0500 Subject: [PATCH 48/48] Make on_key_release use the state_modifier_keys --- lib/matplotlib/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 2410d2f69380..ab2331e0b0e4 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1346,10 +1346,9 @@ def on_key_release(self, event): """Key release event handler and validator""" if self.active: key = event.key or '' - if 'shift' in key: - self.state.discard('square') - if 'ctrl' in key or 'control' in key: - self.state.discard('center') + 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):