Python: matplotlib plot inside QML layout - python

Consider the following python3 PyQt code to display an interactive matplotlib graph with toolbar
import sys, sip
import numpy as np
from PyQt5 import QtGui, QtWidgets
from PyQt5.Qt import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
app = QApplication(sys.argv)
top = QWidget()
fig = plt.figure()
ax = fig.gca()
x = np.linspace(0,5,100)
ax.plot(x,np.sin(x))
canvas = FigureCanvas(fig)
toolbar = NavigationToolbar(canvas, top)
def pick(event):
if (event.xdata is None) or (event.ydata is None): return
ax.plot([0,event.xdata],[0,event.ydata])
canvas.draw()
canvas.mpl_connect('button_press_event', pick)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(toolbar)
layout.addWidget(canvas)
top.setLayout(layout)
top.show()
app.exec_()
Now I'd like to achieve the same by using PyQt with QML instead. I have some experience with creating QML GUIs in C++ and I really like the fact that the layout code is nicely separated from the core logic of the code.
I have found several examples on how to show plots in PyQt and on how to use Python with QML, but nothing that combines the two.
To start off, my python and QML snippets look as follows:
Python:
import sys, sip
import numpy as np
from PyQt5 import QtGui, QtWidgets
from PyQt5.Qt import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(QUrl('layout.qml'))
root = engine.rootObjects()[0]
root.show()
sys.exit(app.exec_())
Layout:
import QtQuick 2.7
import QtQuick.Controls 1.4
ApplicationWindow {
visible: true
width: 400
height: 400
Canvas {
// canvas may not be the right choice here
id: mycanvas
anchors.fill: parent
}
}
But I am quite lost on how to continue.
More concretely, the question would be: 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.)
Can anyone help? Or is the combination of QML and plots just simply discouraged (this question suggests python and QML should work together quite well)?

I don't have a full solution, but if you're OK with just displaying charts and the fact that you'll have to provide any interactive controls by yourself, then there's a reasonably simple way to do that.
First of all, you will need to convert your matplotlib chart into a QImage. Fortunately doing so is surprisingly easy. The canonical backend (renderer) for matplotlib is *Agg`, and it allows you to render your Figure into a memory. Just make a suitable Canvas object for you Figure, then call .draw(). The QImage constructor will take generated data directly as inputs.
canvas = FigureCanvasAgg(figure)
canvas.draw()
img = QtGui.QImage(canvas.buffer_rgba(), *canvas.get_width_height(), QtGui.QImage.Format_RGBA8888).copy()
The Qt way to provide that image into QML is to use QQuickImageProvider. It will get "image name" as input from QML and should provide a suitable image as output. This allows you to serve all matplotlib charts in your app with just one Image provider. When I was working on a small visualization app for internal use, I ended up with a code like this:
import PyQt5.QtCore as QtCore
import PyQt5.QtGui as QtGui
import PyQt5.QtQuick as QtQuick
import PyQt5.QtQml as QtQml
from matplotlib.figure import Figure
from matplotlib.backends.backend_agg import FigureCanvasAgg
class MatplotlibImageProvider(QtQuick.QQuickImageProvider):
figures = dict()
def __init__(self):
QtQuick.QQuickImageProvider.__init__(self, QtQml.QQmlImageProviderBase.Image)
def addFigure(self, name, **kwargs):
figure = Figure(**kwargs)
self.figures[name] = figure
return figure
def getFigure(self, name):
return self.figures.get(name, None)
def requestImage(self, p_str, size):
figure = self.getFigure(p_str)
if figure is None:
return QtQuick.QQuickImageProvider.requestImage(self, p_str, size)
canvas = FigureCanvasAgg(figure)
canvas.draw()
w, h = canvas.get_width_height()
img = QtGui.QImage(canvas.buffer_rgba(), w, h, QtGui.QImage.Format_RGBA8888).copy()
return img, img.size()
Whenever I need to draw a plot in python code, I just create Figure using this addFigure to give it a name and let the Qt to know about it. Once you got Figure, rest of matplotlib drawing happens exactly as usual. Make axes and plot.
self.imageProvider = MatplotlibImageProvider()
figure = self.imageProvider.addFigure("eventStatisticsPlot", figsize=(10,10))
ax = figure.add_subplot(111)
ax.plot(x,y)
Then in QML code I can simply refer matplotlib image by name ("eventStatisticsPlot")
Image {
source: "image://perflog/eventStatisticsPlot"
}
Note that URL is prefixed by "image://" to tell QML that we need to get image from QQuickImageProvider and includes name ("perflog") of a particular provider to use. For this stuff to work we need to register our provider during QML initialization with a call to addImageProvider. For example,
engine = QtQml.QQmlApplicationEngine()
engine.addImageProvider('perflog', qt_model.imageProvider)
engine.load(QtCore.QUrl("PerflogViewer.qml"))
At this point you should be able to see static graphs shown, but they will not be updated properly because Image component in QML assumes that image that we provide does not change. I found no good solution for it, but an ugly workaround is fairly simple. I added a signal called eventStatisticsPlotChanged to my helper class that exposes Python app data to QML and .emit() it whenever the relevant plot is changed. E.g. here's a chunk of code where I get data from QML on a time interval selected by user.
#QtCore.pyqtSlot(float, float)
def selectTimeRange(self, min_time, max_time):
self.selectedTimeRange = (min_time, max_time)
_, ax, _ = self.eventStatisticsPlotElements
ax.set_xlim(*self.selectedTimeRange)
self.eventStatisticsPlotChanged.emit()
See that .emit() in the end? In QML this event forces image to reload URL like this:
Image {
source: "image://perflog/eventStatisticsPlot"
cache: false
function reload() { var t = source; source = ""; source = t; }
}
Connections {
target: myDataSourceObjectExposedFromPython
onEventStatisticsPlotChanged: eventStatisticsPlot.reload()
}
So whenever user moves a control, following happens:
QML sends updated time interval to my data source via selectTimeRange() call
My code calls .set_xlim on appopriate matplotlib object and emit() a signal to notify QML that chart changed
QML queries my imageProvider for updated chart image
My code renders matplotlib chart into new QImage with Agg and passes it to Qt
QML shows that image to user
It might sound a bit complicated, but its actually easy to design and use.
Here's an example of how all this looks in our small visualization app. That's pure Python + QML, with pandas used to organize data and matplotlib to show it. Scroll-like element on bottom of the screen essentially redraws chart on every event and it happens so fast that it feels real-time.
I also tried to use SVG as a way to feed vector image into QML. It's also possible and it also works. Matplotlib offers SVG backend (matplotlib.backends.backend_svg) and Image QML component support inline SVG data as a Source. The SVG data is text so it can be easily passed around between python and QML. You can update (source) field with new data and image will redraw itself automatically, you can rely on data binding. It could've worked quite well, but sadly SVG support in Qt 4 and 5 is poor. Clipping is not supported (charts will go out of the axes); resizing Image does not re-render SVG but resizes pixel image of it; changing SVG causes image to blink; performance is poor. Maybe this will change one day later, but for now stick to agg backend.
I really love design of both matlpotlib and Qt. It's smart and it meshes well without too much effort or boilerplate code.

It's in a fairly basic state, but https://github.com/jmitrevs/matplotlib_backend_qtquick provides a workable model to start from.
To quickly summarize the gist of the example provided with it:
The library provides the types FigureCanvasQtQuickAgg and NavigationToolbar2QtQuick.
The FigureCanvasQtQuickAgg type is registered with QML:
QtQml.qmlRegisterType(FigureCanvasQtQuickAgg, "Backend", 1, 0, "FigureCanvas")
This allows you to use it from within QML:
FigureCanvas {
id: mplView
objectName : "figure"
dpi_ratio: Screen.devicePixelRatio
anchors.fill: parent
}
The objectName property allows the canvas instance to be found from within the Python code.
The toolbar is made out of QML buttons,
In the demo, they provide a DisplayBridge Python class that is linked to the canvas and is responsible for the actual plotting and for forwarding events from the various toolbar buttons
Internally, the FigureCanvasQtQuickAgg backend is a QQuickPaintedItem. In its paint function it copies data from the renderer attribute of the matplotlib FigureCanvasAgg base class into a QImage and the QImage is then painted. This is a fairly similar design to how the QWidget version of matplotlib works.

Related

python qpageview - how to draw a rectangle

I am trying to use qpageview library, but the documentation is quite limited and the core developer does not answer any question. This library is intended to make easier the creation of a PDF editor based on Qt and Poppler.
I am able to import a PDF and I am able to display a slection box with the following code:
import qpageview
from qpageview.export import pdf
from qpageview.rubberband import Rubberband
from PyQt5.Qt import *
a = QApplication([])
v = qpageview.View()
v.show()
v.loadPdf("../pdf/Frances_01.pdf")
r = Rubberband()
v.setRubberband(r)
What I would like to do is to draw white rectangle on the selection made with the Rubberband. It is easy to get the selection: r.selection(). But I don't know how to draw on the existing pages.
Any help would be welcomed.

Get background color of QTabWidget in PyQt5

When creating a PyQt5 tab widget the background colour is whiter than the background of a normal widget. I am looking for a way to get the tab exact background colour.
Some good examples that relate to this question are:
Get the background color of a widget - really
How to make the background color the same as the form background?
The most common suggestions to get the background colour is to use:
widget.palette().color(QtGui.QPalette.Background)
# alternatively with QtGui.QPalette.Base
None of which gets the exact colour. The first being too dark and the later too white.
Whether this works on not also depends on the style and system you are using. On my case it is on a Linux Mint setup but the app is also intended for windows.
===== Using this to set the background of a matplotlib figure =====
The use case for this question is to keep the facecolor of a matplotlib figure consistent with the widget it is embed on.
Here is my example:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib.pyplot as plt
import matplotlib as mpl
class Window(QtWidgets.QMainWindow):
""" Class to use the data pattern widget in a separate window"""
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
# ===== Setup the window =====
self.setWindowTitle("background")
self.resize(600, 400)
self.maintabs = QtWidgets.QTabWidget(self)
self.setCentralWidget(self.maintabs)
self.page = QtWidgets.QWidget(self)
self.mpl_layout = QtWidgets.QHBoxLayout(self.page)
self.maintabs.addTab(self.page, 'Demo tab')
# ===== Set up matplotlib canvas =====
self.mpl_canvas = None
# get background color from widget and convert it to RBG
pyqt_bkg = self.maintabs.palette().color(QtGui.QPalette.Background).getRgbF()
mpl_bkg = mpl.colors.rgb2hex(pyqt_bkg)
self.pltfig = mpl.figure.Figure()
self.pltfig.set_facecolor(mpl_bkg) # uses the background of mainwindow and not tab
self.plot_ax = self.pltfig.add_subplot(111)
self.addmpl(self.pltfig)
def addmpl(self, fig):
self.mpl_canvas = FigureCanvas(fig)
self.mpl_layout.addWidget(self.mpl_canvas)
self.mpl_canvas.draw()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec())
Resulting in,
The colors of a palette are only a reference for how the style will actually paint widgets. Some widgets use gradients to show their contents (consider buttons, which usually "glow" the color given for the palette Button role). Some styles even paint some widgets using roles that seem unrelated to that type of widget.
QTabWidget behaves in a similar way (depending on the style), so even if you get its background, it will probably painted with a slightly different color.
A possible solution is to set the background of the widget using stylesheet, while still keeping the palette reference:
self.maintabs.setStyleSheet('background: palette(window);')
Note that this will make all children widgets of maintabs use that plain background color.
you can use stylesheet to set the background-color, like these:
self.maintabs.setStyleSheet("background-color: rgb(0,0,59)")

How to display an SVG image in Python

I was following this tutorial on how to write a chess program in Python.
It uses the python-chess engine. The functions from that engine apparently return SVG data, that could be used to display a chessboard.
Code from the tutorial:
import chess
import chess.svg
from IPython.display import SVG
board = chess.Board()
SVG(chess.svg.board(board=board,size=400))
but when I run that code, all I see is a line in the terminal and no image.
<IPython.core.display.SVG object>
The tutorial makes a passing reference to Jupyter Notebooks and how they can be used to display SVG images. I have no experience with Jupyter Notebooks and even though I installed the package from pip and I dabbled a little into how to use it, I couldn't make much progress with regards to my original chessboard problem. But what I do have, is, experience with Qt development using C++ and since Qt has Python bindings, I decided to use those bindings.
Here is what I wrote:
import sys
import chess
import chess.svg
from PyQt5 import QtGui, QtSvg
from PyQt5.QtWidgets import QApplication
from IPython.display import SVG, display
app = QApplication(sys.argv);
board = chess.Board();
svgWidget = QtSvg.QSvgWidget(chess.svg.board(board=board, size=400));
#svgWidget.setGeometry(50,50,759,668)
svgWidget.show()
sys.exit(app.exec_())
A Qt window opens and shows nothing and in the terminal I see a lot of text - (apparently the SVG data is ending up in the console and not in the Qt window that is opening?).
I figured I have to install some SVG library under python so I installed drawSvg from pip. But it seems that library generates SVG images. And was of no use for me.
What is even more strange is, after seeing this SO question, I tried the following:
import sys
import chess
import chess.svg
from PyQt5 import QtGui, QtSvg
from PyQt5.QtWidgets import QApplication
from IPython.display import SVG, display
app = QApplication(sys.argv);
board = chess.Board();
svgWidget = QtSvg.QSvgWidget('d:\projects\python_chess\Zeichen_123.svg');
#svgWidget.setGeometry(50,50,759,668)
svgWidget.show()
sys.exit(app.exec_())
And it showed an image - an SVG image! What is the difference then between my case and this case?
Question: So my question is, what I am doing wrong in the case of the chessboard SVG data? Is the SVG data generated by the python-chess library not compatible with QtSvg?
I think you are getting confused by the scripting nature of Python. You say, you have experience with Qt development under C++. Wouldn't you create a main window widget there first and add to it your SVG widget within which you would call or load SVG data?
I would rewrite your code something like this.
import chess
import chess.svg
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100, 100, 1100, 1100)
self.widgetSvg = QSvgWidget(parent=self)
self.widgetSvg.setGeometry(10, 10, 1080, 1080)
self.chessboard = chess.Board()
self.chessboardSvg = chess.svg.board(self.chessboard).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
EDIT
It would be even better if you would add a paint function to the MainWindow class. Because for sure in future, you would want to repaint your board image many times, whenever you would move a piece. So I would do something like this.
def paintEvent(self, event):
self.chessboardSvg = chess.svg.board(self.chessboard).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
this is how i display SVG image with python, no need third party to do that.
First i save SVG image on disk
second i just call it like os.Startfile("exemple.svg")
Exemple:
boardsvg = svg.board(board(fen), size=600, coordinates=True)
with open('temp.svg', 'w') as outputfile:
outputfile.write(boardsvg)
time.sleep(0.1)
os.startfile('temp.svg')
that's it !

Change default name for matplotlib Qt GUI savefig

I can change the default directory, as the save file dialog seems to draw from matplotlib.rcParams["savefig.directory"], but I can't find any option for changing the default name from "image" to e.g. my own self.currentFigure variable.
To save the figure I'm using the NavigationToolbar2QT from the Matplotlib Qt5 backend.
Unfortunately the default filename "image" is hardcoded in the FigureCanvas.
Supposedly you are creating your program by using FigureCanvasQTAgg? In that case you can subclass it to return a different default string.
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
# ..
class MyFigureCanvas(FigureCanvasQTAgg):
def get_window_title(self):
return "my_default_filename"
and then at the point where you'd normally self.canvas = FigureCanvasQTAgg(...), you'd use your custom canvas, e.g. as
self.canvas = MyFigureCanvas(...)
resulting in

Tabbed window for matplotlib figures, is it possible?

I have a python project that outputs several Matplotlib figures; each figure contains several charts. The problem that project launches about 15 figures (windows) every run, which I can not reduce.
Is it possible to concatenate all these figures (windows) to a single tabbed window so that each tab represents one figure?
Any help is much appreciated.
Thanks in advance
Workaround
Thanks to #mobiusklein comments below he suggested a workaround, to export the figures as myltipage pdf file as shown here.
Important note about the multipage pdf example mentioned above.
I tried it, but I got an error regarding the LaTeX use in matplotlib. Because fixing this error is beyond the scope of this question, so I suggest if it occurs to anyone, to set plt.rc('text', usetex=False) instead of usetex=True
I still hope if someone have other solution or workaround to post it for the benefit of others.
I wrote a simple wrapper for matplotlib that does something like you're describing. You need pyqt5 for it to work though.
Here is the code, you build a plotWindow object and feed it figure handles. It'll create a new tab for each figure.
import matplotlib
# prevent NoneType error for versions of matplotlib 3.1.0rc1+ by calling matplotlib.use()
# For more on why it's nececessary, see
# https://stackoverflow.com/questions/59656632/using-qt5agg-backend-with-matplotlib-3-1-2-get-backend-changes-behavior
matplotlib.use('qt5agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QTabWidget, QVBoxLayout
import matplotlib.pyplot as plt
import sys
class plotWindow():
def __init__(self, parent=None):
self.app = QApplication(sys.argv)
self.MainWindow = QMainWindow()
self.MainWindow.__init__()
self.MainWindow.setWindowTitle("plot window")
self.canvases = []
self.figure_handles = []
self.toolbar_handles = []
self.tab_handles = []
self.current_window = -1
self.tabs = QTabWidget()
self.MainWindow.setCentralWidget(self.tabs)
self.MainWindow.resize(1280, 900)
self.MainWindow.show()
def addPlot(self, title, figure):
new_tab = QWidget()
layout = QVBoxLayout()
new_tab.setLayout(layout)
figure.subplots_adjust(left=0.05, right=0.99, bottom=0.05, top=0.91, wspace=0.2, hspace=0.2)
new_canvas = FigureCanvas(figure)
new_toolbar = NavigationToolbar(new_canvas, new_tab)
layout.addWidget(new_canvas)
layout.addWidget(new_toolbar)
self.tabs.addTab(new_tab, title)
self.toolbar_handles.append(new_toolbar)
self.canvases.append(new_canvas)
self.figure_handles.append(figure)
self.tab_handles.append(new_tab)
def show(self):
self.app.exec_()
if __name__ == '__main__':
import numpy as np
pw = plotWindow()
x = np.arange(0, 10, 0.001)
f = plt.figure()
ysin = np.sin(x)
plt.plot(x, ysin, '--')
pw.addPlot("sin", f)
f = plt.figure()
ycos = np.cos(x)
plt.plot(x, ycos, '--')
pw.addPlot("cos", f)
pw.show()
This is also posted at: https://github.com/superjax/plotWindow
Hopefully this could be a good starting point for your application.
The backend you choose to use for matplotlib controls how each figure is displayed. Some backends just render figures to file, while others like the tk, qt, or gtk backends render figures in graphical windows. Those backends determine what functionality those GUI windows have.
The existing backends don't support the type of tabbed navigation you're looking for. Someone else here implemented this using Qt4.
You might also try writing your own report files with PDF or HTML which would let you more easily write more complex image arrangements with simpler libraries.
Something that is functionally similar might be implemented using the widgets. For example, provide a row of buttons, one button for each "tab", and repaint the graphical portion of the window in response to each button.
The buttons example as a workaround is creative. As another stated you can use PyQt to create a tabbed window.
It is for this reason I use PyQtGraph. PyQtGraph only uses PyQt as a backend and therefor "natively" supports both tabbed windows and "docks". Docks allow for movable tabs and splits as well as breaking off a tab or split to a new floating window.
In general, PyQtGraph's docks provide a method for organizing your graphs/plots/images that I haven't been able to get with other libraries.
Bokeh offers tabbed windows through their Panels and Tabs widgets.
I know it is not always feasible to move away from matplotlib but I felt like there was a lack of representation of libraries which have considered and implemented tools specifically for your use case.
I shared my code that allows docking and tabbing with drag-n-drop (qt docking system). It acts as a matplotlib backend, so it's easy to integrate.
https://github.com/peper0/mpldock
I recently released a python package which contains a class called DockablePlotWindow which provides a solution similar to superjax's answer but which provides a little more flexibility to organize how the plots are initially displayed.
I'm interested to continue improving this package so feel free to open pull requests or issues on github. You can find information about it here: https://github.com/nanthony21/mpl_qt_viz
and here:
https://nanthony21.github.io/mpl_qt_viz/

Categories

Resources