PythonQwt 0.15.0__py3-none-any.whl → 0.16.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonqwt-0.15.0.dist-info → pythonqwt-0.16.1.dist-info}/METADATA +22 -10
- {pythonqwt-0.15.0.dist-info → pythonqwt-0.16.1.dist-info}/RECORD +18 -18
- {pythonqwt-0.15.0.dist-info → pythonqwt-0.16.1.dist-info}/WHEEL +1 -1
- qwt/__init__.py +3 -3
- qwt/graphic.py +30 -13
- qwt/null_paintdevice.py +5 -1
- qwt/painter_command.py +49 -12
- qwt/plot_canvas.py +1 -1
- qwt/scale_div.py +5 -3
- qwt/scale_draw.py +79 -36
- qwt/scale_engine.py +17 -10
- qwt/scale_map.py +19 -10
- qwt/tests/test_bodedemo.py +1 -2
- qwt/tests/test_relativemargin.py +2 -2
- qwt/text.py +162 -37
- {pythonqwt-0.15.0.dist-info → pythonqwt-0.16.1.dist-info}/entry_points.txt +0 -0
- {pythonqwt-0.15.0.dist-info → pythonqwt-0.16.1.dist-info}/licenses/LICENSE +0 -0
- {pythonqwt-0.15.0.dist-info → pythonqwt-0.16.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PythonQwt
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.1
|
|
4
4
|
Summary: Qt plotting widgets for Python
|
|
5
5
|
Author-email: Pierre Raybaut <pierre.raybaut@gmail.com>
|
|
6
6
|
License: PythonQwt License Agreement
|
|
@@ -666,12 +666,11 @@ License: PythonQwt License Agreement
|
|
|
666
666
|
Project-URL: Homepage, https://github.com/PlotPyStack/PythonQwt/
|
|
667
667
|
Project-URL: Documentation, https://PythonQwt.readthedocs.io/en/latest/
|
|
668
668
|
Classifier: Topic :: Scientific/Engineering
|
|
669
|
+
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
669
670
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
670
671
|
Classifier: Topic :: Software Development :: Widget Sets
|
|
671
672
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
672
673
|
Classifier: Topic :: Utilities
|
|
673
|
-
Classifier: Topic :: Scientific/Engineering
|
|
674
|
-
Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
|
|
675
674
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
676
675
|
Classifier: Operating System :: MacOS
|
|
677
676
|
Classifier: Operating System :: Microsoft :: Windows
|
|
@@ -683,15 +682,18 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
683
682
|
Classifier: Programming Language :: Python :: 3.11
|
|
684
683
|
Classifier: Programming Language :: Python :: 3.12
|
|
685
684
|
Classifier: Programming Language :: Python :: 3.13
|
|
685
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
686
686
|
Requires-Python: <4,>=3.9
|
|
687
687
|
Description-Content-Type: text/markdown
|
|
688
688
|
License-File: LICENSE
|
|
689
|
-
Requires-Dist: NumPy>=1.
|
|
689
|
+
Requires-Dist: NumPy>=1.21
|
|
690
690
|
Requires-Dist: QtPy>=1.9
|
|
691
691
|
Provides-Extra: dev
|
|
692
|
+
Requires-Dist: build; extra == "dev"
|
|
692
693
|
Requires-Dist: ruff; extra == "dev"
|
|
693
694
|
Requires-Dist: pylint; extra == "dev"
|
|
694
695
|
Requires-Dist: Coverage; extra == "dev"
|
|
696
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
695
697
|
Provides-Extra: doc
|
|
696
698
|
Requires-Dist: PyQt5; extra == "doc"
|
|
697
699
|
Requires-Dist: sphinx>6; extra == "doc"
|
|
@@ -760,7 +762,7 @@ tests.run()
|
|
|
760
762
|
or from the command line:
|
|
761
763
|
|
|
762
764
|
```bash
|
|
763
|
-
PythonQwt
|
|
765
|
+
PythonQwt-tests
|
|
764
766
|
```
|
|
765
767
|
|
|
766
768
|
Tests may also be executed in unattended mode:
|
|
@@ -773,9 +775,9 @@ PythonQwt-tests --mode unattended
|
|
|
773
775
|
|
|
774
776
|
The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations.
|
|
775
777
|
|
|
776
|
-
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `
|
|
778
|
+
The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `PlotPy`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
|
|
777
779
|
|
|
778
|
-
Only the following plot items are currently implemented in `qwt` (the only plot items needed by `
|
|
780
|
+
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`.
|
|
779
781
|
|
|
780
782
|
See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) for more details on API limitations when comparing to Qwt.
|
|
781
783
|
|
|
@@ -798,14 +800,14 @@ Compatibility table:
|
|
|
798
800
|
|
|
799
801
|
| PythonQwt version | PyQt5 | PyQt6 | PySide2 | PySide6 |
|
|
800
802
|
|-------------------|-------|-------|---------|---------|
|
|
801
|
-
| 0.
|
|
803
|
+
| 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
|
|
802
804
|
| Latest | ✅ | ✅ | ❌ | ✅ |
|
|
803
805
|
|
|
804
806
|
### Requirements
|
|
805
807
|
|
|
806
808
|
- Python >=3.9
|
|
807
|
-
- QtPy >= 1.
|
|
808
|
-
- NumPy >= 1.
|
|
809
|
+
- QtPy >= 1.9 (and a Python-to-Qt binding library, see above)
|
|
810
|
+
- NumPy >= 1.21
|
|
809
811
|
|
|
810
812
|
### Optional dependencies
|
|
811
813
|
|
|
@@ -814,12 +816,22 @@ Compatibility table:
|
|
|
814
816
|
|
|
815
817
|
### Installation
|
|
816
818
|
|
|
819
|
+
From PyPI:
|
|
820
|
+
|
|
821
|
+
```bash
|
|
822
|
+
pip install PythonQwt
|
|
823
|
+
```
|
|
824
|
+
|
|
817
825
|
From the source package:
|
|
818
826
|
|
|
819
827
|
```bash
|
|
820
828
|
python -m build
|
|
821
829
|
```
|
|
822
830
|
|
|
831
|
+
## Performance investigation
|
|
832
|
+
|
|
833
|
+
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.
|
|
834
|
+
|
|
823
835
|
## Copyrights
|
|
824
836
|
|
|
825
837
|
### Main code base
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
pythonqwt-0.
|
|
2
|
-
qwt/__init__.py,sha256=
|
|
1
|
+
pythonqwt-0.16.1.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
|
|
2
|
+
qwt/__init__.py,sha256=Aux0XzsSsxiS6O8LFosjbqk7X0rIU4kE3Si0a9jGBaE,5990
|
|
3
3
|
qwt/_math.py,sha256=fNcPJcaK-ldCCFB20T2N58LEkQ1lwZY1-q5veXXhwl8,1501
|
|
4
4
|
qwt/color_map.py,sha256=fLRpymqOXtLrhuPrbbCxTvNINbr2GH7pYBG4fZidrcs,11812
|
|
5
5
|
qwt/column_symbol.py,sha256=0aFyQ05ryTKbaSTctkhtXa0pB_tWG444zlH3991_bt0,5760
|
|
6
6
|
qwt/dyngrid_layout.py,sha256=wgff6mZ1okNhQRRbwaw1I__vOXqGdUG0uafkShuns08,13477
|
|
7
|
-
qwt/graphic.py,sha256=
|
|
7
|
+
qwt/graphic.py,sha256=rSxvgIAm9ctcyszvZrN_03KpBhKSHabbPYdcrztSta8,28962
|
|
8
8
|
qwt/interval.py,sha256=PMrCQxV9NXGk2JG0MNjJ3zGTNTSSRfa_noxbpm-Okns,12287
|
|
9
9
|
qwt/legend.py,sha256=GBUim67HWgwLat5tOWDpK98fWcFxV-RkxWjoyWd9Gao,31313
|
|
10
|
-
qwt/null_paintdevice.py,sha256=
|
|
10
|
+
qwt/null_paintdevice.py,sha256=1pgbcvyRTVN32RGzuPzUOm0iUImjvPQ4l5OfJVyA3hg,9375
|
|
11
11
|
qwt/painter.py,sha256=k345puF4MAoTye0WBveiTNpCNWXx1K2o62_I0PZYlIA,16536
|
|
12
|
-
qwt/painter_command.py,sha256=
|
|
12
|
+
qwt/painter_command.py,sha256=7D3goNsX_2Ufr5kCpC3Cr1t8GoD5Yz0G1oIZJKvoGBY,8501
|
|
13
13
|
qwt/plot.py,sha256=d8rf4F-fpQgdpTTqNKQFJh7B0FlQ4WlquooeJQRn_eo,75660
|
|
14
|
-
qwt/plot_canvas.py,sha256=
|
|
14
|
+
qwt/plot_canvas.py,sha256=INRcxB17dvyGI7Jqi2FX4gm16EG4gzJKULj9inHkibA,30309
|
|
15
15
|
qwt/plot_curve.py,sha256=ThlfxUjB4yiP_Dh96P7PXusyqUfDWcRcyt31OHvu2vk,37059
|
|
16
16
|
qwt/plot_directpainter.py,sha256=JCWNhCC21L1A1HZnxd-C95QNlrVJbLztmGab-X8gCGc,10165
|
|
17
17
|
qwt/plot_grid.py,sha256=h-cDTzibEzQyX2g25SBg5ysyXwmONmMFy9NcuzFySYc,16607
|
|
@@ -20,20 +20,20 @@ qwt/plot_marker.py,sha256=6vDHdMsyGClMruWLA4lChUCpoLzmxGNkvmICm0KjP6I,21284
|
|
|
20
20
|
qwt/plot_renderer.py,sha256=v-fSwicMGQBa_kQx4hJJptlVNpNsKbgWk9jjqG-r_1g,27201
|
|
21
21
|
qwt/plot_series.py,sha256=Wa_Wunu-t_L8vl95RJq6__k-XA8UV2YNIsc8yNGpTT8,10654
|
|
22
22
|
qwt/qthelpers.py,sha256=GpvLr96Ip8SIGwyFfs0f7ocHyKPB7PEjHBQyrpw84xw,1405
|
|
23
|
-
qwt/scale_div.py,sha256=
|
|
24
|
-
qwt/scale_draw.py,sha256=
|
|
25
|
-
qwt/scale_engine.py,sha256=
|
|
26
|
-
qwt/scale_map.py,sha256=
|
|
23
|
+
qwt/scale_div.py,sha256=_iI5cy4s8H6wigE8xsn7W44T2-DqVADqdjQ31uJPa_I,9266
|
|
24
|
+
qwt/scale_draw.py,sha256=KYal9YOpvHjFWGkBFPVVNq_64cxM1vsHg_f_n-2nKNU,40547
|
|
25
|
+
qwt/scale_engine.py,sha256=7KQB5k-JXuJMsEMQXBJBNppCN-rTN97UKQCYXdazvqQ,36674
|
|
26
|
+
qwt/scale_map.py,sha256=rHmPv7e9voEFuAIInQJq60mTtIGrLMViNlgJYu20znA,9605
|
|
27
27
|
qwt/scale_widget.py,sha256=p202DrRgMraGbVcubzkatvVXOLbO9CPho729hz6gqa0,27125
|
|
28
28
|
qwt/symbol.py,sha256=DIXtIOgniO70pScKF-H_mee8GgBuycW3YcsopvfCRGY,39147
|
|
29
|
-
qwt/text.py,sha256=
|
|
29
|
+
qwt/text.py,sha256=DrrWbEzYA65kcU8aBcKu5YpQ2LzPnOmAO3R7jF8s0J8,48058
|
|
30
30
|
qwt/toqimage.py,sha256=H2u_qDt32n7HGZakWvs3Ve0G3u04Uk2ZQYp8MuZkkmU,1723
|
|
31
31
|
qwt/transform.py,sha256=bFkdmI2wDjOJrUZU6E9-sAvwoo_umXycxq6LPvD6N_c,6078
|
|
32
32
|
qwt/tests/__init__.py,sha256=diBkA_H_Xuxb84tNelsNU4djTlUnKJ5HZNZr02EwnjE,1102
|
|
33
33
|
qwt/tests/comparative_benchmarks.py,sha256=hlLFkyWzadbUyUX2vka_l2bdcGe8U9sOKBDny3o3U40,1821
|
|
34
34
|
qwt/tests/conftest.py,sha256=LwqVA58_Cv7Fitpcn9bGdhbWDBwID_3gaZg2LkJQtGQ,2196
|
|
35
35
|
qwt/tests/test_backingstore.py,sha256=AIiRPqcNPY1riXSQXU43tqB2_z02PhxmK1zA4OxQixM,524
|
|
36
|
-
qwt/tests/test_bodedemo.py,sha256=
|
|
36
|
+
qwt/tests/test_bodedemo.py,sha256=A9iZ4FmTmn96YAwxeIQIPBwIZtj4Jk_ftB78bJAIV_c,9367
|
|
37
37
|
qwt/tests/test_cartesian.py,sha256=0VCrvclqbQJSEGhWpZLq6brA9Qm9oRIgluEBbxOTOds,3855
|
|
38
38
|
qwt/tests/test_cpudemo.py,sha256=1XECvuJwOzMjI5JWCBUwvnQ6dCkcDEqZlhQ7KU8N-Rs,12853
|
|
39
39
|
qwt/tests/test_curvebenchmark1.py,sha256=a53CvA149Ng1Vj4oruKo4Gp952-0A5gLXvRPOiNSYQk,5891
|
|
@@ -49,7 +49,7 @@ qwt/tests/test_loadtest.py,sha256=ZTHatcGAbLyvS0BpbOWbzPD2Bjz4rckF7oxZsUVQ5KM,18
|
|
|
49
49
|
qwt/tests/test_logcurve.py,sha256=XOtR0asSqFfzc0VL0qfXN0fXNvxvLnz5fxmNZ2gzQWk,1129
|
|
50
50
|
qwt/tests/test_mapdemo.py,sha256=e7rvB8IPKYVVii8UgdHSbOHLc3i7eB21EQNKZ151Kc8,3423
|
|
51
51
|
qwt/tests/test_multidemo.py,sha256=YS-P08qcakD41OJy6ZI53ofKOnUvD-2_uum0yOLiIRc,2530
|
|
52
|
-
qwt/tests/test_relativemargin.py,sha256=
|
|
52
|
+
qwt/tests/test_relativemargin.py,sha256=Shn0aE7UeETSSaQFpt4DrMOFeMN3p4rmEHKqPhXfQTI,2097
|
|
53
53
|
qwt/tests/test_simple.py,sha256=4opQp_-RkcVdpRyYSh89ddBkeXCPrppB-_2KTiu5vjI,2347
|
|
54
54
|
qwt/tests/test_stylesheet.py,sha256=cuL1iY4pp5ZoMX4OI86amJA6R5SzQsH1JUWLtEiitZ4,950
|
|
55
55
|
qwt/tests/test_symbols.py,sha256=YcGM0_Em8QtcbqpLSU98PxvGo7BIIYviP2o_4Fg2LbI,5607
|
|
@@ -77,8 +77,8 @@ qwt/tests/data/symbol.svg,sha256=ONkkohVqpg3OO2-XEZge7UZLqqNyOQXbSpLgHXcTBFU,127
|
|
|
77
77
|
qwt/tests/data/symbols.png,sha256=wiqRBc2MJluJ-am9LtZ0K9DHV9BnreYMiEvfzJUFzCk,55586
|
|
78
78
|
qwt/tests/data/testlauncher.png,sha256=WmucObp30QYYKsVRBs5-6t1aEN0FbIWRmOVFN1gt-vQ,139676
|
|
79
79
|
qwt/tests/data/vertical.png,sha256=EvyG6q6rZm6pTT08Y_nzdBYCnqmoX1QDuEQtp2pyGlw,34076
|
|
80
|
-
pythonqwt-0.
|
|
81
|
-
pythonqwt-0.
|
|
82
|
-
pythonqwt-0.
|
|
83
|
-
pythonqwt-0.
|
|
84
|
-
pythonqwt-0.
|
|
80
|
+
pythonqwt-0.16.1.dist-info/METADATA,sha256=bE1-gjZ7myzMI-HDjjIwU-zE6mBgkDAS74jYVfPUwqA,45515
|
|
81
|
+
pythonqwt-0.16.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
82
|
+
pythonqwt-0.16.1.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
|
|
83
|
+
pythonqwt-0.16.1.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
|
|
84
|
+
pythonqwt-0.16.1.dist-info/RECORD,,
|
qwt/__init__.py
CHANGED
|
@@ -22,8 +22,8 @@ External resources:
|
|
|
22
22
|
* Project page on GitHub: `GitHubPage`_
|
|
23
23
|
* Bug reports and feature requests: `GitHub`_
|
|
24
24
|
|
|
25
|
-
.. _PyPI: https://pypi.
|
|
26
|
-
.. _GitHubPage:
|
|
25
|
+
.. _PyPI: https://pypi.org/project/PythonQwt/
|
|
26
|
+
.. _GitHubPage: https://github.com/PlotPyStack/PythonQwt
|
|
27
27
|
.. _GitHub: https://github.com/PlotPyStack/PythonQwt
|
|
28
28
|
"""
|
|
29
29
|
|
|
@@ -63,7 +63,7 @@ from qwt.symbol import QwtSymbol as QSbl # see deprecated section
|
|
|
63
63
|
from qwt.text import QwtText # noqa: F401
|
|
64
64
|
from qwt.toqimage import array_to_qimage as toQImage # noqa: F401
|
|
65
65
|
|
|
66
|
-
__version__ = "0.
|
|
66
|
+
__version__ = "0.16.1"
|
|
67
67
|
QWT_VERSION_STR = "6.1.5"
|
|
68
68
|
|
|
69
69
|
|
qwt/graphic.py
CHANGED
|
@@ -26,7 +26,23 @@ from qtpy.QtGui import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
from qwt.null_paintdevice import QwtNullPaintDevice
|
|
29
|
-
from qwt.painter_command import QwtPainterCommand
|
|
29
|
+
from qwt.painter_command import QwtPainterCommand, _flag_int
|
|
30
|
+
|
|
31
|
+
# See painter_command.py for the rationale: cache the QPaintEngine.DirtyXxx
|
|
32
|
+
# flags as plain ints so the State-replay branch below does plain int bitwise
|
|
33
|
+
# tests instead of going through Python's enum.Flag.__and__ on PyQt6.
|
|
34
|
+
_DIRTY_PEN = _flag_int(QPaintEngine.DirtyPen)
|
|
35
|
+
_DIRTY_BRUSH = _flag_int(QPaintEngine.DirtyBrush)
|
|
36
|
+
_DIRTY_BRUSH_ORIGIN = _flag_int(QPaintEngine.DirtyBrushOrigin)
|
|
37
|
+
_DIRTY_FONT = _flag_int(QPaintEngine.DirtyFont)
|
|
38
|
+
_DIRTY_BACKGROUND = _flag_int(QPaintEngine.DirtyBackground)
|
|
39
|
+
_DIRTY_TRANSFORM = _flag_int(QPaintEngine.DirtyTransform)
|
|
40
|
+
_DIRTY_CLIP_ENABLED = _flag_int(QPaintEngine.DirtyClipEnabled)
|
|
41
|
+
_DIRTY_CLIP_REGION = _flag_int(QPaintEngine.DirtyClipRegion)
|
|
42
|
+
_DIRTY_CLIP_PATH = _flag_int(QPaintEngine.DirtyClipPath)
|
|
43
|
+
_DIRTY_HINTS = _flag_int(QPaintEngine.DirtyHints)
|
|
44
|
+
_DIRTY_COMPOSITION_MODE = _flag_int(QPaintEngine.DirtyCompositionMode)
|
|
45
|
+
_DIRTY_OPACITY = _flag_int(QPaintEngine.DirtyOpacity)
|
|
30
46
|
|
|
31
47
|
|
|
32
48
|
def qwtHasScalablePen(painter):
|
|
@@ -83,35 +99,36 @@ def qwtExecCommand(painter, cmd, renderHints, transform, initialTransform):
|
|
|
83
99
|
painter.drawImage(data.rect, data.image, data.subRect, data.flags)
|
|
84
100
|
elif cmd.type() == QwtPainterCommand.State:
|
|
85
101
|
data = cmd.stateData()
|
|
86
|
-
|
|
102
|
+
flags = _flag_int(data.flags)
|
|
103
|
+
if flags & _DIRTY_PEN:
|
|
87
104
|
painter.setPen(data.pen)
|
|
88
|
-
if
|
|
105
|
+
if flags & _DIRTY_BRUSH:
|
|
89
106
|
painter.setBrush(data.brush)
|
|
90
|
-
if
|
|
107
|
+
if flags & _DIRTY_BRUSH_ORIGIN:
|
|
91
108
|
painter.setBrushOrigin(data.brushOrigin)
|
|
92
|
-
if
|
|
109
|
+
if flags & _DIRTY_FONT:
|
|
93
110
|
painter.setFont(data.font)
|
|
94
|
-
if
|
|
111
|
+
if flags & _DIRTY_BACKGROUND:
|
|
95
112
|
painter.setBackgroundMode(data.backgroundMode)
|
|
96
113
|
painter.setBackground(data.backgroundBrush)
|
|
97
|
-
if
|
|
114
|
+
if flags & _DIRTY_TRANSFORM:
|
|
98
115
|
painter.setTransform(data.transform)
|
|
99
|
-
if
|
|
116
|
+
if flags & _DIRTY_CLIP_ENABLED:
|
|
100
117
|
painter.setClipping(data.isClipEnabled)
|
|
101
|
-
if
|
|
118
|
+
if flags & _DIRTY_CLIP_REGION:
|
|
102
119
|
painter.setClipRegion(data.clipRegion, data.clipOperation)
|
|
103
|
-
if
|
|
120
|
+
if flags & _DIRTY_CLIP_PATH:
|
|
104
121
|
painter.setClipPath(data.clipPath, data.clipOperation)
|
|
105
|
-
if
|
|
122
|
+
if flags & _DIRTY_HINTS:
|
|
106
123
|
for hint in (
|
|
107
124
|
QPainter.Antialiasing,
|
|
108
125
|
QPainter.TextAntialiasing,
|
|
109
126
|
QPainter.SmoothPixmapTransform,
|
|
110
127
|
):
|
|
111
128
|
painter.setRenderHint(hint, bool(data.renderHints & hint))
|
|
112
|
-
if
|
|
129
|
+
if flags & _DIRTY_COMPOSITION_MODE:
|
|
113
130
|
painter.setCompositionMode(data.compositionMode)
|
|
114
|
-
if
|
|
131
|
+
if flags & _DIRTY_OPACITY:
|
|
115
132
|
painter.setOpacity(data.opacity)
|
|
116
133
|
|
|
117
134
|
|
qwt/null_paintdevice.py
CHANGED
|
@@ -272,8 +272,12 @@ class QwtNullPaintDevice(QPaintDevice):
|
|
|
272
272
|
* 25.4
|
|
273
273
|
/ self.metric(QPaintDevice.PdmDpiY)
|
|
274
274
|
)
|
|
275
|
+
elif deviceMetric == QPaintDevice.PdmDevicePixelRatio:
|
|
276
|
+
value = 1
|
|
277
|
+
elif deviceMetric == QPaintDevice.PdmDevicePixelRatioScaled:
|
|
278
|
+
value = 1
|
|
275
279
|
else:
|
|
276
|
-
value =
|
|
280
|
+
value = super(QwtNullPaintDevice, self).metric(deviceMetric)
|
|
277
281
|
return value
|
|
278
282
|
|
|
279
283
|
def drawRects(self, rects, rectCount):
|
qwt/painter_command.py
CHANGED
|
@@ -18,6 +18,40 @@ import copy
|
|
|
18
18
|
from qtpy.QtGui import QPaintEngine, QPainterPath
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _flag_int(flag):
|
|
22
|
+
"""Return the integer value of a Qt enum/flag (PyQt5 and PyQt6).
|
|
23
|
+
|
|
24
|
+
PyQt5 exposes Qt enums as plain ints (``int(flag)`` works). PyQt6 wraps
|
|
25
|
+
them as ``enum.Flag`` instances which are not ``int`` subclasses, so
|
|
26
|
+
``int(flag)`` raises -- the value must be read from ``flag.value``.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
return flag.value
|
|
30
|
+
except AttributeError:
|
|
31
|
+
return int(flag)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Cache QPaintEngine.DirtyXxx flags as plain Python ints once at import time.
|
|
35
|
+
# On PyQt6, Qt enums are full ``enum.Flag`` instances and every ``flags &
|
|
36
|
+
# Member`` test goes through Python's ``enum.__and__`` machinery (~6 us each).
|
|
37
|
+
# In ``QwtPainterCommand.__init__`` below, the State branch performs twelve
|
|
38
|
+
# successive flag tests per painter command -- on PyQt6 alone this accounted
|
|
39
|
+
# for ~20 ms of the residual perf gap on the load test. Casting once to int
|
|
40
|
+
# and bitwise-testing against int constants brings each test back to ~50 ns.
|
|
41
|
+
_DIRTY_PEN = _flag_int(QPaintEngine.DirtyPen)
|
|
42
|
+
_DIRTY_BRUSH = _flag_int(QPaintEngine.DirtyBrush)
|
|
43
|
+
_DIRTY_BRUSH_ORIGIN = _flag_int(QPaintEngine.DirtyBrushOrigin)
|
|
44
|
+
_DIRTY_FONT = _flag_int(QPaintEngine.DirtyFont)
|
|
45
|
+
_DIRTY_BACKGROUND = _flag_int(QPaintEngine.DirtyBackground)
|
|
46
|
+
_DIRTY_TRANSFORM = _flag_int(QPaintEngine.DirtyTransform)
|
|
47
|
+
_DIRTY_CLIP_ENABLED = _flag_int(QPaintEngine.DirtyClipEnabled)
|
|
48
|
+
_DIRTY_CLIP_REGION = _flag_int(QPaintEngine.DirtyClipRegion)
|
|
49
|
+
_DIRTY_CLIP_PATH = _flag_int(QPaintEngine.DirtyClipPath)
|
|
50
|
+
_DIRTY_HINTS = _flag_int(QPaintEngine.DirtyHints)
|
|
51
|
+
_DIRTY_COMPOSITION_MODE = _flag_int(QPaintEngine.DirtyCompositionMode)
|
|
52
|
+
_DIRTY_OPACITY = _flag_int(QPaintEngine.DirtyOpacity)
|
|
53
|
+
|
|
54
|
+
|
|
21
55
|
class PixmapData(object):
|
|
22
56
|
def __init__(self):
|
|
23
57
|
self.rect = None
|
|
@@ -125,32 +159,35 @@ class QwtPainterCommand(object):
|
|
|
125
159
|
self.__type = self.State
|
|
126
160
|
self.__stateData = StateData()
|
|
127
161
|
self.__stateData.flags = state.state()
|
|
128
|
-
|
|
162
|
+
# Cast to int once: subsequent bitwise tests are done against
|
|
163
|
+
# the cached _DIRTY_* int constants (see top of module).
|
|
164
|
+
flags = _flag_int(self.__stateData.flags)
|
|
165
|
+
if flags & _DIRTY_PEN:
|
|
129
166
|
self.__stateData.pen = state.pen()
|
|
130
|
-
if
|
|
167
|
+
if flags & _DIRTY_BRUSH:
|
|
131
168
|
self.__stateData.brush = state.brush()
|
|
132
|
-
if
|
|
169
|
+
if flags & _DIRTY_BRUSH_ORIGIN:
|
|
133
170
|
self.__stateData.brushOrigin = state.brushOrigin()
|
|
134
|
-
if
|
|
171
|
+
if flags & _DIRTY_FONT:
|
|
135
172
|
self.__stateData.font = state.font()
|
|
136
|
-
if
|
|
173
|
+
if flags & _DIRTY_BACKGROUND:
|
|
137
174
|
self.__stateData.backgroundMode = state.backgroundMode()
|
|
138
175
|
self.__stateData.backgroundBrush = state.backgroundBrush()
|
|
139
|
-
if
|
|
176
|
+
if flags & _DIRTY_TRANSFORM:
|
|
140
177
|
self.__stateData.transform = state.transform()
|
|
141
|
-
if
|
|
178
|
+
if flags & _DIRTY_CLIP_ENABLED:
|
|
142
179
|
self.__stateData.isClipEnabled = state.isClipEnabled()
|
|
143
|
-
if
|
|
180
|
+
if flags & _DIRTY_CLIP_REGION:
|
|
144
181
|
self.__stateData.clipRegion = state.clipRegion()
|
|
145
182
|
self.__stateData.clipOperation = state.clipOperation()
|
|
146
|
-
if
|
|
183
|
+
if flags & _DIRTY_CLIP_PATH:
|
|
147
184
|
self.__stateData.clipPath = state.clipPath()
|
|
148
185
|
self.__stateData.clipOperation = state.clipOperation()
|
|
149
|
-
if
|
|
186
|
+
if flags & _DIRTY_HINTS:
|
|
150
187
|
self.__stateData.renderHints = state.renderHints()
|
|
151
|
-
if
|
|
188
|
+
if flags & _DIRTY_COMPOSITION_MODE:
|
|
152
189
|
self.__stateData.compositionMode = state.compositionMode()
|
|
153
|
-
if
|
|
190
|
+
if flags & _DIRTY_OPACITY:
|
|
154
191
|
self.__stateData.opacity = state.opacity()
|
|
155
192
|
elif len(args) == 3:
|
|
156
193
|
rect, pixmap, subRect = args
|
qwt/plot_canvas.py
CHANGED
|
@@ -787,7 +787,7 @@ class QwtPlotCanvas(QFrame):
|
|
|
787
787
|
import warnings
|
|
788
788
|
|
|
789
789
|
warnings.warn(
|
|
790
|
-
"`invalidatePaintCache` has been removed:
|
|
790
|
+
"`invalidatePaintCache` has been removed: please use `replot` instead",
|
|
791
791
|
RuntimeWarning,
|
|
792
792
|
)
|
|
793
793
|
self.replot()
|
qwt/scale_div.py
CHANGED
|
@@ -235,9 +235,11 @@ class QwtScaleDiv(object):
|
|
|
235
235
|
:param float value: Value
|
|
236
236
|
:return: True/False
|
|
237
237
|
"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
lb = self.__lowerBound
|
|
239
|
+
ub = self.__upperBound
|
|
240
|
+
if lb <= ub:
|
|
241
|
+
return lb <= value <= ub
|
|
242
|
+
return ub <= value <= lb
|
|
241
243
|
|
|
242
244
|
def invert(self):
|
|
243
245
|
"""
|
qwt/scale_draw.py
CHANGED
|
@@ -39,11 +39,23 @@ from qwt.scale_div import QwtScaleDiv
|
|
|
39
39
|
from qwt.scale_map import QwtScaleMap
|
|
40
40
|
from qwt.text import QwtText
|
|
41
41
|
|
|
42
|
+
# Plain-int aliases for Qt alignment flags. Qt6 exposes alignment flags as
|
|
43
|
+
# IntEnum members and bitwise operations on them go through Python's
|
|
44
|
+
# enum machinery (`__and__`/`__call__`), which is one of the dominant costs
|
|
45
|
+
# of label layout. Casting to int once and using these constants makes the
|
|
46
|
+
# bitwise tests in `labelTransformation` ~10x cheaper without changing
|
|
47
|
+
# semantics.
|
|
48
|
+
_ALIGN_LEFT = int(Qt.AlignLeft)
|
|
49
|
+
_ALIGN_RIGHT = int(Qt.AlignRight)
|
|
50
|
+
_ALIGN_TOP = int(Qt.AlignTop)
|
|
51
|
+
_ALIGN_BOTTOM = int(Qt.AlignBottom)
|
|
52
|
+
|
|
42
53
|
|
|
43
54
|
class QwtAbstractScaleDraw_PrivateData(QObject):
|
|
55
|
+
# QObject base class restored for Qt parent/child ownership semantics.
|
|
56
|
+
|
|
44
57
|
def __init__(self):
|
|
45
58
|
QObject.__init__(self)
|
|
46
|
-
|
|
47
59
|
self.spacing = 4
|
|
48
60
|
self.penWidth = 0
|
|
49
61
|
self.minExtent = 0.0
|
|
@@ -471,11 +483,15 @@ class QwtAbstractScaleDraw(object):
|
|
|
471
483
|
|
|
472
484
|
|
|
473
485
|
class QwtScaleDraw_PrivateData(QObject):
|
|
486
|
+
# QObject base class restored for Qt parent/child ownership semantics.
|
|
487
|
+
|
|
474
488
|
def __init__(self):
|
|
475
489
|
QObject.__init__(self)
|
|
476
|
-
|
|
477
490
|
self.len = 0
|
|
478
491
|
self.alignment = QwtScaleDraw.BottomScale
|
|
492
|
+
# Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment``
|
|
493
|
+
# so that the very hot ``orientation()`` accessor avoids any test.
|
|
494
|
+
self.orientation = Qt.Horizontal
|
|
479
495
|
self.labelAlignment = 0
|
|
480
496
|
self.labelRotation = 0.0
|
|
481
497
|
self.labelAutoSize = True
|
|
@@ -554,6 +570,11 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
554
570
|
:py:meth:`alignment()`
|
|
555
571
|
"""
|
|
556
572
|
self.__data.alignment = align
|
|
573
|
+
# Keep cached orientation in sync (see ``orientation()``).
|
|
574
|
+
if align == self.BottomScale or align == self.TopScale:
|
|
575
|
+
self.__data.orientation = Qt.Horizontal
|
|
576
|
+
else:
|
|
577
|
+
self.__data.orientation = Qt.Vertical
|
|
557
578
|
|
|
558
579
|
def orientation(self):
|
|
559
580
|
"""
|
|
@@ -568,10 +589,8 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
568
589
|
|
|
569
590
|
:py:meth:`alignment()`
|
|
570
591
|
"""
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
elif self.__data.alignment in (self.LeftScale, self.RightScale):
|
|
574
|
-
return Qt.Vertical
|
|
592
|
+
# Pre-computed by ``setAlignment`` - this method is called per tick.
|
|
593
|
+
return self.__data.orientation
|
|
575
594
|
|
|
576
595
|
def getBorderDistHint(self, font):
|
|
577
596
|
"""
|
|
@@ -597,17 +616,19 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
597
616
|
if len(ticks) == 0:
|
|
598
617
|
return start, end
|
|
599
618
|
|
|
619
|
+
scale_map = self.scaleMap()
|
|
620
|
+
transform = scale_map.transform
|
|
600
621
|
minTick = ticks[0]
|
|
601
|
-
minPos =
|
|
622
|
+
minPos = transform(minTick)
|
|
602
623
|
maxTick = minTick
|
|
603
624
|
maxPos = minPos
|
|
604
625
|
|
|
605
626
|
for tick in ticks:
|
|
606
|
-
tickPos =
|
|
627
|
+
tickPos = transform(tick)
|
|
607
628
|
if tickPos < minPos:
|
|
608
629
|
minTick = tick
|
|
609
630
|
minPos = tickPos
|
|
610
|
-
if tickPos >
|
|
631
|
+
if tickPos > maxPos:
|
|
611
632
|
maxTick = tick
|
|
612
633
|
maxPos = tickPos
|
|
613
634
|
|
|
@@ -615,16 +636,16 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
615
636
|
e = 0.0
|
|
616
637
|
if self.orientation() == Qt.Vertical:
|
|
617
638
|
s = -self.labelRect(font, minTick).top()
|
|
618
|
-
s -= abs(minPos - round(
|
|
639
|
+
s -= abs(minPos - round(scale_map.p2()))
|
|
619
640
|
|
|
620
641
|
e = self.labelRect(font, maxTick).bottom()
|
|
621
|
-
e -= abs(maxPos -
|
|
642
|
+
e -= abs(maxPos - scale_map.p1())
|
|
622
643
|
else:
|
|
623
644
|
s = -self.labelRect(font, minTick).left()
|
|
624
|
-
s -= abs(minPos -
|
|
645
|
+
s -= abs(minPos - scale_map.p1())
|
|
625
646
|
|
|
626
647
|
e = self.labelRect(font, maxTick).right()
|
|
627
|
-
e -= abs(maxPos -
|
|
648
|
+
e -= abs(maxPos - scale_map.p2())
|
|
628
649
|
|
|
629
650
|
return max(math.ceil(s), 0), max(math.ceil(e), 0)
|
|
630
651
|
|
|
@@ -763,27 +784,22 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
763
784
|
"""
|
|
764
785
|
tval = self.scaleMap().transform(value)
|
|
765
786
|
dist = self.spacing()
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
787
|
+
hasComponent = self.hasComponent
|
|
788
|
+
if hasComponent(QwtAbstractScaleDraw.Backbone):
|
|
789
|
+
dist += max(1, self.penWidth())
|
|
790
|
+
if hasComponent(QwtAbstractScaleDraw.Ticks):
|
|
769
791
|
dist += self.tickLength(QwtScaleDiv.MajorTick)
|
|
770
792
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
if
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
py = self.__data.pos.y() + dist
|
|
782
|
-
elif self.alignment() == self.TopScale:
|
|
783
|
-
px = tval
|
|
784
|
-
py = self.__data.pos.y() - dist
|
|
785
|
-
|
|
786
|
-
return QPointF(px, py)
|
|
793
|
+
alignment = self.alignment()
|
|
794
|
+
pos = self.__data.pos
|
|
795
|
+
if alignment == self.RightScale:
|
|
796
|
+
return QPointF(pos.x() + dist, tval)
|
|
797
|
+
if alignment == self.LeftScale:
|
|
798
|
+
return QPointF(pos.x() - dist, tval)
|
|
799
|
+
if alignment == self.BottomScale:
|
|
800
|
+
return QPointF(tval, pos.y() + dist)
|
|
801
|
+
# TopScale
|
|
802
|
+
return QPointF(tval, pos.y() - dist)
|
|
787
803
|
|
|
788
804
|
def drawTick(self, painter, value, len_):
|
|
789
805
|
"""
|
|
@@ -1007,17 +1023,19 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
1007
1023
|
flags = self.labelAlignment()
|
|
1008
1024
|
if flags == 0:
|
|
1009
1025
|
flags = self.Flags[self.alignment()]
|
|
1026
|
+
# Cast to plain int once to avoid the per-bit Qt6 enum overhead.
|
|
1027
|
+
flags = int(flags)
|
|
1010
1028
|
|
|
1011
|
-
if flags &
|
|
1029
|
+
if flags & _ALIGN_LEFT:
|
|
1012
1030
|
x = -size.width()
|
|
1013
|
-
elif flags &
|
|
1031
|
+
elif flags & _ALIGN_RIGHT:
|
|
1014
1032
|
x = 0.0
|
|
1015
1033
|
else:
|
|
1016
1034
|
x = -(0.5 * size.width())
|
|
1017
1035
|
|
|
1018
|
-
if flags &
|
|
1036
|
+
if flags & _ALIGN_TOP:
|
|
1019
1037
|
y = -size.height()
|
|
1020
|
-
elif flags &
|
|
1038
|
+
elif flags & _ALIGN_BOTTOM:
|
|
1021
1039
|
y = 0
|
|
1022
1040
|
else:
|
|
1023
1041
|
y = -(0.5 * size.height())
|
|
@@ -1039,6 +1057,31 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
|
|
|
1039
1057
|
lbl, labelSize = self.tickLabel(font, value)
|
|
1040
1058
|
if not lbl or lbl.isEmpty():
|
|
1041
1059
|
return QRectF(0.0, 0.0, 0.0, 0.0)
|
|
1060
|
+
# Fast path: when the label is not rotated, the contribution of
|
|
1061
|
+
# ``pos`` cancels out (transform.translate(pos) followed by
|
|
1062
|
+
# br.translate(-pos)). This avoids ``labelPosition``,
|
|
1063
|
+
# ``labelTransformation`` and ``QTransform.mapRect`` entirely - all
|
|
1064
|
+
# of which are dominant costs in tick-heavy layouts.
|
|
1065
|
+
if self.labelRotation() == 0.0:
|
|
1066
|
+
flags = self.labelAlignment()
|
|
1067
|
+
if flags == 0:
|
|
1068
|
+
flags = self.Flags[self.alignment()]
|
|
1069
|
+
flags = int(flags)
|
|
1070
|
+
w = labelSize.width()
|
|
1071
|
+
h = labelSize.height()
|
|
1072
|
+
if flags & _ALIGN_LEFT:
|
|
1073
|
+
x = -w
|
|
1074
|
+
elif flags & _ALIGN_RIGHT:
|
|
1075
|
+
x = 0.0
|
|
1076
|
+
else:
|
|
1077
|
+
x = -0.5 * w
|
|
1078
|
+
if flags & _ALIGN_TOP:
|
|
1079
|
+
y = -h
|
|
1080
|
+
elif flags & _ALIGN_BOTTOM:
|
|
1081
|
+
y = 0.0
|
|
1082
|
+
else:
|
|
1083
|
+
y = -0.5 * h
|
|
1084
|
+
return QRectF(x, y, w, h)
|
|
1042
1085
|
pos = self.labelPosition(value)
|
|
1043
1086
|
transform = self.labelTransformation(pos, labelSize)
|
|
1044
1087
|
br = transform.mapRect(QRectF(QPointF(0, 0), labelSize))
|
qwt/scale_engine.py
CHANGED
|
@@ -324,11 +324,10 @@ class QwtScaleEngine(object):
|
|
|
324
324
|
"""
|
|
325
325
|
if not interval.isValid():
|
|
326
326
|
return False
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return True
|
|
327
|
+
min_v = interval.minValue()
|
|
328
|
+
max_v = interval.maxValue()
|
|
329
|
+
eps = abs(1.0e-6 * (max_v - min_v))
|
|
330
|
+
return not (min_v - value > eps or value - max_v > eps)
|
|
332
331
|
|
|
333
332
|
def strip(self, ticks, interval):
|
|
334
333
|
"""
|
|
@@ -340,9 +339,17 @@ class QwtScaleEngine(object):
|
|
|
340
339
|
"""
|
|
341
340
|
if not interval.isValid() or not ticks:
|
|
342
341
|
return []
|
|
343
|
-
|
|
342
|
+
# Inline ``contains`` to avoid one Python call per tick: ``strip`` is
|
|
343
|
+
# called by buildTicks for every layout pass and is one of the
|
|
344
|
+
# dominant costs in tick-heavy plots.
|
|
345
|
+
min_v = interval.minValue()
|
|
346
|
+
max_v = interval.maxValue()
|
|
347
|
+
eps = abs(1.0e-6 * (max_v - min_v))
|
|
348
|
+
lo = min_v - eps
|
|
349
|
+
hi = max_v + eps
|
|
350
|
+
if lo <= ticks[0] and ticks[-1] <= hi:
|
|
344
351
|
return ticks
|
|
345
|
-
return [tick for tick in ticks if
|
|
352
|
+
return [tick for tick in ticks if lo <= tick <= hi]
|
|
346
353
|
|
|
347
354
|
def buildInterval(self, value):
|
|
348
355
|
"""
|
|
@@ -594,7 +601,7 @@ class QwtLinearScaleEngine(QwtScaleEngine):
|
|
|
594
601
|
numTicks = int(math.ceil(abs(stepSize / minStep)) - 1)
|
|
595
602
|
medIndex = -1
|
|
596
603
|
if numTicks % 2:
|
|
597
|
-
medIndex = numTicks
|
|
604
|
+
medIndex = numTicks // 2
|
|
598
605
|
for val in ticks[QwtScaleDiv.MajorTick]:
|
|
599
606
|
for k in range(numTicks):
|
|
600
607
|
val += minStep
|
|
@@ -837,7 +844,7 @@ class QwtLogScaleEngine(QwtScaleEngine):
|
|
|
837
844
|
|
|
838
845
|
mediumTickIndex = -1
|
|
839
846
|
if numSteps > 2 and numSteps % 2 == 0:
|
|
840
|
-
mediumTickIndex = numSteps
|
|
847
|
+
mediumTickIndex = numSteps // 2
|
|
841
848
|
|
|
842
849
|
for v in ticks[QwtScaleDiv.MajorTick]:
|
|
843
850
|
s = logBase / numSteps
|
|
@@ -872,7 +879,7 @@ class QwtLogScaleEngine(QwtScaleEngine):
|
|
|
872
879
|
|
|
873
880
|
mediumTickIndex = -1
|
|
874
881
|
if numTicks > 2 and numTicks % 2:
|
|
875
|
-
mediumTickIndex = numTicks
|
|
882
|
+
mediumTickIndex = numTicks // 2
|
|
876
883
|
|
|
877
884
|
minFactor = max([math.pow(logBase, minStep), float(logBase)])
|
|
878
885
|
|
qwt/scale_map.py
CHANGED
|
@@ -217,8 +217,12 @@ class QwtScaleMap(object):
|
|
|
217
217
|
if self.__transform:
|
|
218
218
|
self.__ts1 = self.__transform.transform(self.__ts1)
|
|
219
219
|
ts2 = self.__transform.transform(ts2)
|
|
220
|
-
self.
|
|
221
|
-
|
|
220
|
+
if self.__ts1 == ts2:
|
|
221
|
+
# Degenerate scale: collapse every value to ``p1`` (matches the
|
|
222
|
+
# symmetric guard in ``invTransform_scalar`` and the C++ Qwt
|
|
223
|
+
# behaviour).
|
|
224
|
+
self.__cnv = 0.0
|
|
225
|
+
else:
|
|
222
226
|
self.__cnv = (self.__p2 - self.__p1) / (ts2 - self.__ts1)
|
|
223
227
|
|
|
224
228
|
def transform(self, *args):
|
|
@@ -245,13 +249,18 @@ class QwtScaleMap(object):
|
|
|
245
249
|
|
|
246
250
|
:py:meth:`invTransform()`
|
|
247
251
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
nargs = len(args)
|
|
253
|
+
if nargs == 1:
|
|
254
|
+
# Scalar transform: inline the fast path for the dominant case
|
|
255
|
+
# (avoids one Python call frame per tick label).
|
|
256
|
+
s = args[0]
|
|
257
|
+
if self.__transform:
|
|
258
|
+
s = self.__transform.transform(s)
|
|
259
|
+
return self.__p1 + (s - self.__ts1) * self.__cnv
|
|
260
|
+
elif nargs == 3 and isinstance(args[2], QPointF):
|
|
252
261
|
xMap, yMap, pos = args
|
|
253
262
|
return QPointF(xMap.transform(pos.x()), yMap.transform(pos.y()))
|
|
254
|
-
elif
|
|
263
|
+
elif nargs == 3 and isinstance(args[2], QRectF):
|
|
255
264
|
xMap, yMap, rect = args
|
|
256
265
|
x1 = xMap.transform(rect.left())
|
|
257
266
|
x2 = xMap.transform(rect.right())
|
|
@@ -269,7 +278,7 @@ class QwtScaleMap(object):
|
|
|
269
278
|
y1 = 0.0
|
|
270
279
|
if qwtFuzzyCompare(y2, 0.0, y2 - y1) == 0:
|
|
271
280
|
y2 = 0.0
|
|
272
|
-
return QRectF(x1, y1, x2 - x1
|
|
281
|
+
return QRectF(x1, y1, x2 - x1, y2 - y1)
|
|
273
282
|
else:
|
|
274
283
|
raise TypeError(
|
|
275
284
|
"%s().transform() takes 1 or 3 argument(s) (%s "
|
|
@@ -292,8 +301,8 @@ class QwtScaleMap(object):
|
|
|
292
301
|
elif isinstance(args[2], QRectF):
|
|
293
302
|
xMap, yMap, rect = args
|
|
294
303
|
x1 = xMap.invTransform(rect.left())
|
|
295
|
-
x2 = xMap.invTransform(rect.right()
|
|
304
|
+
x2 = xMap.invTransform(rect.right())
|
|
296
305
|
y1 = yMap.invTransform(rect.top())
|
|
297
|
-
y2 = yMap.invTransform(rect.bottom()
|
|
306
|
+
y2 = yMap.invTransform(rect.bottom())
|
|
298
307
|
r = QRectF(x1, y1, x2 - x1, y2 - y1)
|
|
299
308
|
return r.normalized()
|
qwt/tests/test_bodedemo.py
CHANGED
|
@@ -144,8 +144,7 @@ class BodePlot(QwtPlot):
|
|
|
144
144
|
yvalue=-20.0,
|
|
145
145
|
align=Qt.AlignRight | Qt.AlignBottom,
|
|
146
146
|
label=QwtText.make(
|
|
147
|
-
"[1-(\u03c9/\u03c9<sub>0</sub>)<sup>2</sup>+2j\u03c9/Q]"
|
|
148
|
-
"<sup>-1</sup>",
|
|
147
|
+
"[1-(\u03c9/\u03c9<sub>0</sub>)<sup>2</sup>+2j\u03c9/Q]<sup>-1</sup>",
|
|
149
148
|
color=Qt.white,
|
|
150
149
|
borderradius=2,
|
|
151
150
|
borderpen=QPen(Qt.lightGray, 5),
|
qwt/tests/test_relativemargin.py
CHANGED
|
@@ -30,9 +30,9 @@ class RelativeMarginDemo(QW.QWidget):
|
|
|
30
30
|
def_margin = plot.axisMargin(qwt.QwtPlot.yLeft)
|
|
31
31
|
scale_str = "lin/lin" if not log_scale else "log/lin"
|
|
32
32
|
if relative_margin is None:
|
|
33
|
-
margin_str = f"default ({def_margin*100:.0f}%)"
|
|
33
|
+
margin_str = f"default ({def_margin * 100:.0f}%)"
|
|
34
34
|
else:
|
|
35
|
-
margin_str = f"{relative_margin*100:.0f}%"
|
|
35
|
+
margin_str = f"{relative_margin * 100:.0f}%"
|
|
36
36
|
plot.setTitle(f"{scale_str}, margin: {margin_str}")
|
|
37
37
|
if relative_margin is not None:
|
|
38
38
|
plot.setAxisMargin(qwt.QwtPlot.yLeft, relative_margin)
|
qwt/text.py
CHANGED
|
@@ -72,13 +72,34 @@ QWIDGETSIZE_MAX = (1 << 24) - 1
|
|
|
72
72
|
QT_API = os.environ["QT_API"]
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
# Cache Qt alignment flags as plain ints once at import time. On PyQt6 these
|
|
76
|
+
# are ``Qt.AlignmentFlag`` enum members and every bitwise test goes through
|
|
77
|
+
# ``enum.__and__`` (~6 us each). The test code below combines them in hot
|
|
78
|
+
# paths called per-tick / per-label / per-paint event.
|
|
79
|
+
def _flag_int(flag):
|
|
80
|
+
"""Return the integer value of a Qt enum/flag (PyQt5 and PyQt6)."""
|
|
81
|
+
try:
|
|
82
|
+
return flag.value
|
|
83
|
+
except AttributeError:
|
|
84
|
+
return int(flag)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
_ALIGN_LEFT = _flag_int(Qt.AlignLeft)
|
|
88
|
+
_ALIGN_RIGHT = _flag_int(Qt.AlignRight)
|
|
89
|
+
_ALIGN_TOP = _flag_int(Qt.AlignTop)
|
|
90
|
+
_ALIGN_BOTTOM = _flag_int(Qt.AlignBottom)
|
|
91
|
+
_ALIGN_HCENTER = _flag_int(Qt.AlignHCenter)
|
|
92
|
+
_ALIGN_JUSTIFY = _flag_int(Qt.AlignJustify)
|
|
93
|
+
_ALIGN_CENTER = _flag_int(Qt.AlignCenter)
|
|
94
|
+
|
|
95
|
+
|
|
75
96
|
def taggedRichText(text, flags):
|
|
76
97
|
richText = text
|
|
77
|
-
if flags &
|
|
98
|
+
if flags & _ALIGN_JUSTIFY:
|
|
78
99
|
richText = '<div align="justify">' + richText + "</div>"
|
|
79
|
-
elif flags &
|
|
100
|
+
elif flags & _ALIGN_RIGHT:
|
|
80
101
|
richText = '<div align="right">' + richText + "</div>"
|
|
81
|
-
elif flags &
|
|
102
|
+
elif flags & _ALIGN_HCENTER:
|
|
82
103
|
richText = '<div align="center">' + richText + "</div>"
|
|
83
104
|
return richText
|
|
84
105
|
|
|
@@ -189,6 +210,66 @@ class QwtTextEngine(object):
|
|
|
189
210
|
|
|
190
211
|
ASCENTCACHE = {}
|
|
191
212
|
|
|
213
|
+
# Module-level cache: ``id(font) -> tuple_key`` (fast path) and
|
|
214
|
+
# ``tuple_key -> tuple_key`` (slow path). The tuple key is built from a
|
|
215
|
+
# handful of QFont attributes that uniquely identify the *logical* font for
|
|
216
|
+
# metrics purposes. Tick-rendering uses very few distinct fonts in practice
|
|
217
|
+
# so both dicts stay tiny.
|
|
218
|
+
#
|
|
219
|
+
# This replaces the previous ``id(font) -> font.key()`` design. Two reasons:
|
|
220
|
+
#
|
|
221
|
+
# 1. ``QFont.key()`` is a sip dispatch that costs ~3.3 us/call on PyQt5 and
|
|
222
|
+
# ~9.3 us/call on PyQt6 -- it became the single biggest residual hotspot
|
|
223
|
+
# in ``QwtText.textSize`` on PyQt6.
|
|
224
|
+
# 2. PyQt6 returns a fresh Python wrapper around the same QFont on most
|
|
225
|
+
# calls, so ``id(font)`` changes between calls and the id-keyed fast path
|
|
226
|
+
# misses ~92% of the time. The tuple-key second level recovers the hits
|
|
227
|
+
# those misses would have produced, without paying for ``font.key()``.
|
|
228
|
+
#
|
|
229
|
+
# The tuple key uses ``(family, pixelSize-or-pointSizeF, weight, italic,
|
|
230
|
+
# stretch, styleStrategy)``. This is what determines ``QFontMetrics`` output
|
|
231
|
+
# in practice; if two QFonts share these values they share metrics.
|
|
232
|
+
|
|
233
|
+
_FONT_KEY_CACHE: dict = {} # id(font) -> tuple_key (fast path)
|
|
234
|
+
_FONT_TUPLE_CACHE: dict = {} # tuple_key -> tuple_key (interning, also acts
|
|
235
|
+
# as the "have we seen this logical font" set)
|
|
236
|
+
_FONT_KEY_CACHE_LIMIT = 1024
|
|
237
|
+
_FM_CACHE_LIMIT = 256 # max QFontMetrics/QFontMetricsF entries per engine
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _font_tuple_key(font):
|
|
241
|
+
"""Build a hashable tuple identifying the logical font."""
|
|
242
|
+
px = font.pixelSize()
|
|
243
|
+
return (
|
|
244
|
+
font.family(),
|
|
245
|
+
px if px > 0 else font.pointSizeF(),
|
|
246
|
+
font.weight(),
|
|
247
|
+
font.italic(),
|
|
248
|
+
font.stretch(),
|
|
249
|
+
font.styleStrategy(),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def font_key_cached(font):
|
|
254
|
+
"""Return a hashable cache key uniquely identifying ``font`` for metrics.
|
|
255
|
+
|
|
256
|
+
The returned value is **not** ``QFont.key()`` -- it is a tuple computed
|
|
257
|
+
from a handful of QFont attributes. It is safe to use as a dict key for
|
|
258
|
+
metrics caches (callers in this module always compare by ``==`` only).
|
|
259
|
+
"""
|
|
260
|
+
fid = id(font)
|
|
261
|
+
entry = _FONT_KEY_CACHE.get(fid)
|
|
262
|
+
if entry is not None:
|
|
263
|
+
return entry[1]
|
|
264
|
+
tkey = _font_tuple_key(font)
|
|
265
|
+
# Intern: reuse the same tuple object across all id() variants so dict
|
|
266
|
+
# lookups in caller-side caches benefit from object-identity hash hits.
|
|
267
|
+
interned = _FONT_TUPLE_CACHE.setdefault(tkey, tkey)
|
|
268
|
+
if len(_FONT_KEY_CACHE) >= _FONT_KEY_CACHE_LIMIT:
|
|
269
|
+
_FONT_KEY_CACHE.clear()
|
|
270
|
+
_FONT_KEY_CACHE[fid] = (font, interned)
|
|
271
|
+
return interned
|
|
272
|
+
|
|
192
273
|
|
|
193
274
|
def get_screen_resolution():
|
|
194
275
|
"""Return screen resolution: tuple of floats (DPIx, DPIy)"""
|
|
@@ -226,19 +307,28 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
226
307
|
self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
|
|
227
308
|
self._fm_cache = {}
|
|
228
309
|
self._fm_cache_f = {}
|
|
310
|
+
self._margins_cache = {}
|
|
311
|
+
# Fast path: when textMargins is called repeatedly with the same
|
|
312
|
+
# QFont instance, skip the (expensive) font.key() Qt call.
|
|
313
|
+
self._margins_last_id = -1
|
|
314
|
+
self._margins_last_value = None
|
|
229
315
|
|
|
230
316
|
def fontmetrics(self, font):
|
|
231
|
-
fid = font
|
|
317
|
+
fid = font_key_cached(font)
|
|
232
318
|
try:
|
|
233
319
|
return self._fm_cache[fid]
|
|
234
320
|
except KeyError:
|
|
321
|
+
if len(self._fm_cache) >= _FM_CACHE_LIMIT:
|
|
322
|
+
self._fm_cache.clear()
|
|
235
323
|
return self._fm_cache.setdefault(fid, QFontMetrics(font))
|
|
236
324
|
|
|
237
325
|
def fontmetrics_f(self, font):
|
|
238
|
-
fid = font
|
|
326
|
+
fid = font_key_cached(font)
|
|
239
327
|
try:
|
|
240
328
|
return self._fm_cache_f[fid]
|
|
241
329
|
except KeyError:
|
|
330
|
+
if len(self._fm_cache_f) >= _FM_CACHE_LIMIT:
|
|
331
|
+
self._fm_cache_f.clear()
|
|
242
332
|
return self._fm_cache_f.setdefault(fid, QFontMetricsF(font))
|
|
243
333
|
|
|
244
334
|
def heightForWidth(self, font, flags, text, width):
|
|
@@ -270,10 +360,12 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
270
360
|
|
|
271
361
|
def effectiveAscent(self, font):
|
|
272
362
|
global ASCENTCACHE
|
|
273
|
-
fontKey = font
|
|
363
|
+
fontKey = font_key_cached(font)
|
|
274
364
|
ascent = ASCENTCACHE.get(fontKey)
|
|
275
365
|
if ascent is not None:
|
|
276
366
|
return ascent
|
|
367
|
+
if len(ASCENTCACHE) >= _FM_CACHE_LIMIT:
|
|
368
|
+
ASCENTCACHE.clear()
|
|
277
369
|
return ASCENTCACHE.setdefault(fontKey, self.findAscent(font))
|
|
278
370
|
|
|
279
371
|
def findAscent(self, font):
|
|
@@ -317,11 +409,21 @@ class QwtPlainTextEngine(QwtTextEngine):
|
|
|
317
409
|
:param QFont font: Font of the text
|
|
318
410
|
:return: tuple (left, right, top, bottom) representing margins
|
|
319
411
|
"""
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
412
|
+
# Fast path: same QFont object as the previous call.
|
|
413
|
+
font_id = id(font)
|
|
414
|
+
if font_id == self._margins_last_id:
|
|
415
|
+
return self._margins_last_value
|
|
416
|
+
fkey = font_key_cached(font)
|
|
417
|
+
cached = self._margins_cache.get(fkey)
|
|
418
|
+
if cached is None:
|
|
419
|
+
fm = self.fontmetrics(font)
|
|
420
|
+
cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent())
|
|
421
|
+
if len(self._margins_cache) >= _FM_CACHE_LIMIT:
|
|
422
|
+
self._margins_cache.clear()
|
|
423
|
+
self._margins_cache[fkey] = cached
|
|
424
|
+
self._margins_last_id = font_id
|
|
425
|
+
self._margins_last_value = cached
|
|
426
|
+
return cached
|
|
325
427
|
|
|
326
428
|
def draw(self, painter, rect, flags, text):
|
|
327
429
|
"""
|
|
@@ -465,9 +567,10 @@ class QwtRichTextEngine(QwtTextEngine):
|
|
|
465
567
|
|
|
466
568
|
|
|
467
569
|
class QwtText_PrivateData(QObject):
|
|
570
|
+
# QObject base class restored for Qt parent/child ownership semantics.
|
|
571
|
+
|
|
468
572
|
def __init__(self):
|
|
469
573
|
QObject.__init__(self)
|
|
470
|
-
|
|
471
574
|
self.renderFlags = Qt.AlignCenter
|
|
472
575
|
self.borderRadius = 0
|
|
473
576
|
self.borderPen = Qt.NoPen
|
|
@@ -484,10 +587,13 @@ class QwtText_PrivateData(QObject):
|
|
|
484
587
|
class QwtText_LayoutCache(object):
|
|
485
588
|
def __init__(self):
|
|
486
589
|
self.textSize = None
|
|
487
|
-
self.
|
|
590
|
+
self.fontKey = None
|
|
591
|
+
self.fontId = -1
|
|
488
592
|
|
|
489
593
|
def invalidate(self):
|
|
490
594
|
self.textSize = None
|
|
595
|
+
self.fontKey = None
|
|
596
|
+
self.fontId = -1
|
|
491
597
|
|
|
492
598
|
|
|
493
599
|
class QwtText(object):
|
|
@@ -727,7 +833,13 @@ class QwtText(object):
|
|
|
727
833
|
:py:meth:`renderFlags()`,
|
|
728
834
|
:py:meth:`qwt.text.QwtTextEngine.draw()`
|
|
729
835
|
"""
|
|
730
|
-
|
|
836
|
+
# Wrap into Qt.AlignmentFlag so that downstream Qt APIs (notably
|
|
837
|
+
# ``QTextOption.setAlignment``, ``QPainter.drawText``,
|
|
838
|
+
# ``QFontMetrics.boundingRect``) that strictly require an enum on
|
|
839
|
+
# PyQt6 keep working. Hot bitwise-test sites locally cast back to
|
|
840
|
+
# int to avoid the per-test enum.__and__ cost.
|
|
841
|
+
if not isinstance(renderFlags, Qt.AlignmentFlag):
|
|
842
|
+
renderFlags = Qt.AlignmentFlag(renderFlags)
|
|
731
843
|
if renderFlags != self.__data.renderFlags:
|
|
732
844
|
self.__data.renderFlags = renderFlags
|
|
733
845
|
self.__layoutCache.invalidate()
|
|
@@ -994,17 +1106,24 @@ class QwtText(object):
|
|
|
994
1106
|
:param QFont defaultFont Font, used for the calculation if the text has no font
|
|
995
1107
|
:return: Caluclated size
|
|
996
1108
|
"""
|
|
997
|
-
font =
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1109
|
+
font = self.usedFont(defaultFont)
|
|
1110
|
+
cache = self.__layoutCache
|
|
1111
|
+
font_id = id(font)
|
|
1112
|
+
if cache.textSize is not None and cache.fontId == font_id:
|
|
1113
|
+
sz = QSizeF(cache.textSize)
|
|
1114
|
+
else:
|
|
1115
|
+
fkey = font_key_cached(font)
|
|
1116
|
+
if (
|
|
1117
|
+
cache.textSize is None
|
|
1118
|
+
or not cache.textSize.isValid()
|
|
1119
|
+
or cache.fontKey != fkey
|
|
1120
|
+
):
|
|
1121
|
+
cache.textSize = self.__data.textEngine.textSize(
|
|
1122
|
+
font, self.__data.renderFlags, self.__data.text
|
|
1123
|
+
)
|
|
1124
|
+
cache.fontKey = fkey
|
|
1125
|
+
cache.fontId = font_id
|
|
1126
|
+
sz = QSizeF(cache.textSize)
|
|
1008
1127
|
if self.__data.layoutAttributes & self.MinimumLayout:
|
|
1009
1128
|
(left, right, top, bottom) = self.__data.textEngine.textMargins(font)
|
|
1010
1129
|
sz -= QSizeF(left + right, top + bottom)
|
|
@@ -1072,7 +1191,13 @@ class QwtText(object):
|
|
|
1072
1191
|
return self.__map.get(format_)
|
|
1073
1192
|
elif format_ is not None:
|
|
1074
1193
|
if format_ == QwtText.AutoText:
|
|
1075
|
-
|
|
1194
|
+
# Fast path: a string with no ``<`` cannot be rich text, so
|
|
1195
|
+
# we can return the plain engine without iterating the map
|
|
1196
|
+
# and calling Qt.mightBeRichText (which is a hot Qt call
|
|
1197
|
+
# for tick labels like " 1.5").
|
|
1198
|
+
if "<" not in text:
|
|
1199
|
+
return self.__map[QwtText.PlainText]
|
|
1200
|
+
for key, engine in self.__map.items():
|
|
1076
1201
|
if key != QwtText.PlainText:
|
|
1077
1202
|
if engine and engine.mightRender(text):
|
|
1078
1203
|
return engine
|
|
@@ -1291,10 +1416,10 @@ class QwtTextLabel(QFrame):
|
|
|
1291
1416
|
if indent <= 0:
|
|
1292
1417
|
indent = self.defaultIndent()
|
|
1293
1418
|
if indent > 0:
|
|
1294
|
-
align = self.__data.text.renderFlags()
|
|
1295
|
-
if align &
|
|
1419
|
+
align = _flag_int(self.__data.text.renderFlags())
|
|
1420
|
+
if align & (_ALIGN_LEFT | _ALIGN_RIGHT):
|
|
1296
1421
|
mw += self.__data.indent
|
|
1297
|
-
elif align &
|
|
1422
|
+
elif align & (_ALIGN_TOP | _ALIGN_BOTTOM):
|
|
1298
1423
|
mh += self.__data.indent
|
|
1299
1424
|
sz += QSizeF(mw, mh)
|
|
1300
1425
|
return QSize(math.ceil(sz.width()), math.ceil(sz.height()))
|
|
@@ -1304,15 +1429,15 @@ class QwtTextLabel(QFrame):
|
|
|
1304
1429
|
:param int width: Width
|
|
1305
1430
|
:return: Preferred height for this widget, given the width.
|
|
1306
1431
|
"""
|
|
1307
|
-
renderFlags = self.__data.text.renderFlags()
|
|
1432
|
+
renderFlags = _flag_int(self.__data.text.renderFlags())
|
|
1308
1433
|
indent = self.__data.indent
|
|
1309
1434
|
if indent <= 0:
|
|
1310
1435
|
indent = self.defaultIndent()
|
|
1311
1436
|
width -= 2 * self.frameWidth()
|
|
1312
|
-
if renderFlags &
|
|
1437
|
+
if renderFlags & (_ALIGN_LEFT | _ALIGN_RIGHT):
|
|
1313
1438
|
width -= indent
|
|
1314
1439
|
height = math.ceil(self.__data.text.heightForWidth(width, self.font()))
|
|
1315
|
-
if renderFlags &
|
|
1440
|
+
if renderFlags & (_ALIGN_TOP | _ALIGN_BOTTOM):
|
|
1316
1441
|
height += indent
|
|
1317
1442
|
height += 2 * self.frameWidth()
|
|
1318
1443
|
return height
|
|
@@ -1372,14 +1497,14 @@ class QwtTextLabel(QFrame):
|
|
|
1372
1497
|
if indent <= 0:
|
|
1373
1498
|
indent = self.defaultIndent()
|
|
1374
1499
|
if indent > 0:
|
|
1375
|
-
renderFlags = self.__data.text.renderFlags()
|
|
1376
|
-
if renderFlags &
|
|
1500
|
+
renderFlags = _flag_int(self.__data.text.renderFlags())
|
|
1501
|
+
if renderFlags & _ALIGN_LEFT:
|
|
1377
1502
|
r.setX(r.x() + indent)
|
|
1378
|
-
elif renderFlags &
|
|
1503
|
+
elif renderFlags & _ALIGN_RIGHT:
|
|
1379
1504
|
r.setWidth(r.width() - indent)
|
|
1380
|
-
elif renderFlags &
|
|
1505
|
+
elif renderFlags & _ALIGN_TOP:
|
|
1381
1506
|
r.setY(r.y() + indent)
|
|
1382
|
-
elif renderFlags &
|
|
1507
|
+
elif renderFlags & _ALIGN_BOTTOM:
|
|
1383
1508
|
r.setHeight(r.height() - indent)
|
|
1384
1509
|
return r
|
|
1385
1510
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|