From af8289eea0e8467d9738a9785ade436d3a527181 Mon Sep 17 00:00:00 2001 From: Ian Spielman Date: Sun, 31 Jan 2016 19:47:47 -0500 Subject: [PATCH 1/2] Changes widgets/Cursor: (1) added option so that a right click enables dragging of cursors (2) Changed behavior so cursor starts in the center of the window. (3) Added method self.coord() that returns the coordinates of the cursor. (4) When cursor leaves the axis containing the cursor, the cursor is left at its final position rather than being erased. These changes are designed to make this cursor useful for selecting some point on the figure. Added class Cursors: Similar to Cursor, but many cursors. --- lib/matplotlib/widgets.py | 360 +++++++++++++++++++++++++++++++++----- 1 file changed, 319 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index ad008c4f0d70..d2713dbf3ac2 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -921,49 +921,124 @@ def funchspace(self, val): class Cursor(AxesWidget): """ - A horizontal and vertical line that spans the axes and moves with - the pointer. You can turn off the hline or vline respectively with - the following attributes: + A Cursor: A horizontal and vertical line that spans the axes and + moves with the pointer. + + For the cursor to remain responsive you must keep a reference to it. + + The following attributes are accessible + + *ax* + The :class:`matplotlib.axes.Axes` the cursor renders in. *horizOn* Controls the visibility of the horizontal line *vertOn* - Controls the visibility of the horizontal line - - and the visibility of the cursor itself with the *visible* attribute. + Controls the visibility of the vertical line - For the cursor to remain responsive you must keep a reference to - it. + *visible* + Controls the visibility of the cursor itself """ - def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, - **lineprops): + + def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, + dragging=False, **lineprops): """ - Add a cursor to *ax*. If ``useblit=True``, use the backend- - dependent blitting features for faster updates (GTKAgg - only for now). *lineprops* is a dictionary of line properties. + Parameters + ---------- + ax : matplotlib.axes.Axes + The :class:`matplotlib.axes.Axes` instance the cursor + will be placed into. + + horizOn : boolean + Controls the visibility of the horizontal line. + + vertOn : boolean + Controls the visibility of the vertical line. + + useblit : boolean + use the backend-dependent blitting features for faster + updates (GTKAgg only for now). + + dragging: boolean + drag cursors only when the left mouse button is pressed + + lineprops : dict + a dictionary of line properties. .. plot :: mpl_examples/widgets/cursor.py """ + # TODO: Is the GTKAgg limitation still true? - AxesWidget.__init__(self, ax) + super().__init__(ax) + + self.connect_event('motion_notify_event', self.on_move) + + if dragging: + self.connect_event('button_press_event', self.on_press) + self.connect_event('button_release_event', self.on_release) + self.__pressed = False + else: + self.__pressed = True + - self.connect_event('motion_notify_event', self.onmove) self.connect_event('draw_event', self.clear) self.visible = True self.horizOn = horizOn self.vertOn = vertOn self.useblit = useblit and self.canvas.supports_blit - + if self.useblit: lineprops['animated'] = True - self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops) - self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops) + + self._coord = np.array([ + 0.5*(ax.get_xbound()[0] + ax.get_xbound()[1]), + 0.5*(ax.get_ybound()[0] + ax.get_ybound()[1]) + ]) + + self.lineh = ax.axhline(self._coord[1], visible=False, **lineprops) + self.linev = ax.axvline(self._coord[0], visible=False, **lineprops) self.background = None self.needclear = False + self._draw_cursor() + + def _update(self): + + if self.useblit: + if self.background is not None: + self.canvas.restore_region(self.background) + self.ax.draw_artist(self.linev) + self.ax.draw_artist(self.lineh) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + + return False + + def _draw_cursor(self): + """draw cursor""" + + self.needclear = True + if not self.visible: + return + + self.linev.set_xdata((self._coord[0], self.coord[0])) + self.lineh.set_ydata((self._coord[1], self.coord[1])) + + self.linev.set_visible(self.vertOn) + self.lineh.set_visible(self.horizOn) + + self._update() + + def coord(self): + """ + current coordinates of the cursor + """ + return self._coord.copy() + def clear(self, event): """clear the cursor""" if self.ignore(event): @@ -973,45 +1048,248 @@ def clear(self, event): self.linev.set_visible(False) self.lineh.set_visible(False) - def onmove(self, event): + def on_press(self, event): + """ + on left click we will see if the mouse is over us + and store some data + """ + + # Verify left click + if event.button != 1: return + + #Verify in axis + if event.inaxes != self.ax: return + + # Say movement is OK + self.__pressed = True + + # this click also updates + self.on_move(event) + + def on_release(self, event): + self.__pressed = False + + def on_move(self, event): """on mouse motion draw the cursor if visible""" - if self.ignore(event): - return - if not self.canvas.widgetlock.available(self): - return - if event.inaxes != self.ax: - self.linev.set_visible(False) - self.lineh.set_visible(False) + if self.__pressed: + if self.ignore(event): + return + if not self.canvas.widgetlock.available(self): + return + if event.inaxes != self.ax: + return - if self.needclear: - self.canvas.draw() - self.needclear = False - return - self.needclear = True - if not self.visible: - return - self.linev.set_xdata((event.xdata, event.xdata)) + # Store the new coordinates + self._coord[0] = float(event.xdata) + self._coord[1] = float(event.ydata) - self.lineh.set_ydata((event.ydata, event.ydata)) - self.linev.set_visible(self.visible and self.vertOn) - self.lineh.set_visible(self.visible and self.horizOn) + self._draw_cursor() + +class Cursors(AxesWidget): + """ + Several Cursors: horizontal and vertical lines that span the axes and + move with the pointer. + + For the cursors to remain responsive you must keep a reference to them. + + The following attributes are accessible + + *ax* + The :class:`matplotlib.axes.Axes` the cursor renders in. + + *properties* + a list of dictionaries for each cursor, containing + + *horizOn* + Controls the visibility of the horizontal line + + *vertOn* + Controls the visibility of the vertical line + + *visible* + Controls the visibility of the cursor itself + + The following code will add three cursors to an existing axis + + csr = Cursors(ax, [{'color':'red'}, {'color':'blue'}, {'color':'black'}], + useblit=False) + """ + + def __init__(self, ax, properties, useblit=False): + """ + Parameters + ---------- + ax : matplotlib.axes.Axes + The :class:`matplotlib.axes.Axes` instance the cursor + will be placed into. + + properties: list + list of dictionaries, one for each each cursor defining the + cursor's properties with keys: + + horizOn : boolean + Controls the visibility of the horizontal line. + + vertOn : boolean + Controls the visibility of the vertical line. + + coord : np.array + Initial location of cursor [CURRENTLY IGNORED] + + and all allowed line property keywords + + + useblit : boolean + use the backend-dependent blitting features for faster + updates (GTKAgg only for now). + + .. plot :: mpl_examples/widgets/cursor.py + """ + + # TODO: Is the GTKAgg limitation still true? + super().__init__(ax) + + self.connect_event('motion_notify_event', self.on_move) + + self.connect_event('button_press_event', self.on_press) + self.connect_event('button_release_event', self.on_release) + self.__selected = None + + self.connect_event('draw_event', self.clear) + + self.useblit = useblit and self.canvas.supports_blit + + self.background = None + self.needclear = False + + # Initilize empty list + self.properties = [{} for prop in properties] + + for prop, new_prop in zip(properties, self.properties): + new_prop['visible'] = True + new_prop['horizOn'] = prop.pop('horizOn', True) + new_prop['vertOn'] = prop.pop('vertOn', True) + + new_prop['coord'] = np.array([ + 0.5*(ax.get_xbound()[0] + ax.get_xbound()[1]), + 0.5*(ax.get_ybound()[0] + ax.get_ybound()[1]) + ]) + + if self.useblit: + prop['animated'] = True + + new_prop['lineh'] = ax.axhline( + new_prop['coord'][1], + visible=False, + **prop) + new_prop['linev'] = ax.axvline( + new_prop['coord'][0], + visible=False, + **prop) + + self.__selected = None + self._draw_cursor() - self._update() def _update(self): if self.useblit: if self.background is not None: self.canvas.restore_region(self.background) - self.ax.draw_artist(self.linev) - self.ax.draw_artist(self.lineh) + for prop in self.properties: + self.ax.draw_artist(prop['linev']) + self.ax.draw_artist(prop['lineh']) self.canvas.blit(self.ax.bbox) else: - self.canvas.draw_idle() return False + def _draw_cursor(self): + """draw cursor specified by dictionary entries in prop""" + + self.needclear = True + + for prop in self.properties: + + if prop['visible']: + + prop['linev'].set_xdata((prop['coord'][0], prop['coord'][0])) + prop['lineh'].set_ydata((prop['coord'][1], prop['coord'][1])) + + prop['linev'].set_visible(prop['vertOn']) + prop['lineh'].set_visible(prop['horizOn']) + + self._update() + + def coord(self, cursor): + """ + current coordinates of the cursor + """ + return self.properties[cursor]['coord'].copy() + + def clear(self, event): + """clear the cursor""" + if self.ignore(event): + return + if self.useblit: + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + for prop in self.properties: + prop['linev'].set_visible(False) + prop['lineh'].set_visible(False) + + def on_press(self, event): + """ + on left click we will see if the mouse is over us + and store some data + """ + + # Verify left click + if event.button != 1: return + + #Verify in axis + if event.inaxes != self.ax: return + + # Find cursor closest to click location + # the best user behavior is to look at the closest visible line + min_distance = -1.0 + for prop in self.properties: + if prop['visible']: + if prop['vertOn']: + distance = abs(prop['coord'][0] - event.xdata) + + if min_distance < 0.0 or distance < min_distance: + min_distance = distance + self.__selected = prop + + if prop['horizOn']: + distance = abs(prop['coord'][1] - event.ydata) + + if min_distance < 0.0 or distance < min_distance: + min_distance = distance + self.__selected = prop + + # this click also updates + self.on_move(event) + + def on_release(self, event): + self.__selected = None + + def on_move(self, event): + """on mouse motion draw the cursor if visible""" + if self.__selected is not None: + if self.ignore(event): + return + if not self.canvas.widgetlock.available(self): + return + if event.inaxes != self.ax: + return + + # Store the new coordinates + self.__selected['coord'][0] = float(event.xdata) + self.__selected['coord'][1] = float(event.ydata) + + self._draw_cursor() class MultiCursor(Widget): """ From 5eed3923adbb7d6903601cfd473cb7368a0d502c Mon Sep 17 00:00:00 2001 From: ispielman Date: Mon, 1 Feb 2016 07:33:56 -0500 Subject: [PATCH 2/2] PEP8 consistancy --- lib/matplotlib/widgets.py | 105 +++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index d2713dbf3ac2..799aaaaab9fe 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -921,7 +921,7 @@ def funchspace(self, val): class Cursor(AxesWidget): """ - A Cursor: A horizontal and vertical line that spans the axes and + A Cursor: A horizontal and vertical line that spans the axes and moves with the pointer. For the cursor to remain responsive you must keep a reference to it. @@ -941,7 +941,7 @@ class Cursor(AxesWidget): Controls the visibility of the cursor itself """ - def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, + def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, dragging=False, **lineprops): """ Parameters @@ -957,8 +957,8 @@ def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, Controls the visibility of the vertical line. useblit : boolean - use the backend-dependent blitting features for faster - updates (GTKAgg only for now). + use the backend-dependent blitting features for faster + updates (GTKAgg only for now). dragging: boolean drag cursors only when the left mouse button is pressed @@ -988,15 +988,15 @@ def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, self.horizOn = horizOn self.vertOn = vertOn self.useblit = useblit and self.canvas.supports_blit - + if self.useblit: lineprops['animated'] = True - + self._coord = np.array([ - 0.5*(ax.get_xbound()[0] + ax.get_xbound()[1]), + 0.5*(ax.get_xbound()[0] + ax.get_xbound()[1]), 0.5*(ax.get_ybound()[0] + ax.get_ybound()[1]) ]) - + self.lineh = ax.axhline(self._coord[1], visible=False, **lineprops) self.linev = ax.axvline(self._coord[0], visible=False, **lineprops) @@ -1025,8 +1025,8 @@ def _draw_cursor(self): if not self.visible: return - self.linev.set_xdata((self._coord[0], self.coord[0])) - self.lineh.set_ydata((self._coord[1], self.coord[1])) + self.linev.set_xdata((self._coord[0], self._coord[0])) + self.lineh.set_ydata((self._coord[1], self._coord[1])) self.linev.set_visible(self.vertOn) self.lineh.set_visible(self.horizOn) @@ -1053,12 +1053,14 @@ def on_press(self, event): on left click we will see if the mouse is over us and store some data """ - + # Verify left click - if event.button != 1: return - - #Verify in axis - if event.inaxes != self.ax: return + if event.button != 1: + return + + # Verify in axis + if event.inaxes != self.ax: + return # Say movement is OK self.__pressed = True @@ -1067,6 +1069,9 @@ def on_press(self, event): self.on_move(event) def on_release(self, event): + """ + When button is released disable all actions + """ self.__pressed = False def on_move(self, event): @@ -1087,7 +1092,7 @@ def on_move(self, event): class Cursors(AxesWidget): """ - Several Cursors: horizontal and vertical lines that span the axes and + Several Cursors: horizontal and vertical lines that span the axes and move with the pointer. For the cursors to remain responsive you must keep a reference to them. @@ -1098,19 +1103,19 @@ class Cursors(AxesWidget): The :class:`matplotlib.axes.Axes` the cursor renders in. *properties* - a list of dictionaries for each cursor, containing - + a list of dictionaries for each cursor, containing + *horizOn* Controls the visibility of the horizontal line - + *vertOn* Controls the visibility of the vertical line - + *visible* Controls the visibility of the cursor itself The following code will add three cursors to an existing axis - + csr = Cursors(ax, [{'color':'red'}, {'color':'blue'}, {'color':'black'}], useblit=False) """ @@ -1133,18 +1138,19 @@ def __init__(self, ax, properties, useblit=False): vertOn : boolean Controls the visibility of the vertical line. - coord : np.array - Initial location of cursor [CURRENTLY IGNORED] - and all allowed line property keywords useblit : boolean - use the backend-dependent blitting features for faster - updates (GTKAgg only for now). + use the backend-dependent blitting features for faster + updates (GTKAgg only for now). .. plot :: mpl_examples/widgets/cursor.py """ + #TODO: add to setup dictionary + # coord : np.array + # Initial location of cursor + # TODO: Is the GTKAgg limitation still true? super().__init__(ax) @@ -1164,28 +1170,28 @@ def __init__(self, ax, properties, useblit=False): # Initilize empty list self.properties = [{} for prop in properties] - + for prop, new_prop in zip(properties, self.properties): - new_prop['visible'] = True + new_prop['visible'] = True new_prop['horizOn'] = prop.pop('horizOn', True) new_prop['vertOn'] = prop.pop('vertOn', True) new_prop['coord'] = np.array([ - 0.5*(ax.get_xbound()[0] + ax.get_xbound()[1]), - 0.5*(ax.get_ybound()[0] + ax.get_ybound()[1]) - ]) + 0.5*(ax.get_xbound()[0] + ax.get_xbound()[1]), + 0.5*(ax.get_ybound()[0] + ax.get_ybound()[1]) + ]) if self.useblit: prop['animated'] = True new_prop['lineh'] = ax.axhline( - new_prop['coord'][1], - visible=False, - **prop) + new_prop['coord'][1], + visible=False, + **prop) new_prop['linev'] = ax.axvline( - new_prop['coord'][0], - visible=False, - **prop) + new_prop['coord'][0], + visible=False, + **prop) self.__selected = None self._draw_cursor() @@ -1209,14 +1215,14 @@ def _draw_cursor(self): """draw cursor specified by dictionary entries in prop""" self.needclear = True - + for prop in self.properties: - + if prop['visible']: - + prop['linev'].set_xdata((prop['coord'][0], prop['coord'][0])) prop['lineh'].set_ydata((prop['coord'][1], prop['coord'][1])) - + prop['linev'].set_visible(prop['vertOn']) prop['lineh'].set_visible(prop['horizOn']) @@ -1243,12 +1249,14 @@ def on_press(self, event): on left click we will see if the mouse is over us and store some data """ - + # Verify left click - if event.button != 1: return - - #Verify in axis - if event.inaxes != self.ax: return + if event.button != 1: + return + + # Verify in axis + if event.inaxes != self.ax: + return # Find cursor closest to click location # the best user behavior is to look at the closest visible line @@ -1257,7 +1265,7 @@ def on_press(self, event): if prop['visible']: if prop['vertOn']: distance = abs(prop['coord'][0] - event.xdata) - + if min_distance < 0.0 or distance < min_distance: min_distance = distance self.__selected = prop @@ -1273,6 +1281,9 @@ def on_press(self, event): self.on_move(event) def on_release(self, event): + """ + When button is released disable all actions + """ self.__selected = None def on_move(self, event):