Communication between windows with python matplotlib using gtk.WindowGroup - python

I am developing an application in python which plots information using matplotlib, numpy and gtk. I am plotting numpy data using matplotlib, the user can change some parameters in widget to modify the plot
For design reasons I want to separate the plot (and data) from the control GUI, then I am plotting the data in one window and I have the controls in a separated window. In gtk I found the WindowGroup widget that looks can send data over multiple windows, however I cannot find the how!
The following code shows a simplification of the problem: I plot a sine in a window, the user can modify the variable fm in the window with the vertical adjustment bar (event). When I run the script I can see that the event (move the vertical adjustment bar) triggers the function but does't actualize the plot.
How can change the plot? or send messages between two windows using the WindowGroup widget?
#!/usr/bin/python
import gtk
import numpy
import matplotlib.pyplot as plt
from matplotlib.backends.backend_gtkagg \
import FigureCanvasGTKAgg as FigureCanvas
def update_hscale(event):
global t
fm = hscale.get_value()
y_new = numpy.sin(2*fm*t)
l.set_ydata(y_new)
plt.draw()
print "doing fm=", fm
# define the window group
window_group=gtk.WindowGroup()
# matplotlib plot in window 1
fm = 5.0
t = numpy.arange(200)
y = numpy.sin(2*fm*t)
fig = plt.figure(1)
ax = fig.add_subplot(111)
l, = ax.plot(t, y)
win1 = gtk.Window()
win1.connect("destroy", gtk.main_quit)
win1.set_default_size(800,300)
vbox1 = gtk.VBox()
canvas = FigureCanvas(fig)
vbox1.pack_start(canvas, fill=True)
win1.add(vbox1)
# controls in window 2
win2 = gtk.Window()
vbox2 = gtk.VBox()
adj = gtk.Adjustment(0.0, -5, 5, 1.0, 1.0, 1.0)
hscale = gtk.HScale(adj)
hscale.set_size_request(200, 30)
vbox2.pack_start(hscale)
hscale.connect("value-changed", update_hscale)
win2.add(vbox2)
# adding windows to window_group
window_group.add_window(win1)
window_group.add_window(win2)
win1.set_default_size(400,300)
win1.show_all()
win2.show_all()
gtk.main()

I found a solution, first I deleted all the gtk.WindowGroup stuff:
window_group=gtk.WindowGroup()
window_group.add_window(win1)
window_group.add_window(win2))
and I redraw in the canvas in the function update_hscale not in the plot l:
def update_hscale(event):
global t
fm = hscale.get_value()
y_new = numpy.sin(2*fm*t)
l.set_ydata(y_new)
canvas.draw()
print "doing fm=", fm

Related

How to properly interrupt an animation created using matplotlib?

I would like to properly interrupt an animation.
Background:
I have a matplotlib figure (an animation) encapsulated into a tkinter instance. I want that when the user presses a tkinter button, the animation must stop, be deleted and be restarted. I am interrupting the old animation, by using del fig at the beginning of the call back function (called by the button) which deletes the old instance of the figure class and after creates a new one.
Problem:
I think that the old animations are still somehow running in the background, as I noticed that when I click the button like 5 times, the animation gets slow and moves jerkily.
Code:
import matplotlib
import matplotlib.backends.backend_tkagg as tkagg
import matplotlib.animation as animation
import numpy as np
import tkinter as tk
fig = matplotlib.figure.Figure()
ani = []
#callback function
def solve():
#I want the old animation to disappear, every time the button is pushed
global fig, ani
del fig, ani
#Creating an instance of the figure class
fig = matplotlib.figure.Figure()
#Create a Canvas containing fig into win
aCanvas =tkagg.FigureCanvasTkAgg(fig, master=win)
#Making the canvas a tkinter widget
aFigureWidget=aCanvas.get_tk_widget()
#Showing the figure into win as if it was a normal tkinter widget
aFigureWidget.grid(row=0, column=3, rowspan=10)
# create a time array
ti, tf, dt, delay=0, 10, 0.01, 15
N=(tf-ti)/dt
t = np.arange(ti, tf, dt)
x1=np.sin(t)
N=len(t)
#Creating a sub plot
ax2 = fig.add_subplot(xlim=(0, t[N-1]), ylim=(min(x1), max(x1)))
#Printing a legend with time info
time_template = 'time = %.1fs'
time_text = ax2.text(0.05, 0.95, '', transform=ax2.transAxes, bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=1'))
#I want to create a live plot for x(t). I am using place-holders where the t and x will be
massmotion, = ax2.plot([], [], '-')
CumulativeX1, CumulativeT=[], []
#I am defining what is going to go into the brackets above (I am filling the placeholders)
def animate(i):
CumulativeX1.append(x1[i]), CumulativeT.append(i*dt)
time_text.set_text(time_template % (i*dt))
massmotion.set_data(CumulativeT, CumulativeX1 ) #Update the placeholders for the live plot
return time_text, massmotion
ani = animation.FuncAnimation(fig, animate, np.arange(1, N), interval=delay, blit=True)
fig.canvas.draw()
#Creating the GUI
#Creating an instance of the Tk class
win = tk.Tk()
#Creating an instance of the Button class
#run the solve function when I click the button
aButton=tk.Button(win, text='Start Animation', command=solve)
#Placing the Button instance
aButton.grid(column=0, row=10, columnspan=2)
#Starting the event loop
win.mainloop()

Matplotlib Event Handlers not being Called when Embedded in Tkinter

Overview
I am in the process of embedding a Matplotlib plot in a Tkinter window. I need to use the Matplotlib event handler functions (described here). When run as a standalone Matplotlib figure, I get the correct behavior: the event handlers perform their correct function on the correct user action. But when embedding in a Tkinter window, the Matplotlib event handlers are no longer being called.
Expected Behavior
The Matplotlib event handler should be called when the figure is embedded in a Tkinter window.
Current Behavior
The Matplotlib event handler is not being called.
Minimal Code Snippet
Without Tkinter
import matplotlib.pyplot as plt
def onpick(event):
print("You selected the line!")
if __name__=='__main__':
### MATPLOTLIB SETUP ###
xs = [0,1,2] #x-values of the graph
ys = [4,3,2] #y-values of the graph
fig, ax = plt.subplots(1)
ax.plot(xs, ys, picker=True)
fig.canvas.mpl_connect('pick_event', onpick)
plt.show()
When this code is run, you can select the line on the plot and the onpick method is called, printing the text to stdout. This is the desired output.
Embedded in Tkinter
import tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
def onpick(event):
print("You selected the line!")
if __name__=='__main__':
### MATPLOTLIB SETUP ###
xs = [0,1,2] #x-values of the graph
ys = [4,3,2] #y-values of the graph
fig, ax = plt.subplots(1)
ax.plot(xs, ys, picker=True)
fig.canvas.mpl_connect('pick_event', onpick)
### TKINTER SETUP ###
root = tkinter.Tk()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
tkinter.mainloop()
When this code is run and you try to click on the line, the text is never printed meaning the onpick method is never being called.
Versions
python : 3.6.1
matplotlib : 3.3.4
tkinter : 8.6.6
The event listeners of the Matplotlib Figure object cannot be called when embedded in Tkinter. Instead, you have to add the listeners to the FigureCanvasTkAgg (or similar) object. So a working example built on the previous minimal example would be:
import tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
def onpick(event):
print("You selected the line!")
if __name__=='__main__':
### MATPLOTLIB SETUP ###
xs = [0,1,2] #x-values of the graph
ys = [4,3,2] #y-values of the graph
fig, ax = plt.subplots(1)
ax.plot(xs, ys, picker=True)
#plt.show()
### TKINTER SETUP ###
root = tkinter.Tk()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
canvas.mpl_connect("pick_event", onpick) # Add the listener using this command
tkinter.mainloop()

How to make matplotlib button_press_event ignoring clicking on figure buttons? event.inaxes doesn't work, because button is an axis object

Here is the problem. I need to create ''click catcher''. I need to turn on click catching mode by clicking on the button (Mark) and then, when I click on my plot want my program to remember points list, coordinates, etc. What is especially important - other buttons must work fine when the click catching mode is on. For example I implemented Print button. I would like to click couple of times in my plot, then, without leaving click catching mode, click on the Print button, and then see the list of cought points.
The problem is that my program sees matplotlib.widgets buttons as an axes and it catches points from them whenever I use any button.
I tried to use event.inaxes which which is supposed to check if the cursor is in the axes area. So I've implemented some logic. Unfortunately it works fine only for the outside of the plot, but doesn't work at all for buttons. It seems like matplotlib sees buttons as an axes object, so it completely doesn't work.
Please help me with it, I'm quite beginner in programming and lerned all by myself (so any code review is welcome).
* tl;dr: You can see the bug very well when you turn on catching mode, click on the plot, and then click multiple time on the Print button. Each click on the Print will add to the points list additional coords.*
Here goes the code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
class ClickCatcher:
def __init__(self, window_object):
self.window_object = window_object
self.fig = self.window_object.fig
self.ax = self.window_object.ax
self.window_object.is_click_catcher_working = True
self.cid = window_object.fig.canvas.mpl_connect('button_press_event', self)
self.points, = self.ax.plot([], [], marker='.', ls='None', color='red')
self.data_x = []
self.data_y = []
def __call__(self, event):
if event.button == 1 and event.inaxes:
self.data_x.append(event.xdata)
self.data_y.append(event.ydata)
self.update_plot()
print('{:8.2f} {:8.2f}'.format(event.xdata, event.ydata))
self.window_object.click_data = (self.data_x, self.data_y)
def update_plot(self):
self.points.set_data(self.data_x, self.data_y)
self.fig.canvas.draw()
class Window:
def __init__(self):
self.is_click_catcher_working = False
self.click_data = ()
self.fig, self.ax = plt.subplots()
self.configure_appearance()
self._plot, = plt.plot(np.arange(0, 10, 0.1), np.sin(np.arange(0, 10, 0.1)),
drawstyle='steps-mid', color='black', lw=1)
self._add_buttons()
def configure_appearance(self):
self.fig.set_size_inches(10, 5)
self.ax.tick_params(axis='both', labelsize=14)
self.ax.set_xlabel('Energy (keV)', fontsize=12)
self.ax.set_ylabel('Number of counts', fontsize=12)
plt.subplots_adjust(bottom=0.25)
def _add_buttons(self):
self.button_mark = Button(plt.axes([0.4, 0.05, 0.1, 0.075]), 'Mark')
self.button_mark.on_clicked(self.activate_marking)
self.button_print = Button(plt.axes([0.6, 0.05, 0.1, 0.075]), 'Print')
self.button_print.on_clicked(self.print_points)
def catch_coordinates(self):
click = ClickCatcher(self)
def activate_marking(self, event):
if self.is_click_catcher_working:
pass
else:
self.is_click_catcher_working = True
self.catch_coordinates()
def print_points(self, event):
output = '({:.2f}, {:.2f}) ; '*len(self.click_data[0])
print(output.format(*self.click_data[0], *self.click_data[1]))
if __name__ == '__main__':
win = Window()
plt.show()

Matplotlib: Can't Manipulate plot while script is waiting for `input()`

I am trying to get user input via input() after the user manipulates a plot using the standard zoom controls. Eg. User plays with the plot, figures out the desired X-value, and types it into the command-line prompt.
Plot can be either in a separate window (Spyder/Python) or in-line (in Jupiter Notebook).
After the user types in the value, the script continues (eg. asks for another value from the plot, or does some calculation with the values).
However, I can't get the plot to actually display and be responsive while the command-line is waiting for user-input. I have tried:
plot() statement first, input() statement second.
Spyder with Python 3.6 (I think), from source via MacPorts (updated Spyder as far as I could)
Spyder via Python 3.7 from ContinuumIO's Anaconda package, in IPython
Jupiter Notebook also from Anaconda
Numerous backends: macosx, qt, etc.
Notebook %matplotlib, notebook, inline, qt etc.
separate figure windows (Spyder & Python) vs. in-line figures (Jupyter Notebook)
fig.show( block=False ) and variations of this, eg. plt.show( block=False )
two different MacBooks (2017 and 2010 MacBook Pro's)
I did get the plot to actually update (previously it was either a blank space in a Notebook, or a blank separate figure window) by adding a matplotlib.pyplot.pause(0.5) between the plot() and input() statements. This was major progress, but once the script hits the input() statement, I get a spinning beachball on the Figure window (preventing zooming etc.) until I complete the input() statement by entering something, and then the script completes. At that point the plot is interactive.
It seems like the python console(s) can't handle more than one user-interaction simultaneously? Ie. input() is freezing all other user-interactivity?
I've been searching SO, google etc. for days now and haven't figured this out! The idea was to use this as a "quick and dirty" way to get user input from the plot, prior to undertaking the theoretically more complex task of acquiring user-clicks directly from the plot (which would have to snap to plotted data like data cursors).
Theory
The main execution thread blocks on user input, effectively pausing all other operations including rendering. You can mitigate this by doing plotting in another thread and passing UI input to that thread through a queue so that thread never blocks and stays responsive.
The docs have a great section on interactive figures, including ipython integrations.
Here are some examples:
Use non-blocking plot: plt.show(block=False)
Use matplotlib.animation
Use more complex multithreading and queues (good for integrating into UIs)
Some of the code below is from an old project of mine.
Example using input() with matplotlib.animation
Updates starting x location on input(), quits with q. Note that you can zoom and pan on the plot while waiting for user input. Also note the use of non-blocking plt.show() in mainloop():
import queue
import numpy as np # just used for mocking data, not necessary
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
animation_queue = queue.Queue()
update_rate_ms = 50
xdata = np.linspace(0, 2 * np.pi, 256)
ydata = np.sin(xdata)
zdata = np.cos(xdata)
def normal_plot_stuff():
"""Some run of the mill plotting."""
ax.set_title("Example Responsive Plot")
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.plot(xdata, ydata, "C0", label="sin")
ax.plot(xdata, zdata, "C1", label="cos")
ax.legend(loc="lower right")
def animate(_, q):
"""Define a callback function for the matplotlib animation.
This reads messages from the queue 'q' to adjust the plot.
"""
while not q.empty():
message = q.get_nowait()
q.task_done()
x0 = float(message)
ax.set_xlim([x0, x0 + 5])
def mainloop():
"""The main loop"""
_ = FuncAnimation(fig, animate, interval=update_rate_ms, fargs=(animation_queue,))
normal_plot_stuff()
plt.show(block=False)
while True:
try:
uinput = input("Type starting X value or 'q' to quit: ")
if uinput == "q":
break
animation_queue.put_nowait(float(uinput))
except ValueError:
print("Please enter a valid number.")
mainloop()
Example with a live plot embedded in a UI
The window starting X and window size update as a user enters it in the text field. The matplotlib canvas is tied to the UI rendering for responsiveness.
"""
Imbed a live animation into a PySimpleGUI frontend.
The animation fires on a timer callback from matplotlib and renders to
a PySimpleGUI canvas (which is really just a wrapped tk canvas).
"""
import queue
import numpy as np # just used for mocking data, not necessary
import PySimpleGUI as sg # used just for example
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # used just for example
matplotlib.use("TkAgg")
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
animation_queue = queue.Queue()
update_rate_ms = 50
xdata = np.linspace(0, 2 * np.pi, 256)
ydata = np.sin(xdata)
zdata = np.cos(xdata)
def animate(_, q):
"""Define a callback function for the matplotlib animation."""
message = None
while not q.empty():
message = q.get_nowait()
q.task_done()
if not message: # ignore empty UI events
return
ax.clear()
if message[1]["sin"]: # if SIN enable checkbox is checked
ax.plot(xdata, ydata, "C0", label="sin")
ax.legend(loc="lower right")
if message[1]["cos"]: # if COS enable checkbox is checked
ax.plot(xdata, zdata, "C1", label="cos")
ax.legend(loc="lower right")
x0 = float(message[1]["x_start"])
size = float(message[1]["w_size"])
ax.set_xlim([x0, x0 + size])
ax.set_title("Example Responsive Plot")
ax.set_xlabel("X")
ax.set_ylabel("Y")
layout = [
[
sg.Text("Start X:"),
sg.Input(size=(5, 0), default_text=0, key="x_start"),
sg.Text("Window Size:"),
sg.Input(size=(10, 0), default_text=6.28, key="w_size"),
sg.Button("Exit"),
],
[
sg.Frame(
title="SIN",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True, key="sin", enable_events=True)],
],
),
sg.Frame(
title="COS",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True, key="cos", enable_events=True)],
],
),
],
[sg.Canvas(key="-CANVAS-")],
]
def plot_setup():
"""MUST maintain this order: define animation, plt.draw(), setup
window with finalize=True, then create, draw and pack the TkAgg
canvas.
"""
_ = FuncAnimation(fig, animate, interval=update_rate_ms, fargs=(animation_queue,))
plt.draw()
window = sg.Window(
"Responsive Plot Example",
layout,
font="18",
element_justification="center",
finalize=True,
)
# tie matplotlib renderer to pySimpleGui canvas
canvas = FigureCanvasTkAgg(fig, window["-CANVAS-"].TKCanvas)
canvas.draw()
canvas.get_tk_widget().pack(side="top", fill="both", expand=1)
return window
def mainloop():
"""Main GUI loop. Reads events and sends them to a queue for processing."""
window = plot_setup()
while True:
event, values = window.read(timeout=update_rate_ms)
if event in ("Exit", None):
break
animation_queue.put_nowait([event, values])
window.close()
mainloop()
Example with live data streaming
Specifically, notice that you can type different values into the window field at the top of the UI and the plot immediately updates without blocking/lagging. The ADC controls at the bottom are pretty meaningless for this example, but they do demonstrate more ways of passing UI data to the plotting thread.
"""
Imbed a live animation into a PySimpleGUI frontend, with extra plotting
and sensor control.
Live sensor data gets read from a separate thread and is converted to
PSI using calibration coefficients from a file.
The animation fires on a timer callback from matplotlib and renders to
a PySimpleGUI canvas (which is really just a wrapped tk canvas).
"""
import time
import queue
import random
import threading
from datetime import datetime
import numpy as np # just used for mocking data, not necessary
import PySimpleGUI as sg
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
matplotlib.use("TkAgg")
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
animation_queue = queue.Queue() # to pass GUI events to animation
raw_data_queue = queue.Queue() # to pass raw data to main thread
update_rate_ms = 50 # refresh time in ms
ts, adc0, adc1 = [], [], [] # live data containers
def get_sensors(msg):
"""Return the names of the currently selected sensors from the GUI."""
names = np.array(["A", "B", "C"])
s0 = [msg[2], msg[3], msg[4]] # adc0 sensor
s1 = [msg[6], msg[7], msg[8]] # adc1 sensor
return (names[s0][0], names[s1][0]) # boolean index to the names
def data_collection_thread(data_queue):
"""Simulate some live streamed data that and put it on a queue."""
t = 0
while True:
t += 1
x = np.sin(np.pi * t / 112) * 12000 - 10000
y = random.randrange(-23000, 3000)
line = f"{t}:{x}:{y}"
data_queue.put(line)
time.sleep(0.001)
def process_data(data_queue, message, t, x, y):
"""Consume and process the data from the live streamed data queue."""
while not data_queue.empty():
line = data_queue.get()
try:
t0, v0, v1 = line.split(":")
t.append(float(t0))
x.append(float(v0))
y.append(float(v1))
except ValueError:
pass # ignore bad data
data_queue.task_done()
try: # truncate to appropriate window size
n = int(message[0])
return t[-n:], x[-n:], y[-n:]
except (ValueError, TypeError):
return t, x, y # don't truncate if there is a bad window size
# draws live plot on a timer callback
def animate(_, q):
# get last message on event queue
message = None
while not q.empty():
message = q.get_nowait()
q.task_done()
# plot last n datapoints
try:
n = int(message[1][0]) # parse window size
adc0_window = adc0[-n:]
adc1_window = adc1[-n:]
ts_window = [i for i in range(len(adc0_window))]
ax.clear()
if message[1][1]: # if adc0 enable checkbox is checked
ax.plot(ts_window, adc0_window, "C0", label="adc0")
ax.legend(loc="lower right")
if message[1][5]: # if adc0 enable checkbox is checked
ax.plot(ts_window, adc1_window, "C1", label="adc1")
ax.legend(loc="lower right")
ax.set_title("Live Sensor Readings")
ax.set_xlabel("Time (ms)")
ax.set_ylabel("Pressure (psi)")
# save displayed data
if message[0] == "Save":
basename = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
plt.savefig(basename + ".png")
except (ValueError, TypeError):
pass # ignore poorly formatted messages from the GUI
layout = [
[ # row 1, some control buttons
sg.Text("Window Size (ms):"),
sg.Input(size=(5, 0), default_text=100),
sg.Button("Start"),
sg.Button("Pause"),
sg.Button("Save"),
sg.Button("Exit"),
],
[sg.Canvas(key="-CANVAS-")], # row 2, the animation
[ # row 3, some frames for the ADC options
sg.Frame(
title="ADC 0",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True)],
[
sg.Radio("Sensor A", 1, default=True),
sg.Radio("Sensor B", 1),
sg.Radio("Sensor C", 1),
],
],
),
sg.Frame(
title="ADC 1",
relief=sg.RELIEF_SUNKEN,
layout=[
[sg.Checkbox("Enabled", default=True)],
[
sg.Radio("Sensor A", 2),
sg.Radio("Sensor B", 2, default=True),
sg.Radio("Sensor C", 2),
],
],
),
],
]
# MUST maintain this order: define animation, plt.draw(), setup window
# with finalize=True, then create, draw and pack the TkAgg canvas
ani = animation.FuncAnimation(
fig, animate, interval=update_rate_ms, fargs=(animation_queue,)
)
plt.draw() # must call plot.draw() to start the animation
window = sg.Window(
"Read Pressure Sensors",
layout,
finalize=True,
element_justification="center",
font="18",
)
# tie matplotlib renderer to pySimpleGui canvas
canvas = FigureCanvasTkAgg(fig, window["-CANVAS-"].TKCanvas)
canvas.draw()
canvas.get_tk_widget().pack(side="top", fill="both", expand=1)
# kick off data collection thred
threading.Thread(
target=data_collection_thread, args=(raw_data_queue,), daemon=True
).start()
data_collection_enable = True
# main event loop for GUI
while True:
event, values = window.read(timeout=update_rate_ms)
# check for button events
if event in ("Exit", None):
break
if event == "Start":
data_collection_enable = True
if event == "Pause":
data_collection_enable = False
# send GUI events to animation
animation_queue.put_nowait((event, values))
# process data when not paused
if data_collection_enable:
ts, adc0, adc1 = process_data(raw_data_queue, values, ts, adc0, adc1)
else: # if paused, throw away live data
while not raw_data_queue.empty():
raw_data_queue.get()
raw_data_queue.task_done()
window.close()
Why don't you get the user to select the value using a mouse click? I've played around with this part of the documentation. With this simple solution you can get the x and y values of the image in general and the drawn figure using a mouse click. Then, it closes the plot. If you still need it, you may show it again.
from matplotlib.backend_bases import MouseButton
import matplotlib.pyplot as plt
import numpy as np
# Draw a sample
t = np.arange(0.0, 1.0, 0.01)
s = np.sin(2 * np.pi * t)
fig, ax = plt.subplots()
ax.plot(t, s)
def on_click(event):
if event.button is MouseButton.LEFT:
# get the x and y pixel coords of image
x_img, y_img = event.x, event.y
if event.inaxes:
# get x and y of the drawn figure
x_fig, y_fig = event.xdata, event.ydata
print('x and y of image: %.1f %.1f' %(x_img, y_img))
print('x and y of figure: %.3f %.3f' %(x_fig, y_fig))
plt.close()
plt.connect('button_press_event', on_click)
plt.show()
This is a sample for the output:
x and y of image: 546.0 214.0
x and y of figure: 0.974 -0.140
You can try inserting plt.waitforbuttonpress() statement between the plot() and input() statements. more info at this link - https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.waitforbuttonpress.html
the script should then display the plot, wait for user to press some button and then ask for input. You can also add a optional timer. Before going to input statement, it will wait for user to press some button on keyboard.

matplotlib scatterplot picker

I want a picker on plot_date but it is not responding on clicks. even other events will not connect to the graphs.
This class will get tweets from a local database for sentiment analyse.
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.dates as md
from matplotlib.dates import DayLocator, HourLocator, DateFormatter, drange
import matplotlib.pyplot as plt
import tkinter as Tk
from Core.Database import Database
from numpy import arange
matplotlib.use('TkAgg')
plt.style.use('ggplot')
class SentimentGraph:
figure = None
axes = None
timeStamps = []
sentiment_score = []
def __init__(self, create_figure=True):
# get data from database.
self.get_data()
# create figure.
if create_figure:
self.figure = plt.figure()
# draw graph in figure
self.draw_graph(self.figure)
def draw_graph(self, figure):
neutral = 0
negative = 0
positive = 0
for score in self.sentiment_score:
if score == 0:
neutral += 1
elif score > 0:
positive += 1
elif score < 0:
negative += 1
self.figure, axes = plt.subplots(ncols=2, nrows=1)
ax1, ax2 = axes.ravel()
# The slices will be ordered and plotted counter-clockwise.
labels = 'neutral', 'Negative', 'Positive'
sizes = [neutral, positive, negative]
colors = ['yellowgreen', 'lightcoral', 'lightskyblue']
explode = (0, 0.1, 0.1) # only "explode" the 2nd slice (i.e. 'Hogs')
ax1.pie(sizes, explode=explode, labels=labels, colors=colors,
autopct='%1.1f%%', shadow=True, startangle=90,
radius=0.25, center=(0, 0), frame=True)
# Set aspect ratio to be equal so that pie is drawn as a circle.
ax1.axis('equal')
ax1.axis('off')
ax2.plot_date(self.timeStamps, self.sentiment_score, alpha=0.5, picker=True)
def onclick(event):
index = event.ind
xy = event.artist.get_offsets()
print('--------------')
print(xy[index])
self.figure.canvas.mpl_connect('pick_event', onclick)
ax2.set_title("Sentiment score")
ax2.set_ylabel("Sentiment score")
xfmt = md.DateFormatter('%Y-%m-%d %H:%M')
ax2.xaxis.set_minor_locator(HourLocator(arange(0, 25, 6)))
ax2.xaxis.set_major_formatter(DateFormatter('%H:%M'))
ax2.xaxis.set_major_formatter(xfmt)
ax2.fmt_xdata = md.DateFormatter('%Y-%m-%d %H:%M')
self.figure.autofmt_xdate()
def get_data(self):
db = Database()
result = db.query(
''' select sentiment_score, posted_at / 1000 as timestamp from tweets ''')
rows = result.fetchall()
for row in rows:
self.sentiment_score.append(row[0])
# convert unix timestamp to matplotlib compatible
date = matplotlib.dates.epoch2num(row[1])
self.timeStamps.append(date)
return True
if __name__ == "__main__":
# change config db file location
import config
config.DB_FILE = "../tweets.db"
# create window
root = Tk.Tk()
root.wm_title("time line")
graph = SentimentGraph()
def _quit():
root.quit() # stops mainloop
root.destroy() # this is necessary on Windows to prevent
# Fatal Python Error: PyEval_RestoreThread: NULL tstate
# a tk.DrawingArea
canvas = FigureCanvasTkAgg(graph.figure, master=root)
canvas.show()
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
toolbar = NavigationToolbar2TkAgg(canvas, root)
toolbar.update()
canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
button = Tk.Button(master=root, text='Quit', command=_quit)
button.pack(side=Tk.BOTTOM)
Tk.mainloop()
# If you put root.destroy() here, it will cause an error if
# the window is closed with the window manager.
The root of your problem is that you're not embedding your figure in your Tkinter application.
Instead, you're creating an entirely new tkinter widget and window when you call plt.figure or plt.subplots. You're then "piggybacking" another canvas on top of that pre-existing figure and using it in your application.
Because matplotlib thinks the plot belongs to the original figure you created with plt.subplots, it's not registering any mouse events.
When you're embedding a figure in another application you must use the figure that you create manually. You cannot call plt.figure or plt.subplots (or plt.anything, really).
To solve your problem, set up the canvas inside your application using a manually-created Figure object, similar to the embedding examples in the documentation.

Categories

Resources