PythonQwt 0.15.0__py3-none-any.whl → 0.16.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PythonQwt
3
- Version: 0.15.0
3
+ Version: 0.16.0
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.19
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 `guiqwt`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`.
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 `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`.
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.16 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
803
+ | 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ |
802
804
  | Latest | ✅ | ✅ | ❌ | ✅ |
803
805
 
804
806
  ### Requirements
805
807
 
806
808
  - Python >=3.9
807
- - QtPy >= 1.3 (and a Python-to-Qt binding library, see above)
808
- - NumPy >= 1.5
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.15.0.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
2
- qwt/__init__.py,sha256=IYGoZiDuvezTXga1nUybeNvDi9I-ntKYbQc0qwRIFpc,5993
1
+ pythonqwt-0.16.0.dist-info/licenses/LICENSE,sha256=qjEk8TRuXmFS7QC-omINvD1UPGqWaOs6CzcCZoMEhdI,33457
2
+ qwt/__init__.py,sha256=wOp2xyXj0loXQntSFHwu_OzVj09zvQ6n-BLp46BXy68,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=lISwinyParPXoJCL8D_9x9Q0JyJVeykrTWSm1Ubluzo,28177
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=AAVyQCVgZKyIw7i-J9ElB0_-8KRV9OXDr140TwMxLSs,9144
10
+ qwt/null_paintdevice.py,sha256=1pgbcvyRTVN32RGzuPzUOm0iUImjvPQ4l5OfJVyA3hg,9375
11
11
  qwt/painter.py,sha256=k345puF4MAoTye0WBveiTNpCNWXx1K2o62_I0PZYlIA,16536
12
- qwt/painter_command.py,sha256=b_uZb56YlzYFG3ZmB5ykjkBQQAadQD8epknsQSEai54,6957
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=PiIRrdZsK1YcV5cQkIpz6H5H6A-dTr2HUJ5MHvDHDG0,30312
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=2Ay5mgduXgSm4OrDMcaEWIzenAiBuEASZRoX99PWEE4,9276
24
- qwt/scale_draw.py,sha256=P9WGlfcldgpaHRq6hsrRgnFgtYP7JgTgybVCMbZLFic,38564
25
- qwt/scale_engine.py,sha256=yEjcSBWbpdUKCqXBxJccsyz2ZFcR278_QxfgX_fVVCY,36365
26
- qwt/scale_map.py,sha256=WobkNna6_RPXYZjGK7gZLykNxN_kYnrcB6fEaWgnL2w,9182
23
+ qwt/scale_div.py,sha256=_iI5cy4s8H6wigE8xsn7W44T2-DqVADqdjQ31uJPa_I,9266
24
+ qwt/scale_draw.py,sha256=iO39q7nXJRuFTYan3m5taAGqvmzCq2JX0PhySWtAYVo,41007
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=vLegaksF4ZqnWIf_HidMbVRgu-eXPX8CwM1ykyNt6OE,42467
29
+ qwt/text.py,sha256=isLS7UmbJrAmOoxhszPwrkJSRTh5WFmqVkbz_npIi94,48099
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=bOIApjBQA1J4aZkVBnzByiKqhqEcmQpag1TvIfsuHMs,9386
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=E2PqK4e8Ml-NZGuhslBlCsIPEcFm08nLwsn0jZT_h70,2093
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.15.0.dist-info/METADATA,sha256=QWFqtT14JgbY-aEXt3CoS8Jj0iKN0ZNSpdC8wE4GgZE,45097
81
- pythonqwt-0.15.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- pythonqwt-0.15.0.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
83
- pythonqwt-0.15.0.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
84
- pythonqwt-0.15.0.dist-info/RECORD,,
80
+ pythonqwt-0.16.0.dist-info/METADATA,sha256=jjr2ZDxH0abEGbmHoFLQsXEbk8X-3FLRTRqajqSnqDk,45515
81
+ pythonqwt-0.16.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
82
+ pythonqwt-0.16.0.dist-info/entry_points.txt,sha256=pdPda-YcYigCi00hR4tMxWKu6byxM2x3zA8BQFDYvwI,46
83
+ pythonqwt-0.16.0.dist-info/top_level.txt,sha256=KB1IBdWRWnaItyJMaECwZiEi1jWt3IvqCrRVVhMjTu8,4
84
+ pythonqwt-0.16.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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.python.org/pypi/PythonQwt
26
- .. _GitHubPage: http://pierreraybaut.github.io/PythonQwt
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.15.0"
66
+ __version__ = "0.16.0"
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
- if data.flags & QPaintEngine.DirtyPen:
102
+ flags = _flag_int(data.flags)
103
+ if flags & _DIRTY_PEN:
87
104
  painter.setPen(data.pen)
88
- if data.flags & QPaintEngine.DirtyBrush:
105
+ if flags & _DIRTY_BRUSH:
89
106
  painter.setBrush(data.brush)
90
- if data.flags & QPaintEngine.DirtyBrushOrigin:
107
+ if flags & _DIRTY_BRUSH_ORIGIN:
91
108
  painter.setBrushOrigin(data.brushOrigin)
92
- if data.flags & QPaintEngine.DirtyFont:
109
+ if flags & _DIRTY_FONT:
93
110
  painter.setFont(data.font)
94
- if data.flags & QPaintEngine.DirtyBackground:
111
+ if flags & _DIRTY_BACKGROUND:
95
112
  painter.setBackgroundMode(data.backgroundMode)
96
113
  painter.setBackground(data.backgroundBrush)
97
- if data.flags & QPaintEngine.DirtyTransform:
114
+ if flags & _DIRTY_TRANSFORM:
98
115
  painter.setTransform(data.transform)
99
- if data.flags & QPaintEngine.DirtyClipEnabled:
116
+ if flags & _DIRTY_CLIP_ENABLED:
100
117
  painter.setClipping(data.isClipEnabled)
101
- if data.flags & QPaintEngine.DirtyClipRegion:
118
+ if flags & _DIRTY_CLIP_REGION:
102
119
  painter.setClipRegion(data.clipRegion, data.clipOperation)
103
- if data.flags & QPaintEngine.DirtyClipPath:
120
+ if flags & _DIRTY_CLIP_PATH:
104
121
  painter.setClipPath(data.clipPath, data.clipOperation)
105
- if data.flags & QPaintEngine.DirtyHints:
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 data.flags & QPaintEngine.DirtyCompositionMode:
129
+ if flags & _DIRTY_COMPOSITION_MODE:
113
130
  painter.setCompositionMode(data.compositionMode)
114
- if data.flags & QPaintEngine.DirtyOpacity:
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 = 0
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
- if self.__stateData.flags & QPaintEngine.DirtyPen:
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 self.__stateData.flags & QPaintEngine.DirtyBrush:
167
+ if flags & _DIRTY_BRUSH:
131
168
  self.__stateData.brush = state.brush()
132
- if self.__stateData.flags & QPaintEngine.DirtyBrushOrigin:
169
+ if flags & _DIRTY_BRUSH_ORIGIN:
133
170
  self.__stateData.brushOrigin = state.brushOrigin()
134
- if self.__stateData.flags & QPaintEngine.DirtyFont:
171
+ if flags & _DIRTY_FONT:
135
172
  self.__stateData.font = state.font()
136
- if self.__stateData.flags & QPaintEngine.DirtyBackground:
173
+ if flags & _DIRTY_BACKGROUND:
137
174
  self.__stateData.backgroundMode = state.backgroundMode()
138
175
  self.__stateData.backgroundBrush = state.backgroundBrush()
139
- if self.__stateData.flags & QPaintEngine.DirtyTransform:
176
+ if flags & _DIRTY_TRANSFORM:
140
177
  self.__stateData.transform = state.transform()
141
- if self.__stateData.flags & QPaintEngine.DirtyClipEnabled:
178
+ if flags & _DIRTY_CLIP_ENABLED:
142
179
  self.__stateData.isClipEnabled = state.isClipEnabled()
143
- if self.__stateData.flags & QPaintEngine.DirtyClipRegion:
180
+ if flags & _DIRTY_CLIP_REGION:
144
181
  self.__stateData.clipRegion = state.clipRegion()
145
182
  self.__stateData.clipOperation = state.clipOperation()
146
- if self.__stateData.flags & QPaintEngine.DirtyClipPath:
183
+ if flags & _DIRTY_CLIP_PATH:
147
184
  self.__stateData.clipPath = state.clipPath()
148
185
  self.__stateData.clipOperation = state.clipOperation()
149
- if self.__stateData.flags & QPaintEngine.DirtyHints:
186
+ if flags & _DIRTY_HINTS:
150
187
  self.__stateData.renderHints = state.renderHints()
151
- if self.__stateData.flags & QPaintEngine.DirtyCompositionMode:
188
+ if flags & _DIRTY_COMPOSITION_MODE:
152
189
  self.__stateData.compositionMode = state.compositionMode()
153
- if self.__stateData.flags & QPaintEngine.DirtyOpacity:
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: " "please use `replot` instead",
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
- min_ = min([self.__lowerBound, self.__upperBound])
239
- max_ = max([self.__lowerBound, self.__upperBound])
240
- return value >= min_ and value <= max_
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
@@ -24,7 +24,6 @@ from datetime import datetime
24
24
 
25
25
  from qtpy.QtCore import (
26
26
  QLineF,
27
- QObject,
28
27
  QPoint,
29
28
  QPointF,
30
29
  QRect,
@@ -39,11 +38,34 @@ from qwt.scale_div import QwtScaleDiv
39
38
  from qwt.scale_map import QwtScaleMap
40
39
  from qwt.text import QwtText
41
40
 
41
+ # Plain-int aliases for Qt alignment flags. Qt6 exposes alignment flags as
42
+ # IntEnum members and bitwise operations on them go through Python's
43
+ # enum machinery (`__and__`/`__call__`), which is one of the dominant costs
44
+ # of label layout. Casting to int once and using these constants makes the
45
+ # bitwise tests in `labelTransformation` ~10x cheaper without changing
46
+ # semantics.
47
+ _ALIGN_LEFT = int(Qt.AlignLeft)
48
+ _ALIGN_RIGHT = int(Qt.AlignRight)
49
+ _ALIGN_TOP = int(Qt.AlignTop)
50
+ _ALIGN_BOTTOM = int(Qt.AlignBottom)
51
+
52
+
53
+ class QwtAbstractScaleDraw_PrivateData(object):
54
+ # See QwtText_PrivateData: ``QObject`` inheritance is unused and the
55
+ # base class' ``__init__`` is a measurable cost in tick-heavy renders.
56
+ __slots__ = (
57
+ "spacing",
58
+ "penWidth",
59
+ "minExtent",
60
+ "components",
61
+ "tick_length",
62
+ "tick_lighter_factor",
63
+ "map",
64
+ "scaleDiv",
65
+ "labelCache",
66
+ )
42
67
 
43
- class QwtAbstractScaleDraw_PrivateData(QObject):
44
68
  def __init__(self):
45
- QObject.__init__(self)
46
-
47
69
  self.spacing = 4
48
70
  self.penWidth = 0
49
71
  self.minExtent = 0.0
@@ -470,12 +492,25 @@ class QwtAbstractScaleDraw(object):
470
492
  self.__data.labelCache.clear()
471
493
 
472
494
 
473
- class QwtScaleDraw_PrivateData(QObject):
474
- def __init__(self):
475
- QObject.__init__(self)
495
+ class QwtScaleDraw_PrivateData(object):
496
+ # See QwtText_PrivateData: ``QObject`` inheritance is unused and the
497
+ # base class' ``__init__`` is a measurable cost in tick-heavy renders.
498
+ __slots__ = (
499
+ "len",
500
+ "alignment",
501
+ "orientation",
502
+ "labelAlignment",
503
+ "labelRotation",
504
+ "labelAutoSize",
505
+ "pos",
506
+ )
476
507
 
508
+ def __init__(self):
477
509
  self.len = 0
478
510
  self.alignment = QwtScaleDraw.BottomScale
511
+ # Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment``
512
+ # so that the very hot ``orientation()`` accessor avoids any test.
513
+ self.orientation = Qt.Horizontal
479
514
  self.labelAlignment = 0
480
515
  self.labelRotation = 0.0
481
516
  self.labelAutoSize = True
@@ -554,6 +589,11 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
554
589
  :py:meth:`alignment()`
555
590
  """
556
591
  self.__data.alignment = align
592
+ # Keep cached orientation in sync (see ``orientation()``).
593
+ if align == self.BottomScale or align == self.TopScale:
594
+ self.__data.orientation = Qt.Horizontal
595
+ else:
596
+ self.__data.orientation = Qt.Vertical
557
597
 
558
598
  def orientation(self):
559
599
  """
@@ -568,10 +608,8 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
568
608
 
569
609
  :py:meth:`alignment()`
570
610
  """
571
- if self.__data.alignment in (self.TopScale, self.BottomScale):
572
- return Qt.Horizontal
573
- elif self.__data.alignment in (self.LeftScale, self.RightScale):
574
- return Qt.Vertical
611
+ # Pre-computed by ``setAlignment`` - this method is called per tick.
612
+ return self.__data.orientation
575
613
 
576
614
  def getBorderDistHint(self, font):
577
615
  """
@@ -597,17 +635,19 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
597
635
  if len(ticks) == 0:
598
636
  return start, end
599
637
 
638
+ scale_map = self.scaleMap()
639
+ transform = scale_map.transform
600
640
  minTick = ticks[0]
601
- minPos = self.scaleMap().transform(minTick)
641
+ minPos = transform(minTick)
602
642
  maxTick = minTick
603
643
  maxPos = minPos
604
644
 
605
645
  for tick in ticks:
606
- tickPos = self.scaleMap().transform(tick)
646
+ tickPos = transform(tick)
607
647
  if tickPos < minPos:
608
648
  minTick = tick
609
649
  minPos = tickPos
610
- if tickPos > self.scaleMap().transform(maxTick):
650
+ if tickPos > maxPos:
611
651
  maxTick = tick
612
652
  maxPos = tickPos
613
653
 
@@ -615,16 +655,16 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
615
655
  e = 0.0
616
656
  if self.orientation() == Qt.Vertical:
617
657
  s = -self.labelRect(font, minTick).top()
618
- s -= abs(minPos - round(self.scaleMap().p2()))
658
+ s -= abs(minPos - round(scale_map.p2()))
619
659
 
620
660
  e = self.labelRect(font, maxTick).bottom()
621
- e -= abs(maxPos - self.scaleMap().p1())
661
+ e -= abs(maxPos - scale_map.p1())
622
662
  else:
623
663
  s = -self.labelRect(font, minTick).left()
624
- s -= abs(minPos - self.scaleMap().p1())
664
+ s -= abs(minPos - scale_map.p1())
625
665
 
626
666
  e = self.labelRect(font, maxTick).right()
627
- e -= abs(maxPos - self.scaleMap().p2())
667
+ e -= abs(maxPos - scale_map.p2())
628
668
 
629
669
  return max(math.ceil(s), 0), max(math.ceil(e), 0)
630
670
 
@@ -763,27 +803,22 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
763
803
  """
764
804
  tval = self.scaleMap().transform(value)
765
805
  dist = self.spacing()
766
- if self.hasComponent(QwtAbstractScaleDraw.Backbone):
767
- dist += max([1, self.penWidth()])
768
- if self.hasComponent(QwtAbstractScaleDraw.Ticks):
806
+ hasComponent = self.hasComponent
807
+ if hasComponent(QwtAbstractScaleDraw.Backbone):
808
+ dist += max(1, self.penWidth())
809
+ if hasComponent(QwtAbstractScaleDraw.Ticks):
769
810
  dist += self.tickLength(QwtScaleDiv.MajorTick)
770
811
 
771
- px = 0
772
- py = 0
773
- if self.alignment() == self.RightScale:
774
- px = self.__data.pos.x() + dist
775
- py = tval
776
- elif self.alignment() == self.LeftScale:
777
- px = self.__data.pos.x() - dist
778
- py = tval
779
- elif self.alignment() == self.BottomScale:
780
- px = tval
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)
812
+ alignment = self.alignment()
813
+ pos = self.__data.pos
814
+ if alignment == self.RightScale:
815
+ return QPointF(pos.x() + dist, tval)
816
+ if alignment == self.LeftScale:
817
+ return QPointF(pos.x() - dist, tval)
818
+ if alignment == self.BottomScale:
819
+ return QPointF(tval, pos.y() + dist)
820
+ # TopScale
821
+ return QPointF(tval, pos.y() - dist)
787
822
 
788
823
  def drawTick(self, painter, value, len_):
789
824
  """
@@ -1007,17 +1042,19 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
1007
1042
  flags = self.labelAlignment()
1008
1043
  if flags == 0:
1009
1044
  flags = self.Flags[self.alignment()]
1045
+ # Cast to plain int once to avoid the per-bit Qt6 enum overhead.
1046
+ flags = int(flags)
1010
1047
 
1011
- if flags & Qt.AlignLeft:
1048
+ if flags & _ALIGN_LEFT:
1012
1049
  x = -size.width()
1013
- elif flags & Qt.AlignRight:
1050
+ elif flags & _ALIGN_RIGHT:
1014
1051
  x = 0.0
1015
1052
  else:
1016
1053
  x = -(0.5 * size.width())
1017
1054
 
1018
- if flags & Qt.AlignTop:
1055
+ if flags & _ALIGN_TOP:
1019
1056
  y = -size.height()
1020
- elif flags & Qt.AlignBottom:
1057
+ elif flags & _ALIGN_BOTTOM:
1021
1058
  y = 0
1022
1059
  else:
1023
1060
  y = -(0.5 * size.height())
@@ -1039,6 +1076,31 @@ class QwtScaleDraw(QwtAbstractScaleDraw):
1039
1076
  lbl, labelSize = self.tickLabel(font, value)
1040
1077
  if not lbl or lbl.isEmpty():
1041
1078
  return QRectF(0.0, 0.0, 0.0, 0.0)
1079
+ # Fast path: when the label is not rotated, the contribution of
1080
+ # ``pos`` cancels out (transform.translate(pos) followed by
1081
+ # br.translate(-pos)). This avoids ``labelPosition``,
1082
+ # ``labelTransformation`` and ``QTransform.mapRect`` entirely - all
1083
+ # of which are dominant costs in tick-heavy layouts.
1084
+ if self.labelRotation() == 0.0:
1085
+ flags = self.labelAlignment()
1086
+ if flags == 0:
1087
+ flags = self.Flags[self.alignment()]
1088
+ flags = int(flags)
1089
+ w = labelSize.width()
1090
+ h = labelSize.height()
1091
+ if flags & _ALIGN_LEFT:
1092
+ x = -w
1093
+ elif flags & _ALIGN_RIGHT:
1094
+ x = 0.0
1095
+ else:
1096
+ x = -0.5 * w
1097
+ if flags & _ALIGN_TOP:
1098
+ y = -h
1099
+ elif flags & _ALIGN_BOTTOM:
1100
+ y = 0.0
1101
+ else:
1102
+ y = -0.5 * h
1103
+ return QRectF(x, y, w, h)
1042
1104
  pos = self.labelPosition(value)
1043
1105
  transform = self.labelTransformation(pos, labelSize)
1044
1106
  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
- eps = abs(1.0e-6 * interval.width())
328
- if interval.minValue() - value > eps or value - interval.maxValue() > eps:
329
- return False
330
- else:
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
- if self.contains(interval, ticks[0]) and self.contains(interval, ticks[-1]):
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 self.contains(interval, tick)]
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 / 2
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 / 2
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 / 2
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.__cnv = 1.0
221
- if self.__ts1 != ts2:
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
- if len(args) == 1:
249
- # Scalar transform
250
- return self.transform_scalar(args[0])
251
- elif len(args) == 3 and isinstance(args[2], QPointF):
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 len(args) == 3 and isinstance(args[2], QRectF):
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 + 1, y2 - y1 + 1)
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() - 1)
304
+ x2 = xMap.invTransform(rect.right())
296
305
  y1 = yMap.invTransform(rect.top())
297
- y2 = yMap.invTransform(rect.bottom() - 1)
306
+ y2 = yMap.invTransform(rect.bottom())
298
307
  r = QRectF(x1, y1, x2 - x1, y2 - y1)
299
308
  return r.normalized()
@@ -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),
@@ -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 & Qt.AlignJustify:
98
+ if flags & _ALIGN_JUSTIFY:
78
99
  richText = '<div align="justify">' + richText + "</div>"
79
- elif flags & Qt.AlignRight:
100
+ elif flags & _ALIGN_RIGHT:
80
101
  richText = '<div align="right">' + richText + "</div>"
81
- elif flags & Qt.AlignHCenter:
102
+ elif flags & _ALIGN_HCENTER:
82
103
  richText = '<div align="center">' + richText + "</div>"
83
104
  return richText
84
105
 
@@ -189,6 +210,65 @@ 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
+
238
+
239
+ def _font_tuple_key(font):
240
+ """Build a hashable tuple identifying the logical font."""
241
+ px = font.pixelSize()
242
+ return (
243
+ font.family(),
244
+ px if px > 0 else font.pointSizeF(),
245
+ font.weight(),
246
+ font.italic(),
247
+ font.stretch(),
248
+ font.styleStrategy(),
249
+ )
250
+
251
+
252
+ def font_key_cached(font):
253
+ """Return a hashable cache key uniquely identifying ``font`` for metrics.
254
+
255
+ The returned value is **not** ``QFont.key()`` -- it is a tuple computed
256
+ from a handful of QFont attributes. It is safe to use as a dict key for
257
+ metrics caches (callers in this module always compare by ``==`` only).
258
+ """
259
+ fid = id(font)
260
+ entry = _FONT_KEY_CACHE.get(fid)
261
+ if entry is not None:
262
+ return entry[1]
263
+ tkey = _font_tuple_key(font)
264
+ # Intern: reuse the same tuple object across all id() variants so dict
265
+ # lookups in caller-side caches benefit from object-identity hash hits.
266
+ interned = _FONT_TUPLE_CACHE.setdefault(tkey, tkey)
267
+ if len(_FONT_KEY_CACHE) >= _FONT_KEY_CACHE_LIMIT:
268
+ _FONT_KEY_CACHE.clear()
269
+ _FONT_KEY_CACHE[fid] = (font, interned)
270
+ return interned
271
+
192
272
 
193
273
  def get_screen_resolution():
194
274
  """Return screen resolution: tuple of floats (DPIx, DPIy)"""
@@ -226,16 +306,21 @@ class QwtPlainTextEngine(QwtTextEngine):
226
306
  self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
227
307
  self._fm_cache = {}
228
308
  self._fm_cache_f = {}
309
+ self._margins_cache = {}
310
+ # Fast path: when textMargins is called repeatedly with the same
311
+ # QFont instance, skip the (expensive) font.key() Qt call.
312
+ self._margins_last_id = -1
313
+ self._margins_last_value = None
229
314
 
230
315
  def fontmetrics(self, font):
231
- fid = font.toString()
316
+ fid = font_key_cached(font)
232
317
  try:
233
318
  return self._fm_cache[fid]
234
319
  except KeyError:
235
320
  return self._fm_cache.setdefault(fid, QFontMetrics(font))
236
321
 
237
322
  def fontmetrics_f(self, font):
238
- fid = font.toString()
323
+ fid = font_key_cached(font)
239
324
  try:
240
325
  return self._fm_cache_f[fid]
241
326
  except KeyError:
@@ -270,7 +355,7 @@ class QwtPlainTextEngine(QwtTextEngine):
270
355
 
271
356
  def effectiveAscent(self, font):
272
357
  global ASCENTCACHE
273
- fontKey = font.key()
358
+ fontKey = font_key_cached(font)
274
359
  ascent = ASCENTCACHE.get(fontKey)
275
360
  if ascent is not None:
276
361
  return ascent
@@ -317,11 +402,19 @@ class QwtPlainTextEngine(QwtTextEngine):
317
402
  :param QFont font: Font of the text
318
403
  :return: tuple (left, right, top, bottom) representing margins
319
404
  """
320
- left = right = 0
321
- fm = self.fontmetrics(font)
322
- top = fm.ascent() - self.effectiveAscent(font)
323
- bottom = fm.descent()
324
- return left, right, top, bottom
405
+ # Fast path: same QFont object as the previous call.
406
+ font_id = id(font)
407
+ if font_id == self._margins_last_id:
408
+ return self._margins_last_value
409
+ fkey = font_key_cached(font)
410
+ cached = self._margins_cache.get(fkey)
411
+ if cached is None:
412
+ fm = self.fontmetrics(font)
413
+ cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent())
414
+ self._margins_cache[fkey] = cached
415
+ self._margins_last_id = font_id
416
+ self._margins_last_value = cached
417
+ return cached
325
418
 
326
419
  def draw(self, painter, rect, flags, text):
327
420
  """
@@ -464,10 +557,26 @@ class QwtRichTextEngine(QwtTextEngine):
464
557
  return 0, 0, 0, 0
465
558
 
466
559
 
467
- class QwtText_PrivateData(QObject):
468
- def __init__(self):
469
- QObject.__init__(self)
560
+ class QwtText_PrivateData(object):
561
+ # ``QObject`` was previously used as the base class but no Qt signals
562
+ # or events are ever emitted from ``_PrivateData`` containers and the
563
+ # ``QObject.__init__`` call dominates ``QwtText.__init__`` (it is the
564
+ # single most expensive line for tick-label-heavy renders, see
565
+ # https://github.com/PlotPyStack/PythonQwt/issues/93).
566
+ __slots__ = (
567
+ "renderFlags",
568
+ "borderRadius",
569
+ "borderPen",
570
+ "backgroundBrush",
571
+ "paintAttributes",
572
+ "layoutAttributes",
573
+ "textEngine",
574
+ "text",
575
+ "font",
576
+ "color",
577
+ )
470
578
 
579
+ def __init__(self):
471
580
  self.renderFlags = Qt.AlignCenter
472
581
  self.borderRadius = 0
473
582
  self.borderPen = Qt.NoPen
@@ -484,10 +593,13 @@ class QwtText_PrivateData(QObject):
484
593
  class QwtText_LayoutCache(object):
485
594
  def __init__(self):
486
595
  self.textSize = None
487
- self.font = None
596
+ self.fontKey = None
597
+ self.fontId = -1
488
598
 
489
599
  def invalidate(self):
490
600
  self.textSize = None
601
+ self.fontKey = None
602
+ self.fontId = -1
491
603
 
492
604
 
493
605
  class QwtText(object):
@@ -727,7 +839,13 @@ class QwtText(object):
727
839
  :py:meth:`renderFlags()`,
728
840
  :py:meth:`qwt.text.QwtTextEngine.draw()`
729
841
  """
730
- renderFlags = Qt.AlignmentFlag(renderFlags)
842
+ # Wrap into Qt.AlignmentFlag so that downstream Qt APIs (notably
843
+ # ``QTextOption.setAlignment``, ``QPainter.drawText``,
844
+ # ``QFontMetrics.boundingRect``) that strictly require an enum on
845
+ # PyQt6 keep working. Hot bitwise-test sites locally cast back to
846
+ # int to avoid the per-test enum.__and__ cost.
847
+ if not isinstance(renderFlags, Qt.AlignmentFlag):
848
+ renderFlags = Qt.AlignmentFlag(renderFlags)
731
849
  if renderFlags != self.__data.renderFlags:
732
850
  self.__data.renderFlags = renderFlags
733
851
  self.__layoutCache.invalidate()
@@ -994,17 +1112,24 @@ class QwtText(object):
994
1112
  :param QFont defaultFont Font, used for the calculation if the text has no font
995
1113
  :return: Caluclated size
996
1114
  """
997
- font = QFont(self.usedFont(defaultFont))
998
- if (
999
- self.__layoutCache.textSize is None
1000
- or not self.__layoutCache.textSize.isValid()
1001
- or self.__layoutCache.font is not font
1002
- ):
1003
- self.__layoutCache.textSize = self.__data.textEngine.textSize(
1004
- font, self.__data.renderFlags, self.__data.text
1005
- )
1006
- self.__layoutCache.font = font
1007
- sz = self.__layoutCache.textSize
1115
+ font = self.usedFont(defaultFont)
1116
+ cache = self.__layoutCache
1117
+ font_id = id(font)
1118
+ if cache.textSize is not None and cache.fontId == font_id:
1119
+ sz = QSizeF(cache.textSize)
1120
+ else:
1121
+ fkey = font_key_cached(font)
1122
+ if (
1123
+ cache.textSize is None
1124
+ or not cache.textSize.isValid()
1125
+ or cache.fontKey != fkey
1126
+ ):
1127
+ cache.textSize = self.__data.textEngine.textSize(
1128
+ font, self.__data.renderFlags, self.__data.text
1129
+ )
1130
+ cache.fontKey = fkey
1131
+ cache.fontId = font_id
1132
+ sz = QSizeF(cache.textSize)
1008
1133
  if self.__data.layoutAttributes & self.MinimumLayout:
1009
1134
  (left, right, top, bottom) = self.__data.textEngine.textMargins(font)
1010
1135
  sz -= QSizeF(left + right, top + bottom)
@@ -1072,7 +1197,13 @@ class QwtText(object):
1072
1197
  return self.__map.get(format_)
1073
1198
  elif format_ is not None:
1074
1199
  if format_ == QwtText.AutoText:
1075
- for key, engine in list(self.__map.items()):
1200
+ # Fast path: a string with no ``<`` cannot be rich text, so
1201
+ # we can return the plain engine without iterating the map
1202
+ # and calling Qt.mightBeRichText (which is a hot Qt call
1203
+ # for tick labels like " 1.5").
1204
+ if "<" not in text:
1205
+ return self.__map[QwtText.PlainText]
1206
+ for key, engine in self.__map.items():
1076
1207
  if key != QwtText.PlainText:
1077
1208
  if engine and engine.mightRender(text):
1078
1209
  return engine
@@ -1291,10 +1422,10 @@ class QwtTextLabel(QFrame):
1291
1422
  if indent <= 0:
1292
1423
  indent = self.defaultIndent()
1293
1424
  if indent > 0:
1294
- align = self.__data.text.renderFlags()
1295
- if align & Qt.AlignLeft or align & Qt.AlignRight:
1425
+ align = _flag_int(self.__data.text.renderFlags())
1426
+ if align & (_ALIGN_LEFT | _ALIGN_RIGHT):
1296
1427
  mw += self.__data.indent
1297
- elif align & Qt.AlignTop or align & Qt.AlignBottom:
1428
+ elif align & (_ALIGN_TOP | _ALIGN_BOTTOM):
1298
1429
  mh += self.__data.indent
1299
1430
  sz += QSizeF(mw, mh)
1300
1431
  return QSize(math.ceil(sz.width()), math.ceil(sz.height()))
@@ -1304,15 +1435,15 @@ class QwtTextLabel(QFrame):
1304
1435
  :param int width: Width
1305
1436
  :return: Preferred height for this widget, given the width.
1306
1437
  """
1307
- renderFlags = self.__data.text.renderFlags()
1438
+ renderFlags = _flag_int(self.__data.text.renderFlags())
1308
1439
  indent = self.__data.indent
1309
1440
  if indent <= 0:
1310
1441
  indent = self.defaultIndent()
1311
1442
  width -= 2 * self.frameWidth()
1312
- if renderFlags & Qt.AlignLeft or renderFlags & Qt.AlignRight:
1443
+ if renderFlags & (_ALIGN_LEFT | _ALIGN_RIGHT):
1313
1444
  width -= indent
1314
1445
  height = math.ceil(self.__data.text.heightForWidth(width, self.font()))
1315
- if renderFlags & Qt.AlignTop or renderFlags & Qt.AlignBottom:
1446
+ if renderFlags & (_ALIGN_TOP | _ALIGN_BOTTOM):
1316
1447
  height += indent
1317
1448
  height += 2 * self.frameWidth()
1318
1449
  return height
@@ -1372,14 +1503,14 @@ class QwtTextLabel(QFrame):
1372
1503
  if indent <= 0:
1373
1504
  indent = self.defaultIndent()
1374
1505
  if indent > 0:
1375
- renderFlags = self.__data.text.renderFlags()
1376
- if renderFlags & Qt.AlignLeft:
1506
+ renderFlags = _flag_int(self.__data.text.renderFlags())
1507
+ if renderFlags & _ALIGN_LEFT:
1377
1508
  r.setX(r.x() + indent)
1378
- elif renderFlags & Qt.AlignRight:
1509
+ elif renderFlags & _ALIGN_RIGHT:
1379
1510
  r.setWidth(r.width() - indent)
1380
- elif renderFlags & Qt.AlignTop:
1511
+ elif renderFlags & _ALIGN_TOP:
1381
1512
  r.setY(r.y() + indent)
1382
- elif renderFlags & Qt.AlignBottom:
1513
+ elif renderFlags & _ALIGN_BOTTOM:
1383
1514
  r.setHeight(r.height() - indent)
1384
1515
  return r
1385
1516