Aim
I would like to simultaneously:
1) Select multiple artists on a matplotlib canvas by window-select.
By that I mean that if I hold down the mouse button, drag the mouse, and release, all artists in the rectangle defined by the x,y locations of the button press and the button release are picked.
2) Drag multiple selected artists by then pressing the mouse down over one of the selected artists and moving the mouse, and releasing.
This is exactly the same behaviour as one would expect in a normal file browser.
Previous attempts and remaining problems
To that end, I have started to write two classes, WindowSelect
and
Draggable
, which are shown below.
WindowSelect
implements the logic for (1) apart from the fact that I
don't know how to manually trigger the picker function, as it is
referred to in the matplotlib documentation.
Instead I call a place holder function func
.
Draggable
implements the logic for (2) apart from the fact that a
single artist is picked at a time (shamelessly appropriated from
another indicated SO answer). The matplotlib
documentation
indicates that the picking of multiple artists simultaneously should
be possible (last example on the web page). However, if I, for
example, set the tolerance for the picker very high, only a single
artist appears to get selected / can then be dragged around on the
canvas, so I am unsure how the code needs to change to accommodate
multiple artists.
Code
import numpy as np
import matplotlib.pyplot as plt; plt.ion()
import matplotlib.patches as patches
class WindowSelect(object):
def __init__(self, artists):
self.artists = artists
self.canvases = set(artist.figure.canvas for artist in self.artists)
for canvas in self.canvases:
canvas.mpl_connect('button_press_event', self.on_press)
canvas.mpl_connect('button_release_event', self.on_release)
self.currently_dragging = False
def on_press(self, event):
if not self.currently_dragging:
self.x0 = event.xdata
self.y0 = event.ydata
self.currently_dragging = True
def on_release(self, event):
if self.currently_dragging:
self.x1 = event.xdata
self.y1 = event.ydata
for artist in self.artists:
if self.is_inside_rect(*artist.center):
self.func(artist)
for canvas in self.canvases:
canvas.draw()
self.currently_dragging = False
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 func(self, artist):
artist.set_color('k') # <- just an example operation; would like to pick artist instead
class Draggable(object):
"""
https://stackoverflow.com/questions/21654008/matplotlib-drag-overlapping-points-interactively
"""
def __init__(self, artists, tolerance=5):
for artist in artists:
artist.set_picker(tolerance)
self.artists = artists
self.currently_dragging = False
self.current_artist = None
self.offset = (0, 0)
for canvas in set(artist.figure.canvas for artist in self.artists):
canvas.mpl_connect('button_press_event', self.on_press)
canvas.mpl_connect('button_release_event', self.on_release)
canvas.mpl_connect('pick_event', self.on_pick)
canvas.mpl_connect('motion_notify_event', self.on_motion)
def on_press(self, event):
self.currently_dragging = True
def on_release(self, event):
self.currently_dragging = False
self.current_artist = None
def on_pick(self, event):
if self.current_artist is None:
self.current_artist = event.artist
x0, y0 = event.artist.center
x1, y1 = event.mouseevent.xdata, event.mouseevent.ydata
self.offset = (x0 - x1), (y0 - y1)
def on_motion(self, event):
if not self.currently_dragging:
return
if self.current_artist is None:
return
dx, dy = self.offset
self.current_artist.center = event.xdata + dx, event.ydata + dy
self.current_artist.figure.canvas.draw()
def demo(TestClass):
fig, ax = plt.subplots(1,1)
xlim = [-5, 5]
ylim = [-5, 5]
ax.set(xlim=xlim, ylim=ylim)
circles = [patches.Circle((3.0, 3.0), 0.5, fc='r', alpha=1.0),
patches.Circle((0.0, 0.0), 0.5, fc='b', alpha=1.0),
patches.Circle((0.0, 3.0), 0.5, fc='g', alpha=1.0)]
for circle in circles:
ax.add_patch(circle)
return TestClass(circles)
if __name__ == '__main__':
out1 = demo(Draggable)
out2 = demo(WindowSelect)