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()
Related
I have 3 sheets in a notebook that do the same thing as I show in the code below. (This code works fine here but not in my code)
All sheets have the function checkbox_O and g_checkbox with the same name and all use the global variable lines
When I press the graph button, the graphs are displayed but the checkboxes only work fine on sheet 1.
I did code tests and I realize that, in sheet 2 and 3, the g_checkbox function does not have access to the lines array (I don't know why)
This is what I tried to fix it and it didn't work:
tried to write g_checkbox function as a class method
I tried to place a global lines variable for each sheet
I changed the name of the function checkbox_O in each sheet, so that it is different in each one of them
I need help please to know how to make the checkboxes work in all three sheets.
I don't know what I'm doing wrong and I don't know how to fix it.
Thanks in advance
import numpy as np
import tkinter as tk
from tkinter import ttk
import matplotlib
from matplotlib import pyplot as plt
matplotlib.use('TkAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import(FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib import style
from matplotlib.widgets import CheckButtons, Button, TextBox, RadioButtons
x = np.arange(-15, 15, .01)
lines = []
class root(tk.Tk):
def __init__(self):
super().__init__()
self.geometry('750x618')
self.my_book()
def my_book(self):
#I left it out so the code doesn't get too long
class myFrame(ttk.Frame):
def __init__(self, master=None):
super().__init__(master)
self.master = master
self.my_widgets()
def my_widgets(self):
self.f = Figure(figsize=(7, 5.5), dpi=100)
self.plott = self.f.add_subplot()
self.canvas = FigureCanvasTkAgg(self.f, self)
self.canvas.draw()
self.canvas.get_tk_widget().pack()
self.button_ax = self.f.add_axes([0.76, 0.95, 0.11, 0.04])
self.button_graph = Button(self.button_ax, 'Graph', color='darkgrey')
self.button_graph.on_clicked(self.callback_button)
plt.show()
def callback_button(self, event):
self.graph_1()
self.graph_2()
def checkbox_O(self, lines, f):
self.check_ax = f.add_axes([0.76, 0.085, 0.21, 0.16])
labels = [str(line.get_label()) for line in lines]
labels_visual = ['graph1', 'graph2']
visibility = [line.get_visible() for line in lines]
self.check = CheckButtons(self.check_ax, labels_visual, visibility)
def g_checkbox(label):
selec_index = [ind for ind, lab in enumerate(self.check.labels) if label in lab._text]
for index, laabel in enumerate(labels):
if laabel==label:
lines[index].set_visible(not lines[index].get_visible())
if (lines[index].get_visible()):
self.check.rectangles[selec_index[0]].set_fill(True)
else:
self.check.rectangles[selec_index[0]].set_fill(False)
f.canvas.draw()
self.check.on_clicked(g_checkbox)
plt.show()
def graph_1(self):
global lines
for i in range(0, 15):
V = np.sin(i/10*x) + 2*i
l0, = self.plott.plot(x, V, label='graph1')
lines.append(l0)
self.checkbox_O(lines, self.f)
def graph_2(self):
global lines
for i in range(0, 15):
l0, = self.plott.plot([i, i], [0, 10], label='graph2')
lines.append(l0)
self.checkbox_O(lines, self.f)
if __name__ == "__main__":
app = root()
app.mainloop()
PS: By the way, I took the code from stackoverflow and modified it so that it works like this
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.
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
I am working on a matplotlib figure embedded in a tkinter gui in Python.
First a FigureCanvasTkAgg is created, which includes a previously created object, which contains a matplotlib figure and then it is drawn. This part works perfectly fine.
Afterwards I want to refresh the canvas / its content based on an user action. If the method to refresh the canvas is called, the figure object is refreshed and the canvas is redrawn.
This works as well, the figure is updated, but for some strange reason, the canvas resizes, so it shrinks to about one quarter of its original size. If I print the size of the canvas, I can see that its size changed.
I have tried to resize the canvas back to its original size after the refreshment, without success.
I would be thankful for any input. I have added a shortend version of my code.
#canvas object which is displayed on the gui
class Canvas:
def __init__(self, parent):
#the map_figure object is create
self.map_figure = map_figure()
#the matplotlib figure is extracted from the map_figure
self.figure = self.map_figure.get_figure()
#the canvas is created using the figure and the parent
self.canvas = FigureCanvasTkAgg(self.figure, master=parent)
#I tried both, to manually set the canvas size to the window dimensions (which are
#described by w and h and using the .pack() parameters, both are working fine
self.canvas.get_tk_widget().config(width=w,height=h)
#self.canvas.get_tk_widget().pack(fill='both', expand=True)
self.canvas.get_tk_widget().pack()
#canvas is drawn
self.canvas.draw()
#its size is printed
print(self.get_size())
def refresh(self, parent, point):
#the map_figure of the canvas is refresh
self.map_figure.refresh(data)
#the matplotlib figure is extracted again
self.canvas.figure = self.map_figure.get_figure()
#canvas is redrawn
self.canvas.draw()
#the canvas size is now different for some reason even after calling
#self.canvas.get_tk_widget().config(width=w,height=h) before this again
print(self.canvas.get_width_height())
#the map_figure class if relevant
class map_figure:
def __init__(self, data):
self.figure = self.create_figure(data)
def get_figure(self):
return self.figure
def create_figure(self, data):
#creating a matplotlib figure, closing if there was one before
plt.close(fig=None)
fig = plt.figure()
#creating a figure using the data here
return fig
#refreshing the figure using new data
def refresh(self, data):
self.figure = self.create_figure(data)
Here are also two picture to visualize my problem:
Before
After
clf()
Instead of closing the figure each time you refresh it, you could clear it:
def create_figure(self, date):
#creating a matplotlib figure, closing if there was one before
try:
self.figure.clf()
fig = self.figure
except:
fig = plt.figure()
#creating a figure using the data here
...
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