'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.

enter image description 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