Matplot lib does not plot with PyQt - matplotlib

I'm having a problem with PyQt and Mathplotlib.
Here you can find a pseudocode of what I am doing: I have a class "MainWindow" that creates a main window with a menu and an empty mathplotlib graph. When I click on the menu Item, the method "Select" is executed that opens a new Dialog. There is also a method that plots on the grah the content of the global variable Data.
import TeraGui
Data = []
class MainWindow(QMainWindow, TeraGui.Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.actionSelect.triggered.connect(self.Select)
# Create the frame with the graphs
self.create_main_frame()
#Plot empty graphs
self.axes.clear()
self.canvas.draw()
def create_main_frame(self):
self.main_frame = QWidget()
# Create the mpl Figure and FigCanvas objects.
# 5x4 inches, 100 dots-per-inch
#
self.dpi = 100
self.fig = Figure((5.0, 4.0), dpi=self.dpi)
self.canvas = FigureCanvas(self.fig)
self.canvas.setParent(self.main_frame)
#
self.axes = self.fig.add_subplot(111)
# Create the navigation toolbar, tied to the canvas
#
self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame)
#
# Layout with box sizers
#
vbox = QVBoxLayout()
vbox.addWidget(self.canvas)
vbox.addWidget(self.mpl_toolbar)
self.main_frame.setLayout(vbox)
self.setCentralWidget(self.main_frame)
def Plotting(self):
""" Redraws the figure
"""
print "I am here"
time = Data[0]
sig = Data[]
plot(time, sig)
# clear the axes and redraw the plot anew
#
self.axes.clear()
self.axes.plot(time, sig)
self.canvas.draw()
def Select(self):
dialog = Dialog(self)
dialog.exec_()
Now, if I add in the init method of the MainWindow class these lines:
Global Data
Data = [[1,2,3],[4,5,6]]
self.Plotting()
"I am here" is printed and the plot is correctly displayed into the graph, BUT if I don't add these lines and i try to call Plotting from the Dialog class it doesn't work. "I am here" is plotted but the plot stays empty. In the Dialog class, method "accept" is caled when the "ok" button of a button box is pressed:
class Dialog(QDialog, TeraGui.Ui_SelectFiles):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setupUi(self)
def accept(self):
global Data
Data = [[1,2,3],[4,5,6]]
MainWindow().Plotting()
The Plotting method draws also a separate plot by means of the command "plot(time,sig)". This plot is always showed correctly regardless the way used to call Plotting.
These are my fist tries with PyQt and matplotlib and I am not able to identify the mistake.

The problem is with the line
MainWindow().Plotting()
When you write MainWindow() you are actually creating a new instance of the MainWindow class and calling its Plotting() function, not the one of your existing MainWindow instance. This window is never shown, and since you don't save a reference to it, is subsequently deleted when accept() returns. The only evidence of its existence is the 'i am here' message it writes to the console. This is why you don't see the plot.
In this case, you are setting your MainWindow instance as the parent of dialog through dialog = Dialog(self), so you could access it though a call to parent().
self.parent().Plotting()
You should also consider adding another parameter to the Plotting() function so you can pass your data directly to it instead of having to declare globals everywhere.
def Plotting(self, Data):
...

Related

PyQT5 & QtabWidget - changing values in different tabs

I've build a small PyQt5 application with a QtabWidget as my CentralWidget. Because I wanted to bring some structure into the code I create new tabs by using different classes. This works pretty fine so far.
class Main_window(QtWidgets.QMainWindow):
"""Main Window"""
def __init__(self, parent=None):
"""Initializer"""
super(Main_window, self).__init__(parent)
self.setGeometry(50, 50, 1100, 750)
# Create menu
self.qtMenu3()
self.tabWidget = QtWidgets.QTabWidget()
# sets the tabWidget as the central widget inside the QMainWindow
self.setCentralWidget(self.tabWidget)
self.tab_first = QtWidgets.QWidget()
self.tabWidget.addTab(FirstTab(), 'First')
self.tab_second = QtWidgets.QWidget()
self.tabWidget.addTab(SecondTab(), 'Second')
My SecondTab class looks like this and creates a GroupBox and two QTextEdits
class SecondTab(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Groupbox")
layout = QGridLayout()
self.setLayout(layout)
groupbox = QGroupBox("GroupBox Example")
# groupbox.setCheckable(True)
layout.addWidget(groupbox)
# Layout manager QVBox (vertical)
vbox = QVBoxLayout()
groupbox.setLayout(vbox)
# radiobutton = QRadioButton("Radiobutton 1")
# vbox.addWidget(radiobutton)
textEdit_input = QTextEdit()
vbox.addWidget(textEdit_input)
textEdit_output = QTextEdit()
vbox.addWidget(textEdit_output)
The point where I struggle now is that I want to load a txt file and the text should update the empty textEdit_input in my second tab. Because the function should work for multiple tabs I don't want to attach it to my SecondTab class.
How can I properly address QTextEdit in my second tab?
Thanks for the input musicamante, changed the code according to your suggestions and it works.
self.tab_first = FirstTab()
self.tabWidget.addTab(self.tab_first,"Tab 1")
self.tab_second = SecondTab()
self.tabWidget.addTab(self.tab_second,"Tab 2")
and with self.tab_first.textEdit_input I can now access the needed fields.

Can I draw TKinter objects on top of an embedded FigureCanvasTkAgg?

shortly said:
I am creating a quick TKinter API and I firstly generate a tk.Canvas
I am embedding a FigureCanvasTkAgg canvas with master = tk.Canvas above
With this I am able to show an image via Matplotlib
Now I want to draw TKinter objects ON TOP of the FigureCanvasTkAgg canvas (e.g. rectangles or buttons)
Is this possible? Or is there any particular recommendation (i.e. using only one canvas or the other)?
Here some quick code:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class MyApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
def draw_image_and_button(self):
self.figure_obj = Figure()
a = self.figure_obj.add_axes([0, 0, 1, 1])
imgplot = a.imshow(some_preloaded_data_array, cmap='gray')
# create tkagg canvas
self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
self.canvas_agg.draw()
self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# attempt to draw rectangle
self.rectangle = self.canvas.create_rectangle(0, 0, 100, 100, fill='red')
if __name__ == "__main__":
app = MyApp()
app.draw_image()
app.mainloop()
I mean I see that the rectangle is being drawn before the image. Maybe its my lack of understanding on how FigureCanvasTkAgg is attached to tk.canvas
Thank you!
Ok, this is an app that I recently developed where I have matplotlib widgets and mouse events. You can also have tkinter widgets but I didn't find a way to put them on top of the matplolib canvas. Personally I like matplotlib widgets more than tkinter widgets, so I think it is not too bad.
The only pre-step that you have to take is to modify matplotlib source code because you need pass the canvas to the widget class, while by default the widget takes the figure canvas which will not work when embedding in tk (button would be unresponsive). The modification is actually quite simple, but let's go in order.
Open 'widgets.py' in the matplotlib folder (depending on where you installed it, in my case I have it in "C:\Program Files\Python37\Lib\site-packages\matplotlib").
Go to the class AxesWidget(Widget) (around line 90) and modify the __init__ method with the following code:
def __init__(self, ax, canvas=None):
self.ax = ax
if canvas is None:
self.canvas = ax.figure.canvas
else:
self.canvas = canvas
self.cids = []
As you can see compared to the original code I added a keyword argument canvas=None. In this way the original functionality is mantained, but you can now pass the canvas to the widget.
To have a responsive button on the matplolib canvas that is embedded in tk you now create a widget and you pass the matplolib canvas created with FigureCanvasTkAgg. For example for a Buttonyou would write
from matplotlib.widgets import Button
ok_button = Button(ax_ok_button, 'Ok', canvas=canvas) # canvas created with FigureCanvasTkAgg
Ok now we have all the functionalities required to have matplolib widgets on the matplolib canvas embedded in tk, plus you can also have mouse and key events, which I guess covers 95% of what you expect from a GUI. Note that if you don't want to modify the original source code you can, of course, create your own class copying AxesWidget class.
You find all the available matplolib widgets here https://matplotlib.org/3.1.1/api/widgets_api.html
Here is a modified version of your app where we put everything together:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
from matplotlib.widgets import Button
import numpy as np
class MyApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
self.canvas.pack(side="top", fill="both", expand=True)
def draw_image_and_button(self):
self.figure_obj = Figure()
self.ax = self.figure_obj.add_subplot()
self.figure_obj.subplots_adjust(bottom=0.25)
some_preloaded_data_array = np.zeros((600,600))
imgplot = self.ax.imshow(some_preloaded_data_array, cmap='gray')
# create tkagg canvas
self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
self.canvas_agg.draw()
self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# add matplolib toolbar
toolbar = NavigationToolbar2Tk(self.canvas_agg, self.canvas)
toolbar.update()
self.canvas_agg._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# add matplolib widgets
self.ax_ok_B = self.figure_obj.add_subplot(position=[0.2, 0.2, 0.1, 0.03]) # axes position doesn't really matter here because we have the resize event that adjusts widget position
self.ok_B = Button(self.ax_ok_B, 'Ok', canvas=self.canvas_agg)
# add tkinter widgets (outside of the matplolib canvas)
button = tk.Button(master=self, text="Quit", command=self._quit)
button.pack(side=tk.BOTTOM)
# Connect to Events
self.ok_B.on_clicked(self.ok)
self.canvas_agg.mpl_connect('button_press_event', self.press)
self.canvas_agg.mpl_connect('button_release_event', self.release)
self.canvas_agg.mpl_connect('resize_event', self.resize)
self.canvas_agg.mpl_connect("key_press_event", self.on_key_press)
self.protocol("WM_DELETE_WINDOW", self.abort_exec)
def abort_exec(self):
print('Closing with \'x\' is disabled. Please use quit button')
def _quit(self):
print('Bye bye')
self.quit()
self.destroy()
def ok(self, event):
print('Bye bye')
self.quit()
self.destroy()
def press(self, event):
button = event.button
print('You pressed button {}'.format(button))
if event.inaxes == self.ax and event.button == 3:
self.xp = int(event.xdata)
self.yp = int(event.ydata)
self.cid = (self.canvas_agg).mpl_connect('motion_notify_event',
self.draw_line)
self.pltLine = Line2D([self.xp, self.xp], [self.yp, self.yp])
def draw_line(self, event):
if event.inaxes == self.ax and event.button == 3:
self.yd = int(event.ydata)
self.xd = int(event.xdata)
self.pltLine.set_visible(False)
self.pltLine = Line2D([self.xp, self.xd], [self.yp, self.yd], color='r')
self.ax.add_line(self.pltLine)
(self.canvas_agg).draw_idle()
def release(self, event):
button = event.button
(self.canvas_agg).mpl_disconnect(self.cid)
print('You released button {}'.format(button))
def on_key_press(self, event):
print("you pressed {}".format(event.key))
# Resize event is needed if you want your widget to move together with the plot when you resize the window
def resize(self, event):
ax_ok_left, ax_ok_bottom, ax_ok_right, ax_ok_top = self.ax.get_position().get_points().flatten()
B_h = 0.08 # button width
B_w = 0.2 # button height
B_sp = 0.08 # space between plot and button
self.ax_ok_B.set_position([ax_ok_right-B_w, ax_ok_bottom-B_h-B_sp, B_w, B_h])
print('Window was resized')
if __name__ == "__main__":
app = MyApp()
app.draw_image_and_button()
app.mainloop()
Ok let's see the functionalities of this app:
Press a key on the keyboard → print the pressed key
Press a mouse button → print the pressed button (1 = left, 2 = wheel, 3 = right)
Release a mouse button → print the released button
Press the right button on any point on the plot and draw a line while keeping the mouse button down
Press ok or quit to close the application
Pressing 'x' to close the window is disabled.
Resize the window → Plot and widgets scales accordingly
I also took the liberty to add the classic matplotlib toolbar for other functionalities like zooming.
Note that the image plot is added with add_suplot() method which adds the resizing functionality. In this way when you resize the window the plot scales accordingly.
Most of the things I implemented you also find them on the official tutorial from matplotlib on how to embed in tk (https://matplotlib.org/3.1.3/gallery/user_interfaces/embedding_in_tk_sgskip.html).
Let me know if this answers your question. I wanted to share it because I actually developed something very similar a few days ago.

matplotlib animation embedded in tkinter : update function never called

I've been trying to embed a matplotlib animation into tkinter.
The goal of this app is to simulate some differentials equations with rk4 method and show a real time graph as the simulation goes.
In fact the plot is rightly embedded into the tkinter frame.
However, the animation never run, I've noticed that the update function is never called.
I've been searching everywhere but I didn't find anything.
Thanks for the help.
Here is a code sample of the GUI class showing where I execute the animation
# called when I click on a button "start simulation"
def plot_neutrons_flow(self):
# getting parameters from the graphical interface
if not self._started:
I0 = float(self._field_I0.get())
X0 = float(self._field_X0.get())
flow0 = float(self._field_flow0.get())
time_interval = float(self._field_time_interval.get())
stop = int(self._field_stop.get())
FLOW_CI = [I0, X0, flow0] # [I(T_0), X(T_0), PHI[T_0]]
self._simulation = NeutronsFlow(
edo=neutrons_flow_edo,
t0=0,
ci=FLOW_CI,
time_interval=time_interval,
stop=hour_to_seconds(stop)
)
# launch the animation
self._neutrons_flow_plot.animate(self._simulation)
self._started = True
Here is the code for the matplotlib animation :
import matplotlib
import tkinter as tk
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
matplotlib.use("TkAgg")
style.use('seaborn-whitegrid')
class PlotAnimation(FigureCanvasTkAgg):
def __init__(self, tk_root):
self._figure = Figure(dpi=100)
# bind plot to tkinter frame
super().__init__(self._figure, tk_root)
x_label = "Temps (h)"
y_label = "Flux / Abondance"
self._axes = self._figure.add_subplot(111, xlabel=x_label, ylabel=y_label, yscale="log")
self.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def update(self, interval):
# this is never called
# get data from rk4 simulation
time_set = self._simulation.get_time_set()
y_set = self._simulation.get_y_set()
self._axes.clear()
self._axes.plot(time_set, y_set, visible=True, linewidth=1)
self._axes.legend(fancybox=True)
# redraw canvas
self.draw_idle()
def animate(self, simulation):
# this is called
self._simulation = simulation
# simulate differential equations with rk4 method
self._simulation.resolve()
# https://github.com/matplotlib/matplotlib/issues/1656
anim = animation.FuncAnimation(
self._figure,
self.update,
interval=1000
)
EDIT :
The solution was to instantiate the FuncAnimation function directly in the init method
As indicated in the documentation of the animation module (emphasis mine)
(...) it is critical to keep a reference to the instance object. The
animation is advanced by a timer (typically from the host GUI
framework) which the Animation object holds the only reference to. If
you do not hold a reference to the Animation object, it (and hence the
timers), will be garbage collected which will stop the animation.
You need to return the anim object from your animate() function, and store it somewhere in your code so that it is not garbage-collected

PySide: Adding Navigation Toolbar to embedded plot changes cell size in QGridLayout

My project has a class GUI that uses PySide to embed Matplotlib's FigureCanvas inside one of two cells in a QGridLayout. The other class is MplGrapher, which originally was an extension of FigureCanvas. At first, everything was working fine except the plot didn't have a Navigation Toolbar. Then I found a tutorial that explained that if you want a Navigation Toolbar with your embedded plot, you need create an instance of it first...so after this, my plot included a toolbar, but it also decreased in size and I can't get it to fill the cell of GUI's QGridLayout. I've tried having the MplGrapher class extend QWidget instead...I've tried creating a sub layout inside MplGrapher that houses the plot and the toolbar...I've tried setting QSizePolicy, but nothing seems to work.
Part of what's making this so difficult for me is that I'm not sure the proper way to create a FigureCanvas, and I'm not sure the proper way to create a NavigationToolbar. There are too many different ways of extending, initializing, and setting the parent of this class, MplGrapher, that I'm loosing track of my test cases to figure the problem out for myself. Adding to the confusion is the fact that all these different test cases only produce one of three results:
a proper sized Plot, but without a NavigationToolbar (unable to revert back to when this happens)
a improperly sized Plot with a NavigationToolbar pic
the QMainWindow of the GUI class disappears (not crashing)
Here's some code:
#FigureCanvas to be embedded in PySide GUI
class MplGrapher(FigureCanvas):
def __init__(self,name,parent):
self.figure = Figure()
self.figure.suptitle(name)
self.parent = parent
super(FigureCanvas, self).__init__(self.figure)
self.initFigure()
def initFigure(self):
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self, self.parent)
Here's the another attempt:
# FigureCanvas to be embedded in PySide GUI
class MplGrapher(QWidget):
def __init__(self,name,parent):
self.figure = Figure()
self.figure.suptitle(name)
self.parent = parent
super(MplGrapher, self).__init__(self.parent)
self.initFigure()
def initFigure(self):
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self, self.parent)
And another:
# FigureCanvas to be embedded in PySide GUI
class MplGrapher(FigureCanvas):
def __init__(self,name,parent):
self.figure = Figure()
self.figure.suptitle(name)
self.parent = parent
super(MplGrapher, self).__init__(self.figure)
self.initFigure()
def initFigure(self):
self.setParent(self.parent)
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self, self.parent)
And another:
# FigureCanvas to be embedded in PySide GUI
class MplGrapher(FigureCanvas):
def __init__(self,name,parent=None):
super(FigureCanvas, self).__init__(Figure())
self.initFigure(name)
def initFigure(self, name):
self.layout = QGridLayout()
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
self.figure = Figure()
self.figure.suptitle(name)
self.canvas = FigureCanvas(self.figure)
self.canvas.setParent(self)
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self.canvas, self)
self.layout.addWidget(self.canvas,0,0)
self.layout.addWidget(self.mpl_toolbar,1,0)
Something like this should do what you want.
class MplGrapher(QtGui.QWidget):
def __init__(self,name,parent=None):
super(MplGrapher, self).__init__(parent)
self.initFigure(name)
def initFigure(self, name):
self.figure = Figure()
self.canvas = FigureCanvas(self.figure)
self.plt = self.figure.add_subplot(111)
self.navbar = NavigationToolbar(self.canvas, self)
self.layout = QtGui.QVBoxLayout()
self.layout.addWidget(self.navbar)
self.layout.addWidget(self.canvas)
self.setLayout(self.layout)

How to place wxChoice in a wxGrid header?

I'd like to creat a grid, in which user can change column name by wxchoice (or wxcombo) control.
I imagine it like this:
for example let user has tree columns of data
John,Smith,190
Maria,Kowalsky,180
I'd like to let user match each column to one of three options (firstname, lastname, height)
I'm at very beginning:
#!/usr/bin/python
# coding: utf-8
import wx
from wx.grid import Grid
class DocsVarValGrid(Grid):
"""
"""
def __init__(self, parent, init_data=None, *args, **kwargs):
super(DocsVarValGrid, self).__init__(parent, *args, **kwargs)
self.CreateGrid(1, 1)
self.cols_names = init_data
class MyFrame(wx.Frame):
""""""
def __init__(self, *args, **kwargs):
super(MyFrame, self).__init__(*args, **kwargs)
#self.panel = PickAFile(parent=self)
self.grid = DocsVarValGrid(self, init_data=['a', 'b', 'c'])
self.Layout()
def main():
app = wx.App() # creation of the wx.App object (initialisation of the wxpython toolkit)
frame = MyFrame(None, title="Hello World") # creation of a Frame with "Hello World" as title
frame.Show() # frames are invisible by default so we use Show() to make them visible
app.MainLoop() # here the app enters a loop waiting for user input
if __name__ == '__main__':
main()
You can't actually do that. Instead you would have to draw a combobox yourself. This is allowed using the GridLabelRenderer mixin. At least, that's what is implied in the wxPython demo. If you don't already have that, I recommend downloading it and checking out the examples there. It doesn't actually have a combobox in that example, but I think it will get you going.
The column header window can be fetched from the grid using grid.GetGridColLabelWindow() and you can then do whatever you want with that window, such as override its paint event or placing widgets on it. Take a look at the wx.lib.mixins.gridlabelrenderer module for some helper classes that would help you with overriding the drawing of the labels.
So while drawing a combobox-like thing yourself would be doable, in your case it would probably make most sense to just put a real widget there. You will have to manage its size and position yourself, and adjust it every time the grid and/or columns are resized, but it should not be too overly difficult. If you look in the gridlabelrenderer you will be able to see some code that calculates the rectangle that represents each label, and you can use something like that to resize and reposition your widget.