matplotlib - can you set priority if there's multiple event handlers? - python

I'm building a tool with PyQt5 that will allow users to drag rectangles on click in a matplotlib_widget. The problem is that I want to know which rectangle has been clicked.
Since my canvas and my rectangle are two different classes, I have a hard time communicating properly between those. To resolve this, I added an event handler on the 'button_click_event' of my rectangle's canvas to write its position in a .txt file. On the other hand, I connected as well to the 'button_click_event' to my the canvas, which is to read the .txt and verify in a previously stored list if the position is there. If it was, it means it is this particular rectangle which was clicked.
The problem: the event handler of the canvas that reads the .txt file is called before the handler that writes it.
Is it possible to set priority on the events of figure.canvas ?
Here is a minimally working example of my specific problem. You will notice that I made prints when the function I want do something. If you double click and create 2 rectangles, you should notice that it you click on the rectangle 0 after clicking on the rectangle 1, it'll say that you clicked on the rectangle 1. This is a demonstration that the "saveInFilie" function is called afterwards the file is read.
import matplotlib.pyplot as plt
from matplotlib import patches
class DraggableRectangle:
lock = None # only one can be animated at a time
def __init__(self, rect):
self.rect = rect
self.press = None
self.background = None
def connect(self):
'connect to all the events we need'
self.cidpress = self.rect.figure.canvas.mpl_connect(
'button_press_event', self.on_press)
self.cidrelease = self.rect.figure.canvas.mpl_connect(
'button_release_event', self.on_release)
def on_press(self, event):
'on button press we will see if the mouse is over us and store some data'
if event.inaxes != self.rect.axes: return
if DraggableRectangle.lock is not None: return
contains, attrd = self.rect.contains(event)
if not contains: return
print('Held #', self.rect.xy)
x0, y0 = self.rect.xy
self.saveInFile(str(self.rect.xy))
print("click write succeded")
self.press = x0, y0, event.xdata, event.ydata
DraggableRectangle.lock = self
# draw everything but the selected rectangle and store the pixel buffer
canvas = self.rect.figure.canvas
axes = self.rect.axes
self.rect.set_animated(True)
canvas.draw()
self.background = canvas.copy_from_bbox(self.rect.axes.bbox)
# now redraw just the rectangle
axes.draw_artist(self.rect)
# and blit just the redrawn area
canvas.blit(axes.bbox)
def on_release(self, event):
'on release we reset the press data'
if DraggableRectangle.lock is not self:
return
x0, y0 = self.rect.xy
self.press = None
DraggableRectangle.lock = None
# turn off the rect animation property and reset the background
self.rect.set_animated(False)
self.background = None
self.saveInFile(str(self.rect.xy))
print("release write succeded\n")
# redraw the full figure
self.rect.figure.canvas.draw()
#print("Realeased #", x0, y0)
def saveInFile(self, drop):
filename = "pos.txt"
with open(filename, "w") as file:
file.write(drop)
file.close()
class MyFigure:
def __init__(self):
# Figure initialisation
self.fig = plt.figure()
self.axes = self.fig.add_subplot(1, 1, 1)
self.fig.subplots_adjust(0, 0, 1, 1)
self.axes.set_frame_on(False)
self.axes.invert_yaxis()
self.axes.axis('off')
self.axes.xaxis.set_visible(False)
self.axes.yaxis.set_visible(False)
# Connections
self.ciddouble = self.fig.canvas.mpl_connect('button_press_event', self.createRectangle)
self.CheckClick = self.fig.canvas.mpl_connect('button_press_event', self.checkRectangleOnClick)
self.CheckRelease = self.fig.canvas.mpl_connect('button_release_event', self.checkRectangleOnRelease)
# Variables
self.devices = []
self.drs = []
self.sensorPixelSize = [50, 50]
def createRectangle(self, event):
relPosX = event.x / (self.fig.get_size_inches()[0] * self.fig.dpi)
relPosY = 1 - (event.y / (self.fig.get_size_inches()[1] * self.fig.dpi))
relSizeX = self.sensorPixelSize[0] / (self.fig.get_size_inches()[0] * self.fig.dpi)
relSizeY = self.sensorPixelSize[1] / (self.fig.get_size_inches()[1] * self.fig.dpi)
absPoxX = relPosX * (self.fig.get_size_inches()[0] * self.fig.dpi)
absPosY = relPosY * (self.fig.get_size_inches()[1] * self.fig.dpi)
absSizeX = self.sensorPixelSize[0]
absSizeY = self.sensorPixelSize[1]
if event.dblclick and event.button == 1:
rect = self.axes.add_artist(
patches.Rectangle((relPosX, relPosY), relSizeX, relSizeY, edgecolor='black', facecolor='black',
fill=True))
dr = DraggableRectangle(rect)
dr.connect()
print(dr.rect.xy)
self.drs.append(dr)
self.fig.canvas.draw()
local = ["", "", (relPosX, relPosY), relSizeX, relSizeY, (absPoxX, absPosY), absSizeX, absSizeY]
self.devices.append(local)
def checkRectangleOnClick(self, event):
filename = "pos.txt"
with open(filename, "r") as file:
varia = file.read()
file.flush()
for i in range(len(self.devices)):
if str(varia) == str(self.devices[i][2]):
print("Clicked rectangle #%i" % i)
self.clickedIndex = i
else:
self.clickedIndex = None
def checkRectangleOnRelease(self, event):
filename = "pos.txt"
with open(filename, "r") as file:
varia = file.read()
file.flush()
for i in range(len(self.devices)):
if str(varia) == str(self.devices[i][2]):
print("Realeased rectangle #%i" % i)
self.clickedIndex = i
else:
self.clickedIndex = None
fig = MyFigure()
plt.show()

Resolved this question. In order to put a 'priority' on the event handlers, I only called one function from the event. From then, when the handler finishes its purpose it calls the other handler, which is not connected anymore, and then the second one calls the third one, etc.
I added in the DraggableRectangle class an input of the my personnal canvas class 'MyFigure'. Thus, I can now call functions of MyFigure from the DraggableRectangle. So, in On_Click, after the 'saveInFile' function, I simply call the function checkRectangleOnClick function directly, and then the checkRectangleOnRelease. I disconnected those function from the 'button_click_event'. No more problems, everything happens when it is supposed to.

Related

How can an interactive subplot in Tkinter be improved in terms of speed?

I am currently working on a program that I am creating with Tkinter. Thereby large matrices (signals) are read in, which I represent as an image. In addition, I would like to display the signal at the point X (red vertical line) in the adjacent plot (interactive).
# Imports
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import numpy as np
from tkinter import *
# Global: Selected points with cursor
points = []
# Cursor
class Cursor:
def __init__(self, ax):
self.ax = ax
self.background = None
self.horizontal_line = ax.axhline(color='r', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='r', lw=0.8, ls='--')
self._creating_background = False
ax.figure.canvas.mpl_connect('draw_event', self.on_draw)
def on_draw(self, event):
self.create_new_background()
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
return need_redraw
def create_new_background(self):
if self._creating_background:
return
self._creating_background = True
self.set_cross_hair_visible(False)
self.ax.figure.canvas.draw_idle()
self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)
self.set_cross_hair_visible(True)
self._creating_background = False
def on_mouse_move(self, event, mode: str, matrix=None):
if self.background is None:
self.create_new_background()
if not event.inaxes:
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.restore_region(self.background)
self.ax.figure.canvas.blit(self.ax.bbox)
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
if mode == "both":
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.draw_artist(self.vertical_line)
elif mode == "horizontal":
self.ax.cla()
self.ax.plot(matrix[:, int(x)], range(0, matrix.shape[0], 1))
self.ax.figure.canvas.draw_idle()
self.horizontal_line.set_ydata(y)
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.figure.canvas.blit(self.ax.bbox)
# Graphical User Interface
class ToolGUI:
def __init__(self, master):
self.master = master
# Matrix (Example)
self.matrix = np.random.rand(3000, 5000)
# Subplots
self.fig = plt.figure(constrained_layout=True)
self.spec = self.fig.add_gridspec(5, 6)
self.ax_main = self.fig.add_subplot(self.spec[:, :-1])
self.ax_main.imshow(self.matrix, cmap='gray', aspect='auto')
self.ax_right = self.fig.add_subplot(self.spec[:, -1:], sharey=self.ax_main)
self.ax_right.get_yaxis().set_visible(False)
# Canvas - Drawing Area
self.canvas = FigureCanvasTkAgg(self.fig, master=master)
self.canvas.get_tk_widget().grid(column=0, row=0, sticky=NSEW)
# Cursor with crosshair
self.cursor_main = Cursor(self.ax_main)
self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.cursor_main.on_mouse_move(event, mode="both"))
self.cursor_right = Cursor(self.ax_right)
self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.cursor_right.on_mouse_move(event, mode="horizontal", matrix=self.matrix))
# Update Canvas
self.canvas.draw() # Update canvas
# Create root window
root = Tk()
# Root window title
root.title("Tool")
# Create GUI
my_gui = ToolGUI(root)
# Execute Tkinter
root.mainloop()
This example is only a small part of my program. In my full program, for example, certain points are picked out manually. With the help of the interactive plot a more exact selection of such points is possible.
Unfortunately, the program runs very slowly due to the use of this interactive plot. Since I haven't been working with Python for too long, I would appreciate any suggestions for improvement!
Thanks in advance! - Stefan

How can I add functionality to a user defined button on matplotlib toolbar?

I have a class which sets up a matplotlib figure with the given data and by default it records clicks on the plot. It records every mouseclick, so zooming always add a new point. To prevent that I want to create a toggle button on the mpl toolbar to enable and disable click recording. Here is the code:
import numpy as np
import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolToggleBase
from matplotlib.backend_bases import MouseButton
class EditPeak(object):
def __init__(self, x, y, x_extremal=None, y_extremal=None):
# setting up the plot
self.x = x
self.y = y
self.cid = None
self.figure = plt.figure()
self.press()
plt.plot(self.x, self.y, 'r')
self.x_extremal = x_extremal
self.y_extremal = y_extremal
if not len(self.x_extremal) == len(self.y_extremal):
raise ValueError('Data shapes are different.')
self.lins = plt.plot(self.x_extremal, self.y_extremal, 'ko', markersize=6, zorder=99)
plt.grid(alpha=0.7)
# adding the button
tm = self.figure.canvas.manager.toolmanager
tm.add_tool('Toggle recording', SelectButton)
self.figure.canvas.manager.toolbar.add_tool(tm.get_tool('Toggle recording'), "toolgroup")
plt.show()
def on_clicked(self, event):
""" Function to record and discard points on plot."""
ix, iy = event.xdata, event.ydata
if event.button is MouseButton.RIGHT:
ix, iy, idx = get_closest(ix, self.x_extremal, self.y_extremal)
self.x_extremal = np.delete(self.x_extremal, idx)
self.y_extremal = np.delete(self.y_extremal, idx)
elif event.button is MouseButton.LEFT:
ix, iy, idx = get_closest(ix, self.x, self.y)
self.x_extremal = np.append(self.x_extremal, ix)
self.y_extremal = np.append(self.y_extremal, iy)
else:
pass
plt.cla()
plt.plot(self.x, self.y, 'r')
self.lins = plt.plot(self.x_extremal, self.y_extremal, 'ko', markersize=6, zorder=99)
plt.grid(alpha=0.7)
plt.draw()
return
def press(self):
self.cid = self.figure.canvas.mpl_connect('button_press_event', self.on_clicked)
def release(self):
self.figure.canvas.mpl_disconnect(self.cid)
Where get_closest is the following:
def get_closest(x_value, x_array, y_array):
"""Finds the closest point in a graph to a given x_value, where distance is
measured with respect to x.
"""
idx = (np.abs(x_array - x_value)).argmin()
value = x_array[idx]
return value, y_array[idx], idx
This is the button on the toolbar.
class SelectButton(ToolToggleBase):
default_toggled = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def enable(self, event):
pass
def disable(self, event):
pass
I would like to activate a function when it's triggered, otherwise disable. My problem is that the enable and disable functions are defined in EditPeak class and I couldn't link them to the SelectButton class.
The easiest way you can try it (even though it doesn't really make sense with random values):
x = np.random.normal(0,1,100)
y = np.random.normal(0,1,100)
EditPeak(x, y, x[::2], y[::2])
Left click will add a new point to the closest x value in the given graph, right clicks will delete the closest.
My goal is to call EditPeak.press when the toggle button is on, and call EditPeak.release otherwise.
Any improvement in the code (including style, structure) is appreciated.
Right after:
tm.add_tool('Toggle recording', SelectButton)
you can do something like:
self.my_select_button = tm.get_tool('Toggle recording')
Now the EditPeak instance has a reference to the SelectButton instance created by add_tool that can be used in EditPeak.on_clicked. (The remainder is left as an exercise for the reader :-)

PyQtgraph - Draw ROI by mouse click & drag

I would like to draw ROI's by click and drag events in the PlotWidget. The issue is that several click interactions are already reserved for the PlotWidget, second it is hard to tell the right position of the mouse in the PlotWidget - especially when the image scale has been changed or if the window scale has been changed.
import pyqtgraph as pg
import pyqtgraph.opengl as gl
from pyqtgraph.Qt import QtCore, QtGui, QtWidgets
from PyQt5 import Qt
from vtk.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
import vtk, sys
import numpy as np
from PIL import Image
class GUI:
def __init__(self):
self.init_gui()
def proxyWidget(self, item, width=None, height=None):
proxy = QtGui.QGraphicsProxyWidget()
if(height != None):
height = item.sizeHint().height() if height==None else height
item.setMaximumHeight(height)
if(width!=None):
width = item.sizeHint().width() if width==None else width
item.setMaximumWidth(width)
proxy.setWidget(item)
return proxy
def init_gui(self, win_height=800, win_width=1800):
pg.setConfigOptions(imageAxisOrder='row-major')
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
self.w = pg.GraphicsWindow(size=(win_width,win_height), border=True)
self.img = pg.ImageItem()
self.list_imgs = QtGui.QListWidget()
self.btn_Del_Mark = QtGui.QPushButton('Del Mark')
self.btn_MarkPed = QtGui.QPushButton('Mark ped')
self.lbl_list1 = QtGui.QLabel("List Images")
self.lbl_list2 = QtGui.QLabel("List Markings")
self.list_imgs = QtGui.QListWidget()
self.list_marks = QtGui.QListWidget()
self.layout = QtGui.QGridLayout()
self.w.setLayout(self.layout)
#self.w_3d = pg.GraphicsWindow()
self.vtkWidget = QVTKRenderWindowInteractor()
#self.w_3d.addItem(self.proxyWidget(self.vtkWidget))
self.vtkWidget.Initialize()
self.vtkWidget.Start()
self.ren = vtk.vtkRenderer()
self.vtkWidget.GetRenderWindow().AddRenderer(self.ren)
self.iren = self.vtkWidget.GetRenderWindow().GetInteractor()
# Create source
source = vtk.vtkSphereSource()
source.SetCenter(0, 0, 0)
source.SetRadius(5.0)
# Create a mapper
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(source.GetOutputPort())
# Create an actor
actor = vtk.vtkActor()
actor.SetMapper(mapper)
self.ren.AddActor(actor)
self.ren.ResetCamera()
self.iren.Initialize()
self.iren.Start()
path = "/home/brain/uni/frustum-pointnets/dataset/KITTI/object/testing/image_2/000000.png"
imgdata = Image.open(path)
self.imgArr = np.array(imgdata)
#ToDo: undistort Image if neccessary
self.img.setImage(self.imgArr)
#self.vbLayout = self.w.addLayout(row=0, col=3, rowspan=10, colspan=20)
imageGraph = pg.PlotWidget(name='Signalgraph')
self.vb = imageGraph.plotItem.vb
self.lbl_list1.setAlignment(QtCore.Qt.AlignCenter)
self.lbl_list2.setAlignment(QtCore.Qt.AlignCenter)
self.vb.setAspectLocked()
self.vb.addItem(self.img)
self.vb.invertY(True)
self.vb.setMaximumSize(int(7/10.*win_width), int(9/20.*win_height))
self.layout.addWidget(imageGraph, 1 , 3, 10, 20)
self.layout.addWidget(self.vtkWidget , 11, 3, 10, 20)
self.layout.addWidget(self.lbl_list1 , 0, 1, 1, 1)
self.layout.addWidget(self.lbl_list2 , 0, 2, 1, 1)
self.layout.addWidget(self.list_imgs , 1, 1, 20,1)
self.layout.addWidget(self.list_marks, 1, 2, 20,1)
sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.lbl_list1.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.lbl_list2.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(0.9/20.*win_height))
self.list_imgs.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(18/20.*win_height))
self.list_marks.sizeHint = lambda: pg.QtCore.QSize(int(1./10.*win_width), int(18/20.*win_height))
self.list_imgs.setMaximumWidth(int(1./10.*win_width))
self.list_marks.setMaximumWidth(int(1./10.*win_width))
self.vtkWidget.show()
if __name__ == "__main__":
app = QtGui.QApplication([])
guiobj = GUI()
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I would like to start drawing the ROI by mouse click and stop drawing by mouse release... every hint would be helpful. Please consider that the content of the PlotWidget is drag-able and that it might need to be frozen while drawing a ROI.
EDIT:
I have tried to overwrite temporary the click events with the following lines, but somehow clickevents seem to be triggered somewhere else, since my functions do not get called...
def on_btn_MarkPed(self):
#self.vb.setMouseEnabled(x=False, y=False)
self.creatRoiByMouse("Pedestrian")
def on_btn_MarkCycl(self):
self.creatRoiByMouse("Cyclist")
def on_btn_MarkVehicle(self):
self.creatRoiByMouse("Vehicle")
def creatRoiByMouse(self, class2Mark):
self.img.mousePressEvent = self.ImgMousePressEvent
self.img.mouseReleaseEvent = self.ImgMouseReleaseEvent
def ImgMousePressEvent(self, event):
print(event)
pass
#
#
def ImgMouseReleaseEvent(self, event):
print(event)
pass
I know this post is old, but in case someone else finds it someday I thought I'd post my solution. It's inelegant, but it works for me. In my application, I have a button labeled "draw" that calls a function that overwrites the mouse drag event temporarily to simply draw a box instead. The overwritten mouse drag event restores the native mouse drag event with the finish signal. This code also overwrites the escape key to cancel the draw if pressed after pushing my draw button but before drawing. In my code, imageArrayItem is an existing imageItem in a plot widget, and Dialog is a QDialog containing the plot widget.
def clickDraw(self):
app.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CrossCursor))
imageArrayItem.getViewBox().setMouseMode(ViewBox.RectMode)
imageArrayItem.getViewBox().rbScaleBox.setPen(fn.mkPen((255, 255, 255), width = 1))
imageArrayItem.getViewBox().rbScaleBox.setBrush(fn.mkBrush(255, 255, 255, 100))
def mouseDragEvent(ev, axis = None): # This is a modified version of the original mouseDragEvent function in pyqtgraph.ViewBox
ev.accept() # accept all buttons
dif = (ev.pos() - ev.lastPos()) * -1
mouseEnabled = np.array(imageArrayItem.getViewBox().state['mouseEnabled'], dtype = np.float)
mask = mouseEnabled.copy()
if ev.button() & QtCore.Qt.LeftButton:
if imageArrayItem.getViewBox().state['mouseMode'] == ViewBox.RectMode:
if ev.isFinish():
QtCore.QTimer.singleShot(0, self.restoreCursor)
imageArrayItem.getViewBox().rbScaleBox.hide()
ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(ev.pos()))
ax = imageArrayItem.getViewBox().childGroup.mapRectFromParent(ax)
imageArrayItem.getViewBox().mouseDragEvent = temp # reset to original mouseDragEvent
imageArrayItem.getViewBox().setMouseMode(ViewBox.PanMode)
else:
imageArrayItem.getViewBox().updateScaleBox(ev.buttonDownPos(), ev.pos()) # update shape of scale box
elif ev.button() & QtCore.Qt.MidButton: # allow for panning with middle mouse button
tr = dif*mask
tr = imageArrayItem.getViewBox().mapToView(tr) - imageArrayItem.getViewBox().mapToView(Point(0,0))
x = tr.x() if mask[0] == 1 else None
y = tr.y() if mask[1] == 1 else None
imageArrayItem.getViewBox()._resetTarget()
if x is not None or y is not None:
imageArrayItem.getViewBox().translateBy(x=x, y=y)
imageArrayItem.getViewBox().sigRangeChangedManually.emit(imageArrayItem.getViewBox().state['mouseEnabled'])
def keyPressE_mouseDrag(event): # Override "esc" key to cancel draw
if event.key() == QtCore.Qt.Key_Escape:
QtCore.QTimer.singleShot(0, self.restoreCursor)
imageArrayItem.getViewBox().rbScaleBox.hide()
imageArrayItem.getViewBox().mouseDragEvent = temp # reset to original mouseDragEvent
imageArrayItem.getViewBox().setMouseMode(ViewBox.PanMode)
else:
QtWidgets.QDialog.keyPressEvent(Dialog, event)
Dialog.keyPressEvent = keyPressE_mouseDrag
temp = imageArrayItem.getViewBox().mouseDragEvent # save original mouseDragEvent for later
imageArrayItem.getViewBox().mouseDragEvent = mouseDragEvent # set to modified mouseDragEvent

How to start an animation loop in Tkinter for an event?

I wrote some code using Tkinter in Python 3 that plots a graph in a canvas. I also made it such that when I move the mouse over the canvas the graph scrolls to the left.
The problem is that I want the graph to scroll when I press the space bar for example. But I don't want it to scroll 1 step each time I press the space bar but I want it to start scrolling indefinitely when I press it once and stop the scroll when I press it again. I want the space bar to be a play/pause key.
How can I accomplish this? I don't want to use matplotlib anywhere.
MY CODE AS IT IS NOW:
from tkinter import *
import numpy as np
# The function of the graph
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
# Data for the graph and steps to move to the right
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
# A switch to delete to clear the canvas each iteration before plotting the next frame
self.gate = False
# Setting the Tkinter window and the canvas in place
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws*0.75
hw = self.hs*0.50
self.canvas = Canvas(self.master, width = ww, height = hw, bg = 'black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
# Plot first frame
self.drawData(self.data_x, self.data_y)
# Plot next frames each time I press the space bar
self.canvas.bind('<KeyPress-space>', self.updateData)
def drawData(self, data_x, data_y):
'''This is my function to plot a grpah in a canvas
canvas without embedding any matplotlib figure'''
# Setting the axis limits
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
# Translating data to pixel positions inside the canvas
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
# Deleting previous frame before plotting the next frame (except for the first frame)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
# Plotting
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def updateData(self, event):
# Changing data for the next frame
self.data_x += self.step
self.data_y = f(self.data_x)
# Plot new frame
self.drawData(self.data_x, self.data_y)
root = Tk()
GraphPlot(root)
root.mainloop()
I've tried some ideas. For example I used a new function, PlayPause(), with a while loop and a new switch, self.go, but this didn't work as expected.
CODE THAT I EXPECTED TO WORK BUT DIDN'T:
from tkinter import *
import numpy as np
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
self.go = False # The new switch
self.gate = False
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws*0.75
hw = self.hs*0.50
self.canvas = Canvas(self.master, width = ww, height = hw, bg = 'black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
self.drawData(self.data_x, self.data_y)
self.canvas.bind('<KeyPress-space>', self.PlayPause)
def drawData(self, data_x, data_y):
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def updateData(self):
self.data_x += self.step
self.data_y = f(self.data_x)
self.drawData(self.data_x, self.data_y)
def PlayPause(self, event):
if self.go:
self.go = False
else:
self.go = True
while self.go:
self.updateData()
root = Tk()
GraphPlot(root)
root.mainloop()
You could add a method to toggle_play_pause, and bind the space key to it. Upon space key press, this method toggles a boolean flag pause that when turned off allows the update to be called.
update will keep calling itself every 10/1000 of a second, until the space key is pressed again, and the pause flag set to True.
import tkinter as tk
import numpy as np
def f(x):
return np.sin(x)+np.sin(3*x-1)+np.sin(0.5*(x+np.pi))+0.3*np.sin(10*x)
class GraphPlot():
def __init__(self, master):
self.master = master
# Data for the graph and steps to move to the right
self.data_x = np.linspace(0, 4*np.pi, 1000)
self.data_y = f(self.data_x)
self.step = 0.1
# A switch to delete to clear the canvas each iteration before plotting the next frame
self.gate = False
# Setting the Tkinter window and the canvas in place
self.ws = master.winfo_screenwidth()
self.hs = master.winfo_screenheight()
ww = self.ws * 0.75
hw = self.hs * 0.50
self.canvas = tk.Canvas(self.master, width=ww, height=hw, bg='black')
self.canvas.grid()
self.master.update()
self.w = self.canvas.winfo_width()
self.h = self.canvas.winfo_height()
self.canvas.focus_set()
# Plot first frame
self.drawData(self.data_x, self.data_y)
# Plot next frames each time I press the space bar
self.canvas.bind('<KeyPress-space>', self.toggle_play_pause)
self.pause = True
self._update_call_handle = None
def drawData(self, data_x, data_y):
'''This is my function to plot a grpah in a canvas
canvas without embedding any matplotlib figure'''
# Setting the axis limits
x_min, x_max = min(data_x), max(data_x)
y_min, y_max = min(data_y), max(data_y)
# Translating data to pixel positions inside the canvas
pixel_x = (data_x-x_min)*self.w/(x_max-x_min)
pixel_y = -(data_y-y_max)*self.h/(y_max-y_min)
points = []
for i in range(len(data_x)):
points.append(pixel_x[i])
points.append(pixel_y[i])
points = tuple(points)
# Deleting previous frame before plotting the next frame (except for the first frame)
if self.gate:
self.canvas.delete('curve')
else:
self.gate = True
# Plotting
self.canvas.create_line(points, fill = 'white', tag = 'curve')
def toggle_play_pause(self, dummy_event):
self.pause = not self.pause
if not self.pause:
self.updateData()
def updateData(self):
# Changing data for the next frame
self.data_x += self.step
self.data_y = f(self.data_x)
# Plot new frame
self.drawData(self.data_x, self.data_y)
if not self.pause:
self._update_call_handle = root.after(10, self.updateData)
else:
root.after_cancel(self._update_call_handle)
self._update_call_handle = None
root = tk.Tk()
GraphPlot(root)
root.mainloop()

Interactive PyPlot Figure in Tkinter not registering MouseEvents

I am building an interactive Tkinter GUI wherein a blank pyplot figure and axes are drawn in the GUI along with some buttons, where the user can click, drag, and delete points to form a custom point plot. The coordinates of these points can then be printed in a particular format used as Input in a much more complex Fortran code. I have gotten almost everything to work, except for the initial interactivity of the figure/axes space. I am heavily relying on a wonderful Draggable-Plot object code I found on GitHub by user yuma-m, link below:
https://github.com/yuma-m/matplotlib-draggable-plot/blob/master/draggable_plot.py
After much tweaking of the original Draggable-Plot object I was able to get the interactive plot integrated into my GUI; HOWEVER the bug comes in when I generate the plot for the first time. After setting the correct axes bounds, and clicking 'Update Axes' for the FIRST time, the figure and plot are drawn but do NOT register any MouseEvents. My guess is that when the event.inaxes in [self._axes] condition is checked in the _on_click function, the existence/placement of self._axes is being blocked in some way.
The best part happens when you click the 'Update Axes' button a second time, and a new axes object is plotted directly below the first. When this occurs, the script will begin to register MouseEvents in the INITIAL plot, but will draw all corresponding points in the new SECOND plot. When I restrict the placement of the second plot in the same grid position as the first, no interactivity is registered, as I'm guessing the new axes overlaps the first.
I'm simply looking for a solution to this bizarre problem; obviously the ideal functionality of this GUI would be initial interactivity of the first generated plot, with any subsequently generated axes behaving the same. Thank you!
Image of Two Axes state of GUI
import math
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import tkinter as tk
from tkinter import scrolledtext
from tkinter import *
from tkinter.ttk import *
class B2PtGen(tk.Tk):
def __init__(self):
root = Tk()
root.title("B2 Inputfile Point Generator")
root.geometry("800x800")
app = Frame(root)
app.grid()
self._Xmin = -0.1
self._Xmax = 0.1
self._Ymin = 0
self._Ymax = 1
self._figure, self._axes, self._line = None, None, None
self._dragging_point = None
self._points = {}
Instr = Label(app,text = "Enter ranges of X and Y axes in corresponding text boxes below, then click 'Update Axes'. \nUse plot area to draw shape of Inputfile Curve \n(Left Mouse Button to create points, Right Mouse Button to delete points). \nThen click 'Generate Point List' to create Inputfile Point List")
Instr.grid(column=0,row=0,columnspan=4)
Lbl1 = Label(app, text = "X min")
Lbl1.grid(column=0,row=1)
XminT = Entry(app, width=10)
XminT.insert(0,'-0.1')
XminT.grid(column=0,row=2)
Lbl2 = Label(app, text = "X max")
Lbl2.grid(column=1,row=1)
XmaxT = Entry(app, width=10)
XmaxT.insert(0,'0.1')
XmaxT.grid(column=1,row=2)
Lbl3 = Label(app, text = "Y min")
Lbl3.grid(column=0,row=3)
YminT = Entry(app, width=10)
YminT.insert(0,'0')
YminT.grid(column=0,row=4)
Lbl4 = Label(app, text = "Y max")
Lbl4.grid(column=1,row=3)
YmaxT = Entry(app, width=10)
YmaxT.insert(0,'1')
YmaxT.grid(column=1,row=4)
def clicked():
if float(XminT.get()) < float(XmaxT.get()) and float(YminT.get()) < float(YmaxT.get()):
self._Xmin = float(XminT.get())
self._Xmax = float(XmaxT.get())
self._Ymin = float(YminT.get())
self._Ymax = float(YmaxT.get())
Lbl1.configure(text = "Xmin = " + XminT.get())
Lbl2.configure(text = "Xmax = " + XmaxT.get())
Lbl3.configure(text = "Ymin = " + YminT.get())
Lbl4.configure(text = "Ymax = " + YmaxT.get())
self._init_plot(app)
else:
print("Input values do not form valid ranges")
button1 = Button(app, command=clicked)
button1.grid(column=2,row=2,columnspan=2)
button1['text'] = "Update Axes"
root.mainloop()
def _init_plot(self, app):
if not self._figure:
self._figure = plt.figure(num=1)
if not self._axes:
print('New Axes!')
self._axes = plt.axes()
plt.sca(self._axes)
self._axes.set_xlim(self._Xmin, self._Xmax)
self._axes.set_xlabel('Radial Distance from Separatrix (along Outer Midplane) [m]')
self._axes.set_ylabel('Normalized Coefficient Magnitude')
self._axes.set_ylim(self._Ymin, self._Ymax)
self._axes.grid(b=True,which="both")
#self._axes = axes
self._figure.canvas.mpl_connect('button_press_event', self._on_click)
self._figure.canvas.mpl_connect('button_release_event', self._on_release)
self._figure.canvas.mpl_connect('motion_notify_event', self._on_motion)
canvas = FigureCanvasTkAgg(self._figure, app)
canvas.show()
canvas.get_tk_widget().grid(columnspan=4)
def _update_plot(self):
if not self._points:
return
x, y = zip(*sorted(self._points.items()))
# Add new plot
if not self._line:
self._line, = self._axes.plot(x, y, "b", marker="o", markersize=5)
# Update current plot
else:
self._line.set_data(x, y)
self._figure.canvas.draw()
def _add_point(self, x, y=None):
if isinstance(x, MouseEvent):
x, y = float(x.xdata), float(x.ydata)
self._points[x] = y
return x, y
def _remove_point(self, x, _):
if x in self._points:
self._points.pop(x)
def _find_neighbor_point(self, event):
u""" Find point around mouse position
:rtype: ((int, int)|None)
:return: (x, y) if there are any point around mouse else None
"""
distance_threshold = 0.05*(self._Ymax - self._Ymin)
nearest_point = None
min_distance = math.sqrt((self._Xmax - self._Xmin)**2 + (self._Ymax - self._Ymin)**2)
for x, y in self._points.items():
distance = math.hypot(event.xdata - x, event.ydata - y)
if distance < min_distance:
min_distance = distance
nearest_point = (x, y)
if min_distance < distance_threshold:
return nearest_point
return None
def _on_click(self, event):
u""" callback method for mouse click event
:type event: MouseEvent
"""
# left click
if event.button == 1 and event.inaxes in [self._axes]:
point = self._find_neighbor_point(event)
if point:
self._dragging_point = point
self._remove_point(*point)
else:
self._add_point(event)
print('You clicked!')
self._update_plot()
# right click
elif event.button == 3 and event.inaxes in [self._axes]:
point = self._find_neighbor_point(event)
if point:
self._remove_point(*point)
self._update_plot()
def _on_release(self, event):
u""" callback method for mouse release event
:type event: MouseEvent
"""
if event.button == 1 and event.inaxes in [self._axes] and self._dragging_point:
self._add_point(event)
self._dragging_point = None
self._update_plot()
def _on_motion(self, event):
u""" callback method for mouse motion event
:type event: MouseEvent
"""
if not self._dragging_point:
return
self._remove_point(*self._dragging_point)
self._dragging_point = self._add_point(event)
self._update_plot()
if __name__ == "__main__":
B2PtGen()
Try changing your _init_plot() function to the following, seems to update a little better...
if not self._figure:
self._figure = plt.figure(num=1)
canvas = FigureCanvasTkAgg(self._figure, app)
canvas.show()
canvas.get_tk_widget().grid(columnspan=4)
if not self._axes:
print('New Axes!')
self._axes = plt.axes()
self._axes.set_xlim(self._Xmin, self._Xmax)
self._axes.set_xlabel('Radial Distance from Separatrix (along Outer Midplane) [m]')
self._axes.set_ylabel('Normalized Coefficient Magnitude')
self._axes.set_ylim(self._Ymin, self._Ymax)
self._axes.grid(b=True,which="both")
self._figure.sca(self._axes)
self._figure.canvas.mpl_connect('button_press_event', self._on_click)
self._figure.canvas.mpl_connect('button_release_event', self._on_release)
self._figure.canvas.mpl_connect('motion_notify_event', self._on_motion)
self._figure.canvas.draw()

Categories

Resources