I am learning how to use click events to trigger specific actions in matplotlib.
In my reproductible example, I would like:
These posts Matplotlib draggable data marker and Matplotlib drag overlapping points interactively were very useful to implement the draggable data and I think it is working well in my example.
I think in the code below I should be close to implementing the 'add marker' events but something is not right when I update the data of the artist as the new markers don't appear on the plot.
I don't know what is the best way to implement this one... I think the best way would be to always keep in memory one copy of the plot before a click event is triggered and restore this copy if a double click event (with the right mouse button) is triggered after a click event (= adding marker or dragging the marker somewhere else)
I am using the script below:
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_())
I have done the following changes in your code:
The reason the new marker did not appear in your plot, is that you used event.x
, event.y
instead of event.xdata
, event.ydata
(it gets the coordinates automatically instead of converting pixels to coordinates). I appended the new point to the coordinates of the old markers and updated the plot.
I created a new instance variable self.memory
which contains the coordinates of markers. self.memory
updates before dragging markers or adding new markers with the self.save_to_memory()
function. The self.undo()
function uses self.memory to undo the last change.
I also replaced the line if event.button == 1:
with an elif, because on double click, it accesses both if statements.
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_())
In the following code I added the functionality for multiple undos. The simple solution would be making self.memory
a list, but it would be wasteful of sources. So I gave up the self.memory
idea and created a variable self.history
which is a list that keeps record of every change that takes place. self.history_depth
is the capability of self.history
to keep record of the last n
changes. self.append_to_history
adds history steps to self.history
. I also added the functions self.update_markers
, self.move_marker
, self.append_marker
and self.remove_marker
for shake of readability and maintainability.
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