I need a draggable data marker to manually adjust a set of points in a dataset.
This answer and others solve this problem using a Patch.Circle. However I need the marker to stay the same size when zooming the plot. With circles the circle zooms too. I haven't tried to find a way of resizing the circle dynamically with each new zoom because it looks there should be a simpler way of just moving a marker. Does anybody know how to do this?
Below, I present a MWE that shows one way to do it with the object oriented API of matplotlib and PyQt4. The data markers are draggable using the middle mouse button. The strategy is to plot your data with a line2D artist and to subsequently drag the markers by manipulating the data of the artist and updating the plot. This can be roughly summarized in 3 steps:
Step 1 - When the middle mouse button is clicked, the coordinates of the mouse cursor in pixels are compared to the xy data, transformed in pixels, of the line2D artist. If the linear distance between the cursor and the nearest marker is less than a given value (defined here as the size of the markers), the index of that data marker is saved in the class attribute
draggable is set to
Step 2 - When the mouse is moved, if
draggable is not None, the coordinate of the data marker whose index was stored in the class attribute
draggable are set to the ones of the mouse cursor.
Step 3 - When the middle mouse button is released,
draggable is set back to None.
Note: If desired, it would also be possible to use the pyplot interface to generate the figure instead of the object oriented API.
import sys from PyQt4 import QtGui from matplotlib.figure import Figure from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg from matplotlib.backends.backend_qt4agg import FigureManagerQT import numpy as np class MyFigureCanvas(FigureCanvasQTAgg): def __init__(self): super(MyFigureCanvas, self).__init__(Figure()) # init class attributes: self.background = None self.draggable = None self.msize = 6 # plot some data: x = np.random.rand(25) self.ax = self.figure.add_subplot(111) self.markers, = self.ax.plot(x, marker='o', ms=self.msize) # define event connections: self.mpl_connect('motion_notify_event', self.on_motion) self.mpl_connect('button_press_event', self.on_click) self.mpl_connect('button_release_event', self.on_release) def on_click(self, event): if event.button == 2: # 2 is for middle mouse button # get mouse cursor coordinates in pixels: x = event.x y = event.y # get markers xy coordinate in pixels: xydata = self.ax.transData.transform(self.markers.get_xydata()) xydata = np.array(xydata) xdata = xydata[:, 0] ydata = xydata[:, 1] # compute the linear distance between the markers and the cursor: r = ((xdata - x)**2 + (ydata - y)**2)**0.5 if np.min(r) < self.msize: # save figure background: self.markers.set_visible(False) self.draw() self.background = self.copy_from_bbox(self.ax.bbox) self.markers.set_visible(True) self.ax.draw_artist(self.markers) self.update() # store index of draggable marker: self.draggable = np.argmin(r) else: self.draggable = None def on_motion(self, event): if self.draggable is not None: if event.xdata and event.ydata: # get markers coordinate in data units: xdata, ydata = self.markers.get_data() # change the coordinate of the marker that is # being dragged to the ones of the mouse cursor: xdata[self.draggable] = event.xdata ydata[self.draggable] = event.ydata # update the data of the artist: self.markers.set_xdata(xdata) self.markers.set_ydata(ydata) # update the plot: self.restore_region(self.background) self.ax.draw_artist(self.markers) self.update() def on_release(self, event): self.draggable = None if __name__ == '__main__': app = QtGui.QApplication(sys.argv) canvas = MyFigureCanvas() manager = FigureManagerQT(canvas, 1) manager.show() sys.exit(app.exec_())