Different behavior between PolygonInteractor / LineBuilder in Python / MatplotLib / Tkinter - python

I have been trying to solve my problem without success. As a bit of background to explain the situation. I am developing a GUI tool (with Tkinter) that displays, creates or modify polygons and 2Dlines.
To do this, I am using a LineBuilder class Link to class used and also the PolygonInteractor class Link to class.
As you can see they are very similar, and I checked that callbacks (in debug mode) are created similarly.
My main code, in both situation, calls each class (i'll take the Line2D example that doesn't work) from a menu created in the root window, that calls a class that creates an sub-window (just to save the new line for example). (the code it not supposed to work "as-is" it's just for illustration of the algorithm):
class create_Road(tk.Toplevel): # called from menu of main app
def __init__(self, master):
tk.Toplevel.__init__(self, master)
self.master.status_bar.config(text="Creating a new 2DLine...")
ttk.Button(self, text="Cancel", command=lambda: self.quitAR()).pack(side="bottom", padx=5, pady=5)
ttk.Button(self, text="Save Area", command=lambda: self.saveAR())\
.pack(side="bottom", padx=5, pady=5)
fig, ax = Models.openmodel(modelname) # this creates a matplolib used everywhere in the app
self.fig = fig
xlim = ax.get_xlim()
ylim = ax.get_ylim()
dx = (xlim[1] - xlim[0]) / 4
dy = (ylim[1] + ylim[0]) / 2
line = Line2D([xlim[0] + dx, xlim[1] - dx], [dy, dy], linestyle=':', color='red',
marker='o', markerfacecolor='yellow')
ax.add_line(line) # add a line to the axes determined before
linebuilder = LineBuilder(ax, line) # call the class as mentionned in the link above
fig.canvas.mpl_connect('close_event', lambda event: self.AR_handle_close(event))
fig.canvas.manager.window.wm_geometry(str(self.master.winfo_width() - 330) + "x" +
str(self.master.winfo_height() - 50) + "+300+30")
fig.show()
The LineBuilder _ _ init _ _ method is the folowing :
class LineBuilder(object):
epsilon = 30 #in pixels
def __init__(self, ax, line):
canvas = line.figure.canvas
self.line = line
self.axes = ax
self.xs = list(line.get_xdata())
self.ys = list(line.get_ydata())
self.ind = None
canvas.mpl_connect('button_press_event', self.button_press_callback)
canvas.mpl_connect('button_release_event', self.button_release_callback)
canvas.mpl_connect('key_press_event', self.key_press_callback)
canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
self.canvas = canvas
My problem is that none of the events handling in the class LineBuilder works in this module. I creates a PolygonInteractor in the same module and it works perfectly. What I mean is I have some print('message') for each methods of the event handling, and none of them are called back for the LineBuilder class.
What surprises me is if I run the code in debug mode, and I set a breakpoint just before the code returns to the self.tk.mainloop(n), then I works perfectly. And if I use the PolygonInteractor in the same module, everything works fine.
Anyway, thank you in advance for any help! I could cheat and use the PolygonInteractor to manipulate these lines but... well...
Kristen

Okay. Hope it will help someone. I got a hint on another page mentioning week-reference to event handling. It was the key to answer this problem.
I only had to modify this to have the line builder working:
self.linebuilder = LineBuilder(ax, line)
Instead of only linebuilder = ...
Well, one challenge solved. Thank you anyway to the wonderful community of StackOverflow. Lots of help here...
Krys

Related

Matplotlib FuncAnimation in Tkinter - Axes labels not being displayed/retained

I am using Python with TKinter as GUI; and Matplotlib's FuncAnimation, to display 2 graphs next to each other. These graphs are representing sensor readings (temperature, humidity), and I would like to label both the x-axis and y-axis. (The matplotlib graph title is not required, but I am including it for testing purposes)
I'll try not to divulge unnecessary details; but in simple terms, I have 2 classes representing the sensors, "temperature" and "humidity", and another class "main_window" to combine both the classes into the Tkinter GUI.
This is a preview of the main.py file; including the main_window class.
class main_window(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.wm_title("Indoor Monitoring System")
frame1 = tk.Frame(self)
frame1.grid(column=0, row=0)
frame2 = tk.Frame(self)
frame2.grid(column=1, row=0)
temperature(frame1, self)
humidity(frame2, self)
if __name__ == "__main__":
runObj = main_window()
runObj.mainloop()
and this is a preview of the temperature class
class temperature(tk.Frame):
def __init__(self, parent, controller):
pd.set_option('display.float_format', lambda x: '%.2f' % x)
plt.style.use('fivethirtyeight')
self.x_values = []
self.y_values = []
self.counter = 0
self.fig = plt.figure(figsize=(8, 4), dpi=100)
self.ax1 = self.fig.add_subplot(111)
self.ax1.set(xlabel="Reading Counter", ylabel="Degrees Celcius")
label = tk.Label(parent, text="Temperature Graph", font=("Arial", 12)).grid(column=0, row=0)
canvas = FigureCanvasTkAgg(self.fig, master=parent)
canvas.get_tk_widget().grid(column=0, row=1)
self.ani1 = FuncAnimation(self.fig, self.animate, interval=10000, blit=False)
def animate(self, i):
df = pd.read_csv('C:\\log.csv')
x_temporary_values = df["Counter"]
y_temp_values = df["Temp"]
currrow = x_temporary_values[self.counter]
if currrow is not None:
self.x_values.append(currrow)
self.y_values.append(y_temp_values[self.counter])
self.ax1.plot(self.x_values, self.y_values)
# self.ax1.set(xlabel="Reading Counter", ylabel="Degrees Celcius")
self.counter += 1
The humidity class is very similar.
I am trying to set the xlabel and ylabel as such:
self.ax1.set(xlabel="Reading Counter", ylabel="Degrees Celcius", title="Temperature")
I've also tried:
self.ax1.set_title("Temperature")
self.ax1.set_xlabel("Reading Counter")
self.ax1.set_ylabel("Degrees Celcius")
The title and y_label are initially displayed. After refreshing the graph via the "animate" funxction, the title is retained, the y_label is not. The x_label is never shown.
I have also tried to re-call the self.ax1.set() in the animation function. Does not make a difference.
What am I doing wrong?
Note: that this setup is the only one that worked for me, in the context of trying to animate the graphs. Most other setups that I was more accustomed to, do not work with FuncAnimation.
Also, note that the UI is not refined by any means (especially font sizes). I am concerned with fixing the label issues first.

Python tkinter hangs when I try to close graphs

I've tried using Tk to make a function that will allow users to look at a graph and select if points are wrong. I had this working before, but now my code is hanging every time I run it and try to go to the next graph. Specifically when I click "next graph" and it should run
def _quit(self):
self.master.destroy() # stops mainloop
When I manually stop the code I get this message:
File "C:\Users\laura\Anaconda3\lib\tkinter\__init__.py", line 1429, in mainloop
self.tk.mainloop(n)
KeyboardInterrupt
Any suggestions? I've read using root.destroy() but I haven't been able to get that to work
Thanks!
def config_plot():
tk, ax = plt.subplots()
ax.set(title='Are the peaks okay?')
return (tk, ax)
class matplotlibSwitchGraphs:
def __init__(self, master):
self.master = master
self.frame = Frame(self.master)
self.frame.pack(expand=YES, fill=BOTH)
self.fig = Figure(figsize=(12,7))
self.ax = self.fig.gca()
self.canvas = FigureCanvasTkAgg(self.fig, self.master)
self.config_window()
self.graphIndex = 0
self.draw_graph_one()
def config_window(self):
toolbar = NavigationToolbar2Tk(self.canvas, self.master)
toolbar.update()
self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
print('connect')
self.canvas.mpl_connect("button_press_event", self.on_button_press)
self.button = Button(self.master, text="YES, next graph", command=self._quit) #this is where it gets stuck!
self.button.pack(side=BOTTOM)
self.button_switch = Button(self.master, text="NO", command=self.switch_graphs)
self.button_switch.pack(side=BOTTOM)
def draw_graph_one(self):
self.ax.clear() # clear current axes
self.ax.plot(data)
self.ax.plot(peaks,loc_peaks, marker='o', linestyle='none')
# self.ax.set(xlim=(touchdown_cut[-15]-50,toeoff_cut[-5]+50))
self.ax.set(title='Check the data')
self.canvas.draw()
def draw_graph_two(self):
self.ax.clear()
self.ax.plot(data)
self.ax.set(title='Click all the incorrect peaks')
self.ax.plot(peaks,loc_peaks, marker='o', linestyle='none')
self.canvas.draw()
def on_button_press(self, event):
print('xdata, ydata:', event.xdata, event.ydata)
# return (event.xdata)
global ix
ix = event.xdata
global clicks
clicks.append((ix))
array1 = np.asarray(peaks)
idx1 = (np.abs(array1 - event.xdata)).argmin()
global peaks_adj
global loc_peaks_adj
peaks_adj = np.delete(peaks, idx1)
loc_peaks_adj = np.delete(loc_peaks, idx1)
self.canvas.flush_events()
self.ax.clear()
self.ax.plot(data)
self.ax.set(title='Click all the incorrect peaks')
self.ax.plot(peaks_adj,loc_peaks_adj, marker='o', linestyle='none')
self.canvas.draw()
return (clicks)
def _quit(self):
self.master.destroy() # stops mainloop
def switch_graphs(self):
# Need to call the correct draw, whether we're on graph one or two
self.graphIndex = (self.graphIndex + 1 ) % 2
if self.graphIndex == 0:
self.draw_graph_one()
else:
self.draw_graph_two()
def main():
root = Tk()
matplotlibSwitchGraphs(root)
root.mainloop()
if __name__ == '__main__':
main()
Try changing root.destroy() to self.destroy()as you are working with classes. If using function through a button remove the()after command likecommand=self.destroy`.
Also don't use global, use self. to create a class variable if you want to access the variable in multiple function.
As far as for lags, I would recommend you to use update and update_idletasks.

How do I highlight points by clicking on legend entries in matplotlib?

I am plotting a simple scatter plot with 3 groups of data, labeled A, B and C.
How do I write the code to allow clicking on legend entry A and highlighting points associated with A, while dimming points associated with labels B and C?
Is it possible to select (by click and drag, or click on multiple legend entries with ctrl key) and highlighting the associated points, while dimming points having other labels (labels not selected on the legend)?
Following the example here https://matplotlib.org/3.1.1/gallery/event_handling/legend_picking.html (which uses plot instead of scatter), I have made some progress, but can't quite get the code to work the way I want it. In the example, they set the picker property for each legend entry via leg.get_lines(), and I tried a similar thing (leg.get_patches()) which gave an empty dict. I can't make further progress, hopefully someone can help. Thanks in advance.
import numpy as np
import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class MainApplication(tk.Frame):
def __init__(self, root, *args, **kwargs):
tk.Frame.__init__(self, root, *args, **kwargs)
self.root = root
self.frame = tk.Frame(self.root)
self.frame.grid(row=0, column=0)
self.plot_button = tk.Button(self.frame, text='Plot XY', command=self.xy_plot)
self.plot_button.grid(row=0, column=0)
self.figure = plt.figure(figsize=(5,5))
self.canvas = FigureCanvasTkAgg(self.figure, master=self.frame)
self.canvas.get_tk_widget().grid(row=1, column=0)
self.X = [np.random.rand(5), np.random.rand(5), np.random.rand(5)]
self.Y = [np.random.rand(5), np.random.rand(5), np.random.rand(5)]
self.labels = ['A','B','C']
self.figure.canvas.mpl_connect('pick_event', self.onpick)
def xy_plot(self):
ax = self.figure.add_subplot(111)
self.pts = []
for x, y, grp in zip(self.X, self.Y, self.labels):
self.pts.append(ax.scatter(x, y, label=grp))
leg = ax.legend(self.pts, self.labels, fontsize=12)
leg.set_picker(5) #should be set for individual entries? If so, how?
self.canvas.draw()
def onpick(self, event):
#obviously this function needs to be modified
self.pts[0].set_alpha(0.9)
self.pts[1].set_alpha(0.1
self.canvas.draw()
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root)
root.mainloop()
All the examples that I came across involve looping over leg.get_lines() and setting the set_picker(value) for each element in leg.get_lines(). However, leg.get_lines() is empty for scatter plot. I tried looking at leg.dict['legendHandles'] and I see that it consists of 3 (in my case) collections.PathCollection objects. Do these objects have set_picker() methods? It seems to me that legend for a scatter is very different from legend for a plot. Can someone shed light on this?
By analogy to the example shown in the link in my question, I looped over the legendHandles and set the set_picker(5). This allows clicking on different legend entries and allows me to get the label via event.artist.get_label(). The modification to the onpick function seems like a hack, but it does what I want. Is there a more elegant and proper way to loop over the legendHandles than what I am doing? Any suggestions to improve the "solution" is welcome.
def xy_plot(self):
ax = self.figure.add_axes([0,0,1,1])
self.pts = []
for x, y, grp in zip(self.X, self.Y, self.labels):
self.pts.append(ax.scatter(x, y, label=grp))
self.leg = ax.legend(self.pts, self.labels)
for obj in self.leg.__dict__['legendHandles']:
obj.set_picker(5)
self.canvas.draw()
def onpick(self, event):
groups = {key: val for key, val in zip(self.labels, self.pts)}
label = event.artist.get_label()
for key in groups:
if key == label:
groups[key].set_alpha(1.0)
else:
groups[key].set_alpha(0.2)
self.canvas.draw()

Pickable matplotlib plot in tkk frame

I would like to have a GUI where one part of the interface contains a plot and the rest of the window some tools to work around the plot.
I would like to use mpl_connect to connect the matplotlib canvas with the tkk frame so that I can choose points in the plot to work with.
This was my try, which cowardly refuses to do what I think it should do:
import Tkinter as tk
import ttk
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2TkAgg)
import matplotlib.pyplot as plt
import numpy as np
class Frame_examples_program():
def __init__(self):
self.window = tk.Tk()
self.window.title("Amazing GUI 5000")
self.create_widgets()
def create_widgets(self):
self.window['padx'] = 10
self.window['pady'] = 10
# - - - - - - - - - - - - - - - - - - - - -
# Frame
frame1 = ttk.Frame(self.window, relief=tk.RIDGE)
frame1.grid(row=0, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)
frame2 = ttk.Frame(self.window, relief=tk.RIDGE)
frame2.grid(row=1, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)
self.PlotFrame(frame1, frame2)
class PlotFrame():
# The plot
def __init__(self, parent1, parent2):
self.parent1 = parent1
self.parent2 = parent2
canvas = self.plot()
self.plot_toolbar(canvas)
def plot(self):
# the actual plot
fig, ax = plt.subplots()
plt.imshow(np.ones((100,100)),picker=True)
canvas = FigureCanvasTkAgg(fig, self.parent1)
canvas.mpl_connect('button_press_event', self.onclick)
return(canvas)
def plot_toolbar(self, canvas):
# the tool bar to the plot
toolbar = NavigationToolbar2TkAgg(canvas, self.parent2)
toolbar.update()
canvas.get_tk_widget().grid(row=1, column=1)
canvas.draw()
def onclick(self, event):
# the devilish thing that does nothing!
print('WOHOOOO')
# Create the entire GUI program
program = Frame_examples_program()
# Start the GUI event loop
program.window.mainloop()
As you'll see when you run this, the matplotlib toolbar nicely works, but I just can't call the onclick event! Why?
The PlotFrame instance that is created via self.PlotFrame(frame1, frame2) is not stored anywhere and hence garbage collected. At the point where you expect the callback to happen this instance does not exist in memory anymore.
Solution: Make sure to keep a reference to the PlotFrame at all time, e.g.
self.myplot = self.PlotFrame(frame1, frame2)
Note that this is a more or less general rule: You would almost never instantiate a class without storing it anywhere. In case you do and don't run into trouble, that would mostly be sign that the class is not needed at all.

Can I change the interval of a previously created matplotlib FuncAnimation?

I'm trying to figure out if is there any way for me to change the interval of an existing matplotlib FuncAnimation. I want to be able to adjust the speed of the animation according to the user input.
I found a similar question How do I change the interval between frames (python)?, but since it got no answer I thought I would ask it anyway.
A minimal example of what I need and have is:
"""
Based on Matplotlib Animation Example
author: Jake Vanderplas
https://stackoverflow.com/questions/35658472/animating-a-moving-dot
"""
from matplotlib import pyplot as plt
from matplotlib import animation
import Tkinter as tk
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
class AnimationWindow(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.fig = plt.figure(0, figsize=(10, 10))
self.anim = None
self.speed = 2
self.canvas = FigureCanvasTkAgg(self.fig, self)
self.canvas.show()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.canvas.mpl_connect('resize_event', self.on_resize)
self.bar = tk.Scale(self, from_=0.25, to=10, resolution=0.25, command=self.change_play_speed, orient=tk.HORIZONTAL)
self.bar.pack(fill=tk.X)
def start_animation(self):
ax = plt.axes()
self.x = np.arange(0, 2 * np.pi, 0.01)
self.line, = ax.plot(self.x, np.sin(self.x))
# The return needs to be assigned to a variable in order to prevent the cleaning by the GC
self.anim = animation.FuncAnimation(self.fig, self.animation_update, frames=100,
interval=100/self.speed, blit=True, repeat=False)
def animation_update(self, i):
self.line.set_ydata(np.sin(self.x + i / 10.0)) # update the data
return self.line,
return tuple(self.annotation)
def change_play_speed(self, speed):
self.speed = float(speed)
# This works but I think somehow the previous animation remains
#self.anim = animation.FuncAnimation(self.fig, self.animation_update, frames=100, interval=100/self.speed, blit=True, repeat=False)
def on_resize(self, event):
"""This function runs when the window is resized.
It's used to clear the previous points from the animation which remain after resizing the windows."""
plt.cla()
def main():
root = tk.Tk()
rw = AnimationWindow(root)
rw.pack()
rw.start_animation()
root.mainloop()
if __name__ == '__main__':
main()
In the change speed function I have a commented solution to this problem. This solution presents two main problems: it is most likely very inefficient (I think); and I haven't figured out a way for me to delete the previous animation which results in flickering.
I would not recomment to delete the animation. One option for more complex animations is of course to program them manually. Using a timer which repeatedly calls the update function is actually not much more code than creating the FuncAnimation.
However in this case the solution is very simple. Just change the interval of the underlying event_source:
def change_play_speed(self, speed):
self.speed = float(speed)
self.anim.event_source.interval = 100./self.speed

Categories

Resources