I have to use MatPlotlib in my project. However, I need to design QML Application in PyQt5 and as I understand matplotlib plots are Widgets. So, I need to use matplotlib plots inside QML.
My question is that Is there a way to display an interactive matplotlib plot in QML? (by interactive I mean not just a figure that has been saved as an image, ideally with the standard toolbar for zoom etc.)
In here, it asked before but it isn't answered. Can someone help me?
If you can live with widgets, it is not that much different from PyQT4. This answer has been greatly inspired by this thread.
The PyQT5-compatible code:
import sys, random
from PyQt5.QtWidgets import (
QMainWindow,
QApplication,
QWidget,
QVBoxLayout,
QPushButton,
QHBoxLayout,
)
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
class AppForm(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
# The main_frame keeps all widgets on itself
self.main_frame = QWidget()
# Create the interactive matplotlib figure
self.fig = Figure((10.0, 16.0), dpi=100)
# Create figure canvas that gets a reference to self.fig
# in its __init__
self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111)
# Create the matplotlib navigation toolbar
self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame)
# VBox (V for vertical) where the canvas is placed above the toolbar
plotVbox = QVBoxLayout()
plotVbox.addWidget(self.canvas)
plotVbox.addWidget(self.mpl_toolbar)
# Another VBox with a button, could eventually hold more
# vertically ordered buttons
controls = QVBoxLayout()
self.button = QPushButton("Refresh")
self.button.clicked.connect(self.refresh)
controls.addWidget(self.button)
# HBox (h for horizontal) where the
# controls VBox with buttons is on the left
# and the VBox with the plot is on the riht
hbox = QHBoxLayout()
hbox.addLayout(controls)
hbox.addLayout(plotVbox)
self.main_frame.setLayout(hbox)
self.setCentralWidget(self.main_frame)
def refresh(self):
"""
Here, the functionality of the 'Refresh' button is implemented.
Note that we do not return anything, instead we modify the state
of the AppForm instance.
"""
self.ax.clear()
x = [random.random() for i in range(10)]
y = [random.random() for i in range(10)]
self.ax.plot(x, y, "o")
self.canvas.draw()
def main():
"""
Open the main window.
"""
app = QApplication(sys.argv)
form = AppForm()
form.show()
app.exec_()
if __name__ == "__main__":
main()
Related
I am currently trying to embed a graph I want to plot in a pyqt4 user interface I designed. As I am almost completely new to programming - I do not get how people did the embedding in the examples I found - this one (at the bottom) and that one.
It would be awesome if anybody could post a step-by-step explanation or at least a very small, very simple code only creating e.g. a graph and a button in one pyqt4 GUI.
It is not that complicated actually. Relevant Qt widgets are in matplotlib.backends.backend_qt4agg. FigureCanvasQTAgg and NavigationToolbar2QT are usually what you need. These are regular Qt widgets. You treat them as any other widget. Below is a very simple example with a Figure, Navigation and a single button that draws some random data. I've added comments to explain things.
import sys
from PyQt4 import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import random
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# a figure instance to plot on
self.figure = Figure()
# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some button connected to `plot` method
self.button = QtGui.QPushButton('Plot')
self.button.clicked.connect(self.plot)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def plot(self):
''' plot some random stuff '''
# random data
data = [random.random() for i in range(10)]
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
ax.clear()
# plot data
ax.plot(data, '*-')
# refresh canvas
self.canvas.draw()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
Edit:
Updated to reflect comments and API changes.
NavigationToolbar2QTAgg changed with NavigationToolbar2QT
Directly import Figure instead of pyplot
Replace deprecated ax.hold(False) with ax.clear()
Below is an adaptation of previous code for using under PyQt5 and Matplotlib 2.0.
There are a number of small changes: structure of PyQt submodules, other submodule from matplotlib, deprecated method has been replaced...
import sys
from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
import random
class Window(QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# a figure instance to plot on
self.figure = plt.figure()
# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some button connected to `plot` method
self.button = QPushButton('Plot')
self.button.clicked.connect(self.plot)
# set the layout
layout = QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def plot(self):
''' plot some random stuff '''
# random data
data = [random.random() for i in range(10)]
# instead of ax.hold(False)
self.figure.clear()
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
# ax.hold(False) # deprecated, see above
# plot data
ax.plot(data, '*-')
# refresh canvas
self.canvas.draw()
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
For those looking for a dynamic solution to embed Matplotlib in PyQt5 (even plot data using drag and drop). In PyQt5 you need to use super on the main window class to accept the drops. The dropevent function can be used to get the filename and rest is simple:
def dropEvent(self,e):
"""
This function will enable the drop file directly on to the
main window. The file location will be stored in the self.filename
"""
if e.mimeData().hasUrls:
e.setDropAction(QtCore.Qt.CopyAction)
e.accept()
for url in e.mimeData().urls():
if op_sys == 'Darwin':
fname = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path())
else:
fname = str(url.toLocalFile())
self.filename = fname
print("GOT ADDRESS:",self.filename)
self.readData()
else:
e.ignore() # just like above functions
For starters the reference complete code gives this output:
I have a PyQt file that, on the click of a button, runs a function from another python file (let's call calc.py) that carries out a really advanced calculation and uses matplotlib to plot the result.
I would like to show this plot generated by calc.py in the PyQt window after the calculation is complete/the figure is generated. Although, I'm not sure the best way to carry this out given that the calculation and figure are made in another file.
pyqt.py
import PyQt5 . . .
from calc import do_calc
class Window(QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.title_label = QLabel("Label Text", self) #random label in top of window
###code here to display matplotlib plot in my PyQt app####
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
calc.py
import matplotlib.pyplot as plt
def do_calc():
plt.figure()
for i in x:
... #really complex calculation here
plt.plot()
plt.draw()
I've been looking at other examples of how to display matplotlib plots in Qt but they usually orient around the calculation being done in the PyQt file or widget class which I can't really do in this instance. I can alter the calc.py file to return items or do anything else that might be helpful, but the calculations will likely need to stay in that file so it can be ran independently from the PyQt
The solution is a hack(may fail in the future) that assumes that the backend used by matplotlib in calc.py uses PyQt5, for this it is necessary to import PyQt5 first and then calc.py.
The logic is to make matplotlib not block the eventloop using plt.ion, and then search among the toplevels (windows) that have a FigureCanvas as their centralWidget.
calc.py
import matplotlib.pyplot as plt
import numpy as np
def do_calc():
t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)
fig, ax = plt.subplots()
ax.plot(t, s)
ax.set(
xlabel="time (s)",
ylabel="voltage (mV)",
title="About as simple as it gets, folks",
)
ax.grid()
plt.show()
main.py
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QVBoxLayout, QWidget
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvas
from calc import do_calc
class Window(QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.title_label = QLabel("Label Text")
central_widget = QWidget()
self.setCentralWidget(central_widget)
lay = QVBoxLayout(central_widget)
lay.addWidget(self.title_label)
plt.ion()
do_calc()
for tl in QApplication.topLevelWidgets():
if isinstance(tl, QMainWindow) and isinstance(
tl.centralWidget(), FigureCanvas
):
lay.addWidget(tl)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Another better option is to get all the Figures and then the canvas and finally the window of that canvas:
class Window(QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
self.title_label = QLabel("Label Text")
central_widget = QWidget()
self.setCentralWidget(central_widget)
lay = QVBoxLayout(central_widget)
lay.addWidget(self.title_label)
plt.ion()
do_calc()
for i in plt.get_fignums():
canvas = plt.figure(i).canvas
if isinstance(canvas, QWidget):
lay.addWidget(canvas.window())
I've created a GUI in pyside2 with an embedded matplotlib canvas. I'm planning to display a lot of information on this canvas, so I need more vertical space. I figured I could place the canvas inside a QScrollArea and create a "tall" figure with some subplots below each other.
This answer comes close to what I want, but I don't want horizontal scrolling at all. Rather, I'd like my canvas to expand/shrink according to the GUI, fully occupying the available horizontal space inside the QScrollArea, while still only allowing for vertical scrolling of the figure.
The following code is a MRE without any scrollbars:
import sys
from PySide2.QtCore import Qt
from PySide2.QtWidgets import (QApplication, QHBoxLayout, QMainWindow,
QPushButton, QVBoxLayout, QWidget)
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as Canvas,
NavigationToolbar2QT as Navbar)
from matplotlib.pyplot import Figure
class MyApp(QMainWindow):
def __init__(self) -> None:
super().__init__()
# Main Window setup
self.showMaximized()
self.frame = QWidget(self)
self.setCentralWidget(self.frame)
main_layout = QHBoxLayout()
self.frame.setLayout(main_layout)
# Buttons (left column)
buttons_row = QHBoxLayout()
self.plot_above_button = QPushButton(self, text='Plot Data Above')
self.plot_above_button.clicked.connect(self.plot_above)
buttons_row.addWidget(self.plot_above_button)
self.plot_below_button = QPushButton(self, text='Plot Data Below')
self.plot_below_button.clicked.connect(self.plot_below)
buttons_row.addWidget(self.plot_below_button)
main_layout.addLayout(buttons_row)
# Plot (right column)
right_column = QVBoxLayout()
self.fig = Figure(facecolor='white')
self.ax1 = self.fig.add_axes([0.1, 0.3, 0.8, 0.7])
self.ax2 = self.fig.add_axes([0.1, 0.1, 0.8, 0.1])
self.canvas = Canvas(self.fig)
self.canvas.setParent(self)
right_column.addWidget(self.canvas)
right_column.addWidget(Navbar(self.canvas, self.frame))
main_layout.addLayout(right_column)
def plot_above(self):
self.ax1.plot([1, 2], [3, 4])
self.canvas.draw()
def plot_below(self):
self.ax2.plot([1, 2], [3, 4])
self.canvas.draw()
if __name__ == '__main__':
app = QApplication()
gui = MyApp()
gui.show()
sys.exit(app.exec_())
And this is the GUI I see when I execute the code above:
As you can see, the canvas occupies the entire available space, vertically and horizontally. Except for the lacking vertical scrollbar, this is exactly what I want.
Now, if I try to place self.canvas inside a QScrollArea, by adding these lines to my code:
from PySide2.QtWidgets import QScrollArea # additional import
# ...
self.canvas.setParent(self) # this is old code
# new code starts here
scroll_area = QScrollArea()
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll_area.horizontalScrollBar().setEnabled(False)
scroll_area.setWidget(self.canvas)
right_column.addWidget(scroll_area)
right_column.addWidget(Navbar(self.canvas, self.frame)) # this is old code
# ...
I now end up with:
Instead of automatically adjusting its size, the figure just "sits" inside the QScrollArea, initialized with the default figsize value.
I also tried to manually resize the figure, by writing e.g.:
self.fig = Figure(facecolor='white', figsize=(11, 12))
Which gives me:
This is very close to what I want, but also very hacky: I empirically chose the figsize (11, 12) based on how the canvas looks like on my screen, but these values could be different in another screen or if I add extra widgets to the left side. The canvas also doesn't resize along with the main window.
Is there a way to achieve what I want?
After trying to solve this for another day, I finally figured it out. I'm writing the answer here in case anyone stumbles into this issue in the future.
We can reimplement resizeEvent of the GUI to resize the canvas width to the QScrollArea current width:
def resizeEvent(self, e):
self.canvas.resize(self.scroll_area.width(), self.canvas.height())
super().resizeEvent(e)
If I call self.showMaximized() at the end of the init method, the resize will trigger automatically. This means that the canvas is properly resized when the window is first shown, and it adapts when I change the window size.
This is the outcome:
And the full code used to generate the GUI:
import sys
from PySide2.QtCore import Qt
from PySide2.QtWidgets import (QApplication, QHBoxLayout, QMainWindow,
QPushButton, QScrollArea, QVBoxLayout, QWidget)
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as Canvas,
NavigationToolbar2QT as Navbar)
from matplotlib.pyplot import Figure
class MyApp(QMainWindow):
def __init__(self) -> None:
super().__init__()
# Main Window setup
self.frame = QWidget(self)
self.setCentralWidget(self.frame)
main_layout = QHBoxLayout()
self.frame.setLayout(main_layout)
# Buttons (left column)
buttons_row = QHBoxLayout()
self.plot_above_button = QPushButton(self, text='Plot Data Above')
self.plot_above_button.clicked.connect(self.plot_above)
buttons_row.addWidget(self.plot_above_button)
self.plot_below_button = QPushButton(self, text='Plot Data Below')
self.plot_below_button.clicked.connect(self.plot_below)
buttons_row.addWidget(self.plot_below_button)
main_layout.addLayout(buttons_row)
# Plot (right column)
right_column = QVBoxLayout()
self.fig = Figure(facecolor='white', figsize=(12, 12))
self.ax1 = self.fig.add_axes([0.1, 0.3, 0.8, 0.7])
self.ax2 = self.fig.add_axes([0.1, 0.1, 0.8, 0.1])
self.canvas = Canvas(self.fig)
self.canvas.setParent(self)
self.scroll_area = QScrollArea()
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.scroll_area.horizontalScrollBar().setEnabled(False)
self.scroll_area.setWidget(self.canvas)
right_column.addWidget(self.scroll_area)
right_column.addWidget(Navbar(self.canvas, self.frame))
main_layout.addLayout(right_column)
self.showMaximized()
def plot_above(self):
self.ax1.plot([1, 2], [3, 4])
self.canvas.draw()
def plot_below(self):
self.ax2.plot([1, 2], [3, 4])
self.canvas.draw()
def resizeEvent(self, e):
self.canvas.resize(self.scroll_area.width(), self.canvas.height())
super().resizeEvent(e)
if __name__ == '__main__':
app = QApplication()
gui = MyApp()
gui.show()
sys.exit(app.exec_())
UDPATE: I may have realized that QDockWidget isn't the way to go. I have posted a new question here: PyQt: Is it possible to drag/drop QWidgets in a QGridLayout to rearrange them?
Original question:
I am trying to make a scrollable area containing two columns of several dockable widgets. Furthermore, I would like the scrollable dock area to be placed to the right in a QHBoxLayout. Right now I have a single column of QDockWidgets with no scrolling, and a QTextEdit as my central widget.
I would like help to integrate the following:
Add the possibility for another column of QDockWidgets
Disable the function to undock to a new window. I would still like to rearrange the docks
Add the scrollarea and scrollbar to the dockarea
I would like the layout to be a QHBoxLayout with the dockarea to the right.
The image illustrates what I have and what I want. Hope you can help!
from PyQt5 import QtCore, QtWidgets, QtGui
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
import random
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.textEdit = QtWidgets.QTextEdit()
self.setCentralWidget(self.textEdit)
# First dock
dock1 = QtWidgets.QDockWidget("dock 1", self)
dock1.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
dock1.setFloating(False)
self.figure1 = Figure() # a figure to plot on
self.canvas1 = FigureCanvas(self.figure1)
self.ax1 = self.figure1.add_subplot(111) # create an axis
data = [random.random() for i in range(10)]
self.ax1.plot(data, '*-') # plot data
self.canvas1.draw() # refresh canvas
dock1.setWidget(self.canvas1)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock1)
# Second dock
dock2 = QtWidgets.QDockWidget("dock 2", self)
dock2.setAllowedAreas(QtCore.Qt.RightDockWidgetArea)
self.figure2 = Figure() # a figure to plot on
self.canvas2 = FigureCanvas(self.figure2)
self.ax2 = self.figure2.add_subplot(111) # create an axis
data = [random.random() for i in range(10)]
self.ax2.plot(data, '*-') # plot data
self.canvas2.draw() # refresh canvas
dock2.setWidget(self.canvas2)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock2)
# import dockwidgets_rc
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
I am currently trying to embed a graph I want to plot in a pyqt4 user interface I designed. As I am almost completely new to programming - I do not get how people did the embedding in the examples I found - this one (at the bottom) and that one.
It would be awesome if anybody could post a step-by-step explanation or at least a very small, very simple code only creating e.g. a graph and a button in one pyqt4 GUI.
It is not that complicated actually. Relevant Qt widgets are in matplotlib.backends.backend_qt4agg. FigureCanvasQTAgg and NavigationToolbar2QT are usually what you need. These are regular Qt widgets. You treat them as any other widget. Below is a very simple example with a Figure, Navigation and a single button that draws some random data. I've added comments to explain things.
import sys
from PyQt4 import QtGui
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import random
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# a figure instance to plot on
self.figure = Figure()
# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some button connected to `plot` method
self.button = QtGui.QPushButton('Plot')
self.button.clicked.connect(self.plot)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def plot(self):
''' plot some random stuff '''
# random data
data = [random.random() for i in range(10)]
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
ax.clear()
# plot data
ax.plot(data, '*-')
# refresh canvas
self.canvas.draw()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
Edit:
Updated to reflect comments and API changes.
NavigationToolbar2QTAgg changed with NavigationToolbar2QT
Directly import Figure instead of pyplot
Replace deprecated ax.hold(False) with ax.clear()
Below is an adaptation of previous code for using under PyQt5 and Matplotlib 2.0.
There are a number of small changes: structure of PyQt submodules, other submodule from matplotlib, deprecated method has been replaced...
import sys
from PyQt5.QtWidgets import QDialog, QApplication, QPushButton, QVBoxLayout
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
import random
class Window(QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# a figure instance to plot on
self.figure = plt.figure()
# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some button connected to `plot` method
self.button = QPushButton('Plot')
self.button.clicked.connect(self.plot)
# set the layout
layout = QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def plot(self):
''' plot some random stuff '''
# random data
data = [random.random() for i in range(10)]
# instead of ax.hold(False)
self.figure.clear()
# create an axis
ax = self.figure.add_subplot(111)
# discards the old graph
# ax.hold(False) # deprecated, see above
# plot data
ax.plot(data, '*-')
# refresh canvas
self.canvas.draw()
if __name__ == '__main__':
app = QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
For those looking for a dynamic solution to embed Matplotlib in PyQt5 (even plot data using drag and drop). In PyQt5 you need to use super on the main window class to accept the drops. The dropevent function can be used to get the filename and rest is simple:
def dropEvent(self,e):
"""
This function will enable the drop file directly on to the
main window. The file location will be stored in the self.filename
"""
if e.mimeData().hasUrls:
e.setDropAction(QtCore.Qt.CopyAction)
e.accept()
for url in e.mimeData().urls():
if op_sys == 'Darwin':
fname = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path())
else:
fname = str(url.toLocalFile())
self.filename = fname
print("GOT ADDRESS:",self.filename)
self.readData()
else:
e.ignore() # just like above functions
For starters the reference complete code gives this output: