# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # Copyright (c) 2021 Pierre Raybaut """ Simple example illustrating Qt Charts capabilities to plot curves with a high number of points, using OpenGL accelerated series and comparing it to PythonQwt and Matplotlib performance, with either PyQt5 or PySide2 """ import os # Uncomment one of the following lines to switch from PySide2 and PyQt5: os.environ["QT_API"] = "pyside2" # os.environ["QT_API"] = "pyqt5" try: import matplotlib matplotlib.use("Qt5Agg") from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure except ImportError: FigureCanvasQTAgg = Figure = None from qtpy.QtCharts import QtCharts as qtc from qtpy import QtGui as QG from qtpy import QtWidgets as QW from qtpy import QtCore as QC from qtpy import QT_API from qtpy import PYSIDE2 Qt = QC.Qt if PYSIDE2: import shiboken2 import ctypes import PySide2 PYTHON_QT_API = "PySide2 v" + PySide2.__version__ else: from PyQt5.QtCore import PYQT_VERSION_STR PYTHON_QT_API = "PyQt5 v" + PYQT_VERSION_STR try: import qwt except ImportError: qwt = None import numpy as np import time def array2d_to_qpolygonf(xdata, ydata): """ Utility function to convert two 1D-NumPy arrays representing curve data (X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object). This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy). License/copyright: MIT License © Pierre Raybaut 2020. :param numpy.ndarray xdata: 1D-NumPy array (numpy.float64) :param numpy.ndarray ydata: 1D-NumPy array (numpy.float64) :return: Polyline :rtype: QtGui.QPolygonF """ dtype = np.float if not ( xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0] and xdata.dtype == ydata.dtype == dtype ): raise ValueError("Arguments must be 1D, float64 NumPy arrays with same size") size = xdata.size polyline = QG.QPolygonF(size) if PYSIDE2: # PySide2 (obviously...) address = shiboken2.getCppPointer(polyline.data())[0] buffer = (ctypes.c_double * 2 * size).from_address(address) else: # PyQt4, PyQt5 buffer = polyline.data() buffer.setsize(2 * size * np.finfo(dtype).dtype.itemsize) memory = np.frombuffer(buffer, dtype) memory[: (size - 1) * 2 + 1 : 2] = xdata memory[1 : (size - 1) * 2 + 2 : 2] = ydata return polyline class TestWidget(QW.QWidget): ENGINES = QTCHARTS, PYTHONQWT, MATPLOTLIB = "QtCharts", "PythonQwt", "Matplotlib" def __init__(self, ncurves=5, npoints=100000): super(TestWidget, self).__init__() self.setWindowTitle("Simple performance example - " + PYTHON_QT_API) self.results = [""] * len(self.ENGINES) self.formlayout = QW.QFormLayout() self.vlayout = QW.QVBoxLayout() self.stack = QW.QStackedWidget() self.fill_layouts(ncurves=ncurves, npoints=npoints) self.create_views() layout = QW.QGridLayout(self) layout.addWidget(self.stack, 0, 0, 1, 2) layout.addLayout(self.formlayout, 1, 0, 1, 1) layout.addLayout(self.vlayout, 1, 1, 1, 1) self.setLayout(layout) def fill_layouts(self, ncurves, npoints): self.view_cb = QW.QComboBox() self.view_cb.currentIndexChanged.connect(self.stack.setCurrentIndex) self.formlayout.addRow("Engine", self.view_cb) self.opengl_cb = QW.QCheckBox("Open-GL (performance!)", self) self.opengl_cb.setChecked(True) self.view_cb.currentIndexChanged.connect( lambda index=None: self.opengl_cb.setEnabled(index == 0) ) self.formlayout.addRow("Rendering", self.opengl_cb) self.ncurves_sb = QW.QSpinBox(self) self.ncurves_sb.setValue(ncurves) self.formlayout.addRow("Number of curves", self.ncurves_sb) self.npoints_sb = QW.QSpinBox(self) self.npoints_sb.setSingleStep(10000) self.npoints_sb.setMaximum(int(100e6)) self.npoints_sb.setValue(npoints) self.formlayout.addRow("Number of points", self.npoints_sb) self.results_lb = QW.QLabel("") self.results_lb.setAlignment(Qt.AlignRight) self.results_lb.setFont(QG.QFont("Consolas")) self.view_cb.currentIndexChanged.connect( lambda index=None: self.results_lb.setText(self.results[index]) ) button = QW.QPushButton("Replot", self) button.clicked.connect(self.replot) self.vlayout.addWidget(self.results_lb) self.vlayout.addWidget(button) def create_views(self): # Creating QtCharts view self.qtchart_view = qtc.QChartView(qtc.QChart()) self.qtchart_view.chart().legend().setVisible(False) self.stack.addWidget(self.qtchart_view) self.view_cb.addItem(self.QTCHARTS) if qwt is not None: # Creating PythonQwt view self.qwt_view = qwt.QwtPlot() self.qwt_view.setCanvasBackground(Qt.white) self.qwt_view.setAutoReplot(False) qwt.QwtPlotGrid.make( self.qwt_view, color=Qt.lightGray, width=0.0, style=Qt.DotLine ) self.stack.addWidget(self.qwt_view) self.view_cb.addItem(self.PYTHONQWT) else: self.qwt_view = None if Figure is not None: # Creating Matplotlib view self.mpl_fig = Figure(figsize=(5, 4), dpi=100) self.mpl_axes = self.mpl_fig.add_subplot(111) self.mpl_view = FigureCanvasQTAgg(self.mpl_fig) self.stack.addWidget(self.mpl_view) self.view_cb.addItem(self.MATPLOTLIB) else: self.mpl_fig = self.mpl_axes = self.mpl_axes = None def replot_all(self): for index in range(self.view_cb.count() - 1, -1, -1): self.view_cb.setCurrentIndex(index) self.replot() @property def current_engine(self): return self.view_cb.currentText() def replot(self): t0 = time.time() self.opengl_cb.setEnabled(self.current_engine == self.QTCHARTS) if self.current_engine == self.QTCHARTS: # Qt Charts plot = self.qtchart_view.chart() plot.removeAllSeries() set_title = plot.setTitle elif self.current_engine == self.PYTHONQWT: # PythonQwt plot = self.qwt_view plot.detachItems(qwt.QwtPlotItem.Rtti_PlotCurve) set_title = plot.setTitle elif self.current_engine == self.MATPLOTLIB: # Matplotlib self.mpl_axes.cla() set_title = self.mpl_fig.suptitle colors = (Qt.red, Qt.blue, Qt.green, Qt.yellow, Qt.cyan, Qt.magenta, Qt.gray) delta = [] ncurves = self.ncurves_sb.value() npoints = self.npoints_sb.value() t1 = time.time() for index in range(ncurves): xdata, ydata = generate_arbitrary_data(npoints) title = "Curve #%d" % (index + 1) color = colors[index % len(colors)] polyline = array2d_to_qpolygonf(xdata, ydata) t2 = time.time() if self.current_engine == self.QTCHARTS: # Qt Charts curve = qtc.QLineSeries() curve.setUseOpenGL(self.opengl_cb.isChecked()) pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1) curve.setName(title) curve.setPen(pen) curve.replace(polyline) plot.addSeries(curve) elif self.current_engine == self.PYTHONQWT: # PythonQwt qwt.QwtPlotCurve.make( xdata, ydata, title, plot, linecolor=color, antialiased=True ) elif self.current_engine == self.MATPLOTLIB: # Matplotlib self.mpl_axes.plot(xdata, ydata, color=QG.QColor(color).name()) delta.append(time.time() - t2) t3 = time.time() if self.current_engine == self.QTCHARTS: plot.createDefaultAxes() elif self.current_engine == self.PYTHONQWT: plot.replot() elif self.current_engine == self.MATPLOTLIB: self.mpl_view.draw() t4 = time.time() self.results[self.view_cb.currentIndex()] = results = ( "Total time: {:>03} ms
" "Cleaning-up plot: {:>03} ms
" "Refreshing plot: {:>03} ms
" "Plotting curves: Sum: {:>03} ms
" "(for each curve) Avg: {:>03} ms" "".format( int((t4 - t0) * 1e3), int((t1 - t0) * 1e3), int((t4 - t3) * 1e3), int(np.array(delta).sum() * 1e3), int(np.array(delta).mean() * 1e3), ) ) self.results_lb.setText(results) title = "{0} curves of {1} points".format(ncurves, npoints) print( "Plotting {0} with {1} ({2}):".format( title, self.current_engine, PYTHON_QT_API ) ) print( " " + results.replace("
", os.linesep + " ") .replace("", "") .replace("", "") .replace("", "") .replace("", "") ) set_title(title) def generate_arbitrary_data(npoints): xdata = np.linspace(2, 10.0, npoints) r1, r2, r3, r4 = np.random.rand(4) amp = r1 * 0.25 + 0.75 phi = r2 * np.pi * 0.5 pow = r3 * 0.5 + 0.5 r4 = 0 noise = np.random.rand(npoints) * amp * 0.1 * r4 ydata = noise + amp * np.sin(xdata + phi) / xdata ** pow return xdata, ydata if __name__ == "__main__": from qtpy.QtWidgets import QApplication app = QApplication([]) widget = TestWidget() widget.show() widget.resize(500, 500) widget.replot_all() app.exec_()