如何使用双击事件(例如添加标记、取消之前的操作)与 matplotlib 图进行交互?

How to interact with a matplotlib plot using double clicked events (e.g. adding marker, canceling previous action)?

我正在学习如何使用点击事件来触发 matplotlib 中的特定操作。

在我的可复制示例中,我想:

任务 1

这些帖子 and Matplotlib drag overlapping points interactively 对实现可拖动数据非常有用,我认为它在我的示例中运行良好。

任务 2

我认为在下面的代码中我应该接近实现 'add marker' 事件但是当我更新艺术家的数据时有些不对劲因为新标记没有出现在情节中。

任务 3

我不知道实现这个的最好方法是什么...我认为最好的方法是在触发点击事件之前始终在内存中保留一份情节副本,如果单击事件(= 添加标记或将标记拖动到其他位置)后触发双击事件(使用鼠标右键)

我正在使用下面的脚本:

import sys
from PySide2.QtWidgets import QApplication
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.dblclick:
            if event.button == 1:  # add a marker on the line plotted
                # get mouse cursor coordinates in pixels:
                x, y = event.x, event.y
                # get markers xy coordinate in pixels:
                xydata = self.ax.transData.transform(self.markers.get_xydata())
                xdata, ydata = xydata.T

                # update the data of the artist:
                self.markers.set_xdata(xdata)
                self.markers.set_ydata(ydata)
                self.ax.draw_artist(self.markers)
                self.update()

                print(f"{event.button} - coords: x: {x} / y: {y} ")
            elif event.button == 3:  # cancel previous action
                print(f"Double clicked event - {str(event.button)}")

        if event.button == 1:  # 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())
            xdata, ydata = xydata.T
            # 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 = QApplication(sys.argv)

    canvas = MyFigureCanvas()
    manager = FigureManagerQT(canvas, 1)
    manager.show()

    sys.exit(app.exec_())

我对您的代码做了以下更改:

任务 2

新标记没有出现在您的绘图中的原因是您使用了 event.xevent.y 而不是 event.xdataevent.ydata(它获取坐标自动而不是将像素转换为坐标)。我将新点附加到旧标记的坐标并更新了绘图。

任务 3

我创建了一个包含标记坐标的新实例变量self.memoryself.memory 在拖动标记或使用 self.save_to_memory() 功能添加新标记之前进行更新。 self.undo() 函数使用 self.memory 撤消上次更改。 我还用 elif 替换了行 if event.button == 1:,因为双击时,它会访问两个 if 语句。

import sys
from PySide2.QtWidgets import QApplication
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)

        self.memory = self.markers.get_xydata()

        # 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 save_to_memory(self):
        self.memory = self.markers.get_xydata()

    def undo(self):
        self.markers.set_xdata(self.memory[:, 0])
        self.markers.set_ydata(self.memory[:, 1])
        self.draw()
        self.ax.draw_artist(self.markers)
        self.update()

    def on_click(self, event):
        if event.dblclick:
            if event.button == 1:  # add a marker on the line plotted
                self.save_to_memory()
                # get mouse cursor coordinates in pixels:
                x, y = event.xdata, event.ydata

                # update the data of the artist:
                old_xy = self.markers.get_xydata()
                old_xy = np.vstack((old_xy, np.array([[x, y]])))
                self.markers.set_xdata(old_xy[:, 0])
                self.markers.set_ydata(old_xy[:, 1])
                self.ax.draw_artist(self.markers)
                self.update()

                print(f"{event.button} - coords: x: {x} / y: {y} ")
            elif event.button == 3:  # cancel previous action
                print(f"Double clicked event - {str(event.button)}")
                self.undo()

        elif event.button == 1:  # 2 is for middle mouse button
            # get mouse cursor coordinates in pixels:
            x = event.x
            y = event.y
            # print(f"{event.button} - coords: x: {x} / y: {y} ")
            # get markers xy coordinate in pixels:
            xydata = self.ax.transData.transform(self.markers.get_xydata())
            xdata, ydata = xydata.T
            # 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:
                self.save_to_memory()
                # 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 = QApplication(sys.argv)

    canvas = MyFigureCanvas()
    manager = FigureManagerQT(canvas, 1)
    manager.show()

    sys.exit(app.exec_())

编辑:

在下面的代码中,我添加了多次撤消的功能。简单的解决方案是使 self.memory 成为一个列表,但这会浪费资源。所以我放弃了 self.memory 的想法并创建了一个变量 self.history 这是一个列表,记录了发生的每一个变化。 self.history_depthself.history 记录最后 n 更改的能力。 self.append_to_history 将历史步骤添加到 self.history。我还添加了函数 self.update_markersself.move_markerself.append_markerself.remove_marker 以平衡可读性和可维护性。

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)

        self.history = []
        self.history_depth = 10

        # 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 append_to_history(self, type, index, x=None, y=None):
        if index < 0:
            index += self.markers.get_xydata().shape[0]
        if type == "move":
            self.history.append({"type": "move",
                                 "index": index,
                                 "x": x,
                                 "y": y})
        elif type == "append":
            self.history.append({"type": "append",
                                 "index": index + 1})
        if len(self.history) > self.history_depth:
            del self.history[0]

    def undo(self):
        if len(self.history) > 0:
            last_move = self.history[-1]
            if last_move["type"] == "move":
                self.move_marker(last_move["index"], last_move["x"], last_move["y"])
            elif last_move["type"] == "append":
                self.remove_marker(last_move["index"])
            del self.history[-1]

    def update_markers(self, x_data, y_data):
        self.markers.set_xdata(x_data)
        self.markers.set_ydata(y_data)
        self.draw()
        self.ax.draw_artist(self.markers)
        self.update()

    def move_marker(self, i, x, y):
        xdata, ydata = self.markers.get_data()
        xdata[i], ydata[i] = x, y
        self.update_markers(xdata, ydata)

    def append_marker(self, x, y):
        new_xy = np.vstack((self.markers.get_xydata(), np.array([[x, y]])))
        self.update_markers(new_xy[:, 0], new_xy[:, 1])

    def remove_marker(self, i):
        new_xy = np.delete(self.markers.get_xydata(), i, axis=0)
        self.update_markers(new_xy[:, 0], new_xy[:, 1])

    def on_click(self, event):
        if event.dblclick:
            if event.button == 1:
                self.append_to_history("append", -1)
                self.append_marker(event.xdata, event.ydata)
            elif event.button == 3:
                self.undo()

        # Single Click
        elif event.button == 1:
            x, y = event.x, event.y
            xydata = self.ax.transData.transform(self.markers.get_xydata())
            xdata, ydata = xydata.T
            r = ((xdata - x) ** 2 + (ydata - y) ** 2) ** 0.5
            if np.min(r) < self.msize:
                index = np.where(r == r.min())[0][0]
                x_i, y_i = self.markers.get_xydata()[index]
                self.append_to_history("move", index, x_i, y_i)
                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()
                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:
                self.move_marker(self.draggable, event.xdata, event.ydata)

    def on_release(self, event):
        self.draggable = None