martinako martinako - 1 month ago 27
Python Question

Matplotlib draggable data marker

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?

Answer

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. Otherwise, draggable is set to None.

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_())

enter image description here

Comments