+ℹ️ Created in 2014 by Pierre Raybaut and maintained by the [PlotPyStack](https://github.com/PlotPyStack) organization.
-The `PythonQwt` project was initiated to solve -at least temporarily- the
-obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is
-no longer maintained. The idea was to translate the original Qwt C++ code to
-Python and then to optimize some parts of the code by writing new modules
-based on NumPy and other libraries.
+
-The `PythonQwt` package consists of a single Python package named `qwt` and
-of a few other files (examples, doc, ...).
+The `PythonQwt` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries.
-See documentation [online](https://pythonqwt.readthedocs.io/en/latest/) or [PDF](https://pythonqwt.readthedocs.io/_/downloads/en/latest/pdf/) for more details on
-the library and [changelog](CHANGELOG.md) for recent history of changes.
+The `PythonQwt` package consists of a single Python package named `qwt` and of a few other files (examples, doc, ...).
+
+See documentation [online](https://pythonqwt.readthedocs.io/en/latest/) or [PDF](https://pythonqwt.readthedocs.io/_/downloads/en/latest/pdf/) for more details on the library and [changelog](CHANGELOG.md) for recent history of changes.
## Sample
```python
-import qwt
import numpy as np
+from qtpy import QtWidgets as QW
+
+import qwt
-app = qwt.qt.QtGui.QApplication([])
+app = QW.QApplication([])
# Create plot widget
plot = qwt.QwtPlot("Trigonometric functions")
@@ -35,8 +33,8 @@ plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend)
# Create two curves and attach them to plot
x = np.linspace(-10, 10, 500)
-qwt.QwtPlotCurve.make(x, np.cos(x), "Cosinus", plot, linecolor="red", antialiased=True)
-qwt.QwtPlotCurve.make(x, np.sin(x), "Sinus", plot, linecolor="blue", antialiased=True)
+qwt.QwtPlotCurve.make(x, np.cos(x), "Cosine", plot, linecolor="red", antialiased=True)
+qwt.QwtPlotCurve.make(x, np.sin(x), "Sine", plot, linecolor="blue", antialiased=True)
# Resize and show plot
plot.resize(600, 300)
@@ -45,7 +43,8 @@ plot.show()
app.exec_()
```
-
+
+
## Examples (tests)
The GUI-based test launcher may be executed from Python:
@@ -61,60 +60,88 @@ or from the command line:
PythonQwt-tests
```
+Tests may also be executed in unattended mode:
+
+```bash
+PythonQwt-tests --mode unattended
+```
+
## Overview
-The `qwt` package is a pure Python implementation of `Qwt` C++ library with
-the following limitations.
+The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations.
+
+The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `PlotPy`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
+
+Only the following plot items are currently implemented in `qwt` (the only plot items needed by `PlotPy`): `QwtPlotItem` (base class), `QwtPlotGrid`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
+
+See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) for more details on API limitations when comparing to Qwt.
+
+## Roadmap
-The following `Qwt` classes won't be reimplemented in `qwt` because more
-powerful features already exist in `guiqwt`: `QwtPlotZoomer`,
-`QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
+The `qwt` package short-term roadmap is the following:
-Only the following plot items are currently implemented in `qwt` (the only
-plot items needed by `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`,
-`QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
+- [X] Drop support for PyQt4 and PySide2
+- [X] Drop support for Python <= 3.8
+- [X] Replace `setup.py` by `pyproject.toml`, using `setuptools` (e.g. see `guidata`)
+- [ ] Add more unit tests: the ultimate goal is to reach 90% code coverage
-See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/)
-for more details on API limitations when comparing to Qwt.
+## Dependencies and installation
-## Dependencies
+### Supported Qt versions and bindings
-### Requirements ###
-- Python >=2.6 or Python >=3.2
-- PyQt4 >=4.4 or PyQt5 >= 5.5
-- NumPy >= 1.5
+The whole PlotPyStack set of libraries relies on the [Qt](https://doc.qt.io/) GUI toolkit, thanks to [QtPy](https://pypi.org/project/QtPy/), an abstraction layer which allows to use the same API to interact with different Python-to-Qt bindings (PyQt5, PyQt6, PySide2, PySide6).
-## Installation
+Compatibility table:
+
+| PythonQwt version | PyQt5 | PyQt6 | PySide2 | PySide6 |
+|-------------------|-------|-------|---------|---------|
+| 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
+| Latest | ✅ | ✅ | ❌ | ✅ |
+
+### Requirements
+
+- Python >=3.9
+- QtPy >= 1.9 (and a Python-to-Qt binding library, see above)
+- NumPy >= 1.21
+
+### Optional dependencies
+
+- coverage, pytest (for unit tests)
+- sphinx (for documentation generation)
+
+### Installation
+
+From PyPI:
+
+```bash
+pip install PythonQwt
+```
From the source package:
```bash
-python setup.py install
+python -m build
```
+## Performance investigation
+
+Tooling for performance benchmarks, profiling and visual-regression checks across PyQt5/PyQt6/PySide6 lives in [`scripts/`](scripts/README.md). See [`doc/issue93_optimization_summary.md`](doc/issue93_optimization_summary.md) for a worked example.
+
## Copyrights
-#### Main code base
+### Main code base
+
- Copyright © 2002 Uwe Rathmann, for the original Qwt C++ code
-- Copyright © 2015 Pierre Raybaut, for the Qwt C++ to Python translation and
-optimization
-- Copyright © 2015 Pierre Raybaut, for the PythonQwt specific and exclusive
-Python material
+- Copyright © 2015 Pierre Raybaut, for the Qwt C++ to Python translation and optimization
+- Copyright © 2015 Pierre Raybaut, for the PythonQwt specific and exclusive Python material
-#### PyQt, PySide and Python2/Python3 compatibility modules
-- Copyright © 2009-2013 Pierre Raybaut
-- Copyright © 2013-2015 The Spyder Development Team
+### Some examples
-#### Some examples
- Copyright © 2003-2009 Gerard Vermeulen, for the original PyQwt code
-- Copyright © 2015 Pierre Raybaut, for the PyQt5/PySide port and further
-developments (e.g. ported to PythonQwt API)
+- Copyright © 2015 Pierre Raybaut, for the PyQt5/PySide port and further developments (e.g. ported to PythonQwt API)
## License
-The `qwt` Python package was partly (>95%) translated from Qwt C++ library:
-the associated code is distributed under the terms of the LGPL license. The
-rest of the code was either wrote from scratch or strongly inspired from MIT
-licensed third-party software.
+The `qwt` Python package was partly (>95%) translated from Qwt C++ library: the associated code is distributed under the terms of the LGPL license. The rest of the code was either wrote from scratch or strongly inspired from MIT licensed third-party software.
-See included [LICENSE](LICENSE) file for more details about licensing terms.
\ No newline at end of file
+See included [LICENSE](LICENSE) file for more details about licensing terms.
diff --git a/build_and_upload.bat b/build_and_upload.bat
deleted file mode 100644
index cfa633a..0000000
--- a/build_and_upload.bat
+++ /dev/null
@@ -1,23 +0,0 @@
-@echo off
-set UNATTENDED=1
-call build_doc.bat
-call build_dist.bat
-@echo:
-@echo ==============================================================================
-choice /t 5 /c yn /cs /d n /m "Do you want to upload packages to PyPI (y/n)?"
-if errorlevel 2 goto :no
-if errorlevel 1 goto :yes
-:yes
-@echo ==============================================================================
-@echo:
-twine upload dist/*
-GOTO :continue
-:no
-@echo:
-@echo Warning: Packages were not uploaded to PyPI
-:continue
-@echo:
-@echo ==============================================================================
-@echo:
-@echo End of script
-pause
\ No newline at end of file
diff --git a/build_dist.bat b/build_dist.bat
deleted file mode 100644
index 77c54de..0000000
--- a/build_dist.bat
+++ /dev/null
@@ -1,23 +0,0 @@
-@echo off
-if defined WINPYDIRBASE (
- call %WINPYDIRBASE%\scripts\env.bat
- @echo ==============================================================================
- @echo:
- @echo Using WinPython from %WINPYDIRBASE%
- @echo:
- @echo ==============================================================================
- @echo:
- )
-del MANIFEST
-rmdir /S /Q build
-rmdir /S /Q dist
-set PYTHONPATH=%cd%
-python setup.py sdist bdist_wheel --universal
-python setup.py build sdist
-@echo:
-@echo ==============================================================================
-@echo:
-if not defined UNATTENDED (
- @echo End of script
- pause
- )
\ No newline at end of file
diff --git a/build_doc.bat b/build_doc.bat
deleted file mode 100644
index b4623df..0000000
--- a/build_doc.bat
+++ /dev/null
@@ -1,25 +0,0 @@
-@echo off
-if defined WINPYDIRBASE (
- call %WINPYDIRBASE%\scripts\env.bat
- @echo ==============================================================================
- @echo:
- @echo Using WinPython from %WINPYDIRBASE%
- @echo:
- @echo ==============================================================================
- @echo:
- )
-set PATH=C:\Program Files\7-Zip;C:\Program Files (x86)\7-Zip;C:\Program Files\HTML Help Workshop;C:\Program Files (x86)\HTML Help Workshop;%PATH%
-set PYTHONPATH=%cd%
-sphinx-build -b htmlhelp doc build\doc
-hhc build\doc\PythonQwt.hhp
-copy build\doc\PythonQwt.chm doc
-7z a doc\PythonQwt.chm.zip doc\PythonQwt.chm
-move doc\PythonQwt.chm .
-sphinx-build -b html doc build\doc
-@echo:
-@echo ==============================================================================
-@echo:
-if not defined UNATTENDED (
- @echo End of script
- pause
- )
\ No newline at end of file
diff --git a/doc/_static/PythonQwt_logo.png b/doc/_static/PythonQwt_logo.png
new file mode 100644
index 0000000..93e4143
Binary files /dev/null and b/doc/_static/PythonQwt_logo.png differ
diff --git a/doc/_static/QwtPlot_example.png b/doc/_static/QwtPlot_example.png
new file mode 100644
index 0000000..1110496
Binary files /dev/null and b/doc/_static/QwtPlot_example.png differ
diff --git a/doc/images/panorama.png b/doc/_static/panorama.png
similarity index 100%
rename from doc/images/panorama.png
rename to doc/_static/panorama.png
diff --git a/doc/_static/symbol_path_example.png b/doc/_static/symbol_path_example.png
new file mode 100644
index 0000000..d050a80
Binary files /dev/null and b/doc/_static/symbol_path_example.png differ
diff --git a/doc/conf.py b/doc/conf.py
index eb9a2bc..5969162 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -8,8 +8,6 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-from __future__ import print_function, unicode_literals
-
import sys
# If extensions (or modules to document with autodoc) are in another directory,
@@ -23,7 +21,7 @@
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ["sphinx.ext.autodoc"]
try:
- import sphinx.ext.viewcode
+ import sphinx.ext.viewcode # noqa: F401
extensions.append("sphinx.ext.viewcode")
except ImportError:
@@ -103,7 +101,12 @@
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = "default"
+try:
+ import python_docs_theme # noqa: F401
+
+ html_theme = "python_docs_theme"
+except ImportError:
+ html_theme = "default"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -123,7 +126,7 @@
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-# html_logo = 'images/qwt.png'
+html_logo = "_static/PythonQwt_logo.png"
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -204,4 +207,3 @@
# If false, no module index is generated.
# latex_use_modindex = True
-
diff --git a/doc/examples/bodedemo.rst b/doc/examples/bodedemo.rst
index 2bdc851..6226f5e 100644
--- a/doc/examples/bodedemo.rst
+++ b/doc/examples/bodedemo.rst
@@ -3,5 +3,5 @@ Bode demo
.. image:: /../qwt/tests/data/bodedemo.png
-.. literalinclude:: /../qwt/tests/bodedemo.py
+.. literalinclude:: /../qwt/tests/test_bodedemo.py
:start-after: SHOW
diff --git a/doc/examples/cartesian.rst b/doc/examples/cartesian.rst
index 92f3fdc..bc0a844 100644
--- a/doc/examples/cartesian.rst
+++ b/doc/examples/cartesian.rst
@@ -3,5 +3,5 @@ Cartesian demo
.. image:: /../qwt/tests/data/cartesian.png
-.. literalinclude:: /../qwt/tests/cartesian.py
+.. literalinclude:: /../qwt/tests/test_cartesian.py
:start-after: SHOW
diff --git a/doc/examples/cpudemo.rst b/doc/examples/cpudemo.rst
index dd284e5..58f471f 100644
--- a/doc/examples/cpudemo.rst
+++ b/doc/examples/cpudemo.rst
@@ -3,5 +3,5 @@ CPU plot demo
.. image:: /../qwt/tests/data/cpudemo.png
-.. literalinclude:: /../qwt/tests/cpudemo.py
+.. literalinclude:: /../qwt/tests/test_cpudemo.py
:start-after: SHOW
diff --git a/doc/examples/curvebenchmark1.rst b/doc/examples/curvebenchmark1.rst
index ed59fde..a372c02 100644
--- a/doc/examples/curvebenchmark1.rst
+++ b/doc/examples/curvebenchmark1.rst
@@ -3,5 +3,5 @@ Curve benchmark demo 1
.. image:: /../qwt/tests/data/curvebenchmark1.png
-.. literalinclude:: /../qwt/tests/curvebenchmark1.py
+.. literalinclude:: /../qwt/tests/test_curvebenchmark1.py
:start-after: SHOW
diff --git a/doc/examples/curvebenchmark2.rst b/doc/examples/curvebenchmark2.rst
index d631fd9..2c9daf1 100644
--- a/doc/examples/curvebenchmark2.rst
+++ b/doc/examples/curvebenchmark2.rst
@@ -3,5 +3,5 @@ Curve benchmark demo 2
.. image:: /../qwt/tests/data/curvebenchmark2.png
-.. literalinclude:: /../qwt/tests/curvebenchmark2.py
+.. literalinclude:: /../qwt/tests/test_curvebenchmark2.py
:start-after: SHOW
diff --git a/doc/examples/curvedemo1.rst b/doc/examples/curvedemo1.rst
index 977fe3a..13f3c89 100644
--- a/doc/examples/curvedemo1.rst
+++ b/doc/examples/curvedemo1.rst
@@ -3,5 +3,5 @@ Curve demo 1
.. image:: /../qwt/tests/data/curvedemo1.png
-.. literalinclude:: /../qwt/tests/curvedemo1.py
+.. literalinclude:: /../qwt/tests/test_curvedemo1.py
:start-after: SHOW
diff --git a/doc/examples/curvedemo2.rst b/doc/examples/curvedemo2.rst
index 06225a8..8e4919f 100644
--- a/doc/examples/curvedemo2.rst
+++ b/doc/examples/curvedemo2.rst
@@ -3,5 +3,5 @@ Curve demo 2
.. image:: /../qwt/tests/data/curvedemo2.png
-.. literalinclude:: /../qwt/tests/curvedemo2.py
+.. literalinclude:: /../qwt/tests/test_curvedemo2.py
:start-after: SHOW
diff --git a/doc/examples/data.rst b/doc/examples/data.rst
index 872fc01..fcdda3a 100644
--- a/doc/examples/data.rst
+++ b/doc/examples/data.rst
@@ -3,5 +3,5 @@ Data demo
.. image:: /../qwt/tests/data/data.png
-.. literalinclude:: /../qwt/tests/data.py
+.. literalinclude:: /../qwt/tests/test_data.py
:start-after: SHOW
diff --git a/doc/examples/errorbar.rst b/doc/examples/errorbar.rst
index 2bad552..7981d68 100644
--- a/doc/examples/errorbar.rst
+++ b/doc/examples/errorbar.rst
@@ -3,5 +3,5 @@ Error bar demo
.. image:: /../qwt/tests/data/errorbar.png
-.. literalinclude:: /../qwt/tests/errorbar.py
+.. literalinclude:: /../qwt/tests/test_errorbar.py
:start-after: SHOW
diff --git a/doc/examples/eventfilter.rst b/doc/examples/eventfilter.rst
index 8e358c6..53b4033 100644
--- a/doc/examples/eventfilter.rst
+++ b/doc/examples/eventfilter.rst
@@ -3,5 +3,5 @@ Event filter demo
.. image:: /../qwt/tests/data/eventfilter.png
-.. literalinclude:: /../qwt/tests/eventfilter.py
+.. literalinclude:: /../qwt/tests/test_eventfilter.py
:start-after: SHOW
diff --git a/doc/examples/image.rst b/doc/examples/image.rst
index 18cd0e5..e145e63 100644
--- a/doc/examples/image.rst
+++ b/doc/examples/image.rst
@@ -3,5 +3,5 @@ Image plot demo
.. image:: /../qwt/tests/data/image.png
-.. literalinclude:: /../qwt/tests/image.py
+.. literalinclude:: /../qwt/tests/test_image.py
:start-after: SHOW
diff --git a/doc/examples/index.rst b/doc/examples/index.rst
index 5c38e12..c599332 100644
--- a/doc/examples/index.rst
+++ b/doc/examples/index.rst
@@ -6,24 +6,31 @@ Examples
The test launcher
-----------------
-A lot of examples are available in the `qwt.test` module ::
+A lot of examples are available in the ``qwt.tests`` module ::
from qwt import tests
tests.run()
-The two lines above execute the `PythonQwt` test launcher:
+The two lines above execute the ``PythonQwt-tests`` test launcher:
.. image:: /../qwt/tests/data/testlauncher.png
+GUI-based test launcher can be executed from the command line thanks to the
+``PythonQwt-tests`` test script.
+
+Unit tests may be executed from the command line thanks to the console-based script
+``PythonQwt-tests``: ``PythonQwt-tests --mode unattended``.
Tests
-----
-Here are some examples from the `qwt.test` module:
+
+
+Here are some examples from the `qwt.tests` module:
.. toctree::
:maxdepth: 2
-
+
bodedemo
cartesian
cpudemo
diff --git a/doc/examples/logcurve.rst b/doc/examples/logcurve.rst
index c6f5d1d..48eb3ec 100644
--- a/doc/examples/logcurve.rst
+++ b/doc/examples/logcurve.rst
@@ -3,5 +3,5 @@ Log curve plot demo
.. image:: /../qwt/tests/data/logcurve.png
-.. literalinclude:: /../qwt/tests/logcurve.py
+.. literalinclude:: /../qwt/tests/test_logcurve.py
:start-after: SHOW
diff --git a/doc/examples/mapdemo.rst b/doc/examples/mapdemo.rst
index 5a96e2b..9fba166 100644
--- a/doc/examples/mapdemo.rst
+++ b/doc/examples/mapdemo.rst
@@ -3,5 +3,5 @@ Map demo
.. image:: /../qwt/tests/data/mapdemo.png
-.. literalinclude:: /../qwt/tests/mapdemo.py
+.. literalinclude:: /../qwt/tests/test_mapdemo.py
:start-after: SHOW
diff --git a/doc/examples/multidemo.rst b/doc/examples/multidemo.rst
index 02a9613..84f80d0 100644
--- a/doc/examples/multidemo.rst
+++ b/doc/examples/multidemo.rst
@@ -3,5 +3,5 @@ Multi demo
.. image:: /../qwt/tests/data/multidemo.png
-.. literalinclude:: /../qwt/tests/multidemo.py
+.. literalinclude:: /../qwt/tests/test_multidemo.py
:start-after: SHOW
diff --git a/doc/examples/simple.rst b/doc/examples/simple.rst
index 11ebc94..956923d 100644
--- a/doc/examples/simple.rst
+++ b/doc/examples/simple.rst
@@ -3,5 +3,5 @@ Really simple demo
.. image:: /../qwt/tests/data/simple.png
-.. literalinclude:: /../qwt/tests/simple.py
+.. literalinclude:: /../qwt/tests/test_simple.py
:start-after: SHOW
diff --git a/doc/examples/vertical.rst b/doc/examples/vertical.rst
index 96c971b..c1cedc9 100644
--- a/doc/examples/vertical.rst
+++ b/doc/examples/vertical.rst
@@ -3,5 +3,5 @@ Vertical plot demo
.. image:: /../qwt/tests/data/vertical.png
-.. literalinclude:: /../qwt/tests/vertical.py
+.. literalinclude:: /../qwt/tests/test_vertical.py
:start-after: SHOW
diff --git a/doc/images/QwtPlot_example.png b/doc/images/QwtPlot_example.png
deleted file mode 100644
index 867e916..0000000
Binary files a/doc/images/QwtPlot_example.png and /dev/null differ
diff --git a/doc/images/symbol_path_example.png b/doc/images/symbol_path_example.png
deleted file mode 100644
index 9c6fc85..0000000
Binary files a/doc/images/symbol_path_example.png and /dev/null differ
diff --git a/doc/index.rst b/doc/index.rst
index 1d7d302..4690dcd 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,20 +1,5 @@
.. automodule:: qwt
-.. only:: html and not htmlhelp
-
- .. note::
-
- Windows users may download the :download:`CHM Manual step == 0, the step size is calculated automatically using the maxMajor setting.
.. seealso::
-
- :py:meth:`setAxisMaxMajor()`, :py:meth:`setAxisAutoScale()`,
- :py:meth:`axisStepSize()`,
+
+ :py:meth:`setAxisMaxMajor()`, :py:meth:`setAxisAutoScale()`,
+ :py:meth:`axisStepSize()`,
:py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()`
"""
if self.axisValid(axisId):
@@ -794,14 +746,14 @@ def setAxisScaleDiv(self, axisId, scaleDiv):
Disable autoscaling and specify a fixed scale for a selected axis.
The scale division will be stored locally only until the next call
- of updateAxes(). So updates of the scale widget usually happen delayed with
+ of updateAxes(). So updates of the scale widget usually happen delayed with
the next replot.
-
+
:param int axisId: Axis index
:param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division
.. seealso::
-
+
:py:meth:`setAxisScale()`, :py:meth:`setAxisAutoScale()`
"""
if self.axisValid(axisId):
@@ -814,7 +766,7 @@ def setAxisScaleDiv(self, axisId, scaleDiv):
def setAxisScaleDraw(self, axisId, scaleDraw):
"""
Set a scale draw
-
+
:param int axisId: Axis index
:param qwt.scale_draw.QwtScaleDraw scaleDraw: Object responsible for drawing scales.
@@ -822,14 +774,14 @@ def setAxisScaleDraw(self, axisId, scaleDraw):
functionality and let it take place in QwtPlot. Please note
that scaleDraw has to be created with new and will be deleted
by the corresponding QwtScale member ( like a child object ).
-
+
.. seealso::
-
- :py:class:`qwt.scale_draw.QwtScaleDraw`,
+
+ :py:class:`qwt.scale_draw.QwtScaleDraw`,
:py:class:`qwt.scale_widget.QwtScaleWigdet`
-
+
.. warning::
-
+
The attributes of scaleDraw will be overwritten by those of the
previous QwtScaleDraw.
"""
@@ -840,12 +792,12 @@ def setAxisScaleDraw(self, axisId, scaleDraw):
def setAxisLabelAlignment(self, axisId, alignment):
"""
Change the alignment of the tick labels
-
+
:param int axisId: Axis index
:param Qt.Alignment alignment: Or'd Qt.AlignmentFlags
-
+
.. seealso::
-
+
:py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAlignment()`
"""
if self.axisValid(axisId):
@@ -854,12 +806,12 @@ def setAxisLabelAlignment(self, axisId, alignment):
def setAxisLabelRotation(self, axisId, rotation):
"""
Rotate all tick labels
-
+
:param int axisId: Axis index
:param float rotation: Angle in degrees. When changing the label rotation, the label alignment might be adjusted too.
-
+
.. seealso::
-
+
:py:meth:`setLabelRotation()`, :py:meth:`setAxisLabelAlignment()`
"""
if self.axisValid(axisId):
@@ -868,12 +820,12 @@ def setAxisLabelRotation(self, axisId, rotation):
def setAxisLabelAutoSize(self, axisId, state):
"""
Set tick labels automatic size option (default: on)
-
+
:param int axisId: Axis index
- :param bool state: On/off
-
+ :param bool state: On/off
+
.. seealso::
-
+
:py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAutoSize()`
"""
if self.axisValid(axisId):
@@ -882,12 +834,12 @@ def setAxisLabelAutoSize(self, axisId, state):
def setAxisMaxMinor(self, axisId, maxMinor):
"""
Set the maximum number of minor scale intervals for a specified axis
-
+
:param int axisId: Axis index
:param int maxMinor: Maximum number of minor steps
-
+
.. seealso::
-
+
:py:meth:`axisMaxMinor()`
"""
if self.axisValid(axisId):
@@ -901,12 +853,12 @@ def setAxisMaxMinor(self, axisId, maxMinor):
def setAxisMaxMajor(self, axisId, maxMajor):
"""
Set the maximum number of major scale intervals for a specified axis
-
+
:param int axisId: Axis index
:param int maxMajor: Maximum number of major steps
-
+
.. seealso::
-
+
:py:meth:`axisMaxMajor()`
"""
if self.axisValid(axisId):
@@ -917,10 +869,30 @@ def setAxisMaxMajor(self, axisId, maxMajor):
d.isValid = False
self.autoRefresh()
+ def setAxisMargin(self, axisId, margin):
+ """
+ Set the relative margin of the axis, as a fraction of the full axis range
+
+ :param int axisId: Axis index
+ :param float margin: Relative margin (float between 0 and 1)
+
+ .. seealso::
+
+ :py:meth:`axisMargin()`
+ """
+ if not isinstance(margin, float) or margin < 0.0 or margin > 1.0:
+ raise ValueError("margin must be a float between 0 and 1")
+ if self.axisValid(axisId):
+ d = self.__axisData[axisId]
+ if margin != d.margin:
+ d.margin = margin
+ d.isValid = False
+ self.autoRefresh()
+
def setAxisTitle(self, axisId, title):
"""
Change the title of a specified axis
-
+
:param int axisId: Axis index
:param title: axis title
:type title: qwt.text.QwtText or str
@@ -933,26 +905,26 @@ def updateAxes(self):
"""
Rebuild the axes scales
- In case of autoscaling the boundaries of a scale are calculated
- from the bounding rectangles of all plot items, having the
- `QwtPlotItem.AutoScale` flag enabled (`QwtScaleEngine.autoScale()`).
- Then a scale division is calculated (`QwtScaleEngine.didvideScale()`)
+ In case of autoscaling the boundaries of a scale are calculated
+ from the bounding rectangles of all plot items, having the
+ `QwtPlotItem.AutoScale` flag enabled (`QwtScaleEngine.autoScale()`).
+ Then a scale division is calculated (`QwtScaleEngine.didvideScale()`)
and assigned to scale widget.
-
- When the scale boundaries have been assigned with `setAxisScale()` a
+
+ When the scale boundaries have been assigned with `setAxisScale()` a
scale division is calculated (`QwtScaleEngine.didvideScale()`)
for this interval and assigned to the scale widget.
-
- When the scale has been set explicitly by `setAxisScaleDiv()` the
+
+ When the scale has been set explicitly by `setAxisScaleDiv()` the
locally stored scale division gets assigned to the scale widget.
-
- The scale widget indicates modifications by emitting a
+
+ The scale widget indicates modifications by emitting a
`QwtScaleWidget.scaleDivChanged()` signal.
-
- `updateAxes()` is usually called by `replot()`.
-
+
+ `updateAxes()` is usually called by `replot()`.
+
.. seealso::
-
+
:py:meth:`setAxisAutoScale()`, :py:meth:`setAxisScale()`,
:py:meth:`setAxisScaleDiv()`, :py:meth:`replot()`,
:py:meth:`QwtPlotItem.boundingRect()`
@@ -970,6 +942,7 @@ def updateAxes(self):
intv[item.xAxis()] |= QwtInterval(rect.left(), rect.right())
if rect.height() >= 0.0:
intv[item.yAxis()] |= QwtInterval(rect.top(), rect.bottom())
+
for axisId in self.AXES:
d = self.__axisData[axisId]
minValue = d.minValue
@@ -979,7 +952,9 @@ def updateAxes(self):
d.isValid = False
minValue = intv[axisId].minValue()
maxValue = intv[axisId].maxValue()
- d.scaleEngine.autoScale(d.maxMajor, minValue, maxValue, stepSize)
+ minValue, maxValue, stepSize = d.scaleEngine.autoScale(
+ d.maxMajor, minValue, maxValue, stepSize, d.margin
+ )
if not d.isValid:
d.scaleDiv = d.scaleEngine.divideScale(
minValue, maxValue, d.maxMajor, d.maxMinor, stepSize
@@ -992,8 +967,8 @@ def updateAxes(self):
# Otherwise, when tick labels are large enough, the ticks
# may not be aligned with canvas grid.
# See the following issues for more details:
- # https://github.com/PierreRaybaut/guiqwt/issues/57
- # https://github.com/PierreRaybaut/PythonQwt/issues/30
+ # https://github.com/PlotPyStack/guiqwt/issues/57
+ # https://github.com/PlotPyStack/PythonQwt/issues/30
startDist, endDist = scaleWidget.getBorderDistHint()
scaleWidget.setBorderDist(startDist, endDist)
@@ -1006,13 +981,13 @@ def updateAxes(self):
def setCanvas(self, canvas):
"""
Set the drawing canvas of the plot widget.
-
+
The default canvas is a `QwtPlotCanvas`.
-
+
:param QWidget canvas: Canvas Widget
.. seealso::
-
+
:py:meth:`canvas()`
"""
if canvas == self.__data.canvas:
@@ -1025,12 +1000,11 @@ def setCanvas(self, canvas):
canvas.show()
def event(self, event):
- ok = QFrame.event(self, event)
if event.type() == QEvent.LayoutRequest:
self.updateLayout()
elif event.type() == QEvent.PolishRequest:
self.replot()
- return ok
+ return QFrame.event(self, event)
def eventFilter(self, obj, event):
if obj is self.__data.canvas:
@@ -1054,15 +1028,15 @@ def setAutoReplot(self, tf=True):
Since this may be time-consuming, it is recommended
to leave this option switched off and call :py:meth:`replot()`
explicitly if necessary.
-
+
The autoReplot option is set to false by default, which
- means that the user has to call :py:meth:`replot()` in order
+ means that the user has to call :py:meth:`replot()` in order
to make changes visible.
-
+
:param bool tf: True or False. Defaults to True.
.. seealso::
-
+
:py:meth:`autoReplot()`
"""
self.__data.autoReplot = tf
@@ -1072,7 +1046,7 @@ def autoReplot(self):
:return: True if the autoReplot option is set.
.. seealso::
-
+
:py:meth:`setAutoReplot()`
"""
return self.__data.autoReplot
@@ -1080,12 +1054,12 @@ def autoReplot(self):
def setTitle(self, title):
"""
Change the plot's title
-
+
:param title: New title
:type title: str or qwt.text.QwtText
.. seealso::
-
+
:py:meth:`title()`
"""
current_title = self.__data.titleLabel.text()
@@ -1101,7 +1075,7 @@ def title(self):
:return: Title of the plot
.. seealso::
-
+
:py:meth:`setTitle()`
"""
return self.__data.titleLabel.text()
@@ -1115,12 +1089,12 @@ def titleLabel(self):
def setFooter(self, text):
"""
Change the text the footer
-
+
:param text: New text of the footer
:type text: str or qwt.text.QwtText
.. seealso::
-
+
:py:meth:`footer()`
"""
current_footer = self.__data.footerLabel.text()
@@ -1136,7 +1110,7 @@ def footer(self):
:return: Text of the footer
.. seealso::
-
+
:py:meth:`setFooter()`
"""
return self.__data.footerLabel.text()
@@ -1150,12 +1124,12 @@ def footerLabel(self):
def setPlotLayout(self, layout):
"""
Assign a new plot layout
-
+
:param layout: Layout
:type layout: qwt.plot_layout.QwtPlotLayout
.. seealso::
-
+
:py:meth:`plotLayout()`
"""
if layout != self.__data.layout:
@@ -1167,7 +1141,7 @@ def plotLayout(self):
:return: the plot's layout
.. seealso::
-
+
:py:meth:`setPlotLayout()`
"""
return self.__data.layout
@@ -1177,7 +1151,7 @@ def legend(self):
:return: the plot's legend
.. seealso::
-
+
:py:meth:`insertLegend()`
"""
return self.__data.legend
@@ -1193,7 +1167,7 @@ def sizeHint(self):
:return: Size hint for the plot widget
.. seealso::
-
+
:py:meth:`minimumSizeHint()`
"""
dw = dh = 0
@@ -1238,7 +1212,7 @@ def replot(self):
be refreshed explicitly in order to make changes visible.
.. seealso::
-
+
:py:meth:`updateAxes()`, :py:meth:`setAutoReplot()`
"""
doAutoReplot = self.autoReplot()
@@ -1275,7 +1249,7 @@ def updateLayout(self):
Adjust plot content to its current size.
.. seealso::
-
+
:py:meth:`resizeEvent()`
"""
# state = self.get_layout_state()
@@ -1348,7 +1322,7 @@ def updateLayout(self):
def getCanvasMarginsHint(self, maps, canvasRect):
"""
Calculate the canvas margins
-
+
:param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates
:param QRectF canvasRect: Bounding rectangle where to paint
@@ -1356,7 +1330,7 @@ def getCanvasMarginsHint(self, maps, canvasRect):
at the borders of the canvas by the `QwtPlotItem.Margins` flag.
.. seealso::
-
+
:py:meth:`updateCanvasMargins()`, :py:meth:`getCanvasMarginHint()`
"""
left = top = right = bottom = -1.0
@@ -1381,8 +1355,8 @@ def updateCanvasMargins(self):
at the borders of the canvas by the `QwtPlotItem.Margins` flag.
.. seealso::
-
- :py:meth:`getCanvasMarginsHint()`,
+
+ :py:meth:`getCanvasMarginsHint()`,
:py:meth:`QwtPlotItem.getCanvasMarginHint()`
"""
maps = [self.canvasMap(axisId) for axisId in self.AXES]
@@ -1392,7 +1366,7 @@ def updateCanvasMargins(self):
for axisId in self.AXES:
if margins[axisId] >= 0.0:
- m = np.ceil(margins[axisId])
+ m = math.ceil(margins[axisId])
self.plotLayout().setCanvasMargin(m, axisId)
doUpdate = True
@@ -1402,36 +1376,36 @@ def updateCanvasMargins(self):
def drawCanvas(self, painter):
"""
Redraw the canvas.
-
+
:param QPainter painter: Painter used for drawing
.. warning::
-
+
drawCanvas calls drawItems what is also used
for printing. Applications that like to add individual
plot items better overload drawItems()
.. seealso::
-
- :py:meth:`getCanvasMarginsHint()`,
+
+ :py:meth:`getCanvasMarginsHint()`,
:py:meth:`QwtPlotItem.getCanvasMarginHint()`
"""
maps = [self.canvasMap(axisId) for axisId in self.AXES]
- self.drawItems(painter, self.__data.canvas.contentsRect(), maps)
+ self.drawItems(painter, QRectF(self.__data.canvas.contentsRect()), maps)
def drawItems(self, painter, canvasRect, maps):
"""
Redraw the canvas.
-
+
:param QPainter painter: Painter used for drawing
:param QRectF canvasRect: Bounding rectangle where to paint
:param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates
.. note::
-
+
Usually canvasRect is `contentsRect()` of the plot canvas.
- Due to a bug in Qt this rectangle might be wrong for certain
- frame styles ( f.e `QFrame.Box` ) and it might be necessary to
+ Due to a bug in Qt this rectangle might be wrong for certain
+ frame styles ( f.e `QFrame.Box` ) and it might be necessary to
fix the margins manually using `QWidget.setContentsMargins()`
"""
for item in self.itemList():
@@ -1441,10 +1415,6 @@ def drawItems(self, painter, canvasRect, maps):
QPainter.Antialiasing,
item.testRenderHint(QwtPlotItem.RenderAntialiased),
)
- painter.setRenderHint(
- QPainter.HighQualityAntialiasing,
- item.testRenderHint(QwtPlotItem.RenderAntialiased),
- )
item.draw(painter, maps[item.xAxis()], maps[item.yAxis()], canvasRect)
painter.restore()
@@ -1454,8 +1424,8 @@ def canvasMap(self, axisId):
:return: Map for the axis on the canvas. With this map pixel coordinates can translated to plot coordinates and vice versa.
.. seealso::
-
- :py:class:`qwt.scale_map.QwtScaleMap`,
+
+ :py:class:`qwt.scale_map.QwtScaleMap`,
:py:meth:`transform()`, :py:meth:`invTransform()`
"""
map_ = QwtScaleMap()
@@ -1464,6 +1434,8 @@ def canvasMap(self, axisId):
map_.setTransformation(self.axisScaleEngine(axisId).transformation())
sd = self.axisScaleDiv(axisId)
+ if sd is None:
+ return map_
map_.setScaleInterval(sd.lowerBound(), sd.upperBound())
if self.axisEnabled(axisId):
@@ -1511,7 +1483,7 @@ def setCanvasBackground(self, brush):
:param QBrush brush: New background brush
.. seealso::
-
+
:py:meth:`canvasBackground()`
"""
pal = self.__data.canvas.palette()
@@ -1523,10 +1495,10 @@ def canvasBackground(self):
:return: Background brush of the plotting area.
.. seealso::
-
+
:py:meth:`setCanvasBackground()`
"""
- return self.canvas().palette().brush(QPalette.Normal, QPalette.Window)
+ return self.canvas().palette().brush(QPalette.Active, QPalette.Window)
def axisValid(self, axis_id):
"""
@@ -1543,11 +1515,11 @@ def insertLegend(self, legend, pos=None, ratio=-1):
the legend will be organized in one column from top to down.
Otherwise the legend items will be placed in a table
with a best fit number of columns from left to right.
-
+
insertLegend() will set the plot widget as parent for the legend.
- The legend will be deleted in the destructor of the plot or when
+ The legend will be deleted in the destructor of the plot or when
another legend is inserted.
-
+
Legends, that are not inserted into the layout of the plot widget
need to connect to the legendDataChanged() signal. Calling updateLegend()
initiates this signal for an initial update. When the application code
@@ -1555,24 +1527,24 @@ def insertLegend(self, legend, pos=None, ratio=-1):
rendering plots to a document ( see QwtPlotRenderer ).
:param qwt.legend.QwtAbstractLegend legend: Legend
- :param QwtPlot.LegendPosition pos: The legend's position.
+ :param QwtPlot.LegendPosition pos: The legend's position.
:param float ratio: Ratio between legend and the bounding rectangle of title, canvas and axes
.. note::
- For top/left position the number of columns will be limited to 1,
+ For top/left position the number of columns will be limited to 1,
otherwise it will be set to unlimited.
.. note::
- The legend will be shrunk if it would need more space than the
- given ratio. The ratio is limited to ]0.0 .. 1.0].
- In case of <= 0.0 it will be reset to the default ratio.
+ The legend will be shrunk if it would need more space than the
+ given ratio. The ratio is limited to ]0.0 .. 1.0].
+ In case of <= 0.0 it will be reset to the default ratio.
The default vertical/horizontal ratio is 0.33/0.5.
.. seealso::
-
- :py:meth:`legend()`,
+
+ :py:meth:`legend()`,
:py:meth:`qwt.plot_layout.QwtPlotLayout.legendPosition()`,
:py:meth:`qwt.plot_layout.QwtPlotLayout.setLegendPosition()`
"""
@@ -1581,6 +1553,7 @@ def insertLegend(self, legend, pos=None, ratio=-1):
self.__data.layout.setLegendPosition(pos, ratio)
if legend != self.__data.legend:
if self.__data.legend and self.__data.legend.parent() is self:
+ self.__data.legend.setParent(None)
del self.__data.legend
self.__data.legend = legend
if self.__data.legend:
@@ -1588,9 +1561,9 @@ def insertLegend(self, legend, pos=None, ratio=-1):
if self.__data.legend.parent() is not self:
self.__data.legend.setParent(self)
- qwtEnableLegendItems(self, False)
+ self.legendDataChanged.disconnect(self.updateLegendItems)
self.updateLegend()
- qwtEnableLegendItems(self, True)
+ self.legendDataChanged.connect(self.updateLegendItems)
lpos = self.__data.layout.legendPosition()
@@ -1618,13 +1591,13 @@ def insertLegend(self, legend, pos=None, ratio=-1):
def updateLegend(self, plotItem=None):
"""
- If plotItem is None, emit QwtPlot.legendDataChanged for all
+ If plotItem is None, emit QwtPlot.legendDataChanged for all
plot item. Otherwise, emit the signal for passed plot item.
-
+
:param qwt.plot.QwtPlotItem plotItem: Plot item
.. seealso::
-
+
:py:meth:`QwtPlotItem.legendData()`, :py:data:`QwtPlot.legendDataChanged`
"""
if plotItem is None:
@@ -1643,15 +1616,15 @@ def updateLegendItems(self, plotItem, legendData):
"""
Update all plot items interested in legend attributes
- Call `QwtPlotItem.updateLegend()`, when the
+ Call `QwtPlotItem.updateLegend()`, when the
`QwtPlotItem.LegendInterest` flag is set.
-
+
:param qwt.plot.QwtPlotItem plotItem: Plot item
:param list legendData: Entries to be displayed for the plot item ( usually 1 )
.. seealso::
-
- :py:meth:`QwtPlotItem.LegendInterest()`,
+
+ :py:meth:`QwtPlotItem.LegendInterest()`,
:py:meth:`QwtPlotItem.updateLegend`
"""
if plotItem is not None:
@@ -1662,7 +1635,7 @@ def updateLegendItems(self, plotItem, legendData):
def attachItem(self, plotItem, on):
"""
Attach/Detach a plot item
-
+
:param qwt.plot.QwtPlotItem plotItem: Plot item
:param bool on: When true attach the item, otherwise detach it
"""
@@ -1691,37 +1664,39 @@ def attachItem(self, plotItem, on):
def print_(self, printer):
"""
Print plot to printer
-
+
:param printer: Printer
:type printer: QPaintDevice or QPrinter or QSvgGenerator
"""
- from .plot_renderer import QwtPlotRenderer
+ from qwt.plot_renderer import QwtPlotRenderer
renderer = QwtPlotRenderer(self)
renderer.renderTo(self, printer)
def exportTo(
- self, filename, size=(800, 600), size_mm=None, resolution=72.0, format_=None
+ self, filename, size=(800, 600), size_mm=None, resolution=85, format_=None
):
"""
Export plot to PDF or image file (SVG, PNG, ...)
-
+
:param str filename: Filename
:param tuple size: (width, height) size in pixels
:param tuple size_mm: (width, height) size in millimeters
- :param float resolution: Image resolution
+ :param int resolution: Resolution in dots per Inch (dpi)
:param str format_: File format (PDF, SVG, PNG, ...)
"""
if size_mm is None:
size_mm = tuple(25.4 * np.array(size) / resolution)
- from .plot_renderer import QwtPlotRenderer
+ from qwt.plot_renderer import QwtPlotRenderer
renderer = QwtPlotRenderer(self)
renderer.renderDocument(self, filename, size_mm, resolution, format_)
-class QwtPlotItem_PrivateData(object):
+class QwtPlotItem_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.plot = None
self.isVisible = True
self.attributes = 0
@@ -1737,31 +1712,31 @@ def __init__(self):
class QwtPlotItem(object):
"""
Base class for items on the plot canvas
-
+
A plot item is "something", that can be painted on the plot canvas,
or only affects the scales of the plot widget. They can be categorized as:
-
+
- Representator
A "Representator" is an item that represents some sort of data
on the plot canvas. The different representator classes are organized
according to the characteristics of the data:
- - :py:class:`qwt.plot_marker.QwtPlotMarker`: Represents a point or a
+ - :py:class:`qwt.plot_marker.QwtPlotMarker`: Represents a point or a
horizontal/vertical coordinate
- - :py:class:`qwt.plot_curve.QwtPlotCurve`: Represents a series of
+ - :py:class:`qwt.plot_curve.QwtPlotCurve`: Represents a series of
points
-
+
- Decorators
A "Decorator" is an item, that displays additional information, that
is not related to any data:
- :py:class:`qwt.plot_grid.QwtPlotGrid`
-
+
Depending on the `QwtPlotItem.ItemAttribute` flags, an item is included
into autoscaling or has an entry on the legend.
-
+
Before misusing the existing item classes it might be better to
implement a new type of plot item
( don't implement a watermark as spectrogram ).
@@ -1773,9 +1748,9 @@ class QwtPlotItem(object):
The cpuplot example shows the implementation of additional plot items.
.. py:class:: QwtPlotItem([title=None])
-
+
Constructor
-
+
:param title: Title of the item
:type title: qwt.text.QwtText or str
"""
@@ -1814,7 +1789,7 @@ class QwtPlotItem(object):
# enum RenderHint
RenderAntialiased = 0x1
- def __init__(self, title=None):
+ def __init__(self, title=None, icon=None):
"""title: QwtText"""
if title is None:
title = QwtText("")
@@ -1823,20 +1798,21 @@ def __init__(self, title=None):
assert isinstance(title, QwtText)
self.__data = QwtPlotItem_PrivateData()
self.__data.title = title
+ self.__data.icon = icon
def attach(self, plot):
"""
Attach the item to a plot.
- This method will attach a `QwtPlotItem` to the `QwtPlot` argument.
- It will first detach the `QwtPlotItem` from any plot from a previous
- call to attach (if necessary). If a None argument is passed, it will
+ This method will attach a `QwtPlotItem` to the `QwtPlot` argument.
+ It will first detach the `QwtPlotItem` from any plot from a previous
+ call to attach (if necessary). If a None argument is passed, it will
detach from any `QwtPlot` it was attached to.
-
+
:param qwt.plot.QwtPlot plot: Plot widget
.. seealso::
-
+
:py:meth:`detach()`
"""
if plot is self.__data.plot:
@@ -1854,22 +1830,22 @@ def detach(self):
"""
Detach the item from a plot.
- This method detaches a `QwtPlotItem` from any `QwtPlot` it has been
+ This method detaches a `QwtPlotItem` from any `QwtPlot` it has been
associated with.
.. seealso::
-
+
:py:meth:`attach()`
"""
self.attach(None)
def rtti(self):
"""
- Return rtti for the specific class represented. `QwtPlotItem` is
- simply a virtual interface class, and base classes will implement
- this method with specific rtti values so a user can differentiate
+ Return rtti for the specific class represented. `QwtPlotItem` is
+ simply a virtual interface class, and base classes will implement
+ this method with specific rtti values so a user can differentiate
them.
-
+
:return: rtti value
"""
return self.Rtti_PlotItem
@@ -1883,11 +1859,11 @@ def plot(self):
def z(self):
"""
Plot items are painted in increasing z-order.
-
+
:return: item z order
.. seealso::
-
+
:py:meth:`setZ()`, :py:meth:`QwtPlotDict.itemList()`
"""
return self.__data.z
@@ -1895,13 +1871,13 @@ def z(self):
def setZ(self, z):
"""
Set the z value
-
+
Plot items are painted in increasing z-order.
-
+
:param float z: Z-value
.. seealso::
-
+
:py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()`
"""
if self.__data.z != z:
@@ -1915,12 +1891,12 @@ def setZ(self, z):
def setTitle(self, title):
"""
Set a new title
-
+
:param title: Title
:type title: qwt.text.QwtText or str
.. seealso::
-
+
:py:meth:`title()`
"""
if not isinstance(title, QwtText):
@@ -1934,7 +1910,7 @@ def title(self):
:return: Title of the item
.. seealso::
-
+
:py:meth:`setTitle()`
"""
return self.__data.title
@@ -1942,12 +1918,12 @@ def title(self):
def setItemAttribute(self, attribute, on=True):
"""
Toggle an item attribute
-
+
:param int attribute: Attribute type
:param bool on: True/False
.. seealso::
-
+
:py:meth:`testItemAttribute()`
"""
if bool(self.__data.attributes & attribute) != on:
@@ -1962,12 +1938,12 @@ def setItemAttribute(self, attribute, on=True):
def testItemAttribute(self, attribute):
"""
Test an item attribute
-
+
:param int attribute: Attribute type
:return: True/False
.. seealso::
-
+
:py:meth:`setItemAttribute()`
"""
return bool(self.__data.attributes & attribute)
@@ -1975,12 +1951,12 @@ def testItemAttribute(self, attribute):
def setItemInterest(self, interest, on=True):
"""
Toggle an item interest
-
+
:param int attribute: Interest type
:param bool on: True/False
.. seealso::
-
+
:py:meth:`testItemInterest()`
"""
if bool(self.__data.interests & interest) != on:
@@ -1993,12 +1969,12 @@ def setItemInterest(self, interest, on=True):
def testItemInterest(self, interest):
"""
Test an item interest
-
+
:param int attribute: Interest type
:return: True/False
.. seealso::
-
+
:py:meth:`setItemInterest()`
"""
return bool(self.__data.interests & interest)
@@ -2006,12 +1982,12 @@ def testItemInterest(self, interest):
def setRenderHint(self, hint, on=True):
"""
Toggle a render hint
-
+
:param int hint: Render hint
:param bool on: True/False
.. seealso::
-
+
:py:meth:`testRenderHint()`
"""
if bool(self.__data.renderHints & hint) != on:
@@ -2024,12 +2000,12 @@ def setRenderHint(self, hint, on=True):
def testRenderHint(self, hint):
"""
Test a render hint
-
+
:param int attribute: Render hint
:return: True/False
.. seealso::
-
+
:py:meth:`setRenderHint()`
"""
return bool(self.__data.renderHints & hint)
@@ -2039,11 +2015,11 @@ def setLegendIconSize(self, size):
Set the size of the legend icon
The default setting is 8x8 pixels
-
+
:param QSize size: Size
.. seealso::
-
+
:py:meth:`legendIconSize()`, :py:meth:`legendIcon()`
"""
if self.__data.legendIconSize != size:
@@ -2055,7 +2031,7 @@ def legendIconSize(self):
:return: Legend icon size
.. seealso::
-
+
:py:meth:`setLegendIconSize()`, :py:meth:`legendIcon()`
"""
return self.__data.legendIconSize
@@ -2065,34 +2041,15 @@ def legendIcon(self, index, size):
:param int index: Index of the legend entry (usually there is only one)
:param QSizeF size: Icon size
:return: Icon representing the item on the legend
-
+
The default implementation returns an invalid icon
.. seealso::
-
+
:py:meth:`setLegendIconSize()`, :py:meth:`legendData()`
"""
return QwtGraphic()
- def defaultIcon(self, brush, size):
- """
- Return a default icon from a brush
-
- The default icon is a filled rectangle used
- in several derived classes as legendIcon().
-
- :param QBrush brush: Fill brush
- :param QSizeF size: Icon size
- :return: A filled rectangle
- """
- icon = QwtGraphic()
- if not size.isEmpty():
- icon.setDefaultSize(size)
- r = QRectF(0, 0, size.width(), size.height())
- painter = QPainter(icon)
- painter.fillRect(r, brush)
- return icon
-
def show(self):
"""Show the item"""
self.setVisible(True)
@@ -2104,11 +2061,11 @@ def hide(self):
def setVisible(self, on):
"""
Show/Hide the item
-
+
:param bool on: Show if True, otherwise hide
.. seealso::
-
+
:py:meth:`isVisible()`, :py:meth:`show()`, :py:meth:`hide()`
"""
if on != self.__data.isVisible:
@@ -2120,7 +2077,7 @@ def isVisible(self):
:return: True if visible
.. seealso::
-
+
:py:meth:`setVisible()`, :py:meth:`show()`, :py:meth:`hide()`
"""
return self.__data.isVisible
@@ -2131,7 +2088,7 @@ def itemChanged(self):
parent plot.
.. seealso::
-
+
:py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()`
"""
if self.__data.plot:
@@ -2140,9 +2097,9 @@ def itemChanged(self):
def legendChanged(self):
"""
Update the legend of the parent plot.
-
+
.. seealso::
-
+
:py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()`
"""
if self.testItemAttribute(QwtPlotItem.Legend) and self.__data.plot:
@@ -2153,12 +2110,12 @@ def setAxes(self, xAxis, yAxis):
Set X and Y axis
The item will painted according to the coordinates of its Axes.
-
+
:param int xAxis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`)
:param int yAxis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`)
-
+
.. seealso::
-
+
:py:meth:`setXAxis()`, :py:meth:`setYAxis()`,
:py:meth:`xAxis()`, :py:meth:`yAxis()`
"""
@@ -2173,14 +2130,14 @@ def setAxis(self, xAxis, yAxis):
Set X and Y axis
.. warning::
-
- `setAxis` has been removed in Qwt6: please use
+
+ `setAxis` has been removed in Qwt6: please use
:py:meth:`setAxes()` instead
"""
import warnings
warnings.warn(
- "`setAxis` has been removed in Qwt6: " "please use `setAxes` instead",
+ "`setAxis` has been removed in Qwt6: please use `setAxes` instead",
RuntimeWarning,
)
self.setAxes(xAxis, yAxis)
@@ -2190,11 +2147,11 @@ def setXAxis(self, axis):
Set the X axis
The item will painted according to the coordinates its Axes.
-
+
:param int axis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`)
-
+
.. seealso::
-
+
:py:meth:`setAxes()`, :py:meth:`setYAxis()`,
:py:meth:`xAxis()`, :py:meth:`yAxis()`
"""
@@ -2207,11 +2164,11 @@ def setYAxis(self, axis):
Set the Y axis
The item will painted according to the coordinates its Axes.
-
+
:param int axis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`)
-
+
.. seealso::
-
+
:py:meth:`setAxes()`, :py:meth:`setXAxis()`,
:py:meth:`xAxis()`, :py:meth:`yAxis()`
"""
@@ -2234,9 +2191,9 @@ def yAxis(self):
def boundingRect(self):
"""
:return: An invalid bounding rect: QRectF(1.0, 1.0, -2.0, -2.0)
-
+
.. note::
-
+
A width or height < 0.0 is ignored by the autoscaler
"""
return QRectF(1.0, 1.0, -2.0, -2.0)
@@ -2249,16 +2206,16 @@ def getCanvasMarginHint(self, xMap, yMap, canvasRect):
indicates, that it needs some margins at the borders of the canvas.
This is f.e. used by bar charts to reserve space for displaying
the bars.
-
+
The margins are in target device coordinates ( pixels on screen )
-
+
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
-
+
.. seealso::
-
- :py:meth:`QwtPlot.getCanvasMarginsHint()`,
+
+ :py:meth:`QwtPlot.getCanvasMarginsHint()`,
:py:meth:`QwtPlot.updateCanvasMargins()`,
"""
left = top = right = bottom = 0.0
@@ -2268,20 +2225,20 @@ def legendData(self):
"""
Return all information, that is needed to represent
the item on the legend
-
+
`QwtLegendData` is basically a list of QVariants that makes it
- possible to overload and reimplement legendData() to
+ possible to overload and reimplement legendData() to
return almost any type of information, that is understood
by the receiver that acts as the legend.
-
- The default implementation returns one entry with
+
+ The default implementation returns one entry with
the title() of the item and the legendIcon().
-
+
:return: Data, that is needed to represent the item on the legend
-
+
.. seealso::
-
- :py:meth:`title()`, :py:meth:`legendIcon()`,
+
+ :py:meth:`title()`, :py:meth:`legendIcon()`,
:py:class:`qwt.legend.QwtLegend`
"""
data = QwtLegendData()
@@ -2299,15 +2256,15 @@ def updateLegend(self, item, data):
Plot items that want to display a legend ( not those, that want to
be displayed on a legend ! ) will have to implement updateLegend().
-
+
updateLegend() is only called when the LegendInterest interest
is enabled. The default implementation does nothing.
-
+
:param qwt.plot.QwtPlotItem item: Plot item to be displayed on a legend
:param list data: Attributes how to display item on the legend
-
+
.. note::
-
+
Plot items, that want to be displayed on a legend
need to enable the `QwtPlotItem.Legend` flag and to implement
legendData() and legendIcon()
@@ -2317,7 +2274,7 @@ def updateLegend(self, item, data):
def scaleRect(self, xMap, yMap):
"""
Calculate the bounding scale rectangle of 2 maps
-
+
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:return: Bounding scale rect of the scale maps, not normalized
@@ -2327,7 +2284,7 @@ def scaleRect(self, xMap, yMap):
def paintRect(self, xMap, yMap):
"""
Calculate the bounding paint rectangle of 2 maps
-
+
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:return: Bounding paint rectangle of the scale maps, not normalized
diff --git a/qwt/plot_canvas.py b/qwt/plot_canvas.py
index 1d1b79f..a72d9b8 100644
--- a/qwt/plot_canvas.py
+++ b/qwt/plot_canvas.py
@@ -13,28 +13,26 @@
:members:
"""
-from .null_paintdevice import QwtNullPaintDevice
-from .painter import QwtPainter
+from collections.abc import Sequence
-from .qt import PYQT5
-from .qt.QtGui import (
- QFrame,
- QPaintEngine,
- QPen,
+from qtpy.QtCore import QEvent, QObject, QPoint, QPointF, QRect, QRectF, QSize, Qt
+from qtpy.QtGui import (
QBrush,
- QRegion,
+ QGradient,
QImage,
+ QPaintEngine,
+ QPainter,
QPainterPath,
+ QPen,
QPixmap,
- QGradient,
- QPainter,
- qAlpha,
QPolygonF,
- QStyleOption,
- QStyle,
- QStyleOptionFrame,
+ QRegion,
+ qAlpha,
)
-from .qt.QtCore import Qt, QSizeF, QT_VERSION, QEvent, QPointF, QRectF
+from qtpy.QtWidgets import QFrame, QStyle, QStyleOption, QStyleOptionFrame
+
+from qwt.null_paintdevice import QwtNullPaintDevice
+from qwt.painter import QwtPainter
class Border(object):
@@ -71,8 +69,15 @@ def updateState(self, state):
self.__origin = state.brushOrigin()
def drawRects(self, rects, count):
- for i in range(count):
- self.border.rectList += [rects[i]]
+ if isinstance(rects, (QRect, QRectF)):
+ self.border.rectList = [QRectF(rects)]
+ elif isinstance(rects, Sequence):
+ self.border.rectList.extend(QRectF(rects[i]) for i in range(count))
+ else:
+ raise TypeError(
+ "drawRects() expects a QRect, QRectF or a sequence of them, "
+ f"but got {type(rects).__name__}"
+ )
def drawPath(self, path):
rect = QRectF(QPointF(0.0, 0.0), self.__size)
@@ -123,15 +128,6 @@ def alignCornerRects(self, rect):
r.setBottom(rect.bottom())
-def _rects_conv_PyQt5(rects):
- # PyQt5 compatibility: the conversion from QRect to QRectF should not
- # be necessary but it seems to be anyway... PyQt5 bug?
- if PYQT5:
- return [QRectF(rect) for rect in rects]
- else:
- return rects
-
-
def qwtDrawBackground(painter, canvas):
painter.save()
borderClip = canvas.borderPath(canvas.rect())
@@ -147,7 +143,7 @@ def qwtDrawBackground(painter, canvas):
if brush.gradient().coordinateMode() == QGradient.ObjectBoundingMode:
rects += [canvas.rect()]
else:
- rects += [painter.clipRegion().rects()]
+ rects += [painter.clipRegion().boundingRect()]
useRaster = False
if painter.paintEngine().type() == QPaintEngine.X11:
useRaster = True
@@ -159,20 +155,22 @@ def qwtDrawBackground(painter, canvas):
format_ = QImage.Format_ARGB32
break
image = QImage(canvas.size(), format_)
- p = QPainter(image)
- p.setPen(Qt.NoPen)
- p.setBrush(brush)
- p.drawRects(_rects_conv_PyQt5(rects))
- p.end()
+ pntr = QPainter(image)
+ pntr.setPen(Qt.NoPen)
+ pntr.setBrush(brush)
+ for rect in rects:
+ pntr.drawRect(rect)
+ pntr.end()
painter.drawImage(0, 0, image)
else:
painter.setPen(Qt.NoPen)
painter.setBrush(brush)
- painter.drawRects(_rects_conv_PyQt5(rects))
+ for rect in rects:
+ painter.drawRect(rect)
else:
painter.setPen(Qt.NoPen)
painter.setBrush(brush)
- painter.drawRects(_rects_conv_PyQt5(painter.clipRegion().rects()))
+ painter.drawRect(painter.clipRegion().boundingRect())
painter.restore()
@@ -278,12 +276,12 @@ def qwtFillBackground(*args):
r = canvas.rect()
radius = canvas.borderRadius()
if radius > 0.0:
- sz = QSizeF(radius, radius)
+ sz = QSize(radius, radius)
rects += [
- QRectF(r.topLeft(), sz),
- QRectF(r.topRight() - QPointF(radius, 0), sz),
- QRectF(r.bottomRight() - QPointF(radius, radius), sz),
- QRectF(r.bottomLeft() - QPointF(0, radius), sz),
+ QRect(r.topLeft(), sz),
+ QRect(r.topRight() - QPoint(radius, 0), sz),
+ QRect(r.bottomRight() - QPoint(radius, radius), sz),
+ QRect(r.bottomLeft() - QPoint(0, radius), sz),
]
qwtFillBackground(painter, canvas, rects)
@@ -298,8 +296,7 @@ def qwtFillBackground(*args):
else:
clipRegion = widget.contentsRect()
bgWidget = qwtBackgroundWidget(widget.parentWidget())
- for fillRect in fillRects:
- rect = QRectF(fillRect).toAlignedRect()
+ for rect in fillRects:
if clipRegion.intersects(rect):
pm = QPixmap(rect.size())
QwtPainter.fillPixmap(
@@ -328,8 +325,10 @@ def __init__(self):
self.background = StyleSheetBackground()
-class QwtPlotCanvas_PrivateData(object):
+class QwtPlotCanvas_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.focusIndicator = QwtPlotCanvas.NoFocusIndicator
self.borderRadius = 0
self.paintAttributes = 0
@@ -341,45 +340,45 @@ def __init__(self):
class QwtPlotCanvas(QFrame):
"""
Canvas of a QwtPlot.
-
+
Canvas is the widget where all plot items are displayed
-
+
.. seealso::
-
+
:py:meth:`qwt.plot.QwtPlot.setCanvas()`
-
+
Paint attributes:
-
+
* `QwtPlotCanvas.BackingStore`:
-
- Paint double buffered reusing the content of the pixmap buffer
+
+ Paint double buffered reusing the content of the pixmap buffer
when possible.
-
- Using a backing store might improve the performance significantly,
+
+ Using a backing store might improve the performance significantly,
when working with widget overlays (like rubber bands).
Disabling the cache might improve the performance for
- incremental paints
+ incremental paints
(using :py:class:`qwt.plot_directpainter.QwtPlotDirectPainter`).
-
+
* `QwtPlotCanvas.Opaque`:
-
+
Try to fill the complete contents rectangle of the plot canvas
- When using styled backgrounds Qt assumes, that the canvas doesn't
- fill its area completely (f.e because of rounded borders) and
- fills the area below the canvas. When this is done with gradients
- it might result in a serious performance bottleneck - depending on
+ When using styled backgrounds Qt assumes, that the canvas doesn't
+ fill its area completely (f.e because of rounded borders) and
+ fills the area below the canvas. When this is done with gradients
+ it might result in a serious performance bottleneck - depending on
the size.
When the Opaque attribute is enabled the canvas tries to
identify the gaps with some heuristics and to fill those only.
-
+
.. warning::
-
- Will not work for semitransparent backgrounds
-
+
+ Will not work for semitransparent backgrounds
+
* `QwtPlotCanvas.HackStyledBackground`:
-
+
Try to improve painting of styled backgrounds
`QwtPlotCanvas` supports the box model attributes for
@@ -393,42 +392,42 @@ class QwtPlotCanvas(QFrame):
the border after the plot items. In this order the border
gets perfectly antialiased and you can avoid some pixel
artifacts in the corners.
-
+
* `QwtPlotCanvas.ImmediatePaint`:
-
+
When ImmediatePaint is set replot() calls repaint()
instead of update().
-
+
.. seealso::
-
- :py:meth:`replot()`, :py:meth:`QWidget.repaint()`,
+
+ :py:meth:`replot()`, :py:meth:`QWidget.repaint()`,
:py:meth:`QWidget.update()`
-
+
Focus indicators:
-
+
* `QwtPlotCanvas.NoFocusIndicator`:
-
+
Don't paint a focus indicator
* `QwtPlotCanvas.CanvasFocusIndicator`:
-
+
The focus is related to the complete canvas.
Paint the focus indicator using paintFocus()
* `QwtPlotCanvas.ItemFocusIndicator`:
-
+
The focus is related to an item (curve, point, ...) on
the canvas. It is up to the application to display a
focus indication using f.e. highlighting.
-
+
.. py:class:: QwtPlotCanvas([plot=None])
-
+
Constructor
-
+
:param qwt.plot.QwtPlot plot: Parent plot widget
.. seealso::
-
+
:py:meth:`qwt.plot.QwtPlot.setCanvas()`
"""
@@ -464,17 +463,17 @@ def setPaintAttribute(self, attribute, on=True):
Changing the paint attributes
Paint attributes:
-
+
* `QwtPlotCanvas.BackingStore`
* `QwtPlotCanvas.Opaque`
* `QwtPlotCanvas.HackStyledBackground`
* `QwtPlotCanvas.ImmediatePaint`
-
+
:param int attribute: Paint attribute
:param bool on: On/Off
-
+
.. seealso::
-
+
:py:meth:`testPaintAttribute()`, :py:meth:`backingStore()`
"""
if bool(self.__data.paintAttributes & attribute) == on:
@@ -488,10 +487,7 @@ def setPaintAttribute(self, attribute, on=True):
if self.__data.backingStore is None:
self.__data.backingStore = QPixmap()
if self.isVisible():
- if QT_VERSION >= 0x050000:
- self.__data.backingStore = self.grab(self.rect())
- else:
- self.__data.backingStore = QPixmap.grabWidget(self, self.rect())
+ self.__data.backingStore = self.grab(self.rect())
else:
self.__data.backingStore = None
elif attribute == self.Opaque:
@@ -503,12 +499,12 @@ def setPaintAttribute(self, attribute, on=True):
def testPaintAttribute(self, attribute):
"""
Test whether a paint attribute is enabled
-
+
:param int attribute: Paint attribute
:return: True, when attribute is enabled
-
+
.. seealso::
-
+
:py:meth:`setPaintAttribute()`
"""
return self.__data.paintAttributes & attribute
@@ -529,15 +525,15 @@ def setFocusIndicator(self, focusIndicator):
Set the focus indicator
Focus indicators:
-
+
* `QwtPlotCanvas.NoFocusIndicator`
* `QwtPlotCanvas.CanvasFocusIndicator`
* `QwtPlotCanvas.ItemFocusIndicator`
-
+
:param int focusIndicator: Focus indicator
-
+
.. seealso::
-
+
:py:meth:`focusIndicator()`
"""
self.__data.focusIndicator = focusIndicator
@@ -545,9 +541,9 @@ def setFocusIndicator(self, focusIndicator):
def focusIndicator(self):
"""
:return: Focus indicator
-
+
.. seealso::
-
+
:py:meth:`setFocusIndicator()`
"""
return self.__data.focusIndicator
@@ -555,11 +551,11 @@ def focusIndicator(self):
def setBorderRadius(self, radius):
"""
Set the radius for the corners of the border frame
-
+
:param float radius: Radius of a rounded corner
-
+
.. seealso::
-
+
:py:meth:`borderRadius()`
"""
self.__data.borderRadius = max([0.0, radius])
@@ -567,9 +563,9 @@ def setBorderRadius(self, radius):
def borderRadius(self):
"""
:return: Radius for the corners of the border frame
-
+
.. seealso::
-
+
:py:meth:`setBorderRadius()`
"""
return self.__data.borderRadius
@@ -588,12 +584,10 @@ def paintEvent(self, event):
if (
self.testPaintAttribute(self.BackingStore)
and self.__data.backingStore is not None
+ and not self.__data.backingStore.isNull()
):
bs = self.__data.backingStore
- if QT_VERSION >= 0x050000:
- pixelRatio = bs.devicePixelRatio()
- else:
- pixelRatio = 1.0
+ pixelRatio = bs.devicePixelRatio()
if bs.size() != self.size() * pixelRatio:
bs = QwtPainter.backingStore(self, self.size())
if self.testAttribute(Qt.WA_StyledBackground):
@@ -712,11 +706,11 @@ def drawCanvas(self, painter, withBackground):
def drawBorder(self, painter):
"""
Draw the border of the plot canvas
-
+
:param QPainter painter: Painter
-
+
.. seealso::
-
+
:py:meth:`setBorderRadius()`
"""
if self.__data.borderRadius > 0:
@@ -731,34 +725,33 @@ def drawBorder(self, painter):
self.frameStyle(),
)
else:
- if QT_VERSION >= 0x040500:
- if PYQT5:
- from .qt.QtGui import QStyleOptionFrame
- else:
- from .qt.QtGui import QStyleOptionFrameV3 as QStyleOptionFrame
- opt = QStyleOptionFrame()
- opt.initFrom(self)
- frameShape = self.frameStyle() & QFrame.Shape_Mask
- frameShadow = self.frameStyle() & QFrame.Shadow_Mask
- opt.frameShape = QFrame.Shape(int(opt.frameShape) | frameShape)
- if frameShape in (
- QFrame.Box,
- QFrame.HLine,
- QFrame.VLine,
- QFrame.StyledPanel,
- QFrame.Panel,
- ):
- opt.lineWidth = self.lineWidth()
- opt.midLineWidth = self.midLineWidth()
- else:
- opt.lineWidth = self.frameWidth()
- if frameShadow == self.Sunken:
- opt.state |= QStyle.State_Sunken
- elif frameShadow == self.Raised:
- opt.state |= QStyle.State_Raised
- self.style().drawControl(QStyle.CE_ShapedFrame, opt, painter, self)
+ opt = QStyleOptionFrame()
+ opt.initFrom(self)
+ try:
+ shape_mask = QFrame.Shape_Mask.value
+ shadow_mask = QFrame.Shadow_Mask.value
+ except AttributeError:
+ shape_mask = QFrame.Shape_Mask
+ shadow_mask = QFrame.Shadow_Mask
+ frameShape = self.frameStyle() & shape_mask
+ frameShadow = self.frameStyle() & shadow_mask
+ opt.frameShape = QFrame.Shape(int(opt.frameShape) | frameShape)
+ if frameShape in (
+ QFrame.Box,
+ QFrame.HLine,
+ QFrame.VLine,
+ QFrame.StyledPanel,
+ QFrame.Panel,
+ ):
+ opt.lineWidth = self.lineWidth()
+ opt.midLineWidth = self.midLineWidth()
else:
- self.drawFrame(painter)
+ opt.lineWidth = self.frameWidth()
+ if frameShadow == QFrame.Sunken:
+ opt.state |= QStyle.State_Sunken
+ elif frameShadow == QFrame.Raised:
+ opt.state |= QStyle.State_Raised
+ self.style().drawControl(QStyle.CE_ShapedFrame, opt, painter, self)
def resizeEvent(self, event):
QFrame.resizeEvent(self, event)
@@ -767,7 +760,7 @@ def resizeEvent(self, event):
def drawFocusIndicator(self, painter):
"""
Draw the focus indication
-
+
:param QPainter painter: Painter
"""
margin = 1
@@ -794,7 +787,7 @@ def invalidatePaintCache(self):
import warnings
warnings.warn(
- "`invalidatePaintCache` has been removed: " "please use `replot` instead",
+ "`invalidatePaintCache` has been removed: please use `replot` instead",
RuntimeWarning,
)
self.replot()
diff --git a/qwt/plot_curve.py b/qwt/plot_curve.py
index 0ee3cde..6b4105e 100644
--- a/qwt/plot_curve.py
+++ b/qwt/plot_curve.py
@@ -13,24 +13,33 @@
:members:
"""
-from .text import QwtText
-from .plot import QwtPlot, QwtPlotItem, QwtPlotItem_PrivateData
-from ._math import qwtSqr
-from .graphic import QwtGraphic
-from .plot_series import (
+import math
+import os
+
+import numpy as np
+from qtpy.QtCore import QLineF, QPointF, QRectF, QSize, Qt
+from qtpy.QtGui import QBrush, QColor, QPainter, QPen, QPolygonF
+
+from qwt._math import qwtSqr
+from qwt.graphic import QwtGraphic
+from qwt.plot import QwtPlot, QwtPlotItem, QwtPlotItem_PrivateData
+from qwt.plot_directpainter import QwtPlotDirectPainter
+from qwt.plot_series import (
QwtPlotSeriesItem,
- QwtSeriesStore,
- QwtSeriesData,
QwtPointArrayData,
+ QwtSeriesData,
+ QwtSeriesStore,
)
-from .symbol import QwtSymbol
-from .plot_directpainter import QwtPlotDirectPainter
-from .qthelpers import qcolor_from_str
+from qwt.qthelpers import qcolor_from_str
+from qwt.symbol import QwtSymbol
+from qwt.text import QwtText
-from .qt.QtGui import QPen, QBrush, QPainter, QPolygonF, QColor
-from .qt.QtCore import QSize, Qt, QRectF, QPointF
+QT_API = os.environ["QT_API"]
-import numpy as np
+if QT_API == "pyside6":
+ import ctypes
+
+ import shiboken6 as shiboken
def qwtUpdateLegendIconSize(curve):
@@ -38,7 +47,7 @@ def qwtUpdateLegendIconSize(curve):
sz = curve.symbol().boundingRect().size()
sz += QSize(2, 2)
if curve.testLegendAttribute(QwtPlotCurve.LegendShowLine):
- w = np.ceil(1.5 * sz.width())
+ w = math.ceil(1.5 * sz.width())
if w % 2:
w += 1
sz.setWidth(max([8, w]))
@@ -55,20 +64,47 @@ def qwtVerifyRange(size, i1, i2):
return i2 - i1 + 1
+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 PyQt5 and PySide6 (requires QtPy).
+
+ License/copyright: MIT License © Pierre Raybaut 2020-2021.
+
+ :param numpy.ndarray xdata: 1D-NumPy array
+ :param numpy.ndarray ydata: 1D-NumPy array
+ :return: Polyline
+ :rtype: QtGui.QPolygonF
+ """
+ if not (xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]):
+ raise ValueError("Arguments must be 1D NumPy arrays with same size")
+ size = xdata.size
+ if QT_API.startswith("pyside"): # PySide (obviously...)
+ polyline = QPolygonF()
+ polyline.resize(size)
+ address = shiboken.getCppPointer(polyline.data())[0]
+ buffer = (ctypes.c_double * 2 * size).from_address(address)
+ else: # PyQt
+ if QT_API == "pyqt6":
+ polyline = QPolygonF([QPointF(0, 0)] * size)
+ else:
+ polyline = QPolygonF(size)
+ buffer = polyline.data()
+ buffer.setsize(16 * size) # 16 bytes per point: 8 bytes per X,Y value (float64)
+ memory = np.frombuffer(buffer, np.float64)
+ memory[: (size - 1) * 2 + 1 : 2] = np.asarray(xdata, dtype=np.float64)
+ memory[1 : (size - 1) * 2 + 2 : 2] = np.asarray(ydata, dtype=np.float64)
+ return polyline
+
+
def series_to_polyline(xMap, yMap, series, from_, to):
"""
Convert series data to QPolygon(F) polyline
"""
- polyline = QPolygonF(to - from_ + 1)
- pointer = polyline.data()
- dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo
- pointer.setsize(2 * polyline.size() * tinfo(dtype).dtype.itemsize)
- memory = np.frombuffer(pointer, dtype)
- memory[: (to - from_) * 2 + 1 : 2] = xMap.transform(series.xData()[from_ : to + 1])
- memory[1 : (to - from_) * 2 + 2 : 2] = yMap.transform(
- series.yData()[from_ : to + 1]
- )
- return polyline
+ xdata = xMap.transform(series.xData()[from_ : to + 1])
+ ydata = yMap.transform(series.yData()[from_ : to + 1])
+ return array2d_to_qpolygonf(xdata, ydata)
class QwtPlotCurve_PrivateData(QwtPlotItem_PrivateData):
@@ -89,79 +125,79 @@ class QwtPlotCurve(QwtPlotSeriesItem, QwtSeriesStore):
A curve is the representation of a series of points in the x-y plane.
It supports different display styles and symbols.
-
+
.. seealso::
-
- :py:class:`qwt.symbol.QwtSymbol()`,
+
+ :py:class:`qwt.symbol.QwtSymbol()`,
:py:class:`qwt.scale_map.QwtScaleMap()`
-
+
Curve styles:
-
+
* `QwtPlotCurve.NoCurve`:
-
+
Don't draw a curve. Note: This doesn't affect the symbols.
-
+
* `QwtPlotCurve.Lines`:
Connect the points with straight lines.
* `QwtPlotCurve.Sticks`:
-
- Draw vertical or horizontal sticks ( depending on the
+
+ Draw vertical or horizontal sticks ( depending on the
orientation() ) from a baseline which is defined by setBaseline().
* `QwtPlotCurve.Steps`:
-
+
Connect the points with a step function. The step function
is drawn from the left to the right or vice versa,
depending on the QwtPlotCurve::Inverted attribute.
* `QwtPlotCurve.Dots`:
-
+
Draw dots at the locations of the data points. Note:
This is different from a dotted line (see setPen()), and faster
- as a curve in QwtPlotCurve::NoStyle style and a symbol
+ as a curve in QwtPlotCurve::NoStyle style and a symbol
painting a point.
* `QwtPlotCurve.UserCurve`:
-
+
Styles >= QwtPlotCurve.UserCurve are reserved for derived
classes of QwtPlotCurve that overload drawCurve() with
additional application specific curve types.
-
+
Curve attributes:
-
+
* `QwtPlotCurve.Inverted`:
-
- For `QwtPlotCurve.Steps` only.
+
+ For `QwtPlotCurve.Steps` only.
Draws a step function from the right to the left.
-
+
Legend attributes:
-
+
* `QwtPlotCurve.LegendNoAttribute`:
-
- `QwtPlotCurve` tries to find a color representing the curve
+
+ `QwtPlotCurve` tries to find a color representing the curve
and paints a rectangle with it.
* `QwtPlotCurve.LegendShowLine`:
-
- If the style() is not `QwtPlotCurve.NoCurve` a line
+
+ If the style() is not `QwtPlotCurve.NoCurve` a line
is painted with the curve pen().
* `QwtPlotCurve.LegendShowSymbol`:
-
+
If the curve has a valid symbol it is painted.
* `QwtPlotCurve.LegendShowBrush`:
-
+
If the curve has a brush a rectangle filled with the
curve brush() is painted.
-
+
.. py:class:: QwtPlotCurve([title=None])
-
+
Constructor
-
+
:param title: Curve title
:type title: qwt.text.QwtText or str or None
"""
@@ -211,7 +247,7 @@ def make(
):
"""
Create and setup a new `QwtPlotCurve` object (convenience function).
-
+
:param xdata: List/array of x values
:param ydata: List/array of y values
:param title: Curve title
@@ -240,7 +276,7 @@ def make(
:param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
.. seealso::
-
+
:py:meth:`setData()`, :py:meth:`setPen()`, :py:meth:`attach()`
"""
item = cls(title)
@@ -283,19 +319,19 @@ def rtti(self):
def setLegendAttribute(self, attribute, on=True):
"""
Specify an attribute how to draw the legend icon
-
+
Legend attributes:
-
+
* `QwtPlotCurve.LegendNoAttribute`
* `QwtPlotCurve.LegendShowLine`
* `QwtPlotCurve.LegendShowSymbol`
* `QwtPlotCurve.LegendShowBrush`
-
+
:param int attribute: Legend attribute
:param bool on: On/Off
-
+
.. seealso::
-
+
:py:meth:`testLegendAttribute()`, :py:meth:`legendIcon()`
"""
if on != self.testLegendAttribute(attribute):
@@ -310,9 +346,9 @@ def testLegendAttribute(self, attribute):
"""
:param int attribute: Legend attribute
:return: True, when attribute is enabled
-
+
.. seealso::
-
+
:py:meth:`setLegendAttribute()`
"""
return self.__data.legendAttributes & attribute
@@ -320,20 +356,20 @@ def testLegendAttribute(self, attribute):
def setStyle(self, style):
"""
Set the curve's drawing style
-
+
Valid curve styles:
-
+
* `QwtPlotCurve.NoCurve`
* `QwtPlotCurve.Lines`
* `QwtPlotCurve.Sticks`
* `QwtPlotCurve.Steps`
* `QwtPlotCurve.Dots`
* `QwtPlotCurve.UserCurve`
-
+
:param int style: Curve style
-
+
.. seealso::
-
+
:py:meth:`style()`
"""
if style != self.__data.style:
@@ -344,9 +380,9 @@ def setStyle(self, style):
def style(self):
"""
:return: Style of the curve
-
+
.. seealso::
-
+
:py:meth:`setStyle()`
"""
return self.__data.style
@@ -356,13 +392,13 @@ def setSymbol(self, symbol):
Assign a symbol
The curve will take the ownership of the symbol, hence the previously
- set symbol will be delete by setting a new one. If symbol is None no
+ set symbol will be delete by setting a new one. If symbol is None no
symbol will be drawn.
-
+
:param qwt.symbol.QwtSymbol symbol: Symbol
-
+
.. seealso::
-
+
:py:meth:`symbol()`
"""
if symbol != self.__data.symbol:
@@ -374,9 +410,9 @@ def setSymbol(self, symbol):
def symbol(self):
"""
:return: Current symbol or None, when no symbol has been assigned
-
+
.. seealso::
-
+
:py:meth:`setSymbol()`
"""
return self.__data.symbol
@@ -384,27 +420,29 @@ def symbol(self):
def setPen(self, *args):
"""
Build and/or assign a pen, depending on the arguments.
-
+
.. py:method:: setPen(color, width, style)
-
+ :noindex:
+
Build and assign a pen
-
+
In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
- non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
been introduced to hide this incompatibility.
-
+
:param QColor color: Pen color
:param float width: Pen width
:param Qt.PenStyle style: Pen style
-
+
.. py:method:: setPen(pen)
-
+ :noindex:
+
Assign a pen
-
+
:param QPen pen: New pen
-
+
.. seealso::
-
+
:py:meth:`pen()`, :py:meth:`brush()`
"""
if len(args) == 3:
@@ -429,9 +467,9 @@ def setPen(self, *args):
def pen(self):
"""
:return: Pen used to draw the lines
-
+
.. seealso::
-
+
:py:meth:`setPen()`, :py:meth:`brush()`
"""
return self.__data.pen
@@ -443,17 +481,17 @@ def setBrush(self, brush):
In case of `brush.style() != QBrush.NoBrush`
and `style() != QwtPlotCurve.Sticks`
the area between the curve and the baseline will be filled.
-
+
In case `not brush.color().isValid()` the area will be filled by
`pen.color()`. The fill algorithm simply connects the first and the
last curve point to the baseline. So the curve data has to be sorted
(ascending or descending).
-
+
:param brush: New brush
:type brush: QBrush or QColor
-
+
.. seealso::
-
+
:py:meth:`brush()`, :py:meth:`setBaseline()`, :py:meth:`baseline()`
"""
if isinstance(brush, QColor):
@@ -468,29 +506,29 @@ def setBrush(self, brush):
def brush(self):
"""
:return: Brush used to fill the area between lines and the baseline
-
+
.. seealso::
-
- :py:meth:`setBrush()`, :py:meth:`setBaseline()`,
+
+ :py:meth:`setBrush()`, :py:meth:`setBaseline()`,
:py:meth:`baseline()`
"""
return self.__data.brush
def directPaint(self, from_, to):
"""
- When observing a measurement while it is running, new points have
- to be added to an existing seriesItem. This method can be used to
+ When observing a measurement while it is running, new points have
+ to be added to an existing seriesItem. This method can be used to
display them avoiding a complete redraw of the canvas.
Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)`
- will result in faster painting, if the paint engine of the canvas
+ will result in faster painting, if the paint engine of the canvas
widget supports this feature.
-
+
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted
-
+
.. seealso::
-
+
:py:meth:`drawSeries()`
"""
directPainter = QwtPlotDirectPainter(self.plot())
@@ -499,16 +537,16 @@ def directPaint(self, from_, to):
def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to):
"""
Draw an interval of the curve
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
+
:py:meth:`drawCurve()`, :py:meth:`drawSymbols()`
"""
numSamples = self.dataSize()
@@ -533,7 +571,7 @@ def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to):
def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to):
"""
Draw the line part (without symbols) of a curve interval.
-
+
:param QPainter painter: Painter
:param int style: curve style, see `QwtPlotCurve.CurveStyle`
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
@@ -541,10 +579,10 @@ def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to):
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
- :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawLines()`,
+
+ :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawLines()`,
:py:meth:`drawSteps()`, :py:meth:`drawSticks()`
"""
if style == self.Lines:
@@ -559,17 +597,17 @@ def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to):
def drawLines(self, painter, xMap, yMap, canvasRect, from_, to):
"""
Draw lines
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
- :py:meth:`draw()`, :py:meth:`drawDots()`,
+
+ :py:meth:`draw()`, :py:meth:`drawDots()`,
:py:meth:`drawSteps()`, :py:meth:`drawSticks()`
"""
if from_ > to:
@@ -586,17 +624,17 @@ def drawLines(self, painter, xMap, yMap, canvasRect, from_, to):
def drawSticks(self, painter, xMap, yMap, canvasRect, from_, to):
"""
Draw sticks
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
- :py:meth:`draw()`, :py:meth:`drawDots()`,
+
+ :py:meth:`draw()`, :py:meth:`drawDots()`,
:py:meth:`drawSteps()`, :py:meth:`drawLines()`
"""
painter.save()
@@ -610,25 +648,25 @@ def drawSticks(self, painter, xMap, yMap, canvasRect, from_, to):
xi = xMap.transform(sample.x())
yi = yMap.transform(sample.y())
if o == Qt.Horizontal:
- painter.drawLine(xi, y0, xi, yi)
+ painter.drawLine(QLineF(xi, y0, xi, yi))
else:
- painter.drawLine(x0, yi, xi, yi)
+ painter.drawLine(QLineF(x0, yi, xi, yi))
painter.restore()
def drawDots(self, painter, xMap, yMap, canvasRect, from_, to):
"""
Draw dots
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
- :py:meth:`draw()`, :py:meth:`drawSticks()`,
+
+ :py:meth:`draw()`, :py:meth:`drawSticks()`,
:py:meth:`drawSteps()`, :py:meth:`drawLines()`
"""
doFill = (
@@ -643,20 +681,27 @@ def drawDots(self, painter, xMap, yMap, canvasRect, from_, to):
def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to):
"""
Draw steps
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
- :py:meth:`draw()`, :py:meth:`drawSticks()`,
+
+ :py:meth:`draw()`, :py:meth:`drawSticks()`,
:py:meth:`drawDots()`, :py:meth:`drawLines()`
"""
- polygon = QPolygonF(2 * (to - from_) + 1)
+ size = 2 * (to - from_) + 1
+ if QT_API == "pyside6":
+ polygon = QPolygonF()
+ polygon.resize(size)
+ elif QT_API == "pyqt6":
+ polygon = QPolygonF([QPointF(0, 0)] * size)
+ else:
+ polygon = QPolygonF(size)
inverted = self.orientation() == Qt.Vertical
if self.__data.attributes & self.Inverted:
inverted = not inverted
@@ -681,16 +726,16 @@ def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to):
def setCurveAttribute(self, attribute, on=True):
"""
Specify an attribute for drawing the curve
-
+
Supported curve attributes:
* `QwtPlotCurve.Inverted`
:param int attribute: Curve attribute
:param bool on: On/Off
-
+
.. seealso::
-
+
:py:meth:`testCurveAttribute()`
"""
if (self.__data.attributes & attribute) == on:
@@ -704,9 +749,9 @@ def setCurveAttribute(self, attribute, on=True):
def testCurveAttribute(self, attribute):
"""
:return: True, if attribute is enabled
-
+
.. seealso::
-
+
:py:meth:`setCurveAttribute()`
"""
return self.__data.attributes & attribute
@@ -721,10 +766,10 @@ def fillCurve(self, painter, xMap, yMap, canvasRect, polygon):
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param QPolygonF polygon: Polygon - will be modified !
-
+
.. seealso::
-
- :py:meth:`setBrush()`, :py:meth:`setBaseline()`,
+
+ :py:meth:`setBrush()`, :py:meth:`setBaseline()`,
:py:meth:`setStyle()`
"""
if self.__data.brush.style() == Qt.NoBrush:
@@ -743,7 +788,7 @@ def fillCurve(self, painter, xMap, yMap, canvasRect, polygon):
def closePolyline(self, painter, xMap, yMap, polygon):
"""
- Complete a polygon to be a closed polygon including the
+ Complete a polygon to be a closed polygon including the
area between the original polygon and the baseline.
:param QPainter painter: Painter
@@ -758,19 +803,19 @@ def closePolyline(self, painter, xMap, yMap, polygon):
if yMap.transformation():
baseline = yMap.transformation().bounded(baseline)
refY = yMap.transform(baseline)
- polygon += QPointF(polygon.last().x(), refY)
- polygon += QPointF(polygon.first().x(), refY)
+ polygon.append(QPointF(polygon.last().x(), refY))
+ polygon.append(QPointF(polygon.first().x(), refY))
else:
if xMap.transformation():
baseline = xMap.transformation().bounded(baseline)
refX = xMap.transform(baseline)
- polygon += QPointF(refX, polygon.last().y())
- polygon += QPointF(refX, polygon.first().y())
+ polygon.append(QPointF(refX, polygon.last().y()))
+ polygon.append(QPointF(refX, polygon.first().y()))
def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to):
"""
Draw symbols
-
+
:param QPainter painter: Painter
:param qwt.symbol.QwtSymbol symbol: Curve symbol
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
@@ -778,10 +823,10 @@ def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to):
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
- :py:meth:`setSymbol()`, :py:meth:`drawSeries()`,
+
+ :py:meth:`setSymbol()`, :py:meth:`drawSeries()`,
:py:meth:`drawCurve()`
"""
chunkSize = 500
@@ -797,19 +842,19 @@ def setBaseline(self, value):
The baseline is needed for filling the curve with a brush or
the Sticks drawing style.
-
+
The interpretation of the baseline depends on the `orientation()`.
With `Qt.Horizontal`, the baseline is interpreted as a horizontal line
at y = baseline(), with `Qt.Vertical`, it is interpreted as a vertical
line at x = baseline().
-
+
The default value is 0.0.
-
+
:param float value: Value of the baseline
-
+
.. seealso::
-
- :py:meth:`baseline()`, :py:meth:`setBrush()`,
+
+ :py:meth:`baseline()`, :py:meth:`setBrush()`,
:py:meth:`setStyle()`
"""
if self.__data.baseline != value:
@@ -819,9 +864,9 @@ def setBaseline(self, value):
def baseline(self):
"""
:return: Value of the baseline
-
+
.. seealso::
-
+
:py:meth:`setBaseline()`
"""
return self.__data.baseline
@@ -829,16 +874,16 @@ def baseline(self):
def closestPoint(self, pos):
"""
Find the closest curve point for a specific position
-
+
:param QPoint pos: Position, where to look for the closest curve point
:return: tuple `(index, dist)`
-
- `dist` is the distance between the position and the closest curve
- point. `index` is the index of the closest curve point, or -1 if
+
+ `dist` is the distance between the position and the closest curve
+ point. `index` is the index of the closest curve point, or -1 if
none can be found ( f.e when the curve has no points ).
-
+
.. note::
-
+
`closestPoint()` implements a dumb algorithm, that iterates
over all points
"""
@@ -858,7 +903,7 @@ def closestPoint(self, pos):
if f < dmin:
index = i
dmin = f
- dist = np.sqrt(dmin)
+ dist = math.sqrt(dmin)
return index, dist
def legendIcon(self, index, size):
@@ -866,9 +911,9 @@ def legendIcon(self, index, size):
:param int index: Index of the legend entry (ignored as there is only one)
:param QSizeF size: Icon size
:return: Icon representing the curve on the legend
-
+
.. seealso::
-
+
:py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`,
:py:meth:`qwt.plot.QwtPlotItem.legendData()`
"""
@@ -898,11 +943,9 @@ def legendIcon(self, index, size):
painter.fillRect(r, brush)
if self.__data.legendAttributes & QwtPlotCurve.LegendShowLine:
if self.pen() != Qt.NoPen:
- pn = self.pen()
- # pn.setCapStyle(Qt.FlatCap)
- painter.setPen(pn)
- y = 0.5 * size.height()
- painter.drawLine(0.0, y, size.width(), y)
+ painter.setPen(self.pen())
+ y = size.height() // 2
+ painter.drawLine(QLineF(0, y, size.width(), y))
if self.__data.legendAttributes & QwtPlotCurve.LegendShowSymbol:
if self.__data.symbol:
r = QRectF(0, 0, size.width(), size.height())
@@ -912,28 +955,28 @@ def legendIcon(self, index, size):
def setData(self, *args, **kwargs):
"""
Initialize data with a series data object or an array of points.
-
+
.. py:method:: setData(data):
-
+
:param data: Series data (e.g. `QwtPointArrayData` instance)
:type data: .plot_series.QwtSeriesData
.. py:method:: setData(xData, yData, [size=None], [finite=True]):
Initialize data with `x` and `y` arrays.
-
+
This signature was removed in Qwt6 and is temporarily maintained here to ensure compatibility with Qwt5.
-
+
Same as `setSamples(x, y, [size=None], [finite=True])`
-
+
:param x: List/array of x values
:param y: List/array of y values
:param size: size of xData and yData
:type size: int or None
:param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
-
+
.. seealso::
-
+
:py:meth:`setSamples()`
"""
if len(args) == 1 and not kwargs:
@@ -949,31 +992,31 @@ def setData(self, *args, **kwargs):
def setSamples(self, *args, **kwargs):
"""
Initialize data with an array of points.
-
+
.. py:method:: setSamples(data):
-
+
:param data: Series data (e.g. `QwtPointArrayData` instance)
:type data: .plot_series.QwtSeriesData
-
-
+
+
.. py:method:: setSamples(samples):
-
+
Same as `setData(QwtPointArrayData(samples))`
-
+
:param samples: List/array of points
-
+
.. py:method:: setSamples(xData, yData, [size=None], [finite=True]):
Same as `setData(QwtPointArrayData(xData, yData, [size=None]))`
-
+
:param xData: List/array of x values
:param yData: List/array of y values
:param size: size of xData and yData
:type size: int or None
:param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements
-
+
.. seealso::
-
+
:py:class:`.plot_series.QwtPointArrayData`
"""
if len(args) == 1 and not kwargs:
@@ -1009,4 +1052,3 @@ def setSamples(self, *args, **kwargs):
"%s().setSamples() takes 1, 2 or 3 argument(s) "
"(%s given)" % (self.__class__.__name__, len(args))
)
-
diff --git a/qwt/plot_directpainter.py b/qwt/plot_directpainter.py
index d60b026..06697d3 100644
--- a/qwt/plot_directpainter.py
+++ b/qwt/plot_directpainter.py
@@ -13,11 +13,11 @@
:members:
"""
-from .qt.QtGui import QPainter, QRegion
-from .qt.QtCore import QObject, QT_VERSION, Qt, QEvent
+from qtpy.QtCore import QEvent, QObject, Qt
+from qtpy.QtGui import QPainter, QRegion
-from .plot import QwtPlotItem
-from .plot_canvas import QwtPlotCanvas
+from qwt.plot import QwtPlotItem
+from qwt.plot_canvas import QwtPlotCanvas
def qwtRenderItem(painter, canvasRect, seriesItem, from_, to):
@@ -33,12 +33,16 @@ def qwtRenderItem(painter, canvasRect, seriesItem, from_, to):
def qwtHasBackingStore(canvas):
return (
- canvas.testPaintAttribute(QwtPlotCanvas.BackingStore) and canvas.backingStore()
+ canvas.testPaintAttribute(QwtPlotCanvas.BackingStore)
+ and canvas.backingStore() is not None
+ and not canvas.backingStore().isNull()
)
-class QwtPlotDirectPainter_PrivateData(object):
+class QwtPlotDirectPainter_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.attributes = 0
self.hasClipping = False
self.seriesItem = None # QwtPlotSeriesItem
@@ -62,34 +66,34 @@ class QwtPlotDirectPainter(QObject):
On certain environments it might be important to calculate a proper
clip region before painting. F.e. for Qt Embedded only the clipped part
- of the backing store will be copied to a (maybe unaccelerated)
+ of the backing store will be copied to a (maybe unaccelerated)
frame buffer.
.. warning::
-
+
Incremental painting will only help when no replot is triggered
by another operation (like changing scales) and nothing needs
to be erased.
-
+
Paint attributes:
-
+
* `QwtPlotDirectPainter.AtomicPainter`:
-
+
Initializing a `QPainter` is an expensive operation.
When `AtomicPainter` is set each call of `drawSeries()` opens/closes
a temporary `QPainter`. Otherwise `QwtPlotDirectPainter` tries to
use the same `QPainter` as long as possible.
* `QwtPlotDirectPainter.FullRepaint`:
-
+
When `FullRepaint` is set the plot canvas is explicitly repainted
after the samples have been rendered.
* `QwtPlotDirectPainter.CopyBackingStore`:
-
+
When `QwtPlotCanvas.BackingStore` is enabled the painter
- has to paint to the backing store and the widget. In certain
- situations/environments it might be faster to paint to
+ has to paint to the backing store and the widget. In certain
+ situations/environments it might be faster to paint to
the backing store only and then copy the backing store to the canvas.
This flag can also be useful for settings, where Qt fills the
the clip region with the widget background.
@@ -107,7 +111,7 @@ def __init__(self, parent=None):
def setAttribute(self, attribute, on=True):
"""
Change an attribute
-
+
:param int attribute: Attribute to change
:param bool on: On/Off
@@ -136,12 +140,12 @@ def testAttribute(self, attribute):
def setClipping(self, enable):
"""
En/Disables clipping
-
+
:param bool enable: Enables clipping is true, disable it otherwise
-
+
.. seealso::
-
- :py:meth:`hasClipping()`, :py:meth:`clipRegion()`,
+
+ :py:meth:`hasClipping()`, :py:meth:`clipRegion()`,
:py:meth:`setClipRegion()`
"""
self.__data.hasClipping = enable
@@ -149,10 +153,10 @@ def setClipping(self, enable):
def hasClipping(self):
"""
:return: Return true, when clipping is enabled
-
+
.. seealso::
-
- :py:meth:`setClipping()`, :py:meth:`clipRegion()`,
+
+ :py:meth:`setClipping()`, :py:meth:`clipRegion()`,
:py:meth:`setClipRegion()`
"""
return self.__data.hasClipping
@@ -161,16 +165,16 @@ def setClipRegion(self, region):
"""
Assign a clip region and enable clipping
- Depending on the environment setting a proper clip region might
- improve the performance heavily. F.e. on Qt embedded only the clipped
- part of the backing store will be copied to a (maybe unaccelerated)
+ Depending on the environment setting a proper clip region might
+ improve the performance heavily. F.e. on Qt embedded only the clipped
+ part of the backing store will be copied to a (maybe unaccelerated)
frame buffer device.
-
+
:param QRegion region: Clip region
-
+
.. seealso::
-
- :py:meth:`hasClipping()`, :py:meth:`setClipping()`,
+
+ :py:meth:`hasClipping()`, :py:meth:`setClipping()`,
:py:meth:`clipRegion()`
"""
self.__data.clipRegion = region
@@ -179,10 +183,10 @@ def setClipRegion(self, region):
def clipRegion(self):
"""
:return: Return Currently set clip region.
-
+
.. seealso::
-
- :py:meth:`hasClipping()`, :py:meth:`setClipping()`,
+
+ :py:meth:`hasClipping()`, :py:meth:`setClipping()`,
:py:meth:`setClipRegion()`
"""
return self.__data.clipRegion
@@ -190,15 +194,15 @@ def clipRegion(self):
def drawSeries(self, seriesItem, from_, to):
"""
Draw a set of points of a seriesItem.
-
- When observing a measurement while it is running, new points have
- to be added to an existing seriesItem. drawSeries() can be used to
+
+ When observing a measurement while it is running, new points have
+ to be added to an existing seriesItem. drawSeries() can be used to
display them avoiding a complete redraw of the canvas.
Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)`
will result in faster painting, if the paint engine of the canvas widget
supports this feature.
-
+
:param qwt.plot_series.QwtPlotSeriesItem seriesItem: Item to be painted
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the series will be painted to its last point.
@@ -207,25 +211,16 @@ def drawSeries(self, seriesItem, from_, to):
return
canvas = seriesItem.plot().canvas()
canvasRect = canvas.contentsRect()
- plotCanvas = canvas # XXX: cast to QwtPlotCanvas
- if plotCanvas and qwtHasBackingStore(plotCanvas):
- painter = QPainter(
- plotCanvas.backingStore()
- ) # XXX: cast plotCanvas.backingStore() to QPixmap
+ if canvas and qwtHasBackingStore(canvas):
+ painter = QPainter(canvas.backingStore())
if self.__data.hasClipping:
painter.setClipRegion(self.__data.clipRegion)
qwtRenderItem(painter, canvasRect, seriesItem, from_, to)
painter.end()
if self.testAttribute(self.FullRepaint):
- plotCanvas.repaint()
+ canvas.repaint()
return
- immediatePaint = True
- if not canvas.testAttribute(Qt.WA_WState_InPaintEvent):
- if QT_VERSION >= 0x050000 or not canvas.testAttribute(
- Qt.WA_PaintOutsidePaintEvent
- ):
- immediatePaint = False
- if immediatePaint:
+ if canvas.testAttribute(Qt.WA_WState_InPaintEvent):
if not self.__data.painter.isActive():
self.reset()
self.__data.painter.begin(canvas)
diff --git a/qwt/plot_grid.py b/qwt/plot_grid.py
index 6aee765..a75a07c 100644
--- a/qwt/plot_grid.py
+++ b/qwt/plot_grid.py
@@ -13,18 +13,19 @@
:members:
"""
-from .scale_div import QwtScaleDiv
-from .plot import QwtPlot, QwtPlotItem
-from .text import QwtText
-from ._math import qwtFuzzyGreaterOrEqual, qwtFuzzyLessOrEqual
-from .qthelpers import qcolor_from_str
+from qtpy.QtCore import QLineF, QObject, Qt
+from qtpy.QtGui import QPen
-from .qt.QtGui import QPen
-from .qt.QtCore import Qt
+from qwt._math import qwtFuzzyGreaterOrEqual, qwtFuzzyLessOrEqual
+from qwt.plot import QwtPlotItem
+from qwt.qthelpers import qcolor_from_str
+from qwt.scale_div import QwtScaleDiv
-class QwtPlotGrid_PrivateData(object):
+class QwtPlotGrid_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.xEnabled = True
self.yEnabled = True
self.xMinEnabled = False
@@ -70,7 +71,7 @@ def make(
):
"""
Create and setup a new `QwtPlotGrid` object (convenience function).
-
+
:param plot: Plot to attach the curve to
:type plot: qwt.plot.QwtPlot or None
:param z: Z-value
@@ -93,7 +94,7 @@ def make(
:type minstyle: Qt.PenStyle or None
.. seealso::
-
+
:py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`
"""
item = cls()
@@ -139,11 +140,11 @@ def rtti(self):
def enableX(self, on):
"""
Enable or disable vertical grid lines
-
+
:param bool on: Enable (true) or disable
-
+
.. seealso::
-
+
:py:meth:`enableXMin()`
"""
if self.__data.xEnabled != on:
@@ -154,11 +155,11 @@ def enableX(self, on):
def enableY(self, on):
"""
Enable or disable horizontal grid lines
-
+
:param bool on: Enable (true) or disable
-
+
.. seealso::
-
+
:py:meth:`enableYMin()`
"""
if self.__data.yEnabled != on:
@@ -169,11 +170,11 @@ def enableY(self, on):
def enableXMin(self, on):
"""
Enable or disable minor vertical grid lines.
-
+
:param bool on: Enable (true) or disable
-
+
.. seealso::
-
+
:py:meth:`enableX()`
"""
if self.__data.xMinEnabled != on:
@@ -184,11 +185,11 @@ def enableXMin(self, on):
def enableYMin(self, on):
"""
Enable or disable minor horizontal grid lines.
-
+
:param bool on: Enable (true) or disable
-
+
.. seealso::
-
+
:py:meth:`enableY()`
"""
if self.__data.yMinEnabled != on:
@@ -199,7 +200,7 @@ def enableYMin(self, on):
def setXDiv(self, scaleDiv):
"""
Assign an x axis scale division
-
+
:param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division
"""
if self.__data.xScaleDiv != scaleDiv:
@@ -209,7 +210,7 @@ def setXDiv(self, scaleDiv):
def setYDiv(self, scaleDiv):
"""
Assign an y axis scale division
-
+
:param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division
"""
if self.__data.yScaleDiv != scaleDiv:
@@ -219,27 +220,29 @@ def setYDiv(self, scaleDiv):
def setPen(self, *args):
"""
Build and/or assign a pen for both major and minor grid lines
-
+
.. py:method:: setPen(color, width, style)
-
+ :noindex:
+
Build and assign a pen for both major and minor grid lines
-
+
In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
- non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
been introduced to hide this incompatibility.
-
+
:param QColor color: Pen color
:param float width: Pen width
:param Qt.PenStyle style: Pen style
-
+
.. py:method:: setPen(pen)
-
+ :noindex:
+
Assign a pen for both major and minor grid lines
-
+
:param QPen pen: New pen
-
+
.. seealso::
-
+
:py:meth:`pen()`, :py:meth:`brush()`
"""
if len(args) == 3:
@@ -261,28 +264,30 @@ def setPen(self, *args):
def setMajorPen(self, *args):
"""
Build and/or assign a pen for both major grid lines
-
+
.. py:method:: setMajorPen(color, width, style)
-
+ :noindex:
+
Build and assign a pen for both major grid lines
-
+
In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
- non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
been introduced to hide this incompatibility.
-
+
:param QColor color: Pen color
:param float width: Pen width
:param Qt.PenStyle style: Pen style
-
+
.. py:method:: setMajorPen(pen)
-
+ :noindex:
+
Assign a pen for the major grid lines
-
+
:param QPen pen: New pen
-
+
.. seealso::
-
- :py:meth:`majorPen()`, :py:meth:`setMinorPen()`,
+
+ :py:meth:`majorPen()`, :py:meth:`setMinorPen()`,
:py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()`
"""
if len(args) == 3:
@@ -303,28 +308,30 @@ def setMajorPen(self, *args):
def setMinorPen(self, *args):
"""
Build and/or assign a pen for both minor grid lines
-
+
.. py:method:: setMinorPen(color, width, style)
-
+ :noindex:
+
Build and assign a pen for both minor grid lines
-
+
In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
- non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
been introduced to hide this incompatibility.
-
+
:param QColor color: Pen color
:param float width: Pen width
:param Qt.PenStyle style: Pen style
-
+
.. py:method:: setMinorPen(pen)
-
+ :noindex:
+
Assign a pen for the minor grid lines
-
+
:param QPen pen: New pen
-
+
.. seealso::
-
- :py:meth:`minorPen()`, :py:meth:`setMajorPen()`,
+
+ :py:meth:`minorPen()`, :py:meth:`setMajorPen()`,
:py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()`
"""
if len(args) == 3:
@@ -418,18 +425,18 @@ def drawLines(self, painter, canvasRect, orientation, scaleMap, values):
value = scaleMap.transform(val)
if orientation == Qt.Horizontal:
if qwtFuzzyGreaterOrEqual(value, y1) and qwtFuzzyLessOrEqual(value, y2):
- painter.drawLine(x1, value, x2, value)
+ painter.drawLine(QLineF(x1, value, x2, value))
else:
if qwtFuzzyGreaterOrEqual(value, x1) and qwtFuzzyLessOrEqual(value, x2):
- painter.drawLine(value, y1, value, y2)
+ painter.drawLine(QLineF(value, y1, value, y2))
def majorPen(self):
"""
:return: the pen for the major grid lines
-
+
.. seealso::
-
- :py:meth:`setMajorPen()`, :py:meth:`setMinorPen()`,
+
+ :py:meth:`setMajorPen()`, :py:meth:`setMinorPen()`,
:py:meth:`setPen()`
"""
return self.__data.majorPen
@@ -437,10 +444,10 @@ def majorPen(self):
def minorPen(self):
"""
:return: the pen for the minor grid lines
-
+
.. seealso::
-
- :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`,
+
+ :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`,
:py:meth:`setPen()`
"""
return self.__data.minorPen
@@ -448,9 +455,9 @@ def minorPen(self):
def xEnabled(self):
"""
:return: True if vertical grid lines are enabled
-
+
.. seealso::
-
+
:py:meth:`enableX()`
"""
return self.__data.xEnabled
@@ -458,9 +465,9 @@ def xEnabled(self):
def yEnabled(self):
"""
:return: True if horizontal grid lines are enabled
-
+
.. seealso::
-
+
:py:meth:`enableY()`
"""
return self.__data.yEnabled
@@ -468,9 +475,9 @@ def yEnabled(self):
def xMinEnabled(self):
"""
:return: True if minor vertical grid lines are enabled
-
+
.. seealso::
-
+
:py:meth:`enableXMin()`
"""
return self.__data.xMinEnabled
@@ -478,9 +485,9 @@ def xMinEnabled(self):
def yMinEnabled(self):
"""
:return: True if minor horizontal grid lines are enabled
-
+
.. seealso::
-
+
:py:meth:`enableYMin()`
"""
return self.__data.yMinEnabled
@@ -500,14 +507,13 @@ def yScaleDiv(self):
def updateScaleDiv(self, xScaleDiv, yScaleDiv):
"""
Update the grid to changes of the axes scale division
-
+
:param qwt.scale_map.QwtScaleMap xMap: Scale division of the x-axis
:param qwt.scale_map.QwtScaleMap yMap: Scale division of the y-axis
-
+
.. seealso::
-
+
:py:meth:`updateAxes()`
"""
self.setXDiv(xScaleDiv)
self.setYDiv(yScaleDiv)
-
diff --git a/qwt/plot_layout.py b/qwt/plot_layout.py
index e8d30b1..0919914 100644
--- a/qwt/plot_layout.py
+++ b/qwt/plot_layout.py
@@ -13,15 +13,15 @@
:members:
"""
-from .text import QwtText
-from .scale_widget import QwtScaleWidget
-from .plot import QwtPlot
-from .scale_draw import QwtAbstractScaleDraw
+import math
-from .qt.QtGui import QFont, QRegion
-from .qt.QtCore import QSize, Qt, QRectF
+from qtpy.QtCore import QObject, QRectF, QSize, Qt
+from qtpy.QtGui import QFont, QRegion
-import numpy as np
+from qwt.plot import QwtPlot
+from qwt.scale_draw import QwtAbstractScaleDraw
+from qwt.scale_widget import QwtScaleWidget
+from qwt.text import QwtText
QWIDGETSIZE_MAX = (1 << 24) - 1
@@ -80,7 +80,7 @@ def init(self, plot, rect):
self.legend.hScrollExtent = legend.scrollExtent(Qt.Horizontal)
self.legend.vScrollExtent = legend.scrollExtent(Qt.Vertical)
hint = legend.sizeHint()
- w = min([hint.width(), np.floor(rect.width())])
+ w = min([hint.width(), math.floor(rect.width())])
h = legend.heightForWidth(w)
if h <= 0:
h = hint.height()
@@ -132,11 +132,21 @@ def init(self, plot, rect):
self.scale[axis].baseLineOffset = 0
self.scale[axis].tickOffset = 0.0
self.scale[axis].dimWithoutTitle = 0
- self.canvas.contentsMargins = plot.canvas().getContentsMargins()
-
-
-class QwtPlotLayout_PrivateData(object):
+ layout = plot.canvas().layout()
+ if layout is not None:
+ mgn = layout.contentsMargins()
+ self.canvas.contentsMargins = [
+ mgn.left(),
+ mgn.top(),
+ mgn.right(),
+ mgn.bottom(),
+ ]
+
+
+class QwtPlotLayout_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.spacing = 5
self.titleRect = QRectF()
self.footerRect = QRectF()
@@ -159,11 +169,11 @@ class QwtPlotLayout(object):
a QPrinter, QPixmap/QImage or QSvgRenderer.
.. seealso::
-
+
:py:meth:`qwt.plot.QwtPlot.setPlotLayout()`
-
+
Valid options:
-
+
* `QwtPlotLayout.AlignScales`: Unused
* `QwtPlotLayout.IgnoreScrollbars`: Ignore the dimension of the scrollbars. There are no scrollbars, when the plot is not rendered to widgets.
* `QwtPlotLayout.IgnoreFrames`: Ignore all frames.
@@ -192,16 +202,16 @@ def setCanvasMargin(self, margin, axis=-1):
Change a margin of the canvas. The margin is the space
above/below the scale ticks. A negative margin will
be set to -1, excluding the borders of the scales.
-
+
:param int margin: New margin
:param int axisId: Axis index
-
+
.. seealso::
-
+
:py:meth:`canvasMargin()`
-
+
.. warning::
-
+
The margin will have no effect when `alignCanvasToScale()` is True
"""
if margin < 1:
@@ -216,9 +226,9 @@ def canvasMargin(self, axisId):
"""
:param int axisId: Axis index
:return: Margin around the scale tick borders
-
+
.. seealso::
-
+
:py:meth:`setCanvasMargin()`
"""
if axisId not in QwtPlot.AXES:
@@ -228,31 +238,31 @@ def canvasMargin(self, axisId):
def setAlignCanvasToScales(self, *args):
"""
Change the align-canvas-to-axis-scales setting.
-
+
.. py:method:: setAlignCanvasToScales(on):
-
+
Set the align-canvas-to-axis-scales flag for all axes
-
+
:param bool on: True/False
-
+
.. py:method:: setAlignCanvasToScales(axisId, on):
Change the align-canvas-to-axis-scales setting.
The canvas may:
-
+
- extend beyond the axis scale ends to maximize its size,
- align with the axis scale ends to control its size.
- The axisId parameter is somehow confusing as it identifies a
- border of the plot and not the axes, that are aligned. F.e when
- `QwtPlot.yLeft` is set, the left end of the the x-axes
+ The axisId parameter is somehow confusing as it identifies a
+ border of the plot and not the axes, that are aligned. F.e when
+ `QwtPlot.yLeft` is set, the left end of the the x-axes
(`QwtPlot.xTop`, `QwtPlot.xBottom`) is aligned.
-
+
:param int axisId: Axis index
:param bool on: True/False
-
+
.. seealso::
-
+
:py:meth:`setAlignCanvasToScale()`,
:py:meth:`alignCanvasToScale()`
"""
@@ -272,17 +282,17 @@ def setAlignCanvasToScales(self, *args):
def alignCanvasToScale(self, axisId):
"""
- Return the align-canvas-to-axis-scales setting.
+ Return the align-canvas-to-axis-scales setting.
The canvas may:
-
+
- extend beyond the axis scale ends to maximize its size
- align with the axis scale ends to control its size.
-
+
:param int axisId: Axis index
:return: align-canvas-to-axis-scales setting
-
+
.. seealso::
-
+
:py:meth:`setAlignCanvasToScale()`, :py:meth:`setCanvasMargin()`
"""
if axisId not in QwtPlot.AXES:
@@ -293,11 +303,11 @@ def setSpacing(self, spacing):
"""
Change the spacing of the plot. The spacing is the distance
between the plot components.
-
+
:param int spacing: New spacing
-
+
.. seealso::
-
+
:py:meth:`setCanvasMargin()`, :py:meth:`spacing()`
"""
self.__data.spacing = max([0, spacing])
@@ -305,9 +315,9 @@ def setSpacing(self, spacing):
def spacing(self):
"""
:return: Spacing
-
+
.. seealso::
-
+
:py:meth:`margin()`, :py:meth:`setSpacing()`
"""
return self.__data.spacing
@@ -315,28 +325,28 @@ def spacing(self):
def setLegendPosition(self, *args):
"""
Specify the position of the legend
-
+
.. py:method:: setLegendPosition(pos, [ratio=0.]):
-
+
Specify the position of the legend
-
+
:param QwtPlot.LegendPosition pos: Legend position
:param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes
-
- The legend will be shrunk if it would need more space than the
- given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of
- <= 0.0 it will be reset to the default ratio. The default
+
+ The legend will be shrunk if it would need more space than the
+ given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of
+ <= 0.0 it will be reset to the default ratio. The default
vertical/horizontal ratio is 0.33/0.5.
-
+
Valid position values:
-
+
* `QwtPlot.LeftLegend`,
* `QwtPlot.RightLegend`,
* `QwtPlot.TopLegend`,
* `QwtPlot.BottomLegend`
-
+
.. seealso::
-
+
:py:meth:`setLegendPosition()`
"""
if len(args) == 2:
@@ -367,7 +377,7 @@ def legendPosition(self):
:return: Position of the legend
.. seealso::
-
+
:py:meth:`legendPosition()`
"""
return self.__data.legendPos
@@ -375,16 +385,16 @@ def legendPosition(self):
def setLegendRatio(self, ratio):
"""
Specify the relative size of the legend in the plot
-
+
:param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes
-
- The legend will be shrunk if it would need more space than the
- given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of
- <= 0.0 it will be reset to the default ratio. The default
+
+ The legend will be shrunk if it would need more space than the
+ given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of
+ <= 0.0 it will be reset to the default ratio. The default
vertical/horizontal ratio is 0.33/0.5.
.. seealso::
-
+
:py:meth:`legendRatio()`
"""
self.setLegendPosition(self.legendPosition(), ratio)
@@ -394,7 +404,7 @@ def legendRatio(self):
:return: The relative size of the legend in the plot.
.. seealso::
-
+
:py:meth:`setLegendRatio()`
"""
return self.__data.legendRatio
@@ -405,11 +415,11 @@ def setTitleRect(self, rect):
This method is intended to be used from derived layouts
overloading `activate()`
-
+
:param QRectF rect: Rectangle
.. seealso::
-
+
:py:meth:`titleRect()`, :py:meth:`activate()`
"""
self.__data.titleRect = rect
@@ -417,9 +427,9 @@ def setTitleRect(self, rect):
def titleRect(self):
"""
:return: Geometry for the title
-
+
.. seealso::
-
+
:py:meth:`invalidate()`, :py:meth:`activate()`
"""
return self.__data.titleRect
@@ -430,11 +440,11 @@ def setFooterRect(self, rect):
This method is intended to be used from derived layouts
overloading `activate()`
-
+
:param QRectF rect: Rectangle
.. seealso::
-
+
:py:meth:`footerRect()`, :py:meth:`activate()`
"""
self.__data.footerRect = rect
@@ -442,9 +452,9 @@ def setFooterRect(self, rect):
def footerRect(self):
"""
:return: Geometry for the footer
-
+
.. seealso::
-
+
:py:meth:`invalidate()`, :py:meth:`activate()`
"""
return self.__data.footerRect
@@ -455,11 +465,11 @@ def setLegendRect(self, rect):
This method is intended to be used from derived layouts
overloading `activate()`
-
+
:param QRectF rect: Rectangle for the legend
.. seealso::
-
+
:py:meth:`footerRect()`, :py:meth:`activate()`
"""
self.__data.legendRect = rect
@@ -467,9 +477,9 @@ def setLegendRect(self, rect):
def legendRect(self):
"""
:return: Geometry for the legend
-
+
.. seealso::
-
+
:py:meth:`invalidate()`, :py:meth:`activate()`
"""
return self.__data.legendRect
@@ -485,7 +495,7 @@ def setScaleRect(self, axis, rect):
:param QRectF rect: Rectangle for the scale
.. seealso::
-
+
:py:meth:`scaleRect()`, :py:meth:`activate()`
"""
if axis in QwtPlot.AXES:
@@ -495,9 +505,9 @@ def scaleRect(self, axis):
"""
:param int axisId: Axis index
:return: Geometry for the scale
-
+
.. seealso::
-
+
:py:meth:`invalidate()`, :py:meth:`activate()`
"""
if axis not in QwtPlot.AXES:
@@ -514,7 +524,7 @@ def setCanvasRect(self, rect):
:param QRectF rect: Rectangle
.. seealso::
-
+
:py:meth:`canvasRect()`, :py:meth:`activate()`
"""
self.__data.canvasRect = rect
@@ -522,9 +532,9 @@ def setCanvasRect(self, rect):
def canvasRect(self):
"""
:return: Geometry for the canvas
-
+
.. seealso::
-
+
:py:meth:`invalidate()`, :py:meth:`activate()`
"""
return self.__data.canvasRect
@@ -532,9 +542,9 @@ def canvasRect(self):
def invalidate(self):
"""
Invalidate the geometry of all components.
-
+
.. seealso::
-
+
:py:meth:`activate()`
"""
self.__data.titleRect = QRectF()
@@ -548,9 +558,9 @@ def minimumSizeHint(self, plot):
"""
:param qwt.plot.QwtPlot plot: Plot widget
:return: Minimum size hint
-
+
.. seealso::
-
+
:py:meth:`qwt.plot.QwtPlot.minimumSizeHint()`
"""
@@ -564,7 +574,17 @@ def __init__(self):
scaleData = [_ScaleData() for _i in QwtPlot.AXES]
canvasBorder = [0 for _i in QwtPlot.AXES]
- fw, _, _, _ = plot.canvas().getContentsMargins()
+ layout = plot.canvas().layout()
+ if layout is None:
+ left, top, right, bottom = 0, 0, 0, 0
+ else:
+ mgn = layout.contentsMargins()
+ left, top, right, bottom = (
+ mgn.left(),
+ mgn.top(),
+ mgn.right(),
+ mgn.bottom(),
+ )
for axis in QwtPlot.AXES:
if plot.axisEnabled(axis):
scl = plot.axisWidget(axis)
@@ -575,8 +595,8 @@ def __init__(self):
sd.minLeft, sd.minLeft = scl.getBorderDistHint()
sd.tickOffset = scl.margin()
if scl.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks):
- sd.tickOffset += np.ceil(scl.scaleDraw().maxTickLength())
- canvasBorder[axis] = fw + self.__data.canvasMargin[axis] + 1
+ sd.tickOffset += math.ceil(scl.scaleDraw().maxTickLength())
+ canvasBorder[axis] = left + self.__data.canvasMargin[axis] + 1
for axis in QwtPlot.AXES:
sd = scaleData[axis]
if sd.w and axis in (QwtPlot.xBottom, QwtPlot.xTop):
@@ -614,7 +634,6 @@ def __init__(self):
shiftTop = scaleData[QwtPlot.xTop].tickOffset
sd.h -= shiftTop
canvas = plot.canvas()
- left, top, right, bottom = canvas.getContentsMargins()
minCanvasSize = canvas.minimumSize()
w = scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w
cw = (
@@ -669,12 +688,12 @@ def __init__(self):
if self.__data.legendRatio < 1.0:
legendH = min([legendH, int(h / (1.0 - self.__data.legendRatio))])
h += legendH + self.__data.spacing
- return QSize(w, h)
+ return QSize(int(w), int(h))
def layoutLegend(self, options, rect):
"""
Find the geometry for the legend
-
+
:param options: Options how to layout the legend
:param QRectF rect: Rectangle where to place the legend
:return: Geometry for the legend
@@ -704,7 +723,7 @@ def layoutLegend(self, options, rect):
def alignLegend(self, canvasRect, legendRect):
"""
Align the legend to the canvas
-
+
:param QRectF canvasRect: Geometry of the canvas
:param QRectF legendRect: Maximum geometry for the legend
:return: Geometry for the aligned legend
@@ -724,13 +743,13 @@ def expandLineBreaks(self, options, rect):
"""
Expand all line breaks in text labels, and calculate the height
of their widgets in orientation of the text.
-
+
:param options: Options how to layout the legend
:param QRectF rect: Bounding rectangle for title, footer, axes and canvas.
:return: tuple `(dimTitle, dimFooter, dimAxes)`
-
+
Returns:
-
+
* `dimTitle`: Expanded height of the title widget
* `dimFooter`: Expanded height of the footer widget
* `dimAxes`: Expanded heights of the axis in axis orientation.
@@ -765,7 +784,7 @@ def expandLineBreaks(self, options, rect):
!= self.__data.layoutData.scale[QwtPlot.yRight].isEnabled
):
w -= dimAxes[QwtPlot.yLeft] + dimAxes[QwtPlot.yRight]
- d = np.ceil(self.__data.layoutData.title.text.heightForWidth(w))
+ d = math.ceil(self.__data.layoutData.title.text.heightForWidth(w))
if not (options & self.IgnoreFrames):
d += 2 * self.__data.layoutData.title.frameWidth
if d > dimTitle:
@@ -781,7 +800,7 @@ def expandLineBreaks(self, options, rect):
!= self.__data.layoutData.scale[QwtPlot.yRight].isEnabled
):
w -= dimAxes[QwtPlot.yLeft] + dimAxes[QwtPlot.yRight]
- d = np.ceil(self.__data.layoutData.footer.text.heightForWidth(w))
+ d = math.ceil(self.__data.layoutData.footer.text.heightForWidth(w))
if not (options & self.IgnoreFrames):
d += 2 * self.__data.layoutData.footer.frameWidth
if d > dimFooter:
@@ -848,7 +867,9 @@ def expandLineBreaks(self, options, rect):
length -= dimTitle + self.__data.spacing
d = scaleData.dimWithoutTitle
if not scaleData.scaleWidget.title().isEmpty():
- d += scaleData.scaleWidget.titleHeightForWidth(np.floor(length))
+ d += scaleData.scaleWidget.titleHeightForWidth(
+ math.floor(length)
+ )
if d > dimAxes[axis]:
dimAxes[axis] = d
done = False
@@ -858,7 +879,7 @@ def alignScales(self, options, canvasRect, scaleRect):
"""
Align the ticks of the axis to the canvas borders using
the empty corners.
-
+
:param options: Options how to layout the legend
:param QRectF canvasRect: Geometry of the canvas ( IN/OUT )
:param QRectF scaleRect: Geometry of the scales ( IN/OUT )
@@ -1016,7 +1037,7 @@ def alignScales(self, options, canvasRect, scaleRect):
def activate(self, plot, plotRect, options=0x00):
"""
Recalculate the geometry of all components.
-
+
:param qwt.plot.QwtPlot plot: Plot to be layout
:param QRectF plotRect: Rectangle where to place the components
:param options: Layout options
@@ -1031,9 +1052,11 @@ def activate(self, plot, plotRect, options=0x00):
):
self.__data.legendRect = self.layoutLegend(options, rect)
region = QRegion(rect.toRect())
- rect = region.subtracted(
- QRegion(self.__data.legendRect.toRect())
- ).boundingRect()
+ rect = QRectF(
+ region.subtracted(
+ QRegion(self.__data.legendRect.toRect())
+ ).boundingRect()
+ )
if self.__data.legendPos == QwtPlot.LeftLegend:
rect.setLeft(rect.left() + self.__data.spacing)
elif self.__data.legendPos == QwtPlot.RightLegend:
@@ -1142,4 +1165,3 @@ def activate(self, plot, plotRect, options=0x00):
self.__data.legendRect = self.alignLegend(
self.__data.canvasRect, self.__data.legendRect
)
-
diff --git a/qwt/plot_marker.py b/qwt/plot_marker.py
index cc29141..1db25cd 100644
--- a/qwt/plot_marker.py
+++ b/qwt/plot_marker.py
@@ -13,19 +13,20 @@
:members:
"""
-from .plot import QwtPlot, QwtPlotItem
-from .text import QwtText
-from .painter import QwtPainter
-from .graphic import QwtGraphic
-from .symbol import QwtSymbol
-from .qthelpers import qcolor_from_str
+from qtpy.QtCore import QLineF, QObject, QPointF, QRect, QRectF, QSizeF, Qt
+from qtpy.QtGui import QPainter, QPen
-from .qt.QtGui import QPen, QPainter
-from .qt.QtCore import Qt, QPointF, QRectF, QSizeF, QRect
+from qwt.graphic import QwtGraphic
+from qwt.plot import QwtPlot, QwtPlotItem
+from qwt.qthelpers import qcolor_from_str
+from qwt.symbol import QwtSymbol
+from qwt.text import QwtText
-class QwtPlotMarker_PrivateData(object):
+class QwtPlotMarker_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.labelAlignment = Qt.AlignCenter
self.labelOrientation = Qt.Horizontal
self.spacing = 2
@@ -44,21 +45,21 @@ class QwtPlotMarker(QwtPlotItem):
A marker can be a horizontal line, a vertical line,
a symbol, a label or any combination of them, which can
be drawn around a center point inside a bounding rectangle.
-
+
The `setSymbol()` member assigns a symbol to the marker.
The symbol is drawn at the specified point.
-
+
With `setLabel()`, a label can be assigned to the marker.
- The `setLabelAlignment()` member specifies where the label is drawn. All
+ The `setLabelAlignment()` member specifies where the label is drawn. All
the Align*-constants in `Qt.AlignmentFlags` (see Qt documentation)
are valid. The interpretation of the alignment depends on the marker's
line style. The alignment refers to the center point of
the marker, which means, for example, that the label would be printed
- left above the center point if the alignment was set to
+ left above the center point if the alignment was set to
`Qt.AlignLeft | Qt.AlignTop`.
-
+
Line styles:
-
+
* `QwtPlotMarker.NoLine`: No line
* `QwtPlotMarker.HLine`: A horizontal line
* `QwtPlotMarker.VLine`: A vertical line
@@ -100,7 +101,7 @@ def make(
):
"""
Create and setup a new `QwtPlotMarker` object (convenience function).
-
+
:param xvalue: x position (optional, default: None)
:type xvalue: float or None
:param yvalue: y position (optional, default: None)
@@ -131,7 +132,7 @@ def make(
:param bool antialiased: if True, enable antialiasing rendering
.. seealso::
-
+
:py:meth:`setData()`, :py:meth:`setPen()`, :py:meth:`attach()`
"""
item = cls(title)
@@ -184,13 +185,13 @@ def yValue(self):
def setValue(self, *args):
"""
Set Value
-
+
.. py:method:: setValue(pos):
-
+
:param QPointF pos: Position
-
+
.. py:method:: setValue(x, y):
-
+
:param float x: x position
:param float y: y position
"""
@@ -212,7 +213,7 @@ def setValue(self, *args):
def setXValue(self, x):
"""
Set X Value
-
+
:param float x: x position
"""
self.setValue(x, self.__data.yValue)
@@ -220,7 +221,7 @@ def setXValue(self, x):
def setYValue(self, y):
"""
Set Y Value
-
+
:param float y: y position
"""
self.setValue(self.__data.xValue, y)
@@ -228,7 +229,7 @@ def setYValue(self, y):
def draw(self, painter, xMap, yMap, canvasRect):
"""
Draw the marker
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: x Scale Map
:param qwt.scale_map.QwtScaleMap yMap: y Scale Map
@@ -240,9 +241,8 @@ def draw(self, painter, xMap, yMap, canvasRect):
self.drawLines(painter, canvasRect, pos)
if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol:
sz = self.__data.symbol.size()
- clipRect = QRectF(
- canvasRect.adjusted(-sz.width(), -sz.height(), sz.width(), sz.height())
- )
+ width, height = int(sz.width()), int(sz.height())
+ clipRect = QRectF(canvasRect.adjusted(-width, -height, width, height))
if clipRect.contains(pos):
self.__data.symbol.drawSymbols(painter, [pos])
self.drawLabel(painter, canvasRect, pos)
@@ -250,14 +250,14 @@ def draw(self, painter, xMap, yMap, canvasRect):
def drawLines(self, painter, canvasRect, pos):
"""
Draw the lines marker
-
+
:param QPainter painter: Painter
:param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
:param QPointF pos: Position of the marker, translated into widget coordinates
-
+
.. seealso::
-
- :py:meth:`drawLabel()`,
+
+ :py:meth:`drawLabel()`,
:py:meth:`qwt.symbol.QwtSymbol.drawSymbol()`
"""
if self.__data.style == self.NoLine:
@@ -265,27 +265,27 @@ def drawLines(self, painter, canvasRect, pos):
painter.setPen(self.__data.pen)
if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross):
y = pos.y()
- painter.drawLine(canvasRect.left(), y, canvasRect.right() - 1.0, y)
+ painter.drawLine(QLineF(canvasRect.left(), y, canvasRect.right() - 1.0, y))
if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross):
x = pos.x()
- painter.drawLine(x, canvasRect.top(), x, canvasRect.bottom() - 1.0)
+ painter.drawLine(QLineF(x, canvasRect.top(), x, canvasRect.bottom() - 1.0))
def drawLabel(self, painter, canvasRect, pos):
"""
Align and draw the text label of the marker
-
+
:param QPainter painter: Painter
:param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates
:param QPointF pos: Position of the marker, translated into widget coordinates
-
+
.. seealso::
-
- :py:meth:`drawLabel()`,
+
+ :py:meth:`drawLabel()`,
:py:meth:`qwt.symbol.QwtSymbol.drawSymbol()`
"""
if self.__data.label.isEmpty():
return
- align = Qt.Alignment(self.__data.labelAlignment)
+ align = self.__data.labelAlignment
alignPos = QPointF(pos)
symbolOff = QSizeF(0, 0)
if self.__data.style == QwtPlotMarker.VLine:
@@ -316,7 +316,7 @@ def drawLabel(self, painter, canvasRect, pos):
alignPos.setX(canvasRect.center().x())
else:
if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol:
- symbolOff = self.__data.symbol.size() + QSizeF(1, 1)
+ symbolOff = QSizeF(self.__data.symbol.size()) + QSizeF(1, 1)
symbolOff /= 2
pw2 = self.__data.pen.widthF() / 2.0
if pw2 == 0.0:
@@ -360,18 +360,18 @@ def drawLabel(self, painter, canvasRect, pos):
def setLineStyle(self, style):
"""
Set the line style
-
+
:param int style: Line style
Line styles:
-
+
* `QwtPlotMarker.NoLine`: No line
* `QwtPlotMarker.HLine`: A horizontal line
* `QwtPlotMarker.VLine`: A vertical line
* `QwtPlotMarker.Cross`: A crosshair
-
+
.. seealso::
-
+
:py:meth:`lineStyle()`
"""
if style != self.__data.style:
@@ -382,9 +382,9 @@ def setLineStyle(self, style):
def lineStyle(self):
"""
:return: the line style
-
+
.. seealso::
-
+
:py:meth:`setLineStyle()`
"""
return self.__data.style
@@ -392,11 +392,11 @@ def lineStyle(self):
def setSymbol(self, symbol):
"""
Assign a symbol
-
+
:param qwt.symbol.QwtSymbol symbol: New symbol
-
+
.. seealso::
-
+
:py:meth:`symbol()`
"""
if symbol != self.__data.symbol:
@@ -409,9 +409,9 @@ def setSymbol(self, symbol):
def symbol(self):
"""
:return: the symbol
-
+
.. seealso::
-
+
:py:meth:`setSymbol()`
"""
return self.__data.symbol
@@ -419,12 +419,12 @@ def symbol(self):
def setLabel(self, label):
"""
Set the label
-
+
:param label: Label text
:type label: qwt.text.QwtText or str
-
+
.. seealso::
-
+
:py:meth:`label()`
"""
if not isinstance(label, QwtText):
@@ -436,9 +436,9 @@ def setLabel(self, label):
def label(self):
"""
:return: the label
-
+
.. seealso::
-
+
:py:meth:`setLabel()`
"""
return self.__data.label
@@ -452,13 +452,13 @@ def setLabelAlignment(self, align):
canvas rectangle. In case of `QwtPlotMarker.VLine` the alignment is
relative to the x position of the marker, but the vertical flags
correspond to the canvas rectangle.
-
+
In all other styles the alignment is relative to the marker's position.
-
+
:param Qt.Alignment align: Alignment
-
+
.. seealso::
-
+
:py:meth:`labelAlignment()`, :py:meth:`labelOrientation()`
"""
if align != self.__data.labelAlignment:
@@ -468,9 +468,9 @@ def setLabelAlignment(self, align):
def labelAlignment(self):
"""
:return: the label alignment
-
+
.. seealso::
-
+
:py:meth:`setLabelAlignment()`, :py:meth:`setLabelOrientation()`
"""
return self.__data.labelAlignment
@@ -481,11 +481,11 @@ def setLabelOrientation(self, orientation):
When orientation is `Qt.Vertical` the label is rotated by 90.0 degrees
(from bottom to top).
-
+
:param Qt.Orientation orientation: Orientation of the label
-
+
.. seealso::
-
+
:py:meth:`labelOrientation()`, :py:meth:`setLabelAlignment()`
"""
if orientation != self.__data.labelOrientation:
@@ -495,9 +495,9 @@ def setLabelOrientation(self, orientation):
def labelOrientation(self):
"""
:return: the label orientation
-
+
.. seealso::
-
+
:py:meth:`setLabelOrientation()`, :py:meth:`labelAlignment()`
"""
return self.__data.labelOrientation
@@ -508,11 +508,11 @@ def setSpacing(self, spacing):
When the label is not centered on the marker position, the spacing
is the distance between the position and the label.
-
+
:param int spacing: Spacing
-
+
.. seealso::
-
+
:py:meth:`spacing()`, :py:meth:`setLabelAlignment()`
"""
if spacing < 0:
@@ -524,9 +524,9 @@ def setSpacing(self, spacing):
def spacing(self):
"""
:return: the spacing
-
+
.. seealso::
-
+
:py:meth:`setSpacing()`
"""
return self.__data.spacing
@@ -534,27 +534,29 @@ def spacing(self):
def setLinePen(self, *args):
"""
Build and/or assigna a line pen, depending on the arguments.
-
+
.. py:method:: setLinePen(color, width, style)
-
+ :noindex:
+
Build and assign a line pen
-
+
In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it
- non cosmetic (see `QPen.isCosmetic()`). This method signature has
+ non cosmetic (see `QPen.isCosmetic()`). This method signature has
been introduced to hide this incompatibility.
-
+
:param QColor color: Pen color
:param float width: Pen width
:param Qt.PenStyle style: Pen style
-
+
.. py:method:: setLinePen(pen)
-
+ :noindex:
+
Specify a pen for the line.
-
+
:param QPen pen: New pen
-
+
.. seealso::
-
+
:py:meth:`pen()`, :py:meth:`brush()`
"""
if len(args) == 1 and isinstance(args[0], QPen):
@@ -582,9 +584,9 @@ def setLinePen(self, *args):
def linePen(self):
"""
:return: the line pen
-
+
.. seealso::
-
+
:py:meth:`setLinePen()`
"""
return self.__data.pen
@@ -602,9 +604,9 @@ def legendIcon(self, index, size):
:param int index: Index of the legend entry (ignored as there is only one)
:param QSizeF size: Icon size
:return: Icon representing the marker on the legend
-
+
.. seealso::
-
+
:py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`,
:py:meth:`qwt.plot.QwtPlotItem.legendData()`
"""
@@ -621,10 +623,10 @@ def legendIcon(self, index, size):
painter.setPen(self.__data.pen)
if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross):
y = 0.5 * size.height()
- painter.drawLine(0.0, y, size.width(), y)
+ painter.drawLine(QLineF(0.0, y, size.width(), y))
if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross):
x = 0.5 * size.width()
- painter.drawLine(x, 0.0, x, size.height())
+ painter.drawLine(QLineF(x, 0.0, x, size.height()))
if self.__data.symbol:
r = QRect(0, 0, size.width(), size.height())
self.__data.symbol.drawSymbol(painter, r)
diff --git a/qwt/plot_renderer.py b/qwt/plot_renderer.py
index a7be5da..d8b2640 100644
--- a/qwt/plot_renderer.py
+++ b/qwt/plot_renderer.py
@@ -13,33 +13,32 @@
:members:
"""
-from __future__ import division
-
-from .painter import QwtPainter
-from .plot import QwtPlot
-from .plot_layout import QwtPlotLayout
-from .scale_draw import QwtScaleDraw
-from .scale_map import QwtScaleMap
+import math
+import os.path as osp
-from .qt.QtGui import (
- QPrinter,
- QPainter,
- QImageWriter,
- QImage,
+from qtpy.compat import getsavefilename
+from qtpy.QtCore import QObject, QRect, QRectF, QSizeF, Qt
+from qtpy.QtGui import (
QColor,
+ QImage,
+ QImageWriter,
+ QPageSize,
QPaintDevice,
- QTransform,
- QPalette,
- QFileDialog,
+ QPainter,
QPainterPath,
+ QPalette,
QPen,
+ QTransform,
)
-from .qt.QtCore import Qt, QRect, QRectF, QObject, QSizeF
-from .qt.QtSvg import QSvgGenerator
-from .qt.compat import getsavefilename
+from qtpy.QtPrintSupport import QPrinter
+from qtpy.QtSvg import QSvgGenerator
+from qtpy.QtWidgets import QFileDialog
-import math
-import os.path as osp
+from qwt.painter import QwtPainter
+from qwt.plot import QwtPlot
+from qwt.plot_layout import QwtPlotLayout
+from qwt.scale_draw import QwtScaleDraw
+from qwt.scale_map import QwtScaleMap
def qwtCanvasClip(canvas, canvasRect):
@@ -56,8 +55,10 @@ def qwtCanvasClip(canvas, canvasRect):
return canvas.borderPath(r)
-class QwtPlotRenderer_PrivateData(object):
+class QwtPlotRenderer_PrivateData(QObject):
def __init__(self):
+ QObject.__init__(self)
+
self.discardFlags = QwtPlotRenderer.DiscardNone
self.layoutFlags = QwtPlotRenderer.DefaultLayout
@@ -66,9 +67,9 @@ class QwtPlotRenderer(QObject):
"""
Renderer for exporting a plot to a document, a printer
or anything else, that is supported by QPainter/QPaintDevice
-
+
Discard flags:
-
+
* `QwtPlotRenderer.DiscardNone`: Render all components of the plot
* `QwtPlotRenderer.DiscardBackground`: Don't render the background of the plot
* `QwtPlotRenderer.DiscardTitle`: Don't render the title of the plot
@@ -76,14 +77,14 @@ class QwtPlotRenderer(QObject):
* `QwtPlotRenderer.DiscardCanvasBackground`: Don't render the background of the canvas
* `QwtPlotRenderer.DiscardFooter`: Don't render the footer of the plot
* `QwtPlotRenderer.DiscardCanvasFrame`: Don't render the frame of the canvas
-
+
.. note::
-
+
The `QwtPlotRenderer.DiscardCanvasFrame` flag has no effect when using
style sheets, where the frame is part of the background
-
+
Layout flags:
-
+
* `QwtPlotRenderer.DefaultLayout`: Use the default layout as on screen
* `QwtPlotRenderer.FrameWithScales`: Instead of the scales a box is painted around the plot canvas, where the scale ticks are aligned to.
"""
@@ -113,8 +114,8 @@ def setDiscardFlag(self, flag, on=True):
:param bool on: On/Off
.. seealso::
-
- :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlags()`,
+
+ :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlags()`,
:py:meth:`discardFlags()`
"""
if on:
@@ -128,8 +129,8 @@ def testDiscardFlag(self, flag):
:return: True, if flag is enabled.
.. seealso::
-
- :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`,
+
+ :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`,
:py:meth:`discardFlags()`
"""
return self.__data.discardFlags & flag
@@ -141,8 +142,8 @@ def setDiscardFlags(self, flags):
:param int flags: Flags
.. seealso::
-
- :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlag()`,
+
+ :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlag()`,
:py:meth:`discardFlags()`
"""
self.__data.discardFlags = flags
@@ -152,8 +153,8 @@ def discardFlags(self):
:return: Flags, indicating what to discard from rendering
.. seealso::
-
- :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`,
+
+ :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`,
:py:meth:`testDiscardFlag()`
"""
return self.__data.discardFlags
@@ -165,8 +166,8 @@ def setLayoutFlag(self, flag, on=True):
:param int flag: Flag to change
.. seealso::
-
- :py:meth:`testLayoutFlag()`, :py:meth:`setLayoutFlags()`,
+
+ :py:meth:`testLayoutFlag()`, :py:meth:`setLayoutFlags()`,
:py:meth:`layoutFlags()`
"""
if on:
@@ -180,8 +181,8 @@ def testLayoutFlag(self, flag):
:return: True, if flag is enabled.
.. seealso::
-
- :py:meth:`setLayoutFlag()`, :py:meth:`setLayoutFlags()`,
+
+ :py:meth:`setLayoutFlag()`, :py:meth:`setLayoutFlags()`,
:py:meth:`layoutFlags()`
"""
return self.__data.layoutFlags & flag
@@ -193,8 +194,8 @@ def setLayoutFlags(self, flags):
:param int flags: Flags
.. seealso::
-
- :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()`,
+
+ :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()`,
:py:meth:`layoutFlags()`
"""
self.__data.layoutFlags = flags
@@ -204,8 +205,8 @@ def layoutFlags(self):
:return: Layout flags
.. seealso::
-
- :py:meth:`setLayoutFlags()`, :py:meth:`setLayoutFlag()`,
+
+ :py:meth:`setLayoutFlags()`, :py:meth:`setLayoutFlag()`,
:py:meth:`testLayoutFlag()`
"""
return self.__data.layoutFlags
@@ -218,7 +219,7 @@ def renderDocument(
The format of the document will be auto-detected from the
suffix of the file name.
-
+
:param qwt.plot.QwtPlot plot: Plot widget
:param str fileName: Path of the file, where the document will be stored
:param QSizeF sizeMM: Size for the document in millimeters
@@ -243,12 +244,20 @@ def renderDocument(
if fmt in ("pdf", "ps"):
printer = QPrinter()
if fmt == "pdf":
- printer.setOutputFormat(QPrinter.PdfFormat)
+ try:
+ printer.setOutputFormat(QPrinter.PdfFormat)
+ except AttributeError:
+ # PyQt6 on Linux
+ printer.setPrinterName("")
else:
printer.setOutputFormat(QPrinter.PostScriptFormat)
- printer.setColorMode(QPrinter.Color)
+ try:
+ printer.setColorMode(QPrinter.Color)
+ except AttributeError:
+ # PyQt6 on Linux
+ pass
printer.setFullPage(True)
- printer.setPaperSize(sizeMM, QPrinter.Millimeter)
+ printer.setPageSize(QPageSize(sizeMM, QPageSize.Millimeter))
printer.setDocName(title)
printer.setOutputFileName(filename)
printer.setResolution(resolution)
@@ -294,10 +303,10 @@ def renderTo(self, plot, dest):
:param qwt.plot.QwtPlot plot: Plot widget
:param dest: QPaintDevice, QPrinter or QSvgGenerator instance
-
+
.. seealso::
-
- :py:meth:`render()`,
+
+ :py:meth:`render()`,
:py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()`
"""
if isinstance(dest, QPaintDevice):
@@ -330,10 +339,10 @@ def render(self, plot, painter, plotRect):
:param QPainter painter: Painter
:param str format: Format for the document
:param QRectF plotRect: Bounding rectangle
-
+
.. seealso::
-
- :py:meth:`renderDocument()`, :py:meth:`renderTo()`,
+
+ :py:meth:`renderDocument()`, :py:meth:`renderTo()`,
:py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()`
"""
if (
@@ -358,8 +367,8 @@ def render(self, plot, painter, plotRect):
invtrans, _ok = transform.inverted()
layoutRect = invtrans.mapRect(plotRect)
if not (self.__data.discardFlags & self.DiscardBackground):
- left, top, right, bottom = plot.getContentsMargins()
- layoutRect.adjust(left, top, -right, -bottom)
+ mg = plot.contentsMargins()
+ layoutRect.adjust(mg.left(), mg.top(), -mg.right(), -mg.bottom())
layout = plot.plotLayout()
baseLineDists = canvasMargins = [None] * len(QwtPlot.AXES)
@@ -369,7 +378,10 @@ def render(self, plot, painter, plotRect):
if self.__data.layoutFlags & self.FrameWithScales:
scaleWidget = plot.axisWidget(axisId)
if scaleWidget:
- baseLineDists[axisId] = scaleWidget.margin()
+ mgn = scaleWidget.contentsMargins()
+ baseLineDists[axisId] = max(
+ [mgn.left(), mgn.top(), mgn.right(), mgn.bottom()]
+ )
scaleWidget.setMargin(0)
if not plot.axisEnabled(axisId):
# When we have a scale the frame is painted on
@@ -432,7 +444,8 @@ def render(self, plot, painter, plotRect):
for axisId in QwtPlot.AXES:
scaleWidget = plot.axisWidget(axisId)
if scaleWidget:
- baseDist = scaleWidget.margin()
+ mgn = scaleWidget.contentsMargins()
+ baseDist = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()])
startDist, endDist = scaleWidget.getBorderDistHint()
self.renderScale(
plot,
@@ -554,8 +567,8 @@ def renderCanvas(self, plot, painter, canvasRect, maps):
:param qwt.plot.QwtPlot plot: Plot widget
:param QPainter painter: Painter
- :param qwt.scale_map.QwtScaleMap maps: mapping between plot and paint device coordinates
:param QRectF rect: Bounding rectangle
+ :param qwt.scale_map.QwtScaleMap maps: mapping between plot and paint device coordinates
"""
canvas = plot.canvas()
r = canvasRect.adjusted(0.0, 0.0, -1.0, 1.0)
@@ -687,9 +700,9 @@ def exportTo(self, plot, documentname, sizeMM=None, resolution=85):
:param QSizeF sizeMM: Size for the document in millimeters
:param int resolution: Resolution in dots per Inch (dpi)
:return: True, when exporting was successful
-
+
.. seealso::
-
+
:py:meth:`renderDocument()`
"""
if plot is None:
@@ -723,3 +736,4 @@ def exportTo(self, plot, documentname, sizeMM=None, resolution=85):
return False
self.renderDocument(plot, filename, sizeMM, resolution)
return True
+ return True
diff --git a/qwt/plot_series.py b/qwt/plot_series.py
index 0b844e7..e0f21f8 100644
--- a/qwt/plot_series.py
+++ b/qwt/plot_series.py
@@ -20,7 +20,7 @@
.. autoclass:: QwtSeriesData
:members:
-
+
QwtPointArrayData
~~~~~~~~~~~~~~~~~
@@ -35,11 +35,10 @@
"""
import numpy as np
+from qtpy.QtCore import QPointF, QRectF, Qt
-from .plot import QwtPlotItem, QwtPlotItem_PrivateData
-from .text import QwtText
-
-from .qt.QtCore import Qt, QRectF, QPointF
+from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData
+from qwt.text import QwtText
class QwtPlotSeriesItem_PrivateData(QwtPlotItem_PrivateData):
@@ -69,7 +68,7 @@ def setOrientation(self, orientation):
int `QwtPlotCurve.Steps` or `QwtPlotCurve.Sticks` style.
.. seealso::
-
+
:py:meth`orientation()`
"""
if self.__data.orientation != orientation:
@@ -82,7 +81,7 @@ def orientation(self):
:return: Orientation of the plot item
.. seealso::
-
+
:py:meth`setOrientation()`
"""
return self.__data.orientation
@@ -101,16 +100,16 @@ def draw(self, painter, xMap, yMap, canvasRect):
def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to):
"""
Draw a subset of the samples
-
+
:param QPainter painter: Painter
:param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates.
:param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates.
:param QRectF canvasRect: Contents rectangle of the canvas
:param int from_: Index of the first point to be painted
:param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point.
-
+
.. seealso::
-
+
This method is implemented in `qwt.plot_curve.QwtPlotCurve`
"""
raise NotImplementedError
@@ -142,10 +141,10 @@ class QwtSeriesData(object):
needs to be displayed, without having to copy it, it is recommended
to implement an individual data access.
- A subclass of `QwtSeriesData` must implement:
+ A subclass of `QwtSeriesData` must implement:
- size():
-
+
Should return number of data points.
- sample()
@@ -156,9 +155,9 @@ class QwtSeriesData(object):
- boundingRect()
Should return the bounding rectangle of the data series.
- It is used for autoscaling and might help certain algorithms for
+ It is used for autoscaling and might help certain algorithms for
displaying the data.
- The member `_boundingRect` is intended for caching the calculated
+ The member `_boundingRect` is intended for caching the calculated
rectangle.
"""
@@ -174,7 +173,7 @@ def setRectOfInterest(self, rect):
It can be used to implement different levels of details.
The default implementation does nothing.
-
+
:param QRectF rect: Rectangle of interest
"""
pass
@@ -188,7 +187,7 @@ def size(self):
def sample(self, i):
"""
Return a sample
-
+
:param int i: Index
:return: Sample at position i
"""
@@ -209,9 +208,9 @@ def boundingRect(self):
class QwtPointArrayData(QwtSeriesData):
"""
Interface for iterating over two array objects
-
+
.. py:class:: QwtCQwtPointArrayDataolorMap(x, y, [size=None])
-
+
:param x: Array of x values
:type x: list or tuple or numpy.array
:param y: Array of y values
@@ -237,6 +236,10 @@ def __init__(self, x=None, y=None, size=None, finite=None):
if size is not None:
x = np.resize(x, (size,))
y = np.resize(y, (size,))
+ if len(x) != len(y):
+ minlen = min(len(x), len(y))
+ x = np.resize(x, (minlen,))
+ y = np.resize(y, (minlen,))
if finite if finite is not None else True:
indexes = np.logical_and(np.isfinite(x), np.isfinite(y))
self.__x = x[indexes]
@@ -290,7 +293,7 @@ class QwtSeriesStore(object):
"""
Class storing a `QwtSeriesData` object
- `QwtSeriesStore` and `QwtPlotSeriesItem` are intended as base classes for
+ `QwtSeriesStore` and `QwtPlotSeriesItem` are intended as base classes for
all plot items iterating over a series of samples.
"""
@@ -304,8 +307,8 @@ def setData(self, series):
:param qwt.plot_series.QwtSeriesData series: Data
.. warning::
-
- The item takes ownership of the data object, deleting it
+
+ The item takes ownership of the data object, deleting it
when its not used anymore.
"""
if self.__series != series:
@@ -334,10 +337,10 @@ def sample(self, index):
def dataSize(self):
"""
:return: Number of samples of the series
-
+
.. seealso::
-
- :py:meth:`setData()`,
+
+ :py:meth:`setData()`,
:py:meth:`qwt.plot_series.QwtSeriesData.size()`
"""
if self.__series is None:
@@ -347,9 +350,9 @@ def dataSize(self):
def dataRect(self):
"""
:return: Bounding rectangle of the series or an invalid rectangle, when no series is stored
-
+
.. seealso::
-
+
:py:meth:`qwt.plot_series.QwtSeriesData.boundingRect()`
"""
if self.__series is None or self.dataSize() == 0:
@@ -359,11 +362,11 @@ def dataRect(self):
def setRectOfInterest(self, rect):
"""
Set a the "rect of interest" for the series
-
+
:param QRectF rect: Rectangle of interest
-
+
.. seealso::
-
+
:py:meth:`qwt.plot_series.QwtSeriesData.setRectOfInterest()`
"""
if self.__series:
@@ -372,7 +375,7 @@ def setRectOfInterest(self, rect):
def swapData(self, series):
"""
Replace a series without deleting the previous one
-
+
:param qwt.plot_series.QwtSeriesData series: New series
:return: Previously assigned series
"""
diff --git a/qwt/py3compat.py b/qwt/py3compat.py
deleted file mode 100644
index 0e089ba..0000000
--- a/qwt/py3compat.py
+++ /dev/null
@@ -1,230 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012-2013 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-"""
-qwt.py3compat (exact copy of spyderlib.py3compat)
--------------------------------------------------
-
-Transitional module providing compatibility functions intended to help
-migrating from Python 2 to Python 3.
-
-This module should be fully compatible with:
- * Python >=v2.6
- * Python 3
-"""
-
-from __future__ import print_function
-
-import sys
-import os
-
-PY2 = sys.version[0] == '2'
-PY3 = sys.version[0] == '3'
-
-
-#==============================================================================
-# Data types
-#==============================================================================
-if PY2:
- # Python 2
- TEXT_TYPES = (str, unicode)
- INT_TYPES = (int, long)
-else:
- # Python 3
- TEXT_TYPES = (str,)
- INT_TYPES = (int,)
-NUMERIC_TYPES = tuple(list(INT_TYPES) + [float, complex])
-
-
-#==============================================================================
-# Renamed/Reorganized modules
-#==============================================================================
-if PY2:
- # Python 2
- import __builtin__ as builtins
- import ConfigParser as configparser
- try:
- import _winreg as winreg
- except ImportError:
- pass
- from sys import maxint as maxsize
- try:
- import CStringIO as io
- except ImportError:
- import StringIO as io
- try:
- import cPickle as pickle
- except ImportError:
- import pickle
- from UserDict import DictMixin as MutableMapping
-else:
- # Python 3
- import builtins
- import configparser
- try:
- import winreg
- except ImportError:
- pass
- from sys import maxsize
- import io
- import pickle
- from collections import MutableMapping
-
-
-#==============================================================================
-# Strings
-#==============================================================================
-def is_text_string(obj):
- """Return True if `obj` is a text string, False if it is anything else,
- like binary data (Python 3) or QString (Python 2, PyQt API #1)"""
- if PY2:
- # Python 2
- return isinstance(obj, basestring)
- else:
- # Python 3
- return isinstance(obj, str)
-
-def is_binary_string(obj):
- """Return True if `obj` is a binary string, False if it is anything else"""
- if PY2:
- # Python 2
- return isinstance(obj, str)
- else:
- # Python 3
- return isinstance(obj, bytes)
-
-def is_string(obj):
- """Return True if `obj` is a text or binary Python string object,
- False if it is anything else, like a QString (Python 2, PyQt API #1)"""
- return is_text_string(obj) or is_binary_string(obj)
-
-def is_unicode(obj):
- """Return True if `obj` is unicode"""
- if PY2:
- # Python 2
- return isinstance(obj, unicode)
- else:
- # Python 3
- return isinstance(obj, str)
-
-def to_text_string(obj, encoding=None):
- """Convert `obj` to (unicode) text string"""
- if PY2:
- # Python 2
- if encoding is None:
- return unicode(obj)
- else:
- return unicode(obj, encoding)
- else:
- # Python 3
- if encoding is None:
- return str(obj)
- elif isinstance(obj, str):
- # In case this function is not used properly, this could happen
- return obj
- else:
- return str(obj, encoding)
-
-def to_binary_string(obj, encoding=None):
- """Convert `obj` to binary string (bytes in Python 3, str in Python 2)"""
- if PY2:
- # Python 2
- if encoding is None:
- return str(obj)
- else:
- return obj.encode(encoding)
- else:
- # Python 3
- return bytes(obj, 'utf-8' if encoding is None else encoding)
-
-
-#==============================================================================
-# Function attributes
-#==============================================================================
-def get_func_code(func):
- """Return function code object"""
- if PY2:
- # Python 2
- return func.func_code
- else:
- # Python 3
- return func.__code__
-
-def get_func_name(func):
- """Return function name"""
- if PY2:
- # Python 2
- return func.func_name
- else:
- # Python 3
- return func.__name__
-
-def get_func_defaults(func):
- """Return function default argument values"""
- if PY2:
- # Python 2
- return func.func_defaults
- else:
- # Python 3
- return func.__defaults__
-
-
-#==============================================================================
-# Special method attributes
-#==============================================================================
-def get_meth_func(obj):
- """Return method function object"""
- if PY2:
- # Python 2
- return obj.im_func
- else:
- # Python 3
- return obj.__func__
-
-def get_meth_class_inst(obj):
- """Return method class instance"""
- if PY2:
- # Python 2
- return obj.im_self
- else:
- # Python 3
- return obj.__self__
-
-def get_meth_class(obj):
- """Return method class"""
- if PY2:
- # Python 2
- return obj.im_class
- else:
- # Python 3
- return obj.__self__.__class__
-
-
-#==============================================================================
-# Misc.
-#==============================================================================
-if PY2:
- # Python 2
- input = raw_input
- getcwd = os.getcwdu
- cmp = cmp
- import string
- str_lower = string.lower
-else:
- # Python 3
- input = input
- getcwd = os.getcwd
- def cmp(a, b):
- return (a > b) - (a < b)
- str_lower = str.lower
-
-def qbytearray_to_str(qba):
- """Convert QByteArray object to str in a way compatible with Python 2/3"""
- return str(bytes(qba.toHex()).decode())
-
-
-if __name__ == '__main__':
- pass
diff --git a/qwt/qt/QtCore.py b/qwt/qt/QtCore.py
deleted file mode 100644
index e43f631..0000000
--- a/qwt/qt/QtCore.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtCore import * # analysis:ignore
- from PyQt5.QtCore import QCoreApplication
- from PyQt5.QtCore import pyqtSignal as Signal
- from PyQt5.QtCore import pyqtSlot as Slot
- from PyQt5.QtCore import pyqtProperty as Property
- from PyQt5.QtCore import QT_VERSION_STR as __version__
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.QtCore import * # analysis:ignore
- from PyQt4.Qt import QCoreApplication # analysis:ignore
- from PyQt4.Qt import Qt # analysis:ignore
- from PyQt4.QtCore import pyqtSignal as Signal # analysis:ignore
- from PyQt4.QtCore import pyqtSlot as Slot # analysis:ignore
- from PyQt4.QtCore import pyqtProperty as Property # analysis:ignore
- from PyQt4.QtCore import QT_VERSION_STR as __version__ # analysis:ignore
- # Forces new modules written by PyQt4 developers to be PyQt5-compatible
- del SIGNAL, SLOT
-else:
- import PySide.QtCore
- __version__ = PySide.QtCore.__version__ # analysis:ignore
- from PySide.QtCore import * # analysis:ignore
diff --git a/qwt/qt/QtGui.py b/qwt/qt/QtGui.py
deleted file mode 100644
index 9082327..0000000
--- a/qwt/qt/QtGui.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtCore import QSortFilterProxyModel # analysis:ignore
- from PyQt5.QtPrintSupport import (QPrinter, QPrintDialog, # analysis:ignore
- QAbstractPrintDialog)
- from PyQt5.QtPrintSupport import QPrintPreviewDialog # analysis:ignore
- from PyQt5.QtGui import * # analysis:ignore
- from PyQt5.QtWidgets import * # analysis:ignore
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.Qt import QKeySequence, QTextCursor # analysis:ignore
- from PyQt4.QtGui import * # analysis:ignore
-else:
- from PySide.QtGui import * # analysis:ignore
diff --git a/qwt/qt/QtSvg.py b/qwt/qt/QtSvg.py
deleted file mode 100644
index c142165..0000000
--- a/qwt/qt/QtSvg.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2012 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtSvg import * # analysis:ignore
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.QtSvg import * # analysis:ignore
-else:
- from PySide.QtSvg import * # analysis:ignore
diff --git a/qwt/qt/QtWebKit.py b/qwt/qt/QtWebKit.py
deleted file mode 100644
index e0ed4a8..0000000
--- a/qwt/qt/QtWebKit.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-import os
-
-if os.environ['QT_API'] == 'pyqt5':
- from PyQt5.QtWebKitWidgets import QWebPage, QWebView # analysis:ignore
- from PyQt5.QtWebKit import QWebSettings # analysis:ignore
-elif os.environ['QT_API'] == 'pyqt':
- from PyQt4.QtWebKit import (QWebPage, QWebView, # analysis:ignore
- QWebSettings)
-else:
- from PySide.QtWebKit import * # analysis:ignore
diff --git a/qwt/qt/__init__.py b/qwt/qt/__init__.py
deleted file mode 100644
index 592ddbb..0000000
--- a/qwt/qt/__init__.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011-2012 Pierre Raybaut
-# © 2012-2014 anatoly techtonik
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-"""Compatibility package (PyQt4/PyQt5/PySide)"""
-
-import os
-
-API = os.environ.get('QT_API')
-if API is None:
- try:
- import PyQt5 # analysis:ignore
- API = 'pyqt5'
- except ImportError:
- try:
- import PyQt4 # analysis:ignore
- API = 'pyqt'
- except ImportError:
- API = 'pyside'
-os.environ['QT_API'] = API
-API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyside': 'PySide'}[API]
-
-if API == 'pyqt':
- # Spyder 2.3 is compatible with both #1 and #2 PyQt API,
- # but to avoid issues with IPython and other Qt plugins
- # we choose to support only API #2 for 2.4+
- import sip
- try:
- sip.setapi('QString', 2)
- sip.setapi('QVariant', 2)
- sip.setapi('QDate', 2)
- sip.setapi('QDateTime', 2)
- sip.setapi('QTextStream', 2)
- sip.setapi('QTime', 2)
- sip.setapi('QUrl', 2)
- except AttributeError:
- # PyQt < v4.6. The actual check is done by requirements.check_qt()
- # call from spyder.py
- pass
-
- try:
- from PyQt4.QtCore import PYQT_VERSION_STR as __version__ # analysis:ignore
- except ImportError:
- # Trying PyQt5 before switching to PySide (at this point, PyQt4 may
- # not be installed but PyQt5 or Pyside could still be if the QT_API
- # environment variable hasn't been set-up)
- try:
- import PyQt5 # analysis:ignore
- API = os.environ['QT_API'] = 'pyqt5'
- API_NAME = 'PyQt5'
- except ImportError:
- API = os.environ['QT_API'] = 'pyside'
- API_NAME = 'PySide'
- else:
- is_old_pyqt = __version__.startswith(('4.4', '4.5', '4.6', '4.7'))
- is_pyqt46 = __version__.startswith('4.6')
- import sip
- try:
- API_NAME += (" (API v%d)" % sip.getapi('QString'))
- except AttributeError:
- pass
- from PyQt4 import uic # analysis:ignore
-
-PYQT5 = False
-if API == 'pyqt5':
- try:
- from PyQt5.QtCore import PYQT_VERSION_STR as __version__
- from PyQt5 import uic # analysis:ignore
- PYQT5 = True
- is_old_pyqt = is_pyqt46 = False
- except ImportError:
- pass
-
-if API == 'pyside':
- try:
- from PySide import __version__ # analysis:ignore
- except ImportError:
- raise ImportError("Spyder requires PySide or PyQt to be installed")
- else:
- is_old_pyqt = is_pyqt46 = False
diff --git a/qwt/qt/compat.py b/qwt/qt/compat.py
deleted file mode 100644
index 5c7f9f3..0000000
--- a/qwt/qt/compat.py
+++ /dev/null
@@ -1,209 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © 2011-2012 Pierre Raybaut
-# Licensed under the terms of the MIT License
-# (see LICENSE file for details)
-
-"""
-qwt.qt.compat (exact copy of spyderlib.qt.compat)
--------------------------------------------------
-
-Transitional module providing compatibility functions intended to help
-migrating from PyQt to PySide.
-
-This module should be fully compatible with:
- * PyQt >=v4.4
- * both PyQt API #1 and API #2
- * PySide
-"""
-
-from __future__ import print_function
-
-import os
-import sys
-import collections
-
-from .QtGui import QFileDialog
-from ..py3compat import is_text_string, to_text_string, TEXT_TYPES
-
-#==============================================================================
-# QVariant conversion utilities
-#==============================================================================
-
-PYQT_API_1 = False
-if os.environ['QT_API'] == 'pyqt':
- import sip
- try:
- PYQT_API_1 = sip.getapi('QVariant') == 1 # PyQt API #1
- except AttributeError:
- # PyQt Developped by Pierre Raybaut
-
Copyright © 2020 Pierre Raybaut
-
Python %s, Qt %s on %s"""
- % (
- self.windowTitle(),
- platform.python_version(),
- qt_version,
- platform.system(),
- ),
- )
def run(wait=True):
- """Run PythonQwt tests or test launcher (requires `guidata`)"""
- app = QApplication([])
+ """Run PythonQwt tests or test launcher"""
+ app = QW.QApplication([])
launcher = TestLauncher()
launcher.show()
- unattended = os.environ.get("TEST_UNATTENDED") is not None
- if unattended:
- QTimer.singleShot(100, lambda: take_screenshot(launcher))
- app.exec_()
+ test_env = TestEnvironment()
+ if test_env.screenshots:
+ print("Running PythonQwt tests and taking screenshots automatically:")
+ QC.QTimer.singleShot(100, lambda: take_screenshot(launcher))
+ elif test_env.unattended:
+ print("Running PythonQwt tests in unattended mode:")
+ QC.QTimer.singleShot(0, QW.QApplication.instance().quit)
+ if QT_API == "pyside6":
+ app.exec()
+ else:
+ app.exec_()
launcher.close()
- if unattended:
+ if test_env.unattended:
run_all_tests(wait=wait)
-class TestOptions(QGroupBox):
- """Test options groupbox"""
-
- def __init__(self, parent=None):
- super(TestOptions, self).__init__("Test options", parent)
- self.setLayout(QFormLayout())
- self.hide()
-
- def add_checkbox(self, title, label, slot):
- """Add new checkbox to option panel"""
- widget = QCheckBox(label, self)
- widget.stateChanged.connect(slot)
- self.layout().addRow(title, widget)
- self.show()
- return widget
-
-
-class TestCentralWidget(QWidget):
- """Test central widget"""
-
- def __init__(self, widget_name, parent=None):
- super(TestCentralWidget, self).__init__(parent)
- self.widget_name = widget_name
- self.plots = None
- self.widget_of_interest = self.parent()
- self.setLayout(QVBoxLayout())
- self.options = TestOptions(self)
- self.add_widget(self.options)
-
- def add_widget(self, widget):
- """Add new sub-widget"""
- self.layout().addWidget(widget)
- if isinstance(widget, QwtPlot):
- self.plots = [widget]
- else:
- self.plots = widget.findChildren(QwtPlot)
- for index, plot in enumerate(self.plots):
- plot_name = plot.objectName()
- if not plot_name:
- plot_name = "Plot #%d" % (index + 1)
- widget = self.options.add_checkbox(
- plot_name, "Enable new flat style option", plot.setFlatStyle
- )
- widget.setChecked(plot.flatStyle())
- if len(self.plots) == 1:
- self.widget_of_interest = self.plots[0]
-
-
-def take_screenshot(widget):
- """Take screenshot and save it to the data folder"""
- if PYQT5:
- pixmap = widget.grab()
- else:
- pixmap = QPixmap.grabWidget(widget)
- bname = (widget.objectName().lower() + ".png").replace("window", "")
- bname = bname.replace("plot", "").replace("widget", "")
- pixmap.save(osp.join(TEST_PATH, "data", bname))
- QTimer.singleShot(0, QApplication.instance().quit)
-
-
-def test_widget(widget_class, size=None, title=None, options=True, timeout=1000):
- """Test widget"""
- widget_name = widget_class.__name__
- app = QApplication([])
- window = widget = widget_class()
- if options:
- if isinstance(widget, QMainWindow):
- widget = window.centralWidget()
- widget.setParent(None)
- else:
- window = QMainWindow()
- central_widget = TestCentralWidget(widget_name, parent=window)
- central_widget.add_widget(widget)
- window.setCentralWidget(central_widget)
- widget_of_interest = central_widget.widget_of_interest
- else:
- widget_of_interest = window
- widget_of_interest.setObjectName(widget_name)
- if title is None:
- from qwt import __version__
-
- title = 'Test "%s" - PythonQwt %s' % (widget_name, __version__)
- window.setWindowTitle(title)
- if size is not None:
- width, height = size
- window.resize(width, height)
-
- window.show()
- if os.environ.get("TEST_UNATTENDED") is not None:
- QTimer.singleShot(timeout, lambda: take_screenshot(widget_of_interest))
- app.exec_()
- return app
-
-
if __name__ == "__main__":
run()
diff --git a/qwt/tests/comparative_benchmarks.py b/qwt/tests/comparative_benchmarks.py
index 9f5753c..22c0bca 100644
--- a/qwt/tests/comparative_benchmarks.py
+++ b/qwt/tests/comparative_benchmarks.py
@@ -11,33 +11,52 @@
import os
import os.path as osp
-import sys
import subprocess
+import sys
import time
-def run_script(filename, args=None, wait=True):
+def get_winpython_exe(rootpath, pymajor=None, pyminor=None):
+ """Return WinPython exe list from rootpath"""
+ exelist = []
+ for name1 in os.listdir(rootpath):
+ winroot = osp.join(rootpath, name1)
+ if osp.isdir(winroot):
+ for name2 in os.listdir(winroot):
+ pypath = osp.join(winroot, name2, "python.exe")
+ if osp.isfile(pypath):
+ pymaj, pymin = name2[len("python-") :].split(".")[:2]
+ if pymajor is None or pymajor == int(pymaj):
+ if pyminor is None or int(pymin) >= pyminor:
+ exelist.append(pypath)
+ return exelist
+
+
+def run_script(filename, args=None, wait=True, executable=None):
"""Run Python script"""
- os.environ['PYTHONPATH'] = os.pathsep.join(sys.path)
-
- command = [sys.executable, '"'+filename+'"']
+ os.environ["PYTHONPATH"] = os.pathsep.join(sys.path)
+ if executable is None:
+ executable = sys.executable
+ command = [executable, '"' + filename + '"']
if args is not None:
command.append(args)
+ print(" ".join(command))
proc = subprocess.Popen(" ".join(command), shell=True)
if wait:
proc.wait()
def main():
- for name in ('CurveBenchmark.py', 'CurveStyles.py',):
- for args in (None, 'only_lines'):
- for value in ('', '1'):
- os.environ['USE_PYQWT5'] = value
- filename = osp.join(osp.dirname(osp.abspath(__file__)), name)
- run_script(filename, wait=False, args=args)
- time.sleep(4)
-
-
-if __name__ == '__main__':
+ for name in (
+ "curvebenchmark1.py",
+ "curvebenchmark2.py",
+ ):
+ for executable in get_winpython_exe(r"C:\Apps", pymajor=3, pyminor=6):
+ filename = osp.join(osp.dirname(osp.abspath(__file__)), name)
+ run_script(filename, wait=False, executable=executable)
+ time.sleep(4)
+
+
+if __name__ == "__main__":
+ # print(get_winpython_exe(r"C:\Apps", pymajor=3))
main()
-
\ No newline at end of file
diff --git a/qwt/tests/conftest.py b/qwt/tests/conftest.py
new file mode 100644
index 0000000..e60b9cd
--- /dev/null
+++ b/qwt/tests/conftest.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+
+"""pytest configuration for PythonQwt package tests."""
+
+import os
+
+import qtpy
+
+import qwt
+from qwt.tests.utils import TestEnvironment
+
+# Set the unattended environment variable to 1 to avoid any user interaction
+os.environ[TestEnvironment.UNATTENDED_ENV] = "1"
+
+
+def pytest_addoption(parser):
+ """Add custom command line options to pytest."""
+ # See this StackOverflow answer for more information: https://t.ly/9anqz
+ parser.addoption(
+ "--repeat", action="store", help="Number of times to repeat each test"
+ )
+ parser.addoption(
+ "--show-windows",
+ action="store_true",
+ default=False,
+ help="Display Qt windows during tests (disables QT_QPA_PLATFORM=offscreen)",
+ )
+
+
+def pytest_configure(config):
+ """Configure pytest based on command line options."""
+ if config.option.durations is None:
+ config.option.durations = 10 # Default to showing 10 slowest tests
+ if not config.getoption("--show-windows"):
+ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
+
+
+def pytest_report_header(config):
+ """Add additional information to the pytest report header."""
+ qtbindings_version = qtpy.PYSIDE_VERSION
+ if qtbindings_version is None:
+ qtbindings_version = qtpy.PYQT_VERSION
+ return [
+ f"PythonQwt {qwt.__version__} [closest Qwt version: {qwt.QWT_VERSION_STR}]",
+ f"{qtpy.API_NAME} {qtbindings_version} [Qt version: {qtpy.QT_VERSION}]",
+ ]
+
+
+def pytest_generate_tests(metafunc):
+ """Generate tests for the given metafunc."""
+ # See this StackOverflow answer for more information: https://t.ly/9anqz
+ if metafunc.config.option.repeat is not None:
+ count = int(metafunc.config.option.repeat)
+
+ # We're going to duplicate these tests by parametrizing them,
+ # which requires that each test has a fixture to accept the parameter.
+ # We can add a new fixture like so:
+ metafunc.fixturenames.append("tmp_ct")
+
+ # Now we parametrize. This is what happens when we do e.g.,
+ # @pytest.mark.parametrize('tmp_ct', range(count))
+ # def test_foo(): pass
+ metafunc.parametrize("tmp_ct", range(count))
diff --git a/qwt/tests/data/PythonQwt.svg b/qwt/tests/data/PythonQwt.svg
new file mode 100644
index 0000000..92bbe2c
--- /dev/null
+++ b/qwt/tests/data/PythonQwt.svg
@@ -0,0 +1,484 @@
+
+
+
+
diff --git a/qwt/tests/data/bodedemo.png b/qwt/tests/data/bodedemo.png
index 5267279..e3ca87e 100644
Binary files a/qwt/tests/data/bodedemo.png and b/qwt/tests/data/bodedemo.png differ
diff --git a/qwt/tests/data/cartesian.png b/qwt/tests/data/cartesian.png
index 222b151..449ac14 100644
Binary files a/qwt/tests/data/cartesian.png and b/qwt/tests/data/cartesian.png differ
diff --git a/qwt/tests/data/cpudemo.png b/qwt/tests/data/cpudemo.png
index 1d104dc..5f5660b 100644
Binary files a/qwt/tests/data/cpudemo.png and b/qwt/tests/data/cpudemo.png differ
diff --git a/qwt/tests/data/curvebenchmark1.png b/qwt/tests/data/curvebenchmark1.png
index 016833b..261dfe9 100644
Binary files a/qwt/tests/data/curvebenchmark1.png and b/qwt/tests/data/curvebenchmark1.png differ
diff --git a/qwt/tests/data/curvebenchmark2.png b/qwt/tests/data/curvebenchmark2.png
index 68239f1..162c27f 100644
Binary files a/qwt/tests/data/curvebenchmark2.png and b/qwt/tests/data/curvebenchmark2.png differ
diff --git a/qwt/tests/data/curvedemo1.png b/qwt/tests/data/curvedemo1.png
index 18d3192..8cc3e8e 100644
Binary files a/qwt/tests/data/curvedemo1.png and b/qwt/tests/data/curvedemo1.png differ
diff --git a/qwt/tests/data/curvedemo2.png b/qwt/tests/data/curvedemo2.png
index aa8800c..b060f67 100644
Binary files a/qwt/tests/data/curvedemo2.png and b/qwt/tests/data/curvedemo2.png differ
diff --git a/qwt/tests/data/data.png b/qwt/tests/data/data.png
index 5d89098..f7ae40c 100644
Binary files a/qwt/tests/data/data.png and b/qwt/tests/data/data.png differ
diff --git a/qwt/tests/data/errorbar.png b/qwt/tests/data/errorbar.png
index ab58de6..bea587f 100644
Binary files a/qwt/tests/data/errorbar.png and b/qwt/tests/data/errorbar.png differ
diff --git a/qwt/tests/data/eventfilter.png b/qwt/tests/data/eventfilter.png
index 68e9ed3..5dbcc1b 100644
Binary files a/qwt/tests/data/eventfilter.png and b/qwt/tests/data/eventfilter.png differ
diff --git a/qwt/tests/data/image.png b/qwt/tests/data/image.png
index 82a17b8..d211916 100644
Binary files a/qwt/tests/data/image.png and b/qwt/tests/data/image.png differ
diff --git a/qwt/tests/data/loadtest.png b/qwt/tests/data/loadtest.png
new file mode 100644
index 0000000..8b4b8e5
Binary files /dev/null and b/qwt/tests/data/loadtest.png differ
diff --git a/qwt/tests/data/logcurve.png b/qwt/tests/data/logcurve.png
index c3f9d79..e81de61 100644
Binary files a/qwt/tests/data/logcurve.png and b/qwt/tests/data/logcurve.png differ
diff --git a/qwt/tests/data/mapdemo.png b/qwt/tests/data/mapdemo.png
index ea69b76..623a93e 100644
Binary files a/qwt/tests/data/mapdemo.png and b/qwt/tests/data/mapdemo.png differ
diff --git a/qwt/tests/data/multidemo.png b/qwt/tests/data/multidemo.png
index 836c805..7f7b8a7 100644
Binary files a/qwt/tests/data/multidemo.png and b/qwt/tests/data/multidemo.png differ
diff --git a/qwt/tests/data/simple.png b/qwt/tests/data/simple.png
index 548745f..2aa8593 100644
Binary files a/qwt/tests/data/simple.png and b/qwt/tests/data/simple.png differ
diff --git a/qwt/tests/data/stylesheet.png b/qwt/tests/data/stylesheet.png
new file mode 100644
index 0000000..576a43e
Binary files /dev/null and b/qwt/tests/data/stylesheet.png differ
diff --git a/qwt/tests/data/symbol.svg b/qwt/tests/data/symbol.svg
new file mode 100644
index 0000000..146b0be
--- /dev/null
+++ b/qwt/tests/data/symbol.svg
@@ -0,0 +1,411 @@
+
+
+
+
diff --git a/qwt/tests/data/symbols.png b/qwt/tests/data/symbols.png
new file mode 100644
index 0000000..17cb695
Binary files /dev/null and b/qwt/tests/data/symbols.png differ
diff --git a/qwt/tests/data/testlauncher.png b/qwt/tests/data/testlauncher.png
index a573cf6..df1bf76 100644
Binary files a/qwt/tests/data/testlauncher.png and b/qwt/tests/data/testlauncher.png differ
diff --git a/qwt/tests/data/vertical.png b/qwt/tests/data/vertical.png
index 0c4e4f9..21a981d 100644
Binary files a/qwt/tests/data/vertical.png and b/qwt/tests/data/vertical.png differ
diff --git a/qwt/tests/test_backingstore.py b/qwt/tests/test_backingstore.py
new file mode 100644
index 0000000..4a0467f
--- /dev/null
+++ b/qwt/tests/test_backingstore.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+SHOW = False # Do not show test in GUI-based test launcher
+
+from qwt.tests import utils
+from qwt.tests.test_simple import SimplePlot
+
+
+class BackingStorePlot(SimplePlot):
+ TEST_EXPORT = False
+
+ def __init__(self):
+ SimplePlot.__init__(self)
+ self.canvas().setPaintAttribute(self.canvas().BackingStore, True)
+
+
+def test_backingstore():
+ """Test for backing store"""
+ utils.test_widget(BackingStorePlot, size=(600, 400))
+
+
+if __name__ == "__main__":
+ test_backingstore()
diff --git a/qwt/tests/bodedemo.py b/qwt/tests/test_bodedemo.py
similarity index 86%
rename from qwt/tests/bodedemo.py
rename to qwt/tests/test_bodedemo.py
index 84a5b34..2bbff41 100644
--- a/qwt/tests/bodedemo.py
+++ b/qwt/tests/test_bodedemo.py
@@ -6,42 +6,36 @@
# developments (e.g. ported to PythonQwt API)
# (see LICENSE file for more details)
-from __future__ import unicode_literals
-
SHOW = True # Show test in GUI-based test launcher
-import numpy as np
+import os
-from qwt.qt.QtGui import (
- QPen,
- QBrush,
+import numpy as np
+from qtpy.QtCore import Qt
+from qtpy.QtGui import QFont, QIcon, QPageLayout, QPen, QPixmap
+from qtpy.QtPrintSupport import QPrintDialog, QPrinter
+from qtpy.QtWidgets import (
QFrame,
- QFont,
- QWidget,
- QMainWindow,
- QToolButton,
- QIcon,
- QPixmap,
- QToolBar,
QHBoxLayout,
QLabel,
- QPrinter,
- QPrintDialog,
+ QMainWindow,
+ QToolBar,
+ QToolButton,
+ QWidget,
)
-from qwt.qt.QtCore import QSize, Qt
+
from qwt import (
+ QwtLegend,
+ QwtLogScaleEngine,
QwtPlot,
+ QwtPlotCurve,
+ QwtPlotGrid,
QwtPlotMarker,
+ QwtPlotRenderer,
QwtSymbol,
- QwtLegend,
- QwtPlotGrid,
- QwtPlotCurve,
- QwtPlotItem,
- QwtLogScaleEngine,
QwtText,
- QwtPlotRenderer,
)
-
+from qwt.tests import utils
print_xpm = [
"32 32 12 1",
@@ -150,8 +144,7 @@ def __init__(self, *args):
yvalue=-20.0,
align=Qt.AlignRight | Qt.AlignBottom,
label=QwtText.make(
- "[1-(\u03c9/\u03c90)2+2j\u03c9/Q]"
- "-1",
+ "[1-(\u03c9/\u03c90)2+2j\u03c9/Q]-1",
color=Qt.white,
borderradius=2,
borderpen=QPen(Qt.lightGray, 5),
@@ -199,6 +192,9 @@ def setDamp(self, d):
self.replot()
+FNAME_PDF = "bode.pdf"
+
+
class BodeDemo(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
@@ -242,12 +238,23 @@ def __init__(self, *args):
self.showInfo()
- def print_(self):
- printer = QPrinter(QPrinter.HighResolution)
+ if utils.TestEnvironment().unattended:
+ self.print_(unattended=True)
+
+ def print_(self, unattended=False):
+ try:
+ mode = QPrinter.HighResolution
+ printer = QPrinter(mode)
+ except AttributeError:
+ # Some PySide6 / PyQt6 versions do not have this attribute on Linux
+ printer = QPrinter()
printer.setCreator("Bode example")
- printer.setOrientation(QPrinter.Landscape)
- printer.setColorMode(QPrinter.Color)
+ printer.setPageOrientation(QPageLayout.Landscape)
+ try:
+ printer.setColorMode(QPrinter.Color)
+ except AttributeError:
+ pass
docName = str(self.plot.title().text())
if not docName:
@@ -255,13 +262,16 @@ def print_(self):
printer.setDocName(docName)
dialog = QPrintDialog(printer)
- if dialog.exec_():
+ if unattended:
+ # Configure QPrinter object to print to PDF file
+ printer.setPrinterName("")
+ printer.setOutputFileName(FNAME_PDF)
+ dialog.accept()
+ ok = True
+ else:
+ ok = dialog.exec_()
+ if ok:
renderer = QwtPlotRenderer()
- if QPrinter.GrayScale == printer.colorMode():
- renderer.setDiscardFlag(QwtPlotRenderer.DiscardBackground)
- renderer.setDiscardFlag(QwtPlotRenderer.DiscardCanvasBackground)
- renderer.setDiscardFlag(QwtPlotRenderer.DiscardCanvasFrame)
- renderer.setLayoutFlag(QwtPlotRenderer.FrameWithScales)
renderer.renderTo(self.plot, printer)
def exportDocument(self):
@@ -283,8 +293,12 @@ def selected(self, _):
self.showInfo()
-if __name__ == "__main__":
- from qwt.tests import test_widget
- import os
+def test_bodedemo():
+ """Bode demo"""
+ utils.test_widget(BodeDemo, (640, 480))
+ if os.path.isfile(FNAME_PDF):
+ os.remove(FNAME_PDF)
+
- app = test_widget(BodeDemo, (640, 480))
+if __name__ == "__main__":
+ test_bodedemo()
diff --git a/qwt/tests/cartesian.py b/qwt/tests/test_cartesian.py
similarity index 90%
rename from qwt/tests/cartesian.py
rename to qwt/tests/test_cartesian.py
index 7b532f1..507b551 100644
--- a/qwt/tests/cartesian.py
+++ b/qwt/tests/test_cartesian.py
@@ -9,14 +9,14 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import Qt
-from qwt.qt.QtGui import QPen
-from qwt.qt.QtCore import Qt
-from qwt import QwtPlot, QwtScaleDraw, QwtPlotGrid, QwtPlotCurve, QwtPlotItem
+from qwt import QwtPlot, QwtPlotCurve, QwtPlotGrid, QwtPlotItem, QwtScaleDraw
+from qwt.tests import utils
class CartesianAxis(QwtPlotItem):
- """Supports a coordinate system similar to
+ """Supports a coordinate system similar to
http://en.wikipedia.org/wiki/Image:Cartesian-coordinate-system.svg"""
def __init__(self, masterAxis, slaveAxis):
@@ -54,7 +54,7 @@ def draw(self, painter, xMap, yMap, rect):
class CartesianPlot(QwtPlot):
- """Creates a coordinate system similar system
+ """Creates a coordinate system similar system
http://en.wikipedia.org/wiki/Image:Cartesian-coordinate-system.svg"""
def __init__(self, *args):
@@ -100,7 +100,10 @@ def __init__(self, *args):
self.replot()
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_cartesian():
+ """Cartesian plot test"""
+ utils.test_widget(CartesianPlot, (800, 480))
+
- test_widget(CartesianPlot, (800, 480))
+if __name__ == "__main__":
+ test_cartesian()
diff --git a/qwt/tests/cpudemo.py b/qwt/tests/test_cpudemo.py
similarity index 89%
rename from qwt/tests/cpudemo.py
rename to qwt/tests/test_cpudemo.py
index 87bd285..18ccdf7 100644
--- a/qwt/tests/cpudemo.py
+++ b/qwt/tests/test_cpudemo.py
@@ -9,23 +9,23 @@
SHOW = True # Show test in GUI-based test launcher
import os
+
import numpy as np
+from qtpy.QtCore import QRectF, Qt, QTime
+from qtpy.QtGui import QBrush, QColor
+from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget
-from qwt.qt.QtGui import QColor, QBrush, QWidget, QVBoxLayout, QLabel
-from qwt.qt.QtCore import QRect, QTime, Qt
from qwt import (
- QwtPlot,
- QwtPlotMarker,
- QwtScaleDraw,
QwtLegend,
+ QwtLegendData,
+ QwtPlot,
QwtPlotCurve,
QwtPlotItem,
- QwtLegendData,
+ QwtPlotMarker,
+ QwtScaleDraw,
QwtText,
)
-
-TIMER_INTERVAL = 1000
-SHOW_ALL_CURVES = False
+from qwt.tests import utils
class CpuStat:
@@ -195,17 +195,18 @@ def statistic(self):
return 100.0 * userDelta / totalDelta, 100.0 * systemDelta / totalDelta
def upTime(self):
- result = QTime()
+ result = QTime(0, 0, 0)
for item in self.procValues:
result = result.addSecs(int(0.01 * item))
return result
def __lookup(self):
if os.path.exists("/proc/stat"):
- for line in open("/proc/stat"):
- words = line.split()
- if words[0] == "cpu" and len(words) >= 5:
- return [float(w) for w in words[1:]]
+ with open("/proc/stat") as file:
+ for line in file:
+ words = line.split()
+ if words[0] == "cpu" and len(words) >= 5:
+ return [float(w) for w in words[1:]]
else:
result = CpuStat.dummyValues[CpuStat.counter]
CpuStat.counter += 1
@@ -224,7 +225,7 @@ def rtti(self):
def draw(self, painter, xMap, yMap, rect):
margin = 5
- pieRect = QRect()
+ pieRect = QRectF()
pieRect.setX(rect.x() + margin)
pieRect.setY(rect.y() + margin)
pieRect.setHeight(int(yMap.transform(80.0)))
@@ -262,7 +263,7 @@ def rtti(self):
def draw(self, painter, xMap, yMap, rect):
c = QColor(Qt.white)
- r = QRect(rect)
+ r = QRectF(rect)
for i in range(100, 0, -10):
r.setBottom(int(yMap.transform(i - 10)))
@@ -284,16 +285,15 @@ def setColor(self, color):
self.setBrush(c)
-HISTORY = 60
-
-
class CpuPlot(QwtPlot):
- def __init__(self, *args):
- QwtPlot.__init__(self, *args)
+ HISTORY = 60
+
+ def __init__(self, unattended=False):
+ QwtPlot.__init__(self)
self.curves = {}
self.data = {}
- self.timeData = 1.0 * np.arange(HISTORY - 1, -1, -1)
+ self.timeData = 1.0 * np.arange(self.HISTORY - 1, -1, -1)
self.cpuStat = CpuStat()
self.setAutoReplot(False)
@@ -306,7 +306,7 @@ def __init__(self, *args):
self.setAxisTitle(QwtPlot.xBottom, "System Uptime [h:m:s]")
self.setAxisScaleDraw(QwtPlot.xBottom, TimeScaleDraw(self.cpuStat.upTime()))
- self.setAxisScale(QwtPlot.xBottom, 0, HISTORY)
+ self.setAxisScale(QwtPlot.xBottom, 0, self.HISTORY)
self.setAxisLabelRotation(QwtPlot.xBottom, -50.0)
self.setAxisLabelAlignment(QwtPlot.xBottom, Qt.AlignLeft | Qt.AlignBottom)
@@ -323,35 +323,35 @@ def __init__(self, *args):
curve.setColor(Qt.red)
curve.attach(self)
self.curves["System"] = curve
- self.data["System"] = np.zeros(HISTORY, np.float)
+ self.data["System"] = np.zeros(self.HISTORY, float)
curve = CpuCurve("User")
curve.setColor(Qt.blue)
curve.setZ(curve.z() - 1.0)
curve.attach(self)
self.curves["User"] = curve
- self.data["User"] = np.zeros(HISTORY, np.float)
+ self.data["User"] = np.zeros(self.HISTORY, float)
curve = CpuCurve("Total")
curve.setColor(Qt.black)
curve.setZ(curve.z() - 2.0)
curve.attach(self)
self.curves["Total"] = curve
- self.data["Total"] = np.zeros(HISTORY, np.float)
+ self.data["Total"] = np.zeros(self.HISTORY, float)
curve = CpuCurve("Idle")
curve.setColor(Qt.darkCyan)
curve.setZ(curve.z() - 3.0)
curve.attach(self)
self.curves["Idle"] = curve
- self.data["Idle"] = np.zeros(HISTORY, np.float)
+ self.data["Idle"] = np.zeros(self.HISTORY, float)
self.showCurve(self.curves["System"], True)
self.showCurve(self.curves["User"], True)
- self.showCurve(self.curves["Total"], False or SHOW_ALL_CURVES)
- self.showCurve(self.curves["Idle"], False or SHOW_ALL_CURVES)
+ self.showCurve(self.curves["Total"], False or unattended)
+ self.showCurve(self.curves["Idle"], False or unattended)
- self.startTimer(TIMER_INTERVAL)
+ self.startTimer(20 if unattended else 1000)
legend.checked.connect(self.showCurve)
self.replot()
@@ -381,21 +381,21 @@ def cpuPlotCurve(self, key):
class CpuDemo(QWidget):
- def __init__(self, parent=None):
+ def __init__(self, parent=None, unattended=False):
super(CpuDemo, self).__init__(parent)
layout = QVBoxLayout()
self.setLayout(layout)
- plot = CpuPlot()
+ plot = CpuPlot(unattended=unattended)
plot.setTitle("History")
layout.addWidget(plot)
label = QLabel("Press the legend to en/disable a curve")
layout.addWidget(label)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_cpudemo():
+ """CPU demo"""
+ utils.test_widget(CpuDemo, (600, 400))
- if os.environ.get("TEST_UNATTENDED") is not None:
- SHOW_ALL_CURVES = True
- TIMER_INTERVAL = 20
- app = test_widget(CpuDemo, (600, 400))
+
+if __name__ == "__main__":
+ test_cpudemo()
diff --git a/qwt/tests/curvebenchmark1.py b/qwt/tests/test_curvebenchmark1.py
similarity index 76%
rename from qwt/tests/curvebenchmark1.py
rename to qwt/tests/test_curvebenchmark1.py
index 48c0e05..8032fa5 100644
--- a/qwt/tests/curvebenchmark1.py
+++ b/qwt/tests/test_curvebenchmark1.py
@@ -9,28 +9,21 @@
SHOW = True # Show test in GUI-based test launcher
import time
-import numpy as np
-from qwt.qt.QtGui import (
+import numpy as np
+from qtpy.QtCore import Qt
+from qtpy.QtWidgets import (
QApplication,
- QMainWindow,
QGridLayout,
+ QLineEdit,
+ QMainWindow,
QTabWidget,
- QWidget,
QTextEdit,
- QLineEdit,
+ QWidget,
)
-from qwt.qt.QtCore import Qt
-
-import os
-
-if os.environ.get("USE_PYQWT5", False):
- USE_PYQWT5 = True
- from PyQt4.Qwt5 import QwtPlot, QwtPlotCurve
-else:
- USE_PYQWT5 = False
- from qwt import QwtPlot, QwtPlotCurve # analysis:ignore
+from qwt import QwtPlot, QwtPlotCurve
+from qwt.tests import utils
COLOR_INDEX = None
@@ -45,11 +38,16 @@ def get_curve_color():
return colors[COLOR_INDEX]
+PLOT_ID = 0
+
+
class BMPlot(QwtPlot):
def __init__(self, title, xdata, ydata, style, symbol=None, *args):
super(BMPlot, self).__init__(*args)
- self.setMinimumSize(200, 200)
- self.setTitle(title)
+ global PLOT_ID
+ self.setMinimumSize(200, 150)
+ PLOT_ID += 1
+ self.setTitle("%s (#%d)" % (title, PLOT_ID))
self.setAxisTitle(QwtPlot.xBottom, "x")
self.setAxisTitle(QwtPlot.yLeft, "y")
self.curve_nb = 0
@@ -68,11 +66,11 @@ def __init__(self, title, xdata, ydata, style, symbol=None, *args):
class BMWidget(QWidget):
- def __init__(self, points, *args, **kwargs):
+ def __init__(self, nbcol, points, *args, **kwargs):
super(BMWidget, self).__init__()
self.plot_nb = 0
self.curve_nb = 0
- self.setup(points, *args, **kwargs)
+ self.setup(nbcol, points, *args, **kwargs)
def params(self, *args, **kwargs):
if kwargs.get("only_lines", False):
@@ -83,11 +81,11 @@ def params(self, *args, **kwargs):
("Dots", None),
)
- def setup(self, points, *args, **kwargs):
+ def setup(self, nbcol, points, *args, **kwargs):
x = np.linspace(0.001, 20.0, int(points))
y = (np.sin(x) / x) * np.cos(20 * x)
layout = QGridLayout()
- nbcol, col, row = 2, 0, 0
+ col, row = 0, 0
for style, symbol in self.params(*args, **kwargs):
plot = BMPlot(style, x, y, getattr(QwtPlotCurve, style), symbol=symbol)
layout.addWidget(plot, row, col)
@@ -101,7 +99,7 @@ def setup(self, points, *args, **kwargs):
self.text.setReadOnly(True)
self.text.setAlignment(Qt.AlignCenter)
self.text.setText("Rendering plot...")
- layout.addWidget(self.text, row + 1, 0, 1, 2)
+ layout.addWidget(self.text, row + 1, 0, 1, nbcol)
self.setLayout(layout)
@@ -109,7 +107,7 @@ class BMText(QTextEdit):
def __init__(self, parent=None, title=None):
super(BMText, self).__init__(parent)
self.setReadOnly(True)
- library = "PyQwt5" if USE_PYQWT5 else "PythonQwt"
+ library = "PythonQwt"
wintitle = self.parent().windowTitle()
if not wintitle:
wintitle = "Benchmark"
@@ -120,7 +118,7 @@ def __init__(self, parent=None, title=None):
"""\
%s:
(base plotting library: %s)
-Click on each tab to test if plotting performance is acceptable in terms of
+Click on each tab to test if plotting performance is acceptable in terms of
GUI response time (switch between tabs, resize main windows, ...).
Benchmarks results:
@@ -133,7 +131,7 @@ class CurveBenchmark1(QMainWindow):
TITLE = "Curve benchmark"
SIZE = (1000, 500)
- def __init__(self, max_n=1000000, parent=None, **kwargs):
+ def __init__(self, max_n=1000000, parent=None, unattended=False, **kwargs):
super(CurveBenchmark1, self).__init__(parent=parent)
title = self.TITLE
if kwargs.get("only_lines", False):
@@ -144,19 +142,17 @@ def __init__(self, max_n=1000000, parent=None, **kwargs):
self.text = BMText(self)
self.tabs.addTab(self.text, "Contents")
self.resize(*self.SIZE)
+ self.durations = []
# Force window to show up and refresh (for test purpose only)
self.show()
QApplication.processEvents()
t0g = time.time()
- self.run_benchmark(max_n, **kwargs)
+ self.run_benchmark(max_n, unattended, **kwargs)
dt = time.time() - t0g
self.text.append("
Total elapsed time: %d ms" % (dt * 1e3))
- if os.environ.get("TEST_UNATTENDED") is None:
- self.tabs.setCurrentIndex(0)
- else:
- self.tabs.setCurrentIndex(1)
+ self.tabs.setCurrentIndex(1 if unattended else 0)
def process_iteration(self, title, description, widget, t0):
self.tabs.addTab(widget, title)
@@ -165,15 +161,20 @@ def process_iteration(self, title, description, widget, t0):
# Force widget to refresh (for test purpose only)
QApplication.processEvents()
- time_str = "Elapsed time: %d ms" % ((time.time() - t0) * 1000)
+ duration = (time.time() - t0) * 1000
+ self.durations.append(duration)
+ time_str = "Elapsed time: %d ms" % duration
widget.text.setText(time_str)
self.text.append("
%s:
%s" % (description, time_str))
+ print("[%s] %s" % (utils.get_lib_versions(), time_str))
- def run_benchmark(self, max_n, **kwargs):
- for idx in range(4, -1, -1):
- points = int(max_n / 10 ** idx)
+ def run_benchmark(self, max_n, unattended, **kwargs):
+ max_n = 1000 if unattended else max_n
+ iterations = 0 if unattended else 4
+ for idx in range(iterations, -1, -1):
+ points = int(max_n / 10**idx)
t0 = time.time()
- widget = BMWidget(points, **kwargs)
+ widget = BMWidget(2, points, **kwargs)
title = "%d points" % points
description = "%d plots with %d curves of %d points" % (
widget.plot_nb,
@@ -183,7 +184,10 @@ def run_benchmark(self, max_n, **kwargs):
self.process_iteration(title, description, widget, t0)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_curvebenchmark1():
+ """Curve benchmark example"""
+ utils.test_widget(CurveBenchmark1, options=False)
- app = test_widget(CurveBenchmark1, options=False)
+
+if __name__ == "__main__":
+ test_curvebenchmark1()
diff --git a/qwt/tests/curvebenchmark2.py b/qwt/tests/test_curvebenchmark2.py
similarity index 77%
rename from qwt/tests/curvebenchmark2.py
rename to qwt/tests/test_curvebenchmark2.py
index 5d650d9..c8ded00 100644
--- a/qwt/tests/curvebenchmark2.py
+++ b/qwt/tests/test_curvebenchmark2.py
@@ -10,15 +10,11 @@
import time
-from qwt.qt.QtGui import QPen, QBrush
-from qwt.qt.QtCore import QSize, Qt
+from qtpy.QtCore import Qt
-from qwt.tests import curvebenchmark1 as cb
-
-if cb.USE_PYQWT5:
- from PyQt4.Qwt5 import QwtSymbol
-else:
- from qwt import QwtSymbol # analysis:ignore
+from qwt import QwtSymbol
+from qwt.tests import test_curvebenchmark1 as cb
+from qwt.tests import utils
class CSWidget(cb.BMWidget):
@@ -64,16 +60,18 @@ class CurveBenchmark2(cb.CurveBenchmark1):
TITLE = "Curve styles"
SIZE = (1000, 800)
- def __init__(self, max_n=1000, parent=None, **kwargs):
- super(CurveBenchmark2, self).__init__(max_n=max_n, parent=parent, **kwargs)
+ def __init__(self, max_n=1000, parent=None, unattended=False, **kwargs):
+ super(CurveBenchmark2, self).__init__(
+ max_n=max_n, parent=parent, unattended=unattended, **kwargs
+ )
- def run_benchmark(self, max_n, **kwargs):
+ def run_benchmark(self, max_n, unattended, **kwargs):
for points, symbols in zip(
(max_n / 10, max_n / 10, max_n, max_n), (True, False) * 2
):
t0 = time.time()
symtext = "with%s symbols" % ("" if symbols else "out")
- widget = CSWidget(points, symbols, **kwargs)
+ widget = CSWidget(2, points, symbols, **kwargs)
title = "%d points" % points
description = "%d plots with %d curves of %d points, %s" % (
widget.plot_nb,
@@ -84,7 +82,10 @@ def run_benchmark(self, max_n, **kwargs):
self.process_iteration(title, description, widget, t0)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_curvebenchmark2():
+ """Curve styles benchmark example"""
+ utils.test_widget(CurveBenchmark2, options=False)
- app = test_widget(CurveBenchmark2, options=False)
+
+if __name__ == "__main__":
+ test_curvebenchmark2()
diff --git a/qwt/tests/curvedemo1.py b/qwt/tests/test_curvedemo1.py
similarity index 91%
rename from qwt/tests/curvedemo1.py
rename to qwt/tests/test_curvedemo1.py
index 812fc18..0f37949 100644
--- a/qwt/tests/curvedemo1.py
+++ b/qwt/tests/test_curvedemo1.py
@@ -9,11 +9,12 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import QSize, Qt
+from qtpy.QtGui import QBrush, QFont, QPainter, QPen
+from qtpy.QtWidgets import QFrame
-from qwt.qt.QtGui import QPen, QBrush, QFrame, QFont, QPainter, QPaintEngine
-from qwt.qt.QtCore import QSize
-from qwt.qt.QtCore import Qt
-from qwt import QwtSymbol, QwtPlotCurve, QwtPlotItem, QwtScaleMap
+from qwt import QwtPlotCurve, QwtPlotItem, QwtScaleMap, QwtSymbol
+from qwt.tests import utils
class CurveDemo1(QFrame):
@@ -118,7 +119,10 @@ def drawContents(self, painter):
self.shiftDown(r, dy)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_curvedemo1():
+ """Curve demo 1"""
+ utils.test_widget(CurveDemo1, size=(300, 600), options=False)
+
- app = test_widget(CurveDemo1, size=(300, 600), options=False)
+if __name__ == "__main__":
+ test_curvedemo1()
diff --git a/qwt/tests/curvedemo2.py b/qwt/tests/test_curvedemo2.py
similarity index 91%
rename from qwt/tests/curvedemo2.py
rename to qwt/tests/test_curvedemo2.py
index b8d25af..ee2ec01 100644
--- a/qwt/tests/curvedemo2.py
+++ b/qwt/tests/test_curvedemo2.py
@@ -9,11 +9,12 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import QSize, Qt
+from qtpy.QtGui import QBrush, QColor, QPainter, QPalette, QPen
+from qtpy.QtWidgets import QFrame
-from qwt.qt.QtGui import QPen, QBrush, QFrame, QColor, QPainter, QPalette
-from qwt.qt.QtCore import QSize
-from qwt.qt.QtCore import Qt
-from qwt import QwtScaleMap, QwtSymbol, QwtPlotCurve
+from qwt import QwtPlotCurve, QwtScaleMap, QwtSymbol
+from qwt.tests import utils
Size = 15
USize = 13
@@ -117,7 +118,10 @@ def newValues(self):
self.phase = 0.0
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_curvedemo2():
+ """Curve demo 2"""
+ utils.test_widget(CurveDemo2, options=False)
+
- app = test_widget(CurveDemo2, options=False)
+if __name__ == "__main__":
+ test_curvedemo2()
diff --git a/qwt/tests/data.py b/qwt/tests/test_data.py
similarity index 84%
rename from qwt/tests/data.py
rename to qwt/tests/test_data.py
index 3116696..99ccd46 100644
--- a/qwt/tests/data.py
+++ b/qwt/tests/test_data.py
@@ -9,33 +9,34 @@
SHOW = True # Show test in GUI-based test launcher
import random
+
import numpy as np
+from qtpy.QtCore import QSize, Qt
+from qtpy.QtGui import QBrush, QPen
+from qtpy.QtWidgets import QFrame
-from qwt.qt.QtGui import QPen, QBrush, QFrame
-from qwt.qt.QtCore import QSize, Qt
from qwt import (
+ QwtAbstractScaleDraw,
+ QwtLegend,
QwtPlot,
+ QwtPlotCurve,
QwtPlotMarker,
QwtSymbol,
- QwtLegend,
- QwtPlotCurve,
- QwtAbstractScaleDraw,
)
-
-TIMER_INTERVAL = 50
+from qwt.tests import utils
class DataPlot(QwtPlot):
- def __init__(self, *args):
- QwtPlot.__init__(self, *args)
+ def __init__(self, unattended=False):
+ QwtPlot.__init__(self)
self.setCanvasBackground(Qt.white)
self.alignScales()
# Initialize data
self.x = np.arange(0.0, 100.1, 0.5)
- self.y = np.zeros(len(self.x), np.float)
- self.z = np.zeros(len(self.x), np.float)
+ self.y = np.zeros(len(self.x), float)
+ self.z = np.zeros(len(self.x), float)
self.setTitle("A Moving QwtPlot Demonstration")
self.insertLegend(QwtLegend(), QwtPlot.BottomLegend)
@@ -61,7 +62,7 @@ def __init__(self, *args):
self.setAxisTitle(QwtPlot.xBottom, "Time (seconds)")
self.setAxisTitle(QwtPlot.yLeft, "Values")
- self.startTimer(TIMER_INTERVAL)
+ self.startTimer(10 if unattended else 50)
self.phase = 0.0
def alignScales(self):
@@ -96,10 +97,10 @@ def timerEvent(self, e):
self.phase += np.pi * 0.02
-if __name__ == "__main__":
- from qwt.tests import test_widget
- import os
+def test_data():
+ """Data Test"""
+ utils.test_widget(DataPlot, size=(500, 300))
- if os.environ.get("TEST_UNATTENDED") is not None:
- TIMER_INTERVAL = 10
- app = test_widget(DataPlot, size=(500, 300))
+
+if __name__ == "__main__":
+ test_data()
diff --git a/qwt/tests/errorbar.py b/qwt/tests/test_errorbar.py
similarity index 92%
rename from qwt/tests/errorbar.py
rename to qwt/tests/test_errorbar.py
index 98e692d..55f2fcc 100644
--- a/qwt/tests/errorbar.py
+++ b/qwt/tests/test_errorbar.py
@@ -9,10 +9,11 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import QLineF, QRectF, QSize, Qt
+from qtpy.QtGui import QBrush, QPen
-from qwt.qt.QtGui import QPen, QBrush
-from qwt.qt.QtCore import QSize, QRectF, QLineF, Qt
-from qwt import QwtPlot, QwtSymbol, QwtPlotGrid, QwtPlotCurve
+from qwt import QwtPlot, QwtPlotCurve, QwtPlotGrid, QwtSymbol
+from qwt.tests import utils
class ErrorBarPlotCurve(QwtPlotCurve):
@@ -42,15 +43,15 @@ def __init__(
(x-dx[0], x+dx[1]) or (y-dy[0], y+dy[1]).
curvePen is the pen used to plot the curve
-
+
curveStyle is the style used to plot the curve
-
+
curveSymbol is the symbol used to plot the symbols
-
+
errorPen is the pen used to plot the error bars
-
+
errorCap is the size of the error bar caps
-
+
errorOnTop is a boolean:
- if True, plot the error bars on top of the curve,
- if False, plot the curve on top of the error bars.
@@ -100,11 +101,11 @@ def setData(self, *args):
if len(args) > 3:
dy = args[3]
- self.__x = np.asarray(x, np.float)
+ self.__x = np.asarray(x, float)
if len(self.__x.shape) != 1:
raise RuntimeError("len(asarray(x).shape) != 1")
- self.__y = np.asarray(y, np.float)
+ self.__y = np.asarray(y, float)
if len(self.__y.shape) != 1:
raise RuntimeError("len(asarray(y).shape) != 1")
if len(self.__x) != len(self.__y):
@@ -113,22 +114,21 @@ def setData(self, *args):
if dx is None:
self.__dx = None
else:
- self.__dx = np.asarray(dx, np.float)
+ self.__dx = np.asarray(dx, float)
if len(self.__dx.shape) not in [0, 1, 2]:
raise RuntimeError("len(asarray(dx).shape) not in [0, 1, 2]")
if dy is None:
self.__dy = dy
else:
- self.__dy = np.asarray(dy, np.float)
+ self.__dy = np.asarray(dy, float)
if len(self.__dy.shape) not in [0, 1, 2]:
raise RuntimeError("len(asarray(dy).shape) not in [0, 1, 2]")
QwtPlotCurve.setData(self, self.__x, self.__y)
def boundingRect(self):
- """Return the bounding rectangle of the data, error bars included.
- """
+ """Return the bounding rectangle of the data, error bars included."""
if self.__dx is None:
xmin = min(self.__x)
xmax = max(self.__x)
@@ -159,7 +159,7 @@ def drawSeries(self, painter, xMap, yMap, canvasRect, first, last=-1):
xMap is the QwtDiMap used to map x-values to pixels
yMap is the QwtDiMap used to map y-values to pixels
-
+
first is the index of the first data point to draw
last is the index of the last data point to draw. If last < 0, last
@@ -198,7 +198,13 @@ def drawSeries(self, painter, xMap, yMap, canvasRect, first, last=-1):
if self.errorCap > 0:
# draw the caps
cap = self.errorCap / 2
- n, i, = len(y), 0
+ (
+ n,
+ i,
+ ) = (
+ len(y),
+ 0,
+ )
lines = []
while i < n:
yi = yMap.transform(y[i])
@@ -231,7 +237,13 @@ def drawSeries(self, painter, xMap, yMap, canvasRect, first, last=-1):
ymin = self.__y - self.__dy[0]
ymax = self.__y + self.__dy[1]
x = self.__x
- n, i, = len(x), 0
+ (
+ n,
+ i,
+ ) = (
+ len(x),
+ 0,
+ )
lines = []
while i < n:
xi = xMap.transform(x[i])
@@ -243,7 +255,7 @@ def drawSeries(self, painter, xMap, yMap, canvasRect, first, last=-1):
# draw the caps
if self.errorCap > 0:
cap = self.errorCap / 2
- n, i, j = len(x), 0, 0
+ n, i, _j = len(x), 0, 0
lines = []
while i < n:
xi = xMap.transform(x[i])
@@ -282,7 +294,7 @@ def __init__(self, parent=None, title=None):
grid.setPen(QPen(Qt.black, 0, Qt.DotLine))
# calculate data and errors for a curve with error bars
- x = np.arange(0, 10.1, 0.5, np.float)
+ x = np.arange(0, 10.1, 0.5, float)
y = np.sin(x)
dy = 0.2 * abs(y)
# dy = (0.15 * abs(y), 0.25 * abs(y)) # uncomment for asymmetric error bars
@@ -306,7 +318,10 @@ def __init__(self, parent=None, title=None):
curve.attach(self)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_errorbar():
+ """Errorbar plot example"""
+ utils.test_widget(ErrorBarPlot, size=(640, 480))
- app = test_widget(ErrorBarPlot, size=(640, 480))
+
+if __name__ == "__main__":
+ test_errorbar()
diff --git a/qwt/tests/eventfilter.py b/qwt/tests/test_eventfilter.py
similarity index 91%
rename from qwt/tests/eventfilter.py
rename to qwt/tests/test_eventfilter.py
index 124604f..64fa1d7 100644
--- a/qwt/tests/eventfilter.py
+++ b/qwt/tests/test_eventfilter.py
@@ -8,31 +8,25 @@
SHOW = True # Show test in GUI-based test launcher
+import os
+
import numpy as np
+from qtpy.QtCore import QEvent, QObject, QPoint, QRect, QSize, Qt, Signal
+from qtpy.QtGui import QBrush, QColor, QPainter, QPen
+from qtpy.QtWidgets import QApplication, QMainWindow, QToolBar, QWhatsThis, QWidget
-from qwt.qt.QtGui import (
- QApplication,
- QPen,
- QBrush,
- QColor,
- QWidget,
- QMainWindow,
- QPainter,
- QPixmap,
- QToolBar,
- QWhatsThis,
-)
-from qwt.qt.QtCore import QSize, QEvent, Signal, QRect, QObject, Qt, QPoint
-from qwt.qt import PYQT5
from qwt import (
QwtPlot,
- QwtScaleDraw,
- QwtSymbol,
- QwtPlotGrid,
- QwtPlotCurve,
QwtPlotCanvas,
+ QwtPlotCurve,
+ QwtPlotGrid,
QwtScaleDiv,
+ QwtScaleDraw,
+ QwtSymbol,
)
+from qwt.tests import utils
+
+QT_API = os.environ["QT_API"]
class ColorBar(QWidget):
@@ -73,10 +67,7 @@ def dark(self):
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
- if PYQT5:
- pm = self.grab()
- else:
- pm = QPixmap.grabWidget(self)
+ pm = self.grab()
color = QColor()
color.setRgb(pm.toImage().pixel(event.x(), event.y()))
self.colorSelected.emit(color)
@@ -182,19 +173,17 @@ def scrollLeftAxis(self, value):
self.setAxisScale(QwtPlot.yLeft, value, value + 100)
self.replot()
- def eventFilter(self, object, event):
+ def eventFilter(self, obj, event):
if event.type() == QEvent.Resize:
size = event.size()
- if object == self.axisWidget(QwtPlot.yLeft):
+ if obj == self.axisWidget(QwtPlot.yLeft):
margin = 2
- x = size.width() - object.margin() + margin
- w = object.margin() - 2 * margin
- y = int(object.startBorderDist())
- h = int(
- size.height() - object.startBorderDist() - object.endBorderDist()
- )
+ x = size.width() - obj.margin() + margin
+ w = obj.margin() - 2 * margin
+ y = int(obj.startBorderDist())
+ h = int(size.height() - obj.startBorderDist() - obj.endBorderDist())
self.__colorBar.setGeometry(x, y, w, h)
- return QwtPlot.eventFilter(self, object, event)
+ return QwtPlot.eventFilter(self, obj, event)
def insertCurve(self, axis, base):
if axis == QwtPlot.yLeft or axis == QwtPlot.yRight:
@@ -211,8 +200,8 @@ def __insertCurve(self, orientation, color, base):
curve.setSymbol(
QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.gray), QPen(color), QSize(8, 8))
)
- fixed = base * np.ones(10, np.float)
- changing = np.arange(0, 95.0, 10.0, np.float) + 5.0
+ fixed = base * np.ones(10, float)
+ changing = np.arange(0, 95.0, 10.0, float) + 5.0
if orientation == Qt.Horizontal:
curve.setData(changing, fixed)
else:
@@ -254,14 +243,17 @@ def eventFilter(self, object, event):
if event.type() == QEvent.FocusIn:
self.__showCursor(True)
if event.type() == QEvent.FocusOut:
- self.__showCursor(False)
+ try:
+ self.__showCursor(False)
+ except RuntimeError:
+ pass # ignore error when closing the application
if event.type() == QEvent.Paint:
QApplication.postEvent(self, QEvent(QEvent.User))
elif event.type() == QEvent.MouseButtonPress:
- self.__select(event.pos())
+ self.__select(event.position())
return True
elif event.type() == QEvent.MouseMove:
- self.__move(event.pos())
+ self.__move(event.position())
return True
if event.type() == QEvent.KeyPress:
delta = 5
@@ -334,8 +326,8 @@ def __move(self, pos):
curve = self.__selectedCurve
if not curve:
return
- xData = np.zeros(curve.dataSize(), np.float)
- yData = np.zeros(curve.dataSize(), np.float)
+ xData = np.zeros(curve.dataSize(), float)
+ yData = np.zeros(curve.dataSize(), float)
for i in range(curve.dataSize()):
if i == self.__selectedPoint:
xData[i] = self.__plot.invTransform(curve.xAxis(), pos.x())
@@ -411,7 +403,7 @@ def __init__(self, plot):
def eventFilter(self, object, event):
if event.type() == QEvent.MouseButtonPress:
- self.__mouseClicked(object, event.pos())
+ self.__mouseClicked(object, event.position())
return True
return QObject.eventFilter(self, object, event)
@@ -481,7 +473,10 @@ def __init__(self, parent=None):
scalePicker.clicked.connect(plot.insertCurve)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_eventfilter():
+ """Event filter example"""
+ utils.test_widget(EventFilterWindow, size=(540, 400))
- app = test_widget(EventFilterWindow, size=(540, 400))
+
+if __name__ == "__main__":
+ test_eventfilter()
diff --git a/qwt/tests/test_highdpi.py b/qwt/tests/test_highdpi.py
new file mode 100644
index 0000000..44f951c
--- /dev/null
+++ b/qwt/tests/test_highdpi.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed under the terms of the PyQwt License
+# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example
+# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further
+# developments (e.g. ported to PythonQwt API)
+# (see LICENSE file for more details)
+
+SHOW = True # Show test in GUI-based test launcher
+
+import os
+
+import pytest
+
+from qwt.tests import utils
+from qwt.tests.test_simple import SimplePlot
+
+
+class HighDPIPlot(SimplePlot):
+ NUM_POINTS = 5000000 # 5 million points needed to test high DPI support
+
+
+@pytest.mark.skip(reason="This test is not relevant for the automated test suite")
+def test_highdpi():
+ """Test high DPI support"""
+
+ # Performance should be the same with "1" and "2" scale factors:
+ # (as of today, this is not the case, but it has to be fixed in the future:
+ # https://github.com/PlotPyStack/PythonQwt/issues/83)
+ os.environ["QT_SCALE_FACTOR"] = "2"
+
+ utils.test_widget(HighDPIPlot, (800, 480))
+
+
+if __name__ == "__main__":
+ test_highdpi()
diff --git a/qwt/tests/image.py b/qwt/tests/test_image.py
similarity index 94%
rename from qwt/tests/image.py
rename to qwt/tests/test_image.py
index 17220a3..b0eebfd 100644
--- a/qwt/tests/image.py
+++ b/qwt/tests/test_image.py
@@ -9,22 +9,23 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import Qt
+from qtpy.QtGui import QPen, qRgb
-from qwt.qt.QtGui import QPen, qRgb
-from qwt.qt.QtCore import Qt
from qwt import (
- QwtPlot,
- QwtPlotMarker,
+ QwtInterval,
QwtLegend,
- QwtPlotGrid,
- QwtPlotCurve,
- QwtPlotItem,
QwtLegendData,
QwtLinearColorMap,
- QwtInterval,
+ QwtPlot,
+ QwtPlotCurve,
+ QwtPlotGrid,
+ QwtPlotItem,
+ QwtPlotMarker,
QwtScaleMap,
toQImage,
)
+from qwt.tests import utils
def bytescale(data, cmin=None, cmax=None, high=255, low=0):
@@ -80,8 +81,8 @@ def setData(self, xyzs, xRange=None, yRange=None):
for i in range(0, 256):
self.image.setColor(i, qRgb(i, 0, 255 - i))
- def updateLegend(self, legend):
- QwtPlotItem.updateLegend(self, legend)
+ def updateLegend(self, legend, data):
+ QwtPlotItem.updateLegend(self, legend, data)
legend.find(self).setText(self.title())
def draw(self, painter, xMap, yMap, rect):
@@ -189,13 +190,15 @@ def __init__(self, *args):
self.replot()
def toggleVisibility(self, plotItem, idx):
- """Toggle the visibility of a plot item
- """
+ """Toggle the visibility of a plot item"""
plotItem.setVisible(not plotItem.isVisible())
self.replot()
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_image():
+ """Image plot test"""
+ utils.test_widget(ImagePlot, size=(600, 400))
- app = test_widget(ImagePlot, size=(600, 400))
+
+if __name__ == "__main__":
+ test_image()
diff --git a/qwt/tests/test_loadtest.py b/qwt/tests/test_loadtest.py
new file mode 100644
index 0000000..1576016
--- /dev/null
+++ b/qwt/tests/test_loadtest.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed under the terms of the MIT License
+# Copyright (c) 2015-2021 Pierre Raybaut
+# (see LICENSE file for more details)
+
+"""Load test"""
+
+SHOW = True # Show test in GUI-based test launcher
+
+import time
+
+import numpy as np
+
+# Local imports
+from qwt.tests import test_curvebenchmark1 as cb
+from qwt.tests import utils
+
+NCOLS, NROWS = 6, 5
+NPLOTS = NCOLS * NROWS * 5 * 3
+
+
+class LTWidget(cb.BMWidget):
+ def params(self, *args, **kwargs):
+ return tuple([("Lines", None)] * NCOLS * NROWS)
+
+
+class LoadTest(cb.CurveBenchmark1):
+ TITLE = "Load test [%d plots]" % NPLOTS
+ SIZE = (1600, 700)
+
+ def __init__(self, max_n=100, parent=None, unattended=False, **kwargs):
+ super(LoadTest, self).__init__(
+ max_n=max_n, parent=parent, unattended=unattended, **kwargs
+ )
+
+ def run_benchmark(self, max_n, unattended, **kwargs):
+ points, symbols = 100, False
+ iterator = range(0, 1) if unattended else range(int(NPLOTS / (NCOLS * NROWS)))
+ for _i_page in iterator:
+ t0 = time.time()
+ symtext = "with%s symbols" % ("" if symbols else "out")
+ widget = LTWidget(NCOLS, points, symbols, **kwargs)
+ title = "%d points" % points
+ description = "%d plots with %d curves of %d points, %s" % (
+ widget.plot_nb,
+ widget.curve_nb,
+ points,
+ symtext,
+ )
+ self.process_iteration(title, description, widget, t0)
+ print("")
+ time_str = "Average elapsed time: %d ms" % np.mean(self.durations)
+ print("[%s] %s" % (utils.get_lib_versions(), time_str))
+
+
+def test_loadtest():
+ """Load test"""
+ utils.test_widget(LoadTest, options=False)
+
+
+if __name__ == "__main__":
+ test_loadtest()
diff --git a/qwt/tests/logcurve.py b/qwt/tests/test_logcurve.py
similarity index 79%
rename from qwt/tests/logcurve.py
rename to qwt/tests/test_logcurve.py
index 415fec7..be18b17 100644
--- a/qwt/tests/logcurve.py
+++ b/qwt/tests/test_logcurve.py
@@ -12,9 +12,10 @@
np.seterr(all="raise")
-from qwt.qt.QtGui import QPen
-from qwt.qt.QtCore import Qt
-from qwt import QwtPlot, QwtPlotCurve, QwtLogScaleEngine
+from qtpy.QtCore import Qt
+
+from qwt import QwtLogScaleEngine, QwtPlot, QwtPlotCurve
+from qwt.tests import utils
class LogCurvePlot(QwtPlot):
@@ -30,7 +31,10 @@ def __init__(self):
self.replot()
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_logcurve():
+ """Log curve demo"""
+ utils.test_widget(LogCurvePlot, size=(800, 500))
- app = test_widget(LogCurvePlot, size=(800, 500))
+
+if __name__ == "__main__":
+ test_logcurve()
diff --git a/qwt/tests/mapdemo.py b/qwt/tests/test_mapdemo.py
similarity index 86%
rename from qwt/tests/mapdemo.py
rename to qwt/tests/test_mapdemo.py
index c39a6ba..720790b 100644
--- a/qwt/tests/mapdemo.py
+++ b/qwt/tests/test_mapdemo.py
@@ -10,16 +10,19 @@
import random
import time
+
import numpy as np
+from qtpy.QtCore import QSize, Qt
+from qtpy.QtGui import QBrush, QPen
+from qtpy.QtWidgets import QMainWindow, QToolBar
-from qwt.qt.QtGui import QPen, QBrush, QMainWindow, QToolBar
-from qwt.qt.QtCore import QSize, Qt
-from qwt import QwtPlot, QwtSymbol, QwtPlotCurve
+from qwt import QwtPlot, QwtPlotCurve, QwtSymbol
+from qwt.tests import utils
def standard_map(x, y, kappa):
"""provide one interate of the inital conditions (x, y)
- for the standard map with parameter kappa."""
+ for the standard map with parameter kappa."""
y_new = y - kappa * np.sin(2.0 * np.pi * x)
x_new = x + y_new
# bring back to [0,1.0]^2
@@ -43,8 +46,8 @@ def __init__(self, *args):
self.setCentralWidget(self.plot)
# Initialize map data
self.count = self.i = 1000
- self.xs = np.zeros(self.count, np.float)
- self.ys = np.zeros(self.count, np.float)
+ self.xs = np.zeros(self.count, float)
+ self.ys = np.zeros(self.count, float)
self.kappa = 0.2
self.curve = QwtPlotCurve("Map")
self.curve.attach(self.plot)
@@ -94,7 +97,10 @@ def timerEvent(self, e):
self.plot.replot()
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_mapdemo():
+ """Map demo"""
+ utils.test_widget(MapDemo, size=(600, 600))
- app = test_widget(MapDemo, size=(600, 600))
+
+if __name__ == "__main__":
+ test_mapdemo()
diff --git a/qwt/tests/multidemo.py b/qwt/tests/test_multidemo.py
similarity index 89%
rename from qwt/tests/multidemo.py
rename to qwt/tests/test_multidemo.py
index bcffeb7..951b355 100644
--- a/qwt/tests/multidemo.py
+++ b/qwt/tests/test_multidemo.py
@@ -9,10 +9,12 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import Qt
+from qtpy.QtGui import QPen
+from qtpy.QtWidgets import QGridLayout, QWidget
-from qwt.qt.QtGui import QPen, QGridLayout, QWidget
-from qwt.qt.QtCore import Qt
from qwt import QwtPlot, QwtPlotCurve
+from qwt.tests import utils
def drange(start, stop, step):
@@ -68,7 +70,10 @@ def __init__(self, *args):
list_plot.replot()
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_multidemo():
+ """Multiple plot demo"""
+ utils.test_widget(MultiDemo, size=(400, 300))
+
- app = test_widget(MultiDemo, size=(400, 300))
+if __name__ == "__main__":
+ test_multidemo()
diff --git a/qwt/tests/test_relativemargin.py b/qwt/tests/test_relativemargin.py
new file mode 100644
index 0000000..932d0fd
--- /dev/null
+++ b/qwt/tests/test_relativemargin.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed under the terms of the PyQwt License
+# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example
+# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further
+# developments (e.g. ported to PythonQwt API)
+# (see LICENSE file for more details)
+
+SHOW = True # Show test in GUI-based test launcher
+
+from qtpy import QtWidgets as QW
+from qtpy.QtCore import Qt
+
+import qwt
+from qwt.tests import utils
+
+
+class RelativeMarginDemo(QW.QWidget):
+ def __init__(self, *args):
+ QW.QWidget.__init__(self, *args)
+ layout = QW.QGridLayout(self)
+ x = [1, 2, 3, 4]
+ y = [1, 500, 1000, 1500]
+ for i_row, log_scale in enumerate((False, True)):
+ for i_col, relative_margin in enumerate((0.0, None, 0.2)):
+ plot = qwt.QwtPlot(self)
+ qwt.QwtPlotGrid.make(
+ plot, color=Qt.lightGray, width=0, style=Qt.DotLine
+ )
+ def_margin = plot.axisMargin(qwt.QwtPlot.yLeft)
+ scale_str = "lin/lin" if not log_scale else "log/lin"
+ if relative_margin is None:
+ margin_str = f"default ({def_margin * 100:.0f}%)"
+ else:
+ margin_str = f"{relative_margin * 100:.0f}%"
+ plot.setTitle(f"{scale_str}, margin: {margin_str}")
+ if relative_margin is not None:
+ plot.setAxisMargin(qwt.QwtPlot.yLeft, relative_margin)
+ plot.setAxisMargin(qwt.QwtPlot.xBottom, relative_margin)
+ color = "red" if i_row == 0 else "blue"
+ qwt.QwtPlotCurve.make(x, y, "", plot, linecolor=color)
+ layout.addWidget(plot, i_row, i_col)
+ if log_scale:
+ engine = qwt.QwtLogScaleEngine()
+ plot.setAxisScaleEngine(qwt.QwtPlot.yLeft, engine)
+
+
+def test_relative_margin():
+ """Test relative margin."""
+ utils.test_widget(RelativeMarginDemo, size=(400, 300), options=False)
+
+
+if __name__ == "__main__":
+ test_relative_margin()
diff --git a/qwt/tests/simple.py b/qwt/tests/test_simple.py
similarity index 70%
rename from qwt/tests/simple.py
rename to qwt/tests/test_simple.py
index 7cf808e..00968ab 100644
--- a/qwt/tests/simple.py
+++ b/qwt/tests/test_simple.py
@@ -8,13 +8,21 @@
SHOW = True # Show test in GUI-based test launcher
+import os
+
import numpy as np
+from qtpy.QtCore import Qt, QTimer
-from qwt.qt.QtCore import Qt
import qwt
+from qwt.tests import utils
+
+FNAMES = ("test_simple.svg", "test_simple.pdf", "test_simple.png")
class SimplePlot(qwt.QwtPlot):
+ NUM_POINTS = 100
+ TEST_EXPORT = True
+
def __init__(self):
qwt.QwtPlot.__init__(self)
self.setTitle("Really simple demo")
@@ -27,7 +35,7 @@ def __init__(self):
qwt.QwtPlotGrid.make(self, color=Qt.lightGray, width=0, style=Qt.DotLine)
# insert a few curves
- x = np.arange(0.0, 10.0, 0.1)
+ x = np.linspace(0.0, 10.0, self.NUM_POINTS)
qwt.QwtPlotCurve.make(x, np.sin(x), "y = sin(x)", self, linecolor="red")
qwt.QwtPlotCurve.make(x, np.cos(x), "y = cos(x)", self, linecolor="blue")
@@ -50,8 +58,21 @@ def __init__(self):
plot=self,
)
+ if self.TEST_EXPORT and utils.TestEnvironment().unattended:
+ QTimer.singleShot(0, self.export_to_different_formats)
-if __name__ == "__main__":
- from qwt.tests import test_widget
+ def export_to_different_formats(self):
+ for fname in FNAMES:
+ self.exportTo(fname)
- app = test_widget(SimplePlot, size=(600, 400))
+
+def test_simple():
+ """Simple plot example"""
+ utils.test_widget(SimplePlot, size=(600, 400))
+ for fname in FNAMES:
+ if os.path.isfile(fname):
+ os.remove(fname)
+
+
+if __name__ == "__main__":
+ test_simple()
diff --git a/qwt/tests/test_stylesheet.py b/qwt/tests/test_stylesheet.py
new file mode 100644
index 0000000..5f67acc
--- /dev/null
+++ b/qwt/tests/test_stylesheet.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+
+SHOW = True # Show test in GUI-based test launcher
+
+import os
+
+import numpy as np
+import pytest
+import qtpy
+from qtpy.QtCore import Qt
+
+import qwt
+from qwt.tests import utils
+
+
+class StyleSheetPlot(qwt.QwtPlot):
+ def __init__(self):
+ super().__init__()
+ self.setTitle("Stylesheet test (Issue #63)")
+ self.setStyleSheet("background-color: #19232D; color: #E0E1E3;")
+ qwt.QwtPlotGrid.make(self, color=Qt.white, width=0, style=Qt.DotLine)
+ x = np.arange(-5.0, 5.0, 0.1)
+ qwt.QwtPlotCurve.make(x, np.sinc(x), "y = sinc(x)", self, linecolor="green")
+
+
+# Skip the test for PySide6 on Linux
+@pytest.mark.skipif(
+ qtpy.API_NAME == "PySide6" and os.name == "posix",
+ reason="Fails on Linux with PySide6 for unknown reasons",
+)
+def test_stylesheet():
+ """Stylesheet test"""
+ utils.test_widget(StyleSheetPlot, size=(600, 400))
+
+
+if __name__ == "__main__":
+ test_stylesheet()
diff --git a/qwt/tests/test_symbols.py b/qwt/tests/test_symbols.py
new file mode 100644
index 0000000..36a8852
--- /dev/null
+++ b/qwt/tests/test_symbols.py
@@ -0,0 +1,201 @@
+# -*- coding: utf-8 -*-
+
+SHOW = True # Show test in GUI-based test launcher
+
+import os.path as osp
+
+import numpy as np
+from qtpy import QtCore as QC
+from qtpy import QtGui as QG
+from qtpy import QtWidgets as QW
+
+import qwt
+from qwt.tests import utils
+
+
+class BaseSymbolPlot(qwt.QwtPlot):
+ TITLE = "Base Symbol Example"
+ SYMBOL_CLASS = qwt.QwtSymbol
+
+ def __init__(self):
+ super().__init__()
+ self.setTitle(self.TITLE)
+ self.setAxisScale(self.yLeft, -20, 20)
+ self.setAxisScale(self.xBottom, -20, 20)
+ self.setup_plot()
+
+ def setup_plot(self):
+ samples = ([-15, 0, 15, -15], [0, 15, 0, 0])
+ self.add_curve(self.TITLE, samples, self.SYMBOL_CLASS())
+ self.resize(400, 400)
+
+ def add_curve(self, title, samples, symbol=None):
+ """Add a curve to the plot"""
+ curve = qwt.QwtPlotCurve(title)
+ curve.setSamples(*samples)
+ if symbol is not None:
+ curve.setSymbol(symbol)
+ curve.attach(self)
+ self.replot()
+
+
+class BuiltinSymbolPlot(BaseSymbolPlot):
+ TITLE = "Built-in Symbol Example"
+
+ def setup_plot(self):
+ colors = (QC.Qt.red, QC.Qt.green, QC.Qt.blue, QC.Qt.yellow, QC.Qt.magenta)
+ for index, symbol_name in enumerate(
+ (
+ "Ellipse",
+ "Rect",
+ "Diamond",
+ "Triangle",
+ "DTriangle",
+ "UTriangle",
+ "LTriangle",
+ "RTriangle",
+ "Cross",
+ "XCross",
+ "HLine",
+ "VLine",
+ "Star1",
+ "Star2",
+ "Hexagon",
+ )
+ ):
+ symbol = qwt.symbol.QwtSymbol(getattr(qwt.QwtSymbol, symbol_name))
+ symbol.setSize(7, 7)
+ symbol.setPen(QG.QPen(colors[index % 3]))
+ symbol.setBrush(QG.QBrush(QG.QColor(colors[index % 3]).lighter(150)))
+ x = np.linspace(-10, 10, 100)
+ y = np.sin(x + index * np.pi / 10)
+ samples = (x, y)
+ qwt.plot_marker.QwtPlotMarker.make(
+ xvalue=index * 2 - 10,
+ yvalue=index * 2 - 10,
+ label=qwt.text.QwtText.make(
+ "Marker",
+ color=QC.Qt.black,
+ borderradius=2,
+ brush=QC.Qt.lightGray,
+ ),
+ symbol=symbol,
+ plot=self,
+ )
+ self.add_curve(symbol_name, samples, symbol)
+ self.setAxisAutoScale(self.yLeft, True)
+ self.setAxisAutoScale(self.xBottom, True)
+
+
+class CustomGraphicSymbol(qwt.QwtSymbol):
+ def __init__(self):
+ super(CustomGraphicSymbol, self).__init__(qwt.QwtSymbol.Graphic)
+
+ # Use a built-in Qt icon as QPixmap for demonstration
+ icon = QW.QApplication.style().standardIcon(QW.QStyle.SP_FileIcon)
+ pixmap = icon.pixmap(20, 20)
+
+ # Convert the QPixmap to a QwtGraphic
+ graphic = qwt.graphic.QwtGraphic()
+ graphic.setDefaultSize(pixmap.size())
+ painter = QG.QPainter(graphic)
+ painter.drawPixmap(0, 0, pixmap)
+ painter.end()
+
+ # Set the QwtGraphic as the graphic for the symbol
+ self.setGraphic(graphic)
+
+
+class GraphicPlot(BaseSymbolPlot):
+ TITLE = "Custom QwtGraphic Symbol Example"
+ SYMBOL_CLASS = CustomGraphicSymbol
+
+
+class CustomPixmapSymbol(qwt.QwtSymbol):
+ def __init__(self):
+ super(CustomPixmapSymbol, self).__init__(qwt.QwtSymbol.Pixmap)
+
+ # Use a built-in Qt icon as QPixmap for demonstration
+ icon = QW.QApplication.style().standardIcon(QW.QStyle.SP_DialogYesButton)
+ pixmap = icon.pixmap(20, 20)
+
+ # Set the QPixmap for the symbol
+ self.setPixmap(pixmap)
+
+
+class PixmapPlot(BaseSymbolPlot):
+ TITLE = "Custom QPixmap Symbol Example"
+ SYMBOL_CLASS = CustomPixmapSymbol
+
+
+class CustomPathSymbol(qwt.QwtSymbol):
+ def __init__(self):
+ super(CustomPathSymbol, self).__init__(qwt.QwtSymbol.Path)
+
+ path = QG.QPainterPath()
+ path.moveTo(0, -10) # Top vertex of the triangle
+ path.lineTo(-10, 10) # Bottom-left vertex
+ path.lineTo(10, 10) # Bottom-right vertex
+ path.closeSubpath() # Close the triangle
+
+ self.setPath(path)
+ self.setSize(20, 20)
+
+
+class PathPlot(BaseSymbolPlot):
+ TITLE = "Custom Path Symbol Example"
+ SYMBOL_CLASS = CustomPathSymbol
+
+
+class CustomSvgSymbol(qwt.QwtSymbol):
+ FNAME = osp.join(osp.dirname(__file__), "data", "symbol.svg")
+
+ def __init__(self):
+ super(CustomSvgSymbol, self).__init__(qwt.QwtSymbol.SvgDocument)
+
+ # Load the SVG document from the given file
+ self.setSvgDocument(self.FNAME)
+
+
+class SvgDocumentPlot(BaseSymbolPlot):
+ TITLE = "Custom SVG Symbol Example"
+ SYMBOL_CLASS = CustomSvgSymbol
+
+
+def test_base():
+ """Base symbol test"""
+ utils.test_widget(BaseSymbolPlot, size=(600, 400))
+
+
+def test_builtin():
+ """Built-in symbol test"""
+ utils.test_widget(BuiltinSymbolPlot, size=(600, 400))
+
+
+def test_graphic():
+ """Graphic symbol test"""
+ utils.test_widget(GraphicPlot, size=(600, 400))
+
+
+def test_pixmap():
+ """Pixmap test"""
+ utils.test_widget(PixmapPlot, size=(600, 400))
+
+
+def test_path():
+ """Path symbol test"""
+ utils.test_widget(PathPlot, size=(600, 400))
+
+
+def test_svg():
+ """SVG test"""
+ utils.test_widget(SvgDocumentPlot, size=(600, 400))
+
+
+if __name__ == "__main__":
+ # test_base()
+ test_builtin()
+ # test_graphic()
+ # test_pixmap()
+ # test_path()
+ # test_svg()
diff --git a/qwt/tests/vertical.py b/qwt/tests/test_vertical.py
similarity index 70%
rename from qwt/tests/vertical.py
rename to qwt/tests/test_vertical.py
index 8d3e7ea..eadb18d 100644
--- a/qwt/tests/vertical.py
+++ b/qwt/tests/test_vertical.py
@@ -9,49 +9,28 @@
SHOW = True # Show test in GUI-based test launcher
import numpy as np
+from qtpy.QtCore import Qt
+from qtpy.QtGui import QColor, QPalette, QPen
-from qwt.qt.QtGui import QFont, QPen, QPalette, QColor
-from qwt.qt.QtCore import Qt
-
-import os
-
-if os.environ.get("USE_PYQWT5", False):
- USE_PYQWT5 = True
- from PyQt4.Qwt5 import QwtPlot, QwtPlotCurve, QwtPlotMarker, QwtText
-else:
- USE_PYQWT5 = False
- from qwt import QwtPlot, QwtPlotCurve, QwtPlotMarker, QwtText # analysis:ignore
+from qwt import QwtPlot, QwtPlotCurve, QwtPlotMarker, QwtText
+from qwt.tests import utils
class VerticalPlot(QwtPlot):
def __init__(self, parent=None):
super(VerticalPlot, self).__init__(parent)
- self.setWindowTitle("PyQwt" if USE_PYQWT5 else "PythonQwt")
+ self.setWindowTitle("PythonQwt")
self.enableAxis(self.xTop, True)
self.enableAxis(self.yRight, True)
y = np.linspace(0, 10, 500)
curve1 = QwtPlotCurve.make(np.sin(y), y, title="Test Curve 1")
- curve2 = QwtPlotCurve.make(y ** 3, y, title="Test Curve 2")
- if USE_PYQWT5:
- # PyQwt
- curve2.setAxis(self.xTop, self.yRight)
- self.canvas().setFrameStyle(0)
- self.plotLayout().setCanvasMargin(0)
- self.axisWidget(QwtPlot.yLeft).setMargin(0)
- self.axisWidget(QwtPlot.xTop).setMargin(0)
- self.axisWidget(QwtPlot.yRight).setMargin(0)
- self.axisWidget(QwtPlot.xBottom).setMargin(0)
- else:
- # PythonQwt
- curve2.setAxes(self.xTop, self.yRight)
+ curve2 = QwtPlotCurve.make(y**3, y, title="Test Curve 2")
+ curve2.setAxes(self.xTop, self.yRight)
for item, col, xa, ya in (
(curve1, Qt.green, self.xBottom, self.yLeft),
(curve2, Qt.red, self.xTop, self.yRight),
):
- if not USE_PYQWT5:
- # PythonQwt
- item.setOrientation(Qt.Vertical)
item.attach(self)
item.setPen(QPen(col))
for axis_id in xa, ya:
@@ -96,7 +75,10 @@ def show_layout_details(self):
self.marker.setLabel(QwtText.make(text, family="Courier New", color=Qt.blue))
-if __name__ == "__main__":
- from qwt.tests import test_widget
+def test_vertical():
+ """Vertical plot example"""
+ utils.test_widget(VerticalPlot, size=(300, 650))
- app = test_widget(VerticalPlot, size=(300, 650))
+
+if __name__ == "__main__":
+ test_vertical()
diff --git a/qwt/tests/utils.py b/qwt/tests/utils.py
new file mode 100644
index 0000000..9f4d147
--- /dev/null
+++ b/qwt/tests/utils.py
@@ -0,0 +1,323 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed under the terms of the MIT License
+# Copyright (c) 2015 Pierre Raybaut
+# (see LICENSE file for more details)
+
+"""
+PythonQwt test utilities
+------------------------
+"""
+
+import argparse
+import inspect
+import os
+import os.path as osp
+import platform
+import subprocess
+import sys
+
+from qtpy import QtCore as QC
+from qtpy import QtGui as QG
+from qtpy import QtWidgets as QW
+
+import qwt
+from qwt import QwtPlot
+from qwt import qthelpers as qth
+
+QT_API = os.environ["QT_API"]
+
+if QT_API.startswith("pyside"):
+ from qtpy import PYSIDE_VERSION
+
+ PYTHON_QT_API = "PySide v" + PYSIDE_VERSION
+else:
+ from qtpy import PYQT_VERSION
+
+ PYTHON_QT_API = "PyQt v" + PYQT_VERSION
+
+
+TEST_PATH = osp.abspath(osp.dirname(__file__))
+
+
+class TestEnvironment:
+ UNATTENDED_ARG = "unattended"
+ SCREENSHOTS_ARG = "screenshots"
+ UNATTENDED_ENV = "PYTHONQWT_UNATTENDED_TESTS"
+ SCREENSHOTS_ENV = "PYTHONQWT_TAKE_SCREENSHOTS"
+
+ def __init__(self):
+ self.parse_args()
+
+ @property
+ def unattended(self):
+ return os.environ.get(self.UNATTENDED_ENV) is not None
+
+ @property
+ def screenshots(self):
+ return os.environ.get(self.SCREENSHOTS_ENV) is not None
+
+ def parse_args(self):
+ """Parse command line arguments"""
+ parser = argparse.ArgumentParser(description="Run PythonQwt tests")
+ parser.add_argument(
+ "--mode",
+ choices=[self.UNATTENDED_ARG, self.SCREENSHOTS_ARG],
+ required=False,
+ )
+ args, _unknown = parser.parse_known_args()
+ if args.mode is not None:
+ self.set_env_from_args(args)
+
+ def set_env_from_args(self, args):
+ """Set appropriate environment variables"""
+ for name in (self.UNATTENDED_ENV, self.SCREENSHOTS_ENV):
+ if name in os.environ:
+ os.environ.pop(name)
+ if args.mode == self.UNATTENDED_ARG:
+ os.environ[self.UNATTENDED_ENV] = "1"
+ if args.mode == self.SCREENSHOTS_ARG:
+ os.environ[self.SCREENSHOTS_ENV] = os.environ[self.UNATTENDED_ENV] = "1"
+
+
+def get_tests(package):
+ """Return list of test filenames"""
+ test_package_name = "%s.tests" % package.__name__
+ _temp = __import__(test_package_name)
+ test_package = sys.modules[test_package_name]
+ tests = []
+ test_path = osp.dirname(osp.realpath(test_package.__file__))
+ for fname in sorted(
+ [
+ name
+ for name in os.listdir(test_path)
+ if name.endswith((".py", ".pyw")) and not name.startswith(("_", "conftest"))
+ ]
+ ):
+ module_name = osp.splitext(fname)[0]
+ _temp = __import__(test_package.__name__, fromlist=[module_name])
+ module = getattr(_temp, module_name)
+ if hasattr(module, "SHOW") and module.SHOW:
+ tests.append(osp.abspath(osp.join(test_path, fname)))
+ return tests
+
+
+def run_test(fname, wait=True):
+ """Run test"""
+ os.environ["PYTHONPATH"] = os.pathsep.join(sys.path)
+ args = " ".join([sys.executable, '"' + fname + '"'])
+ if TestEnvironment().unattended:
+ print(" " + args)
+ (subprocess.call if wait else subprocess.Popen)(args, shell=True)
+
+
+def run_all_tests(wait=True):
+ """Run all PythonQwt tests"""
+ for fname in get_tests(qwt):
+ run_test(fname, wait=wait)
+
+
+def get_lib_versions():
+ """Return string containing Python-Qt versions"""
+ from qtpy.QtCore import __version__ as qt_version
+
+ return "Python %s, Qt %s, %s on %s" % (
+ platform.python_version(),
+ qt_version,
+ PYTHON_QT_API,
+ platform.system(),
+ )
+
+
+class TestLauncher(QW.QMainWindow):
+ """PythonQwt Test Launcher main window"""
+
+ COLUMNS = 5
+
+ def __init__(self, parent=None):
+ super(TestLauncher, self).__init__(parent)
+ self.setObjectName("TestLauncher")
+ icon = QG.QIcon(osp.join(TEST_PATH, "data", "PythonQwt.svg"))
+ self.setWindowIcon(icon)
+ self.setWindowTitle("PythonQwt %s - Test Launcher" % qwt.__version__)
+ self.setCentralWidget(QW.QWidget())
+ self.grid_layout = QW.QGridLayout()
+ self.centralWidget().setLayout(self.grid_layout)
+ self.test_nb = None
+ self.fill_layout()
+ self.statusBar().show()
+ self.setStatusTip("Click on any button to run a test")
+
+ def get_std_icon(self, name):
+ """Return Qt standard icon"""
+ return self.style().standardIcon(getattr(QW.QStyle, "SP_" + name))
+
+ def fill_layout(self):
+ """Fill grid layout"""
+ for fname in get_tests(qwt):
+ self.add_test(fname)
+ toolbar = QW.QToolBar(self)
+ all_act = QW.QAction(self.get_std_icon("DialogYesButton"), "", self)
+ all_act.setIconText("Run all tests")
+ all_act.triggered.connect(lambda checked: run_all_tests(wait=False))
+ folder_act = QW.QAction(self.get_std_icon("DirOpenIcon"), "", self)
+ folder_act.setIconText("Open tests folder")
+
+ def open_test_folder(checked):
+ return os.startfile(TEST_PATH)
+
+ folder_act.triggered.connect(open_test_folder)
+ about_act = QW.QAction(self.get_std_icon("FileDialogInfoView"), "", self)
+ about_act.setIconText("About")
+ about_act.triggered.connect(self.about)
+ for action in (all_act, folder_act, None, about_act):
+ if action is None:
+ toolbar.addSeparator()
+ else:
+ toolbar.addAction(action)
+ toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextBesideIcon)
+ self.addToolBar(toolbar)
+
+ def add_test(self, fname):
+ """Add new test"""
+ if self.test_nb is None:
+ self.test_nb = 0
+ self.test_nb += 1
+ column = (self.test_nb - 1) % self.COLUMNS
+ row = (self.test_nb - 1) // self.COLUMNS
+ bname = osp.basename(fname)
+ button = QW.QToolButton(self)
+ button.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon)
+ shot = osp.join(
+ TEST_PATH, "data", bname.replace(".py", ".png").replace("test_", "")
+ )
+ if osp.isfile(shot):
+ button.setIcon(QG.QIcon(shot))
+ else:
+ button.setIcon(self.get_std_icon("DialogYesButton"))
+ button.setText(bname)
+ button.setToolTip(fname)
+ button.setIconSize(QC.QSize(130, 80))
+ button.clicked.connect(lambda checked=None, fname=fname: run_test(fname))
+ self.grid_layout.addWidget(button, row, column)
+
+ def about(self):
+ """About test launcher"""
+ QW.QMessageBox.about(
+ self,
+ "About " + self.windowTitle(),
+ """%s
Developped by Pierre Raybaut
+
Copyright © 2020 Pierre Raybaut
+
%s""" + % (self.windowTitle(), get_lib_versions()), + ) + + +class TestOptions(QW.QGroupBox): + """Test options groupbox""" + + def __init__(self, parent=None): + super(TestOptions, self).__init__("Test options", parent) + self.setLayout(QW.QFormLayout()) + self.hide() + + def add_checkbox(self, title, label, slot): + """Add new checkbox to option panel""" + widget = QW.QCheckBox(label, self) + widget.stateChanged.connect(slot) + self.layout().addRow(title, widget) + self.show() + return widget + + +class TestCentralWidget(QW.QWidget): + """Test central widget""" + + def __init__(self, widget_name, parent=None): + super(TestCentralWidget, self).__init__(parent) + self.widget_name = widget_name + self.plots = None + self.setLayout(QW.QVBoxLayout()) + self.options = TestOptions(self) + self.add_widget(self.options) + + def get_widget_of_interest(self): + """Return widget of interest""" + if self.plots is not None and len(self.plots) == 1: + return self.plots[0] + return self.parent() + + def add_widget(self, widget): + """Add new sub-widget""" + self.layout().addWidget(widget) + if isinstance(widget, QwtPlot): + self.plots = [widget] + else: + self.plots = widget.findChildren(QwtPlot) + for index, plot in enumerate(self.plots): + plot_name = plot.objectName() + if not plot_name: + plot_name = "Plot #%d" % (index + 1) + widget = self.options.add_checkbox( + plot_name, "Enable new flat style option", plot.setFlatStyle + ) + widget.setChecked(plot.flatStyle()) + + +def take_screenshot(widget): + """Take screenshot and save it to the data folder""" + bname = (widget.objectName().lower() + ".png").replace("window", "") + bname = bname.replace("plot", "").replace("widget", "") + qth.take_screenshot(widget, osp.join(TEST_PATH, "data", bname), quit=True) + + +def close_widgets_and_quit() -> None: + """Close Qt top level widgets and quit Qt event loop""" + QW.QApplication.processEvents() + for widget in QW.QApplication.instance().topLevelWidgets(): + assert widget.close() + QC.QTimer.singleShot(0, QW.QApplication.instance().quit) + + +def test_widget(widget_class, size=None, title=None, options=True): + """Test widget""" + widget_name = widget_class.__name__ + app = QW.QApplication.instance() + if app is None: + app = QW.QApplication([]) + test_env = TestEnvironment() + if inspect.signature(widget_class).parameters.get("unattended") is None: + widget = widget_class() + else: + widget = widget_class(unattended=test_env.unattended) + window = widget + if options: + if isinstance(widget, QW.QMainWindow): + widget = window.centralWidget() + widget.setParent(None) + else: + window = QW.QMainWindow() + central_widget = TestCentralWidget(widget_name, parent=window) + central_widget.add_widget(widget) + window.setCentralWidget(central_widget) + widget_of_interest = central_widget.get_widget_of_interest() + else: + widget_of_interest = window + widget_of_interest.setObjectName(widget_name) + if title is None: + title = 'Test "%s" - PythonQwt %s' % (widget_name, qwt.__version__) + window.setWindowTitle(title) + if size is not None: + width, height = size + window.resize(width, height) + + window.show() + if test_env.screenshots: + QC.QTimer.singleShot(1000, lambda: take_screenshot(widget_of_interest)) + elif test_env.unattended: + QC.QTimer.singleShot(0, close_widgets_and_quit) + if QT_API == "pyside6": + app.exec() + else: + app.exec_() + return app diff --git a/qwt/text.py b/qwt/text.py index 36a995a..2fb0f9a 100644 --- a/qwt/text.py +++ b/qwt/text.py @@ -14,7 +14,7 @@ .. autoclass:: QwtText :members: - + QwtTextLabel ~~~~~~~~~~~~ @@ -32,53 +32,74 @@ QwtPlainTextEngine ~~~~~~~~~~~~~~~~~~ - + .. autoclass:: QwtPlainTextEngine :members: QwtRichTextEngine ~~~~~~~~~~~~~~~~~ - + .. autoclass:: QwtRichTextEngine :members: """ -import numpy as np +import math +import os import struct -from .qt.QtGui import ( - QPainter, - QFrame, - QSizePolicy, - QPalette, +from qtpy.QtCore import QObject, QRectF, QSize, QSizeF, Qt +from qtpy.QtGui import ( + QAbstractTextDocumentLayout, + QColor, QFont, + QFontInfo, QFontMetrics, - QApplication, - QColor, - QWidget, - QTextDocument, - QTextOption, QFontMetricsF, + QPainter, + QPalette, QPixmap, - QFontInfo, + QTextDocument, + QTextOption, QTransform, - QAbstractTextDocumentLayout, ) -from .qt.QtCore import Qt, QSizeF, QSize, QRectF +from qtpy.QtWidgets import QApplication, QFrame, QSizePolicy, QWidget -from .painter import QwtPainter -from .qthelpers import qcolor_from_str +from qwt.painter import QwtPainter +from qwt.qthelpers import qcolor_from_str QWIDGETSIZE_MAX = (1 << 24) - 1 +QT_API = os.environ["QT_API"] + + +# Cache Qt alignment flags as plain ints once at import time. On PyQt6 these +# are ``Qt.AlignmentFlag`` enum members and every bitwise test goes through +# ``enum.__and__`` (~6 us each). The test code below combines them in hot +# paths called per-tick / per-label / per-paint event. +def _flag_int(flag): + """Return the integer value of a Qt enum/flag (PyQt5 and PyQt6).""" + try: + return flag.value + except AttributeError: + return int(flag) + + +_ALIGN_LEFT = _flag_int(Qt.AlignLeft) +_ALIGN_RIGHT = _flag_int(Qt.AlignRight) +_ALIGN_TOP = _flag_int(Qt.AlignTop) +_ALIGN_BOTTOM = _flag_int(Qt.AlignBottom) +_ALIGN_HCENTER = _flag_int(Qt.AlignHCenter) +_ALIGN_JUSTIFY = _flag_int(Qt.AlignJustify) +_ALIGN_CENTER = _flag_int(Qt.AlignCenter) + def taggedRichText(text, flags): richText = text - if flags & Qt.AlignJustify: + if flags & _ALIGN_JUSTIFY: richText = '