'Python mouse event for selecting multiple points on a graph
I am trying to select multiple points on a graph and move them. Right now I just have the singular mouse click event where I can click one point and move it, but I would like to be able to drag the mouse and select multiple points instead of having to do it individually. I am unsure of how to get the mouse to select multiple objects.Here is a sample of the graph I am working with, its a matplotlib graph
def mouse_event(event):
global fig, ax, cid, sonarx, sonarz, aw
print('x: {} and y: {}'.format(event.xdata, event.ydata))
zcor=0
while((sonarz[zcor])<(-event.ydata)):
zcor=zcor+1
# mzcor=int(aw/2+(aw/2-zcor))-2
mzcor=int(aw/2+(aw/2-zcor))-2 # z-cord of matching upward data point
print(aw,zcor,mzcor)
print("clicked:",event.xdata,event.ydata)
print("Selected zcor=",zcor, "x=",sonarx[zcor],"z=",sonarz[zcor])
print("Dropping Sonarx:[",zcor,"]",sonarx[zcor])
# print("Raising Sonarx:[",mzcor,"]",sonarx[mzcor])
print("zcor:",zcor," mzcor:",mzcor)
sonarx[zcor]=event.xdata
sonarx[mzcor]=event.xdata
print("New Drop Sonarx:[",zcor,"]",sonarx[zcor])
print("New Raise Sonarx:[",mzcor,"]",sonarx[mzcor])
print("Redrawing Plot...")
# print(sonarx)
ax.clear()
ax.plot(sonarx[0:aw], -sonarz[0:aw], ls='dotted', linewidth=2, color='red')
plt.title("Sonar Scan "+sxfile)
fig.canvas.draw()
# fig.canvas.flush_events()
# plt.show()
Solution 1:[1]
The following is an improved version of another answer here.
#!/usr/bin/env python
"""
Implements ClickableArtists, SelectableArtists, and DraggableArtists
"""
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
class ClickableArtists(object):
"""Implements selection of matplotlib artists via the mouse left click (+/- ctrl or command key).
Notes:
------
Adapted from: https://stackoverflow.com/a/47312637/2912349
"""
def __init__(self, artists):
try:
self.fig, = set(list(artist.figure for artist in artists))
except ValueError:
raise Exception("All artists have to be on the same figure!")
try:
self.ax, = set(list(artist.axes for artist in artists))
except ValueError:
raise Exception("All artists have to be on the same axis!")
# self.fig.canvas.mpl_connect('button_press_event', self._on_press)
self.fig.canvas.mpl_connect('button_release_event', self._on_release)
self._clickable_artists = list(artists)
self._selected_artists = []
self._base_linewidth = dict([(artist, artist.get_linewidth()) for artist in artists])
self._base_edgecolor = dict([(artist, artist.get_edgecolor()) for artist in artists])
if mpl.get_backend() == 'MacOSX':
msg = "You appear to be using the MacOSX backend."
msg += "\nModifier key presses are bugged on this backend. See https://github.com/matplotlib/matplotlib/issues/20486"
msg += "\nConsider using a different backend, e.g. TkAgg (import matplotlib; matplotlib.use('TkAgg'))."
msg += "\nNote that you must set the backend before importing any package depending on matplotlib (includes pyplot, networkx, netgraph)."
warnings.warn(msg)
# def _on_press(self, event):
def _on_release(self, event):
if event.inaxes == self.ax:
for artist in self._clickable_artists:
if artist.contains(event)[0]:
if event.key in ('control', 'super+??', 'ctrl+??'):
self._toggle_select_artist(artist)
else:
self._deselect_all_other_artists(artist)
self._toggle_select_artist(artist)
# NOTE: if two artists are overlapping, only the first one encountered is selected!
break
else:
if not event.key in ('control', 'super+??', 'ctrl+??'):
self._deselect_all_artists()
else:
print("Warning: clicked outside axis limits!")
def _toggle_select_artist(self, artist):
if artist in self._selected_artists:
self._deselect_artist(artist)
else:
self._select_artist(artist)
def _select_artist(self, artist):
if not (artist in self._selected_artists):
linewidth = artist.get_linewidth()
artist.set_linewidth(1.5 * linewidth)
artist.set_edgecolor('black')
self._selected_artists.append(artist)
self.fig.canvas.draw_idle()
def _deselect_artist(self, artist):
if artist in self._selected_artists: # should always be true?
artist.set_linewidth(self._base_linewidth[artist])
artist.set_edgecolor(self._base_edgecolor[artist])
self._selected_artists.remove(artist)
self.fig.canvas.draw()
def _deselect_all_artists(self):
for artist in self._selected_artists[:]: # we make a copy of the list with [:], as we are modifying the list being iterated over
self._deselect_artist(artist)
def _deselect_all_other_artists(self, artist_to_keep):
for artist in self._selected_artists[:]:
if artist != artist_to_keep:
self._deselect_artist(artist)
class SelectableArtists(ClickableArtists):
"""Augments ClickableArtists with a rectangle selector.
Notes:
------
Adapted from: https://stackoverflow.com/a/47312637/2912349
"""
def __init__(self, artists):
super().__init__(artists)
self.fig.canvas.mpl_connect('button_press_event', self._on_press)
# self.fig.canvas.mpl_connect('button_release_event', self._on_release)
self.fig.canvas.mpl_connect('motion_notify_event', self._on_motion)
self._selectable_artists = list(artists)
self._currently_selecting = False
self._rect = plt.Rectangle((0, 0), 1, 1, linestyle="--", edgecolor="crimson", fill=False)
self.ax.add_patch(self._rect)
self._rect.set_visible(False)
self._x0 = 0
self._y0 = 0
self._x1 = 0
self._y1 = 0
def _on_press(self, event):
# super()._on_press(event)
if event.inaxes == self.ax:
# reset rectangle
self._x0 = event.xdata
self._y0 = event.ydata
self._x1 = event.xdata
self._y1 = event.ydata
for artist in self._clickable_artists:
if artist.contains(event)[0]:
break
else:
self._currently_selecting = True
def _on_release(self, event):
super()._on_release(event)
if self._currently_selecting:
# select artists inside window
for artist in self._selectable_artists:
if self._is_inside_rect(*artist.get_xy()):
if event.key in ('control', 'super+??', 'ctrl+??'): # if/else probably superfluouos
self._toggle_select_artist(artist) # as no artists will be selected
else: # if control is not held previously
self._select_artist(artist) #
# stop window selection and draw new state
self._currently_selecting = False
self._rect.set_visible(False)
self.fig.canvas.draw_idle()
def _on_motion(self, event):
if event.inaxes == self.ax:
if self._currently_selecting:
self._x1 = event.xdata
self._y1 = event.ydata
# add rectangle for selection here
self._selector_on()
def _is_inside_rect(self, x, y):
xlim = np.sort([self._x0, self._x1])
ylim = np.sort([self._y0, self._y1])
if (xlim[0]<=x) and (x<xlim[1]) and (ylim[0]<=y) and (y<ylim[1]):
return True
else:
return False
def _selector_on(self):
self._rect.set_visible(True)
xlim = np.sort([self._x0, self._x1])
ylim = np.sort([self._y0, self._y1])
self._rect.set_xy((xlim[0],ylim[0] ) )
self._rect.set_width(np.diff(xlim))
self._rect.set_height(np.diff(ylim))
self.fig.canvas.draw_idle()
class DraggableArtists(SelectableArtists):
"""Augments SelectableArtists to support dragging of artists by holding the left mouse button.
Notes:
------
Adapted from: https://stackoverflow.com/a/47312637/2912349
"""
def __init__(self, artists):
super().__init__(artists)
self._draggable_artists = list(artists)
self._currently_clicking_on_artist = None
self._currently_dragging = False
self._offset = dict()
def _on_press(self, event):
super()._on_press(event)
if event.inaxes == self.ax:
for artist in self._draggable_artists:
if artist.contains(event)[0]:
self._currently_clicking_on_artist = artist
break
else:
print("Warning: clicked outside axis limits!")
def _on_motion(self, event):
super()._on_motion(event)
if event.inaxes == self.ax:
if self._currently_clicking_on_artist:
if self._currently_clicking_on_artist not in self._selected_artists:
if event.key not in ('control', 'super+??', 'ctrl+??'):
self._deselect_all_artists()
self._select_artist(self._currently_clicking_on_artist)
self._offset = {artist : artist.get_xy() - np.array([event.xdata, event.ydata]) for artist in self._selected_artists if artist in self._draggable_artists}
self._currently_clicking_on_artist = None
self._currently_dragging = True
if self._currently_dragging:
self._move(event)
def _on_release(self, event):
if self._currently_dragging:
self._currently_dragging = False
else:
self._currently_clicking_on_artist = None
super()._on_release(event)
def _move(self, event):
cursor_position = np.array([event.xdata, event.ydata])
for artist in self._selected_artists:
artist.set_xy(cursor_position + self._offset[artist])
self.fig.canvas.draw_idle()
class Circle(plt.Circle):
# Most scatter markers are instances of `RegularPolygon`, and hence have a `get_xy` and `set_xy` method.
# Not so `Circle`, which is why we have to define them.
def get_xy(self):
return self.get_center()
def set_xy(self, xy):
self.set_center(xy)
if __name__ == '__main__':
points = np.random.rand(100, 2)
fig, ax = plt.subplots(figsize=(10,10))
ax.set_aspect('equal')
artists = [Circle(p, 0.02) for p in points]
for artist in artists:
ax.add_patch(artist)
# NB: without the reference, artists will be garbage collected, and then cannot be moved anymore!
reference = DraggableArtists(artists)
ax.set_aspect('equal')
plt.show()
# get new coordinates
new_points = np.array([artist.get_xy() for artist in reference._draggable_artists])
print(new_points)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | DharmanBot |