Skip to content

Commit ea486f6

Browse files
committed
Improved PySide2 support: filling QPolygonF objects directly in memory from NumPy arrays
1 parent cdd5ed8 commit ea486f6

File tree

8 files changed

+120
-30
lines changed

8 files changed

+120
-30
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# PythonQwt Releases #
22

3+
### Version 0.8.1 ###
4+
5+
- PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
6+
V0.8.1 thanks to the new `qwt.qwt_curve.array2d_to_qpolygonf` function.
37

48
### Version 0.8.0 ###
59

README.md

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,53 @@ for more details on API limitations when comparing to Qwt.
8787

8888
### Why PySide2 support is still experimental ###
8989

90-
There is still a significant performance issue with PySide2 (drawing polylines with PySide2 is apparently approx. 60 times slower than with PyQt5, see [this bug report](https://bugreports.qt.io/browse/PYSIDE-1366)).
90+
<img src="https://raw.githubusercontent.com/PierreRaybaut/PythonQwt/master/doc/images/pyqt5_vs_pyside2.png">
9191

92-
As a consequence, until an equivalent feature is implemented in PySide2, we strongly
93-
recommend using PyQt5 instead of PySide2.
92+
Try running the `curvebenchmark1.py` test with PyQt5 and PySide: you will notice a
93+
huge performance issue with PySide2 (see screenshot above). This is due to the fact
94+
that `QPainter.drawPolyline` is much more efficient in PyQt5 than it is in PySide2
95+
(see [this bug report](https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1366)).
96+
97+
As a consequence, until this bug is fixed in PySide2, we still recommend using PyQt5
98+
instead of PySide2 when it comes to representing huge data sets.
99+
100+
However, PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
101+
V0.8.1 thanks to the new `array2d_to_qpolygonf` function (see the part related to
102+
PySide2 in the code below).
103+
104+
```python
105+
def array2d_to_qpolygonf(xdata, ydata):
106+
"""
107+
Utility function to convert two 1D-NumPy arrays representing curve data
108+
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
109+
This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy).
110+
111+
License/copyright: MIT License © Pierre Raybaut 2020.
112+
113+
:param numpy.ndarray xdata: 1D-NumPy array (numpy.float64)
114+
:param numpy.ndarray ydata: 1D-NumPy array (numpy.float64)
115+
:return: Polyline
116+
:rtype: QtGui.QPolygonF
117+
"""
118+
dtype = np.float
119+
if not (
120+
xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]
121+
and xdata.dtype == ydata.dtype == dtype
122+
):
123+
raise ValueError("Arguments must be 1D, float64 NumPy arrays with same size")
124+
size = xdata.size
125+
polyline = QPolygonF(size)
126+
if PYSIDE2: # PySide2 (obviously...)
127+
address = shiboken2.getCppPointer(polyline.data())[0]
128+
buffer = (ctypes.c_double * 2 * size).from_address(address)
129+
else: # PyQt4, PyQt5
130+
buffer = polyline.data()
131+
buffer.setsize(2 * size * np.finfo(dtype).dtype.itemsize)
132+
memory = np.frombuffer(buffer, dtype)
133+
memory[: (size - 1) * 2 + 1 : 2] = xdata
134+
memory[1 : (size - 1) * 2 + 2 : 2] = ydata
135+
return polyline
136+
```
94137

95138
## Installation
96139

doc/images/pyqt5_vs_pyside2.png

-15.3 KB
Loading

doc/installation.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ Why PySide2 support is still experimental
2525

2626
Try running the `curvebenchmark1.py` test with PyQt5 and PySide: you will notice a
2727
huge performance issue with PySide2 (see screenshot above). This is due to the fact
28-
that PyQt5 (and PyQt4) allows an efficient way of filling a QPolygonF object from a
29-
Numpy array, and PySide2 is not (see code below).
28+
that `QPainter.drawPolyline` is much more efficient in PyQt5 than it is in PySide2
29+
(see `this bug report <https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-1366>`_).
3030

31-
.. literalinclude:: /../qwt/plot_curve.py
32-
:pyobject: series_to_polyline
31+
As a consequence, until this bug is fixed in PySide2, we still recommend using PyQt5
32+
instead of PySide2 when it comes to representing huge data sets.
33+
34+
However, PySide2 support was significatively improved betwen PythonQwt V0.8.0 and
35+
V0.8.1 thanks to the new `array2d_to_qpolygonf` function (see code below).
3336

34-
As a consequence, until an equivalent feature is implemented in PySide2, we strongly
35-
recommend using PyQt5 instead of PySide2.
37+
.. literalinclude:: /../qwt/plot_curve.py
38+
:pyobject: array2d_to_qpolygonf
3639

3740
Help and support
3841
----------------

doc/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
numpy==1.19.1
22
PyQt5==5.15.0
3-
PyQt5-sip==12.8.0
3+
PyQt5-sip==12.8.0
4+
QtPy==1.9.0

qwt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
.. _GitHubPage: http://pierreraybaut.github.io/PythonQwt
2929
.. _GitHub: https://github.com/PierreRaybaut/PythonQwt
3030
"""
31-
__version__ = "0.8.0"
31+
__version__ = "0.8.1"
3232
QWT_VERSION_STR = "6.1.5"
3333

3434
import warnings

qwt/plot_curve.py

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
from qtpy.QtGui import QPen, QBrush, QPainter, QPolygonF, QColor
3232
from qtpy.QtCore import QSize, Qt, QRectF, QPointF
3333

34+
if PYSIDE2:
35+
import shiboken2
36+
import ctypes
37+
3438
import numpy as np
3539

3640

@@ -56,26 +60,46 @@ def qwtVerifyRange(size, i1, i2):
5660
return i2 - i1 + 1
5761

5862

63+
def array2d_to_qpolygonf(xdata, ydata):
64+
"""
65+
Utility function to convert two 1D-NumPy arrays representing curve data
66+
(X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object).
67+
This feature is compatible with PyQt4, PyQt5 and PySide2 (requires QtPy).
68+
69+
License/copyright: MIT License © Pierre Raybaut 2020.
70+
71+
:param numpy.ndarray xdata: 1D-NumPy array (numpy.float64)
72+
:param numpy.ndarray ydata: 1D-NumPy array (numpy.float64)
73+
:return: Polyline
74+
:rtype: QtGui.QPolygonF
75+
"""
76+
dtype = np.float
77+
if not (
78+
xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]
79+
and xdata.dtype == ydata.dtype == dtype
80+
):
81+
raise ValueError("Arguments must be 1D, float64 NumPy arrays with same size")
82+
size = xdata.size
83+
polyline = QPolygonF(size)
84+
if PYSIDE2: # PySide2 (obviously...)
85+
address = shiboken2.getCppPointer(polyline.data())[0]
86+
buffer = (ctypes.c_double * 2 * size).from_address(address)
87+
else: # PyQt4, PyQt5
88+
buffer = polyline.data()
89+
buffer.setsize(2 * size * np.finfo(dtype).dtype.itemsize)
90+
memory = np.frombuffer(buffer, dtype)
91+
memory[: (size - 1) * 2 + 1 : 2] = xdata
92+
memory[1 : (size - 1) * 2 + 2 : 2] = ydata
93+
return polyline
94+
95+
5996
def series_to_polyline(xMap, yMap, series, from_, to):
6097
"""
6198
Convert series data to QPolygon(F) polyline
6299
"""
63-
xData = xMap.transform(series.xData()[from_ : to + 1])
64-
yData = yMap.transform(series.yData()[from_ : to + 1])
65-
size = to - from_ + 1
66-
if PYSIDE2:
67-
polyline = QPolygonF()
68-
for index in range(size):
69-
polyline.append(QPointF(xData[index], yData[index]))
70-
else:
71-
polyline = QPolygonF(size)
72-
pointer = polyline.data()
73-
dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo
74-
pointer.setsize(2 * polyline.size() * tinfo(dtype).dtype.itemsize)
75-
memory = np.frombuffer(pointer, dtype)
76-
memory[: (to - from_) * 2 + 1 : 2] = xData
77-
memory[1 : (to - from_) * 2 + 2 : 2] = yData
78-
return polyline
100+
xdata = xMap.transform(series.xData()[from_ : to + 1])
101+
ydata = yMap.transform(series.yData()[from_ : to + 1])
102+
return array2d_to_qpolygonf(xdata, ydata)
79103

80104

81105
class QwtPlotCurve_PrivateData(QwtPlotItem_PrivateData):

setup.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@
3333
3434
The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical
3535
user interfaces for the Python programming language. It is compatible with
36-
both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it
37-
could be in the near future as it would "only" requires testing to support
38-
it as a stable alternative to PyQt).
36+
both ``PyQt4``, ``PyQt5`` and ``PySide2`` (see documentation for more information
37+
on a performance issue due to PySide2 itself when plotting huge data sets).
3938
4039
The ``PythonQwt`` project was initiated to solve -at least temporarily- the
4140
obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is
@@ -49,6 +48,22 @@
4948
higher level features to the `guiqwt` library.
5049
5150
See `README`_ and documentation (`online`_ or `PDF`_) for more details on the library and `changelog`_ for recent history of changes.
51+
52+
The following example is a good starting point to see how to set up a simple plot widget::
53+
54+
import qwt
55+
import numpy as np
56+
57+
app = qtpy.QtGui.QApplication([])
58+
x = np.linspace(-10, 10, 500)
59+
plot = qwt.QwtPlot("Trigonometric functions")
60+
plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend)
61+
qwt.QwtPlotCurve.make(x, np.cos(x), "Cosinus", plot, linecolor="red", antialiased=True)
62+
qwt.QwtPlotCurve.make(x, np.sin(x), "Sinus", plot, linecolor="blue", antialiased=True)
63+
plot.resize(600, 300)
64+
plot.show()
65+
66+
.. image:: https://raw.githubusercontent.com/PierreRaybaut/PythonQwt/master/doc/images/QwtPlot_example.png
5267
5368
.. _README: https://github.com/PierreRaybaut/PythonQwt/blob/master/README.md
5469
.. _online: https://pythonqwt.readthedocs.io/en/latest/

0 commit comments

Comments
 (0)