diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 758a37e..0000000 --- a/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[run] -parallel = True -concurrency = multiprocessing,thread -omit = - */qwt/tests/* - -[report] -exclude_lines = - if __name__ == .__main__.: \ No newline at end of file diff --git a/.env.template b/.env.template deleted file mode 100644 index 60f01f7..0000000 --- a/.env.template +++ /dev/null @@ -1 +0,0 @@ -PYTHONPATH=. \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 96f1e35..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,209 +0,0 @@ -# PythonQwt AI Coding Agent Instructions - -## Project Overview - -**PythonQwt** is a pure Python implementation of the Qwt C++ plotting library. It provides low-level Qt plotting widgets that form the foundation for higher-level libraries like PlotPy. - -### Technology Stack - -- **Python**: 3.9+ -- **Core**: NumPy (≥1.21), QtPy (≥1.9) -- **GUI**: Qt via QtPy (PyQt5/PyQt6/PySide6) -- **Testing**: pytest -- **Linting**: Ruff, Pylint - -### Architecture - -``` -qwt/ -├── plot.py # QwtPlot main widget -├── plot_canvas.py # Plot canvas -├── plot_curve.py # QwtPlotCurve -├── plot_marker.py # QwtPlotMarker -├── plot_grid.py # QwtPlotGrid -├── scale_*.py # Scale engine, map, division, drawing -├── symbol.py # QwtSymbol for markers -├── legend.py # QwtLegend -├── text.py # QwtText, QwtTextLabel -├── graphic.py # QwtGraphic -└── tests/ # pytest suite -``` - -## Development Workflows - -### Running Commands - -**Always use `scripts/run_with_env.py`** to load `.env` before running Python commands: - -```powershell -python scripts/run_with_env.py python -m pytest --ff -python scripts/run_with_env.py python -m ruff format -python scripts/run_with_env.py python -m ruff check --fix -``` - -### Running Test Launcher - -```powershell -PythonQwt-tests # GUI test launcher -PythonQwt-tests --mode unattended # Headless tests -``` - -Or from Python: - -```python -from qwt import tests -tests.run() -``` - -## Core Patterns - -### Basic Plot Creation - -```python -import numpy as np -from qtpy import QtWidgets as QW -import qwt - -app = QW.QApplication([]) - -# Create plot widget -plot = qwt.QwtPlot("My Plot Title") -plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend) - -# Add curves -x = np.linspace(0, 10, 100) -qwt.QwtPlotCurve.make(x, np.sin(x), "Sine", plot, - linecolor="blue", antialiased=True) - -# Add grid -grid = qwt.QwtPlotGrid() -grid.attach(plot) - -plot.resize(600, 400) -plot.show() -app.exec_() -``` - -### QwtPlotCurve Factory Method - -The `make` class method simplifies curve creation: - -```python -curve = qwt.QwtPlotCurve.make( - x, y, # Data arrays - title="My Curve", # Legend title - plot=plot, # Parent plot (auto-attaches) - linecolor="red", # Line color - linewidth=2, # Line width - linestyle=Qt.DashLine, # Qt line style - antialiased=True, # Anti-aliasing - symbol=qwt.QwtSymbol( # Marker symbol (QwtSymbol instance) - qwt.QwtSymbol.Ellipse, - QBrush(Qt.yellow), - QPen(Qt.red, 2), - QSize(8, 8), - ), -) -``` - -### Key Classes - -| Class | Purpose | -|-------|---------| -| `QwtPlot` | Main plot widget | -| `QwtPlotCurve` | 2D curve item | -| `QwtPlotMarker` | Point/line markers | -| `QwtPlotGrid` | Grid lines | -| `QwtLegend` | Legend widget | -| `QwtSymbol` | Marker symbols | -| `QwtLinearScaleEngine` | Linear scale calculations | -| `QwtLogScaleEngine` | Logarithmic scale calculations | -| `QwtScaleMap` | Scale transformations | -| `QwtText` | Rich text labels | -| `QwtDateTimeScaleDraw` | Datetime axis tick labels | -| `QwtDateTimeScaleEngine` | Datetime scale divisions | - -### Scale Configuration - -```python -# Set axis titles -plot.setAxisTitle(qwt.QwtPlot.xBottom, "Time (s)") -plot.setAxisTitle(qwt.QwtPlot.yLeft, "Amplitude") - -# Set axis scale -plot.setAxisScale(qwt.QwtPlot.xBottom, 0, 100) - -# Logarithmic scale -plot.setAxisScaleEngine(qwt.QwtPlot.yLeft, qwt.QwtLogScaleEngine()) -``` - -### Symbols and Markers - -```python -# Create marker -marker = qwt.QwtPlotMarker() -marker.setSymbol(qwt.QwtSymbol( - qwt.QwtSymbol.Diamond, - QBrush(Qt.yellow), - QPen(Qt.red, 2), - QSize(10, 10) -)) -marker.setValue(x_pos, y_pos) -marker.attach(plot) -``` - -## Coding Conventions - -### Qt Imports - -Use QtPy for Qt binding abstraction: - -```python -from qtpy.QtCore import Qt, QSize -from qtpy.QtGui import QPen, QBrush, QColor -from qtpy.QtWidgets import QWidget -``` - -### Docstrings - -Standard Python docstrings: - -```python -def setData(self, x, y): - """Set curve data. - - :param x: X coordinates (array-like) - :param y: Y coordinates (array-like) - """ -``` - -## Key Files Reference - -| File | Purpose | -|------|---------| -| `qwt/plot.py` | QwtPlot implementation | -| `qwt/plot_curve.py` | QwtPlotCurve with `make()` factory | -| `qwt/scale_engine.py` | Linear/log/datetime scale engines | -| `qwt/scale_map.py` | Scale transformations | -| `qwt/symbol.py` | QwtSymbol definitions | -| `qwt/tests/__init__.py` | Test launcher | - -## Limitations vs C++ Qwt - -The following are **not implemented** (PlotPy provides these): - -- `QwtPlotZoomer` - Use PlotPy's zoom tools -- `QwtPicker` - Use PlotPy's interactive tools -- `QwtPlotPicker` - Use PlotPy's selection tools - -Only essential plot items are implemented: -- `QwtPlotItem` (base) -- `QwtPlotCurve` -- `QwtPlotMarker` -- `QwtPlotGrid` -- `QwtPlotSeriesItem` - -## Related Projects - -- **guidata**: Dataset/parameter framework (sibling) -- **PlotPy**: High-level plotting using PythonQwt (downstream) diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml deleted file mode 100644 index 34d255a..0000000 --- a/.github/workflows/build_deploy.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Build and upload to PyPI - -on: - release: - types: [published] - -permissions: - contents: read - -jobs: - test-pyqt5: - uses: ./.github/workflows/test-PyQt5.yml - - test-pyqt6: - uses: ./.github/workflows/test-PyQt6.yml - - test-pyside6: - uses: ./.github/workflows/test-PySide6.yml - - deploy: - needs: [test-pyqt5, test-pyqt6, test-pyside6] - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test-PyQt5.yml b/.github/workflows/test-PyQt5.yml deleted file mode 100644 index 18b4cb6..0000000 --- a/.github/workflows/test-PyQt5.yml +++ /dev/null @@ -1,45 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions - -name: Python package - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - workflow_call: - -jobs: - build: - - env: - DISPLAY: ':99.0' - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.9", "3.11", "3.13"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - python -m pip install --upgrade pip - python -m pip install ruff pytest - pip install PyQt5 qtpy numpy - pip install . - - name: Lint with Ruff - run: ruff check --output-format=github qwt - - name: Test with pytest - run: | - pytest diff --git a/.github/workflows/test-PyQt6.yml b/.github/workflows/test-PyQt6.yml deleted file mode 100644 index 4bd9efa..0000000 --- a/.github/workflows/test-PyQt6.yml +++ /dev/null @@ -1,46 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions - -name: Python package - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - workflow_call: - -jobs: - build: - - env: - DISPLAY: ':99.0' - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.9", "3.11", "3.13"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils - sudo apt install libegl1 libxcb-cursor0 - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - python -m pip install --upgrade pip - python -m pip install ruff pytest - pip install PyQt6 qtpy numpy - pip install . - - name: Lint with Ruff - run: ruff check --output-format=github qwt - - name: Test with pytest - run: | - pytest diff --git a/.github/workflows/test-PySide6.yml b/.github/workflows/test-PySide6.yml deleted file mode 100644 index 965509f..0000000 --- a/.github/workflows/test-PySide6.yml +++ /dev/null @@ -1,46 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions - -name: Python package - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - workflow_call: - -jobs: - build: - - env: - DISPLAY: ':99.0' - - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.9", "3.11", "3.13"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils - sudo apt install libegl1 libxcb-cursor0 - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - python -m pip install --upgrade pip - python -m pip install ruff pytest - pip install PySide6 qtpy numpy - pip install . - - name: Lint with Ruff - run: ruff check --output-format=github qwt - - name: Test with pytest - run: | - pytest diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c19ea1c..0000000 --- a/.gitignore +++ /dev/null @@ -1,80 +0,0 @@ -.spyderproject -.spyproject -qwt-6.* -qwt/tests/demo.png -doc.zip -doctmp/ - -# Visual Studio Code -.env - -# Created by https://www.gitignore.io/api/python - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover - -# Translations -*.mo -*.pot - -# Django stuff: -*.log - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Local benchmark venvs (issue #93) -.venvs/ - -# Performance investigation artifacts (see scripts/README.md) -shots/ -profile.out -*.prof_stats -*.lprof diff --git a/.hgtags b/.hgtags deleted file mode 100644 index d07244a..0000000 --- a/.hgtags +++ /dev/null @@ -1,11 +0,0 @@ -62c1865dfdafe71311bb0cac30af71409e466a7a v6.1.2alpha1 -7e90ab6db012fa323e31421926f1c19cb1ebe02b v6.1.2alpha2 -446030e474b9b9a14b789d70696968e16acc9e5e v6.1.2alpha3 -7064b469eea1696ab85fc471f7568e6f5cfecb6e v6.1.2a3 -1050efc1f915a3bf04dc925f44beb23e20c8ead5 v6.1.2a4 -1050efc1f915a3bf04dc925f44beb23e20c8ead5 v6.1.2a4 -e5cb19d0721cda2c5a97533be64f1834dd92233e v6.1.2a4 -e5cb19d0721cda2c5a97533be64f1834dd92233e v6.1.2a4 -aa2d7302a28b22592acca32ac6aa6166b79ccaab v6.1.2a4 -8e044d5aa6eb6aaaf9a7a506f05d1fa8a3663a8d v6.1.2a5 -be644cd20052d4a1219bd6948c2ce973396f8385 v6.1.2a7 diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index f071bdf..0000000 --- a/.pylintrc +++ /dev/null @@ -1,18 +0,0 @@ -[FORMAT] -# Essential to be able to compare code side-by-side (`black` default setting) -# and best compromise to minimize file size -max-line-length=88 - -[TYPECHECK] -ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui - -[MESSAGES CONTROL] -disable=wrong-import-order - -[DESIGN] -max-args=8 # default: 5 -max-attributes=12 # default: 7 -max-branches=17 # default: 12 -max-locals=20 # default: 15 -min-public-methods=0 # default: 2 -max-public-methods=25 # default: 20 \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 1c001a5..0000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Read the Docs configuration file for Sphinx projects -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -version: 2 -build: - os: ubuntu-22.04 - tools: - python: "3.11" - jobs: - post_create_environment: - - pip install QtPy PyQt5 numpy -sphinx: - configuration: doc/conf.py -formats: - - pdf -python: - install: - - method: pip - path: . - extra_requirements: - - doc diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 77a04e7..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. - // Pointez pour afficher la description des attributs existants. - // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python : Test launcher", - "type": "debugpy", - "request": "launch", - "program": "${workspaceFolder}/qwt/tests/__init__.py", - "console": "integratedTerminal" - }, - { - "name": "Python : Current file", - "type": "debugpy", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 16c89ec..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "[bat]": { - "files.encoding": "cp850", - }, - "editor.rulers": [ - 88 - ], - "files.exclude": { - "**/__pycache__": true, - "**/.pytest_cache": true, - "**/.hypothesis": true, - "**/*.pyc": true, - "**/*.pyo": true, - "**/*.pyd": true, - ".venv": true - }, - "files.trimFinalNewlines": true, - "files.trimTrailingWhitespace": true, - "editor.formatOnSave": true, - "python.analysis.autoFormatStrings": true, - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "python.testing.pytestPath": "pytest", - "python.testing.pytestArgs": [], - "[python]": { - "editor.defaultFormatter": "charliermarsh.ruff" - }, - "editor.codeActionsOnSave": { - "source.organizeImports": "explicit", - }, -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index a67d8ef..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,380 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "🧽 Ruff Formatter", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "ruff", - "format", - ], - "options": { - "cwd": "${workspaceFolder}", - "statusbar": { - "hide": true, - }, - }, - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "clear": true, - "echo": true, - "focus": false, - "panel": "dedicated", - "reveal": "always", - "showReuseMessage": true, - }, - "type": "shell", - }, - { - "label": "🔦 Ruff Linter", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "ruff", - "check", - "--fix", - ], - "options": { - "cwd": "${workspaceFolder}", - "statusbar": { - "hide": true, - }, - }, - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "clear": true, - "echo": true, - "focus": false, - "panel": "dedicated", - "reveal": "always", - "showReuseMessage": true, - }, - "type": "shell", - }, - { - "label": "🧽🔦 Ruff", - "dependsOrder": "sequence", - "dependsOn": [ - "🧽 Ruff Formatter", - "🔦 Ruff Linter", - ], - "group": { - "kind": "build", - "isDefault": false, - }, - "presentation": { - "clear": true, - "echo": true, - "focus": false, - "panel": "dedicated", - "reveal": "always", - "showReuseMessage": true, - }, - "type": "shell", - }, - { - "label": "🔦 Pylint", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "pylint", - "qwt", - "--disable=fixme,C,R,W", - ], - "options": { - "cwd": "${workspaceFolder}", - }, - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "clear": true, - "echo": true, - "focus": false, - "panel": "dedicated", - "reveal": "always", - "showReuseMessage": true, - }, - "type": "shell", - }, - { - "label": "🚀 Pytest", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "pytest", - "--ff", - ], - "options": { - "cwd": "${workspaceFolder}", - "env": { - "UNATTENDED": "1", - }, - }, - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "dedicated", - "showReuseMessage": true, - "clear": true, - }, - "type": "shell", - }, - { - "label": "🧪 Coverage tests", - "type": "shell", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "coverage", - "run", - "-m", - "pytest", - "qwt", - ], - "options": { - "cwd": "${workspaceFolder}", - "env": { - "COVERAGE_PROCESS_START": "${workspaceFolder}/.coveragerc", - }, - "statusbar": { - "hide": true, - }, - }, - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "panel": "dedicated", - }, - "problemMatcher": [], - }, - { - "label": "📊 Coverage full", - "type": "shell", - "windows": { - "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine; if ($?) { ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html; if ($?) { start htmlcov\\index.html } }", - }, - "linux": { - "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html && xdg-open htmlcov/index.html", - }, - "osx": { - "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html && open htmlcov/index.html", - }, - "options": { - "cwd": "${workspaceFolder}", - "env": { - "COVERAGE_PROCESS_START": "${workspaceFolder}/.coveragerc", - }, - }, - "presentation": { - "panel": "dedicated", - }, - "problemMatcher": [], - "dependsOrder": "sequence", - "dependsOn": [ - "🧪 Coverage tests", - ], - }, - { - "label": "📷 Take test screenshots", - "type": "shell", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "qwt/tests/__init__.py", - "--mode", - "screenshots", - ], - "options": { - "cwd": "${workspaceFolder}", - }, - "presentation": { - "clear": true, - "panel": "dedicated", - }, - "problemMatcher": [], - }, - { - "label": "📷 Take doc screenshots", - "type": "shell", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "doc/plot_example.py", - ], - "options": { - "cwd": "${workspaceFolder}", - }, - "presentation": { - "panel": "dedicated", - }, - "problemMatcher": [], - }, - { - "label": "📷 Take symbol screenshots", - "type": "shell", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "doc/symbol_path_example.py", - ], - "options": { - "cwd": "${workspaceFolder}", - }, - "presentation": { - "panel": "dedicated", - }, - "problemMatcher": [], - }, - { - "label": "📷 Take screenshots", - "dependsOrder": "sequence", - "dependsOn": [ - "📷 Take test screenshots", - "📷 Take doc screenshots", - "📷 Take symbol screenshots", - ], - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "clear": true, - "panel": "dedicated", - }, - }, - { - "label": "🧹 Clean Up", - "type": "shell", - "windows": { - "command": "Get-ChildItem -Recurse -Directory -Filter __pycache__ | Remove-Item -Recurse -Force; Remove-Item -Recurse -Force -ErrorAction SilentlyContinue build, dist, PythonQwt.egg-info, MANIFEST, htmlcov, .coverage, coverage.xml, sitecustomize.py; Remove-Item -Force -ErrorAction SilentlyContinue .coverage.*", - }, - "linux": { - "command": "find . -type d -name __pycache__ -exec rm -rf {} + ; rm -rf build dist PythonQwt.egg-info MANIFEST htmlcov .coverage coverage.xml sitecustomize.py .coverage.*", - }, - "osx": { - "command": "find . -type d -name __pycache__ -exec rm -rf {} + ; rm -rf build dist PythonQwt.egg-info MANIFEST htmlcov .coverage coverage.xml sitecustomize.py .coverage.*", - }, - "options": { - "cwd": "${workspaceFolder}", - }, - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "echo": true, - "reveal": "always", - "focus": false, - "panel": "shared", - "showReuseMessage": true, - "clear": false, - }, - }, - { - "label": "📚 Build documentation", - "type": "shell", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "sphinx", - "build", - "doc", - "${workspaceFolder}/build/doc", - "-b", - "html", - ], - "options": { - "cwd": "${workspaceFolder}", - }, - "group": { - "kind": "build", - "isDefault": true, - }, - "presentation": { - "clear": true, - "echo": true, - "focus": false, - "panel": "dedicated", - "reveal": "always", - "showReuseMessage": true, - }, - }, - { - "label": "🌐 Open HTML doc", - "type": "shell", - "windows": { - "command": "start build/doc/index.html", - }, - "linux": { - "command": "xdg-open build/doc/index.html", - }, - "osx": { - "command": "open build/doc/index.html", - }, - "options": { - "cwd": "${workspaceFolder}", - }, - "problemMatcher": [], - }, - { - "label": "📦 Build package", - "type": "shell", - "command": "${command:python.interpreterPath}", - "args": [ - "scripts/run_with_env.py", - "${command:python.interpreterPath}", - "-m", - "build", - ], - "options": { - "cwd": "${workspaceFolder}", - }, - "group": { - "kind": "build", - "isDefault": false, - }, - "presentation": { - "clear": true, - "panel": "dedicated", - }, - "problemMatcher": [], - "dependsOrder": "sequence", - "dependsOn": [ - "🧹 Clean Up", - ], - }, - ], -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index b9de2d8..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,376 +0,0 @@ -# PythonQwt Releases - -## Version 0.16.0 - -### Performance - -- Major performance optimizations addressing [Issue #93](https://github.com/PlotPyStack/PythonQwt/issues/93) (rendering performance degradation with Qt 6, also benefitting Qt 5): - - `QwtText`: removed unnecessary `QObject` inheritance and added a font key cache to avoid expensive `QFont` hashing on every text rendering operation - - `QwtText`: cached Qt alignment and text format flags as plain integers to bypass `PyQt6` enum overhead in hot paths - - `QwtText`: enabled font key caching fast path for Qt 6, kept disabled for Qt 5 to preserve consistent text rendering - - `QwtGraphic` and `QwtPainterCommand`: cached Qt flags as integers to reduce per-command overhead, especially under PyQt6 - - `QwtScaleDraw`, `QwtScaleEngine`, `QwtScaleMap` and `QwtScaleDiv`: micro-optimizations on the tick computation and coordinate transform code paths -- Added benchmarking and visual regression scripts under `scripts/` (`bench_qt.ps1`, `bench_plotpy_loadtest.py`, `profile_loadtest.py`, `lineprofile_loadtest.py`, `capture_screenshots.py`, `diff_screenshots.py`) to measure and validate rendering performance and correctness - -### Bug fixes - -- Merged [PR #105](https://github.com/PlotPyStack/PythonQwt/pull/105): fixed legend icon being rendered incorrectly - thanks to @Adrian-B-Moreira -- Fixed `QPaintDevice` warnings on `DevicePixelRatio` and `DevicePixelRatioScaled` metrics in `QwtNullPaintDevice` -- Fixed integer division in `QwtScaleEngine` so that medium ticks are actually produced (previously, the medium tick step was truncated to zero in some configurations) -- Fixed `QwtScaleMap` rectangle transform and degenerate scale handling (when source or destination interval has zero width) - -### Other changes - -- Internal refactor: removed unnecessary `QObject` inheritance from `QwtText` (`QwtText` instances are no longer `QObject` subclasses; this is an internal change with no impact on the public plotting API, but downstream code relying on Qt signals/slots on `QwtText` instances should be adapted) -- Development workflow: replaced legacy `.bat` scripts with a unified `scripts/run_with_env.py` environment loader, refactored VS Code tasks, added coverage tasks and CI gating of PyPI deployment on the test suite passing -- Documentation: updated README, Sphinx documentation, dependencies and added AI coding agent instructions - -## Version 0.15.0 - -- Added support for `QwtDateTimeScaleDraw` and `QwtDateTimeScaleEngine` for datetime axis support (see `QwtDateTimeScaleDraw` and `QwtDateTimeScaleEngine` classes in the `qwt` module) -- Improved font rendering for rotated text in `QwtPlainTextEngine.draw` method: disabled font hinting to avoid character misalignment in rotated text - -## Version 0.14.6 - -- Fixed [Issue #100](https://github.com/PlotPyStack/PythonQwt/issues/100) - TypeError in `QwtSymbol.drawSymbol` method due to outdated `renderSymbols` call -- Fixed [Issue #101](https://github.com/PlotPyStack/PythonQwt/issues/101) - `RuntimeWarning: overflow encountered in cast` when plotting `numpy.float32` curve data -- Merged [PR #103](https://github.com/PlotPyStack/PythonQwt/pull/103): [FIX] wrong handling of `border.rectList` with PySide6 backend - thanks to @martinschwinzerl - -## Version 0.14.5 - -- Merged [PR #98](https://github.com/PlotPyStack/PythonQwt/pull/98): Fix legend still being visible after removed -- Merged [PR #99](https://github.com/PlotPyStack/PythonQwt/pull/99): Fix `QRectF` to `QRect` cast in `QwtPainterClass.drawBackground` - -## Version 0.14.4 - -- Fixed canvas rectangle type in `drawItems` method call in `QwtPlot.drawCanvas` (was causing a hard crash when printing to PDF a canvas with upstream `PlotPy` project) - -## Version 0.14.3 - -- Fixed [Issue #94](https://github.com/PlotPyStack/PythonQwt/issues/94) - Different logarithmic scale behavior when compared to Qwt -- Merged [PR #91](https://github.com/PlotPyStack/PythonQwt/pull/91): Fix: legend now showing up when enabled later - thanks to @nicoddemus -- Removed `QwtPlotItem.setIcon` and `QwtPlotItem.icon` methods (introduced in 0.9.0 but not used in PythonQwt) - -## Version 0.14.2 - -- Merged [PR #89](https://github.com/PlotPyStack/PythonQwt/pull/89): fixed call to `ScaleEngine.autoScale` in `QwtPlot.updateAxes` (returned values were not used) - thanks to @nicoddemus -- Merged [PR #90](https://github.com/PlotPyStack/PythonQwt/pull/90): updated `QwtLinearScaleEngine.autoScale` method implementation to the latest Qwt version - thanks to @nicoddemus - -## Version 0.14.1 - -- Handled `RuntimeError` when running `test_eventfilter.py` on Ubuntu 22.04 (Python 3.12, PyQt5) -- Fixed `ResourceWarning: unclosed file` in `test_cpudemo.py` (purely test issue) -- Fixed segmentation fault in `test_multidemo.py` (purely test issue, related to test utility module `qwt.tests.utils`) -- Update GitHub actions to use the latest versions of actions/checkout, actions/setup-python, ... - -## Version 0.14.0 - -- Dropped support for Python 3.8 - -## Version 0.12.7 - -- Fixed random crashes (segfaults) on Linux related to conflicts between Qt and Python reference counting mechanisms: - - This issue was only happening on Linux, and only with Python 3.12, probably due to changes in Python garbage collector behavior introduced in Python 3.12. Moreover, it was only triggered with an extensive test suite, such as the one provided by the `PlotPy` project. - - The solution was to derive all private classes containing Qt objects from `QObject` instead of `object`, in order to let Qt manage the reference counting of its objects. - - This change was applied to the following classes: - - `QwtLinearColorMap_PrivateData` - - `QwtColumnSymbol_PrivateData` - - `QwtDynGridLayout_PrivateData` - - `QwtGraphic_PrivateData` - - `QwtLegendLabel_PrivateData` - - `QwtNullPaintDevice_PrivateData` - - `QwtPlotCanvas_PrivateData` - - `QwtPlotDirectPainter_PrivateData` - - `QwtPlotGrid_PrivateData` - - `QwtPlotLayout_PrivateData` - - `QwtPlotMarker_PrivateData` - - `QwtPlotRenderer_PrivateData` - - `QwtPlot_PrivateData` - - `QwtAbstractScaleDraw_PrivateData` - - `QwtScaleDraw_PrivateData` - - `QwtScaleWidget_PrivateData` - - `QwtSymbol_PrivateData` - - `QwtText_PrivateData` -- Removed deprecated code regarding PyQt4 compatibility - -## Version 0.12.6 - -- Fixed random crashes (segfaults) on Linux related to Qt objects stored in cache data structures (`QwtText` and `QwtSymbol`) - -- Test suite can simply be run with `pytest` and specific configuration (`conftest.py`) will be taken into account (previously, the test suite has to be run with `pytest qwt` in order to be successfully configured) - -## Version 0.12.5 - -- Add support for NumPy 2.0: - - Use `numpy.asarray` instead of `numpy.array(..., copy=False)` - - Update requirements to remove the NumPy version upper bound constraint - -## Version 0.12.4 - -- Fixed segmentation fault issue reported in the `PlotPy` project: - - See [PlotPy's Issue #13](https://github.com/PlotPyStack/PlotPy/issues/13) for the original issue. - - The issue was caused by the `QwtSymbol` class constructor, and more specifically by its private data object, which instanciated an empty `QtPainterPath` object, causing a segmentation fault on Linux, Python 3.12 and PyQt5. - -## Version 0.12.3 - -- Fixed `Fatal Python error` issue reported in the `PlotPy` project: - - See [PlotPy's Issue #11](https://github.com/PlotPyStack/PlotPy/issues/11) for the original issue, even if the problem is not directly pointed out in the issue comments. - - The issue was caused by the `QwtAbstractScaleDraw` cache mechanism, which was keeping references to `QSizeF` objects that were deleted by the garbage collector at some point. This was causing a segmentation fault, but only on Linux, and only when executing the `PlotPy` test suite in a specific order. - - Thanks to @yuzibo for helping to reproduce the issue and providing a test case, that is the `PlotPy` Debian package build process. - -## Version 0.12.2 - -For this release, test coverage is 72%. - -- Preparing for NumPy V2 compatibility: this is a work in progress, as NumPy V2 is not yet released. In the meantime, requirements have been updated to exclude NumPy V2. -- Fix `QwtPlot.axisInterval` (was raising `AttributeError`) -- Removed unnecessary dependencies (pytest-qt, pytest-cov) -- Moved `conftest.py` to project root -- Project code formatting: using `ruff` instead of `black` and `isort` - -## Version 0.12.1 - -- Fixed `ColorStops.stops` method (was returning a copy of the list of stops instead of the list itself) - -## Version 0.12.0 - -- 30% performance improvement (measured by `qwt.tests.test_loadtest`) by optimizing the `QwtAbstractScaleDraw.tickLabel` method: - - Suppressed an unnecessary call to `QFont.textSize` (which can be quite slow) - - Cached the text size with the label `QwtText` object -- Added support for margins in `QwtPlot` (see Issue #82): - - Default margins are set to 0.05 (5% of the plot area) at each side of the plot - - Margins are adjustable for each plot axis using `QwtPlot.setAxisMargin` (and `QwtPlot.axisMargin` to get the current value) -- Added an additional margin to the left of ticks labels: this margin is set to one character width, to avoid the labels to be truncated while keeping a tight layout -- Slighly improved the new flat style (see V0.7.0) by selecting default fonts -- API breaking change: `QwtLinearColorMap.colorStops` now returns a list of `ColorStop` objects instead of the list of stop values - -## Version 0.11.2 - -- Fixed `TypeError` on `QwtPlotLayout.minimumSizeHint` - -## Version 0.11.1 - -- Fixed remaining `QwtPainter.drawPixmap` call - -## Version 0.11.0 - -- Dropped support for Python 3.7 and earlier -- Dropped support for PyQt4 and PySide2 -- Removed unnecessary argument `numPoints` in `QwtSymbol.drawSymbols` and `QwtSymbol.renderSymbols` methods -- `QwtPlotCanvas`: fixed `BackingStore` feature (`paintAttribute`) - -## Version 0.10.6 - -- Qt6 support: - - Handled all occurences of deprecated ``QWidget.getContentsMargins`` method. - - Removed references to NonCosmeticDefaultPen - - Fixed `QApplication.desktop` `AttributeError` - - Fixed `QPrinter.HighResolution` `AttributeError` on Linux - - Fixed `QPrinter.setColorMode` `AttributeError` on PyQt6/Linux - - Fixed `QPrinter.setOrientation` deprecation issue - - Fixed `QPrinter.setPaperSize` deprecation issue -- Improved unit tests: - - Ensure that tests are entirely executed before quitting (in unattended mode) - - Added more tests on `qwt.symbols` - - Added tests on `qwt.plot_renderer` -- `qwt.plot_renderer`: fixed resolution type -- `qwt.symbols`: fixed `QPointF` type mismatch -- Removed CHM help file generation (obsolete) - -## Version 0.10.5 - -- [Issue #81](https://github.com/PlotPyStack/PythonQwt/issues/81) - Signal disconnection issue with PySide 6.5.3 - -## Version 0.10.4 - -- [Issue #80](https://github.com/PlotPyStack/PythonQwt/issues/80) - Print to PDF: AttributeError: 'NoneType' object has no attribute 'getContentsMargins' - -## Version 0.10.3 - -- [Issue #79](https://github.com/PlotPyStack/PythonQwt/issues/79) - TypeError: unexpected type 'QSize' (thanks to @luc-j-bourhis) - -- Moved project to the [PlotPyStack](https://github.com/PlotPyStack) organization. - -- Unit tests: added support for ``pytest`` and ``coverage`` (60% coverage as of today) - -- [Issue #74](https://github.com/PlotPyStack/PythonQwt/issues/74) - TypeError: QwtPlotDict.__init__() [...] with PySide 6.5.0 - -- [Issue #77](https://github.com/PlotPyStack/PythonQwt/issues/77) - AttributeError: 'XXX' object has no attribute '_QwtPlot__data' - -- [Issue #72](https://github.com/PlotPyStack/PythonQwt/issues/72) - AttributeError: 'QwtScaleWidget' object has no attribute 'maxMajor' / 'maxMinor' / 'stepSize' - -- [Issue #76](https://github.com/PlotPyStack/PythonQwt/issues/76) - [PySide] AttributeError: 'QwtPlotCanvas' object has no attribute 'Sunken' - -- [Issue #63](https://github.com/PlotPyStack/PythonQwt/issues/71) - TypeError: 'PySide2.QtCore.QRect' object is not subscriptable - -## Version 0.10.2 - -- Fixed type mismatch issues on Linux - -## Version 0.10.1 - -- Added support for PyQt6. - -## Version 0.10.0 - -- Added support for QtPy 2 and PySide6. -- Dropped support for Python 2. - -## Version 0.9.2 - -- Curve plotting: added support for `numpy.float32` data type. - -## Version 0.9.1 - -- Added load test showing a large number of plots (eventually highlights performance issues). -- Fixed event management in `QwtPlot` and removed unnecessary `QEvent.LayoutRequest` emission in `QwtScaleWidget` (caused high CPU usage with `guiqwt.ImageWidget`). -- `QwtScaleDiv`: fixed ticks initialization when passing all arguments to constructor. -- tests/image.py: fixed overriden `updateLegend` signature. - -## Version 0.9.0 - -- `QwtPlot`: set the `autoReplot` option at False by default, to avoid time consuming implicit plot updates. -- Added `QwtPlotItem.setIcon` and `QwtPlotItem.icon` method for setting and getting the icon associated to the plot item (as of today, this feature is not strictly needed in PythonQwt: this has been implemented for several use cases in higher level libraries (see PR #61). -- Removed unused `QwtPlotItem.defaultIcon` method. -- Added various minor optimizations for axes/ticks drawing features. -- Fixed `QwtPlot.canvasMap` when `axisScaleDiv` returns None. -- Fixed alias `np.float` which is deprecated in NumPy 1.20. - -## Version 0.8.3 - -- Fixed simple plot examples (setup.py & plot.py's doc page) following the introduction of the new QtPy dependency (Qt compatibility layer) since V0.8.0. - -## Version 0.8.2 - -- Added new GUI-based test script `PythonQwt-py3` to run the test launcher. -- Added command-line options to the `PythonQwt-tests-py3` script to run all the tests simultenously in unattended mode (`--mode unattended`) or to update all the screenshots (`--mode screenshots`). -- Added internal scripts for automated test in virtual environments with both PyQt5 and PySide2. - -## Version 0.8.1 - -- PySide2 support was significatively improved betwen PythonQwt V0.8.0 and V0.8.1 thanks to the new `qwt.qwt_curve.array2d_to_qpolygonf` function. - -## Version 0.8.0 - -- Added PySide2 support: PythonQwt is now compatible with Python 2.7, Python 3.4+, PyQt4, PyQt5 and PySide2! - -## Version 0.7.1 - -- Changed QwtPlotItem.detachItems signature: removed unnecessary "autoDelete" argument, initialiazing "rtti" argument to None (remove all items) -- Improved Qt universal support (PyQt5, ...) - -## Version 0.7.0 - -- Added convenience functions for creating usual objects (curve, grid, marker, ...): - - - `QwtPlotCurve.make` - - `QwtPlotMarker.make` - - `QwtPlotGrid.make` - - `QwtSymbol.make` - - `QwtText.make` - -- Added new test launcher with screenshots (automatically generated) -- Removed `guidata` dependency thanks to the new specific GUI-based test launcher -- Updated documentation (added more examples, using automatically generated screenshots) -- QwtPlot: added "flatStyle" option, a PythonQwt-exclusive feature improving default plot style (without margin, more compact and flat look) -- option is enabled by default -- QwtAbstractScaleDraw: added option to set the tick color lighter factor for each tick type (minor, medium, major) -- this feature is used with the new flatStyle option -- Fixed obvious errors (+ poor implementations) in untested code parts -- Major code cleaning and formatting - -## Version 0.6.2 - -- Fixed Python crash occuring at exit when deleting objects (Python 3 only) -- Moved documentation to -- Added unattended tests with multiple versions of WinPython: - - - WinPython-32bit-2.7.6.4 - - WinPython-64bit-2.7.6.4 - - WinPython-64bit-3.4.4.3 - - WinPython-64bit-3.4.4.3Qt5 - - WPy64-3680 - - WPy64-3771 - - WPy64-3830 - -- Added PyQt4/PyQt5/PySide automatic switch depending on installed libraries - -## Version 0.6.1 - -- Fixed rounding issue with PythonQwt scale engine (0...1000 is now divided in 200-size steps, as in both Qwt and PyQwt) -- Removed unnecessary mask on scaleWidget (this closes #35) -- CurveBenchmark.py: fixed TypeError with numpy.linspace (NumPy=1.18) - -## Version 0.6.0 - -- Ported changes from Qwt 6.1.2 to Qwt 6.1.5 -- `QwtPlotCanvas.setPaintAttribute`: fixed PyQt4 compatibility issue for BackingStore paint attribute -- Fixed DataDemo.py test script (was crashing ; this closes #41) -- `QwtPainterClass.drawBackground`: fixed obvious bug in untested code (this closes #51) -- `qwtFillBackground`: fixed obvious bug in untested code (this closes #50) -- `QwtPainterClass.fillPixmap`: fixed obvious bug in untested code (this closes #49) -- `QwtStyleSheetRecorder`: fixed obvious bug in untested code (this closes #47, closes #48 and closes #52) -- Added "plot without margins" test for Issue #35 - -## Version 0.5.5 - -- `QwtScaleMap.invTransform_scalar`: avoid divide by 0 -- Avoid error when computing ticks: when the axis was so small that no tick could be drawn, an exception used to be raised - -## Version 0.5.4 - -Fixed an annoying bug which caused scale widget (axis ticks in particular) to be misaligned with canvas grid: the user was forced to resize the plot widget as a workaround - -## Version 0.5.3 - -- Better handling of infinity and `NaN` values in scales (removed `NumPy` warnings) -- Now handling infinity and `NaN` values in series data: removing points that can't be drawn -- Fixed logarithmic scale engine: presence of values <= 0 was slowing down series data plotting - -## Version 0.5.2 - -- Added CHM documentation to wheel package -- Fixed `QwtPlotRenderer.setDiscardFlag`/`setLayoutFlag` args -- Fixed `QwtPlotItem.setItemInterest` args -- Fixed `QwtPlot.setAxisAutoScale`/`setAutoReplot` args - -## Version 0.5.1 - -- Fixed Issue #22: fixed scale issues in [CurveDemo2.py](qwt/tests/CurveDemo2.py) and [ImagePlotDemo.py](qwt/tests/ImagePlotDemo.py) -- `QwtPlotCurve`: sticks were not drawn correctly depending on orientation -- `QwtInterval`: avoid overflows with `NumPy` scalars -- Fixed Issue #28: curve shading was broken since v0.5.0 -- setup.py: using setuptools "entry_points" instead of distutils "scripts" -- Showing curves/plots number in benchmarks to avoid any misinterpretation (see Issue #26) -- Added Python2/Python3 scripts for running tests - -## Version 0.5.0 - -- Various optimizations -- Major API simplification, taking into account the feature that won't be implemented (fitting, rounding, weeding out points, clipping, etc.) -- Added `QwtScaleDraw.setLabelAutoSize`/`labelAutoSize` methods to set the new auto size option (see [documentation](http://pythonhosted.org/PythonQwt/)) -- `QwtPainter`: removed unused methods `drawRoundFrame`, `drawImage` and `drawPixmap` - -## Version 0.4.0 - -- Color bar: fixed axis ticks shaking when color bar is enabled -- Fixed `QwtPainter.drawColorBar` for horizontal color bars (typo) -- Restored compatibility with original Qwt signals (`QwtPlot`, ...) - -## Version 0.3.0 - -Renamed the project (python-qwt --> PythonQwt), for various reasons. - -## Version 0.2.1 - -Fixed Issue #23: "argument numPoints is not implemented" error was showing up when calling `QwtSymbol.drawSymbol(symbol, QPoint(x, y))`. - -## Version 0.2.0 - -Added docstrings in all Python modules and a complete documentation based on Sphinx. See the Overview section for API limitations when comparing to Qwt. - -## Version 0.1.1 - -Fixed Issue #21 (blocking issue *only* on non-Windows platforms when building the package): typo in "PythonQwt-tests" script name (in [setup script](setup.py)) - -## Version 0.1.0 - -First alpha public release. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 370a71c..0000000 --- a/LICENSE +++ /dev/null @@ -1,659 +0,0 @@ -PythonQwt License Agreement ---------------------------- - -[1] Software licensed under the terms of Qwt License - -The essential part of the code was translated to Python from Qwt C++ library -and is thus licensed under the terms of the LGPL License from which the Qwt -License 1.0 is derived from (see [***] for more details). - -[2] Software licensed under the terms of the MIT license - -Independent Python modules purely based on new code (no contamination from the -LGPL license inherited from the Qwt Project) are distributed under the terms -of the MIT License (see [*] and [**]). - -[3] Software licensed under the terms of PyQwt License - -Some files under the "tests" subfolder of the main Python package directory -were derived from PyQwt PyQt4 examples and are thus distributed under the -terms of the GPL License from which the PyQwt License 1.0 is derived from -(see [****] for more details). - - -[*] PythonQwt License Agreement for new and exclusive Python material (MIT) - -Copyright (c) 2015 Pierre Raybaut - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - - -[**] Spyder License Agreement - -Spyder License Agreement (MIT License) --------------------------------------- - -Copyright (c) 2009-2013 Pierre Raybaut -Copyright (c) 2013-2015 The Spyder Development Team - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - - -[***] PythonQwt License Agreement for code translated from C++ (Qwt License) - -Copyright (c) 2002 Uwe Rathmann, for the original C++ code -Copyright (c) 2015 Pierre Raybaut, for the Python translation and optimization - - - Qwt License - Version 1.0, January 1, 2003 - -The Qwt library and included programs are provided under the terms -of the GNU LESSER GENERAL PUBLIC LICENSE (LGPL) with the following -exceptions: - - 1. Widgets that are subclassed from Qwt widgets do not - constitute a derivative work. - - 2. Static linking of applications and widgets to the - Qwt library does not constitute a derivative work - and does not require the author to provide source - code for the application or widget, use the shared - Qwt libraries, or link their applications or - widgets against a user-supplied version of Qwt. - - If you link the application or widget to a modified - version of Qwt, then the changes to Qwt must be - provided under the terms of the LGPL in sections - 1, 2, and 4. - - 3. You do not have to provide a copy of the Qwt license - with programs that are linked to the Qwt library, nor - do you have to identify the Qwt license in your - program or documentation as required by section 6 - of the LGPL. - - - However, programs must still identify their use of Qwt. - The following example statement can be included in user - documentation to satisfy this requirement: - - [program/widget] is based in part on the work of - the Qwt project (http://qwt.sf.net). - ----------------------------------------------------------------------- - - - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations below. - - When we speak of free software, we are referring to freedom of use, -not price. Our General Public Licenses are designed to make sure that -you have the freedom to distribute copies of free software (and charge -for this service if you wish); that you receive source code or can get -it if you want it; that you can change the software and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. - - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it becomes -a de-facto standard. To achieve this, non-free programs must be -allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. - - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, complete source code means -all the source code for all modules it contains, plus any associated -interface definition files, plus the scripts used to control compilation -and installation of the library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete source code as you receive it, in any medium, provided that -you conspicuously and appropriately publish on each copy an -appropriate copyright notice and disclaimer of warranty; keep intact -all the notices that refer to this License and to the absence of any -warranty; and distribute a copy of this License along with the -Library. - - You may charge a fee for the physical act of transferring a copy, -and you may at your option offer warranty protection in exchange for a -fee. - - 2. You may modify your copy or copies of the Library or any portion -of it, thus forming a work based on the Library, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Library, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote -it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. - - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you accompany -it with the complete corresponding machine-readable source code, which -must be distributed under the terms of Sections 1 and 2 above on a -medium customarily used for software interchange. - - If distribution of object code is made by offering access to copy -from a designated place, then offering equivalent access to copy the -source code from the same place satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. - - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at - least three years, to give the same user the materials - specified in Subsection 6a, above, for a charge no more - than the cost of performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be distributed need not include anything that is -normally distributed (in either source or binary form) with the major -components (compiler, kernel, and so on) of the operating system on -which the executable runs, unless that component itself accompanies -the executable. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. - - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library is void, and will automatically terminate your -rights under this License. However, parties who have received copies, -or rights, from you under this License will not have their licenses -terminated so long as such parties remain in full compliance. - - 9. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -subject to these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties with -this License. - - 11. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Library. - -If any portion of this section is held invalid or unenforceable under any -particular circumstance, the balance of the section is intended to apply, -and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library under this License may add -an explicit geographical distribution limitation excluding those countries, -so that distribution is permitted only in or among countries not thus -excluded. In such case, this License incorporates the limitation as if -written in the body of this License. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser General Public License from time to time. -Such new versions will be similar in spirit to the present version, -but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Library -specifies a version number of this License which applies to it and -"any later version", you have the option of following the terms and -conditions either of that version or of any later version published by -the Free Software Foundation. If the Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. - - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -write to the author to ask for permission. For software which is -copyrighted by the Free Software Foundation, write to the Free -Software Foundation; we sometimes make exceptions for this. Our -decision will be guided by the two goals of preserving the free status -of all derivatives of our free software and of promoting the sharing -and reuse of software generally. - - NO WARRANTY - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY -KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN -WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY -AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU -FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR -CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE -LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING -RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A -FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms of the -ordinary General Public License). - - To apply these terms, attach the following notices to the library. It is -safest to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the library, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! - - -[****] PyQwt License - -Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt code -Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -developments (e.g. ported to PythonQwt API) - - PyQwt LICENSE - Version 3, March 2006 - -PyQwt is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -PyQwt is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with PyQwt; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -In addition, as a special exception, Gerard Vermeulen gives permission -to link PyQwt dynamically with non-free versions of Qt and PyQt, -and to distribute PyQwt in this form, provided that equally powerful -versions of Qt and PyQt have been released under the terms of the GNU -General Public License. - -If PyQwt is dynamically linked with non-free versions of Qt and PyQt, -PyQwt becomes a free plug-in for a non-free program. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index f814f5f..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -graft doc -include *.desktop \ No newline at end of file diff --git a/PythonQwt-tests.desktop b/PythonQwt-tests.desktop deleted file mode 100644 index e4e67b3..0000000 --- a/PythonQwt-tests.desktop +++ /dev/null @@ -1,10 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Name=PythonQwt-tests -GenericName=PythonQwt Test launcher -Comment=The PythonQwt library provides Qt plotting widgets for Python -TryExec=PythonQwt-tests -Exec=PythonQwt-tests -Icon=PythonQwt.svg -Categories=Education;Science;Physics; diff --git a/README.md b/README.md deleted file mode 100644 index 0c3db65..0000000 --- a/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# PythonQwt: Qt plotting widgets for Python - -[![license](https://img.shields.io/pypi/l/PythonQwt.svg)](./LICENSE) -[![pypi version](https://img.shields.io/pypi/v/PythonQwt.svg)](https://pypi.org/project/PythonQwt/) -[![PyPI status](https://img.shields.io/pypi/status/PythonQwt.svg)](https://github.com/PlotPyStack/PythonQwt) -[![PyPI pyversions](https://img.shields.io/pypi/pyversions/PythonQwt.svg)](https://pypi.python.org/pypi/PythonQwt/) -[![download count](https://img.shields.io/conda/dn/conda-forge/PythonQwt.svg)](https://www.anaconda.com/download/) -[![Documentation Status](https://readthedocs.org/projects/pythonqwt/badge/?version=latest)](https://pythonqwt.readthedocs.io/en/latest/?badge=latest) - -ℹ️ Created in 2014 by Pierre Raybaut and maintained by the [PlotPyStack](https://github.com/PlotPyStack) organization. - -![PythonQwt Test Launcher](https://raw.githubusercontent.com/PlotPyStack/PythonQwt/master/qwt/tests/data/testlauncher.png) - -The `PythonQwt` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries. - -The `PythonQwt` package consists of a single Python package named `qwt` and of a few other files (examples, doc, ...). - -See documentation [online](https://pythonqwt.readthedocs.io/en/latest/) or [PDF](https://pythonqwt.readthedocs.io/_/downloads/en/latest/pdf/) for more details on the library and [changelog](CHANGELOG.md) for recent history of changes. - -## Sample - -```python -import numpy as np -from qtpy import QtWidgets as QW - -import qwt - -app = QW.QApplication([]) - -# Create plot widget -plot = qwt.QwtPlot("Trigonometric functions") -plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend) - -# Create two curves and attach them to plot -x = np.linspace(-10, 10, 500) -qwt.QwtPlotCurve.make(x, np.cos(x), "Cosine", plot, linecolor="red", antialiased=True) -qwt.QwtPlotCurve.make(x, np.sin(x), "Sine", plot, linecolor="blue", antialiased=True) - -# Resize and show plot -plot.resize(600, 300) -plot.show() - -app.exec_() -``` - -![Simple plot example](doc/_static/QwtPlot_example.png) - -## Examples (tests) - -The GUI-based test launcher may be executed from Python: - -```python -from qwt import tests -tests.run() -``` - -or from the command line: - -```bash -PythonQwt-tests -``` - -Tests may also be executed in unattended mode: - -```bash -PythonQwt-tests --mode unattended -``` - -## Overview - -The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations. - -The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `PlotPy`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`. - -Only the following plot items are currently implemented in `qwt` (the only plot items needed by `PlotPy`): `QwtPlotItem` (base class), `QwtPlotGrid`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`. - -See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) for more details on API limitations when comparing to Qwt. - -## Roadmap - -The `qwt` package short-term roadmap is the following: - -- [X] Drop support for PyQt4 and PySide2 -- [X] Drop support for Python <= 3.8 -- [X] Replace `setup.py` by `pyproject.toml`, using `setuptools` (e.g. see `guidata`) -- [ ] Add more unit tests: the ultimate goal is to reach 90% code coverage - -## Dependencies and installation - -### Supported Qt versions and bindings - -The whole PlotPyStack set of libraries relies on the [Qt](https://doc.qt.io/) GUI toolkit, thanks to [QtPy](https://pypi.org/project/QtPy/), an abstraction layer which allows to use the same API to interact with different Python-to-Qt bindings (PyQt5, PyQt6, PySide2, PySide6). - -Compatibility table: - -| PythonQwt version | PyQt5 | PyQt6 | PySide2 | PySide6 | -|-------------------|-------|-------|---------|---------| -| 0.15 and earlier | ✅ | ⚠️ | ❌ | ⚠️ | -| Latest | ✅ | ✅ | ❌ | ✅ | - -### Requirements - -- Python >=3.9 -- QtPy >= 1.9 (and a Python-to-Qt binding library, see above) -- NumPy >= 1.21 - -### Optional dependencies - -- coverage, pytest (for unit tests) -- sphinx (for documentation generation) - -### Installation - -From PyPI: - -```bash -pip install PythonQwt -``` - -From the source package: - -```bash -python -m build -``` - -## Performance investigation - -Tooling for performance benchmarks, profiling and visual-regression checks across PyQt5/PyQt6/PySide6 lives in [`scripts/`](scripts/README.md). See [`doc/issue93_optimization_summary.md`](doc/issue93_optimization_summary.md) for a worked example. - -## Copyrights - -### Main code base - -- Copyright © 2002 Uwe Rathmann, for the original Qwt C++ code -- Copyright © 2015 Pierre Raybaut, for the Qwt C++ to Python translation and optimization -- Copyright © 2015 Pierre Raybaut, for the PythonQwt specific and exclusive Python material - -### Some examples - -- Copyright © 2003-2009 Gerard Vermeulen, for the original PyQwt code -- Copyright © 2015 Pierre Raybaut, for the PyQt5/PySide port and further developments (e.g. ported to PythonQwt API) - -## License - -The `qwt` Python package was partly (>95%) translated from Qwt C++ library: the associated code is distributed under the terms of the LGPL license. The rest of the code was either wrote from scratch or strongly inspired from MIT licensed third-party software. - -See included [LICENSE](LICENSE) file for more details about licensing terms. diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..2f7efbe --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-minimal \ No newline at end of file diff --git a/doc/_static/PythonQwt_logo.png b/doc/_static/PythonQwt_logo.png deleted file mode 100644 index 93e4143..0000000 Binary files a/doc/_static/PythonQwt_logo.png and /dev/null differ diff --git a/doc/_static/QwtPlot_example.png b/doc/_static/QwtPlot_example.png deleted file mode 100644 index 1110496..0000000 Binary files a/doc/_static/QwtPlot_example.png and /dev/null differ diff --git a/doc/_static/panorama.png b/doc/_static/panorama.png deleted file mode 100644 index d30b502..0000000 Binary files a/doc/_static/panorama.png and /dev/null differ diff --git a/doc/_static/symbol_path_example.png b/doc/_static/symbol_path_example.png deleted file mode 100644 index d050a80..0000000 Binary files a/doc/_static/symbol_path_example.png and /dev/null differ diff --git a/doc/conf.py b/doc/conf.py deleted file mode 100644 index 5969162..0000000 --- a/doc/conf.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.append(os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc"] -try: - import sphinx.ext.viewcode # noqa: F401 - - extensions.append("sphinx.ext.viewcode") -except ImportError: - print("WARNING: the Sphinx viewcode extension was not found", file=sys.stderr) - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix of source filenames. -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = "PythonQwt" -import time - -this_year = time.strftime("%Y", time.localtime()) -copyright = "2002 Uwe Rathmann (for the original C++ code/doc), 2015 Pierre Raybaut (for the Python translation/optimization/doc adaptation)" - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -import qwt - -version = ".".join(qwt.__version__.split(".")[:2]) -# The full version, including alpha/beta/rc tags. -release = qwt.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -# unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = [] - -# The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -modindex_common_prefix = ["qwt."] - -autodoc_member_order = "bysource" - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -try: - import python_docs_theme # noqa: F401 - - html_theme = "python_docs_theme" -except ImportError: - html_theme = "default" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -## html_theme_options = {'sidebarbgcolor': '#227A2B', -## 'sidebarlinkcolor': '#98ff99'} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = "%s %s Manual" % (project, version) - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = '%s Manual' % project - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = "_static/PythonQwt_logo.png" - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = 'favicon.ico' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -html_use_modindex = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = "PythonQwt" - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -# latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -# latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ("index", "qwt.tex", "PythonQwt Manual", "Pierre Raybaut", "manual"), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -# latex_preamble = '' - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_use_modindex = True diff --git a/doc/examples/bodedemo.rst b/doc/examples/bodedemo.rst deleted file mode 100644 index 6226f5e..0000000 --- a/doc/examples/bodedemo.rst +++ /dev/null @@ -1,7 +0,0 @@ -Bode demo -~~~~~~~~~ - -.. image:: /../qwt/tests/data/bodedemo.png - -.. literalinclude:: /../qwt/tests/test_bodedemo.py - :start-after: SHOW diff --git a/doc/examples/cartesian.rst b/doc/examples/cartesian.rst deleted file mode 100644 index bc0a844..0000000 --- a/doc/examples/cartesian.rst +++ /dev/null @@ -1,7 +0,0 @@ -Cartesian demo -~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/cartesian.png - -.. literalinclude:: /../qwt/tests/test_cartesian.py - :start-after: SHOW diff --git a/doc/examples/cpudemo.rst b/doc/examples/cpudemo.rst deleted file mode 100644 index 58f471f..0000000 --- a/doc/examples/cpudemo.rst +++ /dev/null @@ -1,7 +0,0 @@ -CPU plot demo -~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/cpudemo.png - -.. literalinclude:: /../qwt/tests/test_cpudemo.py - :start-after: SHOW diff --git a/doc/examples/curvebenchmark1.rst b/doc/examples/curvebenchmark1.rst deleted file mode 100644 index a372c02..0000000 --- a/doc/examples/curvebenchmark1.rst +++ /dev/null @@ -1,7 +0,0 @@ -Curve benchmark demo 1 -~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/curvebenchmark1.png - -.. literalinclude:: /../qwt/tests/test_curvebenchmark1.py - :start-after: SHOW diff --git a/doc/examples/curvebenchmark2.rst b/doc/examples/curvebenchmark2.rst deleted file mode 100644 index 2c9daf1..0000000 --- a/doc/examples/curvebenchmark2.rst +++ /dev/null @@ -1,7 +0,0 @@ -Curve benchmark demo 2 -~~~~~~~~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/curvebenchmark2.png - -.. literalinclude:: /../qwt/tests/test_curvebenchmark2.py - :start-after: SHOW diff --git a/doc/examples/curvedemo1.rst b/doc/examples/curvedemo1.rst deleted file mode 100644 index 13f3c89..0000000 --- a/doc/examples/curvedemo1.rst +++ /dev/null @@ -1,7 +0,0 @@ -Curve demo 1 -~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/curvedemo1.png - -.. literalinclude:: /../qwt/tests/test_curvedemo1.py - :start-after: SHOW diff --git a/doc/examples/curvedemo2.rst b/doc/examples/curvedemo2.rst deleted file mode 100644 index 8e4919f..0000000 --- a/doc/examples/curvedemo2.rst +++ /dev/null @@ -1,7 +0,0 @@ -Curve demo 2 -~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/curvedemo2.png - -.. literalinclude:: /../qwt/tests/test_curvedemo2.py - :start-after: SHOW diff --git a/doc/examples/data.rst b/doc/examples/data.rst deleted file mode 100644 index fcdda3a..0000000 --- a/doc/examples/data.rst +++ /dev/null @@ -1,7 +0,0 @@ -Data demo -~~~~~~~~~ - -.. image:: /../qwt/tests/data/data.png - -.. literalinclude:: /../qwt/tests/test_data.py - :start-after: SHOW diff --git a/doc/examples/errorbar.rst b/doc/examples/errorbar.rst deleted file mode 100644 index 7981d68..0000000 --- a/doc/examples/errorbar.rst +++ /dev/null @@ -1,7 +0,0 @@ -Error bar demo -~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/errorbar.png - -.. literalinclude:: /../qwt/tests/test_errorbar.py - :start-after: SHOW diff --git a/doc/examples/eventfilter.rst b/doc/examples/eventfilter.rst deleted file mode 100644 index 53b4033..0000000 --- a/doc/examples/eventfilter.rst +++ /dev/null @@ -1,7 +0,0 @@ -Event filter demo -~~~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/eventfilter.png - -.. literalinclude:: /../qwt/tests/test_eventfilter.py - :start-after: SHOW diff --git a/doc/examples/image.rst b/doc/examples/image.rst deleted file mode 100644 index e145e63..0000000 --- a/doc/examples/image.rst +++ /dev/null @@ -1,7 +0,0 @@ -Image plot demo -~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/image.png - -.. literalinclude:: /../qwt/tests/test_image.py - :start-after: SHOW diff --git a/doc/examples/index.rst b/doc/examples/index.rst deleted file mode 100644 index c599332..0000000 --- a/doc/examples/index.rst +++ /dev/null @@ -1,49 +0,0 @@ -.. _examples: - -Examples -======== - -The test launcher ------------------ - -A lot of examples are available in the ``qwt.tests`` module :: - - from qwt import tests - tests.run() - -The two lines above execute the ``PythonQwt-tests`` test launcher: - -.. image:: /../qwt/tests/data/testlauncher.png - -GUI-based test launcher can be executed from the command line thanks to the -``PythonQwt-tests`` test script. - -Unit tests may be executed from the command line thanks to the console-based script -``PythonQwt-tests``: ``PythonQwt-tests --mode unattended``. - -Tests ------ - - - -Here are some examples from the `qwt.tests` module: - -.. toctree:: - :maxdepth: 2 - - bodedemo - cartesian - cpudemo - curvebenchmark1 - curvebenchmark2 - curvedemo1 - curvedemo2 - data - errorbar - eventfilter - image - logcurve - mapdemo - multidemo - simple - vertical diff --git a/doc/examples/logcurve.rst b/doc/examples/logcurve.rst deleted file mode 100644 index 48eb3ec..0000000 --- a/doc/examples/logcurve.rst +++ /dev/null @@ -1,7 +0,0 @@ -Log curve plot demo -~~~~~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/logcurve.png - -.. literalinclude:: /../qwt/tests/test_logcurve.py - :start-after: SHOW diff --git a/doc/examples/mapdemo.rst b/doc/examples/mapdemo.rst deleted file mode 100644 index 9fba166..0000000 --- a/doc/examples/mapdemo.rst +++ /dev/null @@ -1,7 +0,0 @@ -Map demo -~~~~~~~~ - -.. image:: /../qwt/tests/data/mapdemo.png - -.. literalinclude:: /../qwt/tests/test_mapdemo.py - :start-after: SHOW diff --git a/doc/examples/multidemo.rst b/doc/examples/multidemo.rst deleted file mode 100644 index 84f80d0..0000000 --- a/doc/examples/multidemo.rst +++ /dev/null @@ -1,7 +0,0 @@ -Multi demo -~~~~~~~~~~ - -.. image:: /../qwt/tests/data/multidemo.png - -.. literalinclude:: /../qwt/tests/test_multidemo.py - :start-after: SHOW diff --git a/doc/examples/simple.rst b/doc/examples/simple.rst deleted file mode 100644 index 956923d..0000000 --- a/doc/examples/simple.rst +++ /dev/null @@ -1,7 +0,0 @@ -Really simple demo -~~~~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/simple.png - -.. literalinclude:: /../qwt/tests/test_simple.py - :start-after: SHOW diff --git a/doc/examples/vertical.rst b/doc/examples/vertical.rst deleted file mode 100644 index c1cedc9..0000000 --- a/doc/examples/vertical.rst +++ /dev/null @@ -1,7 +0,0 @@ -Vertical plot demo -~~~~~~~~~~~~~~~~~~ - -.. image:: /../qwt/tests/data/vertical.png - -.. literalinclude:: /../qwt/tests/test_vertical.py - :start-after: SHOW diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 4690dcd..0000000 --- a/doc/index.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. automodule:: qwt - -Contents: - -.. toctree:: - :maxdepth: 2 - - overview - installation - examples/index - reference/index - - -Indices and tables: - -* :ref:`genindex` -* :ref:`search` diff --git a/doc/installation.rst b/doc/installation.rst deleted file mode 100644 index 7b840f2..0000000 --- a/doc/installation.rst +++ /dev/null @@ -1,33 +0,0 @@ -Installation -============ - -Dependencies ------------- - -Requirements: - * Python 3.9 or higher - * PyQt5 5.15, PyQt6 or PySide6 - * QtPy 1.9 or higher - * NumPy 1.21 or higher - * Sphinx for documentation generation - * pytest, coverage for unit testing - -Installation ------------- - -From PyPI: - - `pip install PythonQwt` - -From the source package: - - `python -m build` - -Help and support ----------------- - -External resources: - - * Bug reports and feature requests: `GitHub`_ - -.. _GitHub: https://github.com/PlotPyStack/PythonQwt diff --git a/doc/issue93_optimization_summary.md b/doc/issue93_optimization_summary.md deleted file mode 100644 index 91ba980..0000000 --- a/doc/issue93_optimization_summary.md +++ /dev/null @@ -1,284 +0,0 @@ -# Issue #93 — Performance degradation with Qt6: optimization summary - -This document summarises the work done on the `fix/93-performance-degradation-with-qt6` branch to investigate and close the Qt5↔Qt6 performance gap reported in [issue #93](https://github.com/PlotPyStack/PythonQwt/issues/93). It walks through each optimization phase, the diagnostic method used, the change applied, and the measured impact. - -All numbers below were collected on the same Windows 11 machine, Python 3.11.9, with three sibling virtual environments (`.venvs/pyqt5`, `.venvs/pyqt6`, `.venvs/pyside6`), each pinning a single Qt binding (PyQt5 5.15.11 / Qt 5.15.2, PyQt6 6.11.0 / Qt 6.11.0, PySide6 6.11.0 / Qt 6.11.0). - -Two benchmarks were used throughout: - -- **`qwt/tests/test_loadtest.py`** — the PythonQwt micro load test (raw QwtPlot widgets, no PlotPy). Driven by `scripts/bench_qt.ps1`. Reports `Average elapsed time: ms` per binding. -- **PlotPy `test_loadtest`** — `plotpy/tests/benchmarks/test_loadtest.py`, the test cited in the original GitHub issue. Driven by `scripts/bench_plotpy_loadtest.py` (60 plot widgets, 3 runs). - -## Baseline (master, commit `1ab70cd`) - -| Benchmark | PyQt5 | PyQt6 | PySide6 | -|---|---:|---:|---:| -| PythonQwt `test_loadtest` (avg of 5) | ~1 900 ms | ~2 300 ms | ~2 900 ms | -| PlotPy `test_loadtest`, 60 plots (avg of 3) | 25 134 ms | 42 202 ms | 53 160 ms | - -Headline gap on PlotPy: **PyQt6 ≈ +68 % slower than PyQt5**, **PySide6 ≈ +111 % slower than PyQt5**. - -The cProfile traces taken on master pointed at four hot families of code paths inside PythonQwt: - -1. `QwtScaleMap.transform()` — called on every coordinate transformed. -2. `QwtScaleDiv.contains()` and `QwtScaleEngine.contains()/strip()` — called on every tick label candidate. -3. `QwtAbstractScaleDraw.labelRect()` and helpers — called on every drawn tick. -4. `QwtText` / `QwtPlainTextEngine` text-size and text-margin computations — called on every tick label and every plot title. - -All four are amortised over thousands of calls per plot, and all four are sensitive to per-call Python overhead (attribute lookups, QObject machinery, redundant Qt round-trips). That is precisely the kind of overhead that the Qt6 bindings (especially PySide6) make more expensive than Qt5, which explains why a regression that is barely visible on Qt5 becomes a 2× slowdown on Qt6. - -## Phase 1 — cProfile-driven optimizations (commit `ef793e1`) - -**Method.** `scripts/profile_loadtest.py` runs the PythonQwt load test under `cProfile` and dumps a sorted-by-cumulative-time stats file. Diff between PyQt5 and PySide6 traces highlighted the four families above. - -**Changes.** - -- **`qwt/scale_map.py`** — inlined the scalar fast path in `QwtScaleMap.transform()` (avoid the array branch and a method dispatch when the input is a plain Python `float`). -- **`qwt/scale_div.py`** — rewrote `QwtScaleDiv.contains()` as a direct comparison against the cached lower/upper bounds, instead of going through `QwtInterval`. -- **`qwt/scale_engine.py`** — `QwtScaleEngine.contains()` and `QwtScaleEngine.strip()` similarly bypass `QwtInterval` round-trips for the common case. -- **`qwt/scale_draw.py`** — replaced the per-call alignment branching in `labelRect()`/`labelPosition()` with module-level constants (`_ALIGN_BOTTOM`, `_ALIGN_TOP`, `_ALIGN_LEFT`, `_ALIGN_RIGHT`); added a rotation==0 fast path in `labelRect()`; cached the axis `orientation` once in `setAlignment()` instead of recomputing it on every call. -- **`qwt/text.py`** — first round of cleanups around `QwtText.textSize()` and `QwtPlainTextEngine.textMargins()`, plus a per-engine "last seen font id" fast path that skips the `QFontMetricsF` rebuild when the same `QFont` instance is reused (which is the dominant case during a single plot repaint). - -**Results after phase 1** (PythonQwt micro `test_loadtest`, 5 runs each): - -| Binding | Before | After phase 1 | Speedup | -|---|---:|---:|---:| -| PyQt5 | ~1 900 ms | ~620 ms | ×3.0 | -| PyQt6 | ~2 300 ms | ~780 ms | ×2.9 | -| PySide6 | ~2 900 ms | ~960 ms | ×3.0 | - -Phase 1 closed most of the absolute slowdown but did not change the *relative* Qt5↔Qt6 gap — all three bindings benefited roughly equally, because the optimizations attacked Python-side overhead that scales with call count regardless of binding. - -## Phase 2 — line-profiler-driven optimizations (commit `27a0e17`) - -**Method.** `scripts/lineprofile_loadtest.py` instruments the surviving hot functions with `line_profiler` (`@profile`) and re-runs the load test. The line-by-line traces revealed two new dominant costs that did not show up clearly in cProfile: - -1. The `QObject` base class on `QwtText_PrivateData` and on the `_PrivateData` classes inside `qwt/scale_draw.py`. Every instantiation went through Qt's meta-object system, which is dramatically more expensive on PyQt6 / PySide6 than on PyQt5. -2. Repeated calls to `QFont.key()` from within `QwtText.textSize()`, `QwtText.effectiveAscent()` and `QwtPlainTextEngine.textMargins()`. Each call serialises the full font descriptor; the same descriptor is hit thousands of times during a single load test because the same default font instance is reused. - -**Changes.** - -- **`qwt/text.py`** — `QwtText_PrivateData` is now a plain `object` subclass with `__slots__`; no QObject. Added a process-wide `_FONT_KEY_CACHE` keyed by `id(font)` that memoizes `font.key()` (with a hard cap of 1024 entries to avoid unbounded growth). Helper `font_key_cached()` is used by `effectiveAscent`, `QwtPlainTextEngine.textMargins`, and `QwtText.textSize`. -- **`qwt/scale_draw.py`** — the various `_PrivateData` containers also drop `QObject` and use `__slots__`. - -**Results after phase 2** (PythonQwt micro `test_loadtest`, 5 runs each): - -| Binding | Before phase 2 | After phase 2 | Speedup vs phase 1 | Speedup vs master | -|---|---:|---:|---:|---:| -| PyQt5 | ~620 ms | ~445 ms | ×1.4 | ×4.3 | -| PyQt6 | ~780 ms | ~480 ms | ×1.6 | ×4.8 | -| PySide6 | ~960 ms | ~600 ms | ×1.6 | ×4.8 | - -Phase 2 finally closed the *relative* gap as well: Qt6 bindings benefit more than Qt5 from removing QObject inheritance and `font.key()` calls, because the per-call overhead they save is binding-cost-dominated. - -## Phase 3 — screenshot regression analysis - -**Method.** Two new helpers were added in `scripts/`: - -- **`capture_screenshots.py`** — runs each of the 22 PythonQwt visual tests in a subprocess with `PYTHONQWT_TAKE_SCREENSHOTS=1` and copies the resulting PNGs into `shots///`. -- **`diff_screenshots.py`** — pixel-compares two screenshot folders (Pillow + NumPy) and emits a markdown table with `IDENTICAL` / `EQUAL_PIXELS` / `DIFFER` status, plus the count and magnitude of differing pixels. - -A full matrix was captured (master × 3 bindings, fix × 3 bindings, plus self-compare baselines master×master and fix×fix to filter out flaky tests that have inherently random or time-stamped output). - -**Findings.** - -- **PyQt6 and PySide6**: zero new deterministic differences vs master. Every diff that appeared was already present in the master self-compare baseline (the 6 tests `test_cpudemo`, `test_curvebenchmark1/2`, `test_data`, `test_loadtest`, `test_mapdemo`, all of which use random data or timestamps). -- **PyQt5**: 6 *new* deterministic, sub-perceptual differences appeared, in `test_backingstore`, `test_bodedemo`, `test_image`, `test_relativemargin`, `test_symbols`, `test_vertical`. All diffs were tiny (a few dozen pixels each, max magnitude ≤ 26/255), scattered around antialiased text and curve edges. - -### Per-test screenshot status (master vs phase-2 fix, all bindings) - -Each cell aggregates two pixel-diffs per test (master vs `master2` self-compare baseline, and master vs phase-2 fix). The classification rule is: - -- ✅ — both diffs report identical or pixel-equal output (test is fully reproducible *and* the optimization branch did not change it). -- ⚠️ — both diffs are non-zero (test is *intrinsically* flaky — random data, timestamps, live system stats — so any difference is noise, not a regression). -- ❌ — baseline is identical but the fix differs (a real visual regression introduced by the optimization branch). - -| Test | PyQt5 | PyQt6 | PySide6 | -|---|:-:|:-:|:-:| -| `test_backingstore` | ❌ 55 px (max=11) | ✅ | ✅ | -| `test_bodedemo` | ❌ 39 px (max=16) | ✅ | ✅ | -| `test_cartesian` | ✅ | ✅ | ✅ | -| `test_cpudemo` | ⚠️ | ⚠️ | ⚠️ | -| `test_curvebenchmark1` | ⚠️ | ⚠️ | ⚠️ | -| `test_curvebenchmark2` | ⚠️ | ⚠️ | ⚠️ | -| `test_curvedemo1` | ✅ | ✅ | ✅ | -| `test_curvedemo2` | ✅ | ✅ | ✅ | -| `test_data` | ⚠️ | ⚠️ | ⚠️ | -| `test_errorbar` | ✅ | ✅ | ✅ | -| `test_eventfilter` | ✅ | ✅ | ✅ | -| `test_highdpi` | ✅ | ✅ | ✅ | -| `test_image` | ❌ 6 px (max=9) | ✅ | ✅ | -| `test_loadtest` | ⚠️ | ⚠️ | ⚠️ | -| `test_logcurve` | ✅ | ✅ | ✅ | -| `test_mapdemo` | ⚠️ | ⚠️ | ⚠️ | -| `test_multidemo` | ✅ | ✅ | ✅ | -| `test_relativemargin` | ❌ 72 px (max=11) | ✅ | ✅ | -| `test_simple` | ✅ | ✅ | ✅ | -| `test_stylesheet` | ✅ | ✅ | ✅ | -| `test_symbols` | ❌ 4 px (max=9) | ✅ | ✅ | -| `test_vertical` | ❌ 88 px (max=26) | ✅ | ✅ | - -**Summary at end of phase 3.** PyQt6 and PySide6: 16 ✅ / 6 ⚠️ / **0 ❌**. PyQt5: 10 ✅ / 6 ⚠️ / **6 ❌**. The 6 ❌ entries on PyQt5 are the regression that phase 4 fixes. - -**Root cause.** The id-keyed `font.key()` cache subtly changes the order in which the Qt5 font engine is asked to materialise specific font descriptors. On Qt5, the font engine hints text glyphs slightly differently depending on first-touch order — invisible to a human, but bit-non-identical to master. Qt6's font engine does not show this sensitivity. - -## Phase 4 — Option A: gate the font-key fast path on Qt5 (current state) - -**Change.** In `qwt/text.py`, the id-keyed cache is now guarded by a Qt-version check: - -```python -from qtpy import QT_VERSION as _QT_VERSION - -_USE_FONT_KEY_FAST_PATH = not str(_QT_VERSION).startswith("5.") - -def font_key_cached(font) -> str: - if not _USE_FONT_KEY_FAST_PATH: - return font.key() - # ... id-keyed cache lookup ... -``` - -On Qt5 this becomes a thin pass-through to `font.key()` — bit-identical output to master is restored. On Qt6 (where it actually matters most for this issue) the optimization stays in place. - -**Verification.** - -1. **Screenshot regression** — re-ran PyQt5 capture and diff. The 6 ❌ entries from the phase 3 table all flip to ✅. Final per-binding tally becomes **16 ✅ / 6 ⚠️ / 0 ❌** on every binding — i.e. byte-identical output to master on every test that is reproducible at all. -2. **Test suite** — `pytest -q` with `PYTHONQWT_UNATTENDED_TESTS=1` on all three bindings: - - PyQt5: 26 passed, 1 skipped - - PyQt6: 26 passed, 1 skipped - - PySide6: 26 passed, 1 skipped, 1 warning -3. **Performance** — PyQt5 micro-bench rose from ~445 ms to ~450–550 ms (≈ +5 ms, well within the run-to-run noise). Qt6 numbers are unchanged. - -## Phase 5 — closing the residual Qt5↔Qt6 gap - -After phases 1–4 the Qt6 path was still measurably slower than Qt5 on the micro load test (~+20 % / +100 ms). The goal of phase 5 was to **understand and remove that residual gap**, not just to keep optimising blindly. - -**Method.** A second cProfile + `line_profiler` pass was run on the post-phase-4 tip, this time focused on the diff between PyQt5 and PyQt6 traces (rather than absolute hotspots). Three concrete root causes were identified, all specific to the Qt6 binding: - -1. **Python `enum.IntFlag` arithmetic.** PyQt6 exposes Qt enums as `enum.Flag` subclasses; every `flags & Qt.SomeFlag` test goes through `enum.__and__ → enum.__call__ → enum.__new__` (~6 µs each). PyQt5 uses plain ints, so the same code costs ~50 ns there. cProfile attributed ≈ 62 ms / run on PyQt6 to `enum.py`, **0 ms on PyQt5**. The single worst caller was `QwtPainterCommand.__init__`, which performs **twelve** successive `flags & QPaintEngine.DirtyXxx` tests per painter command — at ~300 commands per load-test run that is 3 600 enum operations alone. -2. **`QFont.key()` is ~3× slower per call on PyQt6.** Per-call sip dispatch costs were measured at 3.3 µs (PyQt5) vs 9.3 µs (PyQt6) for cheap getters. `font.key()` was the single biggest residual hotspot inside `QwtText.textSize()`. -3. **The `id(font)` fast path misfires on PyQt6.** PyQt6 returns a *fresh* Python wrapper around the same underlying `QFont` on most calls, so `id(font)` changes between calls and the id-keyed cache misses ~92 % of the time (vs ~60 % on PyQt5). The slower `font.key()` path then takes over, compounding cause #2. - -**Changes.** - -- **`qwt/painter_command.py`** — added a `_flag_int(flag)` helper (PyQt5/PyQt6 portable) and module-level `_DIRTY_PEN`, `_DIRTY_BRUSH`, … int constants. The State branch in `__init__` casts `state.state()` to int *once* and bitwise-tests against the cached int constants instead of going through `enum.__and__` 12 times per command. -- **`qwt/graphic.py`** — same pattern in `qwtPaintCommand`'s State-replay branch (12 more flag tests per replayed command). -- **`qwt/text.py`** — same pattern for `Qt.AlignXxx` flags (`_ALIGN_LEFT`, `_ALIGN_RIGHT`, …) in the hot bitwise-test sites in `taggedRichText()`, `QwtTextLabel.sizeHint()/heightForWidth()/textRect()`. The `setRenderFlags()` setter still stores the value as `Qt.AlignmentFlag` so downstream Qt APIs that strictly require an enum on PyQt6 (`QTextOption.setAlignment`, `QPainter.drawText`, `QFontMetrics.boundingRect`) keep working — only the per-test bitwise sites cast back to int locally. -- **`qwt/text.py`** — **replaced the entire `id(font) → font.key()` cache** with a tuple-key cache. The new `font_key_cached(font)` returns an interned `(family, pixelSize-or-pointSizeF, weight, italic, stretch, styleStrategy)` tuple instead of `font.key()`. The two-level design keeps the original id-keyed fast path for repeated calls with the same QFont instance, and falls back to the tuple key (which never calls `QFont.key()`) for the PyQt6 case where wrappers churn. The same key is now also used by `fontmetrics()`/`fontmetrics_f()` — they previously called `font.toString()` per lookup, another ~3× more expensive on PyQt6. -- The Qt-5 fast-path gate (`_USE_FONT_KEY_FAST_PATH`) introduced in phase 4 is no longer needed and was removed: since the new cache never calls `font.key()`, the font-engine first-touch ordering issue that motivated the gate cannot occur. - -**Verification.** - -- **Test suite** — `pytest -q` with `PYTHONQWT_UNATTENDED_TESTS=1` on both bindings: PyQt5 26 passed / 1 skipped, PyQt6 26 passed / 1 skipped. Same as phase 4. -- **Performance** — PythonQwt micro `test_loadtest`, 10 runs each, run back-to-back on the same machine immediately after phase 5: - -| Config | PyQt5 ms (median / mean) | PyQt6 ms (median / mean) | Δ (PyQt6 − PyQt5) | PyQt6/PyQt5 | -|---|--:|--:|--:|--:| -| `master` (no optimisations) | 798 / 805 | 1 000 / 986 | +202 ms | **+25 %** | -| `fix/93` tip (end of phase 4) | 511 / 517 | 611 / 622 | +100 ms | **+20 %** | -| `fix/93` + phase 5 | 539 / 533 | 590 / 591 | **+51 ms** | **+9 %** | - -PyQt5 is essentially unchanged by phase 5 (the new int constants are inert on PyQt5 — Qt5 enums are already plain ints). PyQt6 dropped another ~20 ms median (mean −5 %): the Python `enum.Flag.__and__` budget is gone for the painter-command State branches (~3 600 enum ops/run eliminated), and the tuple-key font cache replaces the ~6 400 `QFont.key()` calls/run that previously cost ~45 ms. - -**Cumulative speed-ups on the micro load test, vs `master`:** - -| Binding | master → end of phase 4 | end of phase 4 → +phase 5 | **Total** | -|---|--:|--:|--:| -| PyQt5 | −36 % | +5 % (noise) | **−33 %** | -| PyQt6 | −39 % | −3 % | **−41 %** | - -**The PyQt6↔PyQt5 ratio more than halved** (+20 % → +9 %). The remaining +9 % is the structural sip-dispatch cost (PyQt6 marshalling for cheap getters like `drawLine`, `boundingRect`, attribute reads) that is *not* removable from PythonQwt — it can only be mitigated by calling Qt fewer times per render, which phases 1–5 already pursue aggressively. - -## Final results - -> Numbers below summarise the state at the end of phase 4 (the version covered by the Option A gate). Phase 5 was applied on top and further closes the residual Qt5↔Qt6 gap on the micro load test from +20 % to +9 % — see the dedicated phase-5 table above. PlotPy load test was not re-run after phase 5; phase 5 is targeted at the per-call enum/sip overhead that dominates the *micro* benchmark, so the PlotPy improvement is expected to be smaller in relative terms but in the same direction. - -### PythonQwt micro `test_loadtest` (5 runs each, ms) - -| Binding | master | fix/93 (Option A) | Speedup | -|---|---:|---:|---:| -| PyQt5 | ~1 900 | ~450–550 | ×3.5–×4.2 | -| PyQt6 | ~2 300 | ~450–675 | ×3.4–×5.1 | -| PySide6 | ~2 900 | ~580–795 | ×3.6–×5.0 | - -### PlotPy `test_loadtest`, 60 plots (3 runs each, ms) - -| Binding | master (`1ab70cd`) | fix/93 (Option A) | Speedup | -|---|---:|---:|---:| -| PyQt5 | 25 134 | **16 169** | ×1.55 | -| PyQt6 | 42 202 | **21 387** | ×1.97 | -| PySide6 | 53 160 | **24 849** | ×2.14 | - -### Cross-binding gap (PlotPy load test) - -| Comparison | master | fix/93 | -|---|---:|---:| -| PyQt6 vs PyQt5 | +68 % slower | **+32 % slower** | -| PySide6 vs PyQt5 | +111 % slower | **+54 % slower** | - -The original issue — a 1.5×–2× penalty for Qt6 over Qt5 — is largely resolved on the PlotPy load test, while the PyQt5 path remains bit-compatible with master both visually and behaviourally. - -## Backwards compatibility & public API surface - -The optimizations are deliberately confined to internal hot paths and do not alter the documented public API: - -- `QwtScaleMap.transform()`, `QwtScaleDiv.contains()`, `QwtScaleEngine.contains()/strip()`, `QwtAbstractScaleDraw.labelRect()/labelPosition()` — same signatures, same semantics, same return values. -- `QwtText` and `QwtPlainTextEngine` — same signatures and semantics. The internal `_PrivateData` containers no longer derive from `QObject`; this is invisible from the outside because `_PrivateData` was a private holder, never exposed and never used as a Qt signal/slot target. -- New module-level helper `qwt.text.font_key_cached()` is internal (lowercase, undocumented). It can be safely removed or refactored later without breaking any public consumer. -- No new dependency. No change to `qtpy` requirements; the Qt-version gate uses `qtpy.QT_VERSION` which is already imported transitively. - -The screenshot regression sweep above is the empirical confirmation of this: byte-identical PNGs on every non-flaky test mean PythonQwt's rendered output is unchanged, on every binding. - -## Reproduction quickstart - -The whole evaluation can be reproduced from a fresh checkout in a few commands. The scripts assume three sibling virtual environments under `.venvs/{pyqt5,pyqt6,pyside6}/`, each with a single Qt binding plus `numpy`, `qtpy`, `pytest`, `pillow`, and `PythonQwt` installed editable. - -```powershell -# 1. PythonQwt micro load test, all three bindings, 5 runs each -.\scripts\bench_qt.ps1 -Repeat 5 - -# 2. Visual regression sweep (PyQt5 example; repeat for pyqt6 / pyside6) -$env:QT_API = "pyqt5" -& .\.venvs\pyqt5\Scripts\python.exe scripts\capture_screenshots.py shots\fix\pyqt5 -& .\.venvs\pyqt5\Scripts\python.exe scripts\capture_screenshots.py shots\master\pyqt5 # after `git checkout master` -& .\.venvs\pyside6\Scripts\python.exe scripts\diff_screenshots.py shots\master\pyqt5 shots\fix\pyqt5 - -# 3. PlotPy load test (the test cited in the original GitHub issue) -$env:PYTHONPATH = "c:\Dev\PlotPy;c:\Dev\guidata" -foreach ($b in "pyqt5","pyqt6","pyside6") { - & ".\.venvs\$b\Scripts\python.exe" scripts\bench_plotpy_loadtest.py --repeat 3 --nplots 60 -} -``` - -## Test environment - -| Component | Value | -|---|---| -| OS | Windows 11 (x64) | -| Python | 3.11.9 (NuGet build) | -| PyQt5 | 5.15.11 (Qt 5.15.2) | -| PyQt6 | 6.11.0 (Qt 6.11.0) | -| PySide6 | 6.11.0 (Qt 6.11.0) | -| qtpy | latest available at the time of capture | -| PlotPy (for PlotPy load test) | 2.9.1 (editable install from `c:\Dev\PlotPy`) | -| guidata (for PlotPy load test) | 3.14.3 (editable install from `c:\Dev\guidata`) | -| Display | physical desktop session (not `offscreen`) — measurements include real Qt paint/composite cost | - -## Files touched - -| File | Phase 1 (cProfile) | Phase 2 (line-profiler) | Phase 4 (Option A) | Phase 5 (Qt5↔Qt6 gap) | -|---|:-:|:-:|:-:|:-:| -| `qwt/scale_map.py` | ✓ | | | | -| `qwt/scale_div.py` | ✓ | | | | -| `qwt/scale_engine.py` | ✓ | | | | -| `qwt/scale_draw.py` | ✓ | ✓ (drop QObject, `__slots__`) | | | -| `qwt/text.py` | ✓ | ✓ (drop QObject, font cache) | ✓ (Qt5 gate) | ✓ (alignment ints, tuple-key font cache, drop Qt5 gate) | -| `qwt/painter_command.py` | | | | ✓ (int-flag State branch, `_flag_int` helper) | -| `qwt/graphic.py` | | | | ✓ (int-flag State-replay branch) | - -Tooling added under `scripts/`: - -- `bench_qt.ps1` — driver for the PythonQwt micro load test across the three venvs. -- `profile_loadtest.py` — cProfile harness used in phase 1. -- `lineprofile_loadtest.py` — line_profiler harness used in phase 2. -- `capture_screenshots.py` / `diff_screenshots.py` — phase 3 visual regression tooling. -- `bench_plotpy_loadtest.py` — driver for the PlotPy load test (the test cited in the original issue). diff --git a/doc/overview.rst b/doc/overview.rst deleted file mode 100644 index 63c28d4..0000000 --- a/doc/overview.rst +++ /dev/null @@ -1,81 +0,0 @@ -Purpose and Motivation -====================== - -The ``PythonQwt`` project was initiated to solve -at least temporarily- -the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) -which is no longer maintained. The idea was to translate the original -Qwt C++ code to Python and then to optimize some parts of the code by -writing new modules based on NumPy and other libraries. - -Overview -======== - -The ``PythonQwt`` package consists of a single Python package named -`qwt` and of a few other files (examples, doc, ...): - - - The subpackage `qwt.tests` contains the PythonQwt unit tests: - - - 75% were directly adapted from Qwt/C++ demos (Bode demo, cartesian demo, etc.). - - - 25% were written specifically for PythonQwt. - - - The test launcher is an exclusive PythonQwt feature. - -The `qwt` package is a pure Python implementation of `Qwt` C++ library -with the following limitations. - -The following `Qwt` classes won't be reimplemented in `qwt` because more -powerful features already exist in `PlotPy`: -`QwtPlotZoomer`, -`QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`. - -Only the following plot items are currently implemented in `qwt` (the -only plot items needed by `PlotPy`): `QwtPlotItem` (base class), -`QwtPlotGrid`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`. - -The `HistogramItem` object implemented in PyQwt's HistogramDemo.py is not -available here (a similar item is already implemented in `PlotPy`). As a -consequence, the following classes are not implemented: `QwtPlotHistogram`, -`QwtIntervalSeriesData`, `QwtIntervalSample`. - -The following data structure objects are not implemented as they seemed -irrelevant with Python and NumPy: `QwtCPointerData` (as a consequence, method -`QwtPlot.setRawSamples` is not implemented), `QwtSyntheticPointData`. - -The following sample data type objects are not implemented as they seemed -quite specific: `QwtSetSample`, `QwtOHLCSample`. For similar reasons, the -`QwtPointPolar` class and the following sample iterator objects are not -implemented: `QwtSetSeriesData`, `QwtTradingChartData` and `QwtPoint3DSeriesData`. - -The following classes are not implemented because they seem inappropriate in -the Python/NumPy context: `QwtArraySeriesData`, `QwtPointSeriesData`, -`QwtAbstractSeriesStore`. - -Threads: - - - Multiple threads for graphic rendering is implemented in Qwt C++ code - thanks to the `QtConcurrent` and `QFuture` Qt features which are - currently not supported by PyQt. - - - As a consequence the following API is not supported in `PythonQwt`: - - `QwtPlotItem.renderThreadCount` - - `QwtPlotItem.setRenderThreadCount` - - option `numThreads` in `QwtPointMapper.toImage` - -The `QwtClipper` class is not implemented yet (and it will probably be -very difficult or even impossible to implement it in pure Python without -performance issues). As a consequence, when zooming in a plot curve, the -entire curve is still painted (in other words, when working with large -amount of data, there is no performance gain when zooming in). - -The curve fitter feature is not implemented because powerful curve fitting -features are already implemented in `PlotPy`. - -Other API compatibility issues with `Qwt`: - - - `QwtPlotCurve.MinimizeMemory` option was removed as this option has no - sense in PythonQwt (the polyline plotting is not taking more memory - than the array data that is already there). - - - `QwtPlotCurve.Fitted` option was removed as this option is not supported - at the moment. diff --git a/doc/plot_example.py b/doc/plot_example.py deleted file mode 100644 index 2385e10..0000000 --- a/doc/plot_example.py +++ /dev/null @@ -1,19 +0,0 @@ -import os.path as osp - -import numpy as np -from qtpy import QtWidgets as QW - -import qwt -from qwt import qthelpers as qth - -app = QW.QApplication([]) -x = np.linspace(-10, 10, 500) -plot = qwt.QwtPlot("Trigonometric functions") -plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend) -qwt.QwtPlotCurve.make(x, np.cos(x), "Cosine", plot, linecolor="red", antialiased=True) -qwt.QwtPlotCurve.make(x, np.sin(x), "Sine", plot, linecolor="blue", antialiased=True) -qth.take_screenshot( - plot, - osp.join(osp.abspath(osp.dirname(__file__)), "_static", "QwtPlot_example.png"), - size=(600, 300), -) diff --git a/doc/reference/graphic.rst b/doc/reference/graphic.rst deleted file mode 100644 index 700cbda..0000000 --- a/doc/reference/graphic.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.graphic diff --git a/doc/reference/index.rst b/doc/reference/index.rst deleted file mode 100644 index 95d8ff3..0000000 --- a/doc/reference/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -Reference -========= - -Public API: - -.. toctree:: - :maxdepth: 2 - - plot - scale - symbol - text - toqimage - -Private API: - -.. toctree:: - :maxdepth: 2 - - graphic - interval - plot_directpainter - plot_layout - plot_series - transform diff --git a/doc/reference/interval.rst b/doc/reference/interval.rst deleted file mode 100644 index 1121d29..0000000 --- a/doc/reference/interval.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.interval diff --git a/doc/reference/plot.rst b/doc/reference/plot.rst deleted file mode 100644 index 5513806..0000000 --- a/doc/reference/plot.rst +++ /dev/null @@ -1,24 +0,0 @@ -Plot widget fundamentals ------------------------- - -.. automodule:: qwt.plot - -.. automodule:: qwt.plot_canvas - -Plot items ----------- - -.. automodule:: qwt.plot_grid - -.. automodule:: qwt.plot_curve - -.. automodule:: qwt.plot_marker - -Additional plot features ------------------------- - -.. automodule:: qwt.legend - -.. automodule:: qwt.color_map - -.. automodule:: qwt.plot_renderer diff --git a/doc/reference/plot_directpainter.rst b/doc/reference/plot_directpainter.rst deleted file mode 100644 index f2af03d..0000000 --- a/doc/reference/plot_directpainter.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.plot_directpainter diff --git a/doc/reference/plot_layout.rst b/doc/reference/plot_layout.rst deleted file mode 100644 index 3600cf3..0000000 --- a/doc/reference/plot_layout.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.plot_layout diff --git a/doc/reference/plot_series.rst b/doc/reference/plot_series.rst deleted file mode 100644 index 487eb5c..0000000 --- a/doc/reference/plot_series.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.plot_series diff --git a/doc/reference/scale.rst b/doc/reference/scale.rst deleted file mode 100644 index 161e5ea..0000000 --- a/doc/reference/scale.rst +++ /dev/null @@ -1,12 +0,0 @@ -Scales ------- - -.. automodule:: qwt.scale_map - -.. automodule:: qwt.scale_widget - -.. automodule:: qwt.scale_div - -.. automodule:: qwt.scale_engine - -.. automodule:: qwt.scale_draw diff --git a/doc/reference/symbol.rst b/doc/reference/symbol.rst deleted file mode 100644 index 2fdd25a..0000000 --- a/doc/reference/symbol.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.symbol diff --git a/doc/reference/text.rst b/doc/reference/text.rst deleted file mode 100644 index 17b032e..0000000 --- a/doc/reference/text.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.text diff --git a/doc/reference/toqimage.rst b/doc/reference/toqimage.rst deleted file mode 100644 index b614ce9..0000000 --- a/doc/reference/toqimage.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.toqimage diff --git a/doc/reference/transform.rst b/doc/reference/transform.rst deleted file mode 100644 index 38411bc..0000000 --- a/doc/reference/transform.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: qwt.transform diff --git a/doc/symbol_path_example.py b/doc/symbol_path_example.py deleted file mode 100644 index 070e139..0000000 --- a/doc/symbol_path_example.py +++ /dev/null @@ -1,55 +0,0 @@ -import os.path as osp - -import numpy as np -from qtpy import QtCore as QC -from qtpy import QtGui as QG -from qtpy import QtWidgets as QW - -import qwt -from qwt import qthelpers as qth - -app = QW.QApplication([]) - -# --- Construct custom symbol --- - -path = QG.QPainterPath() -path.moveTo(0, 8) -path.lineTo(0, 5) -path.lineTo(-3, 5) -path.lineTo(0, 0) -path.lineTo(3, 5) -path.lineTo(0, 5) - -transform = QG.QTransform() -transform.rotate(-30.0) -path = transform.map(path) - -pen = QG.QPen(QC.Qt.black, 2) -pen.setJoinStyle(QC.Qt.MiterJoin) - -symbol = qwt.QwtSymbol() -symbol.setPen(pen) -symbol.setBrush(QC.Qt.red) -symbol.setPath(path) -symbol.setPinPoint(QC.QPointF(0.0, 0.0)) -symbol.setSize(10, 14) - -# --- Test it within a simple plot --- - -curve = qwt.QwtPlotCurve() -curve_pen = QG.QPen(QC.Qt.blue) -curve_pen.setStyle(QC.Qt.DotLine) -curve.setPen(curve_pen) -curve.setSymbol(symbol) -x = np.linspace(0, 10, 10) -curve.setData(x, np.sin(x)) - -plot = qwt.QwtPlot() -curve.attach(plot) -plot.replot() - -qth.take_screenshot( - plot, - osp.join(osp.abspath(osp.dirname(__file__)), "_static", "symbol_path_example.png"), - size=(600, 300), -) diff --git a/index.md b/index.md new file mode 100644 index 0000000..5e5a5ca --- /dev/null +++ b/index.md @@ -0,0 +1,120 @@ +# Qt plotting widgets for Python + +[![license](https://img.shields.io/pypi/l/PythonQwt.svg)](./LICENSE) +[![pypi version](https://img.shields.io/pypi/v/PythonQwt.svg)](https://pypi.org/project/PythonQwt/) +[![PyPI status](https://img.shields.io/pypi/status/PythonQwt.svg)](https://github.com/PierreRaybaut/PythonQwt) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/PythonQwt.svg)](https://pypi.python.org/pypi/PythonQwt/) +[![download count](https://img.shields.io/conda/dn/conda-forge/PythonQwt.svg)](https://www.anaconda.com/download/) +[![Documentation Status](https://readthedocs.org/projects/pythonqwt/badge/?version=latest)](https://pythonqwt.readthedocs.io/en/latest/?badge=latest) + + + +The `PythonQwt` project was initiated to solve -at least temporarily- the +obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is +no longer maintained. The idea was to translate the original Qwt C++ code to +Python and then to optimize some parts of the code by writing new modules +based on NumPy and other libraries. + +The `PythonQwt` package consists of a single Python package named `qwt` and +of a few other files (examples, doc, ...). + +See documentation [online](https://pythonqwt.readthedocs.io/en/latest/) or [PDF](https://pythonqwt.readthedocs.io/_/downloads/en/latest/pdf/) for more details on +the library and [changelog](CHANGELOG.md) for recent history of changes. + +## Sample + +```python +import qwt +import numpy as np + +app = qwt.qt.QtGui.QApplication([]) + +# Create plot widget +plot = qwt.QwtPlot("Trigonometric functions") +plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend) + +# Create two curves and attach them to plot +x = np.linspace(-10, 10, 500) +qwt.QwtPlotCurve.make(x, np.cos(x), "Cosinus", plot, linecolor="red", antialiased=True) +qwt.QwtPlotCurve.make(x, np.sin(x), "Sinus", plot, linecolor="blue", antialiased=True) + +# Resize and show plot +plot.resize(600, 300) +plot.show() + +app.exec_() +``` + + +## Examples (tests) + +The GUI-based test launcher may be executed from Python: + +```python +from qwt import tests +tests.run() +``` + +or from the command line: + +```bash +PythonQwt-tests +``` + +## Overview + +The `qwt` package is a pure Python implementation of `Qwt` C++ library with +the following limitations. + +The following `Qwt` classes won't be reimplemented in `qwt` because more +powerful features already exist in `guiqwt`: `QwtPlotZoomer`, +`QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`. + +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`. + +See "Overview" section in [documentation](https://pythonqwt.readthedocs.io/en/latest/) +for more details on API limitations when comparing to Qwt. + +## Dependencies + +### Requirements ### +- Python >=2.6 or Python >=3.2 +- PyQt4 >=4.4 or PyQt5 >= 5.5 +- NumPy >= 1.5 + +## Installation + +From the source package: + +```bash +python setup.py install +``` + +## Copyrights + +#### Main code base +- Copyright © 2002 Uwe Rathmann, for the original Qwt C++ code +- Copyright © 2015 Pierre Raybaut, for the Qwt C++ to Python translation and +optimization +- Copyright © 2015 Pierre Raybaut, for the PythonQwt specific and exclusive +Python material + +#### PyQt, PySide and Python2/Python3 compatibility modules +- Copyright © 2009-2013 Pierre Raybaut +- Copyright © 2013-2015 The Spyder Development Team + +#### Some examples +- Copyright © 2003-2009 Gerard Vermeulen, for the original PyQwt code +- Copyright © 2015 Pierre Raybaut, for the PyQt5/PySide port and further +developments (e.g. ported to PythonQwt API) + +## License + +The `qwt` Python package was partly (>95%) translated from Qwt C++ library: +the associated code is distributed under the terms of the LGPL license. The +rest of the code was either wrote from scratch or strongly inspired from MIT +licensed third-party software. + +See included [LICENSE](LICENSE) file for more details about licensing terms. diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index c972b99..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,83 +0,0 @@ -# PythonQwt setup configuration file - -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "PythonQwt" -authors = [{ name = "Pierre Raybaut", email = "pierre.raybaut@gmail.com" }] -description = "Qt plotting widgets for Python" -readme = "README.md" -license = { file = "LICENSE" } -classifiers = [ - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Human Machine Interfaces", - "Topic :: Scientific/Engineering :: Visualization", - "Topic :: Software Development :: Widget Sets", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Utilities", - "Topic :: Software Development :: User Interfaces", - "Operating System :: MacOS", - "Operating System :: Microsoft :: Windows", - "Operating System :: OS Independent", - "Operating System :: POSIX", - "Operating System :: Unix", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", -] -requires-python = ">=3.9, <4" -dependencies = ["NumPy>=1.21", "QtPy>=1.9"] -dynamic = ["version"] - -[project.urls] -Homepage = "https://github.com/PlotPyStack/PythonQwt/" -Documentation = "https://PythonQwt.readthedocs.io/en/latest/" - -[project.gui-scripts] -PythonQwt-tests = "qwt.tests:run" - -[project.optional-dependencies] -dev = ["build", "ruff", "pylint", "Coverage", "pre-commit"] -doc = ["PyQt5", "sphinx>6", "python-docs-theme"] -test = ["pytest", "pytest-xvfb"] - -[tool.setuptools.packages.find] -include = ["qwt*"] - -[tool.setuptools.package-data] -"*" = ["*.png", "*.svg", "*.mo", "*.cfg", "*.toml"] - -[tool.setuptools.dynamic] -version = { attr = "qwt.__version__" } - -[tool.pytest.ini_options] -addopts = "qwt" - -[tool.ruff] -exclude = [".git", ".vscode", "build", "dist"] -line-length = 88 # Same as Black. -indent-width = 4 # Same as Black. -target-version = "py39" # Assume Python 3.9. - -[tool.ruff.lint] -# all rules can be found here: https://beta.ruff.rs/docs/rules/ -select = ["E", "F", "W", "I", "NPY201"] -ignore = [ - "E203", # space before : (needed for how black formats slicing) - "E501", # line too long -] - -[tool.ruff.format] -quote-style = "double" # Like Black, use double quotes for strings. -indent-style = "space" # Like Black, indent with spaces, rather than tabs. -skip-magic-trailing-comma = false # Like Black, respect magic trailing commas. -line-ending = "auto" # Like Black, automatically detect the appropriate line ending. - -[tool.ruff.lint.per-file-ignores] -"doc/*" = ["E402"] -"qwt/tests/*" = ["E402"] diff --git a/qwt/__init__.py b/qwt/__init__.py deleted file mode 100644 index 8f0c9ed..0000000 --- a/qwt/__init__.py +++ /dev/null @@ -1,170 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -PythonQwt -========= - -The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical -user interfaces for the Python programming language. - -It consists of a single Python package named `qwt` which is a pure Python -implementation of Qwt C++ library with some limitations. - -.. image:: /../qwt/tests/data/testlauncher.png - -External resources: - * Python Package Index: `PyPI`_ - * Project page on GitHub: `GitHubPage`_ - * Bug reports and feature requests: `GitHub`_ - -.. _PyPI: https://pypi.org/project/PythonQwt/ -.. _GitHubPage: https://github.com/PlotPyStack/PythonQwt -.. _GitHub: https://github.com/PlotPyStack/PythonQwt -""" - -import warnings - -from qwt.color_map import QwtLinearColorMap # noqa: F401 -from qwt.interval import QwtInterval -from qwt.legend import QwtLegend, QwtLegendData, QwtLegendLabel # noqa: F401 -from qwt.painter import QwtPainter # noqa: F401 -from qwt.plot import QwtPlot # noqa: F401 -from qwt.plot_canvas import QwtPlotCanvas # noqa: F401 -from qwt.plot_curve import QwtPlotCurve as QPC # see deprecated section -from qwt.plot_curve import QwtPlotItem # noqa: F401 -from qwt.plot_directpainter import QwtPlotDirectPainter # noqa: F401 -from qwt.plot_grid import QwtPlotGrid as QPG # see deprecated section -from qwt.plot_marker import QwtPlotMarker # noqa: F401 -from qwt.plot_renderer import QwtPlotRenderer # noqa: F401 -from qwt.plot_series import ( # noqa: F401 - QwtPlotSeriesItem, - QwtPointArrayData, - QwtSeriesData, - QwtSeriesStore, -) -from qwt.scale_div import QwtScaleDiv # noqa: F401 -from qwt.scale_draw import ( # noqa: F401 - QwtAbstractScaleDraw, - QwtDateTimeScaleDraw, - QwtScaleDraw, -) -from qwt.scale_engine import ( # noqa: F401 - QwtDateTimeScaleEngine, - QwtLinearScaleEngine, - QwtLogScaleEngine, -) -from qwt.scale_map import QwtScaleMap # noqa: F401 -from qwt.symbol import QwtSymbol as QSbl # see deprecated section -from qwt.text import QwtText # noqa: F401 -from qwt.toqimage import array_to_qimage as toQImage # noqa: F401 - -__version__ = "0.16.0" -QWT_VERSION_STR = "6.1.5" - - -## ============================================================================ -## Deprecated classes and attributes (to be removed in next major release) -## ============================================================================ -# Remove deprecated QwtPlotItem.setAxis (replaced by setAxes) -# Remove deprecated QwtPlotCanvas.invalidatePaintCache (replaced by replot) -## ============================================================================ -class QwtDoubleInterval(QwtInterval): - def __init__(self, minValue=0.0, maxValue=-1.0, borderFlags=None): - warnings.warn( - "`QwtDoubleInterval` has been removed in Qwt6: " - "please use `QwtInterval` instead", - RuntimeWarning, - ) - super(QwtDoubleInterval, self).__init__(minValue, maxValue, borderFlags) - - -## ============================================================================ -class QwtLog10ScaleEngine(QwtLogScaleEngine): - def __init__(self): - warnings.warn( - "`QwtLog10ScaleEngine` has been removed in Qwt6: " - "please use `QwtLogScaleEngine` instead", - RuntimeWarning, - ) - super(QwtLog10ScaleEngine, self).__init__(10) - - -## ============================================================================ -class QwtPlotPrintFilter(object): - def __init__(self): - raise NotImplementedError( - "`QwtPlotPrintFilter` has been removed in Qwt6: " - "please rely on `QwtPlotRenderer` instead" - ) - - -## ============================================================================ -class QwtPlotCurve(QPC): - @property - def Yfx(self): - raise NotImplementedError( - "`Yfx` attribute has been removed " - "(curve types are no longer implemented in Qwt6)" - ) - - @property - def Xfy(self): - raise NotImplementedError( - "`Yfx` attribute has been removed " - "(curve types are no longer implemented in Qwt6)" - ) - - -## ============================================================================ -class QwtSymbol(QSbl): - def draw(self, painter, *args): - warnings.warn( - "`draw` has been removed in Qwt6: " - "please rely on `drawSymbol` and `drawSymbols` instead", - RuntimeWarning, - ) - from qtpy.QtCore import QPointF - - if len(args) == 2: - self.drawSymbols(painter, [QPointF(*args)]) - else: - self.drawSymbol(painter, *args) - - -## ============================================================================ -class QwtPlotGrid(QPG): - def majPen(self): - warnings.warn( - "`majPen` has been removed in Qwt6: please use `majorPen` instead", - RuntimeWarning, - ) - return self.majorPen() - - def minPen(self): - warnings.warn( - "`minPen` has been removed in Qwt6: please use `minorPen` instead", - RuntimeWarning, - ) - return self.minorPen() - - def setMajPen(self, *args): - warnings.warn( - "`setMajPen` has been removed in Qwt6: please use `setMajorPen` instead", - RuntimeWarning, - ) - return self.setMajorPen(*args) - - def setMinPen(self, *args): - warnings.warn( - "`setMinPen` has been removed in Qwt6: please use `setMinorPen` instead", - RuntimeWarning, - ) - return self.setMinorPen(*args) - - -## ============================================================================ diff --git a/qwt/_math.py b/qwt/_math.py deleted file mode 100644 index 5ee6911..0000000 --- a/qwt/_math.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -import math - -from qtpy.QtCore import qFuzzyCompare - - -def qwtFuzzyCompare(value1, value2, intervalSize): - eps = abs(1.0e-6 * intervalSize) - if value2 - value1 > eps: - return -1 - elif value1 - value2 > eps: - return 1 - else: - return 0 - - -def qwtFuzzyGreaterOrEqual(d1, d2): - return (d1 >= d2) or qFuzzyCompare(d1, d2) - - -def qwtFuzzyLessOrEqual(d1, d2): - return (d1 <= d2) or qFuzzyCompare(d1, d2) - - -def qwtSign(x): - if x > 0.0: - return 1 - elif x < 0.0: - return -1 - else: - return 0 - - -def qwtSqr(x): - return x**2 - - -def qwtFastAtan(x): - if x < -1.0: - return -0.5 * math.pi - x / (x**2 + 0.28) - elif x > 1.0: - return 0.5 * math.pi - x / (x**2 + 0.28) - else: - return x / (1.0 + x**2 * 0.28) - - -def qwtFastAtan2(y, x): - if x > 0: - return qwtFastAtan(y / x) - elif x < 0: - d = qwtFastAtan(y / x) - if y >= 0: - return d + math.pi - else: - return d - math.pi - elif y < 0.0: - return -0.5 * math.pi - elif y > 0.0: - return 0.5 * math.pi - else: - return 0.0 - - -def qwtRadians(degrees): - return degrees * math.pi / 180.0 - - -def qwtDegrees(radians): - return radians * 180.0 / math.pi diff --git a/qwt/color_map.py b/qwt/color_map.py deleted file mode 100644 index 9a54cf0..0000000 --- a/qwt/color_map.py +++ /dev/null @@ -1,386 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -Color maps ----------- - -QwtColorMap -~~~~~~~~~~~ - -.. autoclass:: QwtColorMap - :members: - -QwtLinearColorMap -~~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtLinearColorMap - :members: - -QwtAlphaColorMap -~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtAlphaColorMap - :members: -""" - -from qtpy.QtCore import QObject, Qt, qIsNaN -from qtpy.QtGui import QColor, qAlpha, qBlue, qGreen, qRed, qRgb, qRgba - - -class ColorStop(object): - def __init__(self, pos=0.0, color=None): - self.pos = pos - if color is None: - self.rgb = 0 - else: - self.rgb = color.rgba() - self.r = qRed(self.rgb) - self.g = qGreen(self.rgb) - self.b = qBlue(self.rgb) - self.a = qAlpha(self.rgb) - - # when mapping a value to rgb we will have to calcualate: - # - const int v = int( ( s1.v0 + ratio * s1.vStep ) + 0.5 ); - # Thus adding 0.5 ( for rounding ) can be done in advance - self.r0 = self.r + 0.5 - self.g0 = self.g + 0.5 - self.b0 = self.b + 0.5 - self.a0 = self.a + 0.5 - - self.rStep = self.gStep = self.bStep = self.aStep = 0.0 - self.posStep = 0.0 - - def updateSteps(self, nextStop): - self.rStep = nextStop.r - self.r - self.gStep = nextStop.g - self.g - self.bStep = nextStop.b - self.b - self.aStep = nextStop.a - self.a - self.posStep = nextStop.pos - self.pos - - -class ColorStops(object): - def __init__(self): - self.__doAlpha = False - self.__stops = [] - - def insert(self, pos, color): - if pos < 0.0 or pos > 1.0: - return - if len(self.__stops) == 0: - index = 0 - self.__stops = [None] - else: - index = self.findUpper(pos) - if ( - index == len(self.__stops) - or abs(self.__stops[index].pos - pos) >= 0.001 - ): - self.__stops.append(None) - for i in range(len(self.__stops) - 1, index, -1): - self.__stops[i] = self.__stops[i - 1] - self.__stops[index] = ColorStop(pos, color) - self.__doAlpha = color.alpha() != 255 - if index > 0: - self.__stops[index - 1].updateSteps(self.__stops[index]) - if index < len(self.__stops) - 1: - self.__stops[index].updateSteps(self.__stops[index + 1]) - - def stops(self): - return self.__stops - - def findUpper(self, pos): - index = 0 - n = len(self.__stops) - - while n > 0: - half = n >> 1 - middle = index + half - if self.__stops[middle].pos <= pos: - index = middle + 1 - n -= half + 1 - else: - n = half - return index - - def rgb(self, mode, pos): - if pos <= 0.0: - return self.__stops[0].rgb - if pos >= 1.0: - return self.__stops[-1].rgb - - index = self.findUpper(pos) - if mode == QwtLinearColorMap.FixedColors: - return self.__stops[index - 1].rgb - else: - s1 = self.__stops[index - 1] - ratio = (pos - s1.pos) / s1.posStep - r = int(s1.r0 + ratio * s1.rStep) - g = int(s1.g0 + ratio * s1.gStep) - b = int(s1.b0 + ratio * s1.bStep) - if self.__doAlpha: - if s1.aStep: - a = int(s1.a0 + ratio * s1.aStep) - return qRgba(r, g, b, a) - else: - return qRgba(r, g, b, s1.a) - else: - return qRgb(r, g, b) - - -class QwtColorMap(object): - """ - QwtColorMap is used to map values into colors. - - For displaying 3D data on a 2D plane the 3rd dimension is often - displayed using colors, like f.e in a spectrogram. - - Each color map is optimized to return colors for only one of the - following image formats: - - * `QImage.Format_Indexed8` - * `QImage.Format_ARGB32` - - .. py:class:: QwtColorMap(format_) - - :param int format_: Preferred format of the color map (:py:data:`QwtColorMap.RGB` or :py:data:`QwtColorMap.Indexed`) - - .. seealso :: - - :py:data:`qwt.QwtScaleWidget` - """ - - # enum Format - RGB, Indexed = list(range(2)) - - def __init__(self, format_=None): - if format_ is None: - format_ = self.RGB - self.__format = format_ - - def color(self, interval, value): - """ - Map a value into a color - - :param qwt.interval.QwtInterval interval: valid interval for value - :param float value: value - :return: the color corresponding to value - - .. warning :: - - This method is slow for Indexed color maps. If it is necessary to - map many values, its better to get the color table once and find - the color using `colorIndex()`. - """ - if self.__format == self.RGB: - return QColor.fromRgba(self.rgb(interval, value)) - else: - index = self.colorIndex(interval, value) - return self.colorTable(interval)[index] - - def format(self): - return self.__format - - def colorTable(self, interval): - """ - Build and return a color map of 256 colors - - :param qwt.interval.QwtInterval interval: range for the values - :return: a color table, that can be used for a `QImage` - - The color table is needed for rendering indexed images in combination - with using `colorIndex()`. - """ - table = [0] * 256 - if interval.isValid(): - step = interval.width() / (len(table) - 1) - for i in range(len(table)): - table[i] = self.rgb(interval, interval.minValue() + step * i) - return table - - def rgb(self, interval, value): - # To be reimplemented - return QColor().rgb() - - def colorIndex(self, interval, value): - # To be reimplemented - return 0 - - -class QwtLinearColorMap_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.colorStops = ColorStops() - self.mode = None - - -class QwtLinearColorMap(QwtColorMap): - """ - Build a linear color map with two stops. - - .. py:class:: QwtLinearColorMap(format_) - - Build a color map with two stops at 0.0 and 1.0. - The color at 0.0 is `Qt.blue`, at 1.0 it is `Qt.yellow`. - - :param int format_: Preferred format of the color map (:py:data:`QwtColorMap.RGB` or :py:data:`QwtColorMap.Indexed`) - - .. py:class:: QwtLinearColorMap(color1, color2, [format_=QwtColorMap.RGB]): - :noindex: - - Build a color map with two stops at 0.0 and 1.0. - - :param QColor color1: color at 0. - :param QColor color2: color at 1. - :param int format_: Preferred format of the color map (:py:data:`QwtColorMap.RGB` or :py:data:`QwtColorMap.Indexed`) - """ - - # enum Mode - FixedColors, ScaledColors = list(range(2)) - - def __init__(self, *args): - color1, color2 = QColor(Qt.blue), QColor(Qt.yellow) - format_ = QwtColorMap.RGB - if len(args) == 1: - (format_,) = args - elif len(args) == 2: - color1, color2 = args - elif len(args) == 3: - color1, color2, format_ = args - elif len(args) != 0: - raise TypeError( - "%s() takes 0, 1, 2 or 3 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - super(QwtLinearColorMap, self).__init__(format_) - self.__data = QwtLinearColorMap_PrivateData() - self.__data.mode = self.ScaledColors - self.setColorInterval(color1, color2) - - def setMode(self, mode): - """ - Set the mode of the color map - - :param int mode: :py:data:`QwtLinearColorMap.FixedColors` or :py:data:`QwtLinearColorMap.ScaledColors` - - `FixedColors` means the color is calculated from the next lower color - stop. `ScaledColors` means the color is calculated by interpolating - the colors of the adjacent stops. - """ - self.__data.mode = mode - - def mode(self): - """ - :return: the mode of the color map - - .. seealso :: - - :py:meth:`QwtLinearColorMap.setMode` - """ - return self.__data.mode - - def setColorInterval(self, color1, color2): - self.__data.colorStops = ColorStops() - self.__data.colorStops.insert(0.0, QColor(color1)) - self.__data.colorStops.insert(1.0, QColor(color2)) - - def addColorStop(self, value, color): - if value >= 0.0 and value <= 1.0: - self.__data.colorStops.insert(value, QColor(color)) - - def colorStops(self): - return self.__data.colorStops.stops() - - def color1(self): - return QColor(self.__data.colorStops.rgb(self.__data.mode, 0.0)) - - def color2(self): - return QColor(self.__data.colorStops.rgb(self.__data.mode, 1.0)) - - def rgb(self, interval, value): - if qIsNaN(value): - return 0 - width = interval.width() - if width <= 0.0: - return 0 - ratio = (value - interval.minValue()) / width - return self.__data.colorStops.rgb(self.__data.mode, ratio) - - def colorIndex(self, interval, value): - width = interval.width() - if qIsNaN(value) or width <= 0.0 or value <= interval.minValue(): - return 0 - if value >= interval.maxValue(): - return 255 - ratio = (value - interval.minValue()) / width - if self.__data.mode == self.FixedColors: - return int(ratio * 255) - else: - return int(ratio * 255 + 0.5) - - -class QwtAlphaColorMap_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.color = QColor() - self.rgb = QColor().rgb() - self.rgbMax = QColor().rgb() - - -class QwtAlphaColorMap(QwtColorMap): - """ - QwtAlphaColorMap varies the alpha value of a color - - .. py:class:: QwtAlphaColorMap(color) - - Build a color map varying the alpha value of a color. - - :param QColor color: color of the map - """ - - def __init__(self, color): - super(QwtAlphaColorMap, self).__init__(QwtColorMap.RGB) - self.__data = QwtAlphaColorMap_PrivateData() - self.setColor(color) - - def setColor(self, color): - """ - Set the color of the map - - :param QColor color: color of the map - """ - self.__data.color = color - self.__data.rgb = color.rgb() & qRgba(255, 255, 255, 0) - self.__data.rgbMax = self.__data.rgb | (255 << 24) - - def color(self): - """ - :return: the color of the map - - .. seealso :: - - :py:meth:`QwtAlphaColorMap.setColor` - """ - return self.__data.color - - def rgb(self, interval, value): - if qIsNaN(value): - return 0 - width = interval.width() - if width <= 0.0: - return 0 - if value <= interval.minValue(): - return self.__data.rgb - if value >= interval.maxValue(): - return self.__data.rgbMax - ratio = (value - interval.minValue()) / width - return self.__data.rgb | (int(round(255 * ratio)) << 24) - - def colorIndex(self, interval, value): - return 0 diff --git a/qwt/column_symbol.py b/qwt/column_symbol.py deleted file mode 100644 index c4e46b0..0000000 --- a/qwt/column_symbol.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -from qtpy.QtCore import QLineF, QObject, QRectF, Qt -from qtpy.QtGui import QPalette, QPolygonF - -from qwt.interval import QwtInterval - - -def qwtDrawBox(p, rect, pal, lw): - if lw > 0.0: - if rect.width() == 0.0: - p.setPen(pal.dark().color()) - p.drawLine(QLineF(rect.topLeft(), rect.bottomLeft())) - return - if rect.height() == 0.0: - p.setPen(pal.dark().color()) - p.drawLine(QLineF(rect.topLeft(), rect.topRight())) - return - lw = min([lw, rect.height() / 2.0 - 1.0]) - lw = min([lw, rect.width() / 2.0 - 1.0]) - outerRect = rect.adjusted(0, 0, 1, 1) - polygon = QPolygonF(outerRect) - if outerRect.width() > 2 * lw and outerRect.height() > 2 * lw: - innerRect = outerRect.adjusted(lw, lw, -lw, -lw) - polygon = polygon.subtracted(innerRect) - p.setPen(Qt.NoPen) - p.setBrush(pal.dark()) - p.drawPolygon(polygon) - windowRect = rect.adjusted(lw, lw, -lw + 1, -lw + 1) - if windowRect.isValid(): - p.fillRect(windowRect, pal.window()) - - -def qwtDrawPanel(painter, rect, pal, lw): - if lw > 0.0: - if rect.width() == 0.0: - painter.setPen(pal.window().color()) - painter.drawLine(QLineF(rect.topLeft(), rect.bottomLeft())) - return - if rect.height() == 0.0: - painter.setPen(pal.window().color()) - painter.drawLine(QLineF(rect.topLeft(), rect.topRight())) - return - lw = min([lw, rect.height() / 2.0 - 1.0]) - lw = min([lw, rect.width() / 2.0 - 1.0]) - outerRect = rect.adjusted(0, 0, 1, 1) - innerRect = outerRect.adjusted(lw, lw, -lw, -lw) - lines = [QPolygonF(), QPolygonF()] - lines[0] += outerRect.bottomLeft() - lines[0] += outerRect.topLeft() - lines[0] += outerRect.topRight() - lines[0] += innerRect.topRight() - lines[0] += innerRect.topLeft() - lines[0] += innerRect.bottomLeft() - lines[1] += outerRect.topRight() - lines[1] += outerRect.bottomRight() - lines[1] += outerRect.bottomLeft() - lines[1] += innerRect.bottomLeft() - lines[1] += innerRect.bottomRight() - lines[1] += innerRect.topRight() - painter.setPen(Qt.NoPen) - painter.setBrush(pal.light()) - painter.drawPolygon(lines[0]) - painter.setBrush(pal.dark()) - painter.drawPolygon(lines[1]) - painter.fillRect(rect.adjusted(lw, lw, -lw + 1, -lw + 1), pal.window()) - - -class QwtColumnSymbol_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.style = QwtColumnSymbol.Box - self.frameStyle = QwtColumnSymbol.Raised - self.lineWidth = 2 - self.palette = QPalette(Qt.gray) - - -class QwtColumnSymbol(object): - # enum Style - NoStyle = -1 - Box = 0 - UserStyle = 1000 - - # enum FrameStyle - NoFrame, Plain, Raised = list(range(3)) - - def __init__(self, style): - self.__data = QwtColumnSymbol_PrivateData() - self.__data.style = style - - def setStyle(self, style): - self.__data.style = style - - def style(self): - return self.__data.style - - def setPalette(self, palette): - self.__data.palette = palette - - def palette(self): - return self.__data.palette - - def setFrameStyle(self, frameStyle): - self.__data.frameStyle = frameStyle - - def frameStyle(self): - return self.__data.frameStyle - - def setLineWidth(self, width): - self.__data.lineWidth = width - - def lineWidth(self): - return self.__data.lineWidth - - def draw(self, painter, rect): - painter.save() - if self.__data.style == QwtColumnSymbol.Box: - self.drawBox(painter, rect) - painter.restore() - - def drawBox(self, painter, rect): - r = rect.toRect() - if self.__data.frameStyle == QwtColumnSymbol.Raised: - qwtDrawPanel(painter, r, self.__data.palette, self.__data.lineWidth) - elif self.__data.frameStyle == QwtColumnSymbol.Plain: - qwtDrawBox(painter, r, self.__data.palette, self.__data.lineWidth) - else: - painter.fillRect(r.adjusted(0, 0, 1, 1), self.__data.palette.window()) - - -class QwtColumnRect(object): - # enum Direction - LeftToRight, RightToLeft, BottomToTop, TopToBottom = list(range(4)) - - def __init__(self): - self.hInterval = QwtInterval() - self.vInterval = QwtInterval() - self.direction = 0 - - def toRect(self): - r = QRectF( - self.hInterval.minValue(), - self.vInterval.minValue(), - self.hInterval.maxValue() - self.hInterval.minValue(), - self.vInterval.maxValue() - self.vInterval.minValue(), - ) - r = r.normalized() - if self.hInterval.borderFlags() & QwtInterval.ExcludeMinimum: - r.adjust(1, 0, 0, 0) - if self.hInterval.borderFlags() & QwtInterval.ExcludeMaximum: - r.adjust(0, 0, -1, 0) - if self.vInterval.borderFlags() & QwtInterval.ExcludeMinimum: - r.adjust(0, 1, 0, 0) - if self.vInterval.borderFlags() & QwtInterval.ExcludeMaximum: - r.adjust(0, 0, 0, -1) - return r - - def orientation(self): - if self.direction in (self.LeftToRight, self.RightToLeft): - return Qt.Horizontal - return Qt.Vertical diff --git a/qwt/dyngrid_layout.py b/qwt/dyngrid_layout.py deleted file mode 100644 index 45850da..0000000 --- a/qwt/dyngrid_layout.py +++ /dev/null @@ -1,359 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -qwt.dyngrid_layout ------------------- - -The `dyngrid_layout` module provides the `QwtDynGridLayout` class. - -.. autoclass:: QwtDynGridLayout - :members: -""" - -from qtpy.QtCore import QObject, QRect, QSize, Qt -from qtpy.QtWidgets import QLayout - - -class QwtDynGridLayout_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.isDirty = True - self.maxColumns = 0 - self.numRows = 0 - self.numColumns = 0 - self.expanding = Qt.Horizontal - self.itemSizeHints = [] - self.itemList = [] - - def updateLayoutCache(self): - self.itemSizeHints = [it.sizeHint() for it in self.itemList] - self.isDirty = False - - -class QwtDynGridLayout(QLayout): - """ - The `QwtDynGridLayout` class lays out widgets in a grid, - adjusting the number of columns and rows to the current size. - - `QwtDynGridLayout` takes the space it gets, divides it up into rows and - columns, and puts each of the widgets it manages into the correct cell(s). - It lays out as many number of columns as possible (limited by - :py:meth:`maxColumns()`). - - .. py:class:: QwtDynGridLayout(parent, margin, [spacing=-1]) - - :param QWidget parent: parent widget - :param int margin: margin - :param int spacing: spacing - - .. py:class:: QwtDynGridLayout(spacing) - :noindex: - - :param int spacing: spacing - - .. py:class:: QwtDynGridLayout() - :noindex: - - Initialize the layout with default values. - - :param int spacing: spacing - """ - - def __init__(self, *args): - self.__data = None - parent = None - margin = 0 - spacing = -1 - if len(args) in (2, 3): - parent, margin = args[:2] - if len(args) == 3: - spacing = args[-1] - elif len(args) == 1: - if isinstance(args[0], int): - (spacing,) = args - else: - (parent,) = args - elif len(args) != 0: - raise TypeError( - "%s() takes 0, 1, 2 or 3 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - QLayout.__init__(self, parent) - self.__data = QwtDynGridLayout_PrivateData() - self.setSpacing(spacing) - self.setContentsMargins(margin, margin, margin, margin) - - def invalidate(self): - """Invalidate all internal caches""" - self.__data.isDirty = True - QLayout.invalidate(self) - - def setMaxColumns(self, maxColumns): - """Limit the number of columns""" - self.__data.maxColumns = maxColumns - - def maxColumns(self): - """Return the upper limit for the number of columns""" - return self.__data.maxColumns - - def addItem(self, item): - """Add an item to the next free position""" - self.__data.itemList.append(item) - self.invalidate() - - def isEmpty(self): - """Return true if this layout is empty""" - return self.count() == 0 - - def itemCount(self): - """Return number of layout items""" - return self.count() - - def itemAt(self, index): - """Find the item at a specific index""" - if index < 0 or index >= len(self.__data.itemList): - return - return self.__data.itemList[index] - - def takeAt(self, index): - """Find the item at a specific index and remove it from the layout""" - if index < 0 or index >= len(self.__data.itemList): - return - self.__data.isDirty = True - return self.__data.itemList.pop(index) - - def count(self): - """Return Number of items in the layout""" - return len(self.__data.itemList) - - def setExpandingDirections(self, expanding): - """ - Set whether this layout can make use of more space than sizeHint(). - A value of Qt.Vertical or Qt.Horizontal means that it wants to grow in - only one dimension, while Qt.Vertical | Qt.Horizontal means that it - wants to grow in both dimensions. The default value is 0. - """ - self.__data.expanding = expanding - - def expandingDirections(self): - """ - Returns whether this layout can make use of more space than sizeHint(). - A value of Qt.Vertical or Qt.Horizontal means that it wants to grow in - only one dimension, while Qt.Vertical | Qt.Horizontal means that it - wants to grow in both dimensions. - """ - return self.__data.expanding - - def setGeometry(self, rect): - """ - Reorganizes columns and rows and resizes managed items within a - rectangle. - """ - QLayout.setGeometry(self, rect) - if self.isEmpty(): - return - self.__data.numColumns = self.columnsForWidth(rect.width()) - self.__data.numRows = self.itemCount() / self.__data.numColumns - if self.itemCount() % self.__data.numColumns: - self.__data.numRows += 1 - itemGeometries = self.layoutItems(rect, self.__data.numColumns) - for it, geo in zip(self.__data.itemList, itemGeometries): - it.setGeometry(geo) - - def columnsForWidth(self, width): - """ - Calculate the number of columns for a given width. - - The calculation tries to use as many columns as possible - ( limited by maxColumns() ) - """ - if self.isEmpty(): - return 0 - maxColumns = self.itemCount() - if self.__data.maxColumns > 0: - maxColumns = min([self.__data.maxColumns, maxColumns]) - if self.maxRowWidth(maxColumns) <= width: - return maxColumns - for numColumns in range(2, maxColumns + 1): - rowWidth = self.maxRowWidth(numColumns) - if rowWidth > width: - return numColumns - 1 - return 1 - - def maxRowWidth(self, numColumns): - """Calculate the width of a layout for a given number of columns.""" - colWidth = [0] * numColumns - if self.__data.isDirty: - self.__data.updateLayoutCache() - for index, hint in enumerate(self.__data.itemSizeHints): - col = index % numColumns - colWidth[col] = max([colWidth[col], hint.width()]) - margins = self.contentsMargins() - margin_w = margins.left() + margins.right() - return margin_w + (numColumns - 1) * self.spacing() + sum(colWidth) - - def maxItemWidth(self): - """Return the maximum width of all layout items""" - if self.isEmpty(): - return 0 - if self.__data.isDirty: - self.__data.updateLayoutCache() - return max([hint.width() for hint in self.__data.itemSizeHints]) - - def layoutItems(self, rect, numColumns): - """ - Calculate the geometries of the layout items for a layout - with numColumns columns and a given rectangle. - """ - itemGeometries = [] - if numColumns == 0 or self.isEmpty(): - return itemGeometries - numRows = int(self.itemCount() / numColumns) - if numColumns % self.itemCount(): - numRows += 1 - if numRows == 0: - return itemGeometries - rowHeight = [0] * numRows - colWidth = [0] * numColumns - self.layoutGrid(numColumns, rowHeight, colWidth) - expandH = self.expandingDirections() == Qt.Horizontal - expandV = self.expandingDirections() == Qt.Vertical - if expandH or expandV: - self.stretchGrid(rect, numColumns, rowHeight, colWidth) - maxColumns = self.__data.maxColumns - self.__data.maxColumns = numColumns - alignedRect = self.alignmentRect(rect) - self.__data.maxColumns = maxColumns - xOffset = 0 if expandH else alignedRect.x() - yOffset = 0 if expandV else alignedRect.y() - colX = [0] * numColumns - rowY = [0] * numRows - xySpace = self.spacing() - margins = self.contentsMargins() - rowY[0] = yOffset + margins.bottom() - for r in range(1, numRows): - rowY[r] = rowY[r - 1] + rowHeight[r - 1] + xySpace - colX[0] = xOffset + margins.left() - for c in range(1, numColumns): - colX[c] = colX[c - 1] + colWidth[c - 1] + xySpace - itemCount = len(self.__data.itemList) - for i in range(itemCount): - row = int(i / numColumns) - col = i % numColumns - itemGeometry = QRect(colX[col], rowY[row], colWidth[col], rowHeight[row]) - itemGeometries.append(itemGeometry) - return itemGeometries - - def layoutGrid(self, numColumns, rowHeight, colWidth): - """ - Calculate the dimensions for the columns and rows for a grid - of numColumns columns. - """ - if numColumns <= 0: - return - if self.__data.isDirty: - self.__data.updateLayoutCache() - for index in range(len(self.__data.itemSizeHints)): - row = int(index / numColumns) - col = index % numColumns - size = self.__data.itemSizeHints[index] - if col == 0: - rowHeight[row] = size.height() - else: - rowHeight[row] = max([rowHeight[row], size.height()]) - if row == 0: - colWidth[col] = size.width() - else: - colWidth[col] = max([colWidth[col], size.width()]) - - def hasHeightForWidth(self): - """Return true: QwtDynGridLayout implements heightForWidth().""" - return True - - def heightForWidth(self, width): - """Return The preferred height for this layout, given a width.""" - if self.isEmpty(): - return 0 - numColumns = self.columnsForWidth(width) - numRows = int(self.itemCount() / numColumns) - if self.itemCount() % numColumns: - numRows += 1 - rowHeight = [0] * numRows - colWidth = [0] * numColumns - self.layoutGrid(numColumns, rowHeight, colWidth) - margins = self.contentsMargins() - margin_h = margins.top() + margins.bottom() - return margin_h + (numRows - 1) * self.spacing() + sum(rowHeight) - - def stretchGrid(self, rect, numColumns, rowHeight, colWidth): - """ - Stretch columns in case of expanding() & QSizePolicy::Horizontal and - rows in case of expanding() & QSizePolicy::Vertical to fill the entire - rect. Rows and columns are stretched with the same factor. - """ - if numColumns == 0 or self.isEmpty(): - return - expandH = self.expandingDirections() & Qt.Horizontal - expandV = self.expandingDirections() & Qt.Vertical - margins = self.contentsMargins() - wmargins = margins.left() + margins.right() - hmargins = margins.top() + margins.bottom() - if expandH: - xDelta = rect.width() - wmargins - (numColumns - 1) * self.spacing() - for col in range(numColumns): - xDelta -= colWidth[col] - if xDelta > 0: - for col in range(numColumns): - space = xDelta // (numColumns - col) - colWidth[col] += space - xDelta -= space - if expandV: - numRows = self.itemCount() / numColumns - if self.itemCount() % numColumns: - numRows += 1 - yDelta = rect.height() - hmargins - (numRows - 1) * self.spacing() - for row in range(numRows): - yDelta -= rowHeight[row] - if yDelta > 0: - for row in range(numRows): - space = yDelta // (numRows - row) - rowHeight[row] += space - yDelta -= space - - def sizeHint(self): - """ - Return the size hint. If maxColumns() > 0 it is the size for - a grid with maxColumns() columns, otherwise it is the size for - a grid with only one row. - """ - if self.isEmpty(): - return QSize() - numColumns = self.itemCount() - if self.__data.maxColumns > 0: - numColumns = min([self.__data.maxColumns, numColumns]) - numRows = int(self.itemCount() / numColumns) - if self.itemCount() % numColumns: - numRows += 1 - rowHeight = [0] * numRows - colWidth = [0] * numColumns - self.layoutGrid(numColumns, rowHeight, colWidth) - margins = self.contentsMargins() - margin_h = margins.top() + margins.bottom() - margin_w = margins.left() + margins.right() - h = margin_h + (numRows - 1) * self.spacing() + sum(rowHeight) - w = margin_w + (numColumns - 1) * self.spacing() + sum(colWidth) - return QSize(w, h) - - def numRows(self): - """Return Number of rows of the current layout.""" - return self.__data.numRows - - def numColumns(self): - """Return Number of columns of the current layout.""" - return self.__data.numColumns diff --git a/qwt/graphic.py b/qwt/graphic.py deleted file mode 100644 index 9e18a4b..0000000 --- a/qwt/graphic.py +++ /dev/null @@ -1,781 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtGraphic ----------- - -.. autoclass:: QwtGraphic - :members: -""" - -import math - -from qtpy.QtCore import QObject, QPointF, QRect, QRectF, QSize, QSizeF, Qt -from qtpy.QtGui import ( - QImage, - QPaintEngine, - QPainter, - QPainterPathStroker, - QPixmap, - QTransform, -) - -from qwt.null_paintdevice import QwtNullPaintDevice -from qwt.painter_command import QwtPainterCommand, _flag_int - -# See painter_command.py for the rationale: cache the QPaintEngine.DirtyXxx -# flags as plain ints so the State-replay branch below does plain int bitwise -# tests instead of going through Python's enum.Flag.__and__ on PyQt6. -_DIRTY_PEN = _flag_int(QPaintEngine.DirtyPen) -_DIRTY_BRUSH = _flag_int(QPaintEngine.DirtyBrush) -_DIRTY_BRUSH_ORIGIN = _flag_int(QPaintEngine.DirtyBrushOrigin) -_DIRTY_FONT = _flag_int(QPaintEngine.DirtyFont) -_DIRTY_BACKGROUND = _flag_int(QPaintEngine.DirtyBackground) -_DIRTY_TRANSFORM = _flag_int(QPaintEngine.DirtyTransform) -_DIRTY_CLIP_ENABLED = _flag_int(QPaintEngine.DirtyClipEnabled) -_DIRTY_CLIP_REGION = _flag_int(QPaintEngine.DirtyClipRegion) -_DIRTY_CLIP_PATH = _flag_int(QPaintEngine.DirtyClipPath) -_DIRTY_HINTS = _flag_int(QPaintEngine.DirtyHints) -_DIRTY_COMPOSITION_MODE = _flag_int(QPaintEngine.DirtyCompositionMode) -_DIRTY_OPACITY = _flag_int(QPaintEngine.DirtyOpacity) - - -def qwtHasScalablePen(painter): - pen = painter.pen() - scalablePen = False - if pen.style() != Qt.NoPen and pen.brush().style() != Qt.NoBrush: - scalablePen = not pen.isCosmetic() - return scalablePen - - -def qwtStrokedPathRect(painter, path): - stroker = QPainterPathStroker() - stroker.setWidth(painter.pen().widthF()) - stroker.setCapStyle(painter.pen().capStyle()) - stroker.setJoinStyle(painter.pen().joinStyle()) - stroker.setMiterLimit(painter.pen().miterLimit()) - rect = QRectF() - if qwtHasScalablePen(painter): - stroke = stroker.createStroke(path) - rect = painter.transform().map(stroke).boundingRect() - else: - mappedPath = painter.transform().map(path) - mappedPath = stroker.createStroke(mappedPath) - rect = mappedPath.boundingRect() - return rect - - -def qwtExecCommand(painter, cmd, renderHints, transform, initialTransform): - if cmd.type() == QwtPainterCommand.Path: - doMap = False - if ( - bool(renderHints & QwtGraphic.RenderPensUnscaled) - and painter.transform().isScaling() - ): - isCosmetic = painter.pen().isCosmetic() - doMap = not isCosmetic - if doMap: - tr = painter.transform() - painter.resetTransform() - path = tr.map(cmd.path()) - if initialTransform: - painter.setTransform(initialTransform) - invt, _ok = initialTransform.inverted() - path = invt.map(path) - painter.drawPath(path) - painter.setTransform(tr) - else: - painter.drawPath(cmd.path()) - elif cmd.type() == QwtPainterCommand.Pixmap: - data = cmd.pixmapData() - painter.drawPixmap(data.rect, data.pixmap, data.subRect) - elif cmd.type() == QwtPainterCommand.Image: - data = cmd.imageData() - painter.drawImage(data.rect, data.image, data.subRect, data.flags) - elif cmd.type() == QwtPainterCommand.State: - data = cmd.stateData() - flags = _flag_int(data.flags) - if flags & _DIRTY_PEN: - painter.setPen(data.pen) - if flags & _DIRTY_BRUSH: - painter.setBrush(data.brush) - if flags & _DIRTY_BRUSH_ORIGIN: - painter.setBrushOrigin(data.brushOrigin) - if flags & _DIRTY_FONT: - painter.setFont(data.font) - if flags & _DIRTY_BACKGROUND: - painter.setBackgroundMode(data.backgroundMode) - painter.setBackground(data.backgroundBrush) - if flags & _DIRTY_TRANSFORM: - painter.setTransform(data.transform) - if flags & _DIRTY_CLIP_ENABLED: - painter.setClipping(data.isClipEnabled) - if flags & _DIRTY_CLIP_REGION: - painter.setClipRegion(data.clipRegion, data.clipOperation) - if flags & _DIRTY_CLIP_PATH: - painter.setClipPath(data.clipPath, data.clipOperation) - if flags & _DIRTY_HINTS: - for hint in ( - QPainter.Antialiasing, - QPainter.TextAntialiasing, - QPainter.SmoothPixmapTransform, - ): - painter.setRenderHint(hint, bool(data.renderHints & hint)) - if flags & _DIRTY_COMPOSITION_MODE: - painter.setCompositionMode(data.compositionMode) - if flags & _DIRTY_OPACITY: - painter.setOpacity(data.opacity) - - -class PathInfo(object): - def __init__(self, *args): - if len(args) == 0: - self.__scalablePen = False - elif len(args) == 3: - pointRect, boundingRect, scalablePen = args - self.__pointRect = pointRect - self.__boundingRect = boundingRect - self.__scalablePen = scalablePen - else: - raise TypeError( - "%s() takes 0 or 3 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def scaledBoundingRect(self, sx, sy, scalePens): - if sx == 1.0 and sy == 1.0: - return self.__boundingRect - transform = QTransform() - transform.scale(sx, sy) - if scalePens and self.__scalablePen: - rect = transform.mapRect(self.__boundingRect) - else: - rect = transform.mapRect(self.__pointRect) - left_diff = abs(self.__pointRect.left() - self.__boundingRect.left()) - right_diff = abs(self.__pointRect.right() - self.__boundingRect.right()) - top_diff = abs(self.__pointRect.top() - self.__boundingRect.top()) - bottom_diff = abs(self.__pointRect.bottom() - self.__boundingRect.bottom()) - rect.adjust(-left_diff, -top_diff, right_diff, bottom_diff) - return rect - - def scaleFactorX(self, pathRect, targetRect, scalePens): - if pathRect.width() <= 0.0: - return 0.0 - p0 = self.__pointRect.center() - left_diff = abs(pathRect.left() - p0.x()) - r = abs(pathRect.right() - p0.x()) - w = 2.0 * min([left_diff, r]) * targetRect.width() / pathRect.width() - if scalePens and self.__scalablePen: - sx = w / self.__boundingRect.width() - else: - pw = max( - [ - abs(self.__boundingRect.left() - self.__pointRect.left()), - abs(self.__boundingRect.right() - self.__pointRect.right()), - ] - ) - sx = (w - 2 * pw) / self.__pointRect.width() - return sx - - def scaleFactorY(self, pathRect, targetRect, scalePens): - if pathRect.height() <= 0.0: - return 0.0 - p0 = self.__pointRect.center() - t = abs(pathRect.top() - p0.y()) - b = abs(pathRect.bottom() - p0.y()) - h = 2.0 * min([t, b]) * targetRect.height() / pathRect.height() - if scalePens and self.__scalablePen: - sy = h / self.__boundingRect.height() - else: - pw = max( - [ - abs(self.__boundingRect.top() - self.__pointRect.top()), - abs(self.__boundingRect.bottom() - self.__pointRect.bottom()), - ] - ) - sy = (h - 2 * pw) / self.__pointRect.height() - return sy - - -class QwtGraphic_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - self.boundingRect = QRectF(0.0, 0.0, -1.0, -1.0) - self.pointRect = QRectF(0.0, 0.0, -1.0, -1.0) - self.initialTransform = None - self.defaultSize = QSizeF() - self.commands = [] - self.pathInfos = [] - self.renderHints = 0 - - -class QwtGraphic(QwtNullPaintDevice): - """ - A paint device for scalable graphics - - `QwtGraphic` is the representation of a graphic that is tailored for - scalability. Like `QPicture` it will be initialized by `QPainter` - operations and can be replayed later to any target paint device. - - While the usual image representations `QImage` and `QPixmap` are not - scalable `Qt` offers two paint devices, that might be candidates - for representing a vector graphic: - - - `QPicture`: - - Unfortunately `QPicture` had been forgotten, when Qt4 - introduced floating point based render engines. Its API - is still on integers, what make it unusable for proper scaling. - - - `QSvgRenderer`, `QSvgGenerator`: - - Unfortunately `QSvgRenderer` hides to much information about - its nodes in internal APIs, that are necessary for proper - layout calculations. Also it is derived from `QObject` and - can't be copied like `QImage`/`QPixmap`. - - `QwtGraphic` maps all scalable drawing primitives to a `QPainterPath` - and stores them together with the painter state changes - ( pen, brush, transformation ... ) in a list of `QwtPaintCommands`. - For being a complete `QPaintDevice` it also stores pixmaps or images, - what is somehow against the idea of the class, because these objects - can't be scaled without a loss in quality. - - The main issue about scaling a `QwtGraphic` object are the pens used for - drawing the outlines of the painter paths. While non cosmetic pens - ( `QPen.isCosmetic()` ) are scaled with the same ratio as the path, - cosmetic pens have a fixed width. A graphic might have paths with - different pens - cosmetic and non-cosmetic. - - `QwtGraphic` caches 2 different rectangles: - - - control point rectangle: - - The control point rectangle is the bounding rectangle of all - control point rectangles of the painter paths, or the target - rectangle of the pixmaps/images. - - - bounding rectangle: - - The bounding rectangle extends the control point rectangle by - what is needed for rendering the outline with an unscaled pen. - - Because the offset for drawing the outline depends on the shape - of the painter path ( the peak of a triangle is different than the flat side ) - scaling with a fixed aspect ratio always needs to be calculated from the - control point rectangle. - - .. py:class:: QwtGraphic() - - Initializes a null graphic - - .. py:class:: QwtGraphic(other) - :noindex: - - Copy constructor - - :param qwt.graphic.QwtGraphic other: Source - """ - - # enum RenderHint - RenderPensUnscaled = 0x1 - - def __init__(self, *args): - QwtNullPaintDevice.__init__(self) - if len(args) == 0: - self.setMode(QwtNullPaintDevice.PathMode) - self.__data = QwtGraphic_PrivateData() - elif len(args) == 1: - (other,) = args - self.setMode(other.mode()) - self.__data = other.__data - else: - raise TypeError( - "%s() takes 0 or 1 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def reset(self): - """Clear all stored commands""" - self.__data.commands = [] - self.__data.pathInfos = [] - self.__data.boundingRect = QRectF(0.0, 0.0, -1.0, -1.0) - self.__data.pointRect = QRectF(0.0, 0.0, -1.0, -1.0) - self.__data.defaultSize = QSizeF() - - def isNull(self): - """Return True, when no painter commands have been stored""" - return len(self.__data.commands) == 0 - - def isEmpty(self): - """Return True, when the bounding rectangle is empty""" - return self.__data.boundingRect.isEmpty() - - def setRenderHint(self, hint, on=True): - """Toggle an render hint""" - if on: - self.__data.renderHints |= hint - else: - self.__data.renderHints &= ~hint - - def testRenderHint(self, hint): - """Test a render hint""" - return bool(self.__data.renderHints & hint) - - def boundingRect(self): - """ - The bounding rectangle is the :py:meth:`controlPointRect` - extended by the areas needed for rendering the outlines - with unscaled pens. - - :return: Bounding rectangle of the graphic - - .. seealso:: - - :py:meth:`controlPointRect`, :py:meth:`scaledBoundingRect` - """ - if self.__data.boundingRect.width() < 0: - return QRectF() - return self.__data.boundingRect - - def controlPointRect(self): - """ - The control point rectangle is the bounding rectangle - of all control points of the paths and the target - rectangles of the images/pixmaps. - - :return: Control point rectangle - - .. seealso:: - - :py:meth:`boundingRect()`, :py:meth:`scaledBoundingRect()` - """ - if self.__data.pointRect.width() < 0: - return QRectF() - return self.__data.pointRect - - def scaledBoundingRect(self, sx, sy): - """ - Calculate the target rectangle for scaling the graphic - - :param float sx: Horizontal scaling factor - :param float sy: Vertical scaling factor - :return: Scaled bounding rectangle - - .. note:: - - In case of paths that are painted with a cosmetic pen - (see :py:meth:`QPen.isCosmetic()`) the target rectangle is - different to multiplying the bounding rectangle. - - .. seealso:: - - :py:meth:`boundingRect()`, :py:meth:`controlPointRect()` - """ - if sx == 1.0 and sy == 1.0: - return self.__data.boundingRect - transform = QTransform() - transform.scale(sx, sy) - rect = transform.mapRect(self.__data.pointRect) - for pathInfo in self.__data.pathInfos: - rect |= pathInfo.scaledBoundingRect( - sx, sy, not bool(self.__data.renderHints & self.RenderPensUnscaled) - ) - return rect - - def sizeMetrics(self): - """Return Ceiled :py:meth:`defaultSize()`""" - sz = self.defaultSize() - return QSize(math.ceil(sz.width()), math.ceil(sz.height())) - - def setDefaultSize(self, size): - """ - The default size is used in all methods rendering the graphic, - where no size is explicitly specified. Assigning an empty size - means, that the default size will be calculated from the bounding - rectangle. - - :param QSizeF size: Default size - - .. seealso:: - - :py:meth:`defaultSize()`, :py:meth:`boundingRect()` - """ - w = max([0.0, size.width()]) - h = max([0.0, size.height()]) - self.__data.defaultSize = QSizeF(w, h) - - def defaultSize(self): - """ - When a non empty size has been assigned by setDefaultSize() this - size will be returned. Otherwise the default size is the size - of the bounding rectangle. - - The default size is used in all methods rendering the graphic, - where no size is explicitly specified. - - :return: Default size - - .. seealso:: - - :py:meth:`setDefaultSize()`, :py:meth:`boundingRect()` - """ - if not self.__data.defaultSize.isEmpty(): - return self.__data.defaultSize - return self.boundingRect().size() - - def render(self, *args): - """ - .. py:method:: render(painter) - :noindex: - - Replay all recorded painter commands - - :param QPainter painter: Qt painter - - .. py:method:: render(painter, size, aspectRatioMode) - :noindex: - - Replay all recorded painter commands - - The graphic is scaled to fit into the rectangle - of the given size starting at ( 0, 0 ). - - :param QPainter painter: Qt painter - :param QSizeF size: Size for the scaled graphic - :param Qt.AspectRatioMode aspectRatioMode: Mode how to scale - - .. py:method:: render(painter, rect, aspectRatioMode) - :noindex: - - Replay all recorded painter commands - - The graphic is scaled to fit into the given rectangle - - :param QPainter painter: Qt painter - :param QRectF rect: Rectangle for the scaled graphic - :param Qt.AspectRatioMode aspectRatioMode: Mode how to scale - - .. py:method:: render(painter, pos, aspectRatioMode) - :noindex: - - Replay all recorded painter commands - - The graphic is scaled to the :py:meth:`defaultSize()` and aligned - to a position. - - :param QPainter painter: Qt painter - :param QPointF pos: Reference point, where to render - :param Qt.AspectRatioMode aspectRatioMode: Mode how to scale - """ - if len(args) == 1: - (painter,) = args - if self.isNull(): - return - transform = painter.transform() - painter.save() - for command in self.__data.commands: - qwtExecCommand( - painter, - command, - self.__data.renderHints, - transform, - self.__data.initialTransform, - ) - painter.restore() - elif len(args) in (2, 3) and isinstance(args[1], QSizeF): - painter, size = args[:2] - aspectRatioMode = Qt.IgnoreAspectRatio - if len(args) == 3: - aspectRatioMode = args[-1] - r = QRectF(0.0, 0.0, size.width(), size.height()) - self.render(painter, r, aspectRatioMode) - elif len(args) in (2, 3) and isinstance(args[1], QRectF): - painter, rect = args[:2] - aspectRatioMode = Qt.IgnoreAspectRatio - if len(args) == 3: - aspectRatioMode = args[-1] - if self.isEmpty() or rect.isEmpty(): - return - sx = 1.0 - sy = 1.0 - if self.__data.pointRect.width() > 0.0: - sx = rect.width() / self.__data.pointRect.width() - if self.__data.pointRect.height() > 0.0: - sy = rect.height() / self.__data.pointRect.height() - scalePens = not bool(self.__data.renderHints & self.RenderPensUnscaled) - for info in self.__data.pathInfos: - ssx = info.scaleFactorX(self.__data.pointRect, rect, scalePens) - if ssx > 0.0: - sx = min([sx, ssx]) - ssy = info.scaleFactorY(self.__data.pointRect, rect, scalePens) - if ssy > 0.0: - sy = min([sy, ssy]) - if aspectRatioMode == Qt.KeepAspectRatio: - s = min([sx, sy]) - sx = s - sy = s - elif aspectRatioMode == Qt.KeepAspectRatioByExpanding: - s = max([sx, sy]) - sx = s - sy = s - tr = QTransform() - tr.translate( - rect.center().x() - 0.5 * sx * self.__data.pointRect.width(), - rect.center().y() - 0.5 * sy * self.__data.pointRect.height(), - ) - tr.scale(sx, sy) - tr.translate(-self.__data.pointRect.x(), -self.__data.pointRect.y()) - transform = painter.transform() - if not scalePens and transform.isScaling(): - # we don't want to scale pens according to sx/sy, - # but we want to apply the scaling from the - # painter transformation later - self.__data.initialTransform = QTransform() - self.__data.initialTransform.scale(transform.m11(), transform.m22()) - painter.setTransform(tr, True) - self.render(painter) - painter.setTransform(transform) - self.__data.initialTransform = None - elif len(args) in (2, 3) and isinstance(args[1], QPointF): - painter, pos = args[:2] - alignment = Qt.AlignTop | Qt.AlignLeft - if len(args) == 3: - alignment = args[-1] - r = QRectF(pos, self.defaultSize()) - if alignment & Qt.AlignLeft: - r.moveLeft(pos.x()) - elif alignment & Qt.AlignHCenter: - r.moveCenter(QPointF(pos.x(), r.center().y())) - elif alignment & Qt.AlignRight: - r.moveRight(pos.x()) - if alignment & Qt.AlignTop: - r.moveTop(pos.y()) - elif alignment & Qt.AlignVCenter: - r.moveCenter(QPointF(r.center().x(), pos.y())) - elif alignment & Qt.AlignBottom: - r.moveBottom(pos.y()) - self.render(painter, r) - else: - raise TypeError( - "%s().render() takes 1, 2 or 3 argument(s) (%s " - "given)" % (self.__class__.__name__, len(args)) - ) - - def toPixmap(self, *args): - """ - Convert the graphic to a `QPixmap` - - All pixels of the pixmap get initialized by `Qt.transparent` - before the graphic is scaled and rendered on it. - - The size of the pixmap is the default size ( ceiled to integers ) - of the graphic. - - :return: The graphic as pixmap in default size - - .. seealso:: - - :py:meth:`defaultSize()`, :py:meth:`toImage()`, :py:meth:`render()` - """ - if len(args) == 0: - if self.isNull(): - return QPixmap() - sz = self.defaultSize() - w = math.ceil(sz.width()) - h = math.ceil(sz.height()) - pixmap = QPixmap(w, h) - pixmap.fill(Qt.transparent) - r = QRectF(0.0, 0.0, sz.width(), sz.height()) - painter = QPainter(pixmap) - self.render(painter, r, Qt.KeepAspectRatio) - painter.end() - return pixmap - elif len(args) in (1, 2): - size = args[0] - aspectRatioMode = Qt.IgnoreAspectRatio - if len(args) == 2: - aspectRatioMode = args[-1] - pixmap = QPixmap(size) - pixmap.fill(Qt.transparent) - r = QRect(0, 0, size.width(), size.height()) - painter = QPainter(pixmap) - self.render(painter, r, aspectRatioMode) - painter.end() - return pixmap - - def toImage(self, *args): - """ - .. py:method:: toImage() - :noindex: - - Convert the graphic to a `QImage` - - All pixels of the image get initialized by 0 ( transparent ) - before the graphic is scaled and rendered on it. - - The format of the image is `QImage.Format_ARGB32_Premultiplied`. - - The size of the image is the default size ( ceiled to integers ) - of the graphic. - - :return: The graphic as image in default size - - .. py:method:: toImage(size, [aspectRatioMode=Qt.IgnoreAspectRatio]) - :noindex: - - Convert the graphic to a `QImage` - - All pixels of the image get initialized by 0 ( transparent ) - before the graphic is scaled and rendered on it. - - The format of the image is `QImage.Format_ARGB32_Premultiplied`. - - :param QSize size: Size of the image - :param `Qt.AspectRatioMode` aspectRatioMode: Aspect ratio how to scale the graphic - :return: The graphic as image - - .. seealso:: - - :py:meth:`toPixmap()`, :py:meth:`render()` - """ - if len(args) == 0: - if self.isNull(): - return QImage() - sz = self.defaultSize() - w = math.ceil(sz.width()) - h = math.ceil(sz.height()) - image = QImage(w, h, QImage.Format_ARGB32) - image.fill(0) - r = QRect(0, 0, sz.width(), sz.height()) - painter = QPainter(image) - self.render(painter, r, Qt.KeepAspectRatio) - painter.end() - return image - elif len(args) in (1, 2): - size = args[0] - aspectRatioMode = Qt.IgnoreAspectRatio - if len(args) == 2: - aspectRatioMode = args[-1] - image = QImage(size, QImage.Format_ARGB32_Premultiplied) - image.fill(0) - r = QRect(0, 0, size.width(), size.height()) - painter = QPainter(image) - self.render(painter, r, aspectRatioMode) - return image - - def drawPath(self, path): - """ - Store a path command in the command list - - :param QPainterPath path: Painter path - - .. seealso:: - - :py:meth:`QPaintEngine.drawPath()` - """ - painter = self.paintEngine().painter() - if painter is None: - return - self.__data.commands += [QwtPainterCommand(path)] - if not path.isEmpty(): - scaledPath = painter.transform().map(path) - pointRect = scaledPath.boundingRect() - boundingRect = QRectF(pointRect) - if ( - painter.pen().style() != Qt.NoPen - and painter.pen().brush().style() != Qt.NoBrush - ): - boundingRect = qwtStrokedPathRect(painter, path) - self.updateControlPointRect(pointRect) - self.updateBoundingRect(boundingRect) - self.__data.pathInfos += [ - PathInfo(pointRect, boundingRect, qwtHasScalablePen(painter)) - ] - - def drawPixmap(self, rect, pixmap, subRect): - """ - Store a pixmap command in the command list - - :param QRectF rect: target rectangle - :param QPixmap pixmap: Pixmap to be painted - :param QRectF subRect: Reactangle of the pixmap to be painted - - .. seealso:: - - :py:meth:`QPaintEngine.drawPixmap()` - """ - painter = self.paintEngine().painter() - if painter is None: - return - self.__data.commands += [QwtPainterCommand(rect, pixmap, subRect)] - r = painter.transform().mapRect(rect) - self.updateControlPointRect(r) - self.updateBoundingRect(r) - - def drawImage(self, rect, image, subRect, flags): - """ - Store a image command in the command list - - :param QRectF rect: target rectangle - :param QImage image: Pixmap to be painted - :param QRectF subRect: Reactangle of the pixmap to be painted - :param Qt.ImageConversionFlags flags: Pixmap to be painted - - .. seealso:: - - :py:meth:`QPaintEngine.drawImage()` - """ - painter = self.paintEngine().painter() - if painter is None: - return - self.__data.commands += [QwtPainterCommand(rect, image, subRect, flags)] - r = painter.transform().mapRect(rect) - self.updateControlPointRect(r) - self.updateBoundingRect(r) - - def updateState(self, state): - """ - Store a state command in the command list - - :param QPaintEngineState state: State to be stored - - .. seealso:: - - :py:meth:`QPaintEngine.updateState()` - """ - # XXX: shall we call the parent's implementation of updateState? - self.__data.commands += [QwtPainterCommand(state)] - - def updateBoundingRect(self, rect): - br = QRectF(rect) - painter = self.paintEngine().painter() - if painter and painter.hasClipping(): - cr = painter.clipRegion().boundingRect() - cr = painter.transform().mapRect(cr) - br &= cr - if self.__data.boundingRect.width() < 0: - self.__data.boundingRect = br - else: - self.__data.boundingRect |= br - - def updateControlPointRect(self, rect): - if self.__data.pointRect.width() < 0.0: - self.__data.pointRect = rect - else: - self.__data.pointRect |= rect - - def commands(self): - return self.__data.commands - - def setCommands(self, commands): - self.reset() - painter = QPainter(self) - for cmd in commands: - qwtExecCommand(painter, cmd, 0, QTransform(), None) - painter.end() diff --git a/qwt/interval.py b/qwt/interval.py deleted file mode 100644 index bd2a5df..0000000 --- a/qwt/interval.py +++ /dev/null @@ -1,398 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtInterval ------------ - -.. autoclass:: QwtInterval - :members: -""" - - -class QwtInterval(object): - """ - A class representing an interval - - The interval is represented by 2 doubles, the lower and the upper limit. - - .. py:class:: QwtInterval(minValue=0., maxValue=-1., borderFlags=None) - - Build an interval with from min/max values - - :param float minValue: Minimum value - :param float maxValue: Maximum value - :param int borderFlags: Include/Exclude borders - """ - - # enum BorderFlag - IncludeBorders = 0x00 - ExcludeMinimum = 0x01 - ExcludeMaximum = 0x02 - ExcludeBorders = ExcludeMinimum | ExcludeMaximum - - def __init__(self, minValue=0.0, maxValue=-1.0, borderFlags=None): - assert not isinstance(minValue, QwtInterval) - assert not isinstance(maxValue, QwtInterval) - self.__minValue = None - self.__maxValue = None - self.__borderFlags = None - self.setInterval(minValue, maxValue, borderFlags) - - def setInterval(self, minValue, maxValue, borderFlags=None): - """ - Assign the limits of the interval - - :param float minValue: Minimum value - :param float maxValue: Maximum value - :param int borderFlags: Include/Exclude borders - """ - self.__minValue = float(minValue) # avoid overflows with NumPy scalars - self.__maxValue = float(maxValue) # avoid overflows with NumPy scalars - if borderFlags is None: - self.__borderFlags = self.IncludeBorders - else: - self.__borderFlags = borderFlags - - def setBorderFlags(self, borderFlags): - """ - Change the border flags - - :param int borderFlags: Include/Exclude borders - - .. seealso:: - - :py:meth:`borderFlags()` - """ - self.__borderFlags = borderFlags - - def borderFlags(self): - """ - :return: Border flags - - .. seealso:: - - :py:meth:`setBorderFlags()` - """ - return self.__borderFlags - - def setMinValue(self, minValue): - """ - Assign the lower limit of the interval - - :param float minValue: Minimum value - """ - self.__minValue = float(minValue) # avoid overflows with NumPy scalars - - def setMaxValue(self, maxValue): - """ - Assign the upper limit of the interval - - :param float maxValue: Maximum value - """ - self.__maxValue = float(maxValue) # avoid overflows with NumPy scalars - - def minValue(self): - """ - :return: Lower limit of the interval - """ - return self.__minValue - - def maxValue(self): - """ - :return: Upper limit of the interval - """ - return self.__maxValue - - def isValid(self): - """ - A interval is valid when minValue() <= maxValue(). - In case of `QwtInterval.ExcludeBorders` it is true - when minValue() < maxValue() - - :return: True, when the interval is valid - """ - if (self.__borderFlags & self.ExcludeBorders) == 0: - return self.__minValue <= self.__maxValue - else: - return self.__minValue < self.__maxValue - - def width(self): - """ - The width of invalid intervals is 0.0, otherwise the result is - maxValue() - minValue(). - - :return: the width of an interval - """ - if self.isValid(): - return self.__maxValue - self.__minValue - else: - return 0.0 - - def __and__(self, other): - return self.intersect(other) - - def __iand__(self, other): - self = self & other - return self - - def __or__(self, other): - if isinstance(other, QwtInterval): - return self.unite(other) - else: - return self.extend(other) - - def __ior__(self, other): - self = self | other - return self - - def __eq__(self, other): - return ( - self.__minValue == other.__minValue - and self.__maxValue == other.__maxValue - and self.__borderFlags == other.__borderFlags - ) - - def __ne__(self, other): - return not self.__eq__(other) - - def isNull(self): - """ - :return: true, if isValid() && (minValue() >= maxValue()) - """ - return self.isValid() and self.__minValue >= self.__maxValue - - def invalidate(self): - """ - The limits are set to interval [0.0, -1.0] - - .. seealso:: - - :py:meth:`isValid()` - """ - self.__minValue = 0.0 - self.__maxValue = -1.0 - - def normalized(self): - """ - Normalize the limits of the interval - - If maxValue() < minValue() the limits will be inverted. - - :return: Normalized interval - - .. seealso:: - - :py:meth:`isValid()`, :py:meth:`inverted()` - """ - if self.__minValue > self.__maxValue: - return self.inverted() - elif ( - self.__minValue == self.__maxValue - and self.__borderFlags == self.ExcludeMinimum - ): - return self.inverted() - else: - return self - - def inverted(self): - """ - Invert the limits of the interval - - :return: Inverted interval - - .. seealso:: - - :py:meth:`normalized()` - """ - borderFlags = self.IncludeBorders - if self.__borderFlags & self.ExcludeMinimum: - borderFlags |= self.ExcludeMaximum - if self.__borderFlags & self.ExcludeMaximum: - borderFlags |= self.ExcludeMinimum - return QwtInterval(self.__maxValue, self.__minValue, borderFlags) - - def contains(self, value): - """ - Test if a value is inside an interval - - :param float value: Value - :return: true, if value >= minValue() && value <= maxValue() - """ - if not self.isValid(): - return False - elif value < self.__minValue or value > self.__maxValue: - return False - elif value == self.__minValue and self.__borderFlags & self.ExcludeMinimum: - return False - elif value == self.__maxValue and self.__borderFlags & self.ExcludeMaximum: - return False - else: - return True - - def unite(self, other): - """ - Unite two intervals - - :param qwt.interval.QwtInterval other: other interval to united with - :return: united interval - """ - if not self.isValid(): - if not other.isValid(): - return QwtInterval() - else: - return other - elif not other.isValid(): - return self - - united = QwtInterval() - flags = self.IncludeBorders - - # minimum - if self.__minValue < other.minValue(): - united.setMinValue(self.__minValue) - flags &= self.__borderFlags & self.ExcludeMinimum - elif other.minValue() < self.__minValue: - united.setMinValue(other.minValue()) - flags &= other.borderFlags() & self.ExcludeMinimum - else: - united.setMinValue(self.__minValue) - flags &= (self.__borderFlags & other.borderFlags()) & self.ExcludeMinimum - - # maximum - if self.__maxValue > other.maxValue(): - united.setMaxValue(self.__maxValue) - flags &= self.__borderFlags & self.ExcludeMaximum - elif other.maxValue() > self.__maxValue: - united.setMaxValue(other.maxValue()) - flags &= other.borderFlags() & self.ExcludeMaximum - else: - united.setMaxValue(self.__maxValue) - flags &= self.__borderFlags & other.borderFlags() & self.ExcludeMaximum - - united.setBorderFlags(flags) - return united - - def intersect(self, other): - """ - Intersect two intervals - - :param qwt.interval.QwtInterval other: other interval to intersect with - :return: intersected interval - """ - if not other.isValid() or not self.isValid(): - return QwtInterval() - - i1 = self - i2 = other - - if i1.minValue() > i2.minValue(): - i1, i2 = i2, i1 - elif i1.minValue() == i2.minValue(): - if i1.borderFlags() & self.ExcludeMinimum: - i1, i2 = i2, i1 - - if i1.maxValue() < i2.maxValue(): - return QwtInterval() - - if i1.maxValue() == i2.minValue(): - if ( - i1.borderFlags() & self.ExcludeMaximum - or i2.borderFlags() & self.ExcludeMinimum - ): - return QwtInterval() - - intersected = QwtInterval() - flags = self.IncludeBorders - - intersected.setMinValue(i2.minValue()) - flags |= i2.borderFlags() & self.ExcludeMinimum - - if i1.maxValue() < i2.maxValue(): - intersected.setMaxValue(i1.maxValue()) - flags |= i1.borderFlags() & self.ExcludeMaximum - elif i2.maxValue() < i1.maxValue(): - intersected.setMaxValue(i2.maxValue()) - flags |= i2.borderFlags() & self.ExcludeMaximum - else: # i1.maxValue() == i2.maxValue() - intersected.setMaxValue(i1.maxValue()) - flags |= i1.borderFlags() & i2.borderFlags() & self.ExcludeMaximum - - intersected.setBorderFlags(flags) - return intersected - - def intersects(self, other): - """ - Test if two intervals overlap - - :param qwt.interval.QwtInterval other: other interval - :return: True, when the intervals are intersecting - """ - if not other.isValid() or not self.isValid(): - return False - - i1 = self - i2 = other - - if i1.minValue() > i2.minValue(): - i1, i2 = i2, i1 - elif i1.minValue() == i2.minValue() and i1.borderFlags() & self.ExcludeMinimum: - i1, i2 = i2, i1 - - if i1.maxValue() > i2.minValue(): - return True - elif i1.maxValue() == i2.minValue(): - return ( - i1.borderFlags() & self.ExcludeMaximum - and i2.borderFlags() & self.ExcludeMinimum - ) - return False - - def symmetrize(self, value): - """ - Adjust the limit that is closer to value, so that value becomes - the center of the interval. - - :param float value: Center - :return: Interval with value as center - """ - if not self.isValid(): - return self - delta = max([abs(value - self.__maxValue), abs(value - self.__minValue)]) - return QwtInterval(value - delta, value + delta) - - def limited(self, lowerBound, upperBound): - """ - Limit the interval, keeping the border modes - - :param float lowerBound: Lower limit - :param float upperBound: Upper limit - :return: Limited interval - """ - if not self.isValid() or lowerBound > upperBound: - return QwtInterval() - minValue = max([self.__minValue, lowerBound]) - minValue = min([minValue, upperBound]) - maxValue = max([self.__maxValue, lowerBound]) - maxValue = min([maxValue, upperBound]) - return QwtInterval(minValue, maxValue, self.__borderFlags) - - def extend(self, value): - """ - Extend the interval - - If value is below minValue(), value becomes the lower limit. - If value is above maxValue(), value becomes the upper limit. - - extend() has no effect for invalid intervals - - :param float value: Value - :return: extended interval - """ - if not self.isValid(): - return self - return QwtInterval(min([value, self.__minValue]), max([value, self.__maxValue])) diff --git a/qwt/legend.py b/qwt/legend.py deleted file mode 100644 index a117370..0000000 --- a/qwt/legend.py +++ /dev/null @@ -1,1001 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtLegend ---------- - -.. autoclass:: QwtLegendData - :members: - -.. autoclass:: QwtLegendLabel - :members: - -.. autoclass:: QwtLegend - :members: -""" - -import math - -from qtpy.QtCore import QEvent, QObject, QPoint, QRect, QRectF, QSize, Qt, Signal - -# qDrawWinButton, -from qtpy.QtGui import QPainter, QPalette, QPixmap -from qtpy.QtWidgets import ( - QApplication, - QFrame, - QScrollArea, - QStyle, - QStyleOption, - QVBoxLayout, - QWidget, -) - -from qwt.dyngrid_layout import QwtDynGridLayout -from qwt.painter import QwtPainter -from qwt.text import QwtText, QwtTextLabel - - -class QwtLegendData(object): - """ - Attributes of an entry on a legend - - `QwtLegendData` is an abstract container ( like `QAbstractModel` ) - to exchange attributes, that are only known between to - the plot item and the legend. - - By overloading `QwtPlotItem.legendData()` any other set of attributes - could be used, that can be handled by a modified ( or completely - different ) implementation of a legend. - - .. seealso:: - - :py:class:`qwt.legend.QwtLegend` - - .. note:: - - The stockchart example implements a legend as a tree - with checkable items - """ - - # enum Mode - ReadOnly, Clickable, Checkable = list(range(3)) - - # enum Role - ModeRole, TitleRole, IconRole = list(range(3)) - UserRole = 32 - - def __init__(self): - self.__map = {} - - def setValues(self, map_): - """ - Set the legend attributes - - :param dict map_: Values - - .. seealso:: - - :py:meth:`values()` - """ - self.__map = map_ - - def values(self): - """ - :return: Legend attributes - - .. seealso:: - - :py:meth:`setValues()` - """ - return self.__map - - def hasRole(self, role): - """ - :param int role: Attribute role - :return: True, when the internal map has an entry for role - """ - return role in self.__map - - def setValue(self, role, data): - """ - Set an attribute value - - :param int role: Attribute role - :param QVariant data: Attribute value - - .. seealso:: - - :py:meth:`value()` - """ - self.__map[role] = data - - def value(self, role): - """ - :param int role: Attribute role - :return: Attribute value for a specific role - - .. seealso:: - - :py:meth:`setValue()` - """ - return self.__map.get(role) - - def isValid(self): - """ - :return: True, when the internal map is empty - """ - return len(self.__map) != 0 - - def title(self): - """ - :return: Value of the TitleRole attribute - """ - titleValue = self.value(QwtLegendData.TitleRole) - if isinstance(titleValue, QwtText): - text = titleValue - else: - text = QwtText(titleValue) - return text - - def icon(self): - """ - :return: Value of the IconRole attribute - """ - return self.value(QwtLegendData.IconRole) - - def mode(self): - """ - :return: Value of the ModeRole attribute - """ - modeValue = self.value(QwtLegendData.ModeRole) - if isinstance(modeValue, int): - return modeValue - return QwtLegendData.ReadOnly - - -BUTTONFRAME = 2 -MARGIN = 2 - - -def buttonShift(w): - option = QStyleOption() - option.initFrom(w) - ph = w.style().pixelMetric(QStyle.PM_ButtonShiftHorizontal, option, w) - pv = w.style().pixelMetric(QStyle.PM_ButtonShiftVertical, option, w) - return QSize(ph, pv) - - -class QwtLegendLabel_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.itemMode = QwtLegendData.ReadOnly - self.isDown = False - self.spacing = MARGIN - self.legendData = QwtLegendData() - self.icon = QPixmap() - - -class QwtLegendLabel(QwtTextLabel): - """A widget representing something on a QwtLegend.""" - - clicked = Signal() - pressed = Signal() - released = Signal() - checked = Signal(bool) - - def __init__(self, parent=None): - QwtTextLabel.__init__(self, parent) - self.__data = QwtLegendLabel_PrivateData() - self.setMargin(MARGIN) - self.setIndent(MARGIN) - - def setData(self, legendData): - """ - Set the attributes of the legend label - - :param QwtLegendData legendData: Attributes of the label - - .. seealso:: - - :py:meth:`data()` - """ - self.__data.legendData = legendData - doUpdate = self.updatesEnabled() - self.setUpdatesEnabled(False) - self.setText(legendData.title()) - icon = legendData.icon() - if icon is not None: - self.setIcon(icon.toPixmap()) - if legendData.hasRole(QwtLegendData.ModeRole): - self.setItemMode(legendData.mode()) - if doUpdate: - self.setUpdatesEnabled(True) - self.update() - - def data(self): - """ - :return: Attributes of the label - - .. seealso:: - - :py:meth:`setData()`, :py:meth:`qwt.plot.QwtPlotItem.legendData()` - """ - return self.__data.legendData - - def setText(self, text): - """ - Set the text to the legend item - - :param qwt.text.QwtText text: Text label - - .. seealso:: - - :py:meth:`text()` - """ - flags = Qt.AlignLeft | Qt.AlignVCenter | Qt.TextExpandTabs | Qt.TextWordWrap - text.setRenderFlags(flags) - QwtTextLabel.setText(self, text) - - def setItemMode(self, mode): - """ - Set the item mode. - The default is `QwtLegendData.ReadOnly`. - - :param int mode: Item mode - - .. seealso:: - - :py:meth:`itemMode()` - """ - if mode != self.__data.itemMode: - self.__data.itemMode = mode - self.__data.isDown = False - self.setFocusPolicy( - Qt.TabFocus if mode != QwtLegendData.ReadOnly else Qt.NoFocus - ) - self.setMargin(BUTTONFRAME + MARGIN) - self.updateGeometry() - - def itemMode(self): - """ - :return: Item mode - - .. seealso:: - - :py:meth:`setItemMode()` - """ - return self.__data.itemMode - - def setIcon(self, icon): - """ - Assign the icon - - :param QPixmap icon: Pixmap representing a plot item - - .. seealso:: - - :py:meth:`icon()`, :py:meth:`qwt.plot.QwtPlotItem.legendIcon()` - """ - self.__data.icon = icon - indent = self.margin() + self.__data.spacing - if icon.width() > 0: - indent += icon.width() + self.__data.spacing - self.setIndent(indent) - - def icon(self): - """ - :return: Pixmap representing a plot item - - .. seealso:: - - :py:meth:`setIcon()` - """ - return self.__data.icon - - def setSpacing(self, spacing): - """ - Change the spacing between icon and text - - :param int spacing: Spacing - - .. seealso:: - - :py:meth:`spacing()`, :py:meth:`qwt.text.QwtTextLabel.margin()` - """ - spacing = max([spacing, 0]) - if spacing != self.__data.spacing: - self.__data.spacing = spacing - mgn = self.contentsMargins() - margin = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()]) - indent = margin + self.__data.spacing - if self.__data.icon.width() > 0: - indent += self.__data.icon.width() + self.__data.spacing - self.setIndent(indent) - - def spacing(self): - """ - :return: Spacing between icon and text - - .. seealso:: - - :py:meth:`setSpacing()` - """ - return self.__data.spacing - - def setChecked(self, on): - """ - Check/Uncheck a the item - - :param bool on: check/uncheck - - .. seealso:: - - :py:meth:`isChecked()`, :py:meth:`setItemMode()` - """ - if self.__data.itemMode == QwtLegendData.Checkable: - isBlocked = self.signalsBlocked() - self.blockSignals(True) - self.setDown(on) - self.blockSignals(isBlocked) - - def isChecked(self): - """ - :return: true, if the item is checked - - .. seealso:: - - :py:meth:`setChecked()` - """ - return self.__data.itemMode == QwtLegendData.Checkable and self.isDown() - - def setDown(self, down): - """ - Set the item being down - - :param bool on: true, if the item is down - - .. seealso:: - - :py:meth:`isDown()` - """ - if down == self.__data.isDown: - return - self.__data.isDown = down - self.update() - if self.__data.itemMode == QwtLegendData.Clickable: - if self.__data.isDown: - self.pressed.emit() - else: - self.released.emit() - self.clicked.emit() - if self.__data.itemMode == QwtLegendData.Checkable: - self.checked.emit(self.__data.isDown) - - def isDown(self): - """ - :return: true, if the item is down - - .. seealso:: - - :py:meth:`setDown()` - """ - return self.__data.isDown - - def sizeHint(self): - """ - :return: a size hint - """ - sz = QwtTextLabel.sizeHint(self) - sz.setHeight(max([sz.height(), self.__data.icon.height() + 4])) - if self.__data.itemMode != QwtLegendData.ReadOnly: - sz += buttonShift(self) - return sz - - def paintEvent(self, e): - cr = self.contentsRect() - painter = QPainter(self) - painter.setClipRegion(e.region()) - # if self.__data.isDown: - # qDrawWinButton( - # painter, 0, 0, self.width(), self.height(), self.palette(), True - # ) - painter.save() - if self.__data.isDown: - shiftSize = buttonShift(self) - painter.translate(shiftSize.width(), shiftSize.height()) - painter.setClipRect(cr) - self.drawContents(painter) - if not self.__data.icon.isNull(): - iconRect = QRect(cr) - iconRect.setX(iconRect.x() + self.margin()) - if self.__data.itemMode != QwtLegendData.ReadOnly: - iconRect.setX(iconRect.x() + BUTTONFRAME) - iconRect.setSize(self.__data.icon.size()) - iconRect.moveCenter(QPoint(iconRect.center().x(), cr.center().y())) - painter.drawPixmap(iconRect, self.__data.icon) - painter.restore() - - def mousePressEvent(self, e): - if e.button() == Qt.LeftButton: - if self.__data.itemMode == QwtLegendData.Clickable: - self.setDown(True) - return - elif self.__data.itemMode == QwtLegendData.Checkable: - self.setDown(not self.isDown()) - return - QwtTextLabel.mousePressEvent(self, e) - - def mouseReleaseEvent(self, e): - if e.button() == Qt.LeftButton: - if self.__data.itemMode == QwtLegendData.Clickable: - self.setDown(False) - return - elif self.__data.itemMode == QwtLegendData.Checkable: - return - QwtTextLabel.mouseReleaseEvent(self, e) - - def keyPressEvent(self, e): - if e.key() == Qt.Key_Space: - if self.__data.itemMode == QwtLegendData.Clickable: - if not e.isAutoRepeat(): - self.setDown(True) - return - elif self.__data.itemMode == QwtLegendData.Checkable: - if not e.isAutoRepeat(): - self.setDown(not self.isDown()) - return - QwtTextLabel.keyPressEvent(self, e) - - def keyReleaseEvent(self, e): - if e.key() == Qt.Key_Space: - if self.__data.itemMode == QwtLegendData.Clickable: - if not e.isAutoRepeat(): - self.setDown(False) - return - elif self.__data.itemMode == QwtLegendData.Checkable: - return - QwtTextLabel.keyReleaseEvent(self, e) - - -class QwtAbstractLegend(QFrame): - def __init__(self, parent): - QFrame.__init__(self, parent) - - def renderLegend(self, painter, rect, fillBackground): - raise NotImplementedError - - def isEmpty(self): - return 0 - - def scrollExtent(self, orientation): - return 0 - - def updateLegend(self, itemInfo, data): - raise NotImplementedError - - -class Entry(object): - def __init__(self): - self.itemInfo = None - self.widgets = [] - - -class QwtLegendMap(object): - def __init__(self): - self.__entries = [] - - def isEmpty(self): - return len(self.__entries) == 0 - - def insert(self, itemInfo, widgets): - for entry in self.__entries: - if entry.itemInfo == itemInfo: - entry.widgets = widgets - return - newEntry = Entry() - newEntry.itemInfo = itemInfo - newEntry.widgets = widgets - self.__entries += [newEntry] - - def remove(self, itemInfo): - for entry in self.__entries[:]: - if entry.itemInfo == itemInfo: - self.__entries.remove(entry) - return - - def removeWidget(self, widget): - for entry in self.__entries: - while widget in entry.widgets: - entry.widgets.remove(widget) - - def itemInfo(self, widget): - if widget is not None: - for entry in self.__entries: - if widget in entry.widgets: - return entry.itemInfo - - def legendWidgets(self, itemInfo): - if itemInfo is not None: - for entry in self.__entries: - if entry.itemInfo == itemInfo: - return entry.widgets - return [] - - -class LegendView(QScrollArea): - def __init__(self, parent): - QScrollArea.__init__(self, parent) - self.contentsWidget = QWidget(self) - self.contentsWidget.setObjectName("QwtLegendViewContents") - self.setWidget(self.contentsWidget) - self.setWidgetResizable(False) - self.viewport().setObjectName("QwtLegendViewport") - self.contentsWidget.setAutoFillBackground(False) - self.viewport().setAutoFillBackground(False) - - def event(self, event): - if event.type() == QEvent.PolishRequest: - self.setFocusPolicy(Qt.NoFocus) - if event.type() == QEvent.Resize: - cr = self.contentsRect() - w = cr.width() - h = self.contentsWidget.heightForWidth(cr.width()) - if h > w: - w -= self.verticalScrollBar().sizeHint().width() - h = self.contentsWidget.heightForWidth(w) - self.contentsWidget.resize(w, h) - return QScrollArea.event(self, event) - - def viewportEvent(self, event): - ok = QScrollArea.viewportEvent(self, event) - if event.type() == QEvent.Resize: - self.layoutContents() - return ok - - def viewportSize(self, w, h): - sbHeight = self.horizontalScrollBar().sizeHint().height() - sbWidth = self.verticalScrollBar().sizeHint().width() - cw = self.contentsRect().width() - ch = self.contentsRect().height() - vw = cw - vh = ch - if w > vw: - vh -= sbHeight - if h > vh: - vw -= sbWidth - if w > vw and vh == ch: - vh -= sbHeight - return QSize(vw, vh) - - def layoutContents(self): - layout = self.contentsWidget.layout() - if layout is None: - return - visibleSize = self.viewport().contentsRect().size() - margins = layout.contentsMargins() - margin_w = margins.left() + margins.right() - minW = int(layout.maxItemWidth() + margin_w) - w = max([visibleSize.width(), minW]) - h = max([layout.heightForWidth(w), visibleSize.height()]) - vpWidth = self.viewportSize(w, h).width() - if w > vpWidth: - w = max([vpWidth, minW]) - h = max([layout.heightForWidth(w), visibleSize.height()]) - self.contentsWidget.resize(w, h) - - -class QwtLegend_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.itemMode = QwtLegendData.ReadOnly - self.view = QwtDynGridLayout() - self.itemMap = QwtLegendMap() - - -class QwtLegend(QwtAbstractLegend): - """ - The legend widget - - The QwtLegend widget is a tabular arrangement of legend items. Legend - items might be any type of widget, but in general they will be - a QwtLegendLabel. - - .. seealso :: - - :py:class`qwt.legend.QwtLegendLabel`, - :py:class`qwt.plot.QwtPlotItem`, - :py:class`qwt.plot.QwtPlot` - - .. py:class:: QwtLegend([parent=None]) - - Constructor - - :param QWidget parent: Parent widget - - .. py:data:: clicked - - A signal which is emitted when the user has clicked on - a legend label, which is in `QwtLegendData.Clickable` mode. - - :param itemInfo: Info for the item item of the selected legend item - :param index: Index of the legend label in the list of widgets that are associated with the plot item - - .. note:: - - Clicks are disabled as default - - .. py:data:: checked - - A signal which is emitted when the user has clicked on - a legend label, which is in `QwtLegendData.Checkable` mode - - :param itemInfo: Info for the item of the selected legend label - :param index: Index of the legend label in the list of widgets that are associated with the plot item - :param on: True when the legend label is checked - - .. note:: - - Clicks are disabled as default - """ - - clicked = Signal(object, int) - checked = Signal(object, bool, int) - - def __init__(self, parent=None): - QwtAbstractLegend.__init__(self, parent) - self.setFrameStyle(QFrame.NoFrame) - self.__data = QwtLegend_PrivateData() - self.__data.view = LegendView(self) - self.__data.view.setObjectName("QwtLegendView") - self.__data.view.setFrameStyle(QFrame.NoFrame) - gridLayout = QwtDynGridLayout(self.__data.view.contentsWidget) - gridLayout.setAlignment(Qt.AlignHCenter | Qt.AlignTop) - self.__data.view.gridLayout = gridLayout - self.__data.view.contentsWidget.installEventFilter(self) - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.__data.view) - - def setMaxColumns(self, numColumns): - """ - Set the maximum number of entries in a row - - F.e when the maximum is set to 1 all items are aligned - vertically. 0 means unlimited - - :param int numColumns: Maximum number of entries in a row - - .. seealso:: - - :py:meth:`maxColumns()`, - :py:meth:`QwtDynGridLayout.setMaxColumns()` - """ - tl = self.__data.view.gridLayout - if tl is not None: - tl.setMaxColumns(numColumns) - self.updateGeometry() - - def maxColumns(self): - """ - :return: Maximum number of entries in a row - - .. seealso:: - - :py:meth:`setMaxColumns()`, - :py:meth:`QwtDynGridLayout.maxColumns()` - """ - tl = self.__data.view.gridLayout - if tl is not None: - return tl.maxColumns() - return 0 - - def setDefaultItemMode(self, mode): - """ - Set the default mode for legend labels - - Legend labels will be constructed according to the - attributes in a `QwtLegendData` object. When it doesn't - contain a value for the `QwtLegendData.ModeRole` the - label will be initialized with the default mode of the legend. - - :param int mode: Default item mode - - .. seealso:: - - :py:meth:`itemMode()`, - :py:meth:`QwtLegendData.value()`, - :py:meth:`QwtPlotItem::legendData()` - - ... note:: - - Changing the mode doesn't have any effect on existing labels. - """ - self.__data.itemMode = mode - - def defaultItemMode(self): - """ - :return: Default item mode - - .. seealso:: - - :py:meth:`setDefaultItemMode()` - """ - return self.__data.itemMode - - def contentsWidget(self): - """ - The contents widget is the only child of the viewport of - the internal `QScrollArea` and the parent widget of all legend - items. - - :return: Container widget of the legend items - """ - return self.__data.view.contentsWidget - - def horizontalScrollBar(self): - """ - :return: Horizontal scrollbar - - .. seealso:: - - :py:meth:`verticalScrollBar()` - """ - return self.__data.view.horizontalScrollBar() - - def verticalScrollBar(self): - """ - :return: Vertical scrollbar - - .. seealso:: - - :py:meth:`horizontalScrollBar()` - """ - return self.__data.view.verticalScrollBar() - - def updateLegend(self, itemInfo, data): - """ - Update the entries for an item - - :param QVariant itemInfo: Info for an item - :param list data: Default item mode - """ - widgetList = self.legendWidgets(itemInfo) - if len(widgetList) != len(data): - contentsLayout = self.__data.view.gridLayout - while len(widgetList) > len(data): - w = widgetList.pop(-1) - contentsLayout.removeWidget(w) - w.hide() - w.deleteLater() - for i in range(len(widgetList), len(data)): - widget = self.createWidget(data[i]) - if contentsLayout is not None: - contentsLayout.addWidget(widget) - if self.isVisible(): - widget.setVisible(True) - widgetList.append(widget) - if not widgetList: - self.__data.itemMap.remove(itemInfo) - else: - self.__data.itemMap.insert(itemInfo, widgetList) - self.updateTabOrder() - for i in range(len(data)): - self.updateWidget(widgetList[i], data[i]) - - def createWidget(self, data): - """ - Create a widget to be inserted into the legend - - The default implementation returns a `QwtLegendLabel`. - - :param QwtLegendData data: Attributes of the legend entry - :return: Widget representing data on the legend - - ... note:: - - updateWidget() will called soon after createWidget() - with the same attributes. - """ - label = QwtLegendLabel() - label.setItemMode(self.defaultItemMode()) - label.clicked.connect(lambda: self.itemClicked(label)) - label.checked.connect(lambda state: self.itemChecked(state, label)) - return label - - def updateWidget(self, widget, data): - """ - Update the widget - - :param QWidget widget: Usually a QwtLegendLabel - :param QwtLegendData data: Attributes to be displayed - - .. seealso:: - - :py:meth:`createWidget()` - - ... note:: - - When widget is no QwtLegendLabel updateWidget() does nothing. - """ - label = widget # TODO: cast to QwtLegendLabel! - if label is not None: - label.setData(data) - if data.value(QwtLegendData.ModeRole) is None: - label.setItemMode(self.defaultItemMode()) - - def updateTabOrder(self): - contentsLayout = self.__data.view.gridLayout - if contentsLayout is not None: - w = None - for i in range(contentsLayout.count()): - item = contentsLayout.itemAt(i) - if w is not None and item.widget(): - QWidget.setTabOrder(w, item.widget()) - w = item.widget() - - def sizeHint(self): - """Return a size hint""" - hint = self.__data.view.contentsWidget.sizeHint() - hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth()) - return hint - - def heightForWidth(self, width): - """ - :param int width: Width - :return: The preferred height, for a width. - """ - width -= 2 * self.frameWidth() - h = self.__data.view.contentsWidget.heightForWidth(width) - if h >= 0: - h += 2 * self.frameWidth() - return h - - def eventFilter(self, object_, event): - """ - Handle QEvent.ChildRemoved andQEvent.LayoutRequest events - for the contentsWidget(). - - :param QObject object: Object to be filtered - :param QEvent event: Event - :return: Forwarded to QwtAbstractLegend.eventFilter() - """ - if object_ is self.__data.view.contentsWidget: - if event.type() == QEvent.ChildRemoved: - ce = event # TODO: cast to QChildEvent - if ce.child().isWidgetType(): - w = ce.child() # TODO: cast to QWidget - self.__data.itemMap.removeWidget(w) - elif event.type() == QEvent.LayoutRequest: - self.__data.view.layoutContents() - if self.parentWidget() and self.parentWidget().layout() is None: - QApplication.postEvent( - self.parentWidget(), QEvent(QEvent.LayoutRequest) - ) - return QwtAbstractLegend.eventFilter(self, object_, event) - - def itemClicked(self, widget): - # w = self.sender() #TODO: cast to QWidget - w = widget - if w is not None: - itemInfo = self.__data.itemMap.itemInfo(w) - if itemInfo is not None: - widgetList = self.__data.itemMap.legendWidgets(itemInfo) - if w in widgetList: - index = widgetList.index(w) - self.clicked.emit(itemInfo, index) - - def itemChecked(self, on, widget): - # w = self.sender() #TODO: cast to QWidget - w = widget - if w is not None: - itemInfo = self.__data.itemMap.itemInfo(w) - if itemInfo is not None: - widgetList = self.__data.itemMap.legendWidgets(itemInfo) - if w in widgetList: - index = widgetList.index(w) - self.checked.emit(itemInfo, on, index) - - def renderLegend(self, painter, rect, fillBackground): - """ - Render the legend into a given rectangle. - - :param QPainter painter: Painter - :param QRectF rect: Bounding rectangle - :param bool fillBackground: When true, fill rect with the widget background - """ - if self.__data.itemMap.isEmpty(): - return - if fillBackground: - if self.autoFillBackground() or self.testAttribute(Qt.WA_StyledBackground): - QwtPainter.drawBackground(painter, rect, self) - legendLayout = self.__data.view.contentsWidget.layout() - if legendLayout is None: - return - margins = self.layout().contentsMargins() - layoutRect = QRect() - layoutRect.setLeft(math.ceil(rect.left()) + margins.left()) - layoutRect.setTop(math.ceil(rect.top()) + margins.top()) - layoutRect.setRight(math.ceil(rect.right()) - margins.right()) - layoutRect.setBottom(math.ceil(rect.bottom()) - margins.bottom()) - numCols = legendLayout.columnsForWidth(layoutRect.width()) - itemRects = legendLayout.layoutItems(layoutRect, numCols) - index = 0 - for i in range(legendLayout.count()): - item = legendLayout.itemAt(i) - w = item.widget() - if w is not None: - painter.save() - painter.setClipRect(itemRects[index], Qt.IntersectClip) - self.renderItem(painter, w, itemRects[index], fillBackground) - index += 1 - painter.restore() - - def renderItem(self, painter, widget, rect, fillBackground): - """ - Render a legend entry into a given rectangle. - - :param QPainter painter: Painter - :param QWidget widget: Widget representing a legend entry - :param QRectF rect: Bounding rectangle - :param bool fillBackground: When true, fill rect with the widget background - """ - if fillBackground: - if widget.autoFillBackground() or widget.testAttribute( - Qt.WA_StyledBackground - ): - QwtPainter.drawBackground(painter, rect, widget) - label = widget # TODO: cast to QwtLegendLabel - if label is not None: - icon = label.data().icon() - sz = icon.defaultSize() - mgn = label.contentsMargins() - margin = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()]) - iconRect = QRectF( - rect.x() + margin, - rect.center().y() - 0.5 * sz.height(), - sz.width(), - sz.height(), - ) - icon.render(painter, iconRect, Qt.KeepAspectRatio) - titleRect = QRectF(rect) - titleRect.setX(iconRect.right() + 2 * label.spacing()) - painter.setFont(label.font()) - painter.setPen(label.palette().color(QPalette.Text)) - label.drawText(painter, titleRect) # TODO: cast label to QwtLegendLabel - - def legendWidgets(self, itemInfo): - """ - List of widgets associated to a item - - :param QVariant itemInfo: Info about an item - """ - return self.__data.itemMap.legendWidgets(itemInfo) - - def legendWidget(self, itemInfo): - """ - First widget in the list of widgets associated to an item - - :param QVariant itemInfo: Info about an item - """ - list_ = self.__data.itemMap.legendWidgets(itemInfo) - if list_: - return list_[0] - - def itemInfo(self, widget): - """ - Find the item that is associated to a widget - - :param QWidget widget: Widget on the legend - :return: Associated item info - """ - return self.__data.itemMap.itemInfo(widget) - - def isEmpty(self): - return self.__data.itemMap.isEmpty() diff --git a/qwt/null_paintdevice.py b/qwt/null_paintdevice.py deleted file mode 100644 index 80d0877..0000000 --- a/qwt/null_paintdevice.py +++ /dev/null @@ -1,314 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtNullPaintDevice ------------------- - -.. autoclass:: QwtNullPaintDevice - :members: -""" - -import os - -from qtpy.QtCore import QObject -from qtpy.QtGui import QPaintDevice, QPaintEngine, QPainterPath - -QT_API = os.environ["QT_API"] - - -class QwtNullPaintDevice_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.mode = QwtNullPaintDevice.NormalMode - - -class QwtNullPaintDevice_PaintEngine(QPaintEngine): - def __init__(self, paintdevice): - super(QwtNullPaintDevice_PaintEngine, self).__init__(QPaintEngine.AllFeatures) - self.__paintdevice = paintdevice - - def begin(self, paintdevice): - self.setActive(True) - return True - - def end(self): - self.setActive(False) - return True - - def type(self): - return QPaintEngine.User - - def drawRects(self, rects, rectCount=None): - if rectCount is None: - rectCount = len(rects) - device = self.nullDevice() - if device is None: - return - if device.mode() != QwtNullPaintDevice.NormalMode: - try: - QPaintEngine.drawRects(self, rects, rectCount) - except TypeError: - QPaintEngine.drawRects(self, rects) - return - device.drawRects(rects, rectCount) - - def drawLines(self, lines, lineCount=None): - if lineCount is None: - lineCount = len(lines) - device = self.nullDevice() - if device is None: - return - if device.mode() != QwtNullPaintDevice.NormalMode and QT_API.startswith("pyqt"): - try: - QPaintEngine.drawLines(self, lines, lineCount) - except TypeError: - QPaintEngine.drawLines(self, lines) - return - device.drawLines(lines, lineCount) - - def drawEllipse(self, rect): - device = self.nullDevice() - if device is None: - return - if device.mode() != QwtNullPaintDevice.NormalMode: - QPaintEngine.drawEllipse(self, rect) - return - device.drawEllipse(rect) - - def drawPath(self, path): - device = self.nullDevice() - if device is None: - return - device.drawPath(path) - - def drawPoints(self, points, pointCount=None): - if pointCount is None: - pointCount = len(points) - device = self.nullDevice() - if device is None: - return - if device.mode() != QwtNullPaintDevice.NormalMode: - try: - QPaintEngine.drawPoints(self, points, pointCount) - except TypeError: - QPaintEngine.drawPoints(self, points) - return - device.drawPoints(points, pointCount) - - def drawPolygon(self, *args): - if len(args) == 3: - points, pointCount, mode = args - elif len(args) == 2: - points, mode = args - pointCount = len(points) - else: - raise TypeError("Unexpected arguments") - device = self.nullDevice() - if device is None: - return - if device.mode() == QwtNullPaintDevice.PathMode: - path = QPainterPath() - if pointCount > 0: - path.moveTo(points[0]) - for i in range(1, pointCount): - path.lineTo(points[i]) - if mode != QPaintEngine.PolylineMode: - path.closeSubpath() - device.drawPath(path) - return - device.drawPolygon(points, pointCount, mode) - - def drawPixmap(self, rect, pm, subRect): - device = self.nullDevice() - if device is None: - return - device.drawPixmap(rect, pm, subRect) - - def drawTextItem(self, pos, textItem): - device = self.nullDevice() - if device is None: - return - if device.mode() != QwtNullPaintDevice.NormalMode: - QPaintEngine.drawTextItem(self, pos, textItem) - return - device.drawTextItem(pos, textItem) - - def drawTiledPixmap(self, rect, pixmap, subRect): - device = self.nullDevice() - if device is None: - return - if device.mode() != QwtNullPaintDevice.NormalMode: - QPaintEngine.drawTiledPixmap(self, rect, pixmap, subRect) - return - device.drawTiledPixmap(rect, pixmap, subRect) - - def drawImage(self, rect, image, subRect, flags): - device = self.nullDevice() - if device is None: - return - device.drawImage(rect, image, subRect, flags) - - def updateState(self, state): - device = self.nullDevice() - if device is None: - return - device.updateState(state) - - def nullDevice(self): - if not self.isActive(): - return - return self.__paintdevice - - -class QwtNullPaintDevice(QPaintDevice): - """ - A null paint device doing nothing - - Sometimes important layout/rendering geometries are not - available or changeable from the public Qt class interface. - ( f.e hidden in the style implementation ). - - `QwtNullPaintDevice` can be used to manipulate or filter out - this information by analyzing the stream of paint primitives. - - F.e. `QwtNullPaintDevice` is used by `QwtPlotCanvas` to identify - styled backgrounds with rounded corners. - - Modes: - - * `NormalMode`: - - All vector graphic primitives are painted by - the corresponding draw methods - - * `PolygonPathMode`: - - Vector graphic primitives ( beside polygons ) are mapped to a - `QPainterPath` and are painted by `drawPath`. In `PolygonPathMode` - mode only a few draw methods are called: - - - `drawPath()` - - `drawPixmap()` - - `drawImage()` - - `drawPolygon()` - - * `PathMode`: - - Vector graphic primitives are mapped to a `QPainterPath` - and are painted by `drawPath`. In `PathMode` mode - only a few draw methods are called: - - - `drawPath()` - - `drawPixmap()` - - `drawImage()` - """ - - # enum Mode - NormalMode, PolygonPathMode, PathMode = list(range(3)) - - def __init__(self): - super(QwtNullPaintDevice, self).__init__() - self.__engine = None - self.__data = QwtNullPaintDevice_PrivateData() - - def setMode(self, mode): - """ - Set the render mode - - :param int mode: New mode - - .. seealso:: - - :py:meth:`mode()` - """ - self.__data.mode = mode - - def mode(self): - """ - :return: Render mode - - .. seealso:: - - :py:meth:`setMode()` - """ - return self.__data.mode - - def paintEngine(self): - if self.__engine is None: - self.__engine = QwtNullPaintDevice_PaintEngine(self) - return self.__engine - - def metric(self, deviceMetric): - if deviceMetric == QPaintDevice.PdmWidth: - value = self.sizeMetrics().width() - elif deviceMetric == QPaintDevice.PdmHeight: - value = self.sizeMetrics().height() - elif deviceMetric == QPaintDevice.PdmNumColors: - value = 0xFFFFFFFF - elif deviceMetric == QPaintDevice.PdmDepth: - value = 32 - elif deviceMetric in ( - QPaintDevice.PdmPhysicalDpiX, - QPaintDevice.PdmPhysicalDpiY, - QPaintDevice.PdmDpiY, - QPaintDevice.PdmDpiX, - ): - value = 72 - elif deviceMetric == QPaintDevice.PdmWidthMM: - value = round( - self.metric(QPaintDevice.PdmWidth) - * 25.4 - / self.metric(QPaintDevice.PdmDpiX) - ) - elif deviceMetric == QPaintDevice.PdmHeightMM: - value = round( - self.metric(QPaintDevice.PdmHeight) - * 25.4 - / self.metric(QPaintDevice.PdmDpiY) - ) - elif deviceMetric == QPaintDevice.PdmDevicePixelRatio: - value = 1 - elif deviceMetric == QPaintDevice.PdmDevicePixelRatioScaled: - value = 1 - else: - value = super(QwtNullPaintDevice, self).metric(deviceMetric) - return value - - def drawRects(self, rects, rectCount): - pass - - def drawLines(self, lines, lineCount): - pass - - def drawEllipse(self, rect): - pass - - def drawPath(self, path): - pass - - def drawPoints(self, points, pointCount): - pass - - def drawPolygon(self, points, pointCount, mode): - pass - - def drawPixmap(self, rect, pm, subRect): - pass - - def drawTextItem(self, pos, textItem): - pass - - def drawTiledPixmap(self, rect, pm, subRect): - pass - - def drawImage(self, rect, image, subRect, flags): - pass - - def updateState(self, state): - pass diff --git a/qwt/painter.py b/qwt/painter.py deleted file mode 100644 index 6179dfc..0000000 --- a/qwt/painter.py +++ /dev/null @@ -1,419 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPainterClass ---------------- - -.. autoclass:: QwtPainterClass - :members: -""" - -from qtpy.QtCore import QLineF, QPoint, QRect, Qt -from qtpy.QtGui import ( - QColor, - QLinearGradient, - QPaintEngine, - QPainter, - QPainterPath, - QPalette, - QPen, - QPixmap, - QRegion, -) -from qtpy.QtWidgets import ( - QApplication, - QFrame, - QStyle, - QStyleOption, - QStyleOptionFocusRect, -) - -from qwt.color_map import QwtColorMap -from qwt.scale_map import QwtScaleMap - -QWIDGETSIZE_MAX = (1 << 24) - 1 - - -def isX11GraphicsSystem(): - pm = QPixmap(1, 1) - painter = QPainter(pm) - isX11 = painter.paintEngine().type() == QPaintEngine.X11 - del painter - return isX11 - - -def qwtFillRect(widget, painter, rect, brush): - if brush.style() == Qt.TexturePattern: - painter.save() - painter.setClipRect(rect) - painter.drawTiledPixmap(rect, brush.texture(), rect.topLeft()) - painter.restore() - elif brush.gradient(): - painter.save() - painter.setClipRect(rect) - painter.fillRect(0, 0, widget.width(), widget.height(), brush) - painter.restore() - else: - painter.fillRect(rect, brush) - - -class QwtPainterClass(object): - """A collection of `QPainter` workarounds""" - - def drawFocusRect(self, *args): - if len(args) == 2: - painter, widget = args - self.drawFocusRect(painter, widget, widget.rect()) - elif len(args) == 3: - painter, widget, rect = args - opt = QStyleOptionFocusRect() - opt.initFrom(widget) - opt.rect = rect - opt.state |= QStyle.State_HasFocus - palette = widget.palette() - opt.backgroundColor = palette.color(widget.backgroundRole()) - widget.style().drawPrimitive(QStyle.PE_FrameFocusRect, opt, painter, widget) - else: - raise TypeError( - "QwtPainter.drawFocusRect() takes 2 or 3 argument" - "(s) (%s given)" % len(args) - ) - - def drawFrame( - self, - painter, - rect, - palette, - foregroundRole, - frameWidth, - midLineWidth, - frameStyle, - ): - """ - Draw a rectangular frame - - :param QPainter painter: Painter - :param QRectF rect: Frame rectangle - :param QPalette palette: Palette - :param QPalette.ColorRole foregroundRole: Palette - :param int frameWidth: Frame width - :param int midLineWidth: Used for `QFrame.Box` - :param int frameStyle: bitwise OR´ed value of `QFrame.Shape` and `QFrame.Shadow` - """ - if frameWidth <= 0 or rect.isEmpty(): - return - shadow = frameStyle & QFrame.Shadow_Mask - painter.save() - if shadow == QFrame.Plain: - outerRect = rect.adjusted(0.0, 0.0, -1.0, -1.0) - innerRect = outerRect.adjusted( - frameWidth, frameWidth, -frameWidth, -frameWidth - ) - path = QPainterPath() - path.addRect(outerRect) - path.addRect(innerRect) - painter.setPen(Qt.NoPen) - painter.setBrush(palette.color(foregroundRole)) - painter.drawPath(path) - else: - shape = frameStyle & QFrame.Shape_Mask - if shape == QFrame.Box: - outerRect = rect.adjusted(0.0, 0.0, -1.0, -1.0) - midRect1 = outerRect.adjusted( - frameWidth, frameWidth, -frameWidth, -frameWidth - ) - midRect2 = midRect1.adjusted( - midLineWidth, midLineWidth, -midLineWidth, -midLineWidth - ) - innerRect = midRect2.adjusted( - frameWidth, frameWidth, -frameWidth, -frameWidth - ) - path1 = QPainterPath() - path1.moveTo(outerRect.bottomLeft()) - path1.lineTo(outerRect.topLeft()) - path1.lineTo(outerRect.topRight()) - path1.lineTo(midRect1.topRight()) - path1.lineTo(midRect1.topLeft()) - path1.lineTo(midRect1.bottomLeft()) - path2 = QPainterPath() - path2.moveTo(outerRect.bottomLeft()) - path2.lineTo(outerRect.bottomRight()) - path2.lineTo(outerRect.topRight()) - path2.lineTo(midRect1.topRight()) - path2.lineTo(midRect1.bottomRight()) - path2.lineTo(midRect1.bottomLeft()) - path3 = QPainterPath() - path3.moveTo(midRect2.bottomLeft()) - path3.lineTo(midRect2.topLeft()) - path3.lineTo(midRect2.topRight()) - path3.lineTo(innerRect.topRight()) - path3.lineTo(innerRect.topLeft()) - path3.lineTo(innerRect.bottomLeft()) - path4 = QPainterPath() - path4.moveTo(midRect2.bottomLeft()) - path4.lineTo(midRect2.bottomRight()) - path4.lineTo(midRect2.topRight()) - path4.lineTo(innerRect.topRight()) - path4.lineTo(innerRect.bottomRight()) - path4.lineTo(innerRect.bottomLeft()) - path5 = QPainterPath() - path5.addRect(midRect1) - path5.addRect(midRect2) - painter.setPen(Qt.NoPen) - brush1 = palette.dark().color() - brush2 = palette.light().color() - if shadow == QFrame.Raised: - brush1, brush2 = brush2, brush1 - painter.setBrush(brush1) - painter.drawPath(path1) - painter.drawPath(path4) - painter.setBrush(brush2) - painter.drawPath(path2) - painter.drawPath(path3) - painter.setBrush(palette.mid()) - painter.drawPath(path5) - else: - outerRect = rect.adjusted(0.0, 0.0, -1.0, -1.0) - innerRect = outerRect.adjusted( - frameWidth - 1.0, - frameWidth - 1.0, - -(frameWidth - 1.0), - -(frameWidth - 1.0), - ) - path1 = QPainterPath() - path1.moveTo(outerRect.bottomLeft()) - path1.lineTo(outerRect.topLeft()) - path1.lineTo(outerRect.topRight()) - path1.lineTo(innerRect.topRight()) - path1.lineTo(innerRect.topLeft()) - path1.lineTo(innerRect.bottomLeft()) - path2 = QPainterPath() - path2.moveTo(outerRect.bottomLeft()) - path2.lineTo(outerRect.bottomRight()) - path2.lineTo(outerRect.topRight()) - path2.lineTo(innerRect.topRight()) - path2.lineTo(innerRect.bottomRight()) - path2.lineTo(innerRect.bottomLeft()) - painter.setPen(Qt.NoPen) - brush1 = palette.dark().color() - brush2 = palette.light().color() - if shadow == QFrame.Raised: - brush1, brush2 = brush2, brush1 - painter.setBrush(brush1) - painter.drawPath(path1) - painter.setBrush(brush2) - painter.drawPath(path2) - painter.restore() - - def drawRoundedFrame( - self, painter, rect, xRadius, yRadius, palette, lineWidth, frameStyle - ): - """ - Draw a rectangular frame with rounded borders - - :param QPainter painter: Painter - :param QRectF rect: Frame rectangle - :param float xRadius: x-radius of the ellipses defining the corners - :param float yRadius: y-radius of the ellipses defining the corners - :param QPalette palette: `QPalette.WindowText` is used for plain borders, `QPalette.Dark` and `QPalette.Light` for raised or sunken borders - :param int lineWidth: Line width - :param int frameStyle: bitwise OR´ed value of `QFrame.Shape` and `QFrame.Shadow` - """ - painter.save() - painter.setRenderHint(QPainter.Antialiasing, True) - painter.setBrush(Qt.NoBrush) - lw2 = lineWidth * 0.5 - r = rect.adjusted(lw2, lw2, -lw2, -lw2) - path = QPainterPath() - path.addRoundedRect(r, xRadius, yRadius) - Plain, Sunken, Raised = list(range(3)) - style = Plain - if (frameStyle & QFrame.Sunken) == QFrame.Sunken: - style = Sunken - if (frameStyle & QFrame.Raised) == QFrame.Raised: - style = Raised - if style != Plain and path.elementCount() == 17: - pathList = [QPainterPath() for _i in range(8)] - for i in range(4): - j = i * 4 + 1 - pathList[2 * i].moveTo(path.elementAt(j - 1).x, path.elementAt(j - 1).y) - pathList[2 * i].cubicTo( - path.elementAt(j + 0).x, - path.elementAt(j + 0).y, - path.elementAt(j + 1).x, - path.elementAt(j + 1).y, - path.elementAt(j + 2).x, - path.elementAt(j + 2).y, - ) - pathList[2 * i + 1].moveTo( - path.elementAt(j + 2).x, path.elementAt(j + 2).y - ) - pathList[2 * i + 1].lineTo( - path.elementAt(j + 3).x, path.elementAt(j + 3).y - ) - c1 = QColor(palette.color(QPalette.Dark)) - c2 = QColor(palette.color(QPalette.Light)) - if style == Raised: - c1, c2 = c2, c1 - for i in range(4): - r = pathList[2 * i].controlPointRect() - arcPen = QPen() - arcPen.setCapStyle(Qt.FlatCap) - arcPen.setWidth(lineWidth) - linePen = QPen() - linePen.setCapStyle(Qt.FlatCap) - linePen.setWidth(lineWidth) - if i == 0: - arcPen.setColor(c1) - linePen.setColor(c1) - elif i == 1: - gradient = QLinearGradient() - gradient.setStart(r.topLeft()) - gradient.setFinalStop(r.bottomRight()) - gradient.setColorAt(0.0, c1) - gradient.setColorAt(1.0, c2) - arcPen.setBrush(gradient) - linePen.setColor(c2) - elif i == 2: - arcPen.setColor(c2) - linePen.setColor(c2) - elif i == 3: - gradient = QLinearGradient() - gradient.setStart(r.bottomRight()) - gradient.setFinalStop(r.topLeft()) - gradient.setColorAt(0.0, c2) - gradient.setColorAt(1.0, c1) - arcPen.setBrush(gradient) - linePen.setColor(c1) - painter.setPen(arcPen) - painter.drawPath(pathList[2 * i]) - painter.setPen(linePen) - painter.drawPath(pathList[2 * i + 1]) - else: - pen = QPen(palette.color(QPalette.WindowText), lineWidth) - painter.setPen(pen) - painter.drawPath(path) - painter.restore() - - def drawColorBar(self, painter, colorMap, interval, scaleMap, orientation, rect): - """ - Draw a color bar into a rectangle - - :param QPainter painter: Painter - :param qwt.color_map.QwtColorMap colorMap: Color map - :param qwt.interval.QwtInterval interval: Value range - :param qwt.scalemap.QwtScaleMap scaleMap: Scale map - :param Qt.Orientation orientation: Orientation - :param QRectF rect: Target rectangle - """ - colorTable = [] - if colorMap.format() == QwtColorMap.Indexed: - colorTable = colorMap.colorTable(interval) - c = QColor() - devRect = rect.toAlignedRect() - pixmap = QPixmap(devRect.size()) - pixmap.fill(Qt.transparent) - pmPainter = QPainter(pixmap) - pmPainter.translate(-devRect.x(), -devRect.y()) - if orientation == Qt.Horizontal: - sMap = QwtScaleMap(scaleMap) - sMap.setPaintInterval(rect.left(), rect.right()) - for x in range(devRect.left(), devRect.right() + 1): - value = sMap.invTransform(x) - if colorMap.format() == QwtColorMap.RGB: - c.setRgba(colorMap.rgb(interval, value)) - else: - c = colorTable[colorMap.colorIndex(interval, value)] - pmPainter.setPen(c) - pmPainter.drawLine(QLineF(x, devRect.top(), x, devRect.bottom())) - else: - sMap = QwtScaleMap(scaleMap) - sMap.setPaintInterval(rect.bottom(), rect.top()) - for y in range(devRect.top(), devRect.bottom() + 1): - value = sMap.invTransform(y) - if colorMap.format() == QwtColorMap.RGB: - c.setRgba(colorMap.rgb(interval, value)) - else: - c = colorTable[colorMap.colorIndex(interval, value)] - pmPainter.setPen(c) - pmPainter.drawLine(QLineF(devRect.left(), y, devRect.right(), y)) - pmPainter.end() - painter.drawPixmap(devRect, pixmap) - - def fillPixmap(self, widget, pixmap, offset=None): - """ - Fill a pixmap with the content of a widget - - In Qt >= 5.0 `QPixmap.fill()` is a nop, in Qt 4.x it is buggy - for backgrounds with gradients. Thus `fillPixmap()` offers - an alternative implementation. - - :param QWidget widget: Widget - :param QPixmap pixmap: Pixmap to be filled - :param QPoint offset: Offset - - .. seealso:: - - :py:meth:`QPixmap.fill()` - """ - if offset is None: - offset = QPoint() - rect = QRect(offset, pixmap.size()) - painter = QPainter(pixmap) - painter.translate(-offset) - autoFillBrush = widget.palette().brush(widget.backgroundRole()) - if not (widget.autoFillBackground() and autoFillBrush.isOpaque()): - bg = widget.palette().brush(QPalette.Window) - qwtFillRect(widget, painter, rect, bg) - if widget.autoFillBackground(): - qwtFillRect(widget, painter, rect, autoFillBrush) - if widget.testAttribute(Qt.WA_StyledBackground): - painter.setClipRegion(QRegion(rect)) - opt = QStyleOption() - opt.initFrom(widget) - widget.style().drawPrimitive(QStyle.PE_Widget, opt, painter, widget) - - def drawBackground(self, painter, rect, widget): - """ - Fill rect with the background of a widget - - :param QPainter painter: Painter - :param QRectF rect: Rectangle to be filled - :param QWidget widget: Widget - - .. seealso:: - - :py:data:`QStyle.PE_Widget`, :py:meth:`QWidget.backgroundRole()` - """ - if widget.testAttribute(Qt.WA_StyledBackground): - opt = QStyleOption() - opt.initFrom(widget) - opt.rect = rect.toRect() - widget.style().drawPrimitive(QStyle.PE_Widget, opt, painter, widget) - else: - brush = widget.palette().brush(widget.backgroundRole()) - painter.fillRect(rect, brush) - - def backingStore(self, widget, size): - """ - :param QWidget widget: Widget, for which the backinstore is intended - :param QSize size: Size of the pixmap - :return: A pixmap that can be used as backing store - """ - pixelRatio = 1.0 - if widget and widget.windowHandle(): - pixelRatio = widget.windowHandle().devicePixelRatio() - else: - qapp = QApplication.instance() - pixelRatio = qapp.devicePixelRatio() - pm = QPixmap(size * pixelRatio) - pm.setDevicePixelRatio(pixelRatio) - return pm - - -QwtPainter = QwtPainterClass() diff --git a/qwt/painter_command.py b/qwt/painter_command.py deleted file mode 100644 index d923be9..0000000 --- a/qwt/painter_command.py +++ /dev/null @@ -1,240 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPainterCommand ------------------ - -.. autoclass:: QwtPainterCommand - :members: -""" - -import copy - -from qtpy.QtGui import QPaintEngine, QPainterPath - - -def _flag_int(flag): - """Return the integer value of a Qt enum/flag (PyQt5 and PyQt6). - - PyQt5 exposes Qt enums as plain ints (``int(flag)`` works). PyQt6 wraps - them as ``enum.Flag`` instances which are not ``int`` subclasses, so - ``int(flag)`` raises -- the value must be read from ``flag.value``. - """ - try: - return flag.value - except AttributeError: - return int(flag) - - -# Cache QPaintEngine.DirtyXxx flags as plain Python ints once at import time. -# On PyQt6, Qt enums are full ``enum.Flag`` instances and every ``flags & -# Member`` test goes through Python's ``enum.__and__`` machinery (~6 us each). -# In ``QwtPainterCommand.__init__`` below, the State branch performs twelve -# successive flag tests per painter command -- on PyQt6 alone this accounted -# for ~20 ms of the residual perf gap on the load test. Casting once to int -# and bitwise-testing against int constants brings each test back to ~50 ns. -_DIRTY_PEN = _flag_int(QPaintEngine.DirtyPen) -_DIRTY_BRUSH = _flag_int(QPaintEngine.DirtyBrush) -_DIRTY_BRUSH_ORIGIN = _flag_int(QPaintEngine.DirtyBrushOrigin) -_DIRTY_FONT = _flag_int(QPaintEngine.DirtyFont) -_DIRTY_BACKGROUND = _flag_int(QPaintEngine.DirtyBackground) -_DIRTY_TRANSFORM = _flag_int(QPaintEngine.DirtyTransform) -_DIRTY_CLIP_ENABLED = _flag_int(QPaintEngine.DirtyClipEnabled) -_DIRTY_CLIP_REGION = _flag_int(QPaintEngine.DirtyClipRegion) -_DIRTY_CLIP_PATH = _flag_int(QPaintEngine.DirtyClipPath) -_DIRTY_HINTS = _flag_int(QPaintEngine.DirtyHints) -_DIRTY_COMPOSITION_MODE = _flag_int(QPaintEngine.DirtyCompositionMode) -_DIRTY_OPACITY = _flag_int(QPaintEngine.DirtyOpacity) - - -class PixmapData(object): - def __init__(self): - self.rect = None - self.pixmap = None - self.subRect = None - - -class ImageData(object): - def __init__(self): - self.rect = None - self.image = None - self.subRect = None - self.flags = None - - -class StateData(object): - def __init__(self): - self.flags = None - self.pen = None - self.brush = None - self.brushOrigin = None - self.backgroundBrush = None - self.backgroundMode = None - self.font = None - self.matrix = None - self.transform = None - self.clipOperation = None - self.clipRegion = None - self.clipPath = None - self.isClipEnabled = None - self.renderHints = None - self.compositionMode = None - self.opacity = None - - -class QwtPainterCommand(object): - """ - `QwtPainterCommand` represents the attributes of a paint operation - how it is used between `QPainter` and `QPaintDevice` - - It is used by :py:class:`qwt.graphic.QwtGraphic` to record and replay - paint operations - - .. seealso:: - - :py:meth:`qwt.graphic.QwtGraphic.commands()` - - - .. py:class:: QwtPainterCommand() - - Construct an invalid command - - .. py:class:: QwtPainterCommand(path) - :noindex: - - Copy constructor - - :param QPainterPath path: Source - - .. py:class:: QwtPainterCommand(rect, pixmap, subRect) - :noindex: - - Constructor for Pixmap paint operation - - :param QRectF rect: Target rectangle - :param QPixmap pixmap: Pixmap - :param QRectF subRect: Rectangle inside the pixmap - - .. py:class:: QwtPainterCommand(rect, image, subRect, flags) - :noindex: - - Constructor for Image paint operation - - :param QRectF rect: Target rectangle - :param QImage image: Image - :param QRectF subRect: Rectangle inside the image - :param Qt.ImageConversionFlags flags: Conversion flags - - .. py:class:: QwtPainterCommand(state) - :noindex: - - Constructor for State paint operation - - :param QPaintEngineState state: Paint engine state - """ - - # enum Type - Invalid = -1 - Path, Pixmap, Image, State = list(range(4)) - - def __init__(self, *args): - if len(args) == 0: - self.__type = self.Invalid - elif len(args) == 1: - (arg,) = args - if isinstance(arg, QPainterPath): - path = arg - self.__type = self.Path - self.__path = QPainterPath(path) - elif isinstance(arg, QwtPainterCommand): - other = arg - self.copy(other) - else: - state = arg - self.__type = self.State - self.__stateData = StateData() - self.__stateData.flags = state.state() - # Cast to int once: subsequent bitwise tests are done against - # the cached _DIRTY_* int constants (see top of module). - flags = _flag_int(self.__stateData.flags) - if flags & _DIRTY_PEN: - self.__stateData.pen = state.pen() - if flags & _DIRTY_BRUSH: - self.__stateData.brush = state.brush() - if flags & _DIRTY_BRUSH_ORIGIN: - self.__stateData.brushOrigin = state.brushOrigin() - if flags & _DIRTY_FONT: - self.__stateData.font = state.font() - if flags & _DIRTY_BACKGROUND: - self.__stateData.backgroundMode = state.backgroundMode() - self.__stateData.backgroundBrush = state.backgroundBrush() - if flags & _DIRTY_TRANSFORM: - self.__stateData.transform = state.transform() - if flags & _DIRTY_CLIP_ENABLED: - self.__stateData.isClipEnabled = state.isClipEnabled() - if flags & _DIRTY_CLIP_REGION: - self.__stateData.clipRegion = state.clipRegion() - self.__stateData.clipOperation = state.clipOperation() - if flags & _DIRTY_CLIP_PATH: - self.__stateData.clipPath = state.clipPath() - self.__stateData.clipOperation = state.clipOperation() - if flags & _DIRTY_HINTS: - self.__stateData.renderHints = state.renderHints() - if flags & _DIRTY_COMPOSITION_MODE: - self.__stateData.compositionMode = state.compositionMode() - if flags & _DIRTY_OPACITY: - self.__stateData.opacity = state.opacity() - elif len(args) == 3: - rect, pixmap, subRect = args - self.__type = self.Pixmap - self.__pixmapData = PixmapData() - self.__pixmapData.rect = rect - self.__pixmapData.pixmap = pixmap - self.__pixmapData.subRect = subRect - elif len(args) == 4: - rect, image, subRect, flags = args - self.__type = self.Image - self.__imageData = ImageData() - self.__imageData.rect = rect - self.__imageData.image = image - self.__imageData.subRect = subRect - self.__imageData.flags = flags - else: - raise TypeError( - "%s() takes 0, 1, 3 or 4 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def copy(self, other): - self.__type = other.__type - if other.__type == self.Path: - self.__path = QPainterPath(other.__path) - elif other.__type == self.Pixmap: - self.__pixmapData = copy.deepcopy(other.__pixmapData) - elif other.__type == self.Image: - self.__imageData = copy.deepcopy(other.__imageData) - elif other.__type == self.State: - self.__stateData == copy.deepcopy(other.__stateData) - - def reset(self): - self.__type = self.Invalid - - def type(self): - return self.__type - - def path(self): - return self.__path - - def pixmapData(self): - return self.__pixmapData - - def imageData(self): - return self.__imageData - - def stateData(self): - return self.__stateData diff --git a/qwt/plot.py b/qwt/plot.py deleted file mode 100644 index ced78a4..0000000 --- a/qwt/plot.py +++ /dev/null @@ -1,2292 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlot -------- - -.. autoclass:: QwtPlot - :members: - -QwtPlotItem ------------ - -.. autoclass:: QwtPlotItem - :members: -""" - -import math - -import numpy as np -from qtpy.QtCore import QEvent, QObject, QRectF, QSize, Qt, Signal -from qtpy.QtGui import QBrush, QColor, QFont, QPainter, QPalette -from qtpy.QtWidgets import QApplication, QFrame, QSizePolicy, QWidget - -from qwt.graphic import QwtGraphic -from qwt.interval import QwtInterval -from qwt.legend import QwtLegendData -from qwt.plot_canvas import QwtPlotCanvas -from qwt.scale_div import QwtScaleDiv -from qwt.scale_draw import QwtScaleDraw -from qwt.scale_engine import QwtLinearScaleEngine -from qwt.scale_map import QwtScaleMap -from qwt.scale_widget import QwtScaleWidget -from qwt.text import QwtText, QwtTextLabel - - -def qwtSetTabOrder(first, second, with_children): - tab_chain = [first, second] - if with_children: - children = second.findChildren(QWidget) - w = second.nextInFocusChain() - while w in children: - while w in children: - children.remove(w) - tab_chain += [w] - w = w.nextInFocusChain() - for idx in range(len(tab_chain) - 1): - w_from = tab_chain[idx] - w_to = tab_chain[idx + 1] - policy1, policy2 = w_from.focusPolicy(), w_to.focusPolicy() - proxy1, proxy2 = w_from.focusProxy(), w_to.focusProxy() - for w in (w_from, w_to): - w.setFocusPolicy(Qt.TabFocus) - w.setFocusProxy(None) - QWidget.setTabOrder(w_from, w_to) - for w, pl, px in ((w_from, policy1, proxy1), (w_to, policy2, proxy2)): - w.setFocusPolicy(pl) - w.setFocusProxy(px) - - -class ItemList(list): - def sortItems(self): - self.sort(key=lambda item: item.z()) - - def insertItem(self, obj): - self.append(obj) - self.sortItems() - - def removeItem(self, obj): - self.remove(obj) - self.sortItems() - - -class QwtPlot_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.itemList = ItemList() - self.titleLabel = None - self.footerLabel = None - self.canvas = None - self.legend = None - self.layout = None - self.autoReplot = None - self.flatStyle = None - - -class AxisData(object): - def __init__(self): - self.isEnabled = None - self.doAutoScale = None - self.minValue = None - self.maxValue = None - self.stepSize = None - self.maxMajor = None - self.maxMinor = None - self.isValid = None - self.scaleDiv = None # QwtScaleDiv - self.scaleEngine = None # QwtScaleEngine - self.scaleWidget = None # QwtScaleWidget - self.margin = None # Margin (float) in % - - -class QwtPlot(QFrame): - """ - A 2-D plotting widget - - QwtPlot is a widget for plotting two-dimensional graphs. - An unlimited number of plot items can be displayed on its canvas. - Plot items might be curves (:py:class:`qwt.plot_curve.QwtPlotCurve`), - markers (:py:class:`qwt.plot_marker.QwtPlotMarker`), - the grid (:py:class:`qwt.plot_grid.QwtPlotGrid`), or anything else - derived from :py:class:`QwtPlotItem`. - - A plot can have up to four axes, with each plot item attached to an x- and - a y axis. The scales at the axes can be explicitly set (`QwtScaleDiv`), or - are calculated from the plot items, using algorithms (`QwtScaleEngine`) - which can be configured separately for each axis. - - The following example is a good starting point to see how to set up a - plot widget:: - - from qtpy import QtWidgets as QW - import qwt - import numpy as np - - app = QW.QApplication([]) - x = np.linspace(-10, 10, 500) - plot = qwt.QwtPlot("Trigonometric functions") - plot.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.BottomLegend) - qwt.QwtPlotCurve.make(x, np.cos(x), "Cosine", plot, linecolor="red", antialiased=True) - qwt.QwtPlotCurve.make(x, np.sin(x), "Sine", plot, linecolor="blue", antialiased=True) - plot.resize(600, 300) - plot.show() - - .. image:: /_static/QwtPlot_example.png - - .. py:class:: QwtPlot([title=""], [parent=None]) - - :param str title: Title text - :param QWidget parent: Parent widget - - .. py:data:: itemAttached - - A signal indicating, that an item has been attached/detached - - :param plotItem: Plot item - :param on: Attached/Detached - - .. py:data:: legendDataChanged - - A signal with the attributes how to update - the legend entries for a plot item. - - :param itemInfo: Info about a plot item, build from itemToInfo() - :param data: Attributes of the entries (usually <= 1) for the plot item. - - """ - - itemAttached = Signal(object, bool) - legendDataChanged = Signal(object, object) - - # enum Axis - AXES = yLeft, yRight, xBottom, xTop = list(range(4)) - axisCnt = len(AXES) # Not necessary but ensure compatibility with PyQwt - - # enum LegendPosition - LeftLegend, RightLegend, BottomLegend, TopLegend = list(range(4)) - - def __init__(self, *args): - if len(args) == 0: - title, parent = "", None - elif len(args) == 1: - if isinstance(args[0], QWidget) or args[0] is None: - title = "" - (parent,) = args - else: - (title,) = args - parent = None - elif len(args) == 2: - title, parent = args - else: - raise TypeError( - "%s() takes 0, 1 or 2 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - QFrame.__init__(self, parent) - - self.__layout_state = None - - self.__data = QwtPlot_PrivateData() - from qwt.plot_layout import QwtPlotLayout - - self.__data.layout = QwtPlotLayout() - self.__data.autoReplot = False - - self.setAutoReplot(False) - self.setPlotLayout(self.__data.layout) - - # title - self.__data.titleLabel = QwtTextLabel(self) - self.__data.titleLabel.setObjectName("QwtPlotTitle") - text = QwtText(title) - text.setRenderFlags(Qt.AlignCenter | Qt.TextWordWrap) - self.__data.titleLabel.setText(text) - - # footer - self.__data.footerLabel = QwtTextLabel(self) - self.__data.footerLabel.setObjectName("QwtPlotFooter") - footer = QwtText() - footer.setRenderFlags(Qt.AlignCenter | Qt.TextWordWrap) - self.__data.footerLabel.setText(footer) - - # legend - self.__data.legend = None - - # axis - self.__axisData = [] - self.initAxesData() - - # canvas - self.__data.canvas = QwtPlotCanvas(self) - self.__data.canvas.setObjectName("QwtPlotCanvas") - self.__data.canvas.installEventFilter(self) - - # plot style - self.setFlatStyle(True) - - self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) - - focusChain = [ - self, - self.__data.titleLabel, - self.axisWidget(self.xTop), - self.axisWidget(self.yLeft), - self.__data.canvas, - self.axisWidget(self.yRight), - self.axisWidget(self.xBottom), - self.__data.footerLabel, - ] - - for idx in range(len(focusChain) - 1): - qwtSetTabOrder(focusChain[idx], focusChain[idx + 1], False) - - self.legendDataChanged.connect(self.updateLegendItems) - - def insertItem(self, item): - """ - Insert a plot item - - :param qwt.plot.QwtPlotItem item: PlotItem - - .. seealso:: - - :py:meth:`removeItem()` - - .. note:: - - This was a member of QwtPlotDict in older versions. - """ - self.__data.itemList.insertItem(item) - - def removeItem(self, item): - """ - Remove a plot item - - :param qwt.plot.QwtPlotItem item: PlotItem - - .. seealso:: - - :py:meth:`insertItem()` - - .. note:: - - This was a member of QwtPlotDict in older versions. - """ - self.__data.itemList.removeItem(item) - - def detachItems(self, rtti=None): - """ - Detach items from the dictionary - - :param rtti: In case of `QwtPlotItem.Rtti_PlotItem` or None (default) detach all items otherwise only those items of the type rtti. - :type rtti: int or None - - .. note:: - - This was a member of QwtPlotDict in older versions. - """ - for item in self.__data.itemList[:]: - if rtti in (None, QwtPlotItem.Rtti_PlotItem) or item.rtti() == rtti: - item.attach(None) - - def itemList(self, rtti=None): - """ - A list of attached plot items. - - Use caution when iterating these lists, as removing/detaching an - item will invalidate the iterator. Instead you can place pointers - to objects to be removed in a removal list, and traverse that list - later. - - :param int rtti: In case of `QwtPlotItem.Rtti_PlotItem` detach all items otherwise only those items of the type rtti. - :return: List of all attached plot items of a specific type. If rtti is None, return a list of all attached plot items. - - .. note:: - - This was a member of QwtPlotDict in older versions. - """ - if rtti is None or rtti == QwtPlotItem.Rtti_PlotItem: - return self.__data.itemList - return [item for item in self.__data.itemList if item.rtti() == rtti] - - def setFlatStyle(self, state): - """ - Set or reset the flatStyle option - - If the flatStyle option is set, the plot will be - rendered without any margin (scales, canvas, layout). - - Enabling this option makes the plot look flat and compact. - - The flatStyle option is set to True by default. - - :param bool state: True or False. - - .. seealso:: - - :py:meth:`flatStyle()` - """ - - def make_font(family=None, size=None, delta_size=None, weight=None): - finfo = self.fontInfo() - family = finfo.family() if family is None else family - weight = -1 if weight is None else weight - size = size if delta_size is None else finfo.pointSize() + delta_size - return QFont(family, size, weight) - - if state: - # New PythonQwt-exclusive flat style - plot_title_font = make_font(size=12) - axis_title_font = make_font(size=11) - axis_label_font = make_font(size=10) - tick_lighter_factors = (150, 125, 100) - scale_margin = scale_spacing = 0 - canvas_frame_style = QFrame.NoFrame - plot_layout_canvas_margin = plot_layout_spacing = 0 - ticks_color = Qt.darkGray - labels_color = "#444444" - else: - # Old PyQwt / Qwt style - plot_title_font = make_font(size=14, weight=QFont.Bold) - axis_title_font = make_font(size=12, weight=QFont.Bold) - axis_label_font = make_font(size=10) - tick_lighter_factors = (100, 100, 100) - scale_margin = scale_spacing = 2 - canvas_frame_style = QFrame.Panel | QFrame.Sunken - plot_layout_canvas_margin = 4 - plot_layout_spacing = 5 - ticks_color = labels_color = Qt.black - self.canvas().setFrameStyle(canvas_frame_style) - self.plotLayout().setCanvasMargin(plot_layout_canvas_margin) - self.plotLayout().setSpacing(plot_layout_spacing) - palette = self.palette() - palette.setColor(QPalette.WindowText, QColor(ticks_color)) - palette.setColor(QPalette.Text, QColor(labels_color)) - self.setPalette(palette) - for axis_id in self.AXES: - scale_widget = self.axisWidget(axis_id) - scale_draw = self.axisScaleDraw(axis_id) - scale_widget.setFont(axis_label_font) - scale_widget.setMargin(scale_margin) - scale_widget.setSpacing(scale_spacing) - scale_title = scale_widget.title() - scale_title.setFont(axis_title_font) - scale_widget.setTitle(scale_title) - for tick_type, factor in enumerate(tick_lighter_factors): - scale_draw.setTickLighterFactor(tick_type, factor) - plot_title = self.title() - plot_title.setFont(plot_title_font) - self.setTitle(plot_title) - self.__data.flatStyle = state - - def flatStyle(self): - """ - :return: True if the flatStyle option is set. - - .. seealso:: - - :py:meth:`setFlatStyle()` - """ - return self.__data.flatStyle - - def initAxesData(self): - """Initialize axes""" - self.__axisData = [AxisData() for axisId in self.AXES] - - self.__axisData[self.yLeft].scaleWidget = QwtScaleWidget( - QwtScaleDraw.LeftScale, self - ) - self.__axisData[self.yRight].scaleWidget = QwtScaleWidget( - QwtScaleDraw.RightScale, self - ) - self.__axisData[self.xTop].scaleWidget = QwtScaleWidget( - QwtScaleDraw.TopScale, self - ) - self.__axisData[self.xBottom].scaleWidget = QwtScaleWidget( - QwtScaleDraw.BottomScale, self - ) - - self.__axisData[self.yLeft].scaleWidget.setObjectName("QwtPlotAxisYLeft") - self.__axisData[self.yRight].scaleWidget.setObjectName("QwtPlotAxisYRight") - self.__axisData[self.xTop].scaleWidget.setObjectName("QwtPlotAxisXTop") - self.__axisData[self.xBottom].scaleWidget.setObjectName("QwtPlotAxisXBottom") - - for axisId in self.AXES: - d = self.__axisData[axisId] - - d.scaleEngine = QwtLinearScaleEngine() - - d.scaleWidget.setTransformation(d.scaleEngine.transformation()) - d.scaleWidget.setMargin(2) - - text = d.scaleWidget.title() - d.scaleWidget.setTitle(text) - - d.doAutoScale = True - d.margin = 0.05 - d.minValue = 0.0 - d.maxValue = 1000.0 - d.stepSize = 0.0 - d.maxMinor = 5 - d.maxMajor = 8 - d.isValid = False - - self.__axisData[self.yLeft].isEnabled = True - self.__axisData[self.yRight].isEnabled = False - self.__axisData[self.xBottom].isEnabled = True - self.__axisData[self.xTop].isEnabled = False - - def deleteAxesData(self): - # XXX Is is really necessary in Python? (pure transcription of C++) - for axisId in self.AXES: - self.__axisData[axisId].scaleEngine = None - self.__axisData[axisId] = None - - def axisWidget(self, axisId): - """ - :param int axisId: Axis index - :return: Scale widget of the specified axis, or None if axisId is invalid. - """ - if self.axisValid(axisId): - return self.__axisData[axisId].scaleWidget - - def setAxisScaleEngine(self, axisId, scaleEngine): - """ - Change the scale engine for an axis - - :param int axisId: Axis index - :param qwt.scale_engine.QwtScaleEngine scaleEngine: Scale engine - - .. seealso:: - - :py:meth:`axisScaleEngine()` - """ - if self.axisValid(axisId) and scaleEngine is not None: - d = self.__axisData[axisId] - d.scaleEngine = scaleEngine - self.__axisData[axisId].scaleWidget.setTransformation( - scaleEngine.transformation() - ) - d.isValid = False - self.autoRefresh() - - def axisScaleEngine(self, axisId): - """ - :param int axisId: Axis index - :return: Scale engine for a specific axis - - .. seealso:: - - :py:meth:`setAxisScaleEngine()` - """ - if self.axisValid(axisId): - return self.__axisData[axisId].scaleEngine - - def axisAutoScale(self, axisId): - """ - :param int axisId: Axis index - :return: True, if autoscaling is enabled - """ - if self.axisValid(axisId): - return self.__axisData[axisId].doAutoScale - - def axisEnabled(self, axisId): - """ - :param int axisId: Axis index - :return: True, if a specified axis is enabled - """ - if self.axisValid(axisId): - return self.__axisData[axisId].isEnabled - - def axisFont(self, axisId): - """ - :param int axisId: Axis index - :return: The font of the scale labels for a specified axis - """ - if self.axisValid(axisId): - return self.axisWidget(axisId).font() - else: - return QFont() - - def axisMaxMajor(self, axisId): - """ - :param int axisId: Axis index - :return: The maximum number of major ticks for a specified axis - - .. seealso:: - - :py:meth:`setAxisMaxMajor()`, - :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` - """ - if self.axisValid(axisId): - return self.__axisData[axisId].maxMajor - else: - return 0 - - def axisMaxMinor(self, axisId): - """ - :param int axisId: Axis index - :return: The maximum number of minor ticks for a specified axis - - .. seealso:: - - :py:meth:`setAxisMaxMinor()`, - :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` - """ - if self.axisValid(axisId): - return self.__axisData[axisId].maxMinor - else: - return 0 - - def axisScaleDiv(self, axisId): - """ - :param int axisId: Axis index - :return: The scale division of a specified axis - - axisScaleDiv(axisId).lowerBound(), axisScaleDiv(axisId).upperBound() - are the current limits of the axis scale. - - .. seealso:: - - :py:class:`qwt.scale_div.QwtScaleDiv`, - :py:meth:`setAxisScaleDiv()`, - :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` - """ - return self.__axisData[axisId].scaleDiv - - def axisScaleDraw(self, axisId): - """ - :param int axisId: Axis index - :return: Specified scaleDraw for axis, or NULL if axis is invalid. - """ - if self.axisValid(axisId): - return self.axisWidget(axisId).scaleDraw() - - def axisStepSize(self, axisId): - """ - :param int axisId: Axis index - :return: step size parameter value - - This doesn't need to be the step size of the current scale. - - .. seealso:: - - :py:meth:`setAxisScale()`, - :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` - """ - if self.axisValid(axisId): - return self.__axisData[axisId].stepSize - else: - return 0 - - def axisMargin(self, axisId): - """ - :param int axisId: Axis index - :return: Relative margin of the axis, as a fraction of the full axis range - - .. seealso:: - - :py:meth:`setAxisMargin()` - """ - if self.axisValid(axisId): - return self.__axisData[axisId].margin - return 0.0 - - def axisInterval(self, axisId): - """ - :param int axisId: Axis index - :return: The current interval of the specified axis - - This is only a convenience function for axisScaleDiv(axisId).interval() - - .. seealso:: - - :py:class:`qwt.scale_div.QwtScaleDiv`, - :py:meth:`axisScaleDiv()` - """ - if self.axisValid(axisId): - return self.axisScaleDiv(axisId).interval() - else: - return QwtInterval() - - def axisTitle(self, axisId): - """ - :param int axisId: Axis index - :return: Title of a specified axis - """ - if self.axisValid(axisId): - return self.axisWidget(axisId).title() - else: - return QwtText() - - def enableAxis(self, axisId, tf=True): - """ - Enable or disable a specified axis - - When an axis is disabled, this only means that it is not - visible on the screen. Curves, markers and can be attached - to disabled axes, and transformation of screen coordinates - into values works as normal. - - Only xBottom and yLeft are enabled by default. - - :param int axisId: Axis index - :param bool tf: True (enabled) or False (disabled) - """ - if self.axisValid(axisId) and tf != self.__axisData[axisId].isEnabled: - self.__axisData[axisId].isEnabled = tf - self.updateLayout() - - def invTransform(self, axisId, pos): - """ - Transform the x or y coordinate of a position in the - drawing region into a value. - - :param int axisId: Axis index - :param int pos: position - - .. warning:: - - The position can be an x or a y coordinate, - depending on the specified axis. - """ - if self.axisValid(axisId): - return self.canvasMap(axisId).invTransform(pos) - else: - return 0.0 - - def transform(self, axisId, value): - """ - Transform a value into a coordinate in the plotting region - - :param int axisId: Axis index - :param fload value: Value - :return: X or Y coordinate in the plotting region corresponding to the value. - """ - if self.axisValid(axisId): - return self.canvasMap(axisId).transform(value) - else: - return 0.0 - - def setAxisFont(self, axisId, font): - """ - Change the font of an axis - - :param int axisId: Axis index - :param QFont font: Font - - .. warning:: - - This function changes the font of the tick labels, - not of the axis title. - """ - if self.axisValid(axisId): - return self.axisWidget(axisId).setFont(font) - - def setAxisAutoScale(self, axisId, on=True): - """ - Enable autoscaling for a specified axis - - This member function is used to switch back to autoscaling mode - after a fixed scale has been set. Autoscaling is enabled by default. - - :param int axisId: Axis index - :param bool on: On/Off - - .. seealso:: - - :py:meth:`setAxisScale()`, :py:meth:`setAxisScaleDiv()`, - :py:meth:`updateAxes()` - - .. note:: - - The autoscaling flag has no effect until updateAxes() is executed - ( called by replot() ). - """ - if self.axisValid(axisId) and self.__axisData[axisId].doAutoScale != on: - self.__axisData[axisId].doAutoScale = on - self.autoRefresh() - - def setAxisScale(self, axisId, min_, max_, stepSize=0): - """ - Disable autoscaling and specify a fixed scale for a selected axis. - - In updateAxes() the scale engine calculates a scale division from the - specified parameters, that will be assigned to the scale widget. So - updates of the scale widget usually happen delayed with the next replot. - - :param int axisId: Axis index - :param float min_: Minimum of the scale - :param float max_: Maximum of the scale - :param float stepSize: Major step size. If step == 0, the step size is calculated automatically using the maxMajor setting. - - .. seealso:: - - :py:meth:`setAxisMaxMajor()`, :py:meth:`setAxisAutoScale()`, - :py:meth:`axisStepSize()`, - :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` - """ - if self.axisValid(axisId): - d = self.__axisData[axisId] - d.doAutoScale = False - d.isValid = False - d.minValue = min_ - d.maxValue = max_ - d.stepSize = stepSize - self.autoRefresh() - - def setAxisScaleDiv(self, axisId, scaleDiv): - """ - Disable autoscaling and specify a fixed scale for a selected axis. - - The scale division will be stored locally only until the next call - of updateAxes(). So updates of the scale widget usually happen delayed with - the next replot. - - :param int axisId: Axis index - :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division - - .. seealso:: - - :py:meth:`setAxisScale()`, :py:meth:`setAxisAutoScale()` - """ - if self.axisValid(axisId): - d = self.__axisData[axisId] - d.doAutoScale = False - d.scaleDiv = scaleDiv - d.isValid = True - self.autoRefresh() - - def setAxisScaleDraw(self, axisId, scaleDraw): - """ - Set a scale draw - - :param int axisId: Axis index - :param qwt.scale_draw.QwtScaleDraw scaleDraw: Object responsible for drawing scales. - - By passing scaleDraw it is possible to extend QwtScaleDraw - functionality and let it take place in QwtPlot. Please note - that scaleDraw has to be created with new and will be deleted - by the corresponding QwtScale member ( like a child object ). - - .. seealso:: - - :py:class:`qwt.scale_draw.QwtScaleDraw`, - :py:class:`qwt.scale_widget.QwtScaleWigdet` - - .. warning:: - - The attributes of scaleDraw will be overwritten by those of the - previous QwtScaleDraw. - """ - if self.axisValid(axisId): - self.axisWidget(axisId).setScaleDraw(scaleDraw) - self.autoRefresh() - - def setAxisLabelAlignment(self, axisId, alignment): - """ - Change the alignment of the tick labels - - :param int axisId: Axis index - :param Qt.Alignment alignment: Or'd Qt.AlignmentFlags - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAlignment()` - """ - if self.axisValid(axisId): - self.axisWidget(axisId).setLabelAlignment(alignment) - - def setAxisLabelRotation(self, axisId, rotation): - """ - Rotate all tick labels - - :param int axisId: Axis index - :param float rotation: Angle in degrees. When changing the label rotation, the label alignment might be adjusted too. - - .. seealso:: - - :py:meth:`setLabelRotation()`, :py:meth:`setAxisLabelAlignment()` - """ - if self.axisValid(axisId): - self.axisWidget(axisId).setLabelRotation(rotation) - - def setAxisLabelAutoSize(self, axisId, state): - """ - Set tick labels automatic size option (default: on) - - :param int axisId: Axis index - :param bool state: On/off - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAutoSize()` - """ - if self.axisValid(axisId): - self.axisWidget(axisId).setLabelAutoSize(state) - - def setAxisMaxMinor(self, axisId, maxMinor): - """ - Set the maximum number of minor scale intervals for a specified axis - - :param int axisId: Axis index - :param int maxMinor: Maximum number of minor steps - - .. seealso:: - - :py:meth:`axisMaxMinor()` - """ - if self.axisValid(axisId): - maxMinor = max([0, min([maxMinor, 100])]) - d = self.__axisData[axisId] - if maxMinor != d.maxMinor: - d.maxMinor = maxMinor - d.isValid = False - self.autoRefresh() - - def setAxisMaxMajor(self, axisId, maxMajor): - """ - Set the maximum number of major scale intervals for a specified axis - - :param int axisId: Axis index - :param int maxMajor: Maximum number of major steps - - .. seealso:: - - :py:meth:`axisMaxMajor()` - """ - if self.axisValid(axisId): - maxMajor = max([1, min([maxMajor, 10000])]) - d = self.__axisData[axisId] - if maxMajor != d.maxMajor: - d.maxMajor = maxMajor - d.isValid = False - self.autoRefresh() - - def setAxisMargin(self, axisId, margin): - """ - Set the relative margin of the axis, as a fraction of the full axis range - - :param int axisId: Axis index - :param float margin: Relative margin (float between 0 and 1) - - .. seealso:: - - :py:meth:`axisMargin()` - """ - if not isinstance(margin, float) or margin < 0.0 or margin > 1.0: - raise ValueError("margin must be a float between 0 and 1") - if self.axisValid(axisId): - d = self.__axisData[axisId] - if margin != d.margin: - d.margin = margin - d.isValid = False - self.autoRefresh() - - def setAxisTitle(self, axisId, title): - """ - Change the title of a specified axis - - :param int axisId: Axis index - :param title: axis title - :type title: qwt.text.QwtText or str - """ - if self.axisValid(axisId): - self.axisWidget(axisId).setTitle(title) - self.updateLayout() - - def updateAxes(self): - """ - Rebuild the axes scales - - In case of autoscaling the boundaries of a scale are calculated - from the bounding rectangles of all plot items, having the - `QwtPlotItem.AutoScale` flag enabled (`QwtScaleEngine.autoScale()`). - Then a scale division is calculated (`QwtScaleEngine.didvideScale()`) - and assigned to scale widget. - - When the scale boundaries have been assigned with `setAxisScale()` a - scale division is calculated (`QwtScaleEngine.didvideScale()`) - for this interval and assigned to the scale widget. - - When the scale has been set explicitly by `setAxisScaleDiv()` the - locally stored scale division gets assigned to the scale widget. - - The scale widget indicates modifications by emitting a - `QwtScaleWidget.scaleDivChanged()` signal. - - `updateAxes()` is usually called by `replot()`. - - .. seealso:: - - :py:meth:`setAxisAutoScale()`, :py:meth:`setAxisScale()`, - :py:meth:`setAxisScaleDiv()`, :py:meth:`replot()`, - :py:meth:`QwtPlotItem.boundingRect()` - """ - intv = [QwtInterval() for _i in self.AXES] - itmList = self.itemList() - for item in itmList: - if not item.testItemAttribute(QwtPlotItem.AutoScale): - continue - if not item.isVisible(): - continue - if self.axisAutoScale(item.xAxis()) or self.axisAutoScale(item.yAxis()): - rect = item.boundingRect() - if rect.width() >= 0.0: - intv[item.xAxis()] |= QwtInterval(rect.left(), rect.right()) - if rect.height() >= 0.0: - intv[item.yAxis()] |= QwtInterval(rect.top(), rect.bottom()) - - for axisId in self.AXES: - d = self.__axisData[axisId] - minValue = d.minValue - maxValue = d.maxValue - stepSize = d.stepSize - if d.doAutoScale and intv[axisId].isValid(): - d.isValid = False - minValue = intv[axisId].minValue() - maxValue = intv[axisId].maxValue() - minValue, maxValue, stepSize = d.scaleEngine.autoScale( - d.maxMajor, minValue, maxValue, stepSize, d.margin - ) - if not d.isValid: - d.scaleDiv = d.scaleEngine.divideScale( - minValue, maxValue, d.maxMajor, d.maxMinor, stepSize - ) - d.isValid = True - scaleWidget = self.axisWidget(axisId) - scaleWidget.setScaleDiv(d.scaleDiv) - - # It is *really* necessary to update border dist! - # Otherwise, when tick labels are large enough, the ticks - # may not be aligned with canvas grid. - # See the following issues for more details: - # https://github.com/PlotPyStack/guiqwt/issues/57 - # https://github.com/PlotPyStack/PythonQwt/issues/30 - startDist, endDist = scaleWidget.getBorderDistHint() - scaleWidget.setBorderDist(startDist, endDist) - - for item in itmList: - if item.testItemInterest(QwtPlotItem.ScaleInterest): - item.updateScaleDiv( - self.axisScaleDiv(item.xAxis()), self.axisScaleDiv(item.yAxis()) - ) - - def setCanvas(self, canvas): - """ - Set the drawing canvas of the plot widget. - - The default canvas is a `QwtPlotCanvas`. - - :param QWidget canvas: Canvas Widget - - .. seealso:: - - :py:meth:`canvas()` - """ - if canvas == self.__data.canvas: - return - self.__data.canvas = canvas - if canvas is not None: - canvas.setParent(self) - canvas.installEventFilter(self) - if self.isVisible(): - canvas.show() - - def event(self, event): - if event.type() == QEvent.LayoutRequest: - self.updateLayout() - elif event.type() == QEvent.PolishRequest: - self.replot() - return QFrame.event(self, event) - - def eventFilter(self, obj, event): - if obj is self.__data.canvas: - if event.type() == QEvent.Resize: - self.updateCanvasMargins() - elif event.type() == 178: # QEvent.ContentsRectChange: - self.updateLayout() - return QFrame.eventFilter(self, obj, event) - - def autoRefresh(self): - """Replots the plot if :py:meth:`autoReplot()` is True.""" - if self.__data.autoReplot: - self.replot() - - def setAutoReplot(self, tf=True): - """ - Set or reset the autoReplot option - - If the autoReplot option is set, the plot will be - updated implicitly by manipulating member functions. - Since this may be time-consuming, it is recommended - to leave this option switched off and call :py:meth:`replot()` - explicitly if necessary. - - The autoReplot option is set to false by default, which - means that the user has to call :py:meth:`replot()` in order - to make changes visible. - - :param bool tf: True or False. Defaults to True. - - .. seealso:: - - :py:meth:`autoReplot()` - """ - self.__data.autoReplot = tf - - def autoReplot(self): - """ - :return: True if the autoReplot option is set. - - .. seealso:: - - :py:meth:`setAutoReplot()` - """ - return self.__data.autoReplot - - def setTitle(self, title): - """ - Change the plot's title - - :param title: New title - :type title: str or qwt.text.QwtText - - .. seealso:: - - :py:meth:`title()` - """ - current_title = self.__data.titleLabel.text() - if isinstance(title, QwtText) and current_title == title: - return - elif not isinstance(title, QwtText) and current_title.text() == title: - return - self.__data.titleLabel.setText(title) - self.updateLayout() - - def title(self): - """ - :return: Title of the plot - - .. seealso:: - - :py:meth:`setTitle()` - """ - return self.__data.titleLabel.text() - - def titleLabel(self): - """ - :return: Title label widget. - """ - return self.__data.titleLabel - - def setFooter(self, text): - """ - Change the text the footer - - :param text: New text of the footer - :type text: str or qwt.text.QwtText - - .. seealso:: - - :py:meth:`footer()` - """ - current_footer = self.__data.footerLabel.text() - if isinstance(text, QwtText) and current_footer == text: - return - elif not isinstance(text, QwtText) and current_footer.text() == text: - return - self.__data.footerLabel.setText(text) - self.updateLayout() - - def footer(self): - """ - :return: Text of the footer - - .. seealso:: - - :py:meth:`setFooter()` - """ - return self.__data.footerLabel.text() - - def footerLabel(self): - """ - :return: Footer label widget. - """ - return self.__data.footerLabel - - def setPlotLayout(self, layout): - """ - Assign a new plot layout - - :param layout: Layout - :type layout: qwt.plot_layout.QwtPlotLayout - - .. seealso:: - - :py:meth:`plotLayout()` - """ - if layout != self.__data.layout: - self.__data.layout = layout - self.updateLayout() - - def plotLayout(self): - """ - :return: the plot's layout - - .. seealso:: - - :py:meth:`setPlotLayout()` - """ - return self.__data.layout - - def legend(self): - """ - :return: the plot's legend - - .. seealso:: - - :py:meth:`insertLegend()` - """ - return self.__data.legend - - def canvas(self): - """ - :return: the plot's canvas - """ - return self.__data.canvas - - def sizeHint(self): - """ - :return: Size hint for the plot widget - - .. seealso:: - - :py:meth:`minimumSizeHint()` - """ - dw = dh = 0 - for axisId in self.AXES: - if self.axisEnabled(axisId): - niceDist = 40 - scaleWidget = self.axisWidget(axisId) - scaleDiv = scaleWidget.scaleDraw().scaleDiv() - majCnt = len(scaleDiv.ticks(QwtScaleDiv.MajorTick)) - if axisId in (self.yLeft, self.yRight): - hDiff = ( - majCnt - 1 - ) * niceDist - scaleWidget.minimumSizeHint().height() - if hDiff > dh: - dh = hDiff - else: - wDiff = ( - majCnt - 1 - ) * niceDist - scaleWidget.minimumSizeHint().width() - if wDiff > dw: - dw = wDiff - return self.minimumSizeHint() + QSize(dw, dh) - - def minimumSizeHint(self): - """ - :return: Return a minimum size hint - """ - hint = self.__data.layout.minimumSizeHint(self) - hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth()) - return hint - - def resizeEvent(self, e): - QFrame.resizeEvent(self, e) - self.updateLayout() - - def replot(self): - """ - Redraw the plot - - If the `autoReplot` option is not set (which is the default) - or if any curves are attached to raw data, the plot has to - be refreshed explicitly in order to make changes visible. - - .. seealso:: - - :py:meth:`updateAxes()`, :py:meth:`setAutoReplot()` - """ - doAutoReplot = self.autoReplot() - self.setAutoReplot(False) - self.updateAxes() - - # Maybe the layout needs to be updated, because of changed - # axes labels. We need to process them here before painting - # to avoid that scales and canvas get out of sync. - QApplication.sendPostedEvents(self, QEvent.LayoutRequest) - - if self.__data.canvas: - try: - self.__data.canvas.replot() - except (AttributeError, TypeError): - self.__data.canvas.update(self.__data.canvas.contentsRect()) - - self.setAutoReplot(doAutoReplot) - - def get_layout_state(self): - return ( - self.contentsRect(), - self.__data.titleLabel.text(), - self.__data.footerLabel.text(), - [ - (self.axisEnabled(axisId), self.axisTitle(axisId).text()) - for axisId in self.AXES - ], - self.__data.legend, - ) - - def updateLayout(self): - """ - Adjust plot content to its current size. - - .. seealso:: - - :py:meth:`resizeEvent()` - """ - # state = self.get_layout_state() - # if self.__layout_state is not None and\ - # state == self.__layout_state: - # return - # self.__layout_state = state - - self.__data.layout.activate(self, self.contentsRect()) - - titleRect = self.__data.layout.titleRect().toRect() - footerRect = self.__data.layout.footerRect().toRect() - scaleRect = [ - self.__data.layout.scaleRect(axisId).toRect() for axisId in self.AXES - ] - legendRect = self.__data.layout.legendRect().toRect() - canvasRect = self.__data.layout.canvasRect().toRect() - - if self.__data.titleLabel.text(): - self.__data.titleLabel.setGeometry(titleRect) - if not self.__data.titleLabel.isVisibleTo(self): - self.__data.titleLabel.show() - else: - self.__data.titleLabel.hide() - - if self.__data.footerLabel.text(): - self.__data.footerLabel.setGeometry(footerRect) - if not self.__data.footerLabel.isVisibleTo(self): - self.__data.footerLabel.show() - else: - self.__data.footerLabel.hide() - - for axisId in self.AXES: - scaleWidget = self.axisWidget(axisId) - if self.axisEnabled(axisId): - if scaleRect[axisId] != scaleWidget.geometry(): - scaleWidget.setGeometry(scaleRect[axisId]) - startDist, endDist = scaleWidget.getBorderDistHint() - scaleWidget.setBorderDist(startDist, endDist) - - # ------------------------------------------------------------- - # XXX: The following was commented to fix issue #35 - # Note: the same code part in Qwt's original source code is - # annotated with the mention "do we need this code any - # longer ???"... I guess not :) - # if axisId in (self.xBottom, self.xTop): - # r = QRegion(scaleRect[axisId]) - # if self.axisEnabled(self.yLeft): - # r = r.subtracted(QRegion(scaleRect[self.yLeft])) - # if self.axisEnabled(self.yRight): - # r = r.subtracted(QRegion(scaleRect[self.yRight])) - # r.translate(-scaleRect[axisId].x(), -scaleRect[axisId].y()) - # scaleWidget.setMask(r) - # ------------------------------------------------------------- - - if not scaleWidget.isVisibleTo(self): - scaleWidget.show() - else: - scaleWidget.hide() - - if self.__data.legend: - if self.__data.legend.isEmpty(): - self.__data.legend.hide() - else: - self.__data.legend.setGeometry(legendRect) - self.__data.legend.show() - - self.__data.canvas.setGeometry(canvasRect) - - def getCanvasMarginsHint(self, maps, canvasRect): - """ - Calculate the canvas margins - - :param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates - :param QRectF canvasRect: Bounding rectangle where to paint - - Plot items might indicate, that they need some extra space - at the borders of the canvas by the `QwtPlotItem.Margins` flag. - - .. seealso:: - - :py:meth:`updateCanvasMargins()`, :py:meth:`getCanvasMarginHint()` - """ - left = top = right = bottom = -1.0 - - for item in self.itemList(): - if item.testItemAttribute(QwtPlotItem.Margins): - m = item.getCanvasMarginHint( - maps[item.xAxis()], maps[item.yAxis()], canvasRect - ) - left = max([left, m[self.yLeft]]) - top = max([top, m[self.xTop]]) - right = max([right, m[self.yRight]]) - bottom = max([bottom, m[self.xBottom]]) - - return left, top, right, bottom - - def updateCanvasMargins(self): - """ - Update the canvas margins - - Plot items might indicate, that they need some extra space - at the borders of the canvas by the `QwtPlotItem.Margins` flag. - - .. seealso:: - - :py:meth:`getCanvasMarginsHint()`, - :py:meth:`QwtPlotItem.getCanvasMarginHint()` - """ - maps = [self.canvasMap(axisId) for axisId in self.AXES] - margins = self.getCanvasMarginsHint(maps, self.canvas().contentsRect()) - - doUpdate = False - - for axisId in self.AXES: - if margins[axisId] >= 0.0: - m = math.ceil(margins[axisId]) - self.plotLayout().setCanvasMargin(m, axisId) - doUpdate = True - - if doUpdate: - self.updateLayout() - - def drawCanvas(self, painter): - """ - Redraw the canvas. - - :param QPainter painter: Painter used for drawing - - .. warning:: - - drawCanvas calls drawItems what is also used - for printing. Applications that like to add individual - plot items better overload drawItems() - - .. seealso:: - - :py:meth:`getCanvasMarginsHint()`, - :py:meth:`QwtPlotItem.getCanvasMarginHint()` - """ - maps = [self.canvasMap(axisId) for axisId in self.AXES] - self.drawItems(painter, QRectF(self.__data.canvas.contentsRect()), maps) - - def drawItems(self, painter, canvasRect, maps): - """ - Redraw the canvas. - - :param QPainter painter: Painter used for drawing - :param QRectF canvasRect: Bounding rectangle where to paint - :param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates - - .. note:: - - Usually canvasRect is `contentsRect()` of the plot canvas. - Due to a bug in Qt this rectangle might be wrong for certain - frame styles ( f.e `QFrame.Box` ) and it might be necessary to - fix the margins manually using `QWidget.setContentsMargins()` - """ - for item in self.itemList(): - if item and item.isVisible(): - painter.save() - painter.setRenderHint( - QPainter.Antialiasing, - item.testRenderHint(QwtPlotItem.RenderAntialiased), - ) - item.draw(painter, maps[item.xAxis()], maps[item.yAxis()], canvasRect) - painter.restore() - - def canvasMap(self, axisId): - """ - :param int axisId: Axis - :return: Map for the axis on the canvas. With this map pixel coordinates can translated to plot coordinates and vice versa. - - .. seealso:: - - :py:class:`qwt.scale_map.QwtScaleMap`, - :py:meth:`transform()`, :py:meth:`invTransform()` - """ - map_ = QwtScaleMap() - if not self.__data.canvas: - return map_ - - map_.setTransformation(self.axisScaleEngine(axisId).transformation()) - sd = self.axisScaleDiv(axisId) - if sd is None: - return map_ - map_.setScaleInterval(sd.lowerBound(), sd.upperBound()) - - if self.axisEnabled(axisId): - s = self.axisWidget(axisId) - if axisId in (self.yLeft, self.yRight): - y = s.y() + s.startBorderDist() - self.__data.canvas.y() - h = s.height() - s.startBorderDist() - s.endBorderDist() - map_.setPaintInterval(y + h, y) - else: - x = s.x() + s.startBorderDist() - self.__data.canvas.x() - w = s.width() - s.startBorderDist() - s.endBorderDist() - map_.setPaintInterval(x, x + w) - else: - canvasRect = self.__data.canvas.contentsRect() - if axisId in (self.yLeft, self.yRight): - top = 0 - if not self.plotLayout().alignCanvasToScale(self.xTop): - top = self.plotLayout().canvasMargin(self.xTop) - bottom = 0 - if not self.plotLayout().alignCanvasToScale(self.xBottom): - bottom = self.plotLayout().canvasMargin(self.xBottom) - map_.setPaintInterval( - canvasRect.bottom() - bottom, canvasRect.top() + top - ) - else: - left = 0 - if not self.plotLayout().alignCanvasToScale(self.yLeft): - left = self.plotLayout().canvasMargin(self.yLeft) - right = 0 - if not self.plotLayout().alignCanvasToScale(self.yRight): - right = self.plotLayout().canvasMargin(self.yRight) - map_.setPaintInterval( - canvasRect.left() + left, canvasRect.right() - right - ) - return map_ - - def setCanvasBackground(self, brush): - """ - Change the background of the plotting area - - Sets brush to `QPalette.Window` of all color groups of - the palette of the canvas. Using `canvas().setPalette()` - is a more powerful way to set these colors. - - :param QBrush brush: New background brush - - .. seealso:: - - :py:meth:`canvasBackground()` - """ - pal = self.__data.canvas.palette() - pal.setBrush(QPalette.Window, QBrush(brush)) - self.canvas().setPalette(pal) - - def canvasBackground(self): - """ - :return: Background brush of the plotting area. - - .. seealso:: - - :py:meth:`setCanvasBackground()` - """ - return self.canvas().palette().brush(QPalette.Active, QPalette.Window) - - def axisValid(self, axis_id): - """ - :param int axis_id: Axis - :return: True if the specified axis exists, otherwise False - """ - return axis_id in QwtPlot.AXES - - def insertLegend(self, legend, pos=None, ratio=-1): - """ - Insert a legend - - If the position legend is `QwtPlot.LeftLegend` or `QwtPlot.RightLegend` - the legend will be organized in one column from top to down. - Otherwise the legend items will be placed in a table - with a best fit number of columns from left to right. - - insertLegend() will set the plot widget as parent for the legend. - The legend will be deleted in the destructor of the plot or when - another legend is inserted. - - Legends, that are not inserted into the layout of the plot widget - need to connect to the legendDataChanged() signal. Calling updateLegend() - initiates this signal for an initial update. When the application code - wants to implement its own layout this also needs to be done for - rendering plots to a document ( see QwtPlotRenderer ). - - :param qwt.legend.QwtAbstractLegend legend: Legend - :param QwtPlot.LegendPosition pos: The legend's position. - :param float ratio: Ratio between legend and the bounding rectangle of title, canvas and axes - - .. note:: - - For top/left position the number of columns will be limited to 1, - otherwise it will be set to unlimited. - - .. note:: - - The legend will be shrunk if it would need more space than the - given ratio. The ratio is limited to ]0.0 .. 1.0]. - In case of <= 0.0 it will be reset to the default ratio. - The default vertical/horizontal ratio is 0.33/0.5. - - .. seealso:: - - :py:meth:`legend()`, - :py:meth:`qwt.plot_layout.QwtPlotLayout.legendPosition()`, - :py:meth:`qwt.plot_layout.QwtPlotLayout.setLegendPosition()` - """ - if pos is None: - pos = self.RightLegend - self.__data.layout.setLegendPosition(pos, ratio) - if legend != self.__data.legend: - if self.__data.legend and self.__data.legend.parent() is self: - self.__data.legend.setParent(None) - del self.__data.legend - self.__data.legend = legend - if self.__data.legend: - self.legendDataChanged.connect(self.__data.legend.updateLegend) - if self.__data.legend.parent() is not self: - self.__data.legend.setParent(self) - - self.legendDataChanged.disconnect(self.updateLegendItems) - self.updateLegend() - self.legendDataChanged.connect(self.updateLegendItems) - - lpos = self.__data.layout.legendPosition() - - if legend is not None: - if lpos in (self.LeftLegend, self.RightLegend): - if legend.maxColumns() == 0: - legend.setMaxColumns(1) - elif lpos in (self.TopLegend, self.BottomLegend): - legend.setMaxColumns(0) - - previousInChain = None - if lpos == self.LeftLegend: - previousInChain = self.axisWidget(QwtPlot.xTop) - elif lpos == self.TopLegend: - previousInChain = self - elif lpos == self.RightLegend: - previousInChain = self.axisWidget(QwtPlot.yRight) - elif lpos == self.BottomLegend: - previousInChain = self.footerLabel() - - if previousInChain is not None: - qwtSetTabOrder(previousInChain, legend, True) - - self.updateLayout() - - def updateLegend(self, plotItem=None): - """ - If plotItem is None, emit QwtPlot.legendDataChanged for all - plot item. Otherwise, emit the signal for passed plot item. - - :param qwt.plot.QwtPlotItem plotItem: Plot item - - .. seealso:: - - :py:meth:`QwtPlotItem.legendData()`, :py:data:`QwtPlot.legendDataChanged` - """ - if plotItem is None: - items = list(self.itemList()) - else: - items = [plotItem] - for plotItem in items: - if plotItem is None: - continue - legendData = [] - if plotItem.testItemAttribute(QwtPlotItem.Legend): - legendData = plotItem.legendData() - self.legendDataChanged.emit(plotItem, legendData) - - def updateLegendItems(self, plotItem, legendData): - """ - Update all plot items interested in legend attributes - - Call `QwtPlotItem.updateLegend()`, when the - `QwtPlotItem.LegendInterest` flag is set. - - :param qwt.plot.QwtPlotItem plotItem: Plot item - :param list legendData: Entries to be displayed for the plot item ( usually 1 ) - - .. seealso:: - - :py:meth:`QwtPlotItem.LegendInterest()`, - :py:meth:`QwtPlotItem.updateLegend` - """ - if plotItem is not None: - for item in self.itemList(): - if item.testItemInterest(QwtPlotItem.LegendInterest): - item.updateLegend(plotItem, legendData) - - def attachItem(self, plotItem, on): - """ - Attach/Detach a plot item - - :param qwt.plot.QwtPlotItem plotItem: Plot item - :param bool on: When true attach the item, otherwise detach it - """ - if plotItem.testItemInterest(QwtPlotItem.LegendInterest): - for item in self.itemList(): - legendData = [] - if on and item.testItemAttribute(QwtPlotItem.Legend): - legendData = item.legendData() - plotItem.updateLegend(item, legendData) - - if on: - self.insertItem(plotItem) - else: - self.removeItem(plotItem) - - self.itemAttached.emit(plotItem, on) - - if plotItem.testItemAttribute(QwtPlotItem.Legend): - if on: - self.updateLegend(plotItem) - else: - self.legendDataChanged.emit(plotItem, []) - - self.autoRefresh() - - def print_(self, printer): - """ - Print plot to printer - - :param printer: Printer - :type printer: QPaintDevice or QPrinter or QSvgGenerator - """ - from qwt.plot_renderer import QwtPlotRenderer - - renderer = QwtPlotRenderer(self) - renderer.renderTo(self, printer) - - def exportTo( - self, filename, size=(800, 600), size_mm=None, resolution=85, format_=None - ): - """ - Export plot to PDF or image file (SVG, PNG, ...) - - :param str filename: Filename - :param tuple size: (width, height) size in pixels - :param tuple size_mm: (width, height) size in millimeters - :param int resolution: Resolution in dots per Inch (dpi) - :param str format_: File format (PDF, SVG, PNG, ...) - """ - if size_mm is None: - size_mm = tuple(25.4 * np.array(size) / resolution) - from qwt.plot_renderer import QwtPlotRenderer - - renderer = QwtPlotRenderer(self) - renderer.renderDocument(self, filename, size_mm, resolution, format_) - - -class QwtPlotItem_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.plot = None - self.isVisible = True - self.attributes = 0 - self.interests = 0 - self.renderHints = 0 - self.z = 0.0 - self.xAxis = QwtPlot.xBottom - self.yAxis = QwtPlot.yLeft - self.legendIconSize = QSize(8, 8) - self.title = None # QwtText - - -class QwtPlotItem(object): - """ - Base class for items on the plot canvas - - A plot item is "something", that can be painted on the plot canvas, - or only affects the scales of the plot widget. They can be categorized as: - - - Representator - - A "Representator" is an item that represents some sort of data - on the plot canvas. The different representator classes are organized - according to the characteristics of the data: - - - :py:class:`qwt.plot_marker.QwtPlotMarker`: Represents a point or a - horizontal/vertical coordinate - - :py:class:`qwt.plot_curve.QwtPlotCurve`: Represents a series of - points - - - Decorators - - A "Decorator" is an item, that displays additional information, that - is not related to any data: - - - :py:class:`qwt.plot_grid.QwtPlotGrid` - - Depending on the `QwtPlotItem.ItemAttribute` flags, an item is included - into autoscaling or has an entry on the legend. - - Before misusing the existing item classes it might be better to - implement a new type of plot item - ( don't implement a watermark as spectrogram ). - Deriving a new type of `QwtPlotItem` primarily means to implement - the `YourPlotItem.draw()` method. - - .. seealso:: - - The cpuplot example shows the implementation of additional plot items. - - .. py:class:: QwtPlotItem([title=None]) - - Constructor - - :param title: Title of the item - :type title: qwt.text.QwtText or str - """ - - # enum RttiValues - ( - Rtti_PlotItem, - Rtti_PlotGrid, - Rtti_PlotScale, - Rtti_PlotLegend, - Rtti_PlotMarker, - Rtti_PlotCurve, - Rtti_PlotSpectroCurve, - Rtti_PlotIntervalCurve, - Rtti_PlotHistogram, - Rtti_PlotSpectrogram, - Rtti_PlotSVG, - Rtti_PlotTradingCurve, - Rtti_PlotBarChart, - Rtti_PlotMultiBarChart, - Rtti_PlotShape, - Rtti_PlotTextLabel, - Rtti_PlotZone, - ) = list(range(17)) - Rtti_PlotUserItem = 1000 - - # enum ItemAttribute - Legend = 0x01 - AutoScale = 0x02 - Margins = 0x04 - - # enum ItemInterest - ScaleInterest = 0x01 - LegendInterest = 0x02 - - # enum RenderHint - RenderAntialiased = 0x1 - - def __init__(self, title=None, icon=None): - """title: QwtText""" - if title is None: - title = QwtText("") - if hasattr(title, "capitalize"): # avoids dealing with Py3K compat. - title = QwtText(title) - assert isinstance(title, QwtText) - self.__data = QwtPlotItem_PrivateData() - self.__data.title = title - self.__data.icon = icon - - def attach(self, plot): - """ - Attach the item to a plot. - - This method will attach a `QwtPlotItem` to the `QwtPlot` argument. - It will first detach the `QwtPlotItem` from any plot from a previous - call to attach (if necessary). If a None argument is passed, it will - detach from any `QwtPlot` it was attached to. - - :param qwt.plot.QwtPlot plot: Plot widget - - .. seealso:: - - :py:meth:`detach()` - """ - if plot is self.__data.plot: - return - - if self.__data.plot: - self.__data.plot.attachItem(self, False) - - self.__data.plot = plot - - if self.__data.plot: - self.__data.plot.attachItem(self, True) - - def detach(self): - """ - Detach the item from a plot. - - This method detaches a `QwtPlotItem` from any `QwtPlot` it has been - associated with. - - .. seealso:: - - :py:meth:`attach()` - """ - self.attach(None) - - def rtti(self): - """ - Return rtti for the specific class represented. `QwtPlotItem` is - simply a virtual interface class, and base classes will implement - this method with specific rtti values so a user can differentiate - them. - - :return: rtti value - """ - return self.Rtti_PlotItem - - def plot(self): - """ - :return: attached plot - """ - return self.__data.plot - - def z(self): - """ - Plot items are painted in increasing z-order. - - :return: item z order - - .. seealso:: - - :py:meth:`setZ()`, :py:meth:`QwtPlotDict.itemList()` - """ - return self.__data.z - - def setZ(self, z): - """ - Set the z value - - Plot items are painted in increasing z-order. - - :param float z: Z-value - - .. seealso:: - - :py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()` - """ - if self.__data.z != z: - if self.__data.plot: - self.__data.plot.attachItem(self, False) - self.__data.z = z - if self.__data.plot: - self.__data.plot.attachItem(self, True) - self.itemChanged() - - def setTitle(self, title): - """ - Set a new title - - :param title: Title - :type title: qwt.text.QwtText or str - - .. seealso:: - - :py:meth:`title()` - """ - if not isinstance(title, QwtText): - title = QwtText(title) - if self.__data.title != title: - self.__data.title = title - self.legendChanged() - - def title(self): - """ - :return: Title of the item - - .. seealso:: - - :py:meth:`setTitle()` - """ - return self.__data.title - - def setItemAttribute(self, attribute, on=True): - """ - Toggle an item attribute - - :param int attribute: Attribute type - :param bool on: True/False - - .. seealso:: - - :py:meth:`testItemAttribute()` - """ - if bool(self.__data.attributes & attribute) != on: - if on: - self.__data.attributes |= attribute - else: - self.__data.attributes &= ~attribute - if attribute == QwtPlotItem.Legend: - self.legendChanged() - self.itemChanged() - - def testItemAttribute(self, attribute): - """ - Test an item attribute - - :param int attribute: Attribute type - :return: True/False - - .. seealso:: - - :py:meth:`setItemAttribute()` - """ - return bool(self.__data.attributes & attribute) - - def setItemInterest(self, interest, on=True): - """ - Toggle an item interest - - :param int attribute: Interest type - :param bool on: True/False - - .. seealso:: - - :py:meth:`testItemInterest()` - """ - if bool(self.__data.interests & interest) != on: - if on: - self.__data.interests |= interest - else: - self.__data.interests &= ~interest - self.itemChanged() - - def testItemInterest(self, interest): - """ - Test an item interest - - :param int attribute: Interest type - :return: True/False - - .. seealso:: - - :py:meth:`setItemInterest()` - """ - return bool(self.__data.interests & interest) - - def setRenderHint(self, hint, on=True): - """ - Toggle a render hint - - :param int hint: Render hint - :param bool on: True/False - - .. seealso:: - - :py:meth:`testRenderHint()` - """ - if bool(self.__data.renderHints & hint) != on: - if on: - self.__data.renderHints |= hint - else: - self.__data.renderHints &= ~hint - self.itemChanged() - - def testRenderHint(self, hint): - """ - Test a render hint - - :param int attribute: Render hint - :return: True/False - - .. seealso:: - - :py:meth:`setRenderHint()` - """ - return bool(self.__data.renderHints & hint) - - def setLegendIconSize(self, size): - """ - Set the size of the legend icon - - The default setting is 8x8 pixels - - :param QSize size: Size - - .. seealso:: - - :py:meth:`legendIconSize()`, :py:meth:`legendIcon()` - """ - if self.__data.legendIconSize != size: - self.__data.legendIconSize = size - self.legendChanged() - - def legendIconSize(self): - """ - :return: Legend icon size - - .. seealso:: - - :py:meth:`setLegendIconSize()`, :py:meth:`legendIcon()` - """ - return self.__data.legendIconSize - - def legendIcon(self, index, size): - """ - :param int index: Index of the legend entry (usually there is only one) - :param QSizeF size: Icon size - :return: Icon representing the item on the legend - - The default implementation returns an invalid icon - - .. seealso:: - - :py:meth:`setLegendIconSize()`, :py:meth:`legendData()` - """ - return QwtGraphic() - - def show(self): - """Show the item""" - self.setVisible(True) - - def hide(self): - """Hide the item""" - self.setVisible(False) - - def setVisible(self, on): - """ - Show/Hide the item - - :param bool on: Show if True, otherwise hide - - .. seealso:: - - :py:meth:`isVisible()`, :py:meth:`show()`, :py:meth:`hide()` - """ - if on != self.__data.isVisible: - self.__data.isVisible = on - self.itemChanged() - - def isVisible(self): - """ - :return: True if visible - - .. seealso:: - - :py:meth:`setVisible()`, :py:meth:`show()`, :py:meth:`hide()` - """ - return self.__data.isVisible - - def itemChanged(self): - """ - Update the legend and call `QwtPlot.autoRefresh()` for the - parent plot. - - .. seealso:: - - :py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()` - """ - if self.__data.plot: - self.__data.plot.autoRefresh() - - def legendChanged(self): - """ - Update the legend of the parent plot. - - .. seealso:: - - :py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()` - """ - if self.testItemAttribute(QwtPlotItem.Legend) and self.__data.plot: - self.__data.plot.updateLegend(self) - - def setAxes(self, xAxis, yAxis): - """ - Set X and Y axis - - The item will painted according to the coordinates of its Axes. - - :param int xAxis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`) - :param int yAxis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`) - - .. seealso:: - - :py:meth:`setXAxis()`, :py:meth:`setYAxis()`, - :py:meth:`xAxis()`, :py:meth:`yAxis()` - """ - if xAxis == QwtPlot.xBottom or xAxis == QwtPlot.xTop: - self.__data.xAxis = xAxis - if yAxis == QwtPlot.yLeft or yAxis == QwtPlot.yRight: - self.__data.yAxis = yAxis - self.itemChanged() - - def setAxis(self, xAxis, yAxis): - """ - Set X and Y axis - - .. warning:: - - `setAxis` has been removed in Qwt6: please use - :py:meth:`setAxes()` instead - """ - import warnings - - warnings.warn( - "`setAxis` has been removed in Qwt6: please use `setAxes` instead", - RuntimeWarning, - ) - self.setAxes(xAxis, yAxis) - - def setXAxis(self, axis): - """ - Set the X axis - - The item will painted according to the coordinates its Axes. - - :param int axis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`) - - .. seealso:: - - :py:meth:`setAxes()`, :py:meth:`setYAxis()`, - :py:meth:`xAxis()`, :py:meth:`yAxis()` - """ - if axis in (QwtPlot.xBottom, QwtPlot.xTop): - self.__data.xAxis = axis - self.itemChanged() - - def setYAxis(self, axis): - """ - Set the Y axis - - The item will painted according to the coordinates its Axes. - - :param int axis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`) - - .. seealso:: - - :py:meth:`setAxes()`, :py:meth:`setXAxis()`, - :py:meth:`xAxis()`, :py:meth:`yAxis()` - """ - if axis in (QwtPlot.yLeft, QwtPlot.yRight): - self.__data.yAxis = axis - self.itemChanged() - - def xAxis(self): - """ - :return: xAxis - """ - return self.__data.xAxis - - def yAxis(self): - """ - :return: yAxis - """ - return self.__data.yAxis - - def boundingRect(self): - """ - :return: An invalid bounding rect: QRectF(1.0, 1.0, -2.0, -2.0) - - .. note:: - - A width or height < 0.0 is ignored by the autoscaler - """ - return QRectF(1.0, 1.0, -2.0, -2.0) - - def getCanvasMarginHint(self, xMap, yMap, canvasRect): - """ - Calculate a hint for the canvas margin - - When the QwtPlotItem::Margins flag is enabled the plot item - indicates, that it needs some margins at the borders of the canvas. - This is f.e. used by bar charts to reserve space for displaying - the bars. - - The margins are in target device coordinates ( pixels on screen ) - - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates - - .. seealso:: - - :py:meth:`QwtPlot.getCanvasMarginsHint()`, - :py:meth:`QwtPlot.updateCanvasMargins()`, - """ - left = top = right = bottom = 0.0 - return left, top, right, bottom - - def legendData(self): - """ - Return all information, that is needed to represent - the item on the legend - - `QwtLegendData` is basically a list of QVariants that makes it - possible to overload and reimplement legendData() to - return almost any type of information, that is understood - by the receiver that acts as the legend. - - The default implementation returns one entry with - the title() of the item and the legendIcon(). - - :return: Data, that is needed to represent the item on the legend - - .. seealso:: - - :py:meth:`title()`, :py:meth:`legendIcon()`, - :py:class:`qwt.legend.QwtLegend` - """ - data = QwtLegendData() - label = self.title() - label.setRenderFlags(label.renderFlags() & Qt.AlignLeft) - data.setValue(QwtLegendData.TitleRole, label) - graphic = self.legendIcon(0, self.legendIconSize()) - if not graphic.isNull(): - data.setValue(QwtLegendData.IconRole, graphic) - return [data] - - def updateLegend(self, item, data): - """ - Update the item to changes of the legend info - - Plot items that want to display a legend ( not those, that want to - be displayed on a legend ! ) will have to implement updateLegend(). - - updateLegend() is only called when the LegendInterest interest - is enabled. The default implementation does nothing. - - :param qwt.plot.QwtPlotItem item: Plot item to be displayed on a legend - :param list data: Attributes how to display item on the legend - - .. note:: - - Plot items, that want to be displayed on a legend - need to enable the `QwtPlotItem.Legend` flag and to implement - legendData() and legendIcon() - """ - pass - - def scaleRect(self, xMap, yMap): - """ - Calculate the bounding scale rectangle of 2 maps - - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :return: Bounding scale rect of the scale maps, not normalized - """ - return QRectF(xMap.s1(), yMap.s1(), xMap.sDist(), yMap.sDist()) - - def paintRect(self, xMap, yMap): - """ - Calculate the bounding paint rectangle of 2 maps - - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :return: Bounding paint rectangle of the scale maps, not normalized - """ - return QRectF(xMap.p1(), yMap.p1(), xMap.pDist(), yMap.pDist()) diff --git a/qwt/plot_canvas.py b/qwt/plot_canvas.py deleted file mode 100644 index a72d9b8..0000000 --- a/qwt/plot_canvas.py +++ /dev/null @@ -1,847 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotCanvas -------------- - -.. autoclass:: QwtPlotCanvas - :members: -""" - -from collections.abc import Sequence - -from qtpy.QtCore import QEvent, QObject, QPoint, QPointF, QRect, QRectF, QSize, Qt -from qtpy.QtGui import ( - QBrush, - QGradient, - QImage, - QPaintEngine, - QPainter, - QPainterPath, - QPen, - QPixmap, - QPolygonF, - QRegion, - qAlpha, -) -from qtpy.QtWidgets import QFrame, QStyle, QStyleOption, QStyleOptionFrame - -from qwt.null_paintdevice import QwtNullPaintDevice -from qwt.painter import QwtPainter - - -class Border(object): - def __init__(self): - self.pathlist = [] - self.rectList = [] - self.clipRegion = QRegion() - - -class Background(object): - def __init__(self): - self.path = QPainterPath() - self.brush = QBrush() - self.origin = QPointF() - - -class QwtStyleSheetRecorder(QwtNullPaintDevice): - def __init__(self, size): - super(QwtStyleSheetRecorder, self).__init__() - self.__size = size - self.__pen = QPen() - self.__brush = QBrush() - self.__origin = QPointF() - self.clipRects = [] - self.border = Border() - self.background = Background() - - def updateState(self, state): - if state.state() & QPaintEngine.DirtyPen: - self.__pen = state.pen() - if state.state() & QPaintEngine.DirtyBrush: - self.__brush = state.brush() - if state.state() & QPaintEngine.DirtyBrushOrigin: - self.__origin = state.brushOrigin() - - def drawRects(self, rects, count): - if isinstance(rects, (QRect, QRectF)): - self.border.rectList = [QRectF(rects)] - elif isinstance(rects, Sequence): - self.border.rectList.extend(QRectF(rects[i]) for i in range(count)) - else: - raise TypeError( - "drawRects() expects a QRect, QRectF or a sequence of them, " - f"but got {type(rects).__name__}" - ) - - def drawPath(self, path): - rect = QRectF(QPointF(0.0, 0.0), self.__size) - if path.controlPointRect().contains(rect.center()): - self.setCornerRects(path) - self.alignCornerRects(rect) - self.background.path = path - self.background.brush = self.__brush - self.background.origin = self.__origin - else: - self.border.pathlist += [path] - - def setCornerRects(self, path): - pos = QPointF(0.0, 0.0) - for i in range(path.elementCount()): - el = path.elementAt(i) - if el.type in (QPainterPath.MoveToElement, QPainterPath.LineToElement): - pos.setX(el.x) - pos.setY(el.y) - elif el.type == QPainterPath.CurveToElement: - r = QRectF(pos, QPointF(el.x, el.y)) - self.clipRects += [r.normalized()] - pos.setX(el.x) - pos.setY(el.y) - elif el.type == QPainterPath.CurveToDataElement: - if self.clipRects: - r = self.clipRects[-1] - r.setCoords( - min([r.left(), el.x]), - min([r.top(), el.y]), - max([r.right(), el.x]), - max([r.bottom(), el.y]), - ) - self.clipRects[-1] = r.normalized() - - def sizeMetrics(self): - return self.__size - - def alignCornerRects(self, rect): - for r in self.clipRects: - if r.center().x() < rect.center().x(): - r.setLeft(rect.left()) - else: - r.setRight(rect.right()) - if r.center().y() < rect.center().y(): - r.setTop(rect.top()) - else: - r.setBottom(rect.bottom()) - - -def qwtDrawBackground(painter, canvas): - painter.save() - borderClip = canvas.borderPath(canvas.rect()) - if not borderClip.isEmpty(): - painter.setClipPath(borderClip, Qt.IntersectClip) - brush = canvas.palette().brush(canvas.backgroundRole()) - if brush.style() == Qt.TexturePattern: - pm = QPixmap(canvas.size()) - QwtPainter.fillPixmap(canvas, pm) - painter.drawPixmap(0, 0, pm) - elif brush.gradient(): - rects = [] - if brush.gradient().coordinateMode() == QGradient.ObjectBoundingMode: - rects += [canvas.rect()] - else: - rects += [painter.clipRegion().boundingRect()] - useRaster = False - if painter.paintEngine().type() == QPaintEngine.X11: - useRaster = True - if useRaster: - format_ = QImage.Format_RGB32 - stops = brush.gradient().stops() - for stop in stops: - if stop.second.alpha() != 255: - format_ = QImage.Format_ARGB32 - break - image = QImage(canvas.size(), format_) - pntr = QPainter(image) - pntr.setPen(Qt.NoPen) - pntr.setBrush(brush) - for rect in rects: - pntr.drawRect(rect) - pntr.end() - painter.drawImage(0, 0, image) - else: - painter.setPen(Qt.NoPen) - painter.setBrush(brush) - for rect in rects: - painter.drawRect(rect) - else: - painter.setPen(Qt.NoPen) - painter.setBrush(brush) - painter.drawRect(painter.clipRegion().boundingRect()) - - painter.restore() - - -def qwtRevertPath(path): - if path.elementCount() == 4: - el0 = path.elementAt(0) - el3 = path.elementAt(3) - path.setElementPositionAt(0, el3.x, el3.y) - path.setElementPositionAt(3, el0.x, el0.y) - - -def qwtCombinePathList(rect, pathList): - if not pathList: - return QPainterPath() - - ordered = [None] * 8 - for subPath in pathList: - index = -1 - br = subPath.controlPointRect() - if br.center().x() < rect.center().x(): - if br.center().y() < rect.center().y(): - if abs(br.top() - rect.top()) < abs(br.left() - rect.left()): - index = 1 - else: - index = 0 - else: - if abs(br.bottom() - rect.bottom) < abs(br.left() - rect.left()): - index = 6 - else: - index = 7 - if subPath.currentPosition().y() > br.center().y(): - qwtRevertPath(subPath) - else: - if br.center().y() < rect.center().y(): - if abs(br.top() - rect.top()) < abs(br.right() - rect.right()): - index = 2 - else: - index = 3 - else: - if abs(br.bottom() - rect.bottom()) < abs(br.right() - rect.right()): - index = 5 - else: - index = 4 - if subPath.currentPosition().y() < br.center().y(): - qwtRevertPath(subPath) - ordered[index] = subPath - for i in range(4): - if ordered[2 * i].isEmpty() != ordered[2 * i + 1].isEmpty(): - return QPainterPath() - corners = QPolygonF(rect) - path = QPainterPath() - for i in range(4): - if ordered[2 * i].isEmpty(): - path.lineTo(corners[i]) - else: - path.connectPath(ordered[2 * i]) - path.connectPath(ordered[2 * i + 1]) - path.closeSubpath() - return path - - -def qwtDrawStyledBackground(w, painter): - opt = QStyleOption() - opt.initFrom(w) - w.style().drawPrimitive(QStyle.PE_Widget, opt, painter, w) - - -def qwtBackgroundWidget(w): - if w.parentWidget() is None: - return w - if w.autoFillBackground(): - brush = w.palette().brush(w.backgroundRole()) - if brush.color().alpha() > 0: - return w - if w.testAttribute(Qt.WA_StyledBackground): - image = QImage(1, 1, QImage.Format_ARGB32) - image.fill(Qt.transparent) - painter = QPainter(image) - painter.translate(-w.rect().center()) - qwtDrawStyledBackground(w, painter) - painter.end() - if qAlpha(image.pixel(0, 0)) != 0: - return w - return qwtBackgroundWidget(w.parentWidget()) - - -def qwtFillBackground(*args): - if len(args) == 2: - painter, canvas = args - - rects = [] - if canvas.testAttribute(Qt.WA_StyledBackground): - recorder = QwtStyleSheetRecorder(canvas.size()) - p = QPainter(recorder) - qwtDrawStyledBackground(canvas, p) - p.end() - if recorder.background.brush.isOpaque(): - rects = recorder.clipRects - else: - rects += [canvas.rect()] - else: - r = canvas.rect() - radius = canvas.borderRadius() - if radius > 0.0: - sz = QSize(radius, radius) - rects += [ - QRect(r.topLeft(), sz), - QRect(r.topRight() - QPoint(radius, 0), sz), - QRect(r.bottomRight() - QPoint(radius, radius), sz), - QRect(r.bottomLeft() - QPoint(0, radius), sz), - ] - - qwtFillBackground(painter, canvas, rects) - - elif len(args) == 3: - painter, widget, fillRects = args - - if not fillRects: - return - if painter.hasClipping(): - clipRegion = painter.transform().map(painter.clipRegion()) - else: - clipRegion = widget.contentsRect() - bgWidget = qwtBackgroundWidget(widget.parentWidget()) - for rect in fillRects: - if clipRegion.intersects(rect): - pm = QPixmap(rect.size()) - QwtPainter.fillPixmap( - bgWidget, pm, widget.mapTo(bgWidget, rect.topLeft()) - ) - painter.drawPixmap(rect, pm) - - else: - raise TypeError( - "%s() takes 2 or 3 argument(s) (%s given)" - % ("qwtFillBackground", len(args)) - ) - - -class StyleSheetBackground(object): - def __init__(self): - self.brush = QBrush() - self.origin = QPointF() - - -class StyleSheet(object): - def __init__(self): - self.hasBorder = False - self.borderPath = QPainterPath() - self.cornerRects = [] - self.background = StyleSheetBackground() - - -class QwtPlotCanvas_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.focusIndicator = QwtPlotCanvas.NoFocusIndicator - self.borderRadius = 0 - self.paintAttributes = 0 - self.backingStore = None - self.styleSheet = StyleSheet() - self.styleSheet.hasBorder = False - - -class QwtPlotCanvas(QFrame): - """ - Canvas of a QwtPlot. - - Canvas is the widget where all plot items are displayed - - .. seealso:: - - :py:meth:`qwt.plot.QwtPlot.setCanvas()` - - Paint attributes: - - * `QwtPlotCanvas.BackingStore`: - - Paint double buffered reusing the content of the pixmap buffer - when possible. - - Using a backing store might improve the performance significantly, - when working with widget overlays (like rubber bands). - Disabling the cache might improve the performance for - incremental paints - (using :py:class:`qwt.plot_directpainter.QwtPlotDirectPainter`). - - * `QwtPlotCanvas.Opaque`: - - Try to fill the complete contents rectangle of the plot canvas - - When using styled backgrounds Qt assumes, that the canvas doesn't - fill its area completely (f.e because of rounded borders) and - fills the area below the canvas. When this is done with gradients - it might result in a serious performance bottleneck - depending on - the size. - - When the Opaque attribute is enabled the canvas tries to - identify the gaps with some heuristics and to fill those only. - - .. warning:: - - Will not work for semitransparent backgrounds - - * `QwtPlotCanvas.HackStyledBackground`: - - Try to improve painting of styled backgrounds - - `QwtPlotCanvas` supports the box model attributes for - customizing the layout with style sheets. Unfortunately - the design of Qt style sheets has no concept how to - handle backgrounds with rounded corners - beside of padding. - - When HackStyledBackground is enabled the plot canvas tries - to separate the background from the background border - by reverse engineering to paint the background before and - the border after the plot items. In this order the border - gets perfectly antialiased and you can avoid some pixel - artifacts in the corners. - - * `QwtPlotCanvas.ImmediatePaint`: - - When ImmediatePaint is set replot() calls repaint() - instead of update(). - - .. seealso:: - - :py:meth:`replot()`, :py:meth:`QWidget.repaint()`, - :py:meth:`QWidget.update()` - - Focus indicators: - - * `QwtPlotCanvas.NoFocusIndicator`: - - Don't paint a focus indicator - - * `QwtPlotCanvas.CanvasFocusIndicator`: - - The focus is related to the complete canvas. - Paint the focus indicator using paintFocus() - - * `QwtPlotCanvas.ItemFocusIndicator`: - - The focus is related to an item (curve, point, ...) on - the canvas. It is up to the application to display a - focus indication using f.e. highlighting. - - .. py:class:: QwtPlotCanvas([plot=None]) - - Constructor - - :param qwt.plot.QwtPlot plot: Parent plot widget - - .. seealso:: - - :py:meth:`qwt.plot.QwtPlot.setCanvas()` - """ - - # enum PaintAttribute - BackingStore = 1 - Opaque = 2 - HackStyledBackground = 4 - ImmediatePaint = 8 - - # enum FocusIndicator - NoFocusIndicator, CanvasFocusIndicator, ItemFocusIndicator = list(range(3)) - - def __init__(self, plot=None): - super(QwtPlotCanvas, self).__init__(plot) - self.__plot = plot - self.setFrameStyle(QFrame.Panel | QFrame.Sunken) - self.setLineWidth(2) - self.__data = QwtPlotCanvas_PrivateData() - self.setCursor(Qt.CrossCursor) - self.setAutoFillBackground(True) - self.setPaintAttribute(QwtPlotCanvas.BackingStore, False) - self.setPaintAttribute(QwtPlotCanvas.Opaque, True) - self.setPaintAttribute(QwtPlotCanvas.HackStyledBackground, True) - - def plot(self): - """ - :return: Parent plot widget - """ - return self.__plot - - def setPaintAttribute(self, attribute, on=True): - """ - Changing the paint attributes - - Paint attributes: - - * `QwtPlotCanvas.BackingStore` - * `QwtPlotCanvas.Opaque` - * `QwtPlotCanvas.HackStyledBackground` - * `QwtPlotCanvas.ImmediatePaint` - - :param int attribute: Paint attribute - :param bool on: On/Off - - .. seealso:: - - :py:meth:`testPaintAttribute()`, :py:meth:`backingStore()` - """ - if bool(self.__data.paintAttributes & attribute) == on: - return - if on: - self.__data.paintAttributes |= attribute - else: - self.__data.paintAttributes &= ~attribute - if attribute == self.BackingStore: - if on: - if self.__data.backingStore is None: - self.__data.backingStore = QPixmap() - if self.isVisible(): - self.__data.backingStore = self.grab(self.rect()) - else: - self.__data.backingStore = None - elif attribute == self.Opaque: - if on: - self.setAttribute(Qt.WA_OpaquePaintEvent, True) - elif attribute in (self.HackStyledBackground, self.ImmediatePaint): - pass - - def testPaintAttribute(self, attribute): - """ - Test whether a paint attribute is enabled - - :param int attribute: Paint attribute - :return: True, when attribute is enabled - - .. seealso:: - - :py:meth:`setPaintAttribute()` - """ - return self.__data.paintAttributes & attribute - - def backingStore(self): - """ - :return: Backing store, might be None - """ - return self.__data.backingStore - - def invalidateBackingStore(self): - """Invalidate the internal backing store""" - if self.__data.backingStore: - self.__data.backingStore = QPixmap() - - def setFocusIndicator(self, focusIndicator): - """ - Set the focus indicator - - Focus indicators: - - * `QwtPlotCanvas.NoFocusIndicator` - * `QwtPlotCanvas.CanvasFocusIndicator` - * `QwtPlotCanvas.ItemFocusIndicator` - - :param int focusIndicator: Focus indicator - - .. seealso:: - - :py:meth:`focusIndicator()` - """ - self.__data.focusIndicator = focusIndicator - - def focusIndicator(self): - """ - :return: Focus indicator - - .. seealso:: - - :py:meth:`setFocusIndicator()` - """ - return self.__data.focusIndicator - - def setBorderRadius(self, radius): - """ - Set the radius for the corners of the border frame - - :param float radius: Radius of a rounded corner - - .. seealso:: - - :py:meth:`borderRadius()` - """ - self.__data.borderRadius = max([0.0, radius]) - - def borderRadius(self): - """ - :return: Radius for the corners of the border frame - - .. seealso:: - - :py:meth:`setBorderRadius()` - """ - return self.__data.borderRadius - - def event(self, event): - if event.type() == QEvent.PolishRequest: - if self.testPaintAttribute(self.Opaque): - self.setAttribute(Qt.WA_OpaquePaintEvent, True) - if event.type() in (QEvent.PolishRequest, QEvent.StyleChange): - self.updateStyleSheetInfo() - return QFrame.event(self, event) - - def paintEvent(self, event): - painter = QPainter(self) - painter.setClipRegion(event.region()) - if ( - self.testPaintAttribute(self.BackingStore) - and self.__data.backingStore is not None - and not self.__data.backingStore.isNull() - ): - bs = self.__data.backingStore - pixelRatio = bs.devicePixelRatio() - if bs.size() != self.size() * pixelRatio: - bs = QwtPainter.backingStore(self, self.size()) - if self.testAttribute(Qt.WA_StyledBackground): - p = QPainter(bs) - qwtFillBackground(p, self) - self.drawCanvas(p, True) - else: - p = QPainter() - if self.__data.borderRadius <= 0.0: - # print('**DEBUG: QwtPlotCanvas.paintEvent') - QwtPainter.fillPixmap(self, bs) - p.begin(bs) - self.drawCanvas(p, False) - else: - p.begin(bs) - qwtFillBackground(p, self) - self.drawCanvas(p, True) - if self.frameWidth() > 0: - self.drawBorder(p) - p.end() - painter.drawPixmap(0, 0, self.__data.backingStore) - else: - if self.testAttribute(Qt.WA_StyledBackground): - if self.testAttribute(Qt.WA_OpaquePaintEvent): - qwtFillBackground(painter, self) - self.drawCanvas(painter, True) - else: - self.drawCanvas(painter, False) - else: - if self.testAttribute(Qt.WA_OpaquePaintEvent): - if self.autoFillBackground(): - qwtFillBackground(painter, self) - qwtDrawBackground(painter, self) - else: - if self.borderRadius() > 0.0: - clipPath = QPainterPath() - clipPath.addRect(self.rect()) - clipPath = clipPath.subtracted(self.borderPath(self.rect())) - painter.save() - painter.setClipPath(clipPath, Qt.IntersectClip) - qwtFillBackground(painter, self) - qwtDrawBackground(painter, self) - painter.restore() - self.drawCanvas(painter, False) - if self.frameWidth() > 0: - self.drawBorder(painter) - if self.hasFocus() and self.focusIndicator() == self.CanvasFocusIndicator: - self.drawFocusIndicator(painter) - - def drawCanvas(self, painter, withBackground): - hackStyledBackground = False - if ( - withBackground - and self.testAttribute(Qt.WA_StyledBackground) - and self.testPaintAttribute(self.HackStyledBackground) - ): - # Antialiasing rounded borders is done by - # inserting pixels with colors between the - # border color and the color on the canvas, - # When the border is painted before the plot items - # these colors are interpolated for the canvas - # and the plot items need to be clipped excluding - # the anialiased pixels. In situations, where - # the plot items fill the area at the rounded - # borders this is noticeable. - # The only way to avoid these annoying "artefacts" - # is to paint the border on top of the plot items. - if ( - self.__data.styleSheet.hasBorder - and not self.__data.styleSheet.borderPath.isEmpty() - ): - # We have a border with at least one rounded corner - hackStyledBackground = True - if withBackground: - painter.save() - if self.testAttribute(Qt.WA_StyledBackground): - if hackStyledBackground: - # paint background without border - painter.setPen(Qt.NoPen) - painter.setBrush(self.__data.styleSheet.background.brush) - painter.setBrushOrigin(self.__data.styleSheet.background.origin) - painter.setClipPath(self.__data.styleSheet.borderPath) - painter.drawRect(self.contentsRect()) - else: - qwtDrawStyledBackground(self, painter) - elif self.autoFillBackground(): - painter.setPen(Qt.NoPen) - painter.setBrush(self.palette().brush(self.backgroundRole())) - if self.__data.borderRadius > 0.0 and self.rect() == self.frameRect(): - if self.frameWidth() > 0: - painter.setClipPath(self.borderPath(self.rect())) - painter.drawRect(self.rect()) - else: - painter.setRenderHint(QPainter.Antialiasing, True) - painter.drawPath(self.borderPath(self.rect())) - else: - painter.drawRect(self.rect()) - painter.restore() - painter.save() - if not self.__data.styleSheet.borderPath.isEmpty(): - painter.setClipPath(self.__data.styleSheet.borderPath, Qt.IntersectClip) - else: - if self.__data.borderRadius > 0.0: - painter.setClipPath(self.borderPath(self.frameRect()), Qt.IntersectClip) - else: - # print('**DEBUG: QwtPlotCanvas.drawCanvas') - painter.setClipRect(self.contentsRect(), Qt.IntersectClip) - self.plot().drawCanvas(painter) - painter.restore() - if withBackground and hackStyledBackground: - # Now paint the border on top - opt = QStyleOptionFrame() - opt.initFrom(self) - self.style().drawPrimitive(QStyle.PE_Frame, opt, painter, self) - - def drawBorder(self, painter): - """ - Draw the border of the plot canvas - - :param QPainter painter: Painter - - .. seealso:: - - :py:meth:`setBorderRadius()` - """ - if self.__data.borderRadius > 0: - if self.frameWidth() > 0: - QwtPainter.drawRoundedFrame( - painter, - QRectF(self.frameRect()), - self.__data.borderRadius, - self.__data.borderRadius, - self.palette(), - self.frameWidth(), - self.frameStyle(), - ) - else: - opt = QStyleOptionFrame() - opt.initFrom(self) - try: - shape_mask = QFrame.Shape_Mask.value - shadow_mask = QFrame.Shadow_Mask.value - except AttributeError: - shape_mask = QFrame.Shape_Mask - shadow_mask = QFrame.Shadow_Mask - frameShape = self.frameStyle() & shape_mask - frameShadow = self.frameStyle() & shadow_mask - opt.frameShape = QFrame.Shape(int(opt.frameShape) | frameShape) - if frameShape in ( - QFrame.Box, - QFrame.HLine, - QFrame.VLine, - QFrame.StyledPanel, - QFrame.Panel, - ): - opt.lineWidth = self.lineWidth() - opt.midLineWidth = self.midLineWidth() - else: - opt.lineWidth = self.frameWidth() - if frameShadow == QFrame.Sunken: - opt.state |= QStyle.State_Sunken - elif frameShadow == QFrame.Raised: - opt.state |= QStyle.State_Raised - self.style().drawControl(QStyle.CE_ShapedFrame, opt, painter, self) - - def resizeEvent(self, event): - QFrame.resizeEvent(self, event) - self.updateStyleSheetInfo() - - def drawFocusIndicator(self, painter): - """ - Draw the focus indication - - :param QPainter painter: Painter - """ - margin = 1 - focusRect = self.contentsRect() - focusRect.setRect( - focusRect.x() + margin, - focusRect.y() + margin, - focusRect.width() - 2 * margin, - focusRect.height() - 2 * margin, - ) - QwtPainter.drawFocusRect(painter, self, focusRect) - - def replot(self): - """ - Invalidate the paint cache and repaint the canvas - """ - self.invalidateBackingStore() - if self.testPaintAttribute(self.ImmediatePaint): - self.repaint(self.contentsRect()) - else: - self.update(self.contentsRect()) - - def invalidatePaintCache(self): - import warnings - - warnings.warn( - "`invalidatePaintCache` has been removed: please use `replot` instead", - RuntimeWarning, - ) - self.replot() - - def updateStyleSheetInfo(self): - """ - Update the cached information about the current style sheet - """ - if not self.testAttribute(Qt.WA_StyledBackground): - return - recorder = QwtStyleSheetRecorder(self.size()) - painter = QPainter(recorder) - opt = QStyleOption() - opt.initFrom(self) - self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) - painter.end() - self.__data.styleSheet.hasBorder = len(recorder.border.rectList) > 0 - self.__data.styleSheet.cornerRects = recorder.clipRects - if recorder.background.path.isEmpty(): - if self.__data.styleSheet.hasBorder: - self.__data.styleSheet.borderPath = qwtCombinePathList( - self.rect(), recorder.border.pathlist - ) - else: - self.__data.styleSheet.borderPath = recorder.background.path - self.__data.styleSheet.background.brush = recorder.background.brush - self.__data.styleSheet.background.origin = recorder.background.origin - - def borderPath(self, rect): - """ - Calculate the painter path for a styled or rounded border - - When the canvas has no styled background or rounded borders - the painter path is empty. - - :param QRect rect: Bounding rectangle of the canvas - :return: Painter path, that can be used for clipping - """ - if self.testAttribute(Qt.WA_StyledBackground): - recorder = QwtStyleSheetRecorder(rect.size()) - painter = QPainter(recorder) - opt = QStyleOption() - opt.initFrom(self) - opt.rect = rect - self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) - painter.end() - if not recorder.background.path.isEmpty(): - return recorder.background.path - if len(recorder.border.rectList) > 0: - return qwtCombinePathList(rect, recorder.border.pathlist) - elif self.__data.borderRadius > 0.0: - fw2 = self.frameWidth() * 0.5 - r = QRectF(rect).adjusted(fw2, fw2, -fw2, -fw2) - path = QPainterPath() - path.addRoundedRect(r, self.__data.borderRadius, self.__data.borderRadius) - return path - return QPainterPath() diff --git a/qwt/plot_curve.py b/qwt/plot_curve.py deleted file mode 100644 index 6b4105e..0000000 --- a/qwt/plot_curve.py +++ /dev/null @@ -1,1054 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotCurve ------------- - -.. autoclass:: QwtPlotCurve - :members: -""" - -import math -import os - -import numpy as np -from qtpy.QtCore import QLineF, QPointF, QRectF, QSize, Qt -from qtpy.QtGui import QBrush, QColor, QPainter, QPen, QPolygonF - -from qwt._math import qwtSqr -from qwt.graphic import QwtGraphic -from qwt.plot import QwtPlot, QwtPlotItem, QwtPlotItem_PrivateData -from qwt.plot_directpainter import QwtPlotDirectPainter -from qwt.plot_series import ( - QwtPlotSeriesItem, - QwtPointArrayData, - QwtSeriesData, - QwtSeriesStore, -) -from qwt.qthelpers import qcolor_from_str -from qwt.symbol import QwtSymbol -from qwt.text import QwtText - -QT_API = os.environ["QT_API"] - -if QT_API == "pyside6": - import ctypes - - import shiboken6 as shiboken - - -def qwtUpdateLegendIconSize(curve): - if curve.symbol() and curve.testLegendAttribute(QwtPlotCurve.LegendShowSymbol): - sz = curve.symbol().boundingRect().size() - sz += QSize(2, 2) - if curve.testLegendAttribute(QwtPlotCurve.LegendShowLine): - w = math.ceil(1.5 * sz.width()) - if w % 2: - w += 1 - sz.setWidth(max([8, w])) - curve.setLegendIconSize(sz) - - -def qwtVerifyRange(size, i1, i2): - if size < 1: - return 0 - i1 = max([0, min([i1, size - 1])]) - i2 = max([0, min([i2, size - 1])]) - if i1 > i2: - i1, i2 = i2, i1 - return i2 - i1 + 1 - - -def array2d_to_qpolygonf(xdata, ydata): - """ - Utility function to convert two 1D-NumPy arrays representing curve data - (X-axis, Y-axis data) into a single polyline (QtGui.PolygonF object). - This feature is compatible with PyQt5 and PySide6 (requires QtPy). - - License/copyright: MIT License © Pierre Raybaut 2020-2021. - - :param numpy.ndarray xdata: 1D-NumPy array - :param numpy.ndarray ydata: 1D-NumPy array - :return: Polyline - :rtype: QtGui.QPolygonF - """ - if not (xdata.size == ydata.size == xdata.shape[0] == ydata.shape[0]): - raise ValueError("Arguments must be 1D NumPy arrays with same size") - size = xdata.size - if QT_API.startswith("pyside"): # PySide (obviously...) - polyline = QPolygonF() - polyline.resize(size) - address = shiboken.getCppPointer(polyline.data())[0] - buffer = (ctypes.c_double * 2 * size).from_address(address) - else: # PyQt - if QT_API == "pyqt6": - polyline = QPolygonF([QPointF(0, 0)] * size) - else: - polyline = QPolygonF(size) - buffer = polyline.data() - buffer.setsize(16 * size) # 16 bytes per point: 8 bytes per X,Y value (float64) - memory = np.frombuffer(buffer, np.float64) - memory[: (size - 1) * 2 + 1 : 2] = np.asarray(xdata, dtype=np.float64) - memory[1 : (size - 1) * 2 + 2 : 2] = np.asarray(ydata, dtype=np.float64) - return polyline - - -def series_to_polyline(xMap, yMap, series, from_, to): - """ - Convert series data to QPolygon(F) polyline - """ - xdata = xMap.transform(series.xData()[from_ : to + 1]) - ydata = yMap.transform(series.yData()[from_ : to + 1]) - return array2d_to_qpolygonf(xdata, ydata) - - -class QwtPlotCurve_PrivateData(QwtPlotItem_PrivateData): - def __init__(self): - QwtPlotItem_PrivateData.__init__(self) - self.style = QwtPlotCurve.Lines - self.baseline = 0.0 - self.symbol = None - self.attributes = 0 - self.legendAttributes = QwtPlotCurve.LegendShowLine - self.pen = QPen(Qt.black) - self.brush = QBrush() - - -class QwtPlotCurve(QwtPlotSeriesItem, QwtSeriesStore): - """ - A plot item, that represents a series of points - - A curve is the representation of a series of points in the x-y plane. - It supports different display styles and symbols. - - .. seealso:: - - :py:class:`qwt.symbol.QwtSymbol()`, - :py:class:`qwt.scale_map.QwtScaleMap()` - - Curve styles: - - * `QwtPlotCurve.NoCurve`: - - Don't draw a curve. Note: This doesn't affect the symbols. - - * `QwtPlotCurve.Lines`: - - Connect the points with straight lines. - - * `QwtPlotCurve.Sticks`: - - Draw vertical or horizontal sticks ( depending on the - orientation() ) from a baseline which is defined by setBaseline(). - - * `QwtPlotCurve.Steps`: - - Connect the points with a step function. The step function - is drawn from the left to the right or vice versa, - depending on the QwtPlotCurve::Inverted attribute. - - * `QwtPlotCurve.Dots`: - - Draw dots at the locations of the data points. Note: - This is different from a dotted line (see setPen()), and faster - as a curve in QwtPlotCurve::NoStyle style and a symbol - painting a point. - - * `QwtPlotCurve.UserCurve`: - - Styles >= QwtPlotCurve.UserCurve are reserved for derived - classes of QwtPlotCurve that overload drawCurve() with - additional application specific curve types. - - Curve attributes: - - * `QwtPlotCurve.Inverted`: - - For `QwtPlotCurve.Steps` only. - Draws a step function from the right to the left. - - Legend attributes: - - * `QwtPlotCurve.LegendNoAttribute`: - - `QwtPlotCurve` tries to find a color representing the curve - and paints a rectangle with it. - - * `QwtPlotCurve.LegendShowLine`: - - If the style() is not `QwtPlotCurve.NoCurve` a line - is painted with the curve pen(). - - * `QwtPlotCurve.LegendShowSymbol`: - - If the curve has a valid symbol it is painted. - - * `QwtPlotCurve.LegendShowBrush`: - - If the curve has a brush a rectangle filled with the - curve brush() is painted. - - - .. py:class:: QwtPlotCurve([title=None]) - - Constructor - - :param title: Curve title - :type title: qwt.text.QwtText or str or None - """ - - # enum CurveStyle - NoCurve = -1 - Lines, Sticks, Steps, Dots = list(range(4)) - UserCurve = 100 - - # enum CurveAttribute - Inverted = 0x01 - - # enum LegendAttribute - LegendNoAttribute = 0x00 - LegendShowLine = 0x01 - LegendShowSymbol = 0x02 - LegendShowBrush = 0x04 - - def __init__(self, title=None): - if title is None: - title = QwtText("") - if not isinstance(title, QwtText): - title = QwtText(title) - self.__data = None - QwtPlotSeriesItem.__init__(self, title) - QwtSeriesStore.__init__(self) - self.init() - - @classmethod - def make( - cls, - xdata=None, - ydata=None, - title=None, - plot=None, - z=None, - x_axis=None, - y_axis=None, - style=None, - symbol=None, - linecolor=None, - linewidth=None, - linestyle=None, - antialiased=False, - size=None, - finite=None, - ): - """ - Create and setup a new `QwtPlotCurve` object (convenience function). - - :param xdata: List/array of x values - :param ydata: List/array of y values - :param title: Curve title - :type title: qwt.text.QwtText or str or None - :param plot: Plot to attach the curve to - :type plot: qwt.plot.QwtPlot or None - :param z: Z-value - :type z: float or None - :param x_axis: curve X-axis (default: QwtPlot.yLeft) - :type x_axis: int or None - :param y_axis: curve Y-axis (default: QwtPlot.xBottom) - :type y_axis: int or None - :param style: curve style (`QwtPlotCurve.NoCurve`, `QwtPlotCurve.Lines`, `QwtPlotCurve.Sticks`, `QwtPlotCurve.Steps`, `QwtPlotCurve.Dots`, `QwtPlotCurve.UserCurve`) - :type style: int or None - :param symbol: curve symbol - :type symbol: qwt.symbol.QwtSymbol or None - :param linecolor: curve line color - :type linecolor: QColor or str or None - :param linewidth: curve line width - :type linewidth: float or None - :param linestyle: curve pen style - :type linestyle: Qt.PenStyle or None - :param bool antialiased: if True, enable antialiasing rendering - :param size: size of xData and yData - :type size: int or None - :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements - - .. seealso:: - - :py:meth:`setData()`, :py:meth:`setPen()`, :py:meth:`attach()` - """ - item = cls(title) - if z is not None: - item.setZ(z) - if xdata is not None or ydata is not None: - if xdata is None: - raise ValueError("Missing xdata parameter") - if ydata is None: - raise ValueError("Missing ydata parameter") - item.setData(xdata, ydata, size=size, finite=finite) - x_axis = QwtPlot.xBottom if x_axis is None else x_axis - y_axis = QwtPlot.yLeft if y_axis is None else y_axis - item.setAxes(x_axis, y_axis) - if style is not None: - item.setStyle(style) - if symbol is not None: - item.setSymbol(symbol) - linecolor = qcolor_from_str(linecolor, Qt.black) - linewidth = 1.0 if linewidth is None else linewidth - linestyle = Qt.SolidLine if linestyle is None else linestyle - item.setPen(QPen(linecolor, linewidth, linestyle)) - item.setRenderHint(cls.RenderAntialiased, antialiased) - if plot is not None: - item.attach(plot) - return item - - def init(self): - """Initialize internal members""" - self.__data = QwtPlotCurve_PrivateData() - self.setItemAttribute(QwtPlotItem.Legend) - self.setItemAttribute(QwtPlotItem.AutoScale) - self.setData(QwtPointArrayData()) - self.setZ(20.0) - - def rtti(self): - """:return: `QwtPlotItem.Rtti_PlotCurve`""" - return QwtPlotItem.Rtti_PlotCurve - - def setLegendAttribute(self, attribute, on=True): - """ - Specify an attribute how to draw the legend icon - - Legend attributes: - - * `QwtPlotCurve.LegendNoAttribute` - * `QwtPlotCurve.LegendShowLine` - * `QwtPlotCurve.LegendShowSymbol` - * `QwtPlotCurve.LegendShowBrush` - - :param int attribute: Legend attribute - :param bool on: On/Off - - .. seealso:: - - :py:meth:`testLegendAttribute()`, :py:meth:`legendIcon()` - """ - if on != self.testLegendAttribute(attribute): - if on: - self.__data.legendAttributes |= attribute - else: - self.__data.legendAttributes &= ~attribute - qwtUpdateLegendIconSize(self) - self.legendChanged() - - def testLegendAttribute(self, attribute): - """ - :param int attribute: Legend attribute - :return: True, when attribute is enabled - - .. seealso:: - - :py:meth:`setLegendAttribute()` - """ - return self.__data.legendAttributes & attribute - - def setStyle(self, style): - """ - Set the curve's drawing style - - Valid curve styles: - - * `QwtPlotCurve.NoCurve` - * `QwtPlotCurve.Lines` - * `QwtPlotCurve.Sticks` - * `QwtPlotCurve.Steps` - * `QwtPlotCurve.Dots` - * `QwtPlotCurve.UserCurve` - - :param int style: Curve style - - .. seealso:: - - :py:meth:`style()` - """ - if style != self.__data.style: - self.__data.style = style - self.legendChanged() - self.itemChanged() - - def style(self): - """ - :return: Style of the curve - - .. seealso:: - - :py:meth:`setStyle()` - """ - return self.__data.style - - def setSymbol(self, symbol): - """ - Assign a symbol - - The curve will take the ownership of the symbol, hence the previously - set symbol will be delete by setting a new one. If symbol is None no - symbol will be drawn. - - :param qwt.symbol.QwtSymbol symbol: Symbol - - .. seealso:: - - :py:meth:`symbol()` - """ - if symbol != self.__data.symbol: - self.__data.symbol = symbol - qwtUpdateLegendIconSize(self) - self.legendChanged() - self.itemChanged() - - def symbol(self): - """ - :return: Current symbol or None, when no symbol has been assigned - - .. seealso:: - - :py:meth:`setSymbol()` - """ - return self.__data.symbol - - def setPen(self, *args): - """ - Build and/or assign a pen, depending on the arguments. - - .. py:method:: setPen(color, width, style) - :noindex: - - Build and assign a pen - - In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it - non cosmetic (see `QPen.isCosmetic()`). This method signature has - been introduced to hide this incompatibility. - - :param QColor color: Pen color - :param float width: Pen width - :param Qt.PenStyle style: Pen style - - .. py:method:: setPen(pen) - :noindex: - - Assign a pen - - :param QPen pen: New pen - - .. seealso:: - - :py:meth:`pen()`, :py:meth:`brush()` - """ - if len(args) == 3: - color, width, style = args - pen = QPen(color, width, style) - elif len(args) == 1: - (pen,) = args - else: - raise TypeError( - "%s().setPen() takes 1 or 3 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - if pen != self.__data.pen: - if isinstance(pen, QColor): - pen = QPen(pen) - else: - assert isinstance(pen, QPen) - self.__data.pen = pen - self.legendChanged() - self.itemChanged() - - def pen(self): - """ - :return: Pen used to draw the lines - - .. seealso:: - - :py:meth:`setPen()`, :py:meth:`brush()` - """ - return self.__data.pen - - def setBrush(self, brush): - """ - Assign a brush. - - In case of `brush.style() != QBrush.NoBrush` - and `style() != QwtPlotCurve.Sticks` - the area between the curve and the baseline will be filled. - - In case `not brush.color().isValid()` the area will be filled by - `pen.color()`. The fill algorithm simply connects the first and the - last curve point to the baseline. So the curve data has to be sorted - (ascending or descending). - - :param brush: New brush - :type brush: QBrush or QColor - - .. seealso:: - - :py:meth:`brush()`, :py:meth:`setBaseline()`, :py:meth:`baseline()` - """ - if isinstance(brush, QColor): - brush = QBrush(brush) - else: - assert isinstance(brush, QBrush) - if brush != self.__data.brush: - self.__data.brush = brush - self.legendChanged() - self.itemChanged() - - def brush(self): - """ - :return: Brush used to fill the area between lines and the baseline - - .. seealso:: - - :py:meth:`setBrush()`, :py:meth:`setBaseline()`, - :py:meth:`baseline()` - """ - return self.__data.brush - - def directPaint(self, from_, to): - """ - When observing a measurement while it is running, new points have - to be added to an existing seriesItem. This method can be used to - display them avoiding a complete redraw of the canvas. - - Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)` - will result in faster painting, if the paint engine of the canvas - widget supports this feature. - - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted - - .. seealso:: - - :py:meth:`drawSeries()` - """ - directPainter = QwtPlotDirectPainter(self.plot()) - directPainter.drawSeries(self, from_, to) - - def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to): - """ - Draw an interval of the curve - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`drawCurve()`, :py:meth:`drawSymbols()` - """ - numSamples = self.dataSize() - if not painter or numSamples <= 0: - return - if to < 0: - to = numSamples - 1 - if qwtVerifyRange(numSamples, from_, to) > 0: - painter.save() - painter.setPen(self.__data.pen) - self.drawCurve( - painter, self.__data.style, xMap, yMap, canvasRect, from_, to - ) - painter.restore() - if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol: - painter.save() - self.drawSymbols( - painter, self.__data.symbol, xMap, yMap, canvasRect, from_, to - ) - painter.restore() - - def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to): - """ - Draw the line part (without symbols) of a curve interval. - - :param QPainter painter: Painter - :param int style: curve style, see `QwtPlotCurve.CurveStyle` - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawLines()`, - :py:meth:`drawSteps()`, :py:meth:`drawSticks()` - """ - if style == self.Lines: - self.drawLines(painter, xMap, yMap, canvasRect, from_, to) - elif style == self.Sticks: - self.drawSticks(painter, xMap, yMap, canvasRect, from_, to) - elif style == self.Steps: - self.drawSteps(painter, xMap, yMap, canvasRect, from_, to) - elif style == self.Dots: - self.drawDots(painter, xMap, yMap, canvasRect, from_, to) - - def drawLines(self, painter, xMap, yMap, canvasRect, from_, to): - """ - Draw lines - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`draw()`, :py:meth:`drawDots()`, - :py:meth:`drawSteps()`, :py:meth:`drawSticks()` - """ - if from_ > to: - return - doFill = ( - self.__data.brush.style() != Qt.NoBrush - and self.__data.brush.color().alpha() > 0 - ) - polyline = series_to_polyline(xMap, yMap, self.data(), from_, to) - painter.drawPolyline(polyline) - if doFill: - self.fillCurve(painter, xMap, yMap, canvasRect, polyline) - - def drawSticks(self, painter, xMap, yMap, canvasRect, from_, to): - """ - Draw sticks - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`draw()`, :py:meth:`drawDots()`, - :py:meth:`drawSteps()`, :py:meth:`drawLines()` - """ - painter.save() - painter.setRenderHint(QPainter.Antialiasing, False) - x0 = xMap.transform(self.__data.baseline) - y0 = yMap.transform(self.__data.baseline) - o = self.orientation() - series = self.data() - for i in range(from_, to + 1): - sample = series.sample(i) - xi = xMap.transform(sample.x()) - yi = yMap.transform(sample.y()) - if o == Qt.Horizontal: - painter.drawLine(QLineF(xi, y0, xi, yi)) - else: - painter.drawLine(QLineF(x0, yi, xi, yi)) - painter.restore() - - def drawDots(self, painter, xMap, yMap, canvasRect, from_, to): - """ - Draw dots - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`draw()`, :py:meth:`drawSticks()`, - :py:meth:`drawSteps()`, :py:meth:`drawLines()` - """ - doFill = ( - self.__data.brush.style() != Qt.NoBrush - and self.__data.brush.color().alpha() > 0 - ) - polyline = series_to_polyline(xMap, yMap, self.data(), from_, to) - painter.drawPoints(polyline) - if doFill: - self.fillCurve(painter, xMap, yMap, canvasRect, polyline) - - def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to): - """ - Draw steps - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`draw()`, :py:meth:`drawSticks()`, - :py:meth:`drawDots()`, :py:meth:`drawLines()` - """ - size = 2 * (to - from_) + 1 - if QT_API == "pyside6": - polygon = QPolygonF() - polygon.resize(size) - elif QT_API == "pyqt6": - polygon = QPolygonF([QPointF(0, 0)] * size) - else: - polygon = QPolygonF(size) - inverted = self.orientation() == Qt.Vertical - if self.__data.attributes & self.Inverted: - inverted = not inverted - series = self.data() - ip = 0 - for i in range(from_, to + 1): - sample = series.sample(i) - xi = xMap.transform(sample.x()) - yi = yMap.transform(sample.y()) - if ip > 0: - p0 = polygon[ip - 2] - if inverted: - polygon[ip - 1] = QPointF(p0.x(), yi) - else: - polygon[ip - 1] = QPointF(xi, p0.y()) - polygon[ip] = QPointF(xi, yi) - ip += 2 - painter.drawPolyline(polygon) - if self.__data.brush.style() != Qt.NoBrush: - self.fillCurve(painter, xMap, yMap, canvasRect, polygon) - - def setCurveAttribute(self, attribute, on=True): - """ - Specify an attribute for drawing the curve - - Supported curve attributes: - - * `QwtPlotCurve.Inverted` - - :param int attribute: Curve attribute - :param bool on: On/Off - - .. seealso:: - - :py:meth:`testCurveAttribute()` - """ - if (self.__data.attributes & attribute) == on: - return - if on: - self.__data.attributes |= attribute - else: - self.__data.attributes &= ~attribute - self.itemChanged() - - def testCurveAttribute(self, attribute): - """ - :return: True, if attribute is enabled - - .. seealso:: - - :py:meth:`setCurveAttribute()` - """ - return self.__data.attributes & attribute - - def fillCurve(self, painter, xMap, yMap, canvasRect, polygon): - """ - Fill the area between the curve and the baseline with - the curve brush - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param QPolygonF polygon: Polygon - will be modified ! - - .. seealso:: - - :py:meth:`setBrush()`, :py:meth:`setBaseline()`, - :py:meth:`setStyle()` - """ - if self.__data.brush.style() == Qt.NoBrush: - return - self.closePolyline(painter, xMap, yMap, polygon) - if polygon.count() <= 2: - return - brush = self.__data.brush - if not brush.color().isValid(): - brush.setColor(self.__data.pen.color()) - painter.save() - painter.setPen(Qt.NoPen) - painter.setBrush(brush) - painter.drawPolygon(polygon) - painter.restore() - - def closePolyline(self, painter, xMap, yMap, polygon): - """ - Complete a polygon to be a closed polygon including the - area between the original polygon and the baseline. - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QPolygonF polygon: Polygon to be completed - """ - if polygon.size() < 2: - return - baseline = self.__data.baseline - if self.orientation() == Qt.Horizontal: - if yMap.transformation(): - baseline = yMap.transformation().bounded(baseline) - refY = yMap.transform(baseline) - polygon.append(QPointF(polygon.last().x(), refY)) - polygon.append(QPointF(polygon.first().x(), refY)) - else: - if xMap.transformation(): - baseline = xMap.transformation().bounded(baseline) - refX = xMap.transform(baseline) - polygon.append(QPointF(refX, polygon.last().y())) - polygon.append(QPointF(refX, polygon.first().y())) - - def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to): - """ - Draw symbols - - :param QPainter painter: Painter - :param qwt.symbol.QwtSymbol symbol: Curve symbol - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - :py:meth:`setSymbol()`, :py:meth:`drawSeries()`, - :py:meth:`drawCurve()` - """ - chunkSize = 500 - for i in range(from_, to + 1, chunkSize): - n = min([chunkSize, to - i + 1]) - points = series_to_polyline(xMap, yMap, self.data(), i, i + n - 1) - if points.size() > 0: - symbol.drawSymbols(painter, points) - - def setBaseline(self, value): - """ - Set the value of the baseline - - The baseline is needed for filling the curve with a brush or - the Sticks drawing style. - - The interpretation of the baseline depends on the `orientation()`. - With `Qt.Horizontal`, the baseline is interpreted as a horizontal line - at y = baseline(), with `Qt.Vertical`, it is interpreted as a vertical - line at x = baseline(). - - The default value is 0.0. - - :param float value: Value of the baseline - - .. seealso:: - - :py:meth:`baseline()`, :py:meth:`setBrush()`, - :py:meth:`setStyle()` - """ - if self.__data.baseline != value: - self.__data.baseline = value - self.itemChanged() - - def baseline(self): - """ - :return: Value of the baseline - - .. seealso:: - - :py:meth:`setBaseline()` - """ - return self.__data.baseline - - def closestPoint(self, pos): - """ - Find the closest curve point for a specific position - - :param QPoint pos: Position, where to look for the closest curve point - :return: tuple `(index, dist)` - - `dist` is the distance between the position and the closest curve - point. `index` is the index of the closest curve point, or -1 if - none can be found ( f.e when the curve has no points ). - - .. note:: - - `closestPoint()` implements a dumb algorithm, that iterates - over all points - """ - numSamples = self.dataSize() - if self.plot() is None or numSamples <= 0: - return -1 - series = self.data() - xMap = self.plot().canvasMap(self.xAxis()) - yMap = self.plot().canvasMap(self.yAxis()) - index = -1 - dmin = 1.0e10 - for i in range(numSamples): - sample = series.sample(i) - cx = xMap.transform(sample.x()) - pos.x() - cy = yMap.transform(sample.y()) - pos.y() - f = qwtSqr(cx) + qwtSqr(cy) - if f < dmin: - index = i - dmin = f - dist = math.sqrt(dmin) - return index, dist - - def legendIcon(self, index, size): - """ - :param int index: Index of the legend entry (ignored as there is only one) - :param QSizeF size: Icon size - :return: Icon representing the curve on the legend - - .. seealso:: - - :py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`, - :py:meth:`qwt.plot.QwtPlotItem.legendData()` - """ - if size.isEmpty(): - return QwtGraphic() - graphic = QwtGraphic() - graphic.setDefaultSize(size) - graphic.setRenderHint(QwtGraphic.RenderPensUnscaled, True) - painter = QPainter(graphic) - painter.setRenderHint( - QPainter.Antialiasing, self.testRenderHint(QwtPlotItem.RenderAntialiased) - ) - if self.__data.legendAttributes == 0 or ( - self.__data.legendAttributes & QwtPlotCurve.LegendShowBrush - ): - brush = self.__data.brush - if brush.style() == Qt.NoBrush and self.__data.legendAttributes == 0: - if self.style() != QwtPlotCurve.NoCurve: - brush = QBrush(self.pen().color()) - elif ( - self.__data.symbol - and self.__data.symbol.style() != QwtSymbol.NoSymbol - ): - brush = QBrush(self.__data.symbol.pen().color()) - if brush.style() != Qt.NoBrush: - r = QRectF(0, 0, size.width(), size.height()) - painter.fillRect(r, brush) - if self.__data.legendAttributes & QwtPlotCurve.LegendShowLine: - if self.pen() != Qt.NoPen: - painter.setPen(self.pen()) - y = size.height() // 2 - painter.drawLine(QLineF(0, y, size.width(), y)) - if self.__data.legendAttributes & QwtPlotCurve.LegendShowSymbol: - if self.__data.symbol: - r = QRectF(0, 0, size.width(), size.height()) - self.__data.symbol.drawSymbol(painter, r) - return graphic - - def setData(self, *args, **kwargs): - """ - Initialize data with a series data object or an array of points. - - .. py:method:: setData(data): - - :param data: Series data (e.g. `QwtPointArrayData` instance) - :type data: .plot_series.QwtSeriesData - - .. py:method:: setData(xData, yData, [size=None], [finite=True]): - - Initialize data with `x` and `y` arrays. - - This signature was removed in Qwt6 and is temporarily maintained here to ensure compatibility with Qwt5. - - Same as `setSamples(x, y, [size=None], [finite=True])` - - :param x: List/array of x values - :param y: List/array of y values - :param size: size of xData and yData - :type size: int or None - :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements - - .. seealso:: - - :py:meth:`setSamples()` - """ - if len(args) == 1 and not kwargs: - super(QwtPlotCurve, self).setData(*args) - elif len(args) in (2, 3, 4): - self.setSamples(*args, **kwargs) - else: - raise TypeError( - "%s().setData() takes 1, 2, 3 or 4 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def setSamples(self, *args, **kwargs): - """ - Initialize data with an array of points. - - .. py:method:: setSamples(data): - - :param data: Series data (e.g. `QwtPointArrayData` instance) - :type data: .plot_series.QwtSeriesData - - - .. py:method:: setSamples(samples): - - Same as `setData(QwtPointArrayData(samples))` - - :param samples: List/array of points - - .. py:method:: setSamples(xData, yData, [size=None], [finite=True]): - - Same as `setData(QwtPointArrayData(xData, yData, [size=None]))` - - :param xData: List/array of x values - :param yData: List/array of y values - :param size: size of xData and yData - :type size: int or None - :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements - - .. seealso:: - - :py:class:`.plot_series.QwtPointArrayData` - """ - if len(args) == 1 and not kwargs: - (samples,) = args - if isinstance(samples, QwtSeriesData): - self.setData(samples) - else: - self.setData(QwtPointArrayData(samples)) - elif len(args) >= 2: - xData, yData = args[:2] - try: - size = kwargs.pop("size") - except KeyError: - size = None - try: - finite = kwargs.pop("finite") - except KeyError: - finite = None - if kwargs: - raise TypeError( - "%s().setSamples(): unknown %s keyword " - "argument(s)" - % (self.__class__.__name__, ", ".join(list(kwargs.keys()))) - ) - for arg in args[2:]: - if isinstance(arg, bool): - finite = arg - elif isinstance(arg, int): - size = arg - self.setData(QwtPointArrayData(xData, yData, size=size, finite=finite)) - else: - raise TypeError( - "%s().setSamples() takes 1, 2 or 3 argument(s) " - "(%s given)" % (self.__class__.__name__, len(args)) - ) diff --git a/qwt/plot_directpainter.py b/qwt/plot_directpainter.py deleted file mode 100644 index 06697d3..0000000 --- a/qwt/plot_directpainter.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotDirectPainter --------------------- - -.. autoclass:: QwtPlotDirectPainter - :members: -""" - -from qtpy.QtCore import QEvent, QObject, Qt -from qtpy.QtGui import QPainter, QRegion - -from qwt.plot import QwtPlotItem -from qwt.plot_canvas import QwtPlotCanvas - - -def qwtRenderItem(painter, canvasRect, seriesItem, from_, to): - # TODO: A minor performance improvement is possible with caching the maps - plot = seriesItem.plot() - xMap = plot.canvasMap(seriesItem.xAxis()) - yMap = plot.canvasMap(seriesItem.yAxis()) - painter.setRenderHint( - QPainter.Antialiasing, seriesItem.testRenderHint(QwtPlotItem.RenderAntialiased) - ) - seriesItem.drawSeries(painter, xMap, yMap, canvasRect, from_, to) - - -def qwtHasBackingStore(canvas): - return ( - canvas.testPaintAttribute(QwtPlotCanvas.BackingStore) - and canvas.backingStore() is not None - and not canvas.backingStore().isNull() - ) - - -class QwtPlotDirectPainter_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.attributes = 0 - self.hasClipping = False - self.seriesItem = None # QwtPlotSeriesItem - self.clipRegion = QRegion() - self.painter = QPainter() - self.from_ = None - self.to = None - - -class QwtPlotDirectPainter(QObject): - """ - Painter object trying to paint incrementally - - Often applications want to display samples while they are - collected. When there are too many samples complete replots - will be expensive to be processed in a collection cycle. - - `QwtPlotDirectPainter` offers an API to paint - subsets (f.e all additions points) without erasing/repainting - the plot canvas. - - On certain environments it might be important to calculate a proper - clip region before painting. F.e. for Qt Embedded only the clipped part - of the backing store will be copied to a (maybe unaccelerated) - frame buffer. - - .. warning:: - - Incremental painting will only help when no replot is triggered - by another operation (like changing scales) and nothing needs - to be erased. - - Paint attributes: - - * `QwtPlotDirectPainter.AtomicPainter`: - - Initializing a `QPainter` is an expensive operation. - When `AtomicPainter` is set each call of `drawSeries()` opens/closes - a temporary `QPainter`. Otherwise `QwtPlotDirectPainter` tries to - use the same `QPainter` as long as possible. - - * `QwtPlotDirectPainter.FullRepaint`: - - When `FullRepaint` is set the plot canvas is explicitly repainted - after the samples have been rendered. - - * `QwtPlotDirectPainter.CopyBackingStore`: - - When `QwtPlotCanvas.BackingStore` is enabled the painter - has to paint to the backing store and the widget. In certain - situations/environments it might be faster to paint to - the backing store only and then copy the backing store to the canvas. - This flag can also be useful for settings, where Qt fills the - the clip region with the widget background. - """ - - # enum Attribute - AtomicPainter = 0x01 - FullRepaint = 0x02 - CopyBackingStore = 0x04 - - def __init__(self, parent=None): - QObject.__init__(self, parent) - self.__data = QwtPlotDirectPainter_PrivateData() - - def setAttribute(self, attribute, on=True): - """ - Change an attribute - - :param int attribute: Attribute to change - :param bool on: On/Off - - .. seealso:: - - :py:meth:`testAttribute()` - """ - if self.testAttribute(attribute) != on: - self.__data.attributes |= attribute - else: - self.__data.attributes &= ~attribute - if attribute == self.AtomicPainter and on: - self.reset() - - def testAttribute(self, attribute): - """ - :param int attribute: Attribute to be tested - :return: True, when attribute is enabled - - .. seealso:: - - :py:meth:`setAttribute()` - """ - return self.__data.attributes & attribute - - def setClipping(self, enable): - """ - En/Disables clipping - - :param bool enable: Enables clipping is true, disable it otherwise - - .. seealso:: - - :py:meth:`hasClipping()`, :py:meth:`clipRegion()`, - :py:meth:`setClipRegion()` - """ - self.__data.hasClipping = enable - - def hasClipping(self): - """ - :return: Return true, when clipping is enabled - - .. seealso:: - - :py:meth:`setClipping()`, :py:meth:`clipRegion()`, - :py:meth:`setClipRegion()` - """ - return self.__data.hasClipping - - def setClipRegion(self, region): - """ - Assign a clip region and enable clipping - - Depending on the environment setting a proper clip region might - improve the performance heavily. F.e. on Qt embedded only the clipped - part of the backing store will be copied to a (maybe unaccelerated) - frame buffer device. - - :param QRegion region: Clip region - - .. seealso:: - - :py:meth:`hasClipping()`, :py:meth:`setClipping()`, - :py:meth:`clipRegion()` - """ - self.__data.clipRegion = region - self.__data.hasClipping = True - - def clipRegion(self): - """ - :return: Return Currently set clip region. - - .. seealso:: - - :py:meth:`hasClipping()`, :py:meth:`setClipping()`, - :py:meth:`setClipRegion()` - """ - return self.__data.clipRegion - - def drawSeries(self, seriesItem, from_, to): - """ - Draw a set of points of a seriesItem. - - When observing a measurement while it is running, new points have - to be added to an existing seriesItem. drawSeries() can be used to - display them avoiding a complete redraw of the canvas. - - Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)` - will result in faster painting, if the paint engine of the canvas widget - supports this feature. - - :param qwt.plot_series.QwtPlotSeriesItem seriesItem: Item to be painted - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the series will be painted to its last point. - """ - if seriesItem is None or seriesItem.plot() is None: - return - canvas = seriesItem.plot().canvas() - canvasRect = canvas.contentsRect() - if canvas and qwtHasBackingStore(canvas): - painter = QPainter(canvas.backingStore()) - if self.__data.hasClipping: - painter.setClipRegion(self.__data.clipRegion) - qwtRenderItem(painter, canvasRect, seriesItem, from_, to) - painter.end() - if self.testAttribute(self.FullRepaint): - canvas.repaint() - return - if canvas.testAttribute(Qt.WA_WState_InPaintEvent): - if not self.__data.painter.isActive(): - self.reset() - self.__data.painter.begin(canvas) - canvas.installEventFilter(self) - if self.__data.hasClipping: - self.__data.painter.setClipRegion( - QRegion(canvasRect) & self.__data.clipRegion - ) - elif not self.__data.painter.hasClipping(): - self.__data.painter.setClipRect(canvasRect) - qwtRenderItem(self.__data.painter, canvasRect, seriesItem, from_, to) - if self.__data.attributes & self.AtomicPainter: - self.reset() - elif self.__data.hasClipping: - self.__data.painter.setClipping(False) - else: - self.reset() - self.__data.seriesItem = seriesItem - self.__data.from_ = from_ - self.__data.to = to - clipRegion = QRegion(canvasRect) - if self.__data.hasClipping: - clipRegion &= self.__data.clipRegion - canvas.installEventFilter(self) - canvas.repaint(clipRegion) - canvas.removeEventFilter(self) - self.__data.seriesItem = None - - def reset(self): - """Close the internal QPainter""" - if self.__data.painter.isActive(): - w = self.__data.painter.device() # XXX: cast to QWidget - if w: - w.removeEventFilter(self) - self.__data.painter.end() - - def eventFilter(self, obj_, event): - if event.type() == QEvent.Paint: - self.reset() - if self.__data.seriesItem: - pe = event # XXX: cast to QPaintEvent - canvas = self.__data.seriesItem.plot().canvas() - painter = QPainter(canvas) - painter.setClipRegion(pe.region()) - doCopyCache = self.testAttribute(self.CopyBackingStore) - if doCopyCache: - plotCanvas = canvas # XXX: cast to QwtPlotCanvas - if plotCanvas: - doCopyCache = qwtHasBackingStore(plotCanvas) - if doCopyCache: - painter.drawPixmap( - plotCanvas.rect().topLeft(), plotCanvas.backingStore() - ) - if not doCopyCache: - qwtRenderItem( - painter, - canvas.contentsRect(), - self.__data.seriesItem, - self.__data.from_, - self.__data.to, - ) - return True - return False diff --git a/qwt/plot_grid.py b/qwt/plot_grid.py deleted file mode 100644 index a75a07c..0000000 --- a/qwt/plot_grid.py +++ /dev/null @@ -1,519 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotGrid ------------ - -.. autoclass:: QwtPlotGrid - :members: -""" - -from qtpy.QtCore import QLineF, QObject, Qt -from qtpy.QtGui import QPen - -from qwt._math import qwtFuzzyGreaterOrEqual, qwtFuzzyLessOrEqual -from qwt.plot import QwtPlotItem -from qwt.qthelpers import qcolor_from_str -from qwt.scale_div import QwtScaleDiv - - -class QwtPlotGrid_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.xEnabled = True - self.yEnabled = True - self.xMinEnabled = False - self.yMinEnabled = False - self.xScaleDiv = QwtScaleDiv() - self.yScaleDiv = QwtScaleDiv() - self.majorPen = QPen() - self.minorPen = QPen() - - -class QwtPlotGrid(QwtPlotItem): - """ - A class which draws a coordinate grid - - The `QwtPlotGrid` class can be used to draw a coordinate grid. - A coordinate grid consists of major and minor vertical - and horizontal grid lines. The locations of the grid lines - are determined by the X and Y scale divisions which can - be assigned with `setXDiv()` and `setYDiv()`. - The `draw()` member draws the grid within a bounding - rectangle. - """ - - def __init__(self, title="Grid"): - QwtPlotItem.__init__(self, title) - self.__data = QwtPlotGrid_PrivateData() - self.setItemInterest(QwtPlotItem.ScaleInterest, True) - self.setZ(10.0) - - @classmethod - def make( - cls, - plot=None, - z=None, - enablemajor=None, - enableminor=None, - color=None, - width=None, - style=None, - mincolor=None, - minwidth=None, - minstyle=None, - ): - """ - Create and setup a new `QwtPlotGrid` object (convenience function). - - :param plot: Plot to attach the curve to - :type plot: qwt.plot.QwtPlot or None - :param z: Z-value - :type z: float or None - :param enablemajor: Tuple of two boolean values (x, y) for enabling major grid lines - :type enablemajor: bool or None - :param enableminor: Tuple of two boolean values (x, y) for enabling minor grid lines - :type enableminor: bool or None - :param color: Pen color for both major and minor grid lines (default: Qt.gray) - :type color: QColor or str or None - :param width: Pen width for both major and minor grid lines (default: 1.0) - :type width: float or None - :param style: Pen style for both major and minor grid lines (default: Qt.DotLine) - :type style: Qt.PenStyle or None - :param mincolor: Pen color for minor grid lines only (default: Qt.gray) - :type mincolor: QColor or str or None - :param minwidth: Pen width for minor grid lines only (default: 1.0) - :type minwidth: float or None - :param minstyle: Pen style for minor grid lines only (default: Qt.DotLine) - :type minstyle: Qt.PenStyle or None - - .. seealso:: - - :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()` - """ - item = cls() - if z is not None: - item.setZ(z) - color = qcolor_from_str(color, Qt.gray) - width = 1.0 if width is None else float(width) - style = Qt.DotLine if style is None else style - item.setPen(QPen(color, width, style)) - if mincolor is not None or minwidth is not None or minstyle is not None: - mincolor = qcolor_from_str(mincolor, Qt.gray) - minwidth = 1.0 if width is None else minwidth - minstyle = Qt.DotLine if style is None else minstyle - item.setMinorPen(QPen(mincolor, minwidth, minstyle)) - if enablemajor is not None: - if isinstance(enablemajor, tuple) and len(enablemajor) == 2: - item.enableX(enablemajor[0]) - item.enableY(enablemajor[1]) - else: - raise TypeError( - "Invalid enablemajor %r (expecting tuple of two booleans)" - % enablemajor - ) - if enableminor is not None: - if isinstance(enableminor, tuple) and len(enableminor) == 2: - item.enableXMin(enableminor[0]) - item.enableYMin(enableminor[1]) - else: - raise TypeError( - "Invalid enableminor %r (expecting tuple of two booleans)" - % enableminor - ) - if plot is not None: - item.attach(plot) - return item - - def rtti(self): - """ - :return: Return `QwtPlotItem.Rtti_PlotGrid` - """ - return QwtPlotItem.Rtti_PlotGrid - - def enableX(self, on): - """ - Enable or disable vertical grid lines - - :param bool on: Enable (true) or disable - - .. seealso:: - - :py:meth:`enableXMin()` - """ - if self.__data.xEnabled != on: - self.__data.xEnabled = on - self.legendChanged() - self.itemChanged() - - def enableY(self, on): - """ - Enable or disable horizontal grid lines - - :param bool on: Enable (true) or disable - - .. seealso:: - - :py:meth:`enableYMin()` - """ - if self.__data.yEnabled != on: - self.__data.yEnabled = on - self.legendChanged() - self.itemChanged() - - def enableXMin(self, on): - """ - Enable or disable minor vertical grid lines. - - :param bool on: Enable (true) or disable - - .. seealso:: - - :py:meth:`enableX()` - """ - if self.__data.xMinEnabled != on: - self.__data.xMinEnabled = on - self.legendChanged() - self.itemChanged() - - def enableYMin(self, on): - """ - Enable or disable minor horizontal grid lines. - - :param bool on: Enable (true) or disable - - .. seealso:: - - :py:meth:`enableY()` - """ - if self.__data.yMinEnabled != on: - self.__data.yMinEnabled = on - self.legendChanged() - self.itemChanged() - - def setXDiv(self, scaleDiv): - """ - Assign an x axis scale division - - :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division - """ - if self.__data.xScaleDiv != scaleDiv: - self.__data.xScaleDiv = scaleDiv - self.itemChanged() - - def setYDiv(self, scaleDiv): - """ - Assign an y axis scale division - - :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division - """ - if self.__data.yScaleDiv != scaleDiv: - self.__data.yScaleDiv = scaleDiv - self.itemChanged() - - def setPen(self, *args): - """ - Build and/or assign a pen for both major and minor grid lines - - .. py:method:: setPen(color, width, style) - :noindex: - - Build and assign a pen for both major and minor grid lines - - In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it - non cosmetic (see `QPen.isCosmetic()`). This method signature has - been introduced to hide this incompatibility. - - :param QColor color: Pen color - :param float width: Pen width - :param Qt.PenStyle style: Pen style - - .. py:method:: setPen(pen) - :noindex: - - Assign a pen for both major and minor grid lines - - :param QPen pen: New pen - - .. seealso:: - - :py:meth:`pen()`, :py:meth:`brush()` - """ - if len(args) == 3: - color, width, style = args - self.setPen(QPen(color, width, style)) - elif len(args) == 1: - (pen,) = args - if self.__data.majorPen != pen or self.__data.minorPen != pen: - self.__data.majorPen = pen - self.__data.minorPen = pen - self.legendChanged() - self.itemChanged() - else: - raise TypeError( - "%s().setPen() takes 1 or 3 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def setMajorPen(self, *args): - """ - Build and/or assign a pen for both major grid lines - - .. py:method:: setMajorPen(color, width, style) - :noindex: - - Build and assign a pen for both major grid lines - - In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it - non cosmetic (see `QPen.isCosmetic()`). This method signature has - been introduced to hide this incompatibility. - - :param QColor color: Pen color - :param float width: Pen width - :param Qt.PenStyle style: Pen style - - .. py:method:: setMajorPen(pen) - :noindex: - - Assign a pen for the major grid lines - - :param QPen pen: New pen - - .. seealso:: - - :py:meth:`majorPen()`, :py:meth:`setMinorPen()`, - :py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()` - """ - if len(args) == 3: - color, width, style = args - self.setMajorPen(QPen(color, width, style)) - elif len(args) == 1: - (pen,) = args - if self.__data.majorPen != pen: - self.__data.majorPen = pen - self.legendChanged() - self.itemChanged() - else: - raise TypeError( - "%s().setMajorPen() takes 1 or 3 argument(s) (%s " - "given)" % (self.__class__.__name__, len(args)) - ) - - def setMinorPen(self, *args): - """ - Build and/or assign a pen for both minor grid lines - - .. py:method:: setMinorPen(color, width, style) - :noindex: - - Build and assign a pen for both minor grid lines - - In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it - non cosmetic (see `QPen.isCosmetic()`). This method signature has - been introduced to hide this incompatibility. - - :param QColor color: Pen color - :param float width: Pen width - :param Qt.PenStyle style: Pen style - - .. py:method:: setMinorPen(pen) - :noindex: - - Assign a pen for the minor grid lines - - :param QPen pen: New pen - - .. seealso:: - - :py:meth:`minorPen()`, :py:meth:`setMajorPen()`, - :py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()` - """ - if len(args) == 3: - color, width, style = args - self.setMinorPen(QPen(color, width, style)) - elif len(args) == 1: - (pen,) = args - if self.__data.minorPen != pen: - self.__data.minorPen = pen - self.legendChanged() - self.itemChanged() - else: - raise TypeError( - "%s().setMinorPen() takes 1 or 3 argument(s) (%s " - "given)" % (self.__class__.__name__, len(args)) - ) - - def draw(self, painter, xMap, yMap, canvasRect): - """ - Draw the grid - - The grid is drawn into the bounding rectangle such that - grid lines begin and end at the rectangle's borders. The X and Y - maps are used to map the scale divisions into the drawing region - screen. - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: X axis map - :param qwt.scale_map.QwtScaleMap yMap: Y axis - :param QRectF canvasRect: Contents rectangle of the plot canvas - """ - minorPen = QPen(self.__data.minorPen) - minorPen.setCapStyle(Qt.FlatCap) - painter.setPen(minorPen) - if self.__data.xEnabled and self.__data.xMinEnabled: - self.drawLines( - painter, - canvasRect, - Qt.Vertical, - xMap, - self.__data.xScaleDiv.ticks(QwtScaleDiv.MinorTick), - ) - self.drawLines( - painter, - canvasRect, - Qt.Vertical, - xMap, - self.__data.xScaleDiv.ticks(QwtScaleDiv.MediumTick), - ) - if self.__data.yEnabled and self.__data.yMinEnabled: - self.drawLines( - painter, - canvasRect, - Qt.Horizontal, - yMap, - self.__data.yScaleDiv.ticks(QwtScaleDiv.MinorTick), - ) - self.drawLines( - painter, - canvasRect, - Qt.Horizontal, - yMap, - self.__data.yScaleDiv.ticks(QwtScaleDiv.MediumTick), - ) - majorPen = QPen(self.__data.majorPen) - majorPen.setCapStyle(Qt.FlatCap) - painter.setPen(majorPen) - if self.__data.xEnabled: - self.drawLines( - painter, - canvasRect, - Qt.Vertical, - xMap, - self.__data.xScaleDiv.ticks(QwtScaleDiv.MajorTick), - ) - if self.__data.yEnabled: - self.drawLines( - painter, - canvasRect, - Qt.Horizontal, - yMap, - self.__data.yScaleDiv.ticks(QwtScaleDiv.MajorTick), - ) - - def drawLines(self, painter, canvasRect, orientation, scaleMap, values): - x1 = canvasRect.left() - x2 = canvasRect.right() - 1.0 - y1 = canvasRect.top() - y2 = canvasRect.bottom() - 1.0 - for val in values: - value = scaleMap.transform(val) - if orientation == Qt.Horizontal: - if qwtFuzzyGreaterOrEqual(value, y1) and qwtFuzzyLessOrEqual(value, y2): - painter.drawLine(QLineF(x1, value, x2, value)) - else: - if qwtFuzzyGreaterOrEqual(value, x1) and qwtFuzzyLessOrEqual(value, x2): - painter.drawLine(QLineF(value, y1, value, y2)) - - def majorPen(self): - """ - :return: the pen for the major grid lines - - .. seealso:: - - :py:meth:`setMajorPen()`, :py:meth:`setMinorPen()`, - :py:meth:`setPen()` - """ - return self.__data.majorPen - - def minorPen(self): - """ - :return: the pen for the minor grid lines - - .. seealso:: - - :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`, - :py:meth:`setPen()` - """ - return self.__data.minorPen - - def xEnabled(self): - """ - :return: True if vertical grid lines are enabled - - .. seealso:: - - :py:meth:`enableX()` - """ - return self.__data.xEnabled - - def yEnabled(self): - """ - :return: True if horizontal grid lines are enabled - - .. seealso:: - - :py:meth:`enableY()` - """ - return self.__data.yEnabled - - def xMinEnabled(self): - """ - :return: True if minor vertical grid lines are enabled - - .. seealso:: - - :py:meth:`enableXMin()` - """ - return self.__data.xMinEnabled - - def yMinEnabled(self): - """ - :return: True if minor horizontal grid lines are enabled - - .. seealso:: - - :py:meth:`enableYMin()` - """ - return self.__data.yMinEnabled - - def xScaleDiv(self): - """ - :return: the scale division of the x axis - """ - return self.__data.xScaleDiv - - def yScaleDiv(self): - """ - :return: the scale division of the y axis - """ - return self.__data.yScaleDiv - - def updateScaleDiv(self, xScaleDiv, yScaleDiv): - """ - Update the grid to changes of the axes scale division - - :param qwt.scale_map.QwtScaleMap xMap: Scale division of the x-axis - :param qwt.scale_map.QwtScaleMap yMap: Scale division of the y-axis - - .. seealso:: - - :py:meth:`updateAxes()` - """ - self.setXDiv(xScaleDiv) - self.setYDiv(yScaleDiv) diff --git a/qwt/plot_layout.py b/qwt/plot_layout.py deleted file mode 100644 index 0919914..0000000 --- a/qwt/plot_layout.py +++ /dev/null @@ -1,1167 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotLayout -------------- - -.. autoclass:: QwtPlotLayout - :members: -""" - -import math - -from qtpy.QtCore import QObject, QRectF, QSize, Qt -from qtpy.QtGui import QFont, QRegion - -from qwt.plot import QwtPlot -from qwt.scale_draw import QwtAbstractScaleDraw -from qwt.scale_widget import QwtScaleWidget -from qwt.text import QwtText - -QWIDGETSIZE_MAX = (1 << 24) - 1 - - -class LegendData(object): - def __init__(self): - self.frameWidth = None - self.hScrollExtent = None - self.vScrollExtent = None - self.hint = QSize() - - -class TitleData(object): - def __init__(self): - self.text = QwtText() - self.frameWidth = None - - -class FooterData(object): - def __init__(self): - self.text = QwtText() - self.frameWidth = None - - -class ScaleData(object): - def __init__(self): - self.isEnabled = None - self.scaleWidget = QwtScaleWidget() - self.scaleFont = QFont() - self.start = None - self.end = None - self.baseLineOffset = None - self.tickOffset = None - self.dimWithoutTitle = None - - -class CanvasData(object): - def __init__(self): - self.contentsMargins = [0 for _i in QwtPlot.AXES] - - -class QwtPlotLayout_LayoutData(object): - def __init__(self): - self.legend = LegendData() - self.title = TitleData() - self.footer = FooterData() - self.scale = [ScaleData() for _i in QwtPlot.AXES] - self.canvas = CanvasData() - - def init(self, plot, rect): - """Extract all layout relevant data from the plot components""" - # legend - legend = plot.legend() - if legend: - self.legend.frameWidth = legend.frameWidth() - self.legend.hScrollExtent = legend.scrollExtent(Qt.Horizontal) - self.legend.vScrollExtent = legend.scrollExtent(Qt.Vertical) - hint = legend.sizeHint() - w = min([hint.width(), math.floor(rect.width())]) - h = legend.heightForWidth(w) - if h <= 0: - h = hint.height() - self.legend.hint = QSize(w, h) - # title - self.title.frameWidth = 0 - self.title.text = QwtText() - if plot.titleLabel(): - label = plot.titleLabel() - self.title.text = label.text() - if not self.title.text.testPaintAttribute(QwtText.PaintUsingTextFont): - self.title.text.setFont(label.font()) - self.title.frameWidth = plot.titleLabel().frameWidth() - # footer - self.footer.frameWidth = 0 - self.footer.text = QwtText() - if plot.footerLabel(): - label = plot.footerLabel() - self.footer.text = label.text() - if not self.footer.text.testPaintAttribute(QwtText.PaintUsingTextFont): - self.footer.text.setFont(label.font()) - self.footer.frameWidth = plot.footerLabel().frameWidth() - # scales - for axis in QwtPlot.AXES: - if plot.axisEnabled(axis): - scaleWidget = plot.axisWidget(axis) - self.scale[axis].isEnabled = True - self.scale[axis].scaleWidget = scaleWidget - self.scale[axis].scaleFont = scaleWidget.font() - self.scale[axis].start = scaleWidget.startBorderDist() - self.scale[axis].end = scaleWidget.endBorderDist() - self.scale[axis].baseLineOffset = scaleWidget.margin() - self.scale[axis].tickOffset = scaleWidget.margin() - if scaleWidget.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks): - self.scale[ - axis - ].tickOffset += scaleWidget.scaleDraw().maxTickLength() - self.scale[axis].dimWithoutTitle = scaleWidget.dimForLength( - QWIDGETSIZE_MAX, self.scale[axis].scaleFont - ) - if not scaleWidget.title().isEmpty(): - self.scale[axis].dimWithoutTitle -= scaleWidget.titleHeightForWidth( - QWIDGETSIZE_MAX - ) - else: - self.scale[axis].isEnabled = False - self.scale[axis].start = 0 - self.scale[axis].end = 0 - self.scale[axis].baseLineOffset = 0 - self.scale[axis].tickOffset = 0.0 - self.scale[axis].dimWithoutTitle = 0 - layout = plot.canvas().layout() - if layout is not None: - mgn = layout.contentsMargins() - self.canvas.contentsMargins = [ - mgn.left(), - mgn.top(), - mgn.right(), - mgn.bottom(), - ] - - -class QwtPlotLayout_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.spacing = 5 - self.titleRect = QRectF() - self.footerRect = QRectF() - self.legendRect = QRectF() - self.scaleRect = [QRectF() for _i in QwtPlot.AXES] - self.canvasRect = QRectF() - self.layoutData = QwtPlotLayout_LayoutData() - self.legendPos = None - self.legendRatio = None - self.canvasMargin = [0] * len(QwtPlot.AXES) - self.alignCanvasToScales = [False] * len(QwtPlot.AXES) - - -class QwtPlotLayout(object): - """ - Layout engine for QwtPlot. - - It is used by the `QwtPlot` widget to organize its internal widgets - or by `QwtPlot.print()` to render its content to a QPaintDevice like - a QPrinter, QPixmap/QImage or QSvgRenderer. - - .. seealso:: - - :py:meth:`qwt.plot.QwtPlot.setPlotLayout()` - - Valid options: - - * `QwtPlotLayout.AlignScales`: Unused - * `QwtPlotLayout.IgnoreScrollbars`: Ignore the dimension of the scrollbars. There are no scrollbars, when the plot is not rendered to widgets. - * `QwtPlotLayout.IgnoreFrames`: Ignore all frames. - * `QwtPlotLayout.IgnoreLegend`: Ignore the legend. - * `QwtPlotLayout.IgnoreTitle`: Ignore the title. - * `QwtPlotLayout.IgnoreFooter`: Ignore the footer. - """ - - # enum Option - AlignScales = 0x01 - IgnoreScrollbars = 0x02 - IgnoreFrames = 0x04 - IgnoreLegend = 0x08 - IgnoreTitle = 0x10 - IgnoreFooter = 0x20 - - def __init__(self): - self.__data = QwtPlotLayout_PrivateData() - self.setLegendPosition(QwtPlot.BottomLegend) - self.setCanvasMargin(4) - self.setAlignCanvasToScales(False) - self.invalidate() - - def setCanvasMargin(self, margin, axis=-1): - """ - Change a margin of the canvas. The margin is the space - above/below the scale ticks. A negative margin will - be set to -1, excluding the borders of the scales. - - :param int margin: New margin - :param int axisId: Axis index - - .. seealso:: - - :py:meth:`canvasMargin()` - - .. warning:: - - The margin will have no effect when `alignCanvasToScale()` is True - """ - if margin < 1: - margin = -1 - if axis == -1: - for axis in QwtPlot.AXES: - self.__data.canvasMargin[axis] = margin - elif axis in QwtPlot.AXES: - self.__data.canvasMargin[axis] = margin - - def canvasMargin(self, axisId): - """ - :param int axisId: Axis index - :return: Margin around the scale tick borders - - .. seealso:: - - :py:meth:`setCanvasMargin()` - """ - if axisId not in QwtPlot.AXES: - return 0 - return self.__data.canvasMargin[axisId] - - def setAlignCanvasToScales(self, *args): - """ - Change the align-canvas-to-axis-scales setting. - - .. py:method:: setAlignCanvasToScales(on): - - Set the align-canvas-to-axis-scales flag for all axes - - :param bool on: True/False - - .. py:method:: setAlignCanvasToScales(axisId, on): - - Change the align-canvas-to-axis-scales setting. - The canvas may: - - - extend beyond the axis scale ends to maximize its size, - - align with the axis scale ends to control its size. - - The axisId parameter is somehow confusing as it identifies a - border of the plot and not the axes, that are aligned. F.e when - `QwtPlot.yLeft` is set, the left end of the the x-axes - (`QwtPlot.xTop`, `QwtPlot.xBottom`) is aligned. - - :param int axisId: Axis index - :param bool on: True/False - - .. seealso:: - - :py:meth:`setAlignCanvasToScale()`, - :py:meth:`alignCanvasToScale()` - """ - if len(args) == 1: - (on,) = args - for axis in QwtPlot.AXES: - self.__data.alignCanvasToScales[axis] = on - elif len(args) == 2: - axisId, on = args - if axisId in QwtPlot.AXES: - self.__data.alignCanvasToScales[axisId] = on - else: - raise TypeError( - "%s().setAlignCanvasToScales() takes 1 or 2 " - "argument(s) (%s given)" % (self.__class__.__name__, len(args)) - ) - - def alignCanvasToScale(self, axisId): - """ - Return the align-canvas-to-axis-scales setting. - The canvas may: - - - extend beyond the axis scale ends to maximize its size - - align with the axis scale ends to control its size. - - :param int axisId: Axis index - :return: align-canvas-to-axis-scales setting - - .. seealso:: - - :py:meth:`setAlignCanvasToScale()`, :py:meth:`setCanvasMargin()` - """ - if axisId not in QwtPlot.AXES: - return False - return self.__data.alignCanvasToScales[axisId] - - def setSpacing(self, spacing): - """ - Change the spacing of the plot. The spacing is the distance - between the plot components. - - :param int spacing: New spacing - - .. seealso:: - - :py:meth:`setCanvasMargin()`, :py:meth:`spacing()` - """ - self.__data.spacing = max([0, spacing]) - - def spacing(self): - """ - :return: Spacing - - .. seealso:: - - :py:meth:`margin()`, :py:meth:`setSpacing()` - """ - return self.__data.spacing - - def setLegendPosition(self, *args): - """ - Specify the position of the legend - - .. py:method:: setLegendPosition(pos, [ratio=0.]): - - Specify the position of the legend - - :param QwtPlot.LegendPosition pos: Legend position - :param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes - - The legend will be shrunk if it would need more space than the - given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of - <= 0.0 it will be reset to the default ratio. The default - vertical/horizontal ratio is 0.33/0.5. - - Valid position values: - - * `QwtPlot.LeftLegend`, - * `QwtPlot.RightLegend`, - * `QwtPlot.TopLegend`, - * `QwtPlot.BottomLegend` - - .. seealso:: - - :py:meth:`setLegendPosition()` - """ - if len(args) == 2: - pos, ratio = args - if ratio > 1.0: - ratio = 1.0 - if pos in (QwtPlot.TopLegend, QwtPlot.BottomLegend): - if ratio <= 0.0: - ratio = 0.33 - self.__data.legendRatio = ratio - self.__data.legendPos = pos - elif pos in (QwtPlot.LeftLegend, QwtPlot.RightLegend): - if ratio <= 0.0: - ratio = 0.5 - self.__data.legendRatio = ratio - self.__data.legendPos = pos - elif len(args) == 1: - (pos,) = args - self.setLegendPosition(pos, 0.0) - else: - raise TypeError( - "%s().setLegendPosition() takes 1 or 2 argument(s)" - "(%s given)" % (self.__class__.__name__, len(args)) - ) - - def legendPosition(self): - """ - :return: Position of the legend - - .. seealso:: - - :py:meth:`legendPosition()` - """ - return self.__data.legendPos - - def setLegendRatio(self, ratio): - """ - Specify the relative size of the legend in the plot - - :param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes - - The legend will be shrunk if it would need more space than the - given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of - <= 0.0 it will be reset to the default ratio. The default - vertical/horizontal ratio is 0.33/0.5. - - .. seealso:: - - :py:meth:`legendRatio()` - """ - self.setLegendPosition(self.legendPosition(), ratio) - - def legendRatio(self): - """ - :return: The relative size of the legend in the plot. - - .. seealso:: - - :py:meth:`setLegendRatio()` - """ - return self.__data.legendRatio - - def setTitleRect(self, rect): - """ - Set the geometry for the title - - This method is intended to be used from derived layouts - overloading `activate()` - - :param QRectF rect: Rectangle - - .. seealso:: - - :py:meth:`titleRect()`, :py:meth:`activate()` - """ - self.__data.titleRect = rect - - def titleRect(self): - """ - :return: Geometry for the title - - .. seealso:: - - :py:meth:`invalidate()`, :py:meth:`activate()` - """ - return self.__data.titleRect - - def setFooterRect(self, rect): - """ - Set the geometry for the footer - - This method is intended to be used from derived layouts - overloading `activate()` - - :param QRectF rect: Rectangle - - .. seealso:: - - :py:meth:`footerRect()`, :py:meth:`activate()` - """ - self.__data.footerRect = rect - - def footerRect(self): - """ - :return: Geometry for the footer - - .. seealso:: - - :py:meth:`invalidate()`, :py:meth:`activate()` - """ - return self.__data.footerRect - - def setLegendRect(self, rect): - """ - Set the geometry for the legend - - This method is intended to be used from derived layouts - overloading `activate()` - - :param QRectF rect: Rectangle for the legend - - .. seealso:: - - :py:meth:`footerRect()`, :py:meth:`activate()` - """ - self.__data.legendRect = rect - - def legendRect(self): - """ - :return: Geometry for the legend - - .. seealso:: - - :py:meth:`invalidate()`, :py:meth:`activate()` - """ - return self.__data.legendRect - - def setScaleRect(self, axis, rect): - """ - Set the geometry for an axis - - This method is intended to be used from derived layouts - overloading `activate()` - - :param int axisId: Axis index - :param QRectF rect: Rectangle for the scale - - .. seealso:: - - :py:meth:`scaleRect()`, :py:meth:`activate()` - """ - if axis in QwtPlot.AXES: - self.__data.scaleRect[axis] = rect - - def scaleRect(self, axis): - """ - :param int axisId: Axis index - :return: Geometry for the scale - - .. seealso:: - - :py:meth:`invalidate()`, :py:meth:`activate()` - """ - if axis not in QwtPlot.AXES: - return QRectF() - return self.__data.scaleRect[axis] - - def setCanvasRect(self, rect): - """ - Set the geometry for the canvas - - This method is intended to be used from derived layouts - overloading `activate()` - - :param QRectF rect: Rectangle - - .. seealso:: - - :py:meth:`canvasRect()`, :py:meth:`activate()` - """ - self.__data.canvasRect = rect - - def canvasRect(self): - """ - :return: Geometry for the canvas - - .. seealso:: - - :py:meth:`invalidate()`, :py:meth:`activate()` - """ - return self.__data.canvasRect - - def invalidate(self): - """ - Invalidate the geometry of all components. - - .. seealso:: - - :py:meth:`activate()` - """ - self.__data.titleRect = QRectF() - self.__data.footerRect = QRectF() - self.__data.legendRect = QRectF() - self.__data.canvasRect = QRectF() - for axis in QwtPlot.AXES: - self.__data.scaleRect[axis] = QRectF() - - def minimumSizeHint(self, plot): - """ - :param qwt.plot.QwtPlot plot: Plot widget - :return: Minimum size hint - - .. seealso:: - - :py:meth:`qwt.plot.QwtPlot.minimumSizeHint()` - """ - - class _ScaleData(object): - def __init__(self): - self.w = 0 - self.h = 0 - self.minLeft = 0 - self.minRight = 0 - self.tickOffset = 0 - - scaleData = [_ScaleData() for _i in QwtPlot.AXES] - canvasBorder = [0 for _i in QwtPlot.AXES] - layout = plot.canvas().layout() - if layout is None: - left, top, right, bottom = 0, 0, 0, 0 - else: - mgn = layout.contentsMargins() - left, top, right, bottom = ( - mgn.left(), - mgn.top(), - mgn.right(), - mgn.bottom(), - ) - for axis in QwtPlot.AXES: - if plot.axisEnabled(axis): - scl = plot.axisWidget(axis) - sd = scaleData[axis] - hint = scl.minimumSizeHint() - sd.w = hint.width() - sd.h = hint.height() - sd.minLeft, sd.minLeft = scl.getBorderDistHint() - sd.tickOffset = scl.margin() - if scl.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks): - sd.tickOffset += math.ceil(scl.scaleDraw().maxTickLength()) - canvasBorder[axis] = left + self.__data.canvasMargin[axis] + 1 - for axis in QwtPlot.AXES: - sd = scaleData[axis] - if sd.w and axis in (QwtPlot.xBottom, QwtPlot.xTop): - if ( - sd.minLeft > canvasBorder[QwtPlot.yLeft] - and scaleData[QwtPlot.yLeft].w - ): - shiftLeft = sd.minLeft - canvasBorder[QwtPlot.yLeft] - if shiftLeft > scaleData[QwtPlot.yLeft].w: - shiftLeft = scaleData[QwtPlot.yLeft].w - sd.w -= shiftLeft - if ( - sd.minRight > canvasBorder[QwtPlot.yRight] - and scaleData[QwtPlot.yRight].w - ): - shiftRight = sd.minRight - canvasBorder[QwtPlot.yRight] - if shiftRight > scaleData[QwtPlot.yRight].w: - shiftRight = scaleData[QwtPlot.yRight].w - sd.w -= shiftRight - if sd.h and axis in (QwtPlot.yLeft, QwtPlot.yRight): - if ( - sd.minLeft > canvasBorder[QwtPlot.xBottom] - and scaleData[QwtPlot.xBottom].h - ): - shiftBottom = sd.minLeft - canvasBorder[QwtPlot.xBottom] - if shiftBottom > scaleData[QwtPlot.xBottom].tickOffset: - shiftBottom = scaleData[QwtPlot.xBottom].tickOffset - sd.h -= shiftBottom - if ( - sd.minLeft > canvasBorder[QwtPlot.xTop] - and scaleData[QwtPlot.xTop].h - ): - shiftTop = sd.minRight - canvasBorder[QwtPlot.xTop] - if shiftTop > scaleData[QwtPlot.xTop].tickOffset: - shiftTop = scaleData[QwtPlot.xTop].tickOffset - sd.h -= shiftTop - canvas = plot.canvas() - minCanvasSize = canvas.minimumSize() - w = scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w - cw = ( - max([scaleData[QwtPlot.xBottom].w, scaleData[QwtPlot.xTop].w]) - + left - + 1 - + right - + 1 - ) - w += max([cw, minCanvasSize.width()]) - h = scaleData[QwtPlot.xBottom].h + scaleData[QwtPlot.xTop].h - ch = ( - max([scaleData[QwtPlot.yLeft].h, scaleData[QwtPlot.yRight].h]) - + top - + 1 - + bottom - + 1 - ) - h += max([ch, minCanvasSize.height()]) - for label in [plot.titleLabel(), plot.footerLabel()]: - if label and not label.text().isEmpty(): - centerOnCanvas = not plot.axisEnabled( - QwtPlot.yLeft - ) and plot.axisEnabled(QwtPlot.yRight) - labelW = w - if centerOnCanvas: - labelW -= scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w - labelH = label.heightForWidth(labelW) - if labelH > labelW: - w = labelW = labelH - if centerOnCanvas: - w += scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w - labelH = label.heightForWidth(labelW) - h += labelH + self.__data.spacing - legend = plot.legend() - if legend and not legend.isEmpty(): - if self.__data.legendPos in (QwtPlot.LeftLegend, QwtPlot.RightLegend): - legendW = legend.sizeHint().width() - legendH = legend.heightForWidth(legendW) - if legend.frameWidth() > 0: - w += self.__data.spacing - if legendH > h: - legendW += legend.scrollExtent(Qt.Horizontal) - if self.__data.legendRatio < 1.0: - legendW = min([legendW, int(w / (1.0 - self.__data.legendRatio))]) - w += legendW + self.__data.spacing - else: - legendW = min([legend.sizeHint().width(), w]) - legendH = legend.heightForWidth(legendW) - if legend.frameWidth() > 0: - h += self.__data.spacing - if self.__data.legendRatio < 1.0: - legendH = min([legendH, int(h / (1.0 - self.__data.legendRatio))]) - h += legendH + self.__data.spacing - return QSize(int(w), int(h)) - - def layoutLegend(self, options, rect): - """ - Find the geometry for the legend - - :param options: Options how to layout the legend - :param QRectF rect: Rectangle where to place the legend - :return: Geometry for the legend - """ - hint = self.__data.layoutData.legend.hint - if self.__data.legendPos in (QwtPlot.LeftLegend, QwtPlot.RightLegend): - dim = min([hint.width(), int(rect.width() * self.__data.legendRatio)]) - if not (options & self.IgnoreScrollbars): - if hint.height() > rect.height(): - dim += self.__data.layoutData.legend.hScrollExtent - else: - dim = min([hint.height(), int(rect.height() * self.__data.legendRatio)]) - dim = max([dim, self.__data.layoutData.legend.vScrollExtent]) - legendRect = QRectF(rect) - if self.__data.legendPos == QwtPlot.LeftLegend: - legendRect.setWidth(dim) - elif self.__data.legendPos == QwtPlot.RightLegend: - legendRect.setX(rect.right() - dim) - legendRect.setWidth(dim) - elif self.__data.legendPos == QwtPlot.TopLegend: - legendRect.setHeight(dim) - elif self.__data.legendPos == QwtPlot.BottomLegend: - legendRect.setY(rect.bottom() - dim) - legendRect.setHeight(dim) - return legendRect - - def alignLegend(self, canvasRect, legendRect): - """ - Align the legend to the canvas - - :param QRectF canvasRect: Geometry of the canvas - :param QRectF legendRect: Maximum geometry for the legend - :return: Geometry for the aligned legend - """ - alignedRect = legendRect - if self.__data.legendPos in (QwtPlot.BottomLegend, QwtPlot.TopLegend): - if self.__data.layoutData.legend.hint.width() < canvasRect.width(): - alignedRect.setX(canvasRect.x()) - alignedRect.setWidth(canvasRect.width()) - else: - if self.__data.layoutData.legend.hint.height() < canvasRect.height(): - alignedRect.setY(canvasRect.y()) - alignedRect.setHeight(canvasRect.height()) - return alignedRect - - def expandLineBreaks(self, options, rect): - """ - Expand all line breaks in text labels, and calculate the height - of their widgets in orientation of the text. - - :param options: Options how to layout the legend - :param QRectF rect: Bounding rectangle for title, footer, axes and canvas. - :return: tuple `(dimTitle, dimFooter, dimAxes)` - - Returns: - - * `dimTitle`: Expanded height of the title widget - * `dimFooter`: Expanded height of the footer widget - * `dimAxes`: Expanded heights of the axis in axis orientation. - """ - dimTitle = dimFooter = 0 - dimAxes = [0 for axis in QwtPlot.AXES] - backboneOffset = [0 for _i in QwtPlot.AXES] - for axis in QwtPlot.AXES: - if not (options & self.IgnoreFrames): - backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[ - axis - ] - if not self.__data.alignCanvasToScales[axis]: - backboneOffset[axis] += self.__data.canvasMargin[axis] - done = False - while not done: - done = True - # the size for the 4 axis depend on each other. Expanding - # the height of a horizontal axis will shrink the height - # for the vertical axis, shrinking the height of a vertical - # axis will result in a line break what will expand the - # width and results in shrinking the width of a horizontal - # axis what might result in a line break of a horizontal - # axis ... . So we loop as long until no size changes. - if not ( - (options & self.IgnoreTitle) - or self.__data.layoutData.title.text.isEmpty() - ): - w = rect.width() - if ( - self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled - != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled - ): - w -= dimAxes[QwtPlot.yLeft] + dimAxes[QwtPlot.yRight] - d = math.ceil(self.__data.layoutData.title.text.heightForWidth(w)) - if not (options & self.IgnoreFrames): - d += 2 * self.__data.layoutData.title.frameWidth - if d > dimTitle: - dimTitle = d - done = False - if not ( - (options & self.IgnoreFooter) - or self.__data.layoutData.footer.text.isEmpty() - ): - w = rect.width() - if ( - self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled - != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled - ): - w -= dimAxes[QwtPlot.yLeft] + dimAxes[QwtPlot.yRight] - d = math.ceil(self.__data.layoutData.footer.text.heightForWidth(w)) - if not (options & self.IgnoreFrames): - d += 2 * self.__data.layoutData.footer.frameWidth - if d > dimFooter: - dimFooter = d - done = False - for axis in QwtPlot.AXES: - scaleData = self.__data.layoutData.scale[axis] - if scaleData.isEnabled: - if axis in (QwtPlot.xTop, QwtPlot.xBottom): - length = ( - rect.width() - - dimAxes[QwtPlot.yLeft] - - dimAxes[QwtPlot.yRight] - ) - length -= scaleData.start + scaleData.end - if dimAxes[QwtPlot.yRight] > 0: - length -= 1 - length += min( - [ - dimAxes[QwtPlot.yLeft], - scaleData.start - backboneOffset[QwtPlot.yLeft], - ] - ) - length += min( - [ - dimAxes[QwtPlot.yRight], - scaleData.end - backboneOffset[QwtPlot.yRight], - ] - ) - else: - length = ( - rect.height() - - dimAxes[QwtPlot.xTop] - - dimAxes[QwtPlot.xBottom] - ) - length -= scaleData.start + scaleData.end - length -= 1 - if dimAxes[QwtPlot.xBottom] <= 0: - length -= 1 - if dimAxes[QwtPlot.xTop] <= 0: - length -= 1 - if dimAxes[QwtPlot.xBottom] > 0: - length += min( - [ - self.__data.layoutData.scale[ - QwtPlot.xBottom - ].tickOffset, - float( - scaleData.start - - backboneOffset[QwtPlot.xBottom] - ), - ] - ) - if dimAxes[QwtPlot.xTop] > 0: - length += min( - [ - self.__data.layoutData.scale[ - QwtPlot.xTop - ].tickOffset, - float(scaleData.end - backboneOffset[QwtPlot.xTop]), - ] - ) - if dimTitle > 0: - length -= dimTitle + self.__data.spacing - d = scaleData.dimWithoutTitle - if not scaleData.scaleWidget.title().isEmpty(): - d += scaleData.scaleWidget.titleHeightForWidth( - math.floor(length) - ) - if d > dimAxes[axis]: - dimAxes[axis] = d - done = False - return dimTitle, dimFooter, dimAxes - - def alignScales(self, options, canvasRect, scaleRect): - """ - Align the ticks of the axis to the canvas borders using - the empty corners. - - :param options: Options how to layout the legend - :param QRectF canvasRect: Geometry of the canvas ( IN/OUT ) - :param QRectF scaleRect: Geometry of the scales ( IN/OUT ) - """ - backboneOffset = [0 for _i in QwtPlot.AXES] - for axis in QwtPlot.AXES: - backboneOffset[axis] = 0 - if not self.__data.alignCanvasToScales[axis]: - backboneOffset[axis] += self.__data.canvasMargin[axis] - if not options & self.IgnoreFrames: - backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[ - axis - ] - for axis in QwtPlot.AXES: - if not scaleRect[axis].isValid(): - continue - startDist = self.__data.layoutData.scale[axis].start - endDist = self.__data.layoutData.scale[axis].end - axisRect = scaleRect[axis] - if axis in (QwtPlot.xTop, QwtPlot.xBottom): - leftScaleRect = scaleRect[QwtPlot.yLeft] - leftOffset = backboneOffset[QwtPlot.yLeft] - startDist - if leftScaleRect.isValid(): - dx = leftOffset + leftScaleRect.width() - if self.__data.alignCanvasToScales[QwtPlot.yLeft] and dx < 0.0: - cLeft = canvasRect.left() - canvasRect.setLeft(max([cLeft, axisRect.left() - dx])) - else: - minLeft = leftScaleRect.left() - left = axisRect.left() + leftOffset - axisRect.setLeft(max([left, minLeft])) - else: - if ( - self.__data.alignCanvasToScales[QwtPlot.yLeft] - and leftOffset < 0 - ): - canvasRect.setLeft( - max([canvasRect.left(), axisRect.left() - leftOffset]) - ) - else: - if leftOffset > 0: - axisRect.setLeft(axisRect.left() + leftOffset) - rightScaleRect = scaleRect[QwtPlot.yRight] - rightOffset = backboneOffset[QwtPlot.yRight] - endDist + 1 - if rightScaleRect.isValid(): - dx = rightOffset + rightScaleRect.width() - if self.__data.alignCanvasToScales[QwtPlot.yRight] and dx < 0: - cRight = canvasRect.right() - canvasRect.setRight(min([cRight, axisRect.right() + dx])) - maxRight = rightScaleRect.right() - right = axisRect.right() - rightOffset - axisRect.setRight(min([right, maxRight])) - else: - if ( - self.__data.alignCanvasToScales[QwtPlot.yRight] - and rightOffset < 0 - ): - canvasRect.setRight( - min([canvasRect.right(), axisRect.right() + rightOffset]) - ) - else: - if rightOffset > 0: - axisRect.setRight(axisRect.right() - rightOffset) - else: - bottomScaleRect = scaleRect[QwtPlot.xBottom] - bottomOffset = backboneOffset[QwtPlot.xBottom] - endDist + 1 - if bottomScaleRect.isValid(): - dy = bottomOffset + bottomScaleRect.height() - if self.__data.alignCanvasToScales[QwtPlot.xBottom] and dy < 0: - cBottom = canvasRect.bottom() - canvasRect.setBottom(min([cBottom, axisRect.bottom() + dy])) - else: - maxBottom = ( - bottomScaleRect.top() - + self.__data.layoutData.scale[QwtPlot.xBottom].tickOffset - ) - bottom = axisRect.bottom() - bottomOffset - axisRect.setBottom(min([bottom, maxBottom])) - else: - if ( - self.__data.alignCanvasToScales[QwtPlot.xBottom] - and bottomOffset < 0 - ): - canvasRect.setBottom( - min([canvasRect.bottom(), axisRect.bottom() + bottomOffset]) - ) - else: - if bottomOffset > 0: - axisRect.setBottom(axisRect.bottom() - bottomOffset) - topScaleRect = scaleRect[QwtPlot.xTop] - topOffset = backboneOffset[QwtPlot.xTop] - startDist - if topScaleRect.isValid(): - dy = topOffset + topScaleRect.height() - if self.__data.alignCanvasToScales[QwtPlot.xTop] and dy < 0: - cTop = canvasRect.top() - canvasRect.setTop(max([cTop, axisRect.top() - dy])) - else: - minTop = ( - topScaleRect.bottom() - - self.__data.layoutData.scale[QwtPlot.xTop].tickOffset - ) - top = axisRect.top() + topOffset - axisRect.setTop(max([top, minTop])) - else: - if self.__data.alignCanvasToScales[QwtPlot.xTop] and topOffset < 0: - canvasRect.setTop( - max([canvasRect.top(), axisRect.top() - topOffset]) - ) - else: - if topOffset > 0: - axisRect.setTop(axisRect.top() + topOffset) - for axis in QwtPlot.AXES: - sRect = scaleRect[axis] - if not sRect.isValid(): - continue - if axis in (QwtPlot.xBottom, QwtPlot.xTop): - if self.__data.alignCanvasToScales[QwtPlot.yLeft]: - y = canvasRect.left() - self.__data.layoutData.scale[axis].start - if not options & self.IgnoreFrames: - y += self.__data.layoutData.canvas.contentsMargins[ - QwtPlot.yLeft - ] - sRect.setLeft(y) - if self.__data.alignCanvasToScales[QwtPlot.yRight]: - y = canvasRect.right() - 1 + self.__data.layoutData.scale[axis].end - if not options & self.IgnoreFrames: - y -= self.__data.layoutData.canvas.contentsMargins[ - QwtPlot.yRight - ] - sRect.setRight(y) - if self.__data.alignCanvasToScales[axis]: - if axis == QwtPlot.xTop: - sRect.setBottom(canvasRect.top()) - else: - sRect.setTop(canvasRect.bottom()) - else: - if self.__data.alignCanvasToScales[QwtPlot.xTop]: - x = canvasRect.top() - self.__data.layoutData.scale[axis].start - if not options & self.IgnoreFrames: - x += self.__data.layoutData.canvas.contentsMargins[QwtPlot.xTop] - sRect.setTop(x) - if self.__data.alignCanvasToScales[QwtPlot.xBottom]: - x = canvasRect.bottom() - 1 + self.__data.layoutData.scale[axis].end - if not options & self.IgnoreFrames: - x -= self.__data.layoutData.canvas.contentsMargins[ - QwtPlot.xBottom - ] - sRect.setBottom(x) - if self.__data.alignCanvasToScales[axis]: - if axis == QwtPlot.yLeft: - sRect.setRight(canvasRect.left()) - else: - sRect.setLeft(canvasRect.right()) - - def activate(self, plot, plotRect, options=0x00): - """ - Recalculate the geometry of all components. - - :param qwt.plot.QwtPlot plot: Plot to be layout - :param QRectF plotRect: Rectangle where to place the components - :param options: Layout options - """ - self.invalidate() - rect = QRectF(plotRect) - self.__data.layoutData.init(plot, rect) - if ( - not (options & self.IgnoreLegend) - and plot.legend() - and not plot.legend().isEmpty() - ): - self.__data.legendRect = self.layoutLegend(options, rect) - region = QRegion(rect.toRect()) - rect = QRectF( - region.subtracted( - QRegion(self.__data.legendRect.toRect()) - ).boundingRect() - ) - if self.__data.legendPos == QwtPlot.LeftLegend: - rect.setLeft(rect.left() + self.__data.spacing) - elif self.__data.legendPos == QwtPlot.RightLegend: - rect.setRight(rect.right() - self.__data.spacing) - elif self.__data.legendPos == QwtPlot.TopLegend: - rect.setTop(rect.top() + self.__data.spacing) - elif self.__data.legendPos == QwtPlot.BottomLegend: - rect.setBottom(rect.bottom() - self.__data.spacing) - - # +---+-----------+---+ - # | Title | - # +---+-----------+---+ - # | | Axis | | - # +---+-----------+---+ - # | A | | A | - # | x | Canvas | x | - # | i | | i | - # | s | | s | - # +---+-----------+---+ - # | | Axis | | - # +---+-----------+---+ - # | Footer | - # +---+-----------+---+ - - # title, footer and axes include text labels. The height of each - # label depends on its line breaks, that depend on the width - # for the label. A line break in a horizontal text will reduce - # the available width for vertical texts and vice versa. - # expandLineBreaks finds the height/width for title, footer and axes - # including all line breaks. - - dimTitle, dimFooter, dimAxes = self.expandLineBreaks(options, rect) - if dimTitle > 0: - self.__data.titleRect.setRect( - rect.left(), rect.top(), rect.width(), dimTitle - ) - rect.setTop(self.__data.titleRect.bottom() + self.__data.spacing) - if ( - self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled - != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled - ): - self.__data.titleRect.setX(rect.left() + dimAxes[QwtPlot.yLeft]) - self.__data.titleRect.setWidth( - rect.width() - dimAxes[QwtPlot.yLeft] - dimAxes[QwtPlot.yRight] - ) - if dimFooter > 0: - self.__data.footerRect.setRect( - rect.left(), rect.bottom() - dimFooter, rect.width(), dimFooter - ) - rect.setBottom(self.__data.footerRect.top() - self.__data.spacing) - if ( - self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled - != self.__data.layoutData.scale[QwtPlot.yRight].isEnabled - ): - self.__data.footerRect.setX(rect.left() + dimAxes[QwtPlot.yLeft]) - self.__data.footerRect.setWidth( - rect.width() - dimAxes[QwtPlot.yLeft] - dimAxes[QwtPlot.yRight] - ) - self.__data.canvasRect.setRect( - rect.x() + dimAxes[QwtPlot.yLeft], - rect.y() + dimAxes[QwtPlot.xTop], - rect.width() - dimAxes[QwtPlot.yRight] - dimAxes[QwtPlot.yLeft], - rect.height() - dimAxes[QwtPlot.xBottom] - dimAxes[QwtPlot.xTop], - ) - for axis in QwtPlot.AXES: - if dimAxes[axis]: - dim = dimAxes[axis] - scaleRect = self.__data.scaleRect[axis] - scaleRect.setRect(*self.__data.canvasRect.getRect()) - if axis == QwtPlot.yLeft: - scaleRect.setX(self.__data.canvasRect.left() - dim) - scaleRect.setWidth(dim) - elif axis == QwtPlot.yRight: - scaleRect.setX(self.__data.canvasRect.right()) - scaleRect.setWidth(dim) - elif axis == QwtPlot.xBottom: - scaleRect.setY(self.__data.canvasRect.bottom()) - scaleRect.setHeight(dim) - elif axis == QwtPlot.xTop: - scaleRect.setY(self.__data.canvasRect.top() - dim) - scaleRect.setHeight(dim) - scaleRect = scaleRect.normalized() - - # +---+-----------+---+ - # | <- Axis -> | - # +-^-+-----------+-^-+ - # | | | | | | - # | | | | - # | A | | A | - # | x | Canvas | x | - # | i | | i | - # | s | | s | - # | | | | - # | | | | | | - # +-V-+-----------+-V-+ - # | <- Axis -> | - # +---+-----------+---+ - - # The ticks of the axes - not the labels above - should - # be aligned to the canvas. So we try to use the empty - # corners to extend the axes, so that the label texts - # left/right of the min/max ticks are moved into them. - - self.alignScales(options, self.__data.canvasRect, self.__data.scaleRect) - if not self.__data.legendRect.isEmpty(): - self.__data.legendRect = self.alignLegend( - self.__data.canvasRect, self.__data.legendRect - ) diff --git a/qwt/plot_marker.py b/qwt/plot_marker.py deleted file mode 100644 index 1db25cd..0000000 --- a/qwt/plot_marker.py +++ /dev/null @@ -1,633 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotMarker -------------- - -.. autoclass:: QwtPlotMarker - :members: -""" - -from qtpy.QtCore import QLineF, QObject, QPointF, QRect, QRectF, QSizeF, Qt -from qtpy.QtGui import QPainter, QPen - -from qwt.graphic import QwtGraphic -from qwt.plot import QwtPlot, QwtPlotItem -from qwt.qthelpers import qcolor_from_str -from qwt.symbol import QwtSymbol -from qwt.text import QwtText - - -class QwtPlotMarker_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.labelAlignment = Qt.AlignCenter - self.labelOrientation = Qt.Horizontal - self.spacing = 2 - self.symbol = None - self.style = QwtPlotMarker.NoLine - self.xValue = 0.0 - self.yValue = 0.0 - self.label = QwtText() - self.pen = QPen() - - -class QwtPlotMarker(QwtPlotItem): - """ - A class for drawing markers - - A marker can be a horizontal line, a vertical line, - a symbol, a label or any combination of them, which can - be drawn around a center point inside a bounding rectangle. - - The `setSymbol()` member assigns a symbol to the marker. - The symbol is drawn at the specified point. - - With `setLabel()`, a label can be assigned to the marker. - The `setLabelAlignment()` member specifies where the label is drawn. All - the Align*-constants in `Qt.AlignmentFlags` (see Qt documentation) - are valid. The interpretation of the alignment depends on the marker's - line style. The alignment refers to the center point of - the marker, which means, for example, that the label would be printed - left above the center point if the alignment was set to - `Qt.AlignLeft | Qt.AlignTop`. - - Line styles: - - * `QwtPlotMarker.NoLine`: No line - * `QwtPlotMarker.HLine`: A horizontal line - * `QwtPlotMarker.VLine`: A vertical line - * `QwtPlotMarker.Cross`: A crosshair - """ - - # enum LineStyle - NoLine, HLine, VLine, Cross = list(range(4)) - - def __init__(self, title=None): - if title is None: - title = "" - if not isinstance(title, QwtText): - title = QwtText(title) - QwtPlotItem.__init__(self, title) - self.__data = QwtPlotMarker_PrivateData() - self.setZ(30.0) - - @classmethod - def make( - cls, - xvalue=None, - yvalue=None, - title=None, - label=None, - symbol=None, - plot=None, - z=None, - x_axis=None, - y_axis=None, - align=None, - orientation=None, - spacing=None, - linestyle=None, - color=None, - width=None, - style=None, - antialiased=False, - ): - """ - Create and setup a new `QwtPlotMarker` object (convenience function). - - :param xvalue: x position (optional, default: None) - :type xvalue: float or None - :param yvalue: y position (optional, default: None) - :type yvalue: float or None - :param title: Marker title - :type title: qwt.text.QwtText or str or None - :param label: Label text - :type label: qwt.text.QwtText or str or None - :param symbol: New symbol - :type symbol: qwt.symbol.QwtSymbol or None - :param plot: Plot to attach the curve to - :type plot: qwt.plot.QwtPlot or None - :param z: Z-value - :type z: float or None - :param int x_axis: curve X-axis (default: QwtPlot.yLeft) - :param int y_axis: curve Y-axis (default: QwtPlot.xBottom) - :param align: Alignment of the label - :type align: Qt.Alignment or None - :param orientation: Orientation of the label - :type orientation: Qt.Orientation or None - :param spacing: Spacing (distance between the position and the label) - :type spacing: int or None - :param int linestyle: Line style - :param color: Pen color - :type color: QColor or str or None - :param float width: Pen width - :param Qt.PenStyle style: Pen style - :param bool antialiased: if True, enable antialiasing rendering - - .. seealso:: - - :py:meth:`setData()`, :py:meth:`setPen()`, :py:meth:`attach()` - """ - item = cls(title) - if z is not None: - item.setZ(z) - if symbol is not None: - item.setSymbol(symbol) - if xvalue is not None: - item.setXValue(xvalue) - if yvalue is not None: - item.setYValue(yvalue) - if label is not None: - item.setLabel(label) - x_axis = QwtPlot.xBottom if x_axis is None else x_axis - y_axis = QwtPlot.yLeft if y_axis is None else y_axis - item.setAxes(x_axis, y_axis) - if align is not None: - item.setLabelAlignment(align) - if orientation is not None: - item.setLabelOrientation(orientation) - if spacing is not None: - item.setSpacing(spacing) - color = qcolor_from_str(color, Qt.black) - width = 1.0 if width is None else width - style = Qt.SolidLine if style is None else style - item.setLinePen(QPen(color, width, style)) - item.setRenderHint(cls.RenderAntialiased, antialiased) - if linestyle is not None: - item.setLineStyle(linestyle) - if plot is not None: - item.attach(plot) - return item - - def rtti(self): - """:return: `QwtPlotItem.Rtti_PlotMarker`""" - return QwtPlotItem.Rtti_PlotMarker - - def value(self): - """:return: Value""" - return QPointF(self.__data.xValue, self.__data.yValue) - - def xValue(self): - """:return: x Value""" - return self.__data.xValue - - def yValue(self): - """:return: y Value""" - return self.__data.yValue - - def setValue(self, *args): - """ - Set Value - - .. py:method:: setValue(pos): - - :param QPointF pos: Position - - .. py:method:: setValue(x, y): - - :param float x: x position - :param float y: y position - """ - if len(args) == 1: - (pos,) = args - self.setValue(pos.x(), pos.y()) - elif len(args) == 2: - x, y = args - if x != self.__data.xValue or y != self.__data.yValue: - self.__data.xValue = x - self.__data.yValue = y - self.itemChanged() - else: - raise TypeError( - "%s() takes 1 or 2 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def setXValue(self, x): - """ - Set X Value - - :param float x: x position - """ - self.setValue(x, self.__data.yValue) - - def setYValue(self, y): - """ - Set Y Value - - :param float y: y position - """ - self.setValue(self.__data.xValue, y) - - def draw(self, painter, xMap, yMap, canvasRect): - """ - Draw the marker - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: x Scale Map - :param qwt.scale_map.QwtScaleMap yMap: y Scale Map - :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates - """ - pos = QPointF( - xMap.transform(self.__data.xValue), yMap.transform(self.__data.yValue) - ) - self.drawLines(painter, canvasRect, pos) - if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol: - sz = self.__data.symbol.size() - width, height = int(sz.width()), int(sz.height()) - clipRect = QRectF(canvasRect.adjusted(-width, -height, width, height)) - if clipRect.contains(pos): - self.__data.symbol.drawSymbols(painter, [pos]) - self.drawLabel(painter, canvasRect, pos) - - def drawLines(self, painter, canvasRect, pos): - """ - Draw the lines marker - - :param QPainter painter: Painter - :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates - :param QPointF pos: Position of the marker, translated into widget coordinates - - .. seealso:: - - :py:meth:`drawLabel()`, - :py:meth:`qwt.symbol.QwtSymbol.drawSymbol()` - """ - if self.__data.style == self.NoLine: - return - painter.setPen(self.__data.pen) - if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross): - y = pos.y() - painter.drawLine(QLineF(canvasRect.left(), y, canvasRect.right() - 1.0, y)) - if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross): - x = pos.x() - painter.drawLine(QLineF(x, canvasRect.top(), x, canvasRect.bottom() - 1.0)) - - def drawLabel(self, painter, canvasRect, pos): - """ - Align and draw the text label of the marker - - :param QPainter painter: Painter - :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates - :param QPointF pos: Position of the marker, translated into widget coordinates - - .. seealso:: - - :py:meth:`drawLabel()`, - :py:meth:`qwt.symbol.QwtSymbol.drawSymbol()` - """ - if self.__data.label.isEmpty(): - return - align = self.__data.labelAlignment - alignPos = QPointF(pos) - symbolOff = QSizeF(0, 0) - if self.__data.style == QwtPlotMarker.VLine: - # In VLine-style the y-position is pointless and - # the alignment flags are relative to the canvas - if bool(self.__data.labelAlignment & Qt.AlignTop): - alignPos.setY(canvasRect.top()) - align &= ~Qt.AlignTop - align |= Qt.AlignBottom - elif bool(self.__data.labelAlignment & Qt.AlignBottom): - # In HLine-style the x-position is pointless and - # the alignment flags are relative to the canvas - alignPos.setY(canvasRect.bottom() - 1) - align &= ~Qt.AlignBottom - align |= Qt.AlignTop - else: - alignPos.setY(canvasRect.center().y()) - elif self.__data.style == QwtPlotMarker.HLine: - if bool(self.__data.labelAlignment & Qt.AlignLeft): - alignPos.setX(canvasRect.left()) - align &= ~Qt.AlignLeft - align |= Qt.AlignRight - elif bool(self.__data.labelAlignment & Qt.AlignRight): - alignPos.setX(canvasRect.right() - 1) - align &= ~Qt.AlignRight - align |= Qt.AlignLeft - else: - alignPos.setX(canvasRect.center().x()) - else: - if self.__data.symbol and self.__data.symbol.style() != QwtSymbol.NoSymbol: - symbolOff = QSizeF(self.__data.symbol.size()) + QSizeF(1, 1) - symbolOff /= 2 - pw2 = self.__data.pen.widthF() / 2.0 - if pw2 == 0.0: - pw2 = 0.5 - spacing = self.__data.spacing - xOff = max([pw2, symbolOff.width()]) - yOff = max([pw2, symbolOff.height()]) - textSize = self.__data.label.textSize(painter.font()) - if align & Qt.AlignLeft: - alignPos.setX(alignPos.x() - (xOff + spacing)) - if self.__data.labelOrientation == Qt.Vertical: - alignPos.setX(alignPos.x() - textSize.height()) - else: - alignPos.setX(alignPos.x() - textSize.width()) - elif align & Qt.AlignRight: - alignPos.setX(alignPos.x() + xOff + spacing) - else: - if self.__data.labelOrientation == Qt.Vertical: - alignPos.setX(alignPos.x() - textSize.height() / 2) - else: - alignPos.setX(alignPos.x() - textSize.width() / 2) - if align & Qt.AlignTop: - alignPos.setY(alignPos.y() - (yOff + spacing)) - if self.__data.labelOrientation != Qt.Vertical: - alignPos.setY(alignPos.y() - textSize.height()) - elif align & Qt.AlignBottom: - alignPos.setY(alignPos.y() + yOff + spacing) - if self.__data.labelOrientation == Qt.Vertical: - alignPos.setY(alignPos.y() + textSize.width()) - else: - if self.__data.labelOrientation == Qt.Vertical: - alignPos.setY(alignPos.y() + textSize.width() / 2) - else: - alignPos.setY(alignPos.y() - textSize.height() / 2) - painter.translate(alignPos.x(), alignPos.y()) - if self.__data.labelOrientation == Qt.Vertical: - painter.rotate(-90.0) - textRect = QRectF(0, 0, textSize.width(), textSize.height()) - self.__data.label.draw(painter, textRect) - - def setLineStyle(self, style): - """ - Set the line style - - :param int style: Line style - - Line styles: - - * `QwtPlotMarker.NoLine`: No line - * `QwtPlotMarker.HLine`: A horizontal line - * `QwtPlotMarker.VLine`: A vertical line - * `QwtPlotMarker.Cross`: A crosshair - - .. seealso:: - - :py:meth:`lineStyle()` - """ - if style != self.__data.style: - self.__data.style = style - self.legendChanged() - self.itemChanged() - - def lineStyle(self): - """ - :return: the line style - - .. seealso:: - - :py:meth:`setLineStyle()` - """ - return self.__data.style - - def setSymbol(self, symbol): - """ - Assign a symbol - - :param qwt.symbol.QwtSymbol symbol: New symbol - - .. seealso:: - - :py:meth:`symbol()` - """ - if symbol != self.__data.symbol: - self.__data.symbol = symbol - if symbol is not None: - self.setLegendIconSize(symbol.boundingRect().size()) - self.legendChanged() - self.itemChanged() - - def symbol(self): - """ - :return: the symbol - - .. seealso:: - - :py:meth:`setSymbol()` - """ - return self.__data.symbol - - def setLabel(self, label): - """ - Set the label - - :param label: Label text - :type label: qwt.text.QwtText or str - - .. seealso:: - - :py:meth:`label()` - """ - if not isinstance(label, QwtText): - label = QwtText(label) - if label != self.__data.label: - self.__data.label = label - self.itemChanged() - - def label(self): - """ - :return: the label - - .. seealso:: - - :py:meth:`setLabel()` - """ - return self.__data.label - - def setLabelAlignment(self, align): - """ - Set the alignment of the label - - In case of `QwtPlotMarker.HLine` the alignment is relative to the - y position of the marker, but the horizontal flags correspond to the - canvas rectangle. In case of `QwtPlotMarker.VLine` the alignment is - relative to the x position of the marker, but the vertical flags - correspond to the canvas rectangle. - - In all other styles the alignment is relative to the marker's position. - - :param Qt.Alignment align: Alignment - - .. seealso:: - - :py:meth:`labelAlignment()`, :py:meth:`labelOrientation()` - """ - if align != self.__data.labelAlignment: - self.__data.labelAlignment = align - self.itemChanged() - - def labelAlignment(self): - """ - :return: the label alignment - - .. seealso:: - - :py:meth:`setLabelAlignment()`, :py:meth:`setLabelOrientation()` - """ - return self.__data.labelAlignment - - def setLabelOrientation(self, orientation): - """ - Set the orientation of the label - - When orientation is `Qt.Vertical` the label is rotated by 90.0 degrees - (from bottom to top). - - :param Qt.Orientation orientation: Orientation of the label - - .. seealso:: - - :py:meth:`labelOrientation()`, :py:meth:`setLabelAlignment()` - """ - if orientation != self.__data.labelOrientation: - self.__data.labelOrientation = orientation - self.itemChanged() - - def labelOrientation(self): - """ - :return: the label orientation - - .. seealso:: - - :py:meth:`setLabelOrientation()`, :py:meth:`labelAlignment()` - """ - return self.__data.labelOrientation - - def setSpacing(self, spacing): - """ - Set the spacing - - When the label is not centered on the marker position, the spacing - is the distance between the position and the label. - - :param int spacing: Spacing - - .. seealso:: - - :py:meth:`spacing()`, :py:meth:`setLabelAlignment()` - """ - if spacing < 0: - spacing = 0 - if spacing != self.__data.spacing: - self.__data.spacing = spacing - self.itemChanged() - - def spacing(self): - """ - :return: the spacing - - .. seealso:: - - :py:meth:`setSpacing()` - """ - return self.__data.spacing - - def setLinePen(self, *args): - """ - Build and/or assigna a line pen, depending on the arguments. - - .. py:method:: setLinePen(color, width, style) - :noindex: - - Build and assign a line pen - - In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it - non cosmetic (see `QPen.isCosmetic()`). This method signature has - been introduced to hide this incompatibility. - - :param QColor color: Pen color - :param float width: Pen width - :param Qt.PenStyle style: Pen style - - .. py:method:: setLinePen(pen) - :noindex: - - Specify a pen for the line. - - :param QPen pen: New pen - - .. seealso:: - - :py:meth:`pen()`, :py:meth:`brush()` - """ - if len(args) == 1 and isinstance(args[0], QPen): - (pen,) = args - elif len(args) in (1, 2, 3): - color = args[0] - width = 0.0 - style = Qt.SolidLine - if len(args) > 1: - width = args[1] - if len(args) > 2: - style = args[2] - pen = QPen(color, width, style) - self.setLinePen(pen) - else: - raise TypeError( - "%s().setLinePen() takes 1, 2 or 3 argument(s) " - "(%s given)" % (self.__class__.__name__, len(args)) - ) - if pen != self.__data.pen: - self.__data.pen = pen - self.legendChanged() - self.itemChanged() - - def linePen(self): - """ - :return: the line pen - - .. seealso:: - - :py:meth:`setLinePen()` - """ - return self.__data.pen - - def boundingRect(self): - if self.__data.style == QwtPlotMarker.HLine: - return QRectF(self.__data.xValue, self.__data.yValue, -1.0, 0.0) - elif self.__data.style == QwtPlotMarker.VLine: - return QRectF(self.__data.xValue, self.__data.yValue, 0.0, -1.0) - else: - return QRectF(self.__data.xValue, self.__data.yValue, 0.0, 0.0) - - def legendIcon(self, index, size): - """ - :param int index: Index of the legend entry (ignored as there is only one) - :param QSizeF size: Icon size - :return: Icon representing the marker on the legend - - .. seealso:: - - :py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`, - :py:meth:`qwt.plot.QwtPlotItem.legendData()` - """ - if size.isEmpty(): - return QwtGraphic() - icon = QwtGraphic() - icon.setDefaultSize(size) - icon.setRenderHint(QwtGraphic.RenderPensUnscaled, True) - painter = QPainter(icon) - painter.setRenderHint( - QPainter.Antialiasing, self.testRenderHint(QwtPlotItem.RenderAntialiased) - ) - if self.__data.style != QwtPlotMarker.NoLine: - painter.setPen(self.__data.pen) - if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross): - y = 0.5 * size.height() - painter.drawLine(QLineF(0.0, y, size.width(), y)) - if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross): - x = 0.5 * size.width() - painter.drawLine(QLineF(x, 0.0, x, size.height())) - if self.__data.symbol: - r = QRect(0, 0, size.width(), size.height()) - self.__data.symbol.drawSymbol(painter, r) - return icon diff --git a/qwt/plot_renderer.py b/qwt/plot_renderer.py deleted file mode 100644 index d8b2640..0000000 --- a/qwt/plot_renderer.py +++ /dev/null @@ -1,739 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtPlotRenderer ---------------- - -.. autoclass:: QwtPlotRenderer - :members: -""" - -import math -import os.path as osp - -from qtpy.compat import getsavefilename -from qtpy.QtCore import QObject, QRect, QRectF, QSizeF, Qt -from qtpy.QtGui import ( - QColor, - QImage, - QImageWriter, - QPageSize, - QPaintDevice, - QPainter, - QPainterPath, - QPalette, - QPen, - QTransform, -) -from qtpy.QtPrintSupport import QPrinter -from qtpy.QtSvg import QSvgGenerator -from qtpy.QtWidgets import QFileDialog - -from qwt.painter import QwtPainter -from qwt.plot import QwtPlot -from qwt.plot_layout import QwtPlotLayout -from qwt.scale_draw import QwtScaleDraw -from qwt.scale_map import QwtScaleMap - - -def qwtCanvasClip(canvas, canvasRect): - """ - The clip region is calculated in integers - To avoid too much rounding errors better - calculate it in target device resolution - """ - x1 = math.ceil(canvasRect.left()) - x2 = math.floor(canvasRect.right()) - y1 = math.ceil(canvasRect.top()) - y2 = math.floor(canvasRect.bottom()) - r = QRect(x1, y1, x2 - x1 - 1, y2 - y1 - 1) - return canvas.borderPath(r) - - -class QwtPlotRenderer_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.discardFlags = QwtPlotRenderer.DiscardNone - self.layoutFlags = QwtPlotRenderer.DefaultLayout - - -class QwtPlotRenderer(QObject): - """ - Renderer for exporting a plot to a document, a printer - or anything else, that is supported by QPainter/QPaintDevice - - Discard flags: - - * `QwtPlotRenderer.DiscardNone`: Render all components of the plot - * `QwtPlotRenderer.DiscardBackground`: Don't render the background of the plot - * `QwtPlotRenderer.DiscardTitle`: Don't render the title of the plot - * `QwtPlotRenderer.DiscardLegend`: Don't render the legend of the plot - * `QwtPlotRenderer.DiscardCanvasBackground`: Don't render the background of the canvas - * `QwtPlotRenderer.DiscardFooter`: Don't render the footer of the plot - * `QwtPlotRenderer.DiscardCanvasFrame`: Don't render the frame of the canvas - - .. note:: - - The `QwtPlotRenderer.DiscardCanvasFrame` flag has no effect when using - style sheets, where the frame is part of the background - - Layout flags: - - * `QwtPlotRenderer.DefaultLayout`: Use the default layout as on screen - * `QwtPlotRenderer.FrameWithScales`: Instead of the scales a box is painted around the plot canvas, where the scale ticks are aligned to. - """ - - # enum DiscardFlag - DiscardNone = 0x00 - DiscardBackground = 0x01 - DiscardTitle = 0x02 - DiscardLegend = 0x04 - DiscardCanvasBackground = 0x08 - DiscardFooter = 0x10 - DiscardCanvasFrame = 0x20 - - # enum LayoutFlag - DefaultLayout = 0x00 - FrameWithScales = 0x01 - - def __init__(self, parent=None): - QObject.__init__(self, parent) - self.__data = QwtPlotRenderer_PrivateData() - - def setDiscardFlag(self, flag, on=True): - """ - Change a flag, indicating what to discard from rendering - - :param int flag: Flag to change - :param bool on: On/Off - - .. seealso:: - - :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlags()`, - :py:meth:`discardFlags()` - """ - if on: - self.__data.discardFlags |= flag - else: - self.__data.discardFlags &= ~flag - - def testDiscardFlag(self, flag): - """ - :param int flag: Flag to be tested - :return: True, if flag is enabled. - - .. seealso:: - - :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`, - :py:meth:`discardFlags()` - """ - return self.__data.discardFlags & flag - - def setDiscardFlags(self, flags): - """ - Set the flags, indicating what to discard from rendering - - :param int flags: Flags - - .. seealso:: - - :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlag()`, - :py:meth:`discardFlags()` - """ - self.__data.discardFlags = flags - - def discardFlags(self): - """ - :return: Flags, indicating what to discard from rendering - - .. seealso:: - - :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`, - :py:meth:`testDiscardFlag()` - """ - return self.__data.discardFlags - - def setLayoutFlag(self, flag, on=True): - """ - Change a layout flag - - :param int flag: Flag to change - - .. seealso:: - - :py:meth:`testLayoutFlag()`, :py:meth:`setLayoutFlags()`, - :py:meth:`layoutFlags()` - """ - if on: - self.__data.layoutFlags |= flag - else: - self.__data.layoutFlags &= ~flag - - def testLayoutFlag(self, flag): - """ - :param int flag: Flag to be tested - :return: True, if flag is enabled. - - .. seealso:: - - :py:meth:`setLayoutFlag()`, :py:meth:`setLayoutFlags()`, - :py:meth:`layoutFlags()` - """ - return self.__data.layoutFlags & flag - - def setLayoutFlags(self, flags): - """ - Set the layout flags - - :param int flags: Flags - - .. seealso:: - - :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()`, - :py:meth:`layoutFlags()` - """ - self.__data.layoutFlags = flags - - def layoutFlags(self): - """ - :return: Layout flags - - .. seealso:: - - :py:meth:`setLayoutFlags()`, :py:meth:`setLayoutFlag()`, - :py:meth:`testLayoutFlag()` - """ - return self.__data.layoutFlags - - def renderDocument( - self, plot, filename, sizeMM=(300, 200), resolution=85, format_=None - ): - """ - Render a plot to a file - - The format of the document will be auto-detected from the - suffix of the file name. - - :param qwt.plot.QwtPlot plot: Plot widget - :param str fileName: Path of the file, where the document will be stored - :param QSizeF sizeMM: Size for the document in millimeters - :param int resolution: Resolution in dots per Inch (dpi) - """ - if isinstance(sizeMM, tuple): - sizeMM = QSizeF(*sizeMM) - if format_ is None: - ext = osp.splitext(filename)[1] - if not ext: - raise TypeError("Unable to determine target format from filename") - format_ = ext[1:] - if plot is None or sizeMM.isEmpty() or resolution <= 0: - return - title = plot.title().text() - if not title: - title = "Plot Document" - mmToInch = 1.0 / 25.4 - size = sizeMM * mmToInch * resolution - documentRect = QRectF(0.0, 0.0, size.width(), size.height()) - fmt = format_.lower() - if fmt in ("pdf", "ps"): - printer = QPrinter() - if fmt == "pdf": - try: - printer.setOutputFormat(QPrinter.PdfFormat) - except AttributeError: - # PyQt6 on Linux - printer.setPrinterName("") - else: - printer.setOutputFormat(QPrinter.PostScriptFormat) - try: - printer.setColorMode(QPrinter.Color) - except AttributeError: - # PyQt6 on Linux - pass - printer.setFullPage(True) - printer.setPageSize(QPageSize(sizeMM, QPageSize.Millimeter)) - printer.setDocName(title) - printer.setOutputFileName(filename) - printer.setResolution(resolution) - painter = QPainter(printer) - self.render(plot, painter, documentRect) - painter.end() - elif fmt == "svg": - generator = QSvgGenerator() - generator.setTitle(title) - generator.setFileName(filename) - generator.setResolution(resolution) - generator.setViewBox(documentRect) - painter = QPainter(generator) - self.render(plot, painter, documentRect) - painter.end() - elif fmt in QImageWriter.supportedImageFormats(): - imageRect = documentRect.toRect() - dotsPerMeter = int(round(resolution * mmToInch * 1000.0)) - image = QImage(imageRect.size(), QImage.Format_ARGB32) - image.setDotsPerMeterX(dotsPerMeter) - image.setDotsPerMeterY(dotsPerMeter) - image.fill(QColor(Qt.white).rgb()) - painter = QPainter(image) - self.render(plot, painter, imageRect) - painter.end() - image.save(filename, fmt) - else: - raise TypeError("Unsupported file format '%s'" % fmt) - - def renderTo(self, plot, dest): - """ - Render a plot to a file - - Supported formats are: - - - pdf: Portable Document Format PDF - - ps: Postcript - - svg: Scalable Vector Graphics SVG - - all image formats supported by Qt, see QImageWriter.supportedImageFormats() - - Scalable vector graphic formats like PDF or SVG are superior to - raster graphics formats. - - :param qwt.plot.QwtPlot plot: Plot widget - :param dest: QPaintDevice, QPrinter or QSvgGenerator instance - - .. seealso:: - - :py:meth:`render()`, - :py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()` - """ - if isinstance(dest, QPaintDevice): - w = dest.width() - h = dest.height() - rect = QRectF(0, 0, w, h) - elif isinstance(dest, QPrinter): - w = dest.width() - h = dest.height() - rect = QRectF(0, 0, w, h) - aspect = rect.width() / rect.height() - if aspect < 1.0: - rect.setHeight(aspect * rect.width()) - elif isinstance(dest, QSvgGenerator): - rect = dest.viewBoxF() - if rect.isEmpty(): - rect.setRect(0, 0, dest.width(), dest.height()) - if rect.isEmpty(): - rect.setRect(0, 0, 800, 600) - else: - raise TypeError("Unsupported destination type %s" % type(dest)) - p = QPainter(dest) - self.render(plot, p, rect) - - def render(self, plot, painter, plotRect): - """ - Paint the contents of a QwtPlot instance into a given rectangle. - - :param qwt.plot.QwtPlot plot: Plot to be rendered - :param QPainter painter: Painter - :param str format: Format for the document - :param QRectF plotRect: Bounding rectangle - - .. seealso:: - - :py:meth:`renderDocument()`, :py:meth:`renderTo()`, - :py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()` - """ - if ( - painter == 0 - or not painter.isActive() - or not plotRect.isValid() - or plot.size().isNull() - ): - return - if not self.__data.discardFlags & self.DiscardBackground: - QwtPainter.drawBackground(painter, plotRect, plot) - - # The layout engine uses the same methods as they are used - # by the Qt layout system. Therefore we need to calculate the - # layout in screen coordinates and paint with a scaled painter. - transform = QTransform() - transform.scale( - float(painter.device().logicalDpiX()) / plot.logicalDpiX(), - float(painter.device().logicalDpiY()) / plot.logicalDpiY(), - ) - - invtrans, _ok = transform.inverted() - layoutRect = invtrans.mapRect(plotRect) - if not (self.__data.discardFlags & self.DiscardBackground): - mg = plot.contentsMargins() - layoutRect.adjust(mg.left(), mg.top(), -mg.right(), -mg.bottom()) - - layout = plot.plotLayout() - baseLineDists = canvasMargins = [None] * len(QwtPlot.AXES) - - for axisId in QwtPlot.AXES: - canvasMargins[axisId] = layout.canvasMargin(axisId) - if self.__data.layoutFlags & self.FrameWithScales: - scaleWidget = plot.axisWidget(axisId) - if scaleWidget: - mgn = scaleWidget.contentsMargins() - baseLineDists[axisId] = max( - [mgn.left(), mgn.top(), mgn.right(), mgn.bottom()] - ) - scaleWidget.setMargin(0) - if not plot.axisEnabled(axisId): - # When we have a scale the frame is painted on - # the position of the backbone - otherwise we - # need to introduce a margin around the canvas - if axisId == QwtPlot.yLeft: - layoutRect.adjust(1, 0, 0, 0) - elif axisId == QwtPlot.yRight: - layoutRect.adjust(0, 0, -1, 0) - elif axisId == QwtPlot.xTop: - layoutRect.adjust(0, 1, 0, 0) - elif axisId == QwtPlot.xBottom: - layoutRect.adjust(0, 0, 0, -1) - - # Calculate the layout for the document. - layoutOptions = QwtPlotLayout.IgnoreScrollbars - - if ( - self.__data.layoutFlags & self.FrameWithScales - or self.__data.discardFlags & self.DiscardCanvasFrame - ): - layoutOptions |= QwtPlotLayout.IgnoreFrames - - if self.__data.discardFlags & self.DiscardLegend: - layoutOptions |= QwtPlotLayout.IgnoreLegend - if self.__data.discardFlags & self.DiscardTitle: - layoutOptions |= QwtPlotLayout.IgnoreTitle - if self.__data.discardFlags & self.DiscardFooter: - layoutOptions |= QwtPlotLayout.IgnoreFooter - - layout.activate(plot, layoutRect, layoutOptions) - - maps = self.buildCanvasMaps(plot, layout.canvasRect()) - if self.updateCanvasMargins(plot, layout.canvasRect(), maps): - # recalculate maps and layout, when the margins - # have been changed - layout.activate(plot, layoutRect, layoutOptions) - maps = self.buildCanvasMaps(plot, layout.canvasRect()) - - painter.save() - painter.setWorldTransform(transform, True) - - self.renderCanvas(plot, painter, layout.canvasRect(), maps) - - if ( - not self.__data.discardFlags & self.DiscardTitle - ) and plot.titleLabel().text(): - self.renderTitle(plot, painter, layout.titleRect()) - - if ( - not self.__data.discardFlags & self.DiscardFooter - ) and plot.titleLabel().text(): - self.renderFooter(plot, painter, layout.footerRect()) - - if ( - not self.__data.discardFlags & self.DiscardLegend - ) and plot.titleLabel().text(): - self.renderLegend(plot, painter, layout.legendRect()) - - for axisId in QwtPlot.AXES: - scaleWidget = plot.axisWidget(axisId) - if scaleWidget: - mgn = scaleWidget.contentsMargins() - baseDist = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()]) - startDist, endDist = scaleWidget.getBorderDistHint() - self.renderScale( - plot, - painter, - axisId, - startDist, - endDist, - baseDist, - layout.scaleRect(axisId), - ) - - painter.restore() - - for axisId in QwtPlot.AXES: - if self.__data.layoutFlags & self.FrameWithScales: - scaleWidget = plot.axisWidget(axisId) - if scaleWidget: - scaleWidget.setMargin(baseLineDists[axisId]) - layout.setCanvasMargin(canvasMargins[axisId]) - - layout.invalidate() - - def renderTitle(self, plot, painter, rect): - """ - Render the title into a given rectangle. - - :param qwt.plot.QwtPlot plot: Plot widget - :param QPainter painter: Painter - :param QRectF rect: Bounding rectangle - """ - painter.setFont(plot.titleLabel().font()) - color = plot.titleLabel().palette().color(QPalette.Active, QPalette.Text) - painter.setPen(color) - plot.titleLabel().text().draw(painter, rect) - - def renderFooter(self, plot, painter, rect): - """ - Render the footer into a given rectangle. - - :param qwt.plot.QwtPlot plot: Plot widget - :param QPainter painter: Painter - :param QRectF rect: Bounding rectangle - """ - painter.setFont(plot.footerLabel().font()) - color = plot.footerLabel().palette().color(QPalette.Active, QPalette.Text) - painter.setPen(color) - plot.footerLabel().text().draw(painter, rect) - - def renderLegend(self, plot, painter, rect): - """ - Render the legend into a given rectangle. - - :param qwt.plot.QwtPlot plot: Plot widget - :param QPainter painter: Painter - :param QRectF rect: Bounding rectangle - """ - if plot.legend(): - fillBackground = not self.__data.discardFlags & self.DiscardBackground - plot.legend().renderLegend(painter, rect, fillBackground) - - def renderScale(self, plot, painter, axisId, startDist, endDist, baseDist, rect): - """ - Paint a scale into a given rectangle. - Paint the scale into a given rectangle. - - :param qwt.plot.QwtPlot plot: Plot widget - :param QPainter painter: Painter - :param int axisId: Axis - :param int startDist: Start border distance - :param int endDist: End border distance - :param int baseDist: Base distance - :param QRectF rect: Bounding rectangle - """ - if not plot.axisEnabled(axisId): - return - scaleWidget = plot.axisWidget(axisId) - if scaleWidget.isColorBarEnabled() and scaleWidget.colorBarWidth() > 0: - scaleWidget.drawColorBar(painter, scaleWidget.colorBarRect(rect)) - baseDist += scaleWidget.colorBarWidth() + scaleWidget.spacing() - painter.save() - if axisId == QwtPlot.yLeft: - x = rect.right() - 1.0 - baseDist - y = rect.y() + startDist - w = rect.height() - startDist - endDist - align = QwtScaleDraw.LeftScale - elif axisId == QwtPlot.yRight: - x = rect.left() + baseDist - y = rect.y() + startDist - w = rect.height() - startDist - endDist - align = QwtScaleDraw.RightScale - elif axisId == QwtPlot.xTop: - x = rect.left() + startDist - y = rect.bottom() - 1.0 - baseDist - w = rect.width() - startDist - endDist - align = QwtScaleDraw.TopScale - else: # QwtPlot.xBottom - x = rect.left() + startDist - y = rect.top() + baseDist - w = rect.width() - startDist - endDist - align = QwtScaleDraw.BottomScale - - scaleWidget.drawTitle(painter, align, rect) - painter.setFont(scaleWidget.font()) - sd = scaleWidget.scaleDraw() - sdPos = sd.pos() - sdLength = sd.length() - sd.move(x, y) - sd.setLength(w) - palette = scaleWidget.palette() - palette.setCurrentColorGroup(QPalette.Active) - sd.draw(painter, palette) - sd.move(sdPos) - sd.setLength(sdLength) - painter.restore() - - def renderCanvas(self, plot, painter, canvasRect, maps): - """ - Render the canvas into a given rectangle. - - :param qwt.plot.QwtPlot plot: Plot widget - :param QPainter painter: Painter - :param QRectF rect: Bounding rectangle - :param qwt.scale_map.QwtScaleMap maps: mapping between plot and paint device coordinates - """ - canvas = plot.canvas() - r = canvasRect.adjusted(0.0, 0.0, -1.0, 1.0) - if self.__data.layoutFlags & self.FrameWithScales: - painter.save() - r.adjust(-1.0, -1.0, 1.0, 1.0) - painter.setPen(QPen(Qt.black)) - if not (self.__data.discardFlags & self.DiscardCanvasBackground): - bgBrush = canvas.palette().brush(plot.backgroundRole()) - painter.setBrush(bgBrush) - painter.drawRect(r) - painter.restore() - painter.save() - painter.setClipRect(canvasRect) - plot.drawItems(painter, canvasRect, maps) - painter.restore() - elif canvas.testAttribute(Qt.WA_StyledBackground): - clipPath = QPainterPath() - painter.save() - if not self.__data.discardFlags & self.DiscardCanvasBackground: - QwtPainter.drawBackground(painter, r, canvas) - clipPath = qwtCanvasClip(canvas, canvasRect) - painter.restore() - painter.save() - if clipPath.isEmpty(): - painter.setClipRect(canvasRect) - else: - painter.setClipPath(clipPath) - plot.drawItems(painter, canvasRect, maps) - painter.restore() - else: - clipPath = QPainterPath() - frameWidth = 0 - if not self.__data.discardFlags & self.DiscardCanvasFrame: - frameWidth = canvas.frameWidth() - clipPath = qwtCanvasClip(canvas, canvasRect) - innerRect = canvasRect.adjusted( - frameWidth, frameWidth, -frameWidth, -frameWidth - ) - painter.save() - if clipPath.isEmpty(): - painter.setClipRect(innerRect) - else: - painter.setClipPath(clipPath) - if not self.__data.discardFlags & self.DiscardCanvasBackground: - QwtPainter.drawBackground(painter, innerRect, canvas) - plot.drawItems(painter, innerRect, maps) - painter.restore() - if frameWidth > 0: - painter.save() - frameStyle = canvas.frameShadow() | canvas.frameShape() - radius = canvas.borderRadius() - if radius > 0.0: - QwtPainter.drawRoundedFrame( - painter, - canvasRect, - radius, - radius, - canvas.palette(), - frameWidth, - frameStyle, - ) - else: - midLineWidth = canvas.midLineWidth() - QwtPainter.drawFrame( - painter, - canvasRect, - canvas.palette(), - canvas.foregroundRole(), - frameWidth, - midLineWidth, - frameStyle, - ) - painter.restore() - - def buildCanvasMaps(self, plot, canvasRect): - """ - Calculated the scale maps for rendering the canvas - - :param qwt.plot.QwtPlot plot: Plot widget - :param QRectF canvasRect: Target rectangle - :return: Calculated scale maps - """ - maps = [] - for axisId in QwtPlot.AXES: - map_ = QwtScaleMap() - map_.setTransformation(plot.axisScaleEngine(axisId).transformation()) - sd = plot.axisScaleDiv(axisId) - map_.setScaleInterval(sd.lowerBound(), sd.upperBound()) - if plot.axisEnabled(axisId): - s = plot.axisWidget(axisId) - scaleRect = plot.plotLayout().scaleRect(axisId) - if axisId in (QwtPlot.xTop, QwtPlot.xBottom): - from_ = scaleRect.left() + s.startBorderDist() - to = scaleRect.right() - s.endBorderDist() - else: - from_ = scaleRect.bottom() - s.endBorderDist() - to = scaleRect.top() + s.startBorderDist() - else: - margin = 0 - if not plot.plotLayout().alignCanvasToScale(axisId): - margin = plot.plotLayout().canvasMargin(axisId) - if axisId in (QwtPlot.yLeft, QwtPlot.yRight): - from_ = canvasRect.bottom() - margin - to = canvasRect.top() + margin - else: - from_ = canvasRect.left() + margin - to = canvasRect.right() - margin - map_.setPaintInterval(from_, to) - maps.append(map_) - return maps - - def updateCanvasMargins(self, plot, canvasRect, maps): - margins = plot.getCanvasMarginsHint(maps, canvasRect) - marginsChanged = False - for axisId in QwtPlot.AXES: - if margins[axisId] >= 0.0: - m = math.ceil(margins[axisId]) - plot.plotLayout().setCanvasMargin(m, axisId) - marginsChanged = True - return marginsChanged - - def exportTo(self, plot, documentname, sizeMM=None, resolution=85): - """ - Execute a file dialog and render the plot to the selected file - - :param qwt.plot.QwtPlot plot: Plot widget - :param str documentName: Default document name - :param QSizeF sizeMM: Size for the document in millimeters - :param int resolution: Resolution in dots per Inch (dpi) - :return: True, when exporting was successful - - .. seealso:: - - :py:meth:`renderDocument()` - """ - if plot is None: - return - if sizeMM is None: - sizeMM = QSizeF(300, 200) - filename = documentname - imageFormats = QImageWriter.supportedImageFormats() - filter_ = [ - "PDF documents (*.pdf)", - "SVG documents (*.svg)", - "Postscript documents (*.ps)", - ] - if imageFormats: - imageFilter = "Images" - imageFilter += " (" - for idx, fmt in enumerate(imageFormats): - if idx > 0: - imageFilter += " " - imageFilter += "*." + str(fmt) - imageFilter += ")" - filter_ += [imageFilter] - filename, _s = getsavefilename( - plot, - "Export File Name", - filename, - ";;".join(filter_), - options=QFileDialog.DontConfirmOverwrite, - ) - if not filename: - return False - self.renderDocument(plot, filename, sizeMM, resolution) - return True - return True diff --git a/qwt/plot_series.py b/qwt/plot_series.py deleted file mode 100644 index e0f21f8..0000000 --- a/qwt/plot_series.py +++ /dev/null @@ -1,384 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -Plotting series item --------------------- - -QwtPlotSeriesItem -~~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtPlotSeriesItem - :members: - -QwtSeriesData -~~~~~~~~~~~~~ - -.. autoclass:: QwtSeriesData - :members: - -QwtPointArrayData -~~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtPointArrayData - :members: - -QwtSeriesStore -~~~~~~~~~~~~~~ - -.. autoclass:: QwtSeriesStore - :members: -""" - -import numpy as np -from qtpy.QtCore import QPointF, QRectF, Qt - -from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData -from qwt.text import QwtText - - -class QwtPlotSeriesItem_PrivateData(QwtPlotItem_PrivateData): - def __init__(self): - QwtPlotItem_PrivateData.__init__(self) - self.orientation = Qt.Horizontal - - -class QwtPlotSeriesItem(QwtPlotItem): - """ - Base class for plot items representing a series of samples - """ - - def __init__(self, title): - if not isinstance(title, QwtText): - title = QwtText(title) - QwtPlotItem.__init__(self, title) - self.__data = QwtPlotSeriesItem_PrivateData() - self.setItemInterest(QwtPlotItem.ScaleInterest, True) - - def setOrientation(self, orientation): - """ - Set the orientation of the item. Default is `Qt.Horizontal`. - - The `orientation()` might be used in specific way by a plot item. - F.e. a QwtPlotCurve uses it to identify how to display the curve - int `QwtPlotCurve.Steps` or `QwtPlotCurve.Sticks` style. - - .. seealso:: - - :py:meth`orientation()` - """ - if self.__data.orientation != orientation: - self.__data.orientation = orientation - self.legendChanged() - self.itemChanged() - - def orientation(self): - """ - :return: Orientation of the plot item - - .. seealso:: - - :py:meth`setOrientation()` - """ - return self.__data.orientation - - def draw(self, painter, xMap, yMap, canvasRect): - """ - Draw the complete series - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - """ - self.drawSeries(painter, xMap, yMap, canvasRect, 0, -1) - - def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to): - """ - Draw a subset of the samples - - :param QPainter painter: Painter - :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. - :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. - :param QRectF canvasRect: Contents rectangle of the canvas - :param int from_: Index of the first point to be painted - :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. - - .. seealso:: - - This method is implemented in `qwt.plot_curve.QwtPlotCurve` - """ - raise NotImplementedError - - def boundingRect(self): - return self.dataRect() # dataRect method is implemented in QwtSeriesStore - - def updateScaleDiv(self, xScaleDiv, yScaleDiv): - rect = QRectF( - xScaleDiv.lowerBound(), - yScaleDiv.lowerBound(), - xScaleDiv.range(), - yScaleDiv.range(), - ) - self.setRectOfInterest( - rect - ) # setRectOfInterest method is implemented in QwtSeriesData - - def dataChanged(self): - self.itemChanged() - - -class QwtSeriesData(object): - """ - Abstract interface for iterating over samples - - `PythonQwt` offers several implementations of the QwtSeriesData API, - but in situations, where data of an application specific format - needs to be displayed, without having to copy it, it is recommended - to implement an individual data access. - - A subclass of `QwtSeriesData` must implement: - - - size(): - - Should return number of data points. - - - sample() - - Should return values x and y values of the sample at specific position - as QPointF object. - - - boundingRect() - - Should return the bounding rectangle of the data series. - It is used for autoscaling and might help certain algorithms for - displaying the data. - The member `_boundingRect` is intended for caching the calculated - rectangle. - """ - - def __init__(self): - self._boundingRect = QRectF(0.0, 0.0, -1.0, -1.0) - - def setRectOfInterest(self, rect): - """ - Set a the "rect of interest" - - QwtPlotSeriesItem defines the current area of the plot canvas - as "rectangle of interest" ( QwtPlotSeriesItem::updateScaleDiv() ). - It can be used to implement different levels of details. - - The default implementation does nothing. - - :param QRectF rect: Rectangle of interest - """ - pass - - def size(self): - """ - :return: Number of samples - """ - pass - - def sample(self, i): - """ - Return a sample - - :param int i: Index - :return: Sample at position i - """ - pass - - def boundingRect(self): - """ - Calculate the bounding rect of all samples - - The bounding rect is necessary for autoscaling and can be used - for a couple of painting optimizations. - - :return: Bounding rectangle - """ - pass - - -class QwtPointArrayData(QwtSeriesData): - """ - Interface for iterating over two array objects - - .. py:class:: QwtCQwtPointArrayDataolorMap(x, y, [size=None]) - - :param x: Array of x values - :type x: list or tuple or numpy.array - :param y: Array of y values - :type y: list or tuple or numpy.array - :param int size: Size of the x and y arrays - :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements - """ - - def __init__(self, x=None, y=None, size=None, finite=None): - QwtSeriesData.__init__(self) - if x is None and y is not None: - x = np.arange(len(y)) - elif y is None and x is not None: - y = x - x = np.arange(len(y)) - elif x is None and y is None: - x = np.array([]) - y = np.array([]) - if isinstance(x, (tuple, list)): - x = np.array(x) - if isinstance(y, (tuple, list)): - y = np.array(y) - if size is not None: - x = np.resize(x, (size,)) - y = np.resize(y, (size,)) - if len(x) != len(y): - minlen = min(len(x), len(y)) - x = np.resize(x, (minlen,)) - y = np.resize(y, (minlen,)) - if finite if finite is not None else True: - indexes = np.logical_and(np.isfinite(x), np.isfinite(y)) - self.__x = x[indexes] - self.__y = y[indexes] - else: - self.__x = x - self.__y = y - - def boundingRect(self): - """ - Calculate the bounding rectangle - - The bounding rectangle is calculated once by iterating over all - points and is stored for all following requests. - - :return: Bounding rectangle - """ - xmin = self.__x.min() - xmax = self.__x.max() - ymin = self.__y.min() - ymax = self.__y.max() - return QRectF(xmin, ymin, xmax - xmin, ymax - ymin) - - def size(self): - """ - :return: Size of the data set - """ - return min([self.__x.size, self.__y.size]) - - def sample(self, index): - """ - :param int index: Index - :return: Sample at position `index` - """ - return QPointF(self.__x[index], self.__y[index]) - - def xData(self): - """ - :return: Array of the x-values - """ - return self.__x - - def yData(self): - """ - :return: Array of the y-values - """ - return self.__y - - -class QwtSeriesStore(object): - """ - Class storing a `QwtSeriesData` object - - `QwtSeriesStore` and `QwtPlotSeriesItem` are intended as base classes for - all plot items iterating over a series of samples. - """ - - def __init__(self): - self.__series = None - - def setData(self, series): - """ - Assign a series of samples - - :param qwt.plot_series.QwtSeriesData series: Data - - .. warning:: - - The item takes ownership of the data object, deleting it - when its not used anymore. - """ - if self.__series != series: - self.__series = series - self.dataChanged() - - def dataChanged(self): - raise NotImplementedError - - def data(self): - """ - :return: the series data - """ - return self.__series - - def sample(self, index): - """ - :param int index: Index - :return: Sample at position index - """ - if self.__series: - return self.__series.sample(index) - else: - return - - def dataSize(self): - """ - :return: Number of samples of the series - - .. seealso:: - - :py:meth:`setData()`, - :py:meth:`qwt.plot_series.QwtSeriesData.size()` - """ - if self.__series is None: - return 0 - return self.__series.size() - - def dataRect(self): - """ - :return: Bounding rectangle of the series or an invalid rectangle, when no series is stored - - .. seealso:: - - :py:meth:`qwt.plot_series.QwtSeriesData.boundingRect()` - """ - if self.__series is None or self.dataSize() == 0: - return QRectF(1.0, 1.0, -2.0, -2.0) - return self.__series.boundingRect() - - def setRectOfInterest(self, rect): - """ - Set a the "rect of interest" for the series - - :param QRectF rect: Rectangle of interest - - .. seealso:: - - :py:meth:`qwt.plot_series.QwtSeriesData.setRectOfInterest()` - """ - if self.__series: - self.__series.setRectOfInterest(rect) - - def swapData(self, series): - """ - Replace a series without deleting the previous one - - :param qwt.plot_series.QwtSeriesData series: New series - :return: Previously assigned series - """ - swappedSeries = self.__series - self.__series = series - return swappedSeries diff --git a/qwt/qthelpers.py b/qwt/qthelpers.py deleted file mode 100644 index 283ccf1..0000000 --- a/qwt/qthelpers.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -"""Qt helpers""" - -import os - -from qtpy import QtCore as QC -from qtpy import QtGui as QG -from qtpy import QtWidgets as QW - -QT_API = os.environ["QT_API"] - - -def qcolor_from_str(color, default): - """Return QColor object from str - - :param color: Input color - :type color: QColor or str or None - :param QColor default: Default color (returned if color is None) - - If color is already a QColor instance, simply return color. - If color is None, return default color. - If color is neither an str nor a QColor instance nor None, raise TypeError. - """ - if color is None: - return default - elif isinstance(color, str): - try: - return getattr(QC.Qt, color) - except AttributeError: - raise ValueError("Unknown Qt color %r" % color) - else: - try: - return QG.QColor(color) - except TypeError: - raise TypeError("Invalid color %r" % color) - - -def take_screenshot(widget, path, size=None, quit=True): - """Take screenshot of widget""" - if size is not None: - widget.resize(*size) - widget.show() - QW.QApplication.processEvents() - pixmap = widget.grab() - pixmap.save(path) - if quit: - QC.QTimer.singleShot(0, QW.QApplication.instance().quit) diff --git a/qwt/scale_div.py b/qwt/scale_div.py deleted file mode 100644 index 9ff2e5e..0000000 --- a/qwt/scale_div.py +++ /dev/null @@ -1,316 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtScaleDiv ------------ - -.. autoclass:: QwtScaleDiv - :members: -""" - -import copy - -from qwt.interval import QwtInterval - - -class QwtScaleDiv(object): - """ - A class representing a scale division - - A Qwt scale is defined by its boundaries and 3 list - for the positions of the major, medium and minor ticks. - - The `upperLimit()` might be smaller than the `lowerLimit()` - to indicate inverted scales. - - Scale divisions can be calculated from a `QwtScaleEngine`. - - .. seealso:: - - :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()`, - :py:meth:`qwt.plot.QwtPlot.setAxisScaleDiv()` - - Scale tick types: - - * `QwtScaleDiv.NoTick`: No ticks - * `QwtScaleDiv.MinorTick`: Minor ticks - * `QwtScaleDiv.MediumTick`: Medium ticks - * `QwtScaleDiv.MajorTick`: Major ticks - * `QwtScaleDiv.NTickTypes`: Number of valid tick types - - .. py:class:: QwtScaleDiv() - - Basic constructor. Lower bound = Upper bound = 0. - - .. py:class:: QwtScaleDiv(interval, ticks) - :noindex: - - :param qwt.interval.QwtInterval interval: Interval - :param list ticks: list of major, medium and minor ticks - - .. py:class:: QwtScaleDiv(lowerBound, upperBound) - :noindex: - - :param float lowerBound: First boundary - :param float upperBound: Second boundary - - .. py:class:: QwtScaleDiv(lowerBound, upperBound, ticks) - :noindex: - - :param float lowerBound: First boundary - :param float upperBound: Second boundary - :param list ticks: list of major, medium and minor ticks - - .. py:class:: QwtScaleDiv(lowerBound, upperBound, minorTicks, mediumTicks, majorTicks) - :noindex: - - :param float lowerBound: First boundary - :param float upperBound: Second boundary - :param list minorTicks: list of minor ticks - :param list mediumTicks: list of medium ticks - :param list majorTicks: list of major ticks - - .. note:: - - lowerBound might be greater than upperBound for inverted scales - """ - - # enum TickType - NoTick = -1 - MinorTick, MediumTick, MajorTick, NTickTypes = list(range(4)) - - def __init__(self, *args): - self.__ticks = None - if len(args) == 2 and isinstance(args[1], (tuple, list)): - interval, ticks = args - self.__lowerBound = interval.minValue() - self.__upperBound = interval.maxValue() - self.__ticks = ticks[:] - elif len(args) == 2: - self.__lowerBound, self.__upperBound = args - elif len(args) == 3: - self.__lowerBound, self.__upperBound, ticks = args - self.__ticks = ticks[:] - elif len(args) == 5: - ( - self.__lowerBound, - self.__upperBound, - minorTicks, - mediumTicks, - majorTicks, - ) = args - self.__ticks = [0] * self.NTickTypes - self.__ticks[self.MinorTick] = minorTicks - self.__ticks[self.MediumTick] = mediumTicks - self.__ticks[self.MajorTick] = majorTicks - elif len(args) == 0: - self.__lowerBound, self.__upperBound = 0.0, 0.0 - else: - raise TypeError( - "%s() takes 0, 2, 3 or 5 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def setInterval(self, *args): - """ - Change the interval - - .. py:method:: setInterval(lowerBound, upperBound) - :noindex: - - :param float lowerBound: First boundary - :param float upperBound: Second boundary - - .. py:method:: setInterval(interval) - :noindex: - - :param qwt.interval.QwtInterval interval: Interval - - .. note:: - - lowerBound might be greater than upperBound for inverted scales - """ - if len(args) == 2: - self.__lowerBound, self.__upperBound = args - elif len(args) == 1: - (interval,) = args - self.__lowerBound = interval.minValue() - self.__upperBound = interval.maxValue() - else: - raise TypeError( - "%s().setInterval() takes 1 or 2 argument(s) (%s " - "given)" % (self.__class__.__name__, len(args)) - ) - - def interval(self): - """ - :return: Interval - """ - return QwtInterval(self.__lowerBound, self.__upperBound) - - def setLowerBound(self, lowerBound): - """ - Set the first boundary - - :param float lowerBound: First boundary - - .. seealso:: - - :py:meth:`lowerBound()`, :py:meth:`setUpperBound()` - """ - self.__lowerBound = lowerBound - - def lowerBound(self): - """ - :return: the first boundary - - .. seealso:: - - :py:meth:`upperBound()` - """ - return self.__lowerBound - - def setUpperBound(self, upperBound): - """ - Set the second boundary - - :param float lowerBound: Second boundary - - .. seealso:: - - :py:meth:`upperBound()`, :py:meth:`setLowerBound()` - """ - self.__upperBound = upperBound - - def upperBound(self): - """ - :return: the second boundary - - .. seealso:: - - :py:meth:`lowerBound()` - """ - return self.__upperBound - - def range(self): - """ - :return: upperBound() - lowerBound() - """ - return self.__upperBound - self.__lowerBound - - def __eq__(self, other): - if self.__ticks is None: - return False - if ( - self.__lowerBound != other.__lowerBound - or self.__upperBound != other.__upperBound - ): - return False - return self.__ticks == other.__ticks - - def __ne__(self, other): - return not self.__eq__(other) - - def isEmpty(self): - """ - Check if the scale division is empty( lowerBound() == upperBound() ) - """ - return self.__lowerBound == self.__upperBound - - def isIncreasing(self): - """ - Check if the scale division is increasing( lowerBound() <= upperBound() ) - """ - return self.__lowerBound <= self.__upperBound - - def contains(self, value): - """ - Return if a value is between lowerBound() and upperBound() - - :param float value: Value - :return: True/False - """ - lb = self.__lowerBound - ub = self.__upperBound - if lb <= ub: - return lb <= value <= ub - return ub <= value <= lb - - def invert(self): - """ - Invert the scale division - - .. seealso:: - - :py:meth:`inverted()` - """ - (self.__lowerBound, self.__upperBound) = self.__upperBound, self.__lowerBound - for index in range(self.NTickTypes): - self.__ticks[index].reverse() - - def inverted(self): - """ - :return: A scale division with inverted boundaries and ticks - - .. seealso:: - - :py:meth:`invert()` - """ - other = copy.deepcopy(self) - other.invert() - return other - - def bounded(self, lowerBound, upperBound): - """ - Return a scale division with an interval [lowerBound, upperBound] - where all ticks outside this interval are removed - - :param float lowerBound: First boundary - :param float lowerBound: Second boundary - :return: Scale division with all ticks inside of the given interval - - .. note:: - - lowerBound might be greater than upperBound for inverted scales - """ - min_ = min([self.__lowerBound, self.__upperBound]) - max_ = max([self.__lowerBound, self.__upperBound]) - sd = QwtScaleDiv() - sd.setInterval(lowerBound, upperBound) - for tickType in range(self.NTickTypes): - sd.setTicks( - tickType, - [ - tick - for tick in self.__ticks[tickType] - if tick >= min_ and tick <= max_ - ], - ) - return sd - - def setTicks(self, tickType, ticks): - """ - Assign ticks - - :param int type: MinorTick, MediumTick or MajorTick - :param list ticks: Values of the tick positions - """ - if tickType in range(self.NTickTypes): - self.__ticks[tickType] = ticks - - def ticks(self, tickType): - """ - Return a list of ticks - - :param int type: MinorTick, MediumTick or MajorTick - :return: Tick list - """ - if self.__ticks is not None and tickType in range(self.NTickTypes): - return self.__ticks[tickType] - else: - return [] diff --git a/qwt/scale_draw.py b/qwt/scale_draw.py deleted file mode 100644 index dd049ac..0000000 --- a/qwt/scale_draw.py +++ /dev/null @@ -1,1342 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtAbstractScaleDraw --------------------- - -.. autoclass:: QwtAbstractScaleDraw - :members: - -QwtScaleDraw ------------- - -.. autoclass:: QwtScaleDraw - :members: -""" - -import math -from datetime import datetime - -from qtpy.QtCore import ( - QLineF, - QPoint, - QPointF, - QRect, - QRectF, - Qt, - qFuzzyCompare, -) -from qtpy.QtGui import QFontMetrics, QPalette, QTransform - -from qwt._math import qwtRadians -from qwt.scale_div import QwtScaleDiv -from qwt.scale_map import QwtScaleMap -from qwt.text import QwtText - -# Plain-int aliases for Qt alignment flags. Qt6 exposes alignment flags as -# IntEnum members and bitwise operations on them go through Python's -# enum machinery (`__and__`/`__call__`), which is one of the dominant costs -# of label layout. Casting to int once and using these constants makes the -# bitwise tests in `labelTransformation` ~10x cheaper without changing -# semantics. -_ALIGN_LEFT = int(Qt.AlignLeft) -_ALIGN_RIGHT = int(Qt.AlignRight) -_ALIGN_TOP = int(Qt.AlignTop) -_ALIGN_BOTTOM = int(Qt.AlignBottom) - - -class QwtAbstractScaleDraw_PrivateData(object): - # See QwtText_PrivateData: ``QObject`` inheritance is unused and the - # base class' ``__init__`` is a measurable cost in tick-heavy renders. - __slots__ = ( - "spacing", - "penWidth", - "minExtent", - "components", - "tick_length", - "tick_lighter_factor", - "map", - "scaleDiv", - "labelCache", - ) - - def __init__(self): - self.spacing = 4 - self.penWidth = 0 - self.minExtent = 0.0 - - self.components = ( - QwtAbstractScaleDraw.Backbone - | QwtAbstractScaleDraw.Ticks - | QwtAbstractScaleDraw.Labels - ) - self.tick_length = { - QwtScaleDiv.MinorTick: 4.0, - QwtScaleDiv.MediumTick: 6.0, - QwtScaleDiv.MajorTick: 8.0, - } - self.tick_lighter_factor = { - QwtScaleDiv.MinorTick: 100, - QwtScaleDiv.MediumTick: 100, - QwtScaleDiv.MajorTick: 100, - } - - self.map = QwtScaleMap() - self.scaleDiv = QwtScaleDiv() - - self.labelCache = {} - - -class QwtAbstractScaleDraw(object): - """ - A abstract base class for drawing scales - - `QwtAbstractScaleDraw` can be used to draw linear or logarithmic scales. - - After a scale division has been specified as a `QwtScaleDiv` object - using `setScaleDiv()`, the scale can be drawn with the `draw()` member. - - Scale components: - - * `QwtAbstractScaleDraw.Backbone`: Backbone = the line where the ticks are located - * `QwtAbstractScaleDraw.Ticks`: Ticks - * `QwtAbstractScaleDraw.Labels`: Labels - - .. py:class:: QwtAbstractScaleDraw() - - The range of the scale is initialized to [0, 100], - The spacing (distance between ticks and labels) is - set to 4, the tick lengths are set to 4,6 and 8 pixels - """ - - # enum ScaleComponent - Backbone = 0x01 - Ticks = 0x02 - Labels = 0x04 - - def __init__(self): - self.__data = QwtAbstractScaleDraw_PrivateData() - - def extent(self, font): - """ - Calculate the extent - - The extent is the distance from the baseline to the outermost - pixel of the scale draw in opposite to its orientation. - It is at least minimumExtent() pixels. - - :param QFont font: Font used for drawing the tick labels - :return: Number of pixels - - .. seealso:: - - :py:meth:`setMinimumExtent()`, :py:meth:`minimumExtent()` - """ - return 0.0 - - def drawTick(self, painter, value, len_): - """ - Draw a tick - - :param QPainter painter: Painter - :param float value: Value of the tick - :param float len: Length of the tick - - .. seealso:: - - :py:meth:`drawBackbone()`, :py:meth:`drawLabel()` - """ - pass - - def drawBackbone(self, painter): - """ - Draws the baseline of the scale - - :param QPainter painter: Painter - - .. seealso:: - - :py:meth:`drawTick()`, :py:meth:`drawLabel()` - """ - pass - - def drawLabel(self, painter, value): - """ - Draws the label for a major scale tick - - :param QPainter painter: Painter - :param float value: Value - - .. seealso:: - - :py:meth:`drawTick()`, :py:meth:`drawBackbone()` - """ - pass - - def enableComponent(self, component, enable): - """ - En/Disable a component of the scale - - :param int component: Scale component - :param bool enable: On/Off - - .. seealso:: - - :py:meth:`hasComponent()` - """ - if enable: - self.__data.components |= component - else: - self.__data.components &= ~component - - def hasComponent(self, component): - """ - Check if a component is enabled - - :param int component: Component type - :return: True, when component is enabled - - .. seealso:: - - :py:meth:`enableComponent()` - """ - return self.__data.components & component - - def setScaleDiv(self, scaleDiv): - """ - Change the scale division - - :param qwt.scale_div.QwtScaleDiv scaleDiv: New scale division - """ - self.__data.scaleDiv = scaleDiv - self.__data.map.setScaleInterval(scaleDiv.lowerBound(), scaleDiv.upperBound()) - self.invalidateCache() - - def setTransformation(self, transformation): - """ - Change the transformation of the scale - - :param qwt.transform.QwtTransform transformation: New scale transformation - """ - self.__data.map.setTransformation(transformation) - - def scaleMap(self): - """ - :return: Map how to translate between scale and pixel values - """ - return self.__data.map - - def scaleDiv(self): - """ - :return: scale division - """ - return self.__data.scaleDiv - - def setPenWidth(self, width): - """ - Specify the width of the scale pen - - :param int width: Pen width - - .. seealso:: - - :py:meth:`penWidth()` - """ - if width < 0: - width = 0 - if width != self.__data.penWidth: - self.__data.penWidth = width - - def penWidth(self): - """ - :return: Scale pen width - - .. seealso:: - - :py:meth:`setPenWidth()` - """ - return self.__data.penWidth - - def draw(self, painter, palette): - """ - Draw the scale - - :param QPainter painter: The painter - :param QPalette palette: Palette, text color is used for the labels, - foreground color for ticks and backbone - """ - painter.save() - - pen = painter.pen() - pen.setWidth(self.__data.penWidth) - pen.setCosmetic(False) - painter.setPen(pen) - - if self.hasComponent(QwtAbstractScaleDraw.Labels): - painter.save() - painter.setPen(palette.color(QPalette.Text)) - majorTicks = self.__data.scaleDiv.ticks(QwtScaleDiv.MajorTick) - for v in majorTicks: - if self.__data.scaleDiv.contains(v): - self.drawLabel(painter, v) - painter.restore() - - if self.hasComponent(QwtAbstractScaleDraw.Ticks): - painter.save() - pen = painter.pen() - pen.setCapStyle(Qt.FlatCap) - default_color = palette.color(QPalette.WindowText) - for tickType in range(QwtScaleDiv.NTickTypes): - tickLen = self.__data.tick_length[tickType] - if tickLen <= 0.0: - continue - factor = self.__data.tick_lighter_factor[tickType] - pen.setColor(default_color.lighter(factor)) - painter.setPen(pen) - ticks = self.__data.scaleDiv.ticks(tickType) - for v in ticks: - if self.__data.scaleDiv.contains(v): - self.drawTick(painter, v, tickLen) - painter.restore() - - if self.hasComponent(QwtAbstractScaleDraw.Backbone): - painter.save() - pen = painter.pen() - pen.setColor(palette.color(QPalette.WindowText)) - pen.setCapStyle(Qt.FlatCap) - painter.setPen(pen) - self.drawBackbone(painter) - painter.restore() - - painter.restore() - - def setSpacing(self, spacing): - """ - Set the spacing between tick and labels - - The spacing is the distance between ticks and labels. - The default spacing is 4 pixels. - - :param float spacing: Spacing - - .. seealso:: - - :py:meth:`spacing()` - """ - if spacing < 0: - spacing = 0 - self.__data.spacing = spacing - - def spacing(self): - """ - Get the spacing - - The spacing is the distance between ticks and labels. - The default spacing is 4 pixels. - - :return: Spacing - - .. seealso:: - - :py:meth:`setSpacing()` - """ - return self.__data.spacing - - def setMinimumExtent(self, minExtent): - """ - Set a minimum for the extent - - The extent is calculated from the components of the - scale draw. In situations, where the labels are - changing and the layout depends on the extent (f.e scrolling - a scale), setting an upper limit as minimum extent will - avoid jumps of the layout. - - :param float minExtent: Minimum extent - - .. seealso:: - - :py:meth:`extent()`, :py:meth:`minimumExtent()` - """ - if minExtent < 0.0: - minExtent = 0.0 - self.__data.minExtent = minExtent - - def minimumExtent(self): - """ - Get the minimum extent - - :return: Minimum extent - - .. seealso:: - - :py:meth:`extent()`, :py:meth:`setMinimumExtent()` - """ - return self.__data.minExtent - - def setTickLength(self, tick_type, length): - """ - Set the length of the ticks - - :param int tick_type: Tick type - :param float length: New length - - .. warning:: - - the length is limited to [0..1000] - """ - if tick_type not in self.__data.tick_length: - raise ValueError("Invalid tick type: %r" % tick_type) - self.__data.tick_length[tick_type] = min([1000.0, max([0.0, length])]) - - def tickLength(self, tick_type): - """ - :param int tick_type: Tick type - :return: Length of the ticks - - .. seealso:: - - :py:meth:`setTickLength()`, :py:meth:`maxTickLength()` - """ - if tick_type not in self.__data.tick_length: - raise ValueError("Invalid tick type: %r" % tick_type) - return self.__data.tick_length[tick_type] - - def maxTickLength(self): - """ - :return: Length of the longest tick - - Useful for layout calculations - - .. seealso:: - - :py:meth:`tickLength()`, :py:meth:`setTickLength()` - """ - return max([0.0] + list(self.__data.tick_length.values())) - - def setTickLighterFactor(self, tick_type, factor): - """ - Set the color lighter factor of the ticks - - :param int tick_type: Tick type - :param int factor: New factor - """ - if tick_type not in self.__data.tick_length: - raise ValueError("Invalid tick type: %r" % tick_type) - self.__data.tick_lighter_factor[tick_type] = min([0, factor]) - - def tickLighterFactor(self, tick_type): - """ - :param int tick_type: Tick type - :return: Color lighter factor of the ticks - - .. seealso:: - - :py:meth:`setTickLighterFactor()` - """ - if tick_type not in self.__data.tick_length: - raise ValueError("Invalid tick type: %r" % tick_type) - return self.__data.tick_lighter_factor[tick_type] - - def label(self, value): - """ - Convert a value into its representing label - - The value is converted to a plain text using - `QLocale().toString(value)`. - This method is often overloaded by applications to have individual - labels. - - :param float value: Value - :return: Label string - """ - # Adding a space before the value is a way to add a margin on the left - # of the scale. This helps to avoid truncating the first digit of the - # tick labels while keeping a tight layout. - return " %g" % value - - def tickLabel(self, font, value): - """ - Convert a value into its representing label and cache it. - - The conversion between value and label is called very often - in the layout and painting code. Unfortunately the - calculation of the label sizes might be slow (really slow - for rich text in Qt4), so it's necessary to cache the labels. - - :param QFont font: Font - :param float value: Value - :return: Tuple (tick label, text size) - """ - lbl = self.__data.labelCache.get(value) - if lbl is None: - lbl = QwtText(self.label(value)) - lbl.setRenderFlags(0) - lbl.setLayoutAttribute(QwtText.MinimumLayout) - self.__data.labelCache[value] = lbl - return lbl, lbl.textSize(font) - - def invalidateCache(self): - """ - Invalidate the cache used by `tickLabel()` - - The cache is invalidated, when a new `QwtScaleDiv` is set. If - the labels need to be changed. while the same `QwtScaleDiv` is set, - `invalidateCache()` needs to be called manually. - """ - self.__data.labelCache.clear() - - -class QwtScaleDraw_PrivateData(object): - # See QwtText_PrivateData: ``QObject`` inheritance is unused and the - # base class' ``__init__`` is a measurable cost in tick-heavy renders. - __slots__ = ( - "len", - "alignment", - "orientation", - "labelAlignment", - "labelRotation", - "labelAutoSize", - "pos", - ) - - def __init__(self): - self.len = 0 - self.alignment = QwtScaleDraw.BottomScale - # Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment`` - # so that the very hot ``orientation()`` accessor avoids any test. - self.orientation = Qt.Horizontal - self.labelAlignment = 0 - self.labelRotation = 0.0 - self.labelAutoSize = True - self.pos = QPointF() - - -class QwtScaleDraw(QwtAbstractScaleDraw): - """ - A class for drawing scales - - QwtScaleDraw can be used to draw linear or logarithmic scales. - A scale has a position, an alignment and a length, which can be specified . - The labels can be rotated and aligned - to the ticks using `setLabelRotation()` and `setLabelAlignment()`. - - After a scale division has been specified as a QwtScaleDiv object - using `QwtAbstractScaleDraw.setScaleDiv(scaleDiv)`, - the scale can be drawn with the `QwtAbstractScaleDraw.draw()` member. - - Alignment of the scale draw: - - * `QwtScaleDraw.BottomScale`: The scale is below - * `QwtScaleDraw.TopScale`: The scale is above - * `QwtScaleDraw.LeftScale`: The scale is left - * `QwtScaleDraw.RightScale`: The scale is right - - .. py:class:: QwtScaleDraw() - - The range of the scale is initialized to [0, 100], - The position is at (0, 0) with a length of 100. - The orientation is `QwtAbstractScaleDraw.Bottom`. - """ - - # enum Alignment - BottomScale, TopScale, LeftScale, RightScale = list(range(4)) - Flags = ( - Qt.AlignHCenter | Qt.AlignBottom, # BottomScale - Qt.AlignHCenter | Qt.AlignTop, # TopScale - Qt.AlignLeft | Qt.AlignVCenter, # LeftScale - Qt.AlignRight | Qt.AlignVCenter, # RightScale - ) - - def __init__(self): - QwtAbstractScaleDraw.__init__(self) - self.__data = QwtScaleDraw_PrivateData() - self.setLength(100) - self._max_label_sizes = {} - - def alignment(self): - """ - :return: Alignment of the scale - - .. seealso:: - - :py:meth:`setAlignment()` - """ - return self.__data.alignment - - def setAlignment(self, align): - """ - Set the alignment of the scale - - :param int align: Alignment of the scale - - Alignment of the scale draw: - - * `QwtScaleDraw.BottomScale`: The scale is below - * `QwtScaleDraw.TopScale`: The scale is above - * `QwtScaleDraw.LeftScale`: The scale is left - * `QwtScaleDraw.RightScale`: The scale is right - - The default alignment is `QwtScaleDraw.BottomScale` - - .. seealso:: - - :py:meth:`alignment()` - """ - self.__data.alignment = align - # Keep cached orientation in sync (see ``orientation()``). - if align == self.BottomScale or align == self.TopScale: - self.__data.orientation = Qt.Horizontal - else: - self.__data.orientation = Qt.Vertical - - def orientation(self): - """ - Return the orientation - - TopScale, BottomScale are horizontal (`Qt.Horizontal`) scales, - LeftScale, RightScale are vertical (`Qt.Vertical`) scales. - - :return: Orientation of the scale - - .. seealso:: - - :py:meth:`alignment()` - """ - # Pre-computed by ``setAlignment`` - this method is called per tick. - return self.__data.orientation - - def getBorderDistHint(self, font): - """ - Determine the minimum border distance - - This member function returns the minimum space - needed to draw the mark labels at the scale's endpoints. - - :param QFont font: Font - :return: tuple `(start, end)` - - Returned tuple: - - * start: Start border distance - * end: End border distance - """ - start, end = 0, 1.0 - - if not self.hasComponent(QwtAbstractScaleDraw.Labels): - return start, end - - ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) - if len(ticks) == 0: - return start, end - - scale_map = self.scaleMap() - transform = scale_map.transform - minTick = ticks[0] - minPos = transform(minTick) - maxTick = minTick - maxPos = minPos - - for tick in ticks: - tickPos = transform(tick) - if tickPos < minPos: - minTick = tick - minPos = tickPos - if tickPos > maxPos: - maxTick = tick - maxPos = tickPos - - s = 0.0 - e = 0.0 - if self.orientation() == Qt.Vertical: - s = -self.labelRect(font, minTick).top() - s -= abs(minPos - round(scale_map.p2())) - - e = self.labelRect(font, maxTick).bottom() - e -= abs(maxPos - scale_map.p1()) - else: - s = -self.labelRect(font, minTick).left() - s -= abs(minPos - scale_map.p1()) - - e = self.labelRect(font, maxTick).right() - e -= abs(maxPos - scale_map.p2()) - - return max(math.ceil(s), 0), max(math.ceil(e), 0) - - def minLabelDist(self, font): - """ - Determine the minimum distance between two labels, that is necessary - that the texts don't overlap. - - :param QFont font: Font - :return: The maximum width of a label - - .. seealso:: - - :py:meth:`getBorderDistHint()` - """ - if not self.hasComponent(QwtAbstractScaleDraw.Labels): - return 0 - - ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) - if not ticks: - return 0 - - fm = QFontMetrics(font) - vertical = self.orientation() == Qt.Vertical - - bRect1 = QRectF() - bRect2 = self.labelRect(font, ticks[0]) - if vertical: - bRect2.setRect(-bRect2.bottom(), 0.0, bRect2.height(), bRect2.width()) - - maxDist = 0.0 - - for tick in ticks: - bRect1 = bRect2 - bRect2 = self.labelRect(font, tick) - if vertical: - bRect2.setRect(-bRect2.bottom(), 0.0, bRect2.height(), bRect2.width()) - - dist = fm.leading() - if bRect1.right() > 0: - dist += bRect1.right() - if bRect2.left() < 0: - dist += -bRect2.left() - - if dist > maxDist: - maxDist = dist - - angle = qwtRadians(self.labelRotation()) - if vertical: - angle += math.pi / 2 - - sinA = math.sin(angle) - if qFuzzyCompare(sinA + 1.0, 1.0): - return math.ceil(maxDist) - - fmHeight = fm.ascent() - 2 - - labelDist = fmHeight / math.sin(angle) * math.cos(angle) - if labelDist < 0: - labelDist = -labelDist - - if labelDist > maxDist: - labelDist = maxDist - - if labelDist < fmHeight: - labelDist = fmHeight - - return math.ceil(labelDist) - - def extent(self, font): - """ - Calculate the width/height that is needed for a - vertical/horizontal scale. - - The extent is calculated from the pen width of the backbone, - the major tick length, the spacing and the maximum width/height - of the labels. - - :param QFont font: Font used for painting the labels - :return: Extent - - .. seealso:: - - :py:meth:`minLength()` - """ - d = 0.0 - if self.hasComponent(QwtAbstractScaleDraw.Labels): - if self.orientation() == Qt.Vertical: - d = self.maxLabelWidth(font) - else: - d = self.maxLabelHeight(font) - if d > 0: - d += self.spacing() - if self.hasComponent(QwtAbstractScaleDraw.Ticks): - d += self.maxTickLength() - if self.hasComponent(QwtAbstractScaleDraw.Backbone): - pw = max([1, self.penWidth()]) - d += pw - return max([d, self.minimumExtent()]) - - def minLength(self, font): - """ - Calculate the minimum length that is needed to draw the scale - - :param QFont font: Font used for painting the labels - :return: Minimum length that is needed to draw the scale - - .. seealso:: - - :py:meth:`extent()` - """ - startDist, endDist = self.getBorderDistHint(font) - sd = self.scaleDiv() - minorCount = len(sd.ticks(QwtScaleDiv.MinorTick)) + len( - sd.ticks(QwtScaleDiv.MediumTick) - ) - majorCount = len(sd.ticks(QwtScaleDiv.MajorTick)) - lengthForLabels = 0 - if self.hasComponent(QwtAbstractScaleDraw.Labels): - lengthForLabels = self.minLabelDist(font) * majorCount - lengthForTicks = 0 - if self.hasComponent(QwtAbstractScaleDraw.Ticks): - pw = max([1, self.penWidth()]) - lengthForTicks = math.ceil((majorCount + minorCount) * (pw + 1.0)) - return startDist + endDist + max([lengthForLabels, lengthForTicks]) - - def labelPosition(self, value): - """ - Find the position, where to paint a label - - The position has a distance that depends on the length of the ticks - in direction of the `alignment()`. - - :param float value: Value - :return: Position, where to paint a label - """ - tval = self.scaleMap().transform(value) - dist = self.spacing() - hasComponent = self.hasComponent - if hasComponent(QwtAbstractScaleDraw.Backbone): - dist += max(1, self.penWidth()) - if hasComponent(QwtAbstractScaleDraw.Ticks): - dist += self.tickLength(QwtScaleDiv.MajorTick) - - alignment = self.alignment() - pos = self.__data.pos - if alignment == self.RightScale: - return QPointF(pos.x() + dist, tval) - if alignment == self.LeftScale: - return QPointF(pos.x() - dist, tval) - if alignment == self.BottomScale: - return QPointF(tval, pos.y() + dist) - # TopScale - return QPointF(tval, pos.y() - dist) - - def drawTick(self, painter, value, len_): - """ - Draw a tick - - :param QPainter painter: Painter - :param float value: Value of the tick - :param float len: Length of the tick - - .. seealso:: - - :py:meth:`drawBackbone()`, :py:meth:`drawLabel()` - """ - if len_ <= 0: - return - pos = self.__data.pos - tval = self.scaleMap().transform(value) - pw = self.penWidth() - a = 0 - if self.alignment() == self.LeftScale: - x1 = pos.x() + a - x2 = pos.x() + a - pw - len_ - painter.drawLine(QLineF(x1, tval, x2, tval)) - elif self.alignment() == self.RightScale: - x1 = pos.x() - x2 = pos.x() + pw + len_ - painter.drawLine(QLineF(x1, tval, x2, tval)) - elif self.alignment() == self.BottomScale: - y1 = pos.y() - y2 = pos.y() + pw + len_ - painter.drawLine(QLineF(tval, y1, tval, y2)) - elif self.alignment() == self.TopScale: - y1 = pos.y() + a - y2 = pos.y() - pw - len_ + a - painter.drawLine(QLineF(tval, y1, tval, y2)) - - def drawBackbone(self, painter): - """ - Draws the baseline of the scale - - :param QPainter painter: Painter - - .. seealso:: - - :py:meth:`drawTick()`, :py:meth:`drawLabel()` - """ - pos = self.__data.pos - len_ = self.__data.len - off = 0.5 * self.penWidth() - if self.alignment() == self.LeftScale: - x = pos.x() - off - painter.drawLine(QLineF(x, pos.y(), x, pos.y() + len_)) - elif self.alignment() == self.RightScale: - x = pos.x() + off - painter.drawLine(QLineF(x, pos.y(), x, pos.y() + len_)) - elif self.alignment() == self.TopScale: - y = pos.y() - off - painter.drawLine(QLineF(pos.x(), y, pos.x() + len_, y)) - elif self.alignment() == self.BottomScale: - y = pos.y() + off - painter.drawLine(QLineF(pos.x(), y, pos.x() + len_, y)) - - def move(self, *args): - """ - Move the position of the scale - - The meaning of the parameter pos depends on the alignment: - - * `QwtScaleDraw.LeftScale`: - - The origin is the topmost point of the backbone. The backbone is a - vertical line. Scale marks and labels are drawn at the left of the - backbone. - - * `QwtScaleDraw.RightScale`: - - The origin is the topmost point of the backbone. The backbone is a - vertical line. Scale marks and labels are drawn at the right of - the backbone. - - * `QwtScaleDraw.TopScale`: - - The origin is the leftmost point of the backbone. The backbone is - a horizontal line. Scale marks and labels are drawn above the - backbone. - - * `QwtScaleDraw.BottomScale`: - - The origin is the leftmost point of the backbone. The backbone is - a horizontal line Scale marks and labels are drawn below the - backbone. - - .. py:method:: move(x, y) - :noindex: - - :param float x: X coordinate - :param float y: Y coordinate - - .. py:method:: move(pos) - :noindex: - - :param QPointF pos: position - - .. seealso:: - - :py:meth:`pos()`, :py:meth:`setLength()` - """ - if len(args) == 2: - x, y = args - self.move(QPointF(x, y)) - elif len(args) == 1: - (pos,) = args - self.__data.pos = pos - self.updateMap() - else: - raise TypeError( - "%s().move() takes 1 or 2 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def pos(self): - """ - :return: Origin of the scale - - .. seealso:: - - :py:meth:`pos()`, :py:meth:`setLength()` - """ - return self.__data.pos - - def setLength(self, length): - """ - Set the length of the backbone. - - The length doesn't include the space needed for overlapping labels. - - :param float length: Length of the backbone - - .. seealso:: - - :py:meth:`move()`, :py:meth:`minLabelDist()` - """ - if length >= 0 and length < 10: - length = 10 - if length < 0 and length > -10: - length = -10 - self.__data.len = length - self.updateMap() - - def length(self): - """ - :return: the length of the backbone - - .. seealso:: - - :py:meth:`setLength()`, :py:meth:`pos()` - """ - return self.__data.len - - def drawLabel(self, painter, value): - """ - Draws the label for a major scale tick - - :param QPainter painter: Painter - :param float value: Value - - .. seealso:: - - :py:meth:`drawTick()`, :py:meth:`drawBackbone()`, - :py:meth:`boundingLabelRect()` - """ - lbl, labelSize = self.tickLabel(painter.font(), value) - if lbl is None or lbl.isEmpty(): - return - pos = self.labelPosition(value) - transform = self.labelTransformation(pos, labelSize) - painter.save() - painter.setWorldTransform(transform, True) - lbl.draw(painter, QRect(QPoint(0, 0), labelSize.toSize())) - painter.restore() - - def boundingLabelRect(self, font, value): - """ - Find the bounding rectangle for the label. - - The coordinates of the rectangle are absolute (calculated from - `pos()`) in direction of the tick. - - :param QFont font: Font used for painting - :param float value: Value - :return: Bounding rectangle - - .. seealso:: - - :py:meth:`labelRect()` - """ - lbl, labelSize = self.tickLabel(font, value) - if lbl.isEmpty(): - return QRect() - pos = self.labelPosition(value) - transform = self.labelTransformation(pos, labelSize) - return transform.mapRect(QRect(QPoint(0, 0), labelSize.toSize())) - - def labelTransformation(self, pos, size): - """ - Calculate the transformation that is needed to paint a label - depending on its alignment and rotation. - - :param QPointF pos: Position where to paint the label - :param QSizeF size: Size of the label - :return: Transformation matrix - - .. seealso:: - - :py:meth:`setLabelAlignment()`, :py:meth:`setLabelRotation()` - """ - transform = QTransform() - transform.translate(pos.x(), pos.y()) - transform.rotate(self.labelRotation()) - - flags = self.labelAlignment() - if flags == 0: - flags = self.Flags[self.alignment()] - # Cast to plain int once to avoid the per-bit Qt6 enum overhead. - flags = int(flags) - - if flags & _ALIGN_LEFT: - x = -size.width() - elif flags & _ALIGN_RIGHT: - x = 0.0 - else: - x = -(0.5 * size.width()) - - if flags & _ALIGN_TOP: - y = -size.height() - elif flags & _ALIGN_BOTTOM: - y = 0 - else: - y = -(0.5 * size.height()) - - transform.translate(x, y) - - return transform - - def labelRect(self, font, value): - """ - Find the bounding rectangle for the label. The coordinates of - the rectangle are relative to spacing + tick length from the backbone - in direction of the tick. - - :param QFont font: Font used for painting - :param float value: Value - :return: Bounding rectangle that is needed to draw a label - """ - lbl, labelSize = self.tickLabel(font, value) - if not lbl or lbl.isEmpty(): - return QRectF(0.0, 0.0, 0.0, 0.0) - # Fast path: when the label is not rotated, the contribution of - # ``pos`` cancels out (transform.translate(pos) followed by - # br.translate(-pos)). This avoids ``labelPosition``, - # ``labelTransformation`` and ``QTransform.mapRect`` entirely - all - # of which are dominant costs in tick-heavy layouts. - if self.labelRotation() == 0.0: - flags = self.labelAlignment() - if flags == 0: - flags = self.Flags[self.alignment()] - flags = int(flags) - w = labelSize.width() - h = labelSize.height() - if flags & _ALIGN_LEFT: - x = -w - elif flags & _ALIGN_RIGHT: - x = 0.0 - else: - x = -0.5 * w - if flags & _ALIGN_TOP: - y = -h - elif flags & _ALIGN_BOTTOM: - y = 0.0 - else: - y = -0.5 * h - return QRectF(x, y, w, h) - pos = self.labelPosition(value) - transform = self.labelTransformation(pos, labelSize) - br = transform.mapRect(QRectF(QPointF(0, 0), labelSize)) - br.translate(-pos.x(), -pos.y()) - return br - - def labelSize(self, font, value): - """ - Calculate the size that is needed to draw a label - - :param QFont font: Label font - :param float value: Value - :return: Size that is needed to draw a label - """ - return self.labelRect(font, value).size() - - def setLabelRotation(self, rotation): - """ - Rotate all labels. - - When changing the rotation, it might be necessary to - adjust the label flags too. Finding a useful combination is - often the result of try and error. - - :param float rotation: Angle in degrees. When changing the label rotation, the - label flags often needs to be adjusted too. - - .. seealso:: - - :py:meth:`setLabelAlignment()`, :py:meth:`labelRotation()`, - :py:meth:`labelAlignment()` - """ - self.__data.labelRotation = rotation - - def labelRotation(self): - """ - :return: the label rotation - - .. seealso:: - - :py:meth:`setLabelRotation()`, :py:meth:`labelAlignment()` - """ - return self.__data.labelRotation - - def setLabelAlignment(self, alignment): - """ - Change the label flags - - Labels are aligned to the point tick length + spacing away from the - backbone. - - The alignment is relative to the orientation of the label text. - In case of an flags of 0 the label will be aligned - depending on the orientation of the scale: - - * `QwtScaleDraw.TopScale`: `Qt.AlignHCenter | Qt.AlignTop` - * `QwtScaleDraw.BottomScale`: `Qt.AlignHCenter | Qt.AlignBottom` - * `QwtScaleDraw.LeftScale`: `Qt.AlignLeft | Qt.AlignVCenter` - * `QwtScaleDraw.RightScale`: `Qt.AlignRight | Qt.AlignVCenter` - - Changing the alignment is often necessary for rotated labels. - - :param Qt.Alignment alignment Or'd `Qt.AlignmentFlags` - - .. seealso:: - - :py:meth:`setLabelRotation()`, :py:meth:`labelRotation()`, - :py:meth:`labelAlignment()` - - .. warning:: - - The various alignments might be confusing. The alignment of the - label is not the alignment of the scale and is not the alignment - of the flags (`QwtText.flags()`) returned from - `QwtAbstractScaleDraw.label()`. - """ - self.__data.labelAlignment = alignment - - def labelAlignment(self): - """ - :return: the label flags - - .. seealso:: - - :py:meth:`setLabelAlignment()`, :py:meth:`labelRotation()` - """ - return self.__data.labelAlignment - - def setLabelAutoSize(self, state): - """ - Set label automatic size option state - - When drawing text labels, if automatic size mode is enabled (default - behavior), the axes are drawn in order to optimize layout space and - depends on text label individual sizes. Otherwise, width and height - won't change when axis range is changing. - - This option is not implemented in Qwt C++ library: this may be used - either as an optimization (updating plot layout is faster when this - option is enabled) or as an appearance preference (with Qwt default - behavior, the size of axes may change when zooming and/or panning - plot canvas which in some cases may not be desired). - - :param bool state: On/off - - .. seealso:: - - :py:meth:`labelAutoSize()` - """ - self.__data.labelAutoSize = state - - def labelAutoSize(self): - """ - :return: True if automatic size option is enabled for labels - - .. seealso:: - - :py:meth:`setLabelAutoSize()` - """ - return self.__data.labelAutoSize - - def _get_max_label_size(self, font): - key = (font.toString(), self.labelRotation()) - size = self._max_label_sizes.get(key) - if size is None: - size = self.labelSize(font, -999999) # -999999 is the biggest label - size.setWidth(math.ceil(size.width())) - size.setHeight(math.ceil(size.height())) - return self._max_label_sizes.setdefault(key, size) - else: - return size - - def maxLabelWidth(self, font): - """ - :param QFont font: Font - :return: the maximum width of a label - """ - ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) - if not ticks: - return 0 - if self.labelAutoSize(): - vmax = sorted( - [v for v in ticks if self.scaleDiv().contains(v)], - key=lambda obj: len("%g" % obj), - )[-1] - return math.ceil(self.labelSize(font, vmax).width()) - ## Original implementation (closer to Qwt's C++ code, but slower): - # return math.ceil(max([self.labelSize(font, v).width() - # for v in ticks if self.scaleDiv().contains(v)])) - else: - return self._get_max_label_size(font).width() - - def maxLabelHeight(self, font): - """ - :param QFont font: Font - :return: the maximum height of a label - """ - ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) - if not ticks: - return 0 - if self.labelAutoSize(): - vmax = sorted( - [v for v in ticks if self.scaleDiv().contains(v)], - key=lambda obj: len("%g" % obj), - )[-1] - return math.ceil(self.labelSize(font, vmax).height()) - ## Original implementation (closer to Qwt's C++ code, but slower): - # return math.ceil(max([self.labelSize(font, v).height() - # for v in ticks if self.scaleDiv().contains(v)])) - else: - return self._get_max_label_size(font).height() - - def updateMap(self): - pos = self.__data.pos - len_ = self.__data.len - sm = self.scaleMap() - if self.orientation() == Qt.Vertical: - sm.setPaintInterval(pos.y() + len_, pos.y()) - else: - sm.setPaintInterval(pos.x(), pos.x() + len_) - - -class QwtDateTimeScaleDraw(QwtScaleDraw): - """Scale draw for datetime axis - - This class formats axis labels as date/time strings from Unix timestamps. - - Args: - format: Format string for datetime display (default: "%Y-%m-%d %H:%M:%S"). - Uses Python datetime.strftime() format codes. - spacing: Spacing between labels (default: 4) - - Examples: - >>> # Create a datetime scale with default format - >>> scale = QwtDateTimeScaleDraw() - - >>> # Create a datetime scale with custom format (time only) - >>> scale = QwtDateTimeScaleDraw(format="%H:%M:%S") - - >>> # Create a datetime scale with date only - >>> scale = QwtDateTimeScaleDraw(format="%Y-%m-%d", spacing=4) - """ - - def __init__(self, format: str = "%Y-%m-%d %H:%M:%S", spacing: int = 4) -> None: - super().__init__() - self._format = format - self.setSpacing(spacing) - - def get_format(self) -> str: - """Get the current datetime format string - - Returns: - str: Format string - """ - return self._format - - def set_format(self, format: str) -> None: - """Set the datetime format string - - Args: - format: Format string for datetime display - """ - self._format = format - - def label(self, value: float) -> QwtText: - """Convert a timestamp value to a formatted date/time label - - Args: - value: Unix timestamp (seconds since epoch) - - Returns: - QwtText: Formatted label - """ - try: - dt = datetime.fromtimestamp(value) - return QwtText(dt.strftime(self._format)) - except (ValueError, OSError): - # Handle invalid timestamps - return QwtText("") diff --git a/qwt/scale_engine.py b/qwt/scale_engine.py deleted file mode 100644 index 8b6545f..0000000 --- a/qwt/scale_engine.py +++ /dev/null @@ -1,1087 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtScaleEngine --------------- - -.. autoclass:: QwtScaleEngine - :members: - -QwtLinearScaleEngine --------------------- - -.. autoclass:: QwtLinearScaleEngine - :members: - -QwtLogScaleEngine ------------------ - -.. autoclass:: QwtLogScaleEngine - :members: -""" - -import math -import sys - -import numpy as np -from qtpy.QtCore import qFuzzyCompare - -from qwt._math import qwtFuzzyCompare -from qwt.interval import QwtInterval -from qwt.scale_div import QwtScaleDiv -from qwt.transform import QwtLogTransform, QwtTransform - -DBL_MAX = sys.float_info.max -LOG_MIN = 1.0e-150 -LOG_MAX = 1.0e150 - - -def qwtLogInterval(base, interval): - return QwtInterval( - math.log(interval.minValue(), base), math.log(interval.maxValue(), base) - ) - - -def qwtPowInterval(base, interval): - return QwtInterval( - math.pow(base, interval.minValue()), math.pow(base, interval.maxValue()) - ) - - -def qwtStepSize(intervalSize, maxSteps, base): - """this version often doesn't find the best ticks: f.e for 15: 5, 10""" - minStep = divideInterval(intervalSize, maxSteps, base) - if minStep != 0.0: - # # ticks per interval - numTicks = math.ceil(abs(intervalSize / minStep)) - 1 - # Do the minor steps fit into the interval? - if ( - qwtFuzzyCompare( - (numTicks + 1) * abs(minStep), abs(intervalSize), intervalSize - ) - > 0 - ): - # The minor steps doesn't fit into the interval - return 0.5 * intervalSize - return minStep - - -EPS = 1.0e-6 - - -def ceilEps(value, intervalSize): - """ - Ceil a value, relative to an interval - - :param float value: Value to be ceiled - :param float intervalSize: Interval size - :return: Rounded value - - .. seealso:: - - :py:func:`qwt.scale_engine.floorEps()` - """ - eps = EPS * intervalSize - value = (value - eps) / intervalSize - return math.ceil(value) * intervalSize - - -def floorEps(value, intervalSize): - """ - Floor a value, relative to an interval - - :param float value: Value to be floored - :param float intervalSize: Interval size - :return: Rounded value - - .. seealso:: - - :py:func:`qwt.scale_engine.ceilEps()` - """ - eps = EPS * intervalSize - value = (value + eps) / intervalSize - return math.floor(value) * intervalSize - - -def divideEps(intervalSize, numSteps): - """ - Divide an interval into steps - - `stepSize = (intervalSize - intervalSize * 10**-6) / numSteps` - - :param float intervalSize: Interval size - :param float numSteps: Number of steps - :return: Step size - """ - if numSteps == 0.0 or intervalSize == 0.0: - return 0.0 - return (intervalSize - (EPS * intervalSize)) / numSteps - - -def divideInterval(intervalSize, numSteps, base): - """ - Calculate a step size for a given interval - - :param float intervalSize: Interval size - :param float numSteps: Number of steps - :param int base: Base for the division (usually 10) - :return: Calculated step size - """ - if numSteps <= 0: - return 0.0 - v = divideEps(intervalSize, numSteps) - if v == 0.0: - return 0.0 - - lx = math.log(abs(v), base) - p = math.floor(lx) - fraction = math.pow(base, lx - p) - n = base - while n > 1 and fraction <= n // 2: - n //= 2 - - stepSize = n * math.pow(base, p) - if v < 0: - stepSize = -stepSize - - return stepSize - - -class QwtScaleEngine_PrivateData(object): - def __init__(self): - self.attributes = QwtScaleEngine.NoAttribute - self.lowerMargin = 0.0 - self.upperMargin = 0.0 - self.referenceValue = 0.0 - self.base = 10 - self.transform = None # QwtTransform - - -class QwtScaleEngine(object): - """ - Base class for scale engines. - - A scale engine tries to find "reasonable" ranges and step sizes - for scales. - - The layout of the scale can be varied with `setAttribute()`. - - `PythonQwt` offers implementations for logarithmic and linear scales. - - Layout attributes: - - * `QwtScaleEngine.NoAttribute`: No attributes - * `QwtScaleEngine.IncludeReference`: Build a scale which includes the - `reference()` value - * `QwtScaleEngine.Symmetric`: Build a scale which is symmetric to the - `reference()` value - * `QwtScaleEngine.Floating`: The endpoints of the scale are supposed to - be equal the outmost included values plus the specified margins (see - `setMargins()`). If this attribute is *not* set, the endpoints of the - scale will be integer multiples of the step size. - * `QwtScaleEngine.Inverted`: Turn the scale upside down - """ - - # enum Attribute - NoAttribute = 0x00 - IncludeReference = 0x01 - Symmetric = 0x02 - Floating = 0x04 - Inverted = 0x08 - - def __init__(self, base=10): - self.__data = QwtScaleEngine_PrivateData() - self.setBase(base) - - def autoScale(self, maxNumSteps, x1, x2, stepSize, relativeMargin=0.0): - """ - Align and divide an interval - - :param int maxNumSteps: Max. number of steps - :param float x1: First limit of the interval (In/Out) - :param float x2: Second limit of the interval (In/Out) - :param float stepSize: Step size - :param float relativeMargin: Margin as a fraction of the interval width - :return: tuple (x1, x2, stepSize) - """ - pass - - def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.0): - """ - Calculate a scale division - - :param float x1: First interval limit - :param float x2: Second interval limit - :param int maxMajorSteps: Maximum for the number of major steps - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one - :return: Calculated scale division - """ - pass - - def setTransformation(self, transform): - """ - Assign a transformation - - :param qwt.transform.QwtTransform transform: Transformation - - The transformation object is used as factory for clones - that are returned by `transformation()` - - The scale engine takes ownership of the transformation. - - .. seealso:: - - :py:meth:`QwtTransform.copy()`, :py:meth:`transformation()` - """ - assert transform is None or isinstance(transform, QwtTransform) - if transform != self.__data.transform: - self.__data.transform = transform - - def transformation(self): - """ - Create and return a clone of the transformation - of the engine. When the engine has no special transformation - None is returned, indicating no transformation. - - :return: A clone of the transfomation - - .. seealso:: - - :py:meth:`setTransformation()` - """ - if self.__data.transform: - return self.__data.transform.copy() - - def lowerMargin(self): - """ - :return: the margin at the lower end of the scale - - The default margin is 0. - - .. seealso:: - - :py:meth:`setMargins()` - """ - return self.__data.lowerMargin - - def upperMargin(self): - """ - :return: the margin at the upper end of the scale - - The default margin is 0. - - .. seealso:: - - :py:meth:`setMargins()` - """ - return self.__data.upperMargin - - def setMargins(self, lower, upper): - """ - Specify margins at the scale's endpoints - - :param float lower: minimum distance between the scale's lower boundary and the smallest enclosed value - :param float upper: minimum distance between the scale's upper boundary and the greatest enclosed value - :return: A clone of the transfomation - - Margins can be used to leave a minimum amount of space between - the enclosed intervals and the boundaries of the scale. - - .. warning:: - - `QwtLogScaleEngine` measures the margins in decades. - - .. seealso:: - - :py:meth:`upperMargin()`, :py:meth:`lowerMargin()` - """ - self.__data.lowerMargin = max([lower, 0.0]) - self.__data.upperMargin = max([upper, 0.0]) - - def divideInterval(self, intervalSize, numSteps): - """ - Calculate a step size for a given interval - - :param float intervalSize: Interval size - :param float numSteps: Number of steps - :return: Step size - """ - return divideInterval(intervalSize, numSteps, self.__data.base) - - def contains(self, interval, value): - """ - Check if an interval "contains" a value - - :param float intervalSize: Interval size - :param float value: Value - :return: True, when the value is inside the interval - """ - if not interval.isValid(): - return False - min_v = interval.minValue() - max_v = interval.maxValue() - eps = abs(1.0e-6 * (max_v - min_v)) - return not (min_v - value > eps or value - max_v > eps) - - def strip(self, ticks, interval): - """ - Remove ticks from a list, that are not inside an interval - - :param list ticks: Tick list - :param qwt.interval.QwtInterval interval: Interval - :return: Stripped tick list - """ - if not interval.isValid() or not ticks: - return [] - # Inline ``contains`` to avoid one Python call per tick: ``strip`` is - # called by buildTicks for every layout pass and is one of the - # dominant costs in tick-heavy plots. - min_v = interval.minValue() - max_v = interval.maxValue() - eps = abs(1.0e-6 * (max_v - min_v)) - lo = min_v - eps - hi = max_v + eps - if lo <= ticks[0] and ticks[-1] <= hi: - return ticks - return [tick for tick in ticks if lo <= tick <= hi] - - def buildInterval(self, value): - """ - Build an interval around a value - - In case of v == 0.0 the interval is [-0.5, 0.5], - otherwide it is [0.5 * v, 1.5 * v] - - :param float value: Initial value - :return: Calculated interval - """ - if value == 0.0: - delta = 0.5 - else: - delta = abs(0.5 * value) - if DBL_MAX - delta < value: - return QwtInterval(DBL_MAX - delta, DBL_MAX) - if -DBL_MAX + delta > value: - return QwtInterval(-DBL_MAX, -DBL_MAX + delta) - return QwtInterval(value - delta, value + delta) - - def setAttribute(self, attribute, on=True): - """ - Change a scale attribute - - :param int attribute: Attribute to change - :param bool on: On/Off - :return: Calculated interval - - .. seealso:: - - :py:meth:`testAttribute()` - """ - if on: - self.__data.attributes |= attribute - else: - self.__data.attributes &= ~attribute - - def testAttribute(self, attribute): - """ - :param int attribute: Attribute to be tested - :return: True, if attribute is enabled - - .. seealso:: - - :py:meth:`setAttribute()` - """ - return self.__data.attributes & attribute - - def setAttributes(self, attributes): - """ - Change the scale attribute - - :param attributes: Set scale attributes - - .. seealso:: - - :py:meth:`attributes()` - """ - self.__data.attributes = attributes - - def attributes(self): - """ - :return: Scale attributes - - .. seealso:: - - :py:meth:`setAttributes()`, :py:meth:`testAttribute()` - """ - return self.__data.attributes - - def setReference(self, r): - """ - Specify a reference point - - :param float r: new reference value - - The reference point is needed if options `IncludeReference` or - `Symmetric` are active. Its default value is 0.0. - """ - self.__data.referenceValue = r - - def reference(self): - """ - :return: the reference value - - .. seealso:: - - :py:meth:`setReference()`, :py:meth:`setAttribute()` - """ - return self.__data.referenceValue - - def setBase(self, base): - """ - Set the base of the scale engine - - While a base of 10 is what 99.9% of all applications need - certain scales might need a different base: f.e 2 - - The default setting is 10 - - :param int base: Base of the engine - - .. seealso:: - - :py:meth:`base()` - """ - self.__data.base = max([base, 2]) - - def base(self): - """ - :return: Base of the scale engine - - .. seealso:: - - :py:meth:`setBase()` - """ - return self.__data.base - - -class QwtLinearScaleEngine(QwtScaleEngine): - r""" - A scale engine for linear scales - - The step size will fit into the pattern - \f$\left\{ 1,2,5\right\} \cdot 10^{n}\f$, where n is an integer. - """ - - def __init__(self, base=10): - super(QwtLinearScaleEngine, self).__init__(base) - - def autoScale(self, maxNumSteps, x1, x2, stepSize, relativeMargin=0.0): - """ - Align and divide an interval - - :param int maxNumSteps: Max. number of steps - :param float x1: First limit of the interval (In/Out) - :param float x2: Second limit of the interval (In/Out) - :param float stepSize: Step size - :param float relativeMargin: Margin as a fraction of the interval width - :return: tuple (x1, x2, stepSize) - - .. seealso:: - - :py:meth:`setAttribute()` - """ - # Apply the relative margin (fraction of the interval width) in linear space: - if relativeMargin > 0.0: - margin = (x2 - x1) * relativeMargin - x1 -= margin - x2 += margin - - interval = QwtInterval(x1, x2) - interval = interval.normalized() - interval.setMinValue(interval.minValue() - self.lowerMargin()) - interval.setMaxValue(interval.maxValue() + self.upperMargin()) - if self.testAttribute(QwtScaleEngine.Symmetric): - interval = interval.symmetrize(self.reference()) - if self.testAttribute(QwtScaleEngine.IncludeReference): - interval = interval.extend(self.reference()) - if interval.width() == 0.0: - interval = self.buildInterval(interval.minValue()) - stepSize = divideInterval(interval.width(), max([maxNumSteps, 1]), self.base()) - if not self.testAttribute(QwtScaleEngine.Floating): - interval = self.align(interval, stepSize) - x1 = interval.minValue() - x2 = interval.maxValue() - if self.testAttribute(QwtScaleEngine.Inverted): - x1, x2 = x2, x1 - stepSize = -stepSize - return x1, x2, stepSize - - def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.0): - """ - Calculate a scale division for an interval - - :param float x1: First interval limit - :param float x2: Second interval limit - :param int maxMajorSteps: Maximum for the number of major steps - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one - :return: Calculated scale division - """ - interval = QwtInterval(x1, x2).normalized() - if interval.width() <= 0: - return QwtScaleDiv() - stepSize = abs(stepSize) - if stepSize == 0.0: - if maxMajorSteps < 1: - maxMajorSteps = 1 - stepSize = divideInterval(interval.width(), maxMajorSteps, self.base()) - scaleDiv = QwtScaleDiv() - if stepSize != 0.0: - ticks = self.buildTicks(interval, stepSize, maxMinorSteps) - scaleDiv = QwtScaleDiv(interval, ticks) - if x1 > x2: - scaleDiv.invert() - return scaleDiv - - def buildTicks(self, interval, stepSize, maxMinorSteps): - """ - Calculate ticks for an interval - - :param qwt.interval.QwtInterval interval: Interval - :param float stepSize: Step size - :param int maxMinorSteps: Maximum number of minor steps - :return: Calculated ticks - """ - ticks = [[] for _i in range(QwtScaleDiv.NTickTypes)] - boundingInterval = self.align(interval, stepSize) - ticks[QwtScaleDiv.MajorTick] = self.buildMajorTicks(boundingInterval, stepSize) - if maxMinorSteps > 0: - self.buildMinorTicks(ticks, maxMinorSteps, stepSize) - for i in range(QwtScaleDiv.NTickTypes): - ticks[i] = self.strip(ticks[i], interval) - for j in range(len(ticks[i])): - if qwtFuzzyCompare(ticks[i][j], 0.0, stepSize) == 0: - ticks[i][j] = 0.0 - return ticks - - def buildMajorTicks(self, interval, stepSize): - """ - Calculate major ticks for an interval - - :param qwt.interval.QwtInterval interval: Interval - :param float stepSize: Step size - :return: Calculated ticks - """ - numTicks = min([round(interval.width() / stepSize) + 1, 10000]) - if np.isnan(numTicks): - numTicks = 0 - ticks = [interval.minValue()] - for i in range(1, int(numTicks - 1)): - ticks += [interval.minValue() + i * stepSize] - ticks += [interval.maxValue()] - return ticks - - def buildMinorTicks(self, ticks, maxMinorSteps, stepSize): - """ - Calculate minor ticks for an interval - - :param list ticks: Major ticks (returned) - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Step size - """ - minStep = qwtStepSize(stepSize, maxMinorSteps, self.base()) - if minStep == 0.0: - return - numTicks = int(math.ceil(abs(stepSize / minStep)) - 1) - medIndex = -1 - if numTicks % 2: - medIndex = numTicks // 2 - for val in ticks[QwtScaleDiv.MajorTick]: - for k in range(numTicks): - val += minStep - alignedValue = val - if qwtFuzzyCompare(val, 0.0, stepSize) == 0: - alignedValue = 0.0 - if k == medIndex: - ticks[QwtScaleDiv.MediumTick] += [alignedValue] - else: - ticks[QwtScaleDiv.MinorTick] += [alignedValue] - - def align(self, interval, stepSize): - """ - Align an interval to a step size - - The limits of an interval are aligned that both are integer - multiples of the step size. - - :param qwt.interval.QwtInterval interval: Interval - :param float stepSize: Step size - :return: Aligned interval - """ - x1 = interval.minValue() - x2 = interval.maxValue() - eps = 0.000000000001 - if -DBL_MAX + stepSize <= x1: - x = floorEps(x1, stepSize) - if abs(x) <= eps or not qFuzzyCompare(x1, x): - x1 = x - if DBL_MAX - stepSize >= x2: - x = ceilEps(x2, stepSize) - if abs(x) <= eps or not qFuzzyCompare(x2, x): - x2 = x - return QwtInterval(x1, x2) - - -class QwtLogScaleEngine(QwtScaleEngine): - """ - A scale engine for logarithmic scales - - The step size is measured in *decades* and the major step size will be - adjusted to fit the pattern {1,2,3,5}.10**n, where n is a natural number - including zero. - - .. warning:: - - The step size as well as the margins are measured in *decades*. - """ - - def __init__(self, base=10): - super(QwtLogScaleEngine, self).__init__(base) - self.setTransformation(QwtLogTransform()) - - def autoScale(self, maxNumSteps, x1, x2, stepSize, relativeMargin=0.0): - """ - Align and divide an interval - - :param int maxNumSteps: Max. number of steps - :param float x1: First limit of the interval (In/Out) - :param float x2: Second limit of the interval (In/Out) - :param float stepSize: Step size - :param float relativeMargin: Margin as a fraction of the interval width - :return: tuple (x1, x2, stepSize) - - .. seealso:: - - :py:meth:`setAttribute()` - """ - if x1 > x2: - x1, x2 = x2, x1 - logBase = self.base() - - # Apply the relative margin (fraction of the interval width) in logarithmic - # space, and convert back to linear space. - if relativeMargin is not None: - x1 = min(max([x1, LOG_MIN]), LOG_MAX) - x2 = min(max([x2, LOG_MIN]), LOG_MAX) - log_margin = math.log(x2 / x1, logBase) * relativeMargin - x1 /= math.pow(logBase, log_margin) - x2 *= math.pow(logBase, log_margin) - - interval = QwtInterval( - x1 / math.pow(logBase, self.lowerMargin()), - x2 * math.pow(logBase, self.upperMargin()), - ) - if interval.maxValue() / interval.minValue() < logBase: - linearScaler = QwtLinearScaleEngine() - linearScaler.setAttributes(self.attributes()) - linearScaler.setReference(self.reference()) - linearScaler.setMargins(self.lowerMargin(), self.upperMargin()) - - x1, x2, stepSize = linearScaler.autoScale(maxNumSteps, x1, x2, stepSize) - - linearInterval = QwtInterval(x1, x2).normalized() - linearInterval = linearInterval.limited(LOG_MIN, LOG_MAX) - - if linearInterval.maxValue() / linearInterval.minValue() < logBase: - # The min / max interval is too short to be represented as a log scale. - # Set the step to 0, so that a new step is calculated and a linear scale is used. - stepSize = 0.0 - return x1, x2, stepSize - - logRef = 1.0 - if self.reference() > LOG_MIN / 2: - logRef = min([self.reference(), LOG_MAX / 2]) - - if self.testAttribute(QwtScaleEngine.Symmetric): - delta = max([interval.maxValue() / logRef, logRef / interval.minValue()]) - interval.setInterval(logRef / delta, logRef * delta) - - if self.testAttribute(QwtScaleEngine.IncludeReference): - interval = interval.extend(logRef) - - interval = interval.limited(LOG_MIN, LOG_MAX) - - if interval.width() == 0.0: - interval = self.buildInterval(interval.minValue()) - - stepSize = self.divideInterval( - qwtLogInterval(logBase, interval).width(), max([maxNumSteps, 1]) - ) - if stepSize < 1.0: - stepSize = 1.0 - - if not self.testAttribute(QwtScaleEngine.Floating): - interval = self.align(interval, stepSize) - - x1 = interval.minValue() - x2 = interval.maxValue() - - if self.testAttribute(QwtScaleEngine.Inverted): - x1, x2 = x2, x1 - stepSize = -stepSize - - return x1, x2, stepSize - - def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.0): - """ - Calculate a scale division for an interval - - :param float x1: First interval limit - :param float x2: Second interval limit - :param int maxMajorSteps: Maximum for the number of major steps - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one - :return: Calculated scale division - """ - interval = QwtInterval(x1, x2).normalized() - interval = interval.limited(LOG_MIN, LOG_MAX) - - if interval.width() <= 0: - return QwtScaleDiv() - - logBase = self.base() - - if interval.maxValue() / interval.minValue() < logBase: - linearScaler = QwtLinearScaleEngine() - linearScaler.setAttributes(self.attributes()) - linearScaler.setReference(self.reference()) - linearScaler.setMargins(self.lowerMargin(), self.upperMargin()) - return linearScaler.divideScale( - x1, x2, maxMajorSteps, maxMinorSteps, stepSize - ) - - stepSize = abs(stepSize) - if stepSize == 0.0: - if maxMajorSteps < 1: - maxMajorSteps = 1 - stepSize = self.divideInterval( - qwtLogInterval(logBase, interval).width(), maxMajorSteps - ) - if stepSize < 1.0: - stepSize = 1.0 - - scaleDiv = QwtScaleDiv() - if stepSize != 0.0: - ticks = self.buildTicks(interval, stepSize, maxMinorSteps) - scaleDiv = QwtScaleDiv(interval, ticks) - - if x1 > x2: - scaleDiv.invert() - - return scaleDiv - - def buildTicks(self, interval, stepSize, maxMinorSteps): - """ - Calculate ticks for an interval - - :param qwt.interval.QwtInterval interval: Interval - :param float stepSize: Step size - :param int maxMinorSteps: Maximum number of minor steps - :return: Calculated ticks - """ - ticks = [[] for _i in range(QwtScaleDiv.NTickTypes)] - boundingInterval = self.align(interval, stepSize) - ticks[QwtScaleDiv.MajorTick] = self.buildMajorTicks(boundingInterval, stepSize) - if maxMinorSteps > 0: - self.buildMinorTicks(ticks, maxMinorSteps, stepSize) - for i in range(QwtScaleDiv.NTickTypes): - ticks[i] = self.strip(ticks[i], interval) - return ticks - - def buildMajorTicks(self, interval, stepSize): - """ - Calculate major ticks for an interval - - :param qwt.interval.QwtInterval interval: Interval - :param float stepSize: Step size - :return: Calculated ticks - """ - width = qwtLogInterval(self.base(), interval).width() - numTicks = min([int(round(width / stepSize)) + 1, 10000]) - - lxmin = math.log(interval.minValue()) - lxmax = math.log(interval.maxValue()) - lstep = (lxmax - lxmin) / float(numTicks - 1) - - ticks = [interval.minValue()] - for i in range(1, numTicks - 1): - ticks += [math.exp(lxmin + float(i) * lstep)] - ticks += [interval.maxValue()] - return ticks - - def buildMinorTicks(self, ticks, maxMinorSteps, stepSize): - """ - Calculate minor ticks for an interval - - :param list ticks: Major ticks (returned) - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Step size - """ - logBase = self.base() - - if stepSize < 1.1: - minStep = self.divideInterval(stepSize, maxMinorSteps + 1) - if minStep == 0.0: - return - - numSteps = int(round(stepSize / minStep)) - - mediumTickIndex = -1 - if numSteps > 2 and numSteps % 2 == 0: - mediumTickIndex = numSteps // 2 - - for v in ticks[QwtScaleDiv.MajorTick]: - s = logBase / numSteps - if s >= 1.0: - if not qFuzzyCompare(s, 1.0): - ticks[QwtScaleDiv.MinorTick] += [v * s] - for j in range(2, numSteps): - ticks[QwtScaleDiv.MinorTick] += [v * j * s] - else: - for j in range(1, numSteps): - tick = v + j * v * (logBase - 1) / numSteps - if j == mediumTickIndex: - ticks[QwtScaleDiv.MediumTick] += [tick] - else: - ticks[QwtScaleDiv.MinorTick] += [tick] - - else: - minStep = self.divideInterval(stepSize, maxMinorSteps) - if minStep == 0.0: - return - - if minStep < 1.0: - minStep = 1.0 - - numTicks = int(round(stepSize / minStep)) - 1 - - if qwtFuzzyCompare((numTicks + 1) * minStep, stepSize, stepSize) > 0: - numTicks = 0 - - if numTicks < 1: - return - - mediumTickIndex = -1 - if numTicks > 2 and numTicks % 2: - mediumTickIndex = numTicks // 2 - - minFactor = max([math.pow(logBase, minStep), float(logBase)]) - - for tick in ticks[QwtScaleDiv.MajorTick]: - for j in range(numTicks): - tick *= minFactor - if j == mediumTickIndex: - ticks[QwtScaleDiv.MediumTick] += [tick] - else: - ticks[QwtScaleDiv.MinorTick] += [tick] - - def align(self, interval, stepSize): - """ - Align an interval to a step size - - The limits of an interval are aligned that both are integer - multiples of the step size. - - :param qwt.interval.QwtInterval interval: Interval - :param float stepSize: Step size - :return: Aligned interval - """ - intv = qwtLogInterval(self.base(), interval) - - x1 = floorEps(intv.minValue(), stepSize) - if qwtFuzzyCompare(interval.minValue(), x1, stepSize) == 0: - x1 = interval.minValue() - - x2 = ceilEps(intv.maxValue(), stepSize) - if qwtFuzzyCompare(interval.maxValue(), x2, stepSize) == 0: - x2 = interval.maxValue() - - return qwtPowInterval(self.base(), QwtInterval(x1, x2)) - - -class QwtDateTimeScaleEngine(QwtLinearScaleEngine): - """ - A scale engine for datetime scales that creates intelligent time-based tick intervals. - - This engine calculates tick intervals that correspond to meaningful time units - (seconds, minutes, hours, days, weeks, months, years) rather than arbitrary - numerical spacing. - """ - - # Time intervals in seconds - TIME_INTERVALS = [ - 1, # 1 second - 5, # 5 seconds - 10, # 10 seconds - 15, # 15 seconds - 30, # 30 seconds - 60, # 1 minute - 2 * 60, # 2 minutes - 5 * 60, # 5 minutes - 10 * 60, # 10 minutes - 15 * 60, # 15 minutes - 30 * 60, # 30 minutes - 60 * 60, # 1 hour - 2 * 60 * 60, # 2 hours - 3 * 60 * 60, # 3 hours - 6 * 60 * 60, # 6 hours - 12 * 60 * 60, # 12 hours - 24 * 60 * 60, # 1 day - 2 * 24 * 60 * 60, # 2 days - 7 * 24 * 60 * 60, # 1 week - 2 * 7 * 24 * 60 * 60, # 2 weeks - 30 * 24 * 60 * 60, # 1 month (approx) - 3 * 30 * 24 * 60 * 60, # 3 months (approx) - 6 * 30 * 24 * 60 * 60, # 6 months (approx) - 365 * 24 * 60 * 60, # 1 year (approx) - ] - - def __init__(self, base=10): - super(QwtDateTimeScaleEngine, self).__init__(base) - - def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.0): - """ - Calculate a scale division for a datetime interval - - :param float x1: First interval limit (Unix timestamp) - :param float x2: Second interval limit (Unix timestamp) - :param int maxMajorSteps: Maximum for the number of major steps - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Step size. If stepSize == 0.0, calculates intelligent datetime step - :return: Calculated scale division - """ - interval = QwtInterval(x1, x2).normalized() - if interval.width() <= 0: - return QwtScaleDiv() - - # If stepSize is provided and > 0, use parent implementation - if stepSize > 0.0: - return super(QwtDateTimeScaleEngine, self).divideScale( - x1, x2, maxMajorSteps, maxMinorSteps, stepSize - ) - - # Calculate intelligent datetime step size - duration = interval.width() # Duration in seconds - - # Find the best time interval for the given duration and max steps - best_step = self._find_best_time_step(duration, maxMajorSteps) - - # Use the calculated datetime step - scaleDiv = QwtScaleDiv() - if best_step > 0.0: - ticks = self.buildTicks(interval, best_step, maxMinorSteps) - scaleDiv = QwtScaleDiv(interval, ticks) - - if x1 > x2: - scaleDiv.invert() - - return scaleDiv - - def _find_best_time_step(self, duration, max_steps): - """ - Find the best time interval step for the given duration and maximum steps. - - :param float duration: Total duration in seconds - :param int max_steps: Maximum number of major ticks - :return: Best step size in seconds - """ - if max_steps < 1: - max_steps = 1 - - # Calculate the target step size - target_step = duration / max_steps - - # Find the time interval that is closest to our target - best_step = self.TIME_INTERVALS[0] - min_error = abs(target_step - best_step) - - for interval in self.TIME_INTERVALS: - error = abs(target_step - interval) - if error < min_error: - min_error = error - best_step = interval - # If the interval is getting much larger than target, stop - elif interval > target_step * 2: - break - - return float(best_step) - - def buildMinorTicks(self, ticks, maxMinorSteps, stepSize): - """ - Calculate minor ticks for datetime intervals - - :param list ticks: List of tick arrays - :param int maxMinorSteps: Maximum number of minor steps - :param float stepSize: Major tick step size - """ - if maxMinorSteps < 1: - return - - # For datetime, create intelligent minor tick intervals - minor_step = self._get_minor_step(stepSize, maxMinorSteps) - - if minor_step <= 0: - return - - major_ticks = ticks[QwtScaleDiv.MajorTick] - if len(major_ticks) < 2: - return - - minor_ticks = [] - - # Generate minor ticks between each pair of major ticks - for i in range(len(major_ticks) - 1): - start = major_ticks[i] - end = major_ticks[i + 1] - - # Add minor ticks between start and end - current = start + minor_step - while current < end: - minor_ticks.append(current) - current += minor_step - - ticks[QwtScaleDiv.MinorTick] = minor_ticks - - def _get_minor_step(self, major_step, max_minor_steps): - """ - Calculate appropriate minor tick step size for datetime intervals - - :param float major_step: Major tick step size in seconds - :param int max_minor_steps: Maximum number of minor steps - :return: Minor tick step size in seconds - """ - # Define sensible minor tick divisions for different time scales - if major_step >= 365 * 24 * 60 * 60: # 1 year or more - return 30 * 24 * 60 * 60 # 1 month - elif major_step >= 30 * 24 * 60 * 60: # 1 month or more - return 7 * 24 * 60 * 60 # 1 week - elif major_step >= 7 * 24 * 60 * 60: # 1 week or more - return 24 * 60 * 60 # 1 day - elif major_step >= 24 * 60 * 60: # 1 day or more - return 6 * 60 * 60 # 6 hours - elif major_step >= 60 * 60: # 1 hour or more - return 15 * 60 # 15 minutes - elif major_step >= 10 * 60: # 10 minutes or more - return 2 * 60 # 2 minutes - elif major_step >= 60: # 1 minute or more - return 15 # 15 seconds - elif major_step >= 10: # 10 seconds or more - return 2 # 2 seconds - else: # Less than 10 seconds - return major_step / max(max_minor_steps, 2) diff --git a/qwt/scale_map.py b/qwt/scale_map.py deleted file mode 100644 index b99fe65..0000000 --- a/qwt/scale_map.py +++ /dev/null @@ -1,308 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtScaleMap ------------ - -.. autoclass:: QwtScaleMap - :members: -""" - -from qtpy.QtCore import QPointF, QRectF - -from qwt._math import qwtFuzzyCompare - - -class QwtScaleMap(object): - """ - A scale map - - `QwtScaleMap` offers transformations from the coordinate system - of a scale into the linear coordinate system of a paint device - and vice versa. - - The scale and paint device intervals are both set to [0,1]. - - .. py:class:: QwtScaleMap([other=None]) - - Constructor (eventually, copy constructor) - - :param qwt.scale_map.QwtScaleMap other: Other scale map - - .. py:class:: QwtScaleMap(p1, p2, s1, s2) - :noindex: - - Constructor (was provided by `PyQwt` but not by `Qwt`) - - :param int p1: First border of the paint interval - :param int p2: Second border of the paint interval - :param float s1: First border of the scale interval - :param float s2: Second border of the scale interval - """ - - def __init__(self, *args): - self.__transform = None # QwtTransform - self.__s1 = 0.0 - self.__s2 = 1.0 - self.__p1 = 0.0 - self.__p2 = 1.0 - other = None - if len(args) == 1: - (other,) = args - elif len(args) == 4: - p1, p2, s1, s2 = args - self.__s1 = s1 - self.__s2 = s2 - self.__p1 = p1 - self.__p2 = p2 - elif len(args) != 0: - raise TypeError( - "%s() takes 0, 1, or 4 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - if other is None: - self.__cnv = 1.0 - self.__ts1 = 0.0 - else: - self.__s1 = other.__s1 - self.__s2 = other.__s2 - self.__p1 = other.__p1 - self.__p2 = other.__p2 - self.__cnv = other.__cnv - self.__ts1 = other.__ts1 - if other.__transform: - self.__transform = other.__transform.copy() - - def __eq__(self, other): - return ( - self.__s1 == other.__s1 - and self.__s2 == other.__s2 - and self.__p1 == other.__p1 - and self.__p2 == other.__p2 - and self.__cnv == other.__cnv - and self.__ts1 == other.__ts1 - ) - - def s1(self): - """ - :return: First border of the scale interval - """ - return self.__s1 - - def s2(self): - """ - :return: Second border of the scale interval - """ - return self.__s2 - - def p1(self): - """ - :return: First border of the paint interval - """ - return self.__p1 - - def p2(self): - """ - :return: Second border of the paint interval - """ - return self.__p2 - - def pDist(self): - """ - :return: `abs(p2() - p1())` - """ - return abs(self.__p2 - self.__p1) - - def sDist(self): - """ - :return: `abs(s2() - s1())` - """ - return abs(self.__s2 - self.__s1) - - def transform_scalar(self, s): - """ - Transform a point related to the scale interval into an point - related to the interval of the paint device - - :param float s: Value relative to the coordinates of the scale - :return: Transformed value - - .. seealso:: - - :py:meth:`invTransform_scalar()` - """ - if self.__transform: - s = self.__transform.transform(s) - return self.__p1 + (s - self.__ts1) * self.__cnv - - def invTransform_scalar(self, p): - """ - Transform an paint device value into a value in the - interval of the scale. - - :param float p: Value relative to the coordinates of the paint device - :return: Transformed value - - .. seealso:: - - :py:meth:`transform_scalar()` - """ - if self.__cnv == 0: - s = self.__ts1 # avoid divide by zero - else: - s = self.__ts1 + (p - self.__p1) / self.__cnv - if self.__transform: - s = self.__transform.invTransform(s) - return s - - def isInverting(self): - """ - :return: True, when ( p1() < p2() ) != ( s1() < s2() ) - """ - return (self.__p1 < self.__p2) != (self.__s1 < self.__s2) - - def setTransformation(self, transform): - """ - Initialize the map with a transformation - - :param qwt.transform.QwtTransform transform: Transformation - """ - if self.__transform != transform: - self.__transform = transform - self.setScaleInterval(self.__s1, self.__s2) - - def transformation(self): - """ - :return: the transformation - """ - return self.__transform - - def setScaleInterval(self, s1, s2): - """ - Specify the borders of the scale interval - - :param float s1: first border - :param float s2: second border - - .. warning:: - - Scales might be aligned to transformation depending boundaries - """ - self.__s1 = s1 - self.__s2 = s2 - if self.__transform: - self.__s1 = self.__transform.bounded(self.__s1) - self.__s2 = self.__transform.bounded(self.__s2) - self.updateFactor() - - def setPaintInterval(self, p1, p2): - """ - Specify the borders of the paint device interval - - :param float p1: first border - :param float p2: second border - """ - self.__p1 = p1 - self.__p2 = p2 - self.updateFactor() - - def updateFactor(self): - self.__ts1 = self.__s1 - ts2 = self.__s2 - if self.__transform: - self.__ts1 = self.__transform.transform(self.__ts1) - ts2 = self.__transform.transform(ts2) - if self.__ts1 == ts2: - # Degenerate scale: collapse every value to ``p1`` (matches the - # symmetric guard in ``invTransform_scalar`` and the C++ Qwt - # behaviour). - self.__cnv = 0.0 - else: - self.__cnv = (self.__p2 - self.__p1) / (ts2 - self.__ts1) - - def transform(self, *args): - """ - Transform a rectangle from scale to paint coordinates. - - Transfom a scalar: - - :param float scalar: Scalar - - Transfom a rectangle: - - :param qwt.scale_map.QwtScaleMap xMap: X map - :param qwt.scale_map.QwtScaleMap yMap: Y map - :param QRectF rect: Rectangle in paint coordinates - - Transfom a point: - - :param qwt.scale_map.QwtScaleMap xMap: X map - :param qwt.scale_map.QwtScaleMap yMap: Y map - :param QPointF pos: Position in scale coordinates - - .. seealso:: - - :py:meth:`invTransform()` - """ - nargs = len(args) - if nargs == 1: - # Scalar transform: inline the fast path for the dominant case - # (avoids one Python call frame per tick label). - s = args[0] - if self.__transform: - s = self.__transform.transform(s) - return self.__p1 + (s - self.__ts1) * self.__cnv - elif nargs == 3 and isinstance(args[2], QPointF): - xMap, yMap, pos = args - return QPointF(xMap.transform(pos.x()), yMap.transform(pos.y())) - elif nargs == 3 and isinstance(args[2], QRectF): - xMap, yMap, rect = args - x1 = xMap.transform(rect.left()) - x2 = xMap.transform(rect.right()) - y1 = yMap.transform(rect.top()) - y2 = yMap.transform(rect.bottom()) - if x2 < x1: - x1, x2 = x2, x1 - if y2 < y1: - y1, y2 = y2, y1 - if qwtFuzzyCompare(x1, 0.0, x2 - x1) == 0: - x1 = 0.0 - if qwtFuzzyCompare(x2, 0.0, x2 - x1) == 0: - x2 = 0.0 - if qwtFuzzyCompare(y1, 0.0, y2 - y1) == 0: - y1 = 0.0 - if qwtFuzzyCompare(y2, 0.0, y2 - y1) == 0: - y2 = 0.0 - return QRectF(x1, y1, x2 - x1, y2 - y1) - else: - raise TypeError( - "%s().transform() takes 1 or 3 argument(s) (%s " - "given)" % (self.__class__.__name__, len(args)) - ) - - def invTransform(self, *args): - """Transform from paint to scale coordinates - - Scalar: scalemap.invTransform(scalar) - Point (QPointF): scalemap.invTransform(xMap, yMap, pos) - Rectangle (QRectF): scalemap.invTransform(xMap, yMap, rect) - """ - if len(args) == 1: - # Scalar transform - return self.invTransform_scalar(args[0]) - elif isinstance(args[2], QPointF): - xMap, yMap, pos = args - return QPointF(xMap.invTransform(pos.x()), yMap.invTransform(pos.y())) - elif isinstance(args[2], QRectF): - xMap, yMap, rect = args - x1 = xMap.invTransform(rect.left()) - x2 = xMap.invTransform(rect.right()) - y1 = yMap.invTransform(rect.top()) - y2 = yMap.invTransform(rect.bottom()) - r = QRectF(x1, y1, x2 - x1, y2 - y1) - return r.normalized() diff --git a/qwt/scale_widget.py b/qwt/scale_widget.py deleted file mode 100644 index fb3eac6..0000000 --- a/qwt/scale_widget.py +++ /dev/null @@ -1,853 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtScaleWidget --------------- - -.. autoclass:: QwtScaleWidget - :members: -""" - -import math - -from qtpy.QtCore import QObject, QRectF, QSize, Qt, Signal -from qtpy.QtGui import QPainter, QPalette -from qtpy.QtWidgets import QSizePolicy, QStyle, QStyleOption, QWidget - -from qwt.color_map import QwtColorMap, QwtLinearColorMap -from qwt.interval import QwtInterval -from qwt.painter import QwtPainter -from qwt.scale_draw import QwtScaleDraw -from qwt.scale_engine import QwtLinearScaleEngine -from qwt.text import QwtText - - -class ColorBar(object): - def __init__(self): - self.isEnabled = None - self.width = None - self.interval = QwtInterval() - self.colorMap = QwtColorMap() - - -class QwtScaleWidget_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.scaleDraw = None - self.borderDist = [None] * 2 - self.minBorderDist = [None] * 2 - self.scaleLength = None - self.margin = None - self.titleOffset = None - self.spacing = None - self.title = QwtText() - self.layoutFlags = None - self.colorBar = ColorBar() - - -class QwtScaleWidget(QWidget): - """ - A Widget which contains a scale - - This Widget can be used to decorate composite widgets with - a scale. - - Layout flags: - - * `QwtScaleWidget.TitleInverted`: The title of vertical scales is painted from top to bottom. Otherwise it is painted from bottom to top. - - .. py:class:: QwtScaleWidget([parent=None]) - - Alignment default is `QwtScaleDraw.LeftScale`. - - :param parent: Parent widget - :type parent: QWidget or None - - .. py:class:: QwtScaleWidget(align, parent) - :noindex: - - :param int align: Alignment - :param QWidget parent: Parent widget - """ - - scaleDivChanged = Signal() - - # enum LayoutFlag - TitleInverted = 1 - - def __init__(self, *args): - self.__data = None - align = QwtScaleDraw.LeftScale - if len(args) == 0: - parent = None - elif len(args) == 1: - (parent,) = args - elif len(args) == 2: - align, parent = args - else: - raise TypeError( - "%s() takes 0, 1 or 2 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - super(QwtScaleWidget, self).__init__(parent) - self.initScale(align) - - def initScale(self, align): - """ - Initialize the scale - - :param int align: Alignment - """ - self.__data = QwtScaleWidget_PrivateData() - self.__data.layoutFlags = 0 - if align == QwtScaleDraw.RightScale: - self.__data.layoutFlags |= self.TitleInverted - - self.__data.borderDist = [0, 0] - self.__data.minBorderDist = [0, 0] - self.__data.margin = 4 - self.__data.titleOffset = 0 - self.__data.spacing = 2 - - self.__data.scaleDraw = QwtScaleDraw() - self.__data.scaleDraw.setAlignment(align) - self.__data.scaleDraw.setLength(10) - - self.__data.scaleDraw.setScaleDiv( - QwtLinearScaleEngine().divideScale(0.0, 100.0, 10, 5) - ) - - self.__data.colorBar.colorMap = QwtLinearColorMap() - self.__data.colorBar.isEnabled = False - self.__data.colorBar.width = 10 - - flags = Qt.AlignmentFlag(Qt.AlignHCenter | Qt.TextExpandTabs | Qt.TextWordWrap) - self.__data.title.setRenderFlags(flags) - self.__data.title.setFont(self.font()) - - policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) - if self.__data.scaleDraw.orientation() == Qt.Vertical: - policy.transpose() - - self.setSizePolicy(policy) - - self.setAttribute(Qt.WA_WState_OwnSizePolicy, False) - - def setLayoutFlag(self, flag, on=True): - """ - Toggle an layout flag - - :param int flag: Layout flag - :param bool on: True/False - - .. seealso:: - - :py:meth:`testLayoutFlag()` - """ - if (self.__data.layoutFlags & flag != 0) != on: - if on: - self.__data.layoutFlags |= flag - else: - self.__data.layoutFlags &= ~flag - self.update() - - def testLayoutFlag(self, flag): - """ - Test a layout flag - - :param int flag: Layout flag - :return: True/False - - .. seealso:: - - :py:meth:`setLayoutFlag()` - """ - return self.__data.layoutFlags & flag - - def setTitle(self, title): - """ - Give title new text contents - - :param title: New title - :type title: qwt.text.QwtText or str - - .. seealso:: - - :py:meth:`title()` - """ - if isinstance(title, QwtText): - flags = title.renderFlags() & (~int(Qt.AlignTop | Qt.AlignBottom)) - title.setRenderFlags(flags) - if title != self.__data.title: - self.__data.title = title - self.layoutScale() - else: - if self.__data.title.text() != title: - self.__data.title.setText(title) - self.layoutScale() - - def setAlignment(self, alignment): - """ - Change the alignment - - :param int alignment: New alignment - - Valid alignment values: see :py:class:`qwt.scale_draw.QwtScaleDraw` - - .. seealso:: - - :py:meth:`alignment()` - """ - if self.__data.scaleDraw: - self.__data.scaleDraw.setAlignment(alignment) - if not self.testAttribute(Qt.WA_WState_OwnSizePolicy): - policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) - if self.__data.scaleDraw.orientation() == Qt.Vertical: - policy.transpose() - self.setSizePolicy(policy) - self.setAttribute(Qt.WA_WState_OwnSizePolicy, False) - self.layoutScale() - - def alignment(self): - """ - :return: position - - .. seealso:: - - :py:meth:`setAlignment()` - """ - if not self.scaleDraw(): - return QwtScaleDraw.LeftScale - return self.scaleDraw().alignment() - - def setBorderDist(self, dist1, dist2): - """ - Specify distances of the scale's endpoints from the - widget's borders. The actual borders will never be less - than minimum border distance. - - :param int dist1: Left or top Distance - :param int dist2: Right or bottom distance - - .. seealso:: - - :py:meth:`borderDist()` - """ - if dist1 != self.__data.borderDist[0] or dist2 != self.__data.borderDist[1]: - self.__data.borderDist = [dist1, dist2] - self.layoutScale() - - def setMargin(self, margin): - """ - Specify the margin to the colorBar/base line. - - :param int margin: Margin - - .. seealso:: - - :py:meth:`margin()` - """ - margin = max([0, margin]) - if margin != self.__data.margin: - self.__data.margin = margin - self.layoutScale() - - def setSpacing(self, spacing): - """ - Specify the distance between color bar, scale and title - - :param int spacing: Spacing - - .. seealso:: - - :py:meth:`spacing()` - """ - spacing = max([0, spacing]) - if spacing != self.__data.spacing: - self.__data.spacing = spacing - self.layoutScale() - - def setLabelAlignment(self, alignment): - """ - Change the alignment for the labels. - - :param int spacing: Spacing - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAlignment()`, - :py:meth:`setLabelRotation()` - """ - self.__data.scaleDraw.setLabelAlignment(alignment) - self.layoutScale() - - def setLabelRotation(self, rotation): - """ - Change the rotation for the labels. - - :param float rotation: Rotation - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelRotation()`, - :py:meth:`setLabelFlags()` - """ - self.__data.scaleDraw.setLabelRotation(rotation) - self.layoutScale() - - def setLabelAutoSize(self, state): - """ - Set the automatic size option for labels (default: on). - - :param bool state: On/off - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAutoSize()` - """ - self.__data.scaleDraw.setLabelAutoSize(state) - self.layoutScale() - - def setScaleDraw(self, scaleDraw): - """ - Set a scale draw - - scaleDraw has to be created with new and will be deleted in - class destructor or the next call of `setScaleDraw()`. - scaleDraw will be initialized with the attributes of - the previous scaleDraw object. - - :param qwt.scale_draw.QwtScaleDraw scaleDraw: ScaleDraw object - - .. seealso:: - - :py:meth:`scaleDraw()` - """ - if scaleDraw is None or scaleDraw == self.__data.scaleDraw: - return - sd = self.__data.scaleDraw - if sd is not None: - scaleDraw.setAlignment(sd.alignment()) - scaleDraw.setScaleDiv(sd.scaleDiv()) - transform = None - if sd.scaleMap().transformation(): - transform = sd.scaleMap().transformation().copy() - scaleDraw.setTransformation(transform) - self.__data.scaleDraw = scaleDraw - self.layoutScale() - - def scaleDraw(self): - """ - :return: scaleDraw of this scale - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtScaleDraw.setScaleDraw()` - """ - return self.__data.scaleDraw - - def title(self): - """ - :return: title - - .. seealso:: - - :py:meth:`setTitle` - """ - return self.__data.title - - def startBorderDist(self): - """ - :return: start border distance - - .. seealso:: - - :py:meth:`setBorderDist` - """ - return self.__data.borderDist[0] - - def endBorderDist(self): - """ - :return: end border distance - - .. seealso:: - - :py:meth:`setBorderDist` - """ - return self.__data.borderDist[1] - - def margin(self): - """ - :return: margin - - .. seealso:: - - :py:meth:`setMargin` - """ - return self.__data.margin - - def spacing(self): - """ - :return: distance between scale and title - - .. seealso:: - - :py:meth:`setSpacing` - """ - return self.__data.spacing - - def paintEvent(self, event): - painter = QPainter(self) - painter.setClipRegion(event.region()) - opt = QStyleOption() - opt.initFrom(self) - self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) - self.draw(painter) - - def draw(self, painter): - """ - Draw the scale - - :param QPainter painter: Painter - """ - self.__data.scaleDraw.draw(painter, self.palette()) - if ( - self.__data.colorBar.isEnabled - and self.__data.colorBar.width > 0 - and self.__data.colorBar.interval.isValid() - ): - self.drawColorBar(painter, self.colorBarRect(self.contentsRect())) - - r = QRectF(self.contentsRect()) - if self.__data.scaleDraw.orientation() == Qt.Horizontal: - r.setLeft(r.left() + self.__data.borderDist[0]) - r.setWidth(r.width() - self.__data.borderDist[1]) - else: - r.setTop(r.top() + self.__data.borderDist[0]) - r.setHeight(r.height() - self.__data.borderDist[1]) - - if not self.__data.title.isEmpty(): - self.drawTitle(painter, self.__data.scaleDraw.alignment(), r) - - def colorBarRect(self, rect): - """ - Calculate the the rectangle for the color bar - - :param QRectF rect: Bounding rectangle for all components of the scale - :return: Rectangle for the color bar - """ - cr = QRectF(rect) - if self.__data.scaleDraw.orientation() == Qt.Horizontal: - cr.setLeft(cr.left() + self.__data.borderDist[0]) - cr.setWidth(cr.width() - self.__data.borderDist[1] + 1) - else: - cr.setTop(cr.top() + self.__data.borderDist[0]) - cr.setHeight(cr.height() - self.__data.borderDist[1] + 1) - sda = self.__data.scaleDraw.alignment() - if sda == QwtScaleDraw.LeftScale: - cr.setLeft(cr.right() - self.__data.margin - self.__data.colorBar.width) - cr.setWidth(self.__data.colorBar.width) - elif sda == QwtScaleDraw.RightScale: - cr.setLeft(cr.left() + self.__data.margin) - cr.setWidth(self.__data.colorBar.width) - elif sda == QwtScaleDraw.BottomScale: - cr.setTop(cr.top() + self.__data.margin) - cr.setHeight(self.__data.colorBar.width) - elif sda == QwtScaleDraw.TopScale: - cr.setTop(cr.bottom() - self.__data.margin - self.__data.colorBar.width) - cr.setHeight(self.__data.colorBar.width) - return cr - - def resizeEvent(self, event): - self.layoutScale(False) - - def layoutScale(self, update_geometry=True): - """ - Recalculate the scale's geometry and layout based on - the current geometry and fonts. - - :param bool update_geometry: Notify the layout system and call update to redraw the scale - """ - bd0, bd1 = self.getBorderDistHint() - if self.__data.borderDist[0] > bd0: - bd0 = self.__data.borderDist[0] - if self.__data.borderDist[1] > bd1: - bd1 = self.__data.borderDist[1] - - colorBarWidth = 0 - if self.__data.colorBar.isEnabled and self.__data.colorBar.interval.isValid(): - colorBarWidth = self.__data.colorBar.width + self.__data.spacing - - r = self.contentsRect() - if self.__data.scaleDraw.orientation() == Qt.Vertical: - y = r.top() + bd0 - length = r.height() - (bd0 + bd1) - if self.__data.scaleDraw.alignment() == QwtScaleDraw.LeftScale: - x = r.right() - 1.0 - self.__data.margin - colorBarWidth - else: - x = r.left() + self.__data.margin + colorBarWidth - else: - x = r.left() + bd0 - length = r.width() - (bd0 + bd1) - if self.__data.scaleDraw.alignment() == QwtScaleDraw.BottomScale: - y = r.top() + self.__data.margin + colorBarWidth - else: - y = r.bottom() - 1.0 - self.__data.margin - colorBarWidth - - self.__data.scaleDraw.move(x, y) - self.__data.scaleDraw.setLength(length) - - extent = math.ceil(self.__data.scaleDraw.extent(self.font())) - self.__data.titleOffset = ( - self.__data.margin + self.__data.spacing + colorBarWidth + extent - ) - - if update_geometry: - self.updateGeometry() - - # The following was removed because it caused a high CPU usage - # in guiqwt.ImageWidget. The origin of these lines was an - # attempt to transpose PythonQwt from Qwt 6.1.2 to Qwt 6.1.5. - - # --> Begin of removed lines <-------------------------------------- - # # for some reason updateGeometry does not send a LayoutRequest - # # event when the parent is not visible and has no layout - # widget = self.parentWidget() - # if widget and not widget.isVisible() and widget.layout() is None: - # if widget.testAttribute(Qt.WA_WState_Polished): - # QApplication.postEvent( - # self.parentWidget(), QEvent(QEvent.LayoutRequest) - # ) - # --> End of removed lines <---------------------------------------- - - self.update() - - def drawColorBar(self, painter, rect): - """ - Draw the color bar of the scale widget - - :param QPainter painter: Painter - :param QRectF rect: Bounding rectangle for the color bar - - .. seealso:: - - :py:meth:`setColorBarEnabled()` - """ - if not self.__data.colorBar.interval.isValid(): - return - sd = self.__data.scaleDraw - QwtPainter.drawColorBar( - painter, - self.__data.colorBar.colorMap, - self.__data.colorBar.interval.normalized(), - sd.scaleMap(), - sd.orientation(), - rect, - ) - - def drawTitle(self, painter, align, rect): - """ - Rotate and paint a title according to its position into a given rectangle. - - :param QPainter painter: Painter - :param int align: Alignment - :param QRectF rect: Bounding rectangle - """ - r = rect - flags = self.__data.title.renderFlags() & ( - ~int(Qt.AlignTop | Qt.AlignBottom | Qt.AlignVCenter) - ) - if align == QwtScaleDraw.LeftScale: - angle = -90.0 - flags |= Qt.AlignTop - r.setRect( - r.left(), r.bottom(), r.height(), r.width() - self.__data.titleOffset - ) - elif align == QwtScaleDraw.RightScale: - angle = -90.0 - flags |= Qt.AlignTop - r.setRect( - r.left() + self.__data.titleOffset, - r.bottom(), - r.height(), - r.width() - self.__data.titleOffset, - ) - elif align == QwtScaleDraw.BottomScale: - angle = 0.0 - flags |= Qt.AlignBottom - r.setTop(r.top() + self.__data.titleOffset) - else: - angle = 0.0 - flags |= Qt.AlignTop - r.setBottom(r.bottom() - self.__data.titleOffset) - - if self.__data.layoutFlags & self.TitleInverted: - if align in (QwtScaleDraw.LeftScale, QwtScaleDraw.RightScale): - angle = -angle - r.setRect(r.x() + r.height(), r.y() - r.width(), r.width(), r.height()) - - painter.save() - painter.setFont(self.font()) - painter.setPen(self.palette().color(QPalette.Text)) - - painter.translate(r.x(), r.y()) - if angle != 0.0: - painter.rotate(angle) - - title = self.__data.title - title.setRenderFlags(flags) - title.draw(painter, QRectF(0.0, 0.0, r.width(), r.height())) - - painter.restore() - - def scaleChange(self): - """ - Notify a change of the scale - - This method can be overloaded by derived classes. The default - implementation updates the geometry and repaints the widget. - """ - self.layoutScale() - - def sizeHint(self): - return self.minimumSizeHint() - - def minimumSizeHint(self): - o = self.__data.scaleDraw.orientation() - length = 0 - mbd1, mbd2 = self.getBorderDistHint() - length += max([0, self.__data.borderDist[0] - mbd1]) - length += max([0, self.__data.borderDist[1] - mbd2]) - length += self.__data.scaleDraw.minLength(self.font()) - - dim = self.dimForLength(length, self.font()) - if length < dim: - length = dim - dim = self.dimForLength(length, self.font()) - - size = QSize(length + 2, dim) - if o == Qt.Vertical: - size.transpose() - - if self.layout() is None: - left, top, right, bottom = 0, 0, 0, 0 - else: - mgn = self.layout().contentsMargins() - left, top, right, bottom = ( - mgn.left(), - mgn.top(), - mgn.right(), - mgn.bottom(), - ) - return size + QSize(left + right, top + bottom) - - def titleHeightForWidth(self, width): - """ - Find the height of the title for a given width. - - :param int width: Width - :return: Height - """ - return math.ceil(self.__data.title.heightForWidth(width, self.font())) - - def dimForLength(self, length, scaleFont): - """ - Find the minimum dimension for a given length. - dim is the height, length the width seen in direction of the title. - - :param int length: width for horizontal, height for vertical scales - :param QFont scaleFont: Font of the scale - :return: height for horizontal, width for vertical scales - """ - extent = math.ceil(self.__data.scaleDraw.extent(scaleFont)) - dim = self.__data.margin + extent + 1 - if not self.__data.title.isEmpty(): - dim += self.titleHeightForWidth(length) + self.__data.spacing - if self.__data.colorBar.isEnabled and self.__data.colorBar.interval.isValid(): - dim += self.__data.colorBar.width + self.__data.spacing - return dim - - def getBorderDistHint(self): - """ - Calculate a hint for the border distances. - - This member function calculates the distance - of the scale's endpoints from the widget borders which - is required for the mark labels to fit into the widget. - The maximum of this distance an the minimum border distance - is returned. - - :param int start: Return parameter for the border width at the beginning of the scale - :param int end: Return parameter for the border width at the end of the scale - - .. warning:: - - The minimum border distance depends on the font. - - .. seealso:: - - :py:meth:`setMinBorderDist()`, :py:meth:`getMinBorderDist()`, - :py:meth:`setBorderDist()` - """ - start, end = self.__data.scaleDraw.getBorderDistHint(self.font()) - if start < self.__data.minBorderDist[0]: - start = self.__data.minBorderDist[0] - if end < self.__data.minBorderDist[1]: - end = self.__data.minBorderDist[1] - return start, end - - def setMinBorderDist(self, start, end): - """ - Set a minimum value for the distances of the scale's endpoints from - the widget borders. This is useful to avoid that the scales - are "jumping", when the tick labels or their positions change - often. - - :param int start: Minimum for the start border - :param int end: Minimum for the end border - - .. seealso:: - - :py:meth:`getMinBorderDist()`, :py:meth:`getBorderDistHint()` - """ - self.__data.minBorderDist = [start, end] - - def getMinBorderDist(self): - """ - Get the minimum value for the distances of the scale's endpoints from - the widget borders. - - :param int start: Return parameter for the border width at the beginning of the scale - :param int end: Return parameter for the border width at the end of the scale - - .. seealso:: - - :py:meth:`setMinBorderDist()`, :py:meth:`getBorderDistHint()` - """ - return self.__data.minBorderDist - - def setScaleDiv(self, scaleDiv): - """ - Assign a scale division - - The scale division determines where to set the tick marks. - - :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale Division - - .. seealso:: - - For more information about scale divisions, - see :py:class:`qwt.scale_div.QwtScaleDiv`. - """ - sd = self.__data.scaleDraw - if sd.scaleDiv() != scaleDiv: - sd.setScaleDiv(scaleDiv) - self.layoutScale() - self.scaleDivChanged.emit() - - def setTransformation(self, transformation): - """ - Set the transformation - - :param qwt.transform.Transform transformation: Transformation - - .. seealso:: - - :py:meth:`qwt.scale_draw.QwtAbstractScaleDraw.scaleDraw()`, - :py:class:`qwt.scale_map.QwtScaleMap` - """ - self.__data.scaleDraw.setTransformation(transformation) - self.layoutScale() - - def setColorBarEnabled(self, on): - """ - En/disable a color bar associated to the scale - - :param bool on: On/Off - - .. seealso:: - - :py:meth:`isColorBarEnabled()`, :py:meth:`setColorBarWidth()` - """ - if on != self.__data.colorBar.isEnabled: - self.__data.colorBar.isEnabled = on - self.layoutScale() - - def isColorBarEnabled(self): - """ - :return: True, when the color bar is enabled - - .. seealso:: - - :py:meth:`setColorBarEnabled()`, :py:meth:`setColorBarWidth()` - """ - return self.__data.colorBar.isEnabled - - def setColorBarWidth(self, width): - """ - Set the width of the color bar - - :param int width: Width - - .. seealso:: - - :py:meth:`colorBarWidth()`, :py:meth:`setColorBarEnabled()` - """ - if width != self.__data.colorBar.width: - self.__data.colorBar.width = width - if self.isColorBarEnabled(): - self.layoutScale() - - def colorBarWidth(self): - """ - :return: Width of the color bar - - .. seealso:: - - :py:meth:`setColorBarWidth()`, :py:meth:`setColorBarEnabled()` - """ - return self.__data.colorBar.width - - def colorBarInterval(self): - """ - :return: Value interval for the color bar - - .. seealso:: - - :py:meth:`setColorMap()`, :py:meth:`colorMap()` - """ - return self.__data.colorBar.interval - - def setColorMap(self, interval, colorMap): - """ - Set the color map and value interval, that are used for displaying - the color bar. - - :param qwt.interval.QwtInterval interval: Value interval - :param qwt.color_map.QwtColorMap colorMap: Color map - - .. seealso:: - - :py:meth:`colorMap()`, :py:meth:`colorBarInterval()` - """ - self.__data.colorBar.interval = interval - if colorMap != self.__data.colorBar.colorMap: - self.__data.colorBar.colorMap = colorMap - if self.isColorBarEnabled(): - self.layoutScale() - - def colorMap(self): - """ - :return: Color map - - .. seealso:: - - :py:meth:`setColorMap()`, :py:meth:`colorBarInterval()` - """ - return self.__data.colorBar.colorMap diff --git a/qwt/symbol.py b/qwt/symbol.py deleted file mode 100644 index 22f7ccb..0000000 --- a/qwt/symbol.py +++ /dev/null @@ -1,1272 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -QwtSymbol ---------- - -.. autoclass:: QwtSymbol - :members: -""" - -import math - -from qtpy.QtCore import ( - QLineF, - QObject, - QPoint, - QPointF, - QRect, - QRectF, - QSize, - QSizeF, - Qt, -) -from qtpy.QtGui import ( - QBrush, - QPainter, - QPen, - QPixmap, - QPolygonF, - QTransform, -) -from qtpy.QtSvg import QSvgRenderer - -from qwt.graphic import QwtGraphic - - -class QwtTriangle(object): - # enum Type - Left, Right, Up, Down = list(range(4)) - - -def qwtPathGraphic(path, pen, brush): - graphic = QwtGraphic() - graphic.setRenderHint(QwtGraphic.RenderPensUnscaled) - painter = QPainter(graphic) - painter.setPen(pen) - painter.setBrush(brush) - painter.drawPath(path) - painter.end() - return graphic - - -def qwtScaleBoundingRect(graphic, size): - scaledSize = QSize(size) - if scaledSize.isEmpty(): - scaledSize = graphic.defaultSize() - sz = graphic.controlPointRect().size() - sx = 1.0 - if sz.width() > 0.0: - sx = scaledSize.width() / sz.width() - sy = 1.0 - if sz.height() > 0.0: - sy = scaledSize.height() / sz.height() - return graphic.scaledBoundingRect(sx, sy) - - -def qwtDrawPixmapSymbols(painter, points, symbol): - size = symbol.size() - if size.isEmpty(): - size = symbol.pixmap().size() - transform = QTransform(painter.transform()) - if transform.isScaling(): - r = QRect(0, 0, size.width(), size.height()) - size = transform.mapRect(r).size() - pm = QPixmap(symbol.pixmap()) - if pm.size() != size: - pm = pm.scaled(size) - pinPoint = QPointF(0.5 * size.width(), 0.5 * size.height()) - if symbol.isPinPointEnabled(): - pinPoint = symbol.pinPoint() - painter.resetTransform() - for pos in points: - pos = QPointF(transform.map(pos)) - pinPoint - painter.drawPixmap(QRect(pos.toPoint(), pm.size()), pm) - - -def qwtDrawSvgSymbols(painter, points, renderer, symbol): - if renderer is None or not renderer.isValid(): - return - viewBox = QRectF(renderer.viewBoxF()) - if viewBox.isEmpty(): - return - sz = QSizeF(symbol.size()) - if not sz.isValid(): - sz = viewBox.size() - sx = sz.width() / viewBox.width() - sy = sz.height() / viewBox.height() - pinPoint = QPointF(viewBox.center()) - if symbol.isPinPointEnabled(): - pinPoint = symbol.pinPoint() - dx = sx * (pinPoint.x() - viewBox.left()) - dy = sy * (pinPoint.y() - viewBox.top()) - for pos in points: - x = pos.x() - dx - y = pos.y() - dy - renderer.render(painter, QRectF(x, y, sz.width(), sz.height())) - - -def qwtDrawGraphicSymbols(painter, points, graphic, symbol): - pointRect = QRectF(graphic.controlPointRect()) - if pointRect.isEmpty(): - return - sx = 1.0 - sy = 1.0 - sz = symbol.size() - if sz.isValid(): - sx = sz.width() / pointRect.width() - sy = sz.height() / pointRect.height() - pinPoint = QPointF(pointRect.center()) - if symbol.isPinPointEnabled(): - pinPoint = symbol.pinPoint() - transform = QTransform(painter.transform()) - for pos in points: - tr = QTransform(transform) - tr.translate(pos.x(), pos.y()) - tr.scale(sx, sy) - tr.translate(-pinPoint.x(), -pinPoint.y()) - painter.setTransform(tr) - graphic.render(painter) - painter.setTransform(transform) - - -def qwtDrawEllipseSymbols(painter, points, symbol): - painter.setBrush(symbol.brush()) - painter.setPen(symbol.pen()) - size = symbol.size() - sw = size.width() - sh = size.height() - sw2 = 0.5 * size.width() - sh2 = 0.5 * size.height() - for pos in points: - x = pos.x() - y = pos.y() - r = QRectF(x - sw2, y - sh2, sw, sh) - painter.drawEllipse(r) - - -def qwtDrawRectSymbols(painter, points, symbol): - size = symbol.size() - pen = QPen(symbol.pen()) - pen.setJoinStyle(Qt.MiterJoin) - painter.setPen(pen) - painter.setBrush(symbol.brush()) - painter.setRenderHint(QPainter.Antialiasing, False) - sw = size.width() - sh = size.height() - sw2 = 0.5 * size.width() - sh2 = 0.5 * size.height() - for pos in points: - x = pos.x() - y = pos.y() - r = QRectF(x - sw2, y - sh2, sw, sh) - painter.drawRect(r) - - -def qwtDrawDiamondSymbols(painter, points, symbol): - size = symbol.size() - pen = QPen(symbol.pen()) - pen.setJoinStyle(Qt.MiterJoin) - painter.setPen(pen) - painter.setBrush(symbol.brush()) - for pos in points: - x1 = pos.x() - 0.5 * size.width() - y1 = pos.y() - 0.5 * size.height() - x2 = x1 + size.width() - y2 = y1 + size.height() - polygon = QPolygonF() - polygon.append(QPointF(pos.x(), y1)) - polygon.append(QPointF(x1, pos.y())) - polygon.append(QPointF(pos.x(), y2)) - polygon.append(QPointF(x2, pos.y())) - painter.drawPolygon(polygon) - - -def qwtDrawTriangleSymbols(painter, type, points, symbol): - size = symbol.size() - pen = QPen(symbol.pen()) - pen.setJoinStyle(Qt.MiterJoin) - painter.setPen(pen) - painter.setBrush(symbol.brush()) - sw2 = 0.5 * size.width() - sh2 = 0.5 * size.height() - for pos in points: - x = pos.x() - y = pos.y() - x1 = x - sw2 - x2 = x1 + size.width() - y1 = y - sh2 - y2 = y1 + size.height() - if type == QwtTriangle.Left: - triangle = [QPointF(x2, y1), QPointF(x1, y), QPointF(x2, y2)] - elif type == QwtTriangle.Right: - triangle = [QPointF(x1, y1), QPointF(x2, y), QPointF(x1, y2)] - elif type == QwtTriangle.Up: - triangle = [QPointF(x1, y2), QPointF(x, y1), QPointF(x2, y2)] - elif type == QwtTriangle.Down: - triangle = [QPointF(x1, y1), QPointF(x, y2), QPointF(x2, y1)] - else: - raise TypeError("Unknown triangle type %s" % type) - painter.drawPolygon(QPolygonF(triangle)) - - -def qwtDrawLineSymbols(painter, orientations, points, symbol): - size = symbol.size() - pen = QPen(symbol.pen()) - if pen.width() > 1: - pen.setCapStyle(Qt.FlatCap) - painter.setPen(pen) - painter.setRenderHint(QPainter.Antialiasing, False) - sw = size.width() - sh = size.height() - sw2 = 0.5 * size.width() - sh2 = 0.5 * size.height() - for pos in points: - if orientations & Qt.Horizontal: - x = round(pos.x()) - sw2 - y = round(pos.y()) - painter.drawLine(QLineF(x, y, x + sw, y)) - if orientations & Qt.Vertical: - x = round(pos.x()) - y = round(pos.y()) - sh2 - painter.drawLine(QLineF(x, y, x, y + sh)) - - -def qwtDrawXCrossSymbols(painter, points, symbol): - size = symbol.size() - pen = QPen(symbol.pen()) - if pen.width() > 1: - pen.setCapStyle(Qt.FlatCap) - painter.setPen(pen) - sw = size.width() - sh = size.height() - sw2 = 0.5 * size.width() - sh2 = 0.5 * size.height() - for pos in points: - x1 = pos.x() - sw2 - x2 = x1 + sw - y1 = pos.y() - sh2 - y2 = y1 + sh - painter.drawLine(QLineF(x1, y1, x2, y2)) - painter.drawLine(QLineF(x2, y1, x1, y2)) - - -def qwtDrawStar1Symbols(painter, points, symbol): - size = symbol.size() - painter.setPen(symbol.pen()) - sqrt1_2 = math.sqrt(0.5) - r = QRectF(0, 0, size.width(), size.height()) - for pos in points: - r.moveCenter(pos) - c = QPointF(r.center()) - d1 = r.width() / 2.0 * (1.0 - sqrt1_2) - painter.drawLine( - QLineF(r.left() + d1, r.top() + d1, r.right() - d1, r.bottom() - d1) - ) - painter.drawLine( - QLineF(r.left() + d1, r.bottom() - d1, r.right() - d1, r.top() + d1) - ) - painter.drawLine(QLineF(c.x(), r.top(), c.x(), r.bottom())) - painter.drawLine(QLineF(r.left(), c.y(), r.right(), c.y())) - - -def qwtDrawStar2Symbols(painter, points, symbol): - pen = QPen(symbol.pen()) - if pen.width() > 1: - pen.setCapStyle(Qt.FlatCap) - pen.setJoinStyle(Qt.MiterJoin) - painter.setPen(pen) - painter.setBrush(symbol.brush()) - cos30 = math.cos(30 * math.pi / 180.0) - dy = 0.25 * symbol.size().height() - dx = 0.5 * symbol.size().width() * cos30 / 3.0 - for pos in points: - x = pos.x() - y = pos.y() - x1 = x - 3 * dx - y1 = y - 2 * dy - x2 = x1 + 1 * dx - x3 = x1 + 2 * dx - x4 = x1 + 3 * dx - x5 = x1 + 4 * dx - x6 = x1 + 5 * dx - x7 = x1 + 6 * dx - y2 = y1 + 1 * dy - y3 = y1 + 2 * dy - y4 = y1 + 3 * dy - y5 = y1 + 4 * dy - star = [ - QPointF(x4, y1), - QPointF(x5, y2), - QPointF(x7, y2), - QPointF(x6, y3), - QPointF(x7, y4), - QPointF(x5, y4), - QPointF(x4, y5), - QPointF(x3, y4), - QPointF(x1, y4), - QPointF(x2, y3), - QPointF(x1, y2), - QPointF(x3, y2), - ] - painter.drawPolygon(QPolygonF(star)) - - -def qwtDrawHexagonSymbols(painter, points, symbol): - painter.setBrush(symbol.brush()) - painter.setPen(symbol.pen()) - cos30 = math.cos(30 * math.pi / 180.0) - dx = 0.5 * (symbol.size().width() - cos30) - dy = 0.25 * symbol.size().height() - for pos in points: - x = pos.x() - y = pos.y() - x1 = x - dx - y1 = y - 2 * dy - x2 = x1 + 1 * dx - x3 = x1 + 2 * dx - y2 = y1 + 1 * dy - y3 = y1 + 3 * dy - y4 = y1 + 4 * dy - hexa = [ - QPointF(x2, y1), - QPointF(x3, y2), - QPointF(x3, y3), - QPointF(x2, y4), - QPointF(x1, y3), - QPointF(x1, y2), - ] - painter.drawPolygon(QPolygonF(hexa)) - - -class QwtSymbol_PrivateData(QObject): - def __init__(self, st, br, pn, sz): - QObject.__init__(self) - self.style = st - self.size = sz - self.brush = br - self.pen = pn - self.isPinPointEnabled = False - self.pinPoint = None - - class Path(object): - def __init__(self): - self.path = None # QPainterPath() - self.graphic = QwtGraphic() - - self.path = Path() - - self.pixmap = None - - class Graphic(object): - def __init__(self): - self.graphic = QwtGraphic() - - self.graphic = Graphic() - - class SVG(object): - def __init__(self): - self.renderer = QSvgRenderer() - - self.svg = SVG() - - class PaintCache(object): - def __init__(self): - self.policy = 0 - self.pixmap = None # QPixmap() - - self.cache = PaintCache() - - -class QwtSymbol(object): - """ - A class for drawing symbols - - Symbol styles: - - * `QwtSymbol.NoSymbol`: No Style. The symbol cannot be drawn. - * `QwtSymbol.Ellipse`: Ellipse or circle - * `QwtSymbol.Rect`: Rectangle - * `QwtSymbol.Diamond`: Diamond - * `QwtSymbol.Triangle`: Triangle pointing upwards - * `QwtSymbol.DTriangle`: Triangle pointing downwards - * `QwtSymbol.UTriangle`: Triangle pointing upwards - * `QwtSymbol.LTriangle`: Triangle pointing left - * `QwtSymbol.RTriangle`: Triangle pointing right - * `QwtSymbol.Cross`: Cross (+) - * `QwtSymbol.XCross`: Diagonal cross (X) - * `QwtSymbol.HLine`: Horizontal line - * `QwtSymbol.VLine`: Vertical line - * `QwtSymbol.Star1`: X combined with + - * `QwtSymbol.Star2`: Six-pointed star - * `QwtSymbol.Hexagon`: Hexagon - * `QwtSymbol.Path`: The symbol is represented by a painter path, where - the origin (0, 0) of the path coordinate system is mapped to the - position of the symbol - - ..seealso:: - - :py:meth:`setPath()`, :py:meth:`path()` - * `QwtSymbol.Pixmap`: The symbol is represented by a pixmap. - The pixmap is centered or aligned to its pin point. - - ..seealso:: - - :py:meth:`setPinPoint()` - * `QwtSymbol.Graphic`: The symbol is represented by a graphic. - The graphic is centered or aligned to its pin point. - - ..seealso:: - - :py:meth:`setPinPoint()` - * `QwtSymbol.SvgDocument`: The symbol is represented by a SVG graphic. - The graphic is centered or aligned to its pin point. - - ..seealso:: - - :py:meth:`setPinPoint()` - * `QwtSymbol.UserStyle`: Styles >= `QwtSymbol.UserStyle` are reserved - for derived classes of `QwtSymbol` that overload `drawSymbols()` with - additional application specific symbol types. - - Cache policies: - - Depending on the render engine and the complexity of the - symbol shape it might be faster to render the symbol - to a pixmap and to paint this pixmap. - - F.e. the raster paint engine is a pure software renderer - where in cache mode a draw operation usually ends in - raster operation with the the backing store, that are usually - faster, than the algorithms for rendering polygons. - But the opposite can be expected for graphic pipelines - that can make use of hardware acceleration. - - The default setting is AutoCache - - ..seealso:: - - :py:meth:`setCachePolicy()`, :py:meth:`cachePolicy()` - - .. note:: - - The policy has no effect, when the symbol is painted - to a vector graphics format (PDF, SVG). - - .. warning:: - - Since Qt 4.8 raster is the default backend on X11 - - Valid cache policies: - - * `QwtSymbol.NoCache`: Don't use a pixmap cache - * `QwtSymbol.Cache`: Always use a pixmap cache - * `QwtSymbol.AutoCache`: Use a cache when the symbol is rendered - with the software renderer (`QPaintEngine.Raster`) - - .. py:class:: QwtSymbol([style=QwtSymbol.NoSymbol]) - - The symbol is constructed with gray interior, - black outline with zero width, no size and style 'NoSymbol'. - - :param int style: Symbol Style - - .. py:class:: QwtSymbol(style, brush, pen, size) - :noindex: - - :param int style: Symbol Style - :param QBrush brush: Brush to fill the interior - :param QPen pen: Outline pen - :param QSize size: Size - - .. py:class:: QwtSymbol(path, brush, pen) - :noindex: - - :param QPainterPath path: Painter path - :param QBrush brush: Brush to fill the interior - :param QPen pen: Outline pen - - .. seealso:: - - :py:meth:`setPath()`, :py:meth:`setBrush()`, - :py:meth:`setPen()`, :py:meth:`setSize()` - """ - - # enum Style - Style = int - NoSymbol = -1 - ( - Ellipse, - Rect, - Diamond, - Triangle, - DTriangle, - UTriangle, - LTriangle, - RTriangle, - Cross, - XCross, - HLine, - VLine, - Star1, - Star2, - Hexagon, - Path, - Pixmap, - Graphic, - SvgDocument, - ) = list(range(19)) - UserStyle = 1000 - - # enum CachePolicy - NoCache, Cache, AutoCache = list(range(3)) - - def __init__(self, *args): - if len(args) in (0, 1): - if args: - (style,) = args - else: - style = QwtSymbol.NoSymbol - self.__data = QwtSymbol_PrivateData( - style, QBrush(Qt.gray), QPen(Qt.black, 0), QSize() - ) - elif len(args) == 4: - style, brush, pen, size = args - self.__data = QwtSymbol_PrivateData(style, brush, pen, size) - elif len(args) == 3: - path, brush, pen = args - self.__data = QwtSymbol_PrivateData(QwtSymbol.Path, brush, pen, QSize()) - self.setPath(path) - else: - raise TypeError( - "%s() takes 1, 3, or 4 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - @classmethod - def make( - cls, - style=None, - brush=None, - pen=None, - size=None, - path=None, - pixmap=None, - graphic=None, - svgdocument=None, - pinpoint=None, - ): - """ - Create and setup a new `QwtSymbol` object (convenience function). - - :param style: Symbol Style - :type style: int or None - :param brush: Brush to fill the interior - :type brush: QBrush or None - :param pen: Outline pen - :type pen: QPen or None - :param size: Size - :type size: QSize or None - :param path: Painter path - :type path: QPainterPath or None - :param path: Painter path - :type path: QPainterPath or None - :param pixmap: Pixmap as symbol - :type pixmap: QPixmap or None - :param graphic: Graphic - :type graphic: qwt.graphic.QwtGraphic or None - :param svgdocument: SVG icon as symbol - - .. seealso:: - - :py:meth:`setPixmap()`, :py:meth:`setGraphic()`, :py:meth:`setPath()` - """ - style = QwtSymbol.NoSymbol if style is None else style - brush = QBrush(Qt.gray) if brush is None else QBrush(brush) - pen = QPen(Qt.black, 0) if pen is None else QPen(pen) - size = QSize() if size is None else size - if not isinstance(size, QSize): - if isinstance(size, tuple) and len(size) == 2: - size = QSize(size[0], size[1]) - else: - raise TypeError("Invalid size %r" % size) - item = cls(style, brush, pen, size) - if path is not None: - item.setPath(path) - elif pixmap is not None: - item.setPixmap(pixmap) - elif graphic is not None: - item.setGraphic(graphic) - elif svgdocument is not None: - item.setSvgDocument(svgdocument) - if pinpoint is not None: - item.setPinPoint(pinpoint) - return item - - def setCachePolicy(self, policy): - """ - Change the cache policy - - The default policy is AutoCache - - :param int policy: Cache policy - - .. seealso:: - - :py:meth:`cachePolicy()` - """ - if self.__data.cache.policy != policy: - self.__data.cache.policy = policy - self.invalidateCache() - - def cachePolicy(self): - """ - :return: Cache policy - - .. seealso:: - - :py:meth:`setCachePolicy()` - """ - return self.__data.cache.policy - - def setPath(self, path): - """ - Set a painter path as symbol - - The symbol is represented by a painter path, where the - origin (0, 0) of the path coordinate system is mapped to - the position of the symbol. - - When the symbol has valid size the painter path gets scaled - to fit into the size. Otherwise the symbol size depends on - the bounding rectangle of the path. - - The following code defines a symbol drawing an arrow:: - - from qtpy.QtGui import QApplication, QPen, QPainterPath, QTransform - from qtpy.QtCore import Qt, QPointF - from qwt import QwtPlot, QwtPlotCurve, QwtSymbol - import numpy as np - - app = QApplication([]) - - # --- Construct custom symbol --- - - path = QPainterPath() - path.moveTo(0, 8) - path.lineTo(0, 5) - path.lineTo(-3, 5) - path.lineTo(0, 0) - path.lineTo(3, 5) - path.lineTo(0, 5) - - transform = QTransform() - transform.rotate(-30.0) - path = transform.map(path) - - pen = QPen(Qt.black, 2 ); - pen.setJoinStyle(Qt.MiterJoin) - - symbol = QwtSymbol() - symbol.setPen(pen) - symbol.setBrush(Qt.red) - symbol.setPath(path) - symbol.setPinPoint(QPointF(0., 0.)) - symbol.setSize(10, 14) - - # --- Test it within a simple plot --- - - curve = QwtPlotCurve() - curve_pen = QPen(Qt.blue) - curve_pen.setStyle(Qt.DotLine) - curve.setPen(curve_pen) - curve.setSymbol(symbol) - x = np.linspace(0, 10, 10) - curve.setData(x, np.sin(x)) - - plot = QwtPlot() - curve.attach(plot) - plot.resize(600, 300) - plot.replot() - plot.show() - - app.exec_() - - .. image:: /_static/symbol_path_example.png - - :param QPainterPath path: Painter path - - .. seealso:: - - :py:meth:`path()`, :py:meth:`setSize()` - """ - self.__data.style = QwtSymbol.Path - self.__data.path.path = path - self.__data.path.graphic.reset() - - def path(self): - """ - :return: Painter path for displaying the symbol - - .. seealso:: - - :py:meth:`setPath()` - """ - return self.__data.path.path - - def setPixmap(self, pixmap): - """ - Set a pixmap as symbol - - :param QPixmap pixmap: Pixmap - - .. seealso:: - - :py:meth:`pixmap()`, :py:meth:`setGraphic()` - - .. note:: - - The `style()` is set to `QwtSymbol.Pixmap` - - .. note:: - - `brush()` and `pen()` have no effect - """ - self.__data.style = QwtSymbol.Pixmap - self.__data.pixmap = pixmap - - def pixmap(self): - """ - :return: Assigned pixmap - - .. seealso:: - - :py:meth:`setPixmap()` - """ - if self.__data.pixmap is None: - return QPixmap() - return self.__data.pixmap - - def setGraphic(self, graphic): - """ - Set a graphic as symbol - - :param qwt.graphic.QwtGraphic graphic: Graphic - - .. seealso:: - - :py:meth:`graphic()`, :py:meth:`setPixmap()` - - .. note:: - - The `style()` is set to `QwtSymbol.Graphic` - - .. note:: - - `brush()` and `pen()` have no effect - """ - self.__data.style = QwtSymbol.Graphic - self.__data.graphic.graphic = graphic - - def graphic(self): - """ - :return: Assigned graphic - - .. seealso:: - - :py:meth:`setGraphic()` - """ - return self.__data.graphic.graphic - - def setSvgDocument(self, svgDocument): - """ - Set a SVG icon as symbol - - :param svgDocument: SVG icon - - .. seealso:: - - :py:meth:`setGraphic()`, :py:meth:`setPixmap()` - - .. note:: - - The `style()` is set to `QwtSymbol.SvgDocument` - - .. note:: - - `brush()` and `pen()` have no effect - """ - self.__data.style = QwtSymbol.SvgDocument - if self.__data.svg.renderer is None: - self.__data.svg.renderer = QSvgRenderer() - self.__data.svg.renderer.load(svgDocument) - - def setSize(self, *args): - """ - Specify the symbol's size - - .. py:method:: setSize(width, [height=-1]) - :noindex: - - :param int width: Width - :param int height: Height - - .. py:method:: setSize(size) - :noindex: - - :param QSize size: Size - - .. seealso:: - - :py:meth:`size()` - """ - if len(args) == 2: - width, height = args - if width >= 0 and height < 0: - height = width - self.setSize(QSize(width, height)) - elif len(args) == 1: - if isinstance(args[0], QSize): - (size,) = args - if size.isValid() and size != self.__data.size: - self.__data.size = size - self.invalidateCache() - else: - (width,) = args - self.setSize(width, -1) - else: - raise TypeError( - "%s().setSize() takes 1 or 2 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def size(self): - """ - :return: Size - - .. seealso:: - - :py:meth:`setSize()` - """ - return self.__data.size - - def setBrush(self, brush): - """ - Assign a brush - - The brush is used to draw the interior of the symbol. - - :param QBrush brush: Brush - - .. seealso:: - - :py:meth:`brush()` - """ - if brush != self.__data.brush: - self.__data.brush = brush - self.invalidateCache() - if self.__data.style == QwtSymbol.Path: - self.__data.path.graphic.reset() - - def brush(self): - """ - :return: Brush - - .. seealso:: - - :py:meth:`setBrush()` - """ - return self.__data.brush - - def setPen(self, *args): - """ - Build and/or assign a pen, depending on the arguments. - - .. py:method:: setPen(color, width, style) - :noindex: - - Build and assign a pen - - In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it - non cosmetic (see `QPen.isCosmetic()`). This method signature has - been introduced to hide this incompatibility. - - :param QColor color: Pen color - :param float width: Pen width - :param Qt.PenStyle style: Pen style - - .. py:method:: setPen(pen) - :noindex: - - Assign a pen - - :param QPen pen: New pen - - .. seealso:: - - :py:meth:`pen()`, :py:meth:`brush()` - """ - if len(args) == 3: - color, width, style = args - self.setPen(QPen(color, width, style)) - elif len(args) == 1: - (pen,) = args - if pen != self.__data.pen: - self.__data.pen = pen - self.invalidateCache() - if self.__data.style == QwtSymbol.Path: - self.__data.path.graphic.reset() - else: - raise TypeError( - "%s().setPen() takes 1 or 3 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - - def pen(self): - """ - :return: Pen - - .. seealso:: - - :py:meth:`setPen()`, :py:meth:`brush()` - """ - return self.__data.pen - - def setColor(self, color): - """ - Set the color of the symbol - - Change the color of the brush for symbol types with a filled area. - For all other symbol types the color will be assigned to the pen. - - :param QColor color: Color - - .. seealso:: - - :py:meth:`setPen()`, :py:meth:`setBrush()`, - :py:meth:`brush()`, :py:meth:`pen()` - """ - if self.__data.style in ( - QwtSymbol.Ellipse, - QwtSymbol.Rect, - QwtSymbol.Diamond, - QwtSymbol.Triangle, - QwtSymbol.UTriangle, - QwtSymbol.DTriangle, - QwtSymbol.RTriangle, - QwtSymbol.LTriangle, - QwtSymbol.Star2, - QwtSymbol.Hexagon, - ): - if self.__data.brush.color() != color: - self.__data.brush.setColor(color) - self.invalidateCache() - elif self.__data.style in ( - QwtSymbol.Cross, - QwtSymbol.XCross, - QwtSymbol.HLine, - QwtSymbol.VLine, - QwtSymbol.Star1, - ): - if self.__data.pen.color() != color: - self.__data.pen.setColor(color) - self.invalidateCache() - else: - if self.__data.brush.color() != color or self.__data.pen.color() != color: - self.invalidateCache() - self.__data.brush.setColor(color) - self.__data.pen.setColor(color) - - def setPinPoint(self, pos, enable=True): - """ - Set and enable a pin point - - The position of a complex symbol is not always aligned to its center - ( f.e an arrow, where the peak points to a position ). The pin point - defines the position inside of a Pixmap, Graphic, SvgDocument - or PainterPath symbol where the represented point has to - be aligned to. - - :param QPointF pos: Position - :enable bool enable: En/Disable the pin point alignment - - .. seealso:: - - :py:meth:`pinPoint()`, :py:meth:`setPinPointEnabled()` - """ - if self.__data.pinPoint != pos: - self.__data.pinPoint = pos - if self.__data.isPinPointEnabled: - self.invalidateCache() - self.setPinPointEnabled(enable) - - def pinPoint(self): - """ - :return: Pin point - - .. seealso:: - - :py:meth:`setPinPoint()`, :py:meth:`setPinPointEnabled()` - """ - return self.__data.pinPoint - - def setPinPointEnabled(self, on): - """ - En/Disable the pin point alignment - - :param bool on: Enabled, when on is true - - .. seealso:: - - :py:meth:`setPinPoint()`, :py:meth:`isPinPointEnabled()` - """ - if self.__data.isPinPointEnabled != on: - self.__data.isPinPointEnabled = on - self.invalidateCache() - - def isPinPointEnabled(self): - """ - :return: True, when the pin point translation is enabled - - .. seealso:: - - :py:meth:`setPinPoint()`, :py:meth:`setPinPointEnabled()` - """ - return self.__data.isPinPointEnabled - - def drawSymbols(self, painter, points): - """ - Render an array of symbols - - Painting several symbols is more effective than drawing symbols - one by one, as a couple of layout calculations and setting of pen/brush - can be done once for the complete array. - - :param QPainter painter: Painter - :param QPolygonF points: Positions of the symbols in screen coordinates - """ - painter.save() - self.renderSymbols(painter, points) - painter.restore() - - def drawSymbol(self, painter, point_or_rect): - """ - Draw the symbol into a rectangle - - The symbol is painted centered and scaled into the target rectangle. - It is always painted uncached and the pin point is ignored. - - This method is primarily intended for drawing a symbol to the legend. - - :param QPainter painter: Painter - :param point_or_rect: Position or target rectangle of the symbol in screen coordinates - :type point_or_rect: QPointF or QPoint or QRectF - """ - if isinstance(point_or_rect, (QPointF, QPoint)): - # drawSymbol( QPainter *, const QPointF & ) - self.drawSymbols(painter, [point_or_rect]) - return - # drawSymbol( QPainter *, const QRectF & ) - rect = point_or_rect - assert isinstance(rect, QRectF) - if self.__data.style == QwtSymbol.NoSymbol: - return - if self.__data.style == QwtSymbol.Graphic: - self.__data.graphic.graphic.render(painter, rect, Qt.KeepAspectRatio) - elif self.__data.style == QwtSymbol.Path: - if self.__data.path.graphic.isNull(): - self.__data.path.graphic = qwtPathGraphic( - self.__data.path.path, self.__data.pen, self.__data.brush - ) - self.__data.path.graphic.render(painter, rect, Qt.KeepAspectRatio) - return - elif self.__data.style == QwtSymbol.SvgDocument: - if self.__data.svg.renderer is not None: - scaledRect = QRectF() - sz = QSizeF(self.__data.svg.renderer.viewBoxF().size()) - if not sz.isEmpty(): - sz.scale(rect.size(), Qt.KeepAspectRatio) - scaledRect.setSize(sz) - scaledRect.moveCenter(rect.center()) - else: - scaledRect = rect - self.__data.svg.renderer.render(painter, scaledRect) - else: - br = QRect(self.boundingRect()) - ratio = min([rect.width() / br.width(), rect.height() / br.height()]) - painter.save() - painter.translate(rect.center()) - painter.scale(ratio, ratio) - isPinPointEnabled = self.__data.isPinPointEnabled - self.__data.isPinPointEnabled = False - self.renderSymbols(painter, [QPointF()]) - self.__data.isPinPointEnabled = isPinPointEnabled - painter.restore() - - def renderSymbols(self, painter, points): - """ - Render the symbol to series of points - - :param QPainter painter: Painter - :param point_or_rect: Positions of the symbols - """ - if self.__data.style == QwtSymbol.Ellipse: - qwtDrawEllipseSymbols(painter, points, self) - elif self.__data.style == QwtSymbol.Rect: - qwtDrawRectSymbols(painter, points, self) - elif self.__data.style == QwtSymbol.Diamond: - qwtDrawDiamondSymbols(painter, points, self) - elif self.__data.style == QwtSymbol.Cross: - qwtDrawLineSymbols(painter, Qt.Horizontal | Qt.Vertical, points, self) - elif self.__data.style == QwtSymbol.XCross: - qwtDrawXCrossSymbols(painter, points, self) - elif self.__data.style in (QwtSymbol.Triangle, QwtSymbol.UTriangle): - qwtDrawTriangleSymbols(painter, QwtTriangle.Up, points, self) - elif self.__data.style == QwtSymbol.DTriangle: - qwtDrawTriangleSymbols(painter, QwtTriangle.Down, points, self) - elif self.__data.style == QwtSymbol.RTriangle: - qwtDrawTriangleSymbols(painter, QwtTriangle.Right, points, self) - elif self.__data.style == QwtSymbol.LTriangle: - qwtDrawTriangleSymbols(painter, QwtTriangle.Left, points, self) - elif self.__data.style == QwtSymbol.HLine: - qwtDrawLineSymbols(painter, Qt.Horizontal, points, self) - elif self.__data.style == QwtSymbol.VLine: - qwtDrawLineSymbols(painter, Qt.Vertical, points, self) - elif self.__data.style == QwtSymbol.Star1: - qwtDrawStar1Symbols(painter, points, self) - elif self.__data.style == QwtSymbol.Star2: - qwtDrawStar2Symbols(painter, points, self) - elif self.__data.style == QwtSymbol.Hexagon: - qwtDrawHexagonSymbols(painter, points, self) - elif self.__data.style == QwtSymbol.Path: - if self.__data.path.graphic.isNull(): - self.__data.path.graphic = qwtPathGraphic( - self.__data.path.path, self.__data.pen, self.__data.brush - ) - qwtDrawGraphicSymbols(painter, points, self.__data.path.graphic, self) - elif self.__data.style == QwtSymbol.Pixmap: - qwtDrawPixmapSymbols(painter, points, self) - elif self.__data.style == QwtSymbol.Graphic: - qwtDrawGraphicSymbols(painter, points, self.__data.graphic.graphic, self) - elif self.__data.style == QwtSymbol.SvgDocument: - qwtDrawSvgSymbols(painter, points, self.__data.svg.renderer, self) - - def boundingRect(self): - """ - Calculate the bounding rectangle for a symbol at position (0,0). - - :return: Bounding rectangle - """ - rect = QRectF() - pinPointTranslation = False - if self.__data.style in (QwtSymbol.Ellipse, QwtSymbol.Rect, QwtSymbol.Hexagon): - pw = 0.0 - if self.__data.pen.style() != Qt.NoPen: - pw = max([self.__data.pen.widthF(), 1.0]) - rect.setSize(QSizeF(self.__data.size) + QSizeF(pw, pw)) - rect.moveCenter(QPointF(0.0, 0.0)) - elif self.__data.style in ( - QwtSymbol.XCross, - QwtSymbol.Diamond, - QwtSymbol.Triangle, - QwtSymbol.UTriangle, - QwtSymbol.DTriangle, - QwtSymbol.RTriangle, - QwtSymbol.LTriangle, - QwtSymbol.Star1, - QwtSymbol.Star2, - ): - pw = 0.0 - if self.__data.pen.style() != Qt.NoPen: - pw = max([self.__data.pen.widthF(), 1.0]) - rect.setSize(QSizeF(self.__data.size) + QSizeF(2 * pw, 2 * pw)) - rect.moveCenter(QPointF(0.0, 0.0)) - elif self.__data.style == QwtSymbol.Path: - if self.__data.path.graphic.isNull(): - self.__data.path.graphic = qwtPathGraphic( - self.__data.path.path, self.__data.pen, self.__data.brush - ) - rect = qwtScaleBoundingRect(self.__data.path.graphic, self.__data.size) - pinPointTranslation = True - elif self.__data.style == QwtSymbol.Pixmap: - if self.__data.size.isEmpty(): - rect.setSize(QSizeF(self.pixmap().size())) - else: - rect.setSize(QSizeF(self.__data.size)) - pinPointTranslation = True - elif self.__data.style == QwtSymbol.Graphic: - rect = qwtScaleBoundingRect(self.__data.graphic.graphic, self.__data.size) - pinPointTranslation = True - elif self.__data.style == QwtSymbol.SvgDocument: - if self.__data.svg.renderer is not None: - rect = self.__data.svg.renderer.viewBoxF() - if self.__data.size.isValid() and not rect.isEmpty(): - sz = QSizeF(rect.size()) - sx = self.__data.size.width() / sz.width() - sy = self.__data.size.height() / sz.height() - transform = QTransform() - transform.scale(sx, sy) - rect = transform.mapRect(rect) - pinPointTranslation = True - else: - rect.setSize(QSizeF(self.__data.size)) - rect.moveCenter(QPointF(0.0, 0.0)) - if pinPointTranslation: - pinPoint = QPointF(0.0, 0.0) - if self.__data.isPinPointEnabled: - pinPoint = rect.center() - self.__data.pinPoint - rect.moveCenter(pinPoint) - r = QRect() - r.setLeft(math.floor(rect.left())) - r.setTop(math.floor(rect.top())) - r.setRight(math.floor(rect.right())) - r.setBottom(math.floor(rect.bottom())) - if self.__data.style != QwtSymbol.Pixmap: - r.adjust(-1, -1, 1, 1) - return r - - def invalidateCache(self): - """ - Invalidate the cached symbol pixmap - - The symbol invalidates its cache, whenever an attribute is changed - that has an effect ob how to display a symbol. In case of derived - classes with individual styles (>= `QwtSymbol.UserStyle`) it - might be necessary to call invalidateCache() for attributes - that are relevant for this style. - - .. seealso:: - - :py:meth:`setCachePolicy()`, :py:meth:`drawSymbols()` - """ - if self.__data.cache.pixmap is not None: - self.__data.cache.pixmap = None - - def setStyle(self, style): - """ - Specify the symbol style - - :param int style: Style - - .. seealso:: - - :py:meth:`style()` - """ - if self.__data.style != style: - self.__data.style = style - self.invalidateCache() - - def style(self): - """ - :return: Current symbol style - - .. seealso:: - - :py:meth:`setStyle()` - """ - return self.__data.style diff --git a/qwt/tests/__init__.py b/qwt/tests/__init__.py deleted file mode 100644 index 6d6348b..0000000 --- a/qwt/tests/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -""" -PythonQwt test package -====================== -""" - -from qtpy import QtCore as QC -from qtpy import QtWidgets as QW - -from qwt.tests.utils import ( - QT_API, - TestEnvironment, - TestLauncher, - run_all_tests, - take_screenshot, -) - - -def run(wait=True): - """Run PythonQwt tests or test launcher""" - app = QW.QApplication([]) - launcher = TestLauncher() - launcher.show() - test_env = TestEnvironment() - if test_env.screenshots: - print("Running PythonQwt tests and taking screenshots automatically:") - QC.QTimer.singleShot(100, lambda: take_screenshot(launcher)) - elif test_env.unattended: - print("Running PythonQwt tests in unattended mode:") - QC.QTimer.singleShot(0, QW.QApplication.instance().quit) - if QT_API == "pyside6": - app.exec() - else: - app.exec_() - launcher.close() - if test_env.unattended: - run_all_tests(wait=wait) - - -if __name__ == "__main__": - run() diff --git a/qwt/tests/comparative_benchmarks.py b/qwt/tests/comparative_benchmarks.py deleted file mode 100644 index 22c0bca..0000000 --- a/qwt/tests/comparative_benchmarks.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -""" -PyQwt5 vs. PythonQwt -==================== -""" - -import os -import os.path as osp -import subprocess -import sys -import time - - -def get_winpython_exe(rootpath, pymajor=None, pyminor=None): - """Return WinPython exe list from rootpath""" - exelist = [] - for name1 in os.listdir(rootpath): - winroot = osp.join(rootpath, name1) - if osp.isdir(winroot): - for name2 in os.listdir(winroot): - pypath = osp.join(winroot, name2, "python.exe") - if osp.isfile(pypath): - pymaj, pymin = name2[len("python-") :].split(".")[:2] - if pymajor is None or pymajor == int(pymaj): - if pyminor is None or int(pymin) >= pyminor: - exelist.append(pypath) - return exelist - - -def run_script(filename, args=None, wait=True, executable=None): - """Run Python script""" - os.environ["PYTHONPATH"] = os.pathsep.join(sys.path) - if executable is None: - executable = sys.executable - command = [executable, '"' + filename + '"'] - if args is not None: - command.append(args) - print(" ".join(command)) - proc = subprocess.Popen(" ".join(command), shell=True) - if wait: - proc.wait() - - -def main(): - for name in ( - "curvebenchmark1.py", - "curvebenchmark2.py", - ): - for executable in get_winpython_exe(r"C:\Apps", pymajor=3, pyminor=6): - filename = osp.join(osp.dirname(osp.abspath(__file__)), name) - run_script(filename, wait=False, executable=executable) - time.sleep(4) - - -if __name__ == "__main__": - # print(get_winpython_exe(r"C:\Apps", pymajor=3)) - main() diff --git a/qwt/tests/conftest.py b/qwt/tests/conftest.py deleted file mode 100644 index e60b9cd..0000000 --- a/qwt/tests/conftest.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- - -"""pytest configuration for PythonQwt package tests.""" - -import os - -import qtpy - -import qwt -from qwt.tests.utils import TestEnvironment - -# Set the unattended environment variable to 1 to avoid any user interaction -os.environ[TestEnvironment.UNATTENDED_ENV] = "1" - - -def pytest_addoption(parser): - """Add custom command line options to pytest.""" - # See this StackOverflow answer for more information: https://t.ly/9anqz - parser.addoption( - "--repeat", action="store", help="Number of times to repeat each test" - ) - parser.addoption( - "--show-windows", - action="store_true", - default=False, - help="Display Qt windows during tests (disables QT_QPA_PLATFORM=offscreen)", - ) - - -def pytest_configure(config): - """Configure pytest based on command line options.""" - if config.option.durations is None: - config.option.durations = 10 # Default to showing 10 slowest tests - if not config.getoption("--show-windows"): - os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") - - -def pytest_report_header(config): - """Add additional information to the pytest report header.""" - qtbindings_version = qtpy.PYSIDE_VERSION - if qtbindings_version is None: - qtbindings_version = qtpy.PYQT_VERSION - return [ - f"PythonQwt {qwt.__version__} [closest Qwt version: {qwt.QWT_VERSION_STR}]", - f"{qtpy.API_NAME} {qtbindings_version} [Qt version: {qtpy.QT_VERSION}]", - ] - - -def pytest_generate_tests(metafunc): - """Generate tests for the given metafunc.""" - # See this StackOverflow answer for more information: https://t.ly/9anqz - if metafunc.config.option.repeat is not None: - count = int(metafunc.config.option.repeat) - - # We're going to duplicate these tests by parametrizing them, - # which requires that each test has a fixture to accept the parameter. - # We can add a new fixture like so: - metafunc.fixturenames.append("tmp_ct") - - # Now we parametrize. This is what happens when we do e.g., - # @pytest.mark.parametrize('tmp_ct', range(count)) - # def test_foo(): pass - metafunc.parametrize("tmp_ct", range(count)) diff --git a/qwt/tests/data/PythonQwt.svg b/qwt/tests/data/PythonQwt.svg deleted file mode 100644 index 92bbe2c..0000000 --- a/qwt/tests/data/PythonQwt.svg +++ /dev/null @@ -1,484 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/qwt/tests/data/bodedemo.png b/qwt/tests/data/bodedemo.png deleted file mode 100644 index e3ca87e..0000000 Binary files a/qwt/tests/data/bodedemo.png and /dev/null differ diff --git a/qwt/tests/data/cartesian.png b/qwt/tests/data/cartesian.png deleted file mode 100644 index 449ac14..0000000 Binary files a/qwt/tests/data/cartesian.png and /dev/null differ diff --git a/qwt/tests/data/cpudemo.png b/qwt/tests/data/cpudemo.png deleted file mode 100644 index 5f5660b..0000000 Binary files a/qwt/tests/data/cpudemo.png and /dev/null differ diff --git a/qwt/tests/data/curvebenchmark1.png b/qwt/tests/data/curvebenchmark1.png deleted file mode 100644 index 261dfe9..0000000 Binary files a/qwt/tests/data/curvebenchmark1.png and /dev/null differ diff --git a/qwt/tests/data/curvebenchmark2.png b/qwt/tests/data/curvebenchmark2.png deleted file mode 100644 index 162c27f..0000000 Binary files a/qwt/tests/data/curvebenchmark2.png and /dev/null differ diff --git a/qwt/tests/data/curvedemo1.png b/qwt/tests/data/curvedemo1.png deleted file mode 100644 index 8cc3e8e..0000000 Binary files a/qwt/tests/data/curvedemo1.png and /dev/null differ diff --git a/qwt/tests/data/curvedemo2.png b/qwt/tests/data/curvedemo2.png deleted file mode 100644 index b060f67..0000000 Binary files a/qwt/tests/data/curvedemo2.png and /dev/null differ diff --git a/qwt/tests/data/data.png b/qwt/tests/data/data.png deleted file mode 100644 index f7ae40c..0000000 Binary files a/qwt/tests/data/data.png and /dev/null differ diff --git a/qwt/tests/data/errorbar.png b/qwt/tests/data/errorbar.png deleted file mode 100644 index bea587f..0000000 Binary files a/qwt/tests/data/errorbar.png and /dev/null differ diff --git a/qwt/tests/data/eventfilter.png b/qwt/tests/data/eventfilter.png deleted file mode 100644 index 5dbcc1b..0000000 Binary files a/qwt/tests/data/eventfilter.png and /dev/null differ diff --git a/qwt/tests/data/image.png b/qwt/tests/data/image.png deleted file mode 100644 index d211916..0000000 Binary files a/qwt/tests/data/image.png and /dev/null differ diff --git a/qwt/tests/data/loadtest.png b/qwt/tests/data/loadtest.png deleted file mode 100644 index 8b4b8e5..0000000 Binary files a/qwt/tests/data/loadtest.png and /dev/null differ diff --git a/qwt/tests/data/logcurve.png b/qwt/tests/data/logcurve.png deleted file mode 100644 index e81de61..0000000 Binary files a/qwt/tests/data/logcurve.png and /dev/null differ diff --git a/qwt/tests/data/mapdemo.png b/qwt/tests/data/mapdemo.png deleted file mode 100644 index 623a93e..0000000 Binary files a/qwt/tests/data/mapdemo.png and /dev/null differ diff --git a/qwt/tests/data/multidemo.png b/qwt/tests/data/multidemo.png deleted file mode 100644 index 7f7b8a7..0000000 Binary files a/qwt/tests/data/multidemo.png and /dev/null differ diff --git a/qwt/tests/data/simple.png b/qwt/tests/data/simple.png deleted file mode 100644 index 2aa8593..0000000 Binary files a/qwt/tests/data/simple.png and /dev/null differ diff --git a/qwt/tests/data/stylesheet.png b/qwt/tests/data/stylesheet.png deleted file mode 100644 index 576a43e..0000000 Binary files a/qwt/tests/data/stylesheet.png and /dev/null differ diff --git a/qwt/tests/data/symbol.svg b/qwt/tests/data/symbol.svg deleted file mode 100644 index 146b0be..0000000 --- a/qwt/tests/data/symbol.svg +++ /dev/null @@ -1,411 +0,0 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/qwt/tests/data/symbols.png b/qwt/tests/data/symbols.png deleted file mode 100644 index 17cb695..0000000 Binary files a/qwt/tests/data/symbols.png and /dev/null differ diff --git a/qwt/tests/data/testlauncher.png b/qwt/tests/data/testlauncher.png deleted file mode 100644 index df1bf76..0000000 Binary files a/qwt/tests/data/testlauncher.png and /dev/null differ diff --git a/qwt/tests/data/vertical.png b/qwt/tests/data/vertical.png deleted file mode 100644 index 21a981d..0000000 Binary files a/qwt/tests/data/vertical.png and /dev/null differ diff --git a/qwt/tests/test_backingstore.py b/qwt/tests/test_backingstore.py deleted file mode 100644 index 4a0467f..0000000 --- a/qwt/tests/test_backingstore.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- - -SHOW = False # Do not show test in GUI-based test launcher - -from qwt.tests import utils -from qwt.tests.test_simple import SimplePlot - - -class BackingStorePlot(SimplePlot): - TEST_EXPORT = False - - def __init__(self): - SimplePlot.__init__(self) - self.canvas().setPaintAttribute(self.canvas().BackingStore, True) - - -def test_backingstore(): - """Test for backing store""" - utils.test_widget(BackingStorePlot, size=(600, 400)) - - -if __name__ == "__main__": - test_backingstore() diff --git a/qwt/tests/test_bodedemo.py b/qwt/tests/test_bodedemo.py deleted file mode 100644 index 2bbff41..0000000 --- a/qwt/tests/test_bodedemo.py +++ /dev/null @@ -1,304 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import os - -import numpy as np -from qtpy.QtCore import Qt -from qtpy.QtGui import QFont, QIcon, QPageLayout, QPen, QPixmap -from qtpy.QtPrintSupport import QPrintDialog, QPrinter -from qtpy.QtWidgets import ( - QFrame, - QHBoxLayout, - QLabel, - QMainWindow, - QToolBar, - QToolButton, - QWidget, -) - -from qwt import ( - QwtLegend, - QwtLogScaleEngine, - QwtPlot, - QwtPlotCurve, - QwtPlotGrid, - QwtPlotMarker, - QwtPlotRenderer, - QwtSymbol, - QwtText, -) -from qwt.tests import utils - -print_xpm = [ - "32 32 12 1", - "a c #ffffff", - "h c #ffff00", - "c c #ffffff", - "f c #dcdcdc", - "b c #c0c0c0", - "j c #a0a0a4", - "e c #808080", - "g c #808000", - "d c #585858", - "i c #00ff00", - "# c #000000", - ". c None", - "................................", - "................................", - "...........###..................", - "..........#abb###...............", - ".........#aabbbbb###............", - ".........#ddaaabbbbb###.........", - "........#ddddddaaabbbbb###......", - ".......#deffddddddaaabbbbb###...", - "......#deaaabbbddddddaaabbbbb###", - ".....#deaaaaaaabbbddddddaaabbbb#", - "....#deaaabbbaaaa#ddedddfggaaad#", - "...#deaaaaaaaaaa#ddeeeeafgggfdd#", - "..#deaaabbbaaaa#ddeeeeabbbbgfdd#", - ".#deeefaaaaaaa#ddeeeeabbhhbbadd#", - "#aabbbeeefaaa#ddeeeeabbbbbbaddd#", - "#bbaaabbbeee#ddeeeeabbiibbadddd#", - "#bbbbbaaabbbeeeeeeabbbbbbaddddd#", - "#bjbbbbbbaaabbbbeabbbbbbadddddd#", - "#bjjjjbbbbbbaaaeabbbbbbaddddddd#", - "#bjaaajjjbbbbbbaaabbbbadddddddd#", - "#bbbbbaaajjjbbbbbbaaaaddddddddd#", - "#bjbbbbbbaaajjjbbbbbbddddddddd#.", - "#bjjjjbbbbbbaaajjjbbbdddddddd#..", - "#bjaaajjjbbbbbbjaajjbddddddd#...", - "#bbbbbaaajjjbbbjbbaabdddddd#....", - "###bbbbbbaaajjjjbbbbbddddd#.....", - "...###bbbbbbaaajbbbbbdddd#......", - "......###bbbbbbjbbbbbddd#.......", - ".........###bbbbbbbbbdd#........", - "............###bbbbbbd#.........", - "...............###bbb#..........", - "..................###...........", -] - - -class BodePlot(QwtPlot): - def __init__(self, *args): - QwtPlot.__init__(self, *args) - - self.setTitle("Frequency Response of a 2nd-order System") - self.setCanvasBackground(Qt.darkBlue) - - # legend - legend = QwtLegend() - legend.setFrameStyle(QFrame.Box | QFrame.Sunken) - self.insertLegend(legend, QwtPlot.BottomLegend) - - # grid - QwtPlotGrid.make(plot=self, enableminor=(True, False), color=Qt.darkGray) - - # axes - self.enableAxis(QwtPlot.yRight) - self.setAxisTitle(QwtPlot.xBottom, "\u03c9/\u03c90") - self.setAxisTitle(QwtPlot.yLeft, "Amplitude [dB]") - self.setAxisTitle(QwtPlot.yRight, "Phase [\u00b0]") - - self.setAxisMaxMajor(QwtPlot.xBottom, 6) - self.setAxisMaxMinor(QwtPlot.xBottom, 10) - self.setAxisScaleEngine(QwtPlot.xBottom, QwtLogScaleEngine()) - - # curves - self.curve1 = QwtPlotCurve.make( - title="Amplitude", linecolor=Qt.yellow, plot=self, antialiased=True - ) - self.curve2 = QwtPlotCurve.make( - title="Phase", linecolor=Qt.cyan, plot=self, antialiased=True - ) - self.dB3Marker = QwtPlotMarker.make( - label=QwtText.make(color=Qt.white, brush=Qt.red, weight=QFont.Light), - linestyle=QwtPlotMarker.VLine, - align=Qt.AlignRight | Qt.AlignBottom, - color=Qt.green, - width=2, - style=Qt.DashDotLine, - plot=self, - ) - self.peakMarker = QwtPlotMarker.make( - label=QwtText.make( - color=Qt.red, brush=self.canvasBackground(), weight=QFont.Bold - ), - symbol=QwtSymbol.make(QwtSymbol.Diamond, Qt.yellow, Qt.green, (7, 7)), - linestyle=QwtPlotMarker.HLine, - align=Qt.AlignRight | Qt.AlignBottom, - color=Qt.red, - width=2, - style=Qt.DashDotLine, - plot=self, - ) - QwtPlotMarker.make( - xvalue=0.1, - yvalue=-20.0, - align=Qt.AlignRight | Qt.AlignBottom, - label=QwtText.make( - "[1-(\u03c9/\u03c90)2+2j\u03c9/Q]-1", - color=Qt.white, - borderradius=2, - borderpen=QPen(Qt.lightGray, 5), - brush=Qt.lightGray, - weight=QFont.Bold, - ), - plot=self, - ) - - self.setDamp(0.01) - - def showData(self, frequency, amplitude, phase): - self.curve1.setData(frequency, amplitude) - self.curve2.setData(frequency, phase) - - def showPeak(self, frequency, amplitude): - self.peakMarker.setValue(frequency, amplitude) - label = self.peakMarker.label() - label.setText("Peak: %4g dB" % amplitude) - self.peakMarker.setLabel(label) - - def show3dB(self, frequency): - self.dB3Marker.setValue(frequency, 0.0) - label = self.dB3Marker.label() - label.setText("-3dB at f = %4g" % frequency) - self.dB3Marker.setLabel(label) - - def setDamp(self, d): - self.damping = d - # Numerical Python: f, g, a and p are NumPy arrays! - f = np.exp(np.log(10.0) * np.arange(-2, 2.02, 0.04)) - g = 1.0 / (1.0 - f * f + 2j * self.damping * f) - a = 20.0 * np.log10(abs(g)) - p = 180 * np.arctan2(g.imag, g.real) / np.pi - # for show3dB - i3 = np.argmax(np.where(np.less(a, -3.0), a, -100.0)) - f3 = f[i3] - (a[i3] + 3.0) * (f[i3] - f[i3 - 1]) / (a[i3] - a[i3 - 1]) - # for showPeak - imax = np.argmax(a) - - self.showPeak(f[imax], a[imax]) - self.show3dB(f3) - self.showData(f, a, p) - - self.replot() - - -FNAME_PDF = "bode.pdf" - - -class BodeDemo(QMainWindow): - def __init__(self, *args): - QMainWindow.__init__(self, *args) - - self.plot = BodePlot(self) - self.plot.setContentsMargins(5, 5, 5, 0) - - self.setContextMenuPolicy(Qt.NoContextMenu) - - self.setCentralWidget(self.plot) - - toolBar = QToolBar(self) - self.addToolBar(toolBar) - - btnPrint = QToolButton(toolBar) - btnPrint.setText("Print") - btnPrint.setIcon(QIcon(QPixmap(print_xpm))) - btnPrint.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) - toolBar.addWidget(btnPrint) - btnPrint.clicked.connect(self.print_) - - btnExport = QToolButton(toolBar) - btnExport.setText("Export") - btnExport.setIcon(QIcon(QPixmap(print_xpm))) - btnExport.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) - toolBar.addWidget(btnExport) - btnExport.clicked.connect(self.exportDocument) - - toolBar.addSeparator() - - dampBox = QWidget(toolBar) - dampLayout = QHBoxLayout(dampBox) - dampLayout.setSpacing(0) - dampLayout.addWidget(QWidget(dampBox), 10) # spacer - dampLayout.addWidget(QLabel("Damping Factor", dampBox), 0) - dampLayout.addSpacing(10) - - toolBar.addWidget(dampBox) - - self.statusBar() - - self.showInfo() - - if utils.TestEnvironment().unattended: - self.print_(unattended=True) - - def print_(self, unattended=False): - try: - mode = QPrinter.HighResolution - printer = QPrinter(mode) - except AttributeError: - # Some PySide6 / PyQt6 versions do not have this attribute on Linux - printer = QPrinter() - - printer.setCreator("Bode example") - printer.setPageOrientation(QPageLayout.Landscape) - try: - printer.setColorMode(QPrinter.Color) - except AttributeError: - pass - - docName = str(self.plot.title().text()) - if not docName: - docName.replace("\n", " -- ") - printer.setDocName(docName) - - dialog = QPrintDialog(printer) - if unattended: - # Configure QPrinter object to print to PDF file - printer.setPrinterName("") - printer.setOutputFileName(FNAME_PDF) - dialog.accept() - ok = True - else: - ok = dialog.exec_() - if ok: - renderer = QwtPlotRenderer() - renderer.renderTo(self.plot, printer) - - def exportDocument(self): - renderer = QwtPlotRenderer(self.plot) - renderer.exportTo(self.plot, "bode") - - def showInfo(self, text=""): - self.statusBar().showMessage(text) - - def moved(self, point): - info = "Freq=%g, Ampl=%g, Phase=%g" % ( - self.plot.invTransform(QwtPlot.xBottom, point.x()), - self.plot.invTransform(QwtPlot.yLeft, point.y()), - self.plot.invTransform(QwtPlot.yRight, point.y()), - ) - self.showInfo(info) - - def selected(self, _): - self.showInfo() - - -def test_bodedemo(): - """Bode demo""" - utils.test_widget(BodeDemo, (640, 480)) - if os.path.isfile(FNAME_PDF): - os.remove(FNAME_PDF) - - -if __name__ == "__main__": - test_bodedemo() diff --git a/qwt/tests/test_cartesian.py b/qwt/tests/test_cartesian.py deleted file mode 100644 index 507b551..0000000 --- a/qwt/tests/test_cartesian.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import Qt - -from qwt import QwtPlot, QwtPlotCurve, QwtPlotGrid, QwtPlotItem, QwtScaleDraw -from qwt.tests import utils - - -class CartesianAxis(QwtPlotItem): - """Supports a coordinate system similar to - http://en.wikipedia.org/wiki/Image:Cartesian-coordinate-system.svg""" - - def __init__(self, masterAxis, slaveAxis): - """Valid input values for masterAxis and slaveAxis are QwtPlot.yLeft, - QwtPlot.yRight, QwtPlot.xBottom, and QwtPlot.xTop. When masterAxis is - an x-axis, slaveAxis must be an y-axis; and vice versa.""" - QwtPlotItem.__init__(self) - self.__axis = masterAxis - if masterAxis in (QwtPlot.yLeft, QwtPlot.yRight): - self.setAxes(slaveAxis, masterAxis) - else: - self.setAxes(masterAxis, slaveAxis) - self.scaleDraw = QwtScaleDraw() - self.scaleDraw.setAlignment( - ( - QwtScaleDraw.LeftScale, - QwtScaleDraw.RightScale, - QwtScaleDraw.BottomScale, - QwtScaleDraw.TopScale, - )[masterAxis] - ) - - def draw(self, painter, xMap, yMap, rect): - """Draw an axis on the plot canvas""" - xtr = xMap.transform - ytr = yMap.transform - if self.__axis in (QwtPlot.yLeft, QwtPlot.yRight): - self.scaleDraw.move(round(xtr(0.0)), yMap.p2()) - self.scaleDraw.setLength(yMap.p1() - yMap.p2()) - elif self.__axis in (QwtPlot.xBottom, QwtPlot.xTop): - self.scaleDraw.move(xMap.p1(), round(ytr(0.0))) - self.scaleDraw.setLength(xMap.p2() - xMap.p1()) - self.scaleDraw.setScaleDiv(self.plot().axisScaleDiv(self.__axis)) - self.scaleDraw.draw(painter, self.plot().palette()) - - -class CartesianPlot(QwtPlot): - """Creates a coordinate system similar system - http://en.wikipedia.org/wiki/Image:Cartesian-coordinate-system.svg""" - - def __init__(self, *args): - QwtPlot.__init__(self, *args) - self.setTitle("Cartesian Coordinate System Demo") - # create a plot with a white canvas - self.setCanvasBackground(Qt.white) - # set plot layout - self.plotLayout().setCanvasMargin(0) - self.plotLayout().setAlignCanvasToScales(True) - # attach a grid - QwtPlotGrid.make(self, color=Qt.lightGray, width=0, style=Qt.DotLine, z=-1) - # attach a x-axis - xaxis = CartesianAxis(QwtPlot.xBottom, QwtPlot.yLeft) - xaxis.attach(self) - self.enableAxis(QwtPlot.xBottom, False) - # attach a y-axis - yaxis = CartesianAxis(QwtPlot.yLeft, QwtPlot.xBottom) - yaxis.attach(self) - self.enableAxis(QwtPlot.yLeft, False) - # calculate 3 NumPy arrays - x = np.arange(-2 * np.pi, 2 * np.pi, 0.01) - # attach a curve - QwtPlotCurve.make( - x, - np.pi * np.sin(x), - title="y = pi*sin(x)", - linecolor=Qt.green, - linewidth=2, - plot=self, - antialiased=True, - ) - # attach another curve - QwtPlotCurve.make( - x, - 4 * np.pi * np.cos(x) * np.cos(x) * np.sin(x), - title="y = 4*pi*sin(x)*cos(x)**2", - linecolor=Qt.blue, - linewidth=2, - plot=self, - antialiased=True, - ) - self.replot() - - -def test_cartesian(): - """Cartesian plot test""" - utils.test_widget(CartesianPlot, (800, 480)) - - -if __name__ == "__main__": - test_cartesian() diff --git a/qwt/tests/test_cpudemo.py b/qwt/tests/test_cpudemo.py deleted file mode 100644 index 18ccdf7..0000000 --- a/qwt/tests/test_cpudemo.py +++ /dev/null @@ -1,401 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import os - -import numpy as np -from qtpy.QtCore import QRectF, Qt, QTime -from qtpy.QtGui import QBrush, QColor -from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget - -from qwt import ( - QwtLegend, - QwtLegendData, - QwtPlot, - QwtPlotCurve, - QwtPlotItem, - QwtPlotMarker, - QwtScaleDraw, - QwtText, -) -from qwt.tests import utils - - -class CpuStat: - User = 0 - Nice = 1 - System = 2 - Idle = 3 - counter = 0 - dummyValues = ( - (103726, 0, 23484, 819556), - (103783, 0, 23489, 819604), - (103798, 0, 23490, 819688), - (103820, 0, 23490, 819766), - (103840, 0, 23493, 819843), - (103875, 0, 23499, 819902), - (103917, 0, 23504, 819955), - (103950, 0, 23508, 820018), - (103987, 0, 23510, 820079), - (104020, 0, 23513, 820143), - (104058, 0, 23514, 820204), - (104099, 0, 23520, 820257), - (104121, 0, 23525, 820330), - (104159, 0, 23530, 820387), - (104176, 0, 23534, 820466), - (104215, 0, 23538, 820523), - (104245, 0, 23541, 820590), - (104267, 0, 23545, 820664), - (104311, 0, 23555, 820710), - (104355, 0, 23565, 820756), - (104367, 0, 23567, 820842), - (104383, 0, 23572, 820921), - (104396, 0, 23577, 821003), - (104413, 0, 23579, 821084), - (104446, 0, 23588, 821142), - (104521, 0, 23594, 821161), - (104611, 0, 23604, 821161), - (104708, 0, 23607, 821161), - (104804, 0, 23611, 821161), - (104895, 0, 23620, 821161), - (104993, 0, 23622, 821161), - (105089, 0, 23626, 821161), - (105185, 0, 23630, 821161), - (105281, 0, 23634, 821161), - (105379, 0, 23636, 821161), - (105472, 0, 23643, 821161), - (105569, 0, 23646, 821161), - (105666, 0, 23649, 821161), - (105763, 0, 23652, 821161), - (105828, 0, 23661, 821187), - (105904, 0, 23666, 821206), - (105999, 0, 23671, 821206), - (106094, 0, 23676, 821206), - (106184, 0, 23686, 821206), - (106273, 0, 23692, 821211), - (106306, 0, 23700, 821270), - (106341, 0, 23703, 821332), - (106392, 0, 23709, 821375), - (106423, 0, 23715, 821438), - (106472, 0, 23721, 821483), - (106531, 0, 23727, 821517), - (106562, 0, 23732, 821582), - (106597, 0, 23736, 821643), - (106633, 0, 23737, 821706), - (106666, 0, 23742, 821768), - (106697, 0, 23744, 821835), - (106730, 0, 23748, 821898), - (106765, 0, 23751, 821960), - (106799, 0, 23754, 822023), - (106831, 0, 23758, 822087), - (106862, 0, 23761, 822153), - (106899, 0, 23763, 822214), - (106932, 0, 23766, 822278), - (106965, 0, 23768, 822343), - (107009, 0, 23771, 822396), - (107040, 0, 23775, 822461), - (107092, 0, 23780, 822504), - (107143, 0, 23787, 822546), - (107200, 0, 23795, 822581), - (107250, 0, 23803, 822623), - (107277, 0, 23810, 822689), - (107286, 0, 23810, 822780), - (107313, 0, 23817, 822846), - (107325, 0, 23818, 822933), - (107332, 0, 23818, 823026), - (107344, 0, 23821, 823111), - (107357, 0, 23821, 823198), - (107368, 0, 23823, 823284), - (107375, 0, 23824, 823377), - (107386, 0, 23825, 823465), - (107396, 0, 23826, 823554), - (107422, 0, 23830, 823624), - (107434, 0, 23831, 823711), - (107456, 0, 23835, 823785), - (107468, 0, 23838, 823870), - (107487, 0, 23840, 823949), - (107515, 0, 23843, 824018), - (107528, 0, 23846, 824102), - (107535, 0, 23851, 824190), - (107548, 0, 23853, 824275), - (107562, 0, 23857, 824357), - (107656, 0, 23863, 824357), - (107751, 0, 23868, 824357), - (107849, 0, 23870, 824357), - (107944, 0, 23875, 824357), - (108043, 0, 23876, 824357), - (108137, 0, 23882, 824357), - (108230, 0, 23889, 824357), - (108317, 0, 23902, 824357), - (108412, 0, 23907, 824357), - (108511, 0, 23908, 824357), - (108608, 0, 23911, 824357), - (108704, 0, 23915, 824357), - (108801, 0, 23918, 824357), - (108891, 0, 23928, 824357), - (108987, 0, 23932, 824357), - (109072, 0, 23943, 824361), - (109079, 0, 23943, 824454), - (109086, 0, 23944, 824546), - (109098, 0, 23950, 824628), - (109108, 0, 23955, 824713), - (109115, 0, 23957, 824804), - (109122, 0, 23958, 824896), - (109132, 0, 23959, 824985), - (109142, 0, 23961, 825073), - (109146, 0, 23962, 825168), - (109153, 0, 23964, 825259), - (109162, 0, 23966, 825348), - (109168, 0, 23969, 825439), - (109176, 0, 23971, 825529), - (109185, 0, 23974, 825617), - (109193, 0, 23977, 825706), - (109198, 0, 23978, 825800), - (109206, 0, 23978, 825892), - (109212, 0, 23981, 825983), - (109219, 0, 23981, 826076), - (109225, 0, 23981, 826170), - (109232, 0, 23984, 826260), - (109242, 0, 23984, 826350), - (109255, 0, 23986, 826435), - (109268, 0, 23987, 826521), - (109283, 0, 23990, 826603), - (109288, 0, 23991, 826697), - (109295, 0, 23993, 826788), - (109308, 0, 23994, 826874), - (109322, 0, 24009, 826945), - (109328, 0, 24011, 827037), - (109338, 0, 24012, 827126), - (109347, 0, 24012, 827217), - (109354, 0, 24017, 827305), - (109367, 0, 24017, 827392), - (109371, 0, 24019, 827486), - ) - - def __init__(self): - self.procValues = self.__lookup() - - def statistic(self): - values = self.__lookup() - userDelta = 0.0 - for i in [CpuStat.User, CpuStat.Nice]: - userDelta += values[i] - self.procValues[i] - systemDelta = values[CpuStat.System] - self.procValues[CpuStat.System] - totalDelta = 0.0 - for i in range(len(self.procValues)): - totalDelta += values[i] - self.procValues[i] - self.procValues = values - return 100.0 * userDelta / totalDelta, 100.0 * systemDelta / totalDelta - - def upTime(self): - result = QTime(0, 0, 0) - for item in self.procValues: - result = result.addSecs(int(0.01 * item)) - return result - - def __lookup(self): - if os.path.exists("/proc/stat"): - with open("/proc/stat") as file: - for line in file: - words = line.split() - if words[0] == "cpu" and len(words) >= 5: - return [float(w) for w in words[1:]] - else: - result = CpuStat.dummyValues[CpuStat.counter] - CpuStat.counter += 1 - CpuStat.counter %= len(CpuStat.dummyValues) - return result - - -class CpuPieMarker(QwtPlotMarker): - def __init__(self, *args): - QwtPlotMarker.__init__(self, *args) - self.setZ(1000.0) - self.setRenderHint(QwtPlotItem.RenderAntialiased, True) - - def rtti(self): - return QwtPlotItem.Rtti_PlotUserItem - - def draw(self, painter, xMap, yMap, rect): - margin = 5 - pieRect = QRectF() - pieRect.setX(rect.x() + margin) - pieRect.setY(rect.y() + margin) - pieRect.setHeight(int(yMap.transform(80.0))) - pieRect.setWidth(pieRect.height()) - - angle = 3 * 5760 / 4 - for key in ["User", "System", "Idle"]: - curve = self.plot().cpuPlotCurve(key) - if curve.dataSize(): - value = int(5760 * curve.sample(0).y() / 100.0) - painter.save() - painter.setBrush(QBrush(curve.pen().color(), Qt.SolidPattern)) - painter.drawPie(pieRect, int(-angle), int(-value)) - painter.restore() - angle += value - - -class TimeScaleDraw(QwtScaleDraw): - def __init__(self, baseTime, *args): - QwtScaleDraw.__init__(self, *args) - self.baseTime = baseTime - - def label(self, value): - upTime = self.baseTime.addSecs(int(value)) - return QwtText(upTime.toString()) - - -class Background(QwtPlotItem): - def __init__(self): - QwtPlotItem.__init__(self) - self.setZ(0.0) - - def rtti(self): - return QwtPlotItem.Rtti_PlotUserItem - - def draw(self, painter, xMap, yMap, rect): - c = QColor(Qt.white) - r = QRectF(rect) - - for i in range(100, 0, -10): - r.setBottom(int(yMap.transform(i - 10))) - r.setTop(int(yMap.transform(i))) - painter.fillRect(r, c) - c = c.darker(110) - - -class CpuCurve(QwtPlotCurve): - def __init__(self, *args): - QwtPlotCurve.__init__(self, *args) - self.setRenderHint(QwtPlotItem.RenderAntialiased) - - def setColor(self, color): - c = QColor(color) - c.setAlpha(150) - - self.setPen(c) - self.setBrush(c) - - -class CpuPlot(QwtPlot): - HISTORY = 60 - - def __init__(self, unattended=False): - QwtPlot.__init__(self) - - self.curves = {} - self.data = {} - self.timeData = 1.0 * np.arange(self.HISTORY - 1, -1, -1) - self.cpuStat = CpuStat() - - self.setAutoReplot(False) - - self.plotLayout().setAlignCanvasToScales(True) - - legend = QwtLegend() - legend.setDefaultItemMode(QwtLegendData.Checkable) - self.insertLegend(legend, QwtPlot.RightLegend) - - self.setAxisTitle(QwtPlot.xBottom, "System Uptime [h:m:s]") - self.setAxisScaleDraw(QwtPlot.xBottom, TimeScaleDraw(self.cpuStat.upTime())) - self.setAxisScale(QwtPlot.xBottom, 0, self.HISTORY) - self.setAxisLabelRotation(QwtPlot.xBottom, -50.0) - self.setAxisLabelAlignment(QwtPlot.xBottom, Qt.AlignLeft | Qt.AlignBottom) - - self.setAxisTitle(QwtPlot.yLeft, "Cpu Usage [%]") - self.setAxisScale(QwtPlot.yLeft, 0, 100) - - background = Background() - background.attach(self) - - pie = CpuPieMarker() - pie.attach(self) - - curve = CpuCurve("System") - curve.setColor(Qt.red) - curve.attach(self) - self.curves["System"] = curve - self.data["System"] = np.zeros(self.HISTORY, float) - - curve = CpuCurve("User") - curve.setColor(Qt.blue) - curve.setZ(curve.z() - 1.0) - curve.attach(self) - self.curves["User"] = curve - self.data["User"] = np.zeros(self.HISTORY, float) - - curve = CpuCurve("Total") - curve.setColor(Qt.black) - curve.setZ(curve.z() - 2.0) - curve.attach(self) - self.curves["Total"] = curve - self.data["Total"] = np.zeros(self.HISTORY, float) - - curve = CpuCurve("Idle") - curve.setColor(Qt.darkCyan) - curve.setZ(curve.z() - 3.0) - curve.attach(self) - self.curves["Idle"] = curve - self.data["Idle"] = np.zeros(self.HISTORY, float) - - self.showCurve(self.curves["System"], True) - self.showCurve(self.curves["User"], True) - self.showCurve(self.curves["Total"], False or unattended) - self.showCurve(self.curves["Idle"], False or unattended) - - self.startTimer(20 if unattended else 1000) - - legend.checked.connect(self.showCurve) - self.replot() - - def timerEvent(self, e): - for data in self.data.values(): - data[1:] = data[0:-1] - self.data["User"][0], self.data["System"][0] = self.cpuStat.statistic() - self.data["Total"][0] = self.data["User"][0] + self.data["System"][0] - self.data["Idle"][0] = 100.0 - self.data["Total"][0] - - self.timeData += 1.0 - - self.setAxisScale(QwtPlot.xBottom, self.timeData[-1], self.timeData[0]) - for key in self.curves.keys(): - self.curves[key].setData(self.timeData, self.data[key]) - - self.replot() - - def showCurve(self, item, on, index=None): - item.setVisible(on) - self.legend().legendWidget(item).setChecked(on) - self.replot() - - def cpuPlotCurve(self, key): - return self.curves[key] - - -class CpuDemo(QWidget): - def __init__(self, parent=None, unattended=False): - super(CpuDemo, self).__init__(parent) - layout = QVBoxLayout() - self.setLayout(layout) - plot = CpuPlot(unattended=unattended) - plot.setTitle("History") - layout.addWidget(plot) - label = QLabel("Press the legend to en/disable a curve") - layout.addWidget(label) - - -def test_cpudemo(): - """CPU demo""" - utils.test_widget(CpuDemo, (600, 400)) - - -if __name__ == "__main__": - test_cpudemo() diff --git a/qwt/tests/test_curvebenchmark1.py b/qwt/tests/test_curvebenchmark1.py deleted file mode 100644 index 8032fa5..0000000 --- a/qwt/tests/test_curvebenchmark1.py +++ /dev/null @@ -1,193 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -"""Curve benchmark example""" - -SHOW = True # Show test in GUI-based test launcher - -import time - -import numpy as np -from qtpy.QtCore import Qt -from qtpy.QtWidgets import ( - QApplication, - QGridLayout, - QLineEdit, - QMainWindow, - QTabWidget, - QTextEdit, - QWidget, -) - -from qwt import QwtPlot, QwtPlotCurve -from qwt.tests import utils - -COLOR_INDEX = None - - -def get_curve_color(): - global COLOR_INDEX - colors = (Qt.blue, Qt.red, Qt.green, Qt.yellow, Qt.magenta, Qt.cyan) - if COLOR_INDEX is None: - COLOR_INDEX = 0 - else: - COLOR_INDEX = (COLOR_INDEX + 1) % len(colors) - return colors[COLOR_INDEX] - - -PLOT_ID = 0 - - -class BMPlot(QwtPlot): - def __init__(self, title, xdata, ydata, style, symbol=None, *args): - super(BMPlot, self).__init__(*args) - global PLOT_ID - self.setMinimumSize(200, 150) - PLOT_ID += 1 - self.setTitle("%s (#%d)" % (title, PLOT_ID)) - self.setAxisTitle(QwtPlot.xBottom, "x") - self.setAxisTitle(QwtPlot.yLeft, "y") - self.curve_nb = 0 - for idx in range(1, 11): - self.curve_nb += 1 - QwtPlotCurve.make( - xdata, - ydata * idx, - style=style, - symbol=symbol, - linecolor=get_curve_color(), - antialiased=True, - plot=self, - ) - self.replot() - - -class BMWidget(QWidget): - def __init__(self, nbcol, points, *args, **kwargs): - super(BMWidget, self).__init__() - self.plot_nb = 0 - self.curve_nb = 0 - self.setup(nbcol, points, *args, **kwargs) - - def params(self, *args, **kwargs): - if kwargs.get("only_lines", False): - return (("Lines", None),) - else: - return ( - ("Lines", None), - ("Dots", None), - ) - - def setup(self, nbcol, points, *args, **kwargs): - x = np.linspace(0.001, 20.0, int(points)) - y = (np.sin(x) / x) * np.cos(20 * x) - layout = QGridLayout() - col, row = 0, 0 - for style, symbol in self.params(*args, **kwargs): - plot = BMPlot(style, x, y, getattr(QwtPlotCurve, style), symbol=symbol) - layout.addWidget(plot, row, col) - self.plot_nb += 1 - self.curve_nb += plot.curve_nb - col += 1 - if col >= nbcol: - row += 1 - col = 0 - self.text = QLineEdit() - self.text.setReadOnly(True) - self.text.setAlignment(Qt.AlignCenter) - self.text.setText("Rendering plot...") - layout.addWidget(self.text, row + 1, 0, 1, nbcol) - self.setLayout(layout) - - -class BMText(QTextEdit): - def __init__(self, parent=None, title=None): - super(BMText, self).__init__(parent) - self.setReadOnly(True) - library = "PythonQwt" - wintitle = self.parent().windowTitle() - if not wintitle: - wintitle = "Benchmark" - if title is None: - title = "%s example" % wintitle - self.parent().setWindowTitle("%s [%s]" % (wintitle, library)) - self.setText( - """\ -%s:
-(base plotting library: %s)

-Click on each tab to test if plotting performance is acceptable in terms of -GUI response time (switch between tabs, resize main windows, ...).
-

-Benchmarks results: -""" - % (title, library) - ) - - -class CurveBenchmark1(QMainWindow): - TITLE = "Curve benchmark" - SIZE = (1000, 500) - - def __init__(self, max_n=1000000, parent=None, unattended=False, **kwargs): - super(CurveBenchmark1, self).__init__(parent=parent) - title = self.TITLE - if kwargs.get("only_lines", False): - title = "%s (%s)" % (title, "only lines") - self.setWindowTitle(title) - self.tabs = QTabWidget() - self.setCentralWidget(self.tabs) - self.text = BMText(self) - self.tabs.addTab(self.text, "Contents") - self.resize(*self.SIZE) - self.durations = [] - - # Force window to show up and refresh (for test purpose only) - self.show() - QApplication.processEvents() - - t0g = time.time() - self.run_benchmark(max_n, unattended, **kwargs) - dt = time.time() - t0g - self.text.append("

Total elapsed time: %d ms" % (dt * 1e3)) - self.tabs.setCurrentIndex(1 if unattended else 0) - - def process_iteration(self, title, description, widget, t0): - self.tabs.addTab(widget, title) - self.tabs.setCurrentWidget(widget) - - # Force widget to refresh (for test purpose only) - QApplication.processEvents() - - duration = (time.time() - t0) * 1000 - self.durations.append(duration) - time_str = "Elapsed time: %d ms" % duration - widget.text.setText(time_str) - self.text.append("
%s:
%s" % (description, time_str)) - print("[%s] %s" % (utils.get_lib_versions(), time_str)) - - def run_benchmark(self, max_n, unattended, **kwargs): - max_n = 1000 if unattended else max_n - iterations = 0 if unattended else 4 - for idx in range(iterations, -1, -1): - points = int(max_n / 10**idx) - t0 = time.time() - widget = BMWidget(2, points, **kwargs) - title = "%d points" % points - description = "%d plots with %d curves of %d points" % ( - widget.plot_nb, - widget.curve_nb, - points, - ) - self.process_iteration(title, description, widget, t0) - - -def test_curvebenchmark1(): - """Curve benchmark example""" - utils.test_widget(CurveBenchmark1, options=False) - - -if __name__ == "__main__": - test_curvebenchmark1() diff --git a/qwt/tests/test_curvebenchmark2.py b/qwt/tests/test_curvebenchmark2.py deleted file mode 100644 index c8ded00..0000000 --- a/qwt/tests/test_curvebenchmark2.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -"""Curve styles""" - -SHOW = True # Show test in GUI-based test launcher - -import time - -from qtpy.QtCore import Qt - -from qwt import QwtSymbol -from qwt.tests import test_curvebenchmark1 as cb -from qwt.tests import utils - - -class CSWidget(cb.BMWidget): - def params(self, *args, **kwargs): - (symbols,) = args - symb1 = QwtSymbol.make( - QwtSymbol.Ellipse, brush=Qt.yellow, pen=Qt.blue, size=(5, 5) - ) - symb2 = QwtSymbol.make(QwtSymbol.XCross, pen=Qt.darkMagenta, size=(5, 5)) - if symbols: - if kwargs.get("only_lines", False): - return ( - ("Lines", symb1), - ("Lines", symb1), - ("Lines", symb2), - ("Lines", symb2), - ) - else: - return ( - ("Sticks", symb1), - ("Lines", symb1), - ("Steps", symb2), - ("Dots", symb2), - ) - else: - if kwargs.get("only_lines", False): - return ( - ("Lines", None), - ("Lines", None), - ("Lines", None), - ("Lines", None), - ) - else: - return ( - ("Sticks", None), - ("Lines", None), - ("Steps", None), - ("Dots", None), - ) - - -class CurveBenchmark2(cb.CurveBenchmark1): - TITLE = "Curve styles" - SIZE = (1000, 800) - - def __init__(self, max_n=1000, parent=None, unattended=False, **kwargs): - super(CurveBenchmark2, self).__init__( - max_n=max_n, parent=parent, unattended=unattended, **kwargs - ) - - def run_benchmark(self, max_n, unattended, **kwargs): - for points, symbols in zip( - (max_n / 10, max_n / 10, max_n, max_n), (True, False) * 2 - ): - t0 = time.time() - symtext = "with%s symbols" % ("" if symbols else "out") - widget = CSWidget(2, points, symbols, **kwargs) - title = "%d points" % points - description = "%d plots with %d curves of %d points, %s" % ( - widget.plot_nb, - widget.curve_nb, - points, - symtext, - ) - self.process_iteration(title, description, widget, t0) - - -def test_curvebenchmark2(): - """Curve styles benchmark example""" - utils.test_widget(CurveBenchmark2, options=False) - - -if __name__ == "__main__": - test_curvebenchmark2() diff --git a/qwt/tests/test_curvedemo1.py b/qwt/tests/test_curvedemo1.py deleted file mode 100644 index 0f37949..0000000 --- a/qwt/tests/test_curvedemo1.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import QSize, Qt -from qtpy.QtGui import QBrush, QFont, QPainter, QPen -from qtpy.QtWidgets import QFrame - -from qwt import QwtPlotCurve, QwtPlotItem, QwtScaleMap, QwtSymbol -from qwt.tests import utils - - -class CurveDemo1(QFrame): - def __init__(self, *args): - QFrame.__init__(self, *args) - - self.xMap = QwtScaleMap() - self.xMap.setScaleInterval(-0.5, 10.5) - self.yMap = QwtScaleMap() - self.yMap.setScaleInterval(-1.1, 1.1) - - # frame style - self.setFrameStyle(QFrame.Box | QFrame.Raised) - self.setLineWidth(2) - self.setMidLineWidth(3) - - # calculate values - self.x = np.arange(0, 10.0, 10.0 / 27) - self.y = np.sin(self.x) * np.cos(2 * self.x) - - # make curves with different styles - self.curves = [] - self.titles = [] - # curve 1 - self.titles.append("Style: Sticks, Symbol: Ellipse") - curve = QwtPlotCurve() - curve.setPen(QPen(Qt.red)) - curve.setStyle(QwtPlotCurve.Sticks) - curve.setSymbol( - QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.yellow), QPen(Qt.blue), QSize(5, 5)) - ) - self.curves.append(curve) - # curve 2 - self.titles.append("Style: Lines, Symbol: None") - curve = QwtPlotCurve() - curve.setPen(QPen(Qt.darkBlue)) - curve.setStyle(QwtPlotCurve.Lines) - self.curves.append(curve) - # curve 3 - self.titles.append("Style: Lines, Symbol: None, Antialiased") - curve = QwtPlotCurve() - curve.setPen(QPen(Qt.darkBlue)) - curve.setStyle(QwtPlotCurve.Lines) - curve.setRenderHint(QwtPlotItem.RenderAntialiased) - self.curves.append(curve) - # curve 4 - self.titles.append("Style: Steps, Symbol: None") - curve = QwtPlotCurve() - curve.setPen(QPen(Qt.darkCyan)) - curve.setStyle(QwtPlotCurve.Steps) - self.curves.append(curve) - # curve 5 - self.titles.append("Style: NoCurve, Symbol: XCross") - curve = QwtPlotCurve() - curve.setStyle(QwtPlotCurve.NoCurve) - curve.setSymbol( - QwtSymbol(QwtSymbol.XCross, QBrush(), QPen(Qt.darkMagenta), QSize(5, 5)) - ) - self.curves.append(curve) - - # attach data, using Numeric - for curve in self.curves: - curve.setData(self.x, self.y) - - def shiftDown(self, rect, offset): - rect.translate(0, offset) - - def paintEvent(self, event): - QFrame.paintEvent(self, event) - painter = QPainter(self) - painter.setClipRect(self.contentsRect()) - self.drawContents(painter) - - def drawContents(self, painter): - # draw curves - r = self.contentsRect() - dy = int(r.height() / len(self.curves)) - r.setHeight(dy) - for curve in self.curves: - self.xMap.setPaintInterval(r.left(), r.right()) - self.yMap.setPaintInterval(r.top(), r.bottom()) - painter.setRenderHint( - QPainter.Antialiasing, - curve.testRenderHint(QwtPlotItem.RenderAntialiased), - ) - curve.draw(painter, self.xMap, self.yMap, r) - self.shiftDown(r, dy) - # draw titles - r = self.contentsRect() - r.setHeight(dy) - painter.setFont(QFont("Helvetica", 8)) - painter.setPen(Qt.black) - for title in self.titles: - painter.drawText( - 0, - r.top(), - r.width(), - painter.fontMetrics().height(), - Qt.AlignTop | Qt.AlignHCenter, - title, - ) - self.shiftDown(r, dy) - - -def test_curvedemo1(): - """Curve demo 1""" - utils.test_widget(CurveDemo1, size=(300, 600), options=False) - - -if __name__ == "__main__": - test_curvedemo1() diff --git a/qwt/tests/test_curvedemo2.py b/qwt/tests/test_curvedemo2.py deleted file mode 100644 index ee2ec01..0000000 --- a/qwt/tests/test_curvedemo2.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import QSize, Qt -from qtpy.QtGui import QBrush, QColor, QPainter, QPalette, QPen -from qtpy.QtWidgets import QFrame - -from qwt import QwtPlotCurve, QwtScaleMap, QwtSymbol -from qwt.tests import utils - -Size = 15 -USize = 13 - - -class CurveDemo2(QFrame): - def __init__(self, *args): - QFrame.__init__(self, *args) - - self.setFrameStyle(QFrame.Box | QFrame.Raised) - self.setLineWidth(2) - self.setMidLineWidth(3) - - p = QPalette() - p.setColor(self.backgroundRole(), QColor(30, 30, 50)) - self.setPalette(p) - # make curves and maps - self.tuples = [] - # curve 1 - curve = QwtPlotCurve() - curve.setPen(QPen(QColor(150, 150, 200), 2)) - curve.setStyle(QwtPlotCurve.Lines) - curve.setSymbol( - QwtSymbol(QwtSymbol.XCross, QBrush(), QPen(Qt.yellow, 2), QSize(7, 7)) - ) - self.tuples.append( - (curve, QwtScaleMap(0, 100, -1.5, 1.5), QwtScaleMap(0, 100, 0.0, 2 * np.pi)) - ) - # curve 2 - curve = QwtPlotCurve() - curve.setPen(QPen(QColor(200, 150, 50), 1, Qt.DashDotDotLine)) - curve.setStyle(QwtPlotCurve.Sticks) - curve.setSymbol( - QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.blue), QPen(Qt.yellow), QSize(5, 5)) - ) - self.tuples.append( - (curve, QwtScaleMap(0, 100, 0.0, 2 * np.pi), QwtScaleMap(0, 100, -3.0, 1.1)) - ) - # curve 3 - curve = QwtPlotCurve() - curve.setPen(QPen(QColor(100, 200, 150))) - curve.setStyle(QwtPlotCurve.Lines) - self.tuples.append( - (curve, QwtScaleMap(0, 100, -1.1, 3.0), QwtScaleMap(0, 100, -1.1, 3.0)) - ) - # curve 4 - curve = QwtPlotCurve() - curve.setPen(QPen(Qt.red)) - curve.setStyle(QwtPlotCurve.Lines) - self.tuples.append( - (curve, QwtScaleMap(0, 100, -5.0, 1.1), QwtScaleMap(0, 100, -1.1, 5.0)) - ) - # data - self.phase = 0.0 - self.base = np.arange(0.0, 2.01 * np.pi, 2 * np.pi / (USize - 1)) - self.uval = np.cos(self.base) - self.vval = np.sin(self.base) - self.uval[1::2] *= 0.5 - self.vval[1::2] *= 0.5 - self.newValues() - # start timer - self.tid = self.startTimer(250) - - def paintEvent(self, event): - QFrame.paintEvent(self, event) - painter = QPainter(self) - painter.setClipRect(self.contentsRect()) - self.drawContents(painter) - - def drawContents(self, painter): - r = self.contentsRect() - for curve, xMap, yMap in self.tuples: - xMap.setPaintInterval(r.left(), r.right()) - yMap.setPaintInterval(r.top(), r.bottom()) - curve.draw(painter, xMap, yMap, r) - - def timerEvent(self, event): - self.newValues() - self.repaint() - - def newValues(self): - phase = self.phase - - self.xval = np.arange(0, 2.01 * np.pi, 2 * np.pi / (Size - 1)) - self.yval = np.sin(self.xval - phase) - self.zval = np.cos(3 * (self.xval + phase)) - - s = 0.25 * np.sin(phase) - c = np.sqrt(1.0 - s * s) - u = self.uval - self.uval = c * self.uval - s * self.vval - self.vval = c * self.vval + s * u - - self.tuples[0][0].setData(self.yval, self.xval) - self.tuples[1][0].setData(self.xval, self.zval) - self.tuples[2][0].setData(self.yval, self.zval) - self.tuples[3][0].setData(self.uval, self.vval) - - self.phase += 2 * np.pi / 100 - if self.phase > 2 * np.pi: - self.phase = 0.0 - - -def test_curvedemo2(): - """Curve demo 2""" - utils.test_widget(CurveDemo2, options=False) - - -if __name__ == "__main__": - test_curvedemo2() diff --git a/qwt/tests/test_data.py b/qwt/tests/test_data.py deleted file mode 100644 index 99ccd46..0000000 --- a/qwt/tests/test_data.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import random - -import numpy as np -from qtpy.QtCore import QSize, Qt -from qtpy.QtGui import QBrush, QPen -from qtpy.QtWidgets import QFrame - -from qwt import ( - QwtAbstractScaleDraw, - QwtLegend, - QwtPlot, - QwtPlotCurve, - QwtPlotMarker, - QwtSymbol, -) -from qwt.tests import utils - - -class DataPlot(QwtPlot): - def __init__(self, unattended=False): - QwtPlot.__init__(self) - - self.setCanvasBackground(Qt.white) - self.alignScales() - - # Initialize data - self.x = np.arange(0.0, 100.1, 0.5) - self.y = np.zeros(len(self.x), float) - self.z = np.zeros(len(self.x), float) - - self.setTitle("A Moving QwtPlot Demonstration") - self.insertLegend(QwtLegend(), QwtPlot.BottomLegend) - - self.curveR = QwtPlotCurve("Data Moving Right") - self.curveR.attach(self) - self.curveL = QwtPlotCurve("Data Moving Left") - self.curveL.attach(self) - - self.curveL.setSymbol( - QwtSymbol(QwtSymbol.Ellipse, QBrush(), QPen(Qt.yellow), QSize(7, 7)) - ) - - self.curveR.setPen(QPen(Qt.red)) - self.curveL.setPen(QPen(Qt.blue)) - - mY = QwtPlotMarker() - mY.setLabelAlignment(Qt.AlignRight | Qt.AlignTop) - mY.setLineStyle(QwtPlotMarker.HLine) - mY.setYValue(0.0) - mY.attach(self) - - self.setAxisTitle(QwtPlot.xBottom, "Time (seconds)") - self.setAxisTitle(QwtPlot.yLeft, "Values") - - self.startTimer(10 if unattended else 50) - self.phase = 0.0 - - def alignScales(self): - self.canvas().setFrameStyle(QFrame.Box | QFrame.Plain) - self.canvas().setLineWidth(1) - for axis_id in QwtPlot.AXES: - scaleWidget = self.axisWidget(axis_id) - if scaleWidget: - scaleWidget.setMargin(0) - scaleDraw = self.axisScaleDraw(axis_id) - if scaleDraw: - scaleDraw.enableComponent(QwtAbstractScaleDraw.Backbone, False) - - def timerEvent(self, e): - if self.phase > np.pi - 0.0001: - self.phase = 0.0 - - # y moves from left to right: - # shift y array right and assign new value y[0] - self.y = np.concatenate((self.y[:1], self.y[:-1])) - self.y[0] = np.sin(self.phase) * (-1.0 + 2.0 * random.random()) - - # z moves from right to left: - # Shift z array left and assign new value to z[n-1]. - self.z = np.concatenate((self.z[1:], self.z[:1])) - self.z[-1] = 0.8 - (2.0 * self.phase / np.pi) + 0.4 * random.random() - - self.curveR.setData(self.x, self.y) - self.curveL.setData(self.x, self.z) - - self.replot() - self.phase += np.pi * 0.02 - - -def test_data(): - """Data Test""" - utils.test_widget(DataPlot, size=(500, 300)) - - -if __name__ == "__main__": - test_data() diff --git a/qwt/tests/test_errorbar.py b/qwt/tests/test_errorbar.py deleted file mode 100644 index 55f2fcc..0000000 --- a/qwt/tests/test_errorbar.py +++ /dev/null @@ -1,327 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import QLineF, QRectF, QSize, Qt -from qtpy.QtGui import QBrush, QPen - -from qwt import QwtPlot, QwtPlotCurve, QwtPlotGrid, QwtSymbol -from qwt.tests import utils - - -class ErrorBarPlotCurve(QwtPlotCurve): - def __init__( - self, - x=[], - y=[], - dx=None, - dy=None, - curvePen=None, - curveStyle=None, - curveSymbol=None, - errorPen=None, - errorCap=0, - errorOnTop=False, - ): - """A curve of x versus y data with error bars in dx and dy. - - Horizontal error bars are plotted if dx is not None. - Vertical error bars are plotted if dy is not None. - - x and y must be sequences with a shape (N,) and dx and dy must be - sequences (if not None) with a shape (), (N,), or (2, N): - - if dx or dy has a shape () or (N,), the error bars are given by - (x-dx, x+dx) or (y-dy, y+dy), - - if dx or dy has a shape (2, N), the error bars are given by - (x-dx[0], x+dx[1]) or (y-dy[0], y+dy[1]). - - curvePen is the pen used to plot the curve - - curveStyle is the style used to plot the curve - - curveSymbol is the symbol used to plot the symbols - - errorPen is the pen used to plot the error bars - - errorCap is the size of the error bar caps - - errorOnTop is a boolean: - - if True, plot the error bars on top of the curve, - - if False, plot the curve on top of the error bars. - """ - - QwtPlotCurve.__init__(self) - - if curvePen is None: - curvePen = QPen(Qt.NoPen) - if curveStyle is None: - curveStyle = QwtPlotCurve.Lines - if curveSymbol is None: - curveSymbol = QwtSymbol() - if errorPen is None: - errorPen = QPen(Qt.NoPen) - - self.setData(x, y, dx, dy) - self.setPen(curvePen) - self.setStyle(curveStyle) - self.setSymbol(curveSymbol) - self.errorPen = errorPen - self.errorCap = errorCap - self.errorOnTop = errorOnTop - - def setData(self, *args): - """Set x versus y data with error bars in dx and dy. - - Horizontal error bars are plotted if dx is not None. - Vertical error bars are plotted if dy is not None. - - x and y must be sequences with a shape (N,) and dx and dy must be - sequences (if not None) with a shape (), (N,), or (2, N): - - if dx or dy has a shape () or (N,), the error bars are given by - (x-dx, x+dx) or (y-dy, y+dy), - - if dx or dy has a shape (2, N), the error bars are given by - (x-dx[0], x+dx[1]) or (y-dy[0], y+dy[1]). - """ - if len(args) == 1: - QwtPlotCurve.setData(self, *args) - return - - dx = None - dy = None - x, y = args[:2] - if len(args) > 2: - dx = args[2] - if len(args) > 3: - dy = args[3] - - self.__x = np.asarray(x, float) - if len(self.__x.shape) != 1: - raise RuntimeError("len(asarray(x).shape) != 1") - - self.__y = np.asarray(y, float) - if len(self.__y.shape) != 1: - raise RuntimeError("len(asarray(y).shape) != 1") - if len(self.__x) != len(self.__y): - raise RuntimeError("len(asarray(x)) != len(asarray(y))") - - if dx is None: - self.__dx = None - else: - self.__dx = np.asarray(dx, float) - if len(self.__dx.shape) not in [0, 1, 2]: - raise RuntimeError("len(asarray(dx).shape) not in [0, 1, 2]") - - if dy is None: - self.__dy = dy - else: - self.__dy = np.asarray(dy, float) - if len(self.__dy.shape) not in [0, 1, 2]: - raise RuntimeError("len(asarray(dy).shape) not in [0, 1, 2]") - - QwtPlotCurve.setData(self, self.__x, self.__y) - - def boundingRect(self): - """Return the bounding rectangle of the data, error bars included.""" - if self.__dx is None: - xmin = min(self.__x) - xmax = max(self.__x) - elif len(self.__dx.shape) in [0, 1]: - xmin = min(self.__x - self.__dx) - xmax = max(self.__x + self.__dx) - else: - xmin = min(self.__x - self.__dx[0]) - xmax = max(self.__x + self.__dx[1]) - - if self.__dy is None: - ymin = min(self.__y) - ymax = max(self.__y) - elif len(self.__dy.shape) in [0, 1]: - ymin = min(self.__y - self.__dy) - ymax = max(self.__y + self.__dy) - else: - ymin = min(self.__y - self.__dy[0]) - ymax = max(self.__y + self.__dy[1]) - - return QRectF(xmin, ymin, xmax - xmin, ymax - ymin) - - def drawSeries(self, painter, xMap, yMap, canvasRect, first, last=-1): - """Draw an interval of the curve, including the error bars - - painter is the QPainter used to draw the curve - - xMap is the QwtDiMap used to map x-values to pixels - - yMap is the QwtDiMap used to map y-values to pixels - - first is the index of the first data point to draw - - last is the index of the last data point to draw. If last < 0, last - is transformed to index the last data point - """ - - if last < 0: - last = self.dataSize() - 1 - - if self.errorOnTop: - QwtPlotCurve.drawSeries(self, painter, xMap, yMap, canvasRect, first, last) - - # draw the error bars - painter.save() - painter.setPen(self.errorPen) - - # draw the error bars with caps in the x direction - if self.__dx is not None: - # draw the bars - if len(self.__dx.shape) in [0, 1]: - xmin = self.__x - self.__dx - xmax = self.__x + self.__dx - else: - xmin = self.__x - self.__dx[0] - xmax = self.__x + self.__dx[1] - y = self.__y - n, i = len(y), 0 - lines = [] - while i < n: - yi = yMap.transform(y[i]) - lines.append( - QLineF(xMap.transform(xmin[i]), yi, xMap.transform(xmax[i]), yi) - ) - i += 1 - painter.drawLines(lines) - if self.errorCap > 0: - # draw the caps - cap = self.errorCap / 2 - ( - n, - i, - ) = ( - len(y), - 0, - ) - lines = [] - while i < n: - yi = yMap.transform(y[i]) - lines.append( - QLineF( - xMap.transform(xmin[i]), - yi - cap, - xMap.transform(xmin[i]), - yi + cap, - ) - ) - lines.append( - QLineF( - xMap.transform(xmax[i]), - yi - cap, - xMap.transform(xmax[i]), - yi + cap, - ) - ) - i += 1 - painter.drawLines(lines) - - # draw the error bars with caps in the y direction - if self.__dy is not None: - # draw the bars - if len(self.__dy.shape) in [0, 1]: - ymin = self.__y - self.__dy - ymax = self.__y + self.__dy - else: - ymin = self.__y - self.__dy[0] - ymax = self.__y + self.__dy[1] - x = self.__x - ( - n, - i, - ) = ( - len(x), - 0, - ) - lines = [] - while i < n: - xi = xMap.transform(x[i]) - lines.append( - QLineF(xi, yMap.transform(ymin[i]), xi, yMap.transform(ymax[i])) - ) - i += 1 - painter.drawLines(lines) - # draw the caps - if self.errorCap > 0: - cap = self.errorCap / 2 - n, i, _j = len(x), 0, 0 - lines = [] - while i < n: - xi = xMap.transform(x[i]) - lines.append( - QLineF( - xi - cap, - yMap.transform(ymin[i]), - xi + cap, - yMap.transform(ymin[i]), - ) - ) - lines.append( - QLineF( - xi - cap, - yMap.transform(ymax[i]), - xi + cap, - yMap.transform(ymax[i]), - ) - ) - i += 1 - painter.drawLines(lines) - - painter.restore() - - if not self.errorOnTop: - QwtPlotCurve.drawSeries(self, painter, xMap, yMap, canvasRect, first, last) - - -class ErrorBarPlot(QwtPlot): - def __init__(self, parent=None, title=None): - super(ErrorBarPlot, self).__init__("Errorbar Demonstation") - self.setCanvasBackground(Qt.white) - self.plotLayout().setAlignCanvasToScales(True) - grid = QwtPlotGrid() - grid.attach(self) - grid.setPen(QPen(Qt.black, 0, Qt.DotLine)) - - # calculate data and errors for a curve with error bars - x = np.arange(0, 10.1, 0.5, float) - y = np.sin(x) - dy = 0.2 * abs(y) - # dy = (0.15 * abs(y), 0.25 * abs(y)) # uncomment for asymmetric error bars - dx = 0.2 # all error bars the same size - errorOnTop = False # uncomment to draw the curve on top of the error bars - # errorOnTop = True # uncomment to draw the error bars on top of the curve - symbol = QwtSymbol( - QwtSymbol.Ellipse, QBrush(Qt.red), QPen(Qt.black, 2), QSize(9, 9) - ) - curve = ErrorBarPlotCurve( - x=x, - y=y, - dx=dx, - dy=dy, - curvePen=QPen(Qt.black, 2), - curveSymbol=symbol, - errorPen=QPen(Qt.blue, 2), - errorCap=10, - errorOnTop=errorOnTop, - ) - curve.attach(self) - - -def test_errorbar(): - """Errorbar plot example""" - utils.test_widget(ErrorBarPlot, size=(640, 480)) - - -if __name__ == "__main__": - test_errorbar() diff --git a/qwt/tests/test_eventfilter.py b/qwt/tests/test_eventfilter.py deleted file mode 100644 index 64fa1d7..0000000 --- a/qwt/tests/test_eventfilter.py +++ /dev/null @@ -1,482 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import os - -import numpy as np -from qtpy.QtCore import QEvent, QObject, QPoint, QRect, QSize, Qt, Signal -from qtpy.QtGui import QBrush, QColor, QPainter, QPen -from qtpy.QtWidgets import QApplication, QMainWindow, QToolBar, QWhatsThis, QWidget - -from qwt import ( - QwtPlot, - QwtPlotCanvas, - QwtPlotCurve, - QwtPlotGrid, - QwtScaleDiv, - QwtScaleDraw, - QwtSymbol, -) -from qwt.tests import utils - -QT_API = os.environ["QT_API"] - - -class ColorBar(QWidget): - colorSelected = Signal(QColor) - - def __init__(self, orientation, *args): - QWidget.__init__(self, *args) - self.__orientation = orientation - self.__light = QColor(Qt.white) - self.__dark = QColor(Qt.black) - self.setCursor(Qt.PointingHandCursor) - - def setOrientation(self, orientation): - self.__orientation = orientation - self.update() - - def orientation(self): - return self.__orientation - - def setRange(self, light, dark): - self.__light = light - self.__dark = dark - self.update() - - def setLight(self, color): - self.__light = color - self.update() - - def setDark(self, color): - self.__dark = color - self.update() - - def light(self): - return self.__light - - def dark(self): - return self.__dark - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton: - pm = self.grab() - color = QColor() - color.setRgb(pm.toImage().pixel(event.x(), event.y())) - self.colorSelected.emit(color) - event.accept() - - def paintEvent(self, _): - painter = QPainter(self) - self.drawColorBar(painter, self.rect()) - - def drawColorBar(self, painter, rect): - h1, s1, v1, _ = self.__light.getHsv() - h2, s2, v2, _ = self.__dark.getHsv() - painter.save() - painter.setClipRect(rect) - painter.setClipping(True) - painter.fillRect(rect, QBrush(self.__dark)) - sectionSize = 2 - if self.__orientation == Qt.Horizontal: - numIntervals = rect.width() / sectionSize - else: - numIntervals = rect.height() / sectionSize - section = QRect() - for i in range(int(numIntervals)): - if self.__orientation == Qt.Horizontal: - section.setRect( - rect.x() + i * sectionSize, rect.y(), sectionSize, rect.heigh() - ) - else: - section.setRect( - rect.x(), rect.y() + i * sectionSize, rect.width(), sectionSize - ) - ratio = float(i) / float(numIntervals) - color = QColor() - color.setHsv( - h1 + int(ratio * (h2 - h1) + 0.5), - s1 + int(ratio * (s2 - s1) + 0.5), - v1 + int(ratio * (v2 - v1) + 0.5), - ) - painter.fillRect(section, color) - painter.restore() - - -class Plot(QwtPlot): - def __init__(self, *args): - QwtPlot.__init__(self, *args) - - self.setTitle("Interactive Plot") - - self.setCanvasColor(Qt.darkCyan) - - grid = QwtPlotGrid() - grid.attach(self) - grid.setMajorPen(QPen(Qt.white, 0, Qt.DotLine)) - - self.setAxisScale(QwtPlot.xBottom, 0.0, 100.0) - self.setAxisScale(QwtPlot.yLeft, 0.0, 100.0) - - # Avoid jumping when label with 3 digits - # appear/disappear when scrolling vertically - scaleDraw = self.axisScaleDraw(QwtPlot.yLeft) - scaleDraw.setMinimumExtent( - scaleDraw.extent(self.axisWidget(QwtPlot.yLeft).font()) - ) - - self.plotLayout().setAlignCanvasToScales(True) - - self.__insertCurve(Qt.Vertical, Qt.blue, 30.0) - self.__insertCurve(Qt.Vertical, Qt.magenta, 70.0) - self.__insertCurve(Qt.Horizontal, Qt.yellow, 30.0) - self.__insertCurve(Qt.Horizontal, Qt.white, 70.0) - - self.replot() - - scaleWidget = self.axisWidget(QwtPlot.yLeft) - scaleWidget.setMargin(10) - - self.__colorBar = ColorBar(Qt.Vertical, scaleWidget) - self.__colorBar.setRange(QColor(Qt.red), QColor(Qt.darkBlue)) - self.__colorBar.setFocusPolicy(Qt.TabFocus) - self.__colorBar.colorSelected.connect(self.setCanvasColor) - - # we need the resize events, to lay out the color bar - scaleWidget.installEventFilter(self) - - # we need the resize events, to lay out the wheel - self.canvas().installEventFilter(self) - - scaleWidget.setWhatsThis( - "Selecting a value at the scale will insert a new curve." - ) - self.__colorBar.setWhatsThis( - "Selecting a color will change the background of the plot." - ) - self.axisWidget(QwtPlot.xBottom).setWhatsThis( - "Selecting a value at the scale will insert a new curve." - ) - - def setCanvasColor(self, color): - self.setCanvasBackground(color) - self.replot() - - def scrollLeftAxis(self, value): - self.setAxisScale(QwtPlot.yLeft, value, value + 100) - self.replot() - - def eventFilter(self, obj, event): - if event.type() == QEvent.Resize: - size = event.size() - if obj == self.axisWidget(QwtPlot.yLeft): - margin = 2 - x = size.width() - obj.margin() + margin - w = obj.margin() - 2 * margin - y = int(obj.startBorderDist()) - h = int(size.height() - obj.startBorderDist() - obj.endBorderDist()) - self.__colorBar.setGeometry(x, y, w, h) - return QwtPlot.eventFilter(self, obj, event) - - def insertCurve(self, axis, base): - if axis == QwtPlot.yLeft or axis == QwtPlot.yRight: - o = Qt.Horizontal - else: - o = Qt.Vertical - self.__insertCurve(o, QColor(Qt.red), base) - self.replot() - - def __insertCurve(self, orientation, color, base): - curve = QwtPlotCurve() - curve.attach(self) - curve.setPen(QPen(color)) - curve.setSymbol( - QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.gray), QPen(color), QSize(8, 8)) - ) - fixed = base * np.ones(10, float) - changing = np.arange(0, 95.0, 10.0, float) + 5.0 - if orientation == Qt.Horizontal: - curve.setData(changing, fixed) - else: - curve.setData(fixed, changing) - - -class CanvasPicker(QObject): - def __init__(self, plot): - QObject.__init__(self, plot) - self.__selectedCurve = None - self.__selectedPoint = -1 - self.__plot = plot - canvas = plot.canvas() - canvas.installEventFilter(self) - # We want the focus, but no focus rect. - # The selected point will be highlighted instead. - canvas.setFocusPolicy(Qt.StrongFocus) - canvas.setCursor(Qt.PointingHandCursor) - canvas.setFocusIndicator(QwtPlotCanvas.ItemFocusIndicator) - canvas.setFocus() - canvas.setWhatsThis( - "All points can be moved using the left mouse button " - "or with these keys:\n\n" - "- Up: Select next curve\n" - "- Down: Select previous curve\n" - '- Left, "-": Select next point\n' - '- Right, "+": Select previous point\n' - "- 7, 8, 9, 4, 6, 1, 2, 3: Move selected point" - ) - self.__shiftCurveCursor(True) - - def event(self, event): - if event.type() == QEvent.User: - self.__showCursor(True) - return True - return QObject.event(self, event) - - def eventFilter(self, object, event): - if event.type() == QEvent.FocusIn: - self.__showCursor(True) - if event.type() == QEvent.FocusOut: - try: - self.__showCursor(False) - except RuntimeError: - pass # ignore error when closing the application - if event.type() == QEvent.Paint: - QApplication.postEvent(self, QEvent(QEvent.User)) - elif event.type() == QEvent.MouseButtonPress: - self.__select(event.position()) - return True - elif event.type() == QEvent.MouseMove: - self.__move(event.position()) - return True - if event.type() == QEvent.KeyPress: - delta = 5 - key = event.key() - if key == Qt.Key_Up: - self.__shiftCurveCursor(True) - return True - elif key == Qt.Key_Down: - self.__shiftCurveCursor(False) - return True - elif key == Qt.Key_Right or key == Qt.Key_Plus: - if self.__selectedCurve: - self.__shiftPointCursor(True) - else: - self.__shiftCurveCursor(True) - return True - elif key == Qt.Key_Left or key == Qt.Key_Minus: - if self.__selectedCurve: - self.__shiftPointCursor(False) - else: - self.__shiftCurveCursor(True) - return True - if key == Qt.Key_1: - self.__moveBy(-delta, delta) - elif key == Qt.Key_2: - self.__moveBy(0, delta) - elif key == Qt.Key_3: - self.__moveBy(delta, delta) - elif key == Qt.Key_4: - self.__moveBy(-delta, 0) - elif key == Qt.Key_6: - self.__moveBy(delta, 0) - elif key == Qt.Key_7: - self.__moveBy(-delta, -delta) - elif key == Qt.Key_8: - self.__moveBy(0, -delta) - elif key == Qt.Key_9: - self.__moveBy(delta, -delta) - return False - - def __select(self, pos): - found, distance, point = None, 1e100, -1 - for curve in self.__plot.itemList(): - if isinstance(curve, QwtPlotCurve): - i, d = curve.closestPoint(pos) - if d < distance: - found = curve - point = i - distance = d - self.__showCursor(False) - self.__selectedCurve = None - self.__selectedPoint = -1 - if found and distance < 10: - self.__selectedCurve = found - self.__selectedPoint = point - self.__showCursor(True) - - def __moveBy(self, dx, dy): - if dx == 0 and dy == 0: - return - curve = self.__selectedCurve - if not curve: - return - s = curve.sample(self.__selectedPoint) - x = self.__plot.transform(curve.xAxis(), s.x()) + dx - y = self.__plot.transform(curve.yAxis(), s.y()) + dy - self.__move(QPoint(x, y)) - - def __move(self, pos): - curve = self.__selectedCurve - if not curve: - return - xData = np.zeros(curve.dataSize(), float) - yData = np.zeros(curve.dataSize(), float) - for i in range(curve.dataSize()): - if i == self.__selectedPoint: - xData[i] = self.__plot.invTransform(curve.xAxis(), pos.x()) - yData[i] = self.__plot.invTransform(curve.yAxis(), pos.y()) - else: - s = curve.sample(i) - xData[i] = s.x() - yData[i] = s.y() - curve.setData(xData, yData) - self.__showCursor(True) - self.__plot.replot() - - def __showCursor(self, showIt): - curve = self.__selectedCurve - if not curve: - return - symbol = curve.symbol() - brush = symbol.brush() - if showIt: - symbol.setBrush(symbol.brush().color().darker(180)) - curve.directPaint(self.__selectedPoint, self.__selectedPoint) - if showIt: - symbol.setBrush(brush) - - def __shiftCurveCursor(self, up): - curves = [ - curve for curve in self.__plot.itemList() if isinstance(curve, QwtPlotCurve) - ] - if not curves: - return - if self.__selectedCurve in curves: - index = curves.index(self.__selectedCurve) - if up: - index += 1 - else: - index -= 1 - # keep index within [0, len(curves)) - index += len(curves) - index %= len(curves) - else: - index = 0 - self.__showCursor(False) - self.__selectedPoint = 0 - self.__selectedCurve = curves[index] - self.__showCursor(True) - - def __shiftPointCursor(self, up): - curve = self.__selectedCurve - if not curve: - return - if up: - index = self.__selectedPoint + 1 - else: - index = self.__selectedPoint - 1 - # keep index within [0, curve.dataSize()) - index += curve.dataSize() - index %= curve.dataSize() - if index != self.__selectedPoint: - self.__showCursor(False) - self.__selectedPoint = index - self.__showCursor(True) - - -class ScalePicker(QObject): - clicked = Signal(int, float) - - def __init__(self, plot): - QObject.__init__(self, plot) - for axis_id in QwtPlot.AXES: - scaleWidget = plot.axisWidget(axis_id) - if scaleWidget: - scaleWidget.installEventFilter(self) - - def eventFilter(self, object, event): - if event.type() == QEvent.MouseButtonPress: - self.__mouseClicked(object, event.position()) - return True - return QObject.eventFilter(self, object, event) - - def __mouseClicked(self, scale, pos): - rect = self.__scaleRect(scale) - margin = 10 - rect.setRect( - rect.x() - margin, - rect.y() - margin, - rect.width() + 2 * margin, - rect.height() + 2 * margin, - ) - if rect.contains(pos): - value = 0.0 - axis = -1 - sd = scale.scaleDraw() - if scale.alignment() == QwtScaleDraw.LeftScale: - value = sd.scaleMap().invTransform(pos.y()) - axis = QwtPlot.yLeft - elif scale.alignment() == QwtScaleDraw.RightScale: - value = sd.scaleMap().invTransform(pos.y()) - axis = QwtPlot.yRight - elif scale.alignment() == QwtScaleDraw.BottomScale: - value = sd.scaleMap().invTransform(pos.x()) - axis = QwtPlot.xBottom - elif scale.alignment() == QwtScaleDraw.TopScale: - value = sd.scaleMap().invTransform(pos.x()) - axis = QwtPlot.xBottom - self.clicked.emit(axis, value) - - def __scaleRect(self, scale): - bld = scale.margin() - mjt = scale.scaleDraw().tickLength(QwtScaleDiv.MajorTick) - sbd = scale.startBorderDist() - ebd = scale.endBorderDist() - if scale.alignment() == QwtScaleDraw.LeftScale: - return QRect( - scale.width() - bld - mjt, sbd, mjt, scale.height() - sbd - ebd - ) - elif scale.alignment() == QwtScaleDraw.RightScale: - return QRect(bld, sbd, mjt, scale.height() - sbd - ebd) - elif scale.alignment() == QwtScaleDraw.BottomScale: - return QRect(sbd, bld, scale.width() - sbd - ebd, mjt) - elif scale.alignment() == QwtScaleDraw.TopScale: - return QRect( - sbd, scale.height() - bld - mjt, scale.width() - sbd - ebd, mjt - ) - else: - return QRect() - - -class EventFilterWindow(QMainWindow): - def __init__(self, parent=None): - super(EventFilterWindow, self).__init__(parent=parent) - toolBar = QToolBar(self) - toolBar.addAction(QWhatsThis.createAction(toolBar)) - self.addToolBar(toolBar) - plot = Plot() - self.setCentralWidget(plot) - plot.setWhatsThis( - "An useless plot to demonstrate how to use event filtering.\n\n" - "You can click on the color bar, the scales or move the slider.\n" - "All points can be moved using the mouse or the keyboard." - ) - CanvasPicker(plot) - scalePicker = ScalePicker(plot) - scalePicker.clicked.connect(plot.insertCurve) - - -def test_eventfilter(): - """Event filter example""" - utils.test_widget(EventFilterWindow, size=(540, 400)) - - -if __name__ == "__main__": - test_eventfilter() diff --git a/qwt/tests/test_highdpi.py b/qwt/tests/test_highdpi.py deleted file mode 100644 index 44f951c..0000000 --- a/qwt/tests/test_highdpi.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import os - -import pytest - -from qwt.tests import utils -from qwt.tests.test_simple import SimplePlot - - -class HighDPIPlot(SimplePlot): - NUM_POINTS = 5000000 # 5 million points needed to test high DPI support - - -@pytest.mark.skip(reason="This test is not relevant for the automated test suite") -def test_highdpi(): - """Test high DPI support""" - - # Performance should be the same with "1" and "2" scale factors: - # (as of today, this is not the case, but it has to be fixed in the future: - # https://github.com/PlotPyStack/PythonQwt/issues/83) - os.environ["QT_SCALE_FACTOR"] = "2" - - utils.test_widget(HighDPIPlot, (800, 480)) - - -if __name__ == "__main__": - test_highdpi() diff --git a/qwt/tests/test_image.py b/qwt/tests/test_image.py deleted file mode 100644 index b0eebfd..0000000 --- a/qwt/tests/test_image.py +++ /dev/null @@ -1,204 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import Qt -from qtpy.QtGui import QPen, qRgb - -from qwt import ( - QwtInterval, - QwtLegend, - QwtLegendData, - QwtLinearColorMap, - QwtPlot, - QwtPlotCurve, - QwtPlotGrid, - QwtPlotItem, - QwtPlotMarker, - QwtScaleMap, - toQImage, -) -from qwt.tests import utils - - -def bytescale(data, cmin=None, cmax=None, high=255, low=0): - if (hasattr(data, "dtype") and data.dtype.char == np.uint8) or ( - hasattr(data, "typecode") and data.typecode == np.uint8 - ): - return data - high = high - low - if cmin is None: - cmin = min(np.ravel(data)) - if cmax is None: - cmax = max(np.ravel(data)) - scale = high * 1.0 / (cmax - cmin or 1) - bytedata = ((data * 1.0 - cmin) * scale + 0.4999).astype(np.uint8) - return bytedata + np.asarray(low).astype(np.uint8) - - -def linearX(nx, ny): - return np.repeat(np.arange(nx, typecode=np.float32)[:, np.newaxis], ny, -1) - - -def linearY(nx, ny): - return np.repeat(np.arange(ny, typecode=np.float32)[np.newaxis, :], nx, 0) - - -def square(n, min, max): - t = np.arange(min, max, float(max - min) / (n - 1)) - # return outer(cos(t), sin(t)) - return np.cos(t) * np.sin(t)[:, np.newaxis] - - -class PlotImage(QwtPlotItem): - def __init__(self, title=""): - QwtPlotItem.__init__(self) - self.setTitle(title) - self.setItemAttribute(QwtPlotItem.Legend) - self.xyzs = None - - def setData(self, xyzs, xRange=None, yRange=None): - self.xyzs = xyzs - shape = xyzs.shape - if not xRange: - xRange = (0, shape[0]) - if not yRange: - yRange = (0, shape[1]) - - self.xMap = QwtScaleMap(0, xyzs.shape[0], *xRange) - self.plot().setAxisScale(QwtPlot.xBottom, *xRange) - self.yMap = QwtScaleMap(0, xyzs.shape[1], *yRange) - self.plot().setAxisScale(QwtPlot.yLeft, *yRange) - - self.image = toQImage(bytescale(self.xyzs)).mirrored(False, True) - for i in range(0, 256): - self.image.setColor(i, qRgb(i, 0, 255 - i)) - - def updateLegend(self, legend, data): - QwtPlotItem.updateLegend(self, legend, data) - legend.find(self).setText(self.title()) - - def draw(self, painter, xMap, yMap, rect): - """Paint image zoomed to xMap, yMap - - Calculate (x1, y1, x2, y2) so that it contains at least 1 pixel, - and copy the visible region to scale it to the canvas. - """ - assert isinstance(self.plot(), QwtPlot) - - # calculate y1, y2 - # the scanline order (index y) is inverted with respect to the y-axis - y1 = y2 = self.image.height() - y1 *= self.yMap.s2() - yMap.s2() - y1 /= self.yMap.s2() - self.yMap.s1() - y1 = max(0, int(y1 - 0.5)) - y2 *= self.yMap.s2() - yMap.s1() - y2 /= self.yMap.s2() - self.yMap.s1() - y2 = min(self.image.height(), int(y2 + 0.5)) - # calculate x1, x2 -- the pixel order (index x) is normal - x1 = x2 = self.image.width() - x1 *= xMap.s1() - self.xMap.s1() - x1 /= self.xMap.s2() - self.xMap.s1() - x1 = max(0, int(x1 - 0.5)) - x2 *= xMap.s2() - self.xMap.s1() - x2 /= self.xMap.s2() - self.xMap.s1() - x2 = min(self.image.width(), int(x2 + 0.5)) - # copy - image = self.image.copy(x1, y1, x2 - x1, y2 - y1) - # zoom - image = image.scaled( - int(xMap.p2() - xMap.p1() + 1), int(yMap.p1() - yMap.p2() + 1) - ) - # draw - painter.drawImage(int(xMap.p1()), int(yMap.p2()), image) - - -class ImagePlot(QwtPlot): - def __init__(self, *args): - QwtPlot.__init__(self, *args) - # set plot title - self.setTitle("ImagePlot") - # set plot layout - self.plotLayout().setCanvasMargin(0) - self.plotLayout().setAlignCanvasToScales(True) - # set legend - legend = QwtLegend() - legend.setDefaultItemMode(QwtLegendData.Clickable) - self.insertLegend(legend, QwtPlot.RightLegend) - # set axis titles - self.setAxisTitle(QwtPlot.xBottom, "time (s)") - self.setAxisTitle(QwtPlot.yLeft, "frequency (Hz)") - - colorMap = QwtLinearColorMap(Qt.blue, Qt.red) - interval = QwtInterval(-1, 1) - self.enableAxis(QwtPlot.yRight) - self.setAxisScale(QwtPlot.yRight, -1, 1) - self.axisWidget(QwtPlot.yRight).setColorBarEnabled(True) - self.axisWidget(QwtPlot.yRight).setColorMap(interval, colorMap) - - # calculate 3 NumPy arrays - x = np.arange(-2 * np.pi, 2 * np.pi, 0.01) - y = np.pi * np.sin(x) - z = 4 * np.pi * np.cos(x) * np.cos(x) * np.sin(x) - # attach a curve - QwtPlotCurve.make( - x, y, title="y = pi*sin(x)", linecolor=Qt.green, linewidth=2, plot=self - ) - # attach another curve - QwtPlotCurve.make( - x, z, title="y = 4*pi*sin(x)*cos(x)**2", linewidth=2, plot=self - ) - # attach a grid - grid = QwtPlotGrid() - grid.attach(self) - grid.setPen(QPen(Qt.black, 0, Qt.DotLine)) - # attach a horizontal marker at y = 0 - QwtPlotMarker.make( - label="y = 0", - linestyle=QwtPlotMarker.HLine, - align=Qt.AlignRight | Qt.AlignTop, - plot=self, - ) - # attach a vertical marker at x = pi - QwtPlotMarker.make( - np.pi, - 0.0, - label="x = pi", - linestyle=QwtPlotMarker.VLine, - align=Qt.AlignRight | Qt.AlignBottom, - plot=self, - ) - # attach a plot image - plotImage = PlotImage("Image") - plotImage.attach(self) - plotImage.setData( - square(512, -2 * np.pi, 2 * np.pi), - (-2 * np.pi, 2 * np.pi), - (-2 * np.pi, 2 * np.pi), - ) - - legend.clicked.connect(self.toggleVisibility) - - # replot - self.replot() - - def toggleVisibility(self, plotItem, idx): - """Toggle the visibility of a plot item""" - plotItem.setVisible(not plotItem.isVisible()) - self.replot() - - -def test_image(): - """Image plot test""" - utils.test_widget(ImagePlot, size=(600, 400)) - - -if __name__ == "__main__": - test_image() diff --git a/qwt/tests/test_loadtest.py b/qwt/tests/test_loadtest.py deleted file mode 100644 index 1576016..0000000 --- a/qwt/tests/test_loadtest.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015-2021 Pierre Raybaut -# (see LICENSE file for more details) - -"""Load test""" - -SHOW = True # Show test in GUI-based test launcher - -import time - -import numpy as np - -# Local imports -from qwt.tests import test_curvebenchmark1 as cb -from qwt.tests import utils - -NCOLS, NROWS = 6, 5 -NPLOTS = NCOLS * NROWS * 5 * 3 - - -class LTWidget(cb.BMWidget): - def params(self, *args, **kwargs): - return tuple([("Lines", None)] * NCOLS * NROWS) - - -class LoadTest(cb.CurveBenchmark1): - TITLE = "Load test [%d plots]" % NPLOTS - SIZE = (1600, 700) - - def __init__(self, max_n=100, parent=None, unattended=False, **kwargs): - super(LoadTest, self).__init__( - max_n=max_n, parent=parent, unattended=unattended, **kwargs - ) - - def run_benchmark(self, max_n, unattended, **kwargs): - points, symbols = 100, False - iterator = range(0, 1) if unattended else range(int(NPLOTS / (NCOLS * NROWS))) - for _i_page in iterator: - t0 = time.time() - symtext = "with%s symbols" % ("" if symbols else "out") - widget = LTWidget(NCOLS, points, symbols, **kwargs) - title = "%d points" % points - description = "%d plots with %d curves of %d points, %s" % ( - widget.plot_nb, - widget.curve_nb, - points, - symtext, - ) - self.process_iteration(title, description, widget, t0) - print("") - time_str = "Average elapsed time: %d ms" % np.mean(self.durations) - print("[%s] %s" % (utils.get_lib_versions(), time_str)) - - -def test_loadtest(): - """Load test""" - utils.test_widget(LoadTest, options=False) - - -if __name__ == "__main__": - test_loadtest() diff --git a/qwt/tests/test_logcurve.py b/qwt/tests/test_logcurve.py deleted file mode 100644 index be18b17..0000000 --- a/qwt/tests/test_logcurve.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np - -np.seterr(all="raise") - -from qtpy.QtCore import Qt - -from qwt import QwtLogScaleEngine, QwtPlot, QwtPlotCurve -from qwt.tests import utils - - -class LogCurvePlot(QwtPlot): - def __init__(self): - super(LogCurvePlot, self).__init__( - "LogCurveDemo.py (or how to handle -inf values)" - ) - self.enableAxis(QwtPlot.xBottom) - self.setAxisScaleEngine(QwtPlot.yLeft, QwtLogScaleEngine()) - x = np.arange(0.0, 10.0, 0.1) - y = 10 * np.cos(x) ** 2 - 0.1 - QwtPlotCurve.make(x, y, linecolor=Qt.magenta, plot=self, antialiased=True) - self.replot() - - -def test_logcurve(): - """Log curve demo""" - utils.test_widget(LogCurvePlot, size=(800, 500)) - - -if __name__ == "__main__": - test_logcurve() diff --git a/qwt/tests/test_mapdemo.py b/qwt/tests/test_mapdemo.py deleted file mode 100644 index 720790b..0000000 --- a/qwt/tests/test_mapdemo.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import random -import time - -import numpy as np -from qtpy.QtCore import QSize, Qt -from qtpy.QtGui import QBrush, QPen -from qtpy.QtWidgets import QMainWindow, QToolBar - -from qwt import QwtPlot, QwtPlotCurve, QwtSymbol -from qwt.tests import utils - - -def standard_map(x, y, kappa): - """provide one interate of the inital conditions (x, y) - for the standard map with parameter kappa.""" - y_new = y - kappa * np.sin(2.0 * np.pi * x) - x_new = x + y_new - # bring back to [0,1.0]^2 - if (x_new > 1.0) or (x_new < 0.0): - x_new = x_new - np.floor(x_new) - if (y_new > 1.0) or (y_new < 0.0): - y_new = y_new - np.floor(y_new) - return x_new, y_new - - -class MapDemo(QMainWindow): - def __init__(self, *args): - QMainWindow.__init__(self, *args) - self.plot = QwtPlot(self) - self.plot.setTitle("A Simple Map Demonstration") - self.plot.setCanvasBackground(Qt.white) - self.plot.setAxisTitle(QwtPlot.xBottom, "x") - self.plot.setAxisTitle(QwtPlot.yLeft, "y") - self.plot.setAxisScale(QwtPlot.xBottom, 0.0, 1.0) - self.plot.setAxisScale(QwtPlot.yLeft, 0.0, 1.0) - self.setCentralWidget(self.plot) - # Initialize map data - self.count = self.i = 1000 - self.xs = np.zeros(self.count, float) - self.ys = np.zeros(self.count, float) - self.kappa = 0.2 - self.curve = QwtPlotCurve("Map") - self.curve.attach(self.plot) - self.curve.setSymbol( - QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.red), QPen(Qt.blue), QSize(5, 5)) - ) - self.curve.setPen(QPen(Qt.cyan)) - toolBar = QToolBar(self) - self.addToolBar(toolBar) - # 1 tick = 1 ms, 10 ticks = 10 ms (Linux clock is 100 Hz) - self.ticks = 10 - self.tid = self.startTimer(self.ticks) - self.timer_tic = None - self.user_tic = None - self.system_tic = None - self.plot.replot() - - def setTicks(self, ticks): - self.i = self.count - self.ticks = int(ticks) - self.killTimer(self.tid) - self.tid = self.startTimer(ticks) - - def moreData(self): - if self.i == self.count: - self.i = 0 - self.x = random.random() - self.y = random.random() - self.xs[self.i] = self.x - self.ys[self.i] = self.y - self.i += 1 - chunks = [] - self.timer_toc = time.time() - if self.timer_tic: - chunks.append("wall: %s s." % (self.timer_toc - self.timer_tic)) - print(" ".join(chunks)) - self.timer_tic = self.timer_toc - else: - self.x, self.y = standard_map(self.x, self.y, self.kappa) - self.xs[self.i] = self.x - self.ys[self.i] = self.y - self.i += 1 - - def timerEvent(self, e): - self.moreData() - self.curve.setData(self.xs[: self.i], self.ys[: self.i]) - self.plot.replot() - - -def test_mapdemo(): - """Map demo""" - utils.test_widget(MapDemo, size=(600, 600)) - - -if __name__ == "__main__": - test_mapdemo() diff --git a/qwt/tests/test_multidemo.py b/qwt/tests/test_multidemo.py deleted file mode 100644 index 951b355..0000000 --- a/qwt/tests/test_multidemo.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import Qt -from qtpy.QtGui import QPen -from qtpy.QtWidgets import QGridLayout, QWidget - -from qwt import QwtPlot, QwtPlotCurve -from qwt.tests import utils - - -def drange(start, stop, step): - start, stop, step = float(start), float(stop), float(step) - size = int(round((stop - start) / step)) - result = [start] * size - for i in range(size): - result[i] += i * step - return result - - -def lorentzian(x): - return 1.0 / (1.0 + (x - 5.0) ** 2) - - -class MultiDemo(QWidget): - def __init__(self, *args): - QWidget.__init__(self, *args) - layout = QGridLayout(self) - # try to create a plot for SciPy arrays - - # make a curve and copy the data - numpy_curve = QwtPlotCurve("y = lorentzian(x)") - x = np.arange(0.0, 10.0, 0.01) - y = lorentzian(x) - numpy_curve.setData(x, y) - # here, we know we can plot NumPy arrays - numpy_plot = QwtPlot(self) - numpy_plot.setTitle("numpy array") - numpy_plot.setCanvasBackground(Qt.white) - numpy_plot.plotLayout().setCanvasMargin(0) - numpy_plot.plotLayout().setAlignCanvasToScales(True) - # insert a curve and make it red - numpy_curve.attach(numpy_plot) - numpy_curve.setPen(QPen(Qt.red)) - layout.addWidget(numpy_plot, 0, 0) - numpy_plot.replot() - - # create a plot widget for lists of Python floats - list_plot = QwtPlot(self) - list_plot.setTitle("Python list") - list_plot.setCanvasBackground(Qt.white) - list_plot.plotLayout().setCanvasMargin(0) - list_plot.plotLayout().setAlignCanvasToScales(True) - x = drange(0.0, 10.0, 0.01) - y = [lorentzian(item) for item in x] - # insert a curve, make it red and copy the lists - list_curve = QwtPlotCurve("y = lorentzian(x)") - list_curve.attach(list_plot) - list_curve.setPen(QPen(Qt.red)) - list_curve.setData(x, y) - layout.addWidget(list_plot, 0, 1) - list_plot.replot() - - -def test_multidemo(): - """Multiple plot demo""" - utils.test_widget(MultiDemo, size=(400, 300)) - - -if __name__ == "__main__": - test_multidemo() diff --git a/qwt/tests/test_relativemargin.py b/qwt/tests/test_relativemargin.py deleted file mode 100644 index 932d0fd..0000000 --- a/qwt/tests/test_relativemargin.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -from qtpy import QtWidgets as QW -from qtpy.QtCore import Qt - -import qwt -from qwt.tests import utils - - -class RelativeMarginDemo(QW.QWidget): - def __init__(self, *args): - QW.QWidget.__init__(self, *args) - layout = QW.QGridLayout(self) - x = [1, 2, 3, 4] - y = [1, 500, 1000, 1500] - for i_row, log_scale in enumerate((False, True)): - for i_col, relative_margin in enumerate((0.0, None, 0.2)): - plot = qwt.QwtPlot(self) - qwt.QwtPlotGrid.make( - plot, color=Qt.lightGray, width=0, style=Qt.DotLine - ) - def_margin = plot.axisMargin(qwt.QwtPlot.yLeft) - scale_str = "lin/lin" if not log_scale else "log/lin" - if relative_margin is None: - margin_str = f"default ({def_margin * 100:.0f}%)" - else: - margin_str = f"{relative_margin * 100:.0f}%" - plot.setTitle(f"{scale_str}, margin: {margin_str}") - if relative_margin is not None: - plot.setAxisMargin(qwt.QwtPlot.yLeft, relative_margin) - plot.setAxisMargin(qwt.QwtPlot.xBottom, relative_margin) - color = "red" if i_row == 0 else "blue" - qwt.QwtPlotCurve.make(x, y, "", plot, linecolor=color) - layout.addWidget(plot, i_row, i_col) - if log_scale: - engine = qwt.QwtLogScaleEngine() - plot.setAxisScaleEngine(qwt.QwtPlot.yLeft, engine) - - -def test_relative_margin(): - """Test relative margin.""" - utils.test_widget(RelativeMarginDemo, size=(400, 300), options=False) - - -if __name__ == "__main__": - test_relative_margin() diff --git a/qwt/tests/test_simple.py b/qwt/tests/test_simple.py deleted file mode 100644 index 00968ab..0000000 --- a/qwt/tests/test_simple.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the PyQwt License -# Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example -# Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further -# developments (e.g. ported to PythonQwt API) -# (see LICENSE file for more details) - -SHOW = True # Show test in GUI-based test launcher - -import os - -import numpy as np -from qtpy.QtCore import Qt, QTimer - -import qwt -from qwt.tests import utils - -FNAMES = ("test_simple.svg", "test_simple.pdf", "test_simple.png") - - -class SimplePlot(qwt.QwtPlot): - NUM_POINTS = 100 - TEST_EXPORT = True - - def __init__(self): - qwt.QwtPlot.__init__(self) - self.setTitle("Really simple demo") - self.insertLegend(qwt.QwtLegend(), qwt.QwtPlot.RightLegend) - self.setAxisTitle(qwt.QwtPlot.xBottom, "X-axis") - self.setAxisTitle(qwt.QwtPlot.yLeft, "Y-axis") - self.enableAxis(self.xBottom) - self.setCanvasBackground(Qt.white) - - qwt.QwtPlotGrid.make(self, color=Qt.lightGray, width=0, style=Qt.DotLine) - - # insert a few curves - x = np.linspace(0.0, 10.0, self.NUM_POINTS) - qwt.QwtPlotCurve.make(x, np.sin(x), "y = sin(x)", self, linecolor="red") - qwt.QwtPlotCurve.make(x, np.cos(x), "y = cos(x)", self, linecolor="blue") - - # insert a horizontal marker at y = 0 - qwt.QwtPlotMarker.make( - label="y = 0", - align=Qt.AlignRight | Qt.AlignTop, - linestyle=qwt.QwtPlotMarker.HLine, - color="darkGreen", - plot=self, - ) - - # insert a vertical marker at x = 2 pi - qwt.QwtPlotMarker.make( - xvalue=2 * np.pi, - label="x = 2 pi", - align=Qt.AlignRight | Qt.AlignTop, - linestyle=qwt.QwtPlotMarker.VLine, - color="darkGreen", - plot=self, - ) - - if self.TEST_EXPORT and utils.TestEnvironment().unattended: - QTimer.singleShot(0, self.export_to_different_formats) - - def export_to_different_formats(self): - for fname in FNAMES: - self.exportTo(fname) - - -def test_simple(): - """Simple plot example""" - utils.test_widget(SimplePlot, size=(600, 400)) - for fname in FNAMES: - if os.path.isfile(fname): - os.remove(fname) - - -if __name__ == "__main__": - test_simple() diff --git a/qwt/tests/test_stylesheet.py b/qwt/tests/test_stylesheet.py deleted file mode 100644 index 5f67acc..0000000 --- a/qwt/tests/test_stylesheet.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- - -SHOW = True # Show test in GUI-based test launcher - -import os - -import numpy as np -import pytest -import qtpy -from qtpy.QtCore import Qt - -import qwt -from qwt.tests import utils - - -class StyleSheetPlot(qwt.QwtPlot): - def __init__(self): - super().__init__() - self.setTitle("Stylesheet test (Issue #63)") - self.setStyleSheet("background-color: #19232D; color: #E0E1E3;") - qwt.QwtPlotGrid.make(self, color=Qt.white, width=0, style=Qt.DotLine) - x = np.arange(-5.0, 5.0, 0.1) - qwt.QwtPlotCurve.make(x, np.sinc(x), "y = sinc(x)", self, linecolor="green") - - -# Skip the test for PySide6 on Linux -@pytest.mark.skipif( - qtpy.API_NAME == "PySide6" and os.name == "posix", - reason="Fails on Linux with PySide6 for unknown reasons", -) -def test_stylesheet(): - """Stylesheet test""" - utils.test_widget(StyleSheetPlot, size=(600, 400)) - - -if __name__ == "__main__": - test_stylesheet() diff --git a/qwt/tests/test_symbols.py b/qwt/tests/test_symbols.py deleted file mode 100644 index 36a8852..0000000 --- a/qwt/tests/test_symbols.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- - -SHOW = True # Show test in GUI-based test launcher - -import os.path as osp - -import numpy as np -from qtpy import QtCore as QC -from qtpy import QtGui as QG -from qtpy import QtWidgets as QW - -import qwt -from qwt.tests import utils - - -class BaseSymbolPlot(qwt.QwtPlot): - TITLE = "Base Symbol Example" - SYMBOL_CLASS = qwt.QwtSymbol - - def __init__(self): - super().__init__() - self.setTitle(self.TITLE) - self.setAxisScale(self.yLeft, -20, 20) - self.setAxisScale(self.xBottom, -20, 20) - self.setup_plot() - - def setup_plot(self): - samples = ([-15, 0, 15, -15], [0, 15, 0, 0]) - self.add_curve(self.TITLE, samples, self.SYMBOL_CLASS()) - self.resize(400, 400) - - def add_curve(self, title, samples, symbol=None): - """Add a curve to the plot""" - curve = qwt.QwtPlotCurve(title) - curve.setSamples(*samples) - if symbol is not None: - curve.setSymbol(symbol) - curve.attach(self) - self.replot() - - -class BuiltinSymbolPlot(BaseSymbolPlot): - TITLE = "Built-in Symbol Example" - - def setup_plot(self): - colors = (QC.Qt.red, QC.Qt.green, QC.Qt.blue, QC.Qt.yellow, QC.Qt.magenta) - for index, symbol_name in enumerate( - ( - "Ellipse", - "Rect", - "Diamond", - "Triangle", - "DTriangle", - "UTriangle", - "LTriangle", - "RTriangle", - "Cross", - "XCross", - "HLine", - "VLine", - "Star1", - "Star2", - "Hexagon", - ) - ): - symbol = qwt.symbol.QwtSymbol(getattr(qwt.QwtSymbol, symbol_name)) - symbol.setSize(7, 7) - symbol.setPen(QG.QPen(colors[index % 3])) - symbol.setBrush(QG.QBrush(QG.QColor(colors[index % 3]).lighter(150))) - x = np.linspace(-10, 10, 100) - y = np.sin(x + index * np.pi / 10) - samples = (x, y) - qwt.plot_marker.QwtPlotMarker.make( - xvalue=index * 2 - 10, - yvalue=index * 2 - 10, - label=qwt.text.QwtText.make( - "Marker", - color=QC.Qt.black, - borderradius=2, - brush=QC.Qt.lightGray, - ), - symbol=symbol, - plot=self, - ) - self.add_curve(symbol_name, samples, symbol) - self.setAxisAutoScale(self.yLeft, True) - self.setAxisAutoScale(self.xBottom, True) - - -class CustomGraphicSymbol(qwt.QwtSymbol): - def __init__(self): - super(CustomGraphicSymbol, self).__init__(qwt.QwtSymbol.Graphic) - - # Use a built-in Qt icon as QPixmap for demonstration - icon = QW.QApplication.style().standardIcon(QW.QStyle.SP_FileIcon) - pixmap = icon.pixmap(20, 20) - - # Convert the QPixmap to a QwtGraphic - graphic = qwt.graphic.QwtGraphic() - graphic.setDefaultSize(pixmap.size()) - painter = QG.QPainter(graphic) - painter.drawPixmap(0, 0, pixmap) - painter.end() - - # Set the QwtGraphic as the graphic for the symbol - self.setGraphic(graphic) - - -class GraphicPlot(BaseSymbolPlot): - TITLE = "Custom QwtGraphic Symbol Example" - SYMBOL_CLASS = CustomGraphicSymbol - - -class CustomPixmapSymbol(qwt.QwtSymbol): - def __init__(self): - super(CustomPixmapSymbol, self).__init__(qwt.QwtSymbol.Pixmap) - - # Use a built-in Qt icon as QPixmap for demonstration - icon = QW.QApplication.style().standardIcon(QW.QStyle.SP_DialogYesButton) - pixmap = icon.pixmap(20, 20) - - # Set the QPixmap for the symbol - self.setPixmap(pixmap) - - -class PixmapPlot(BaseSymbolPlot): - TITLE = "Custom QPixmap Symbol Example" - SYMBOL_CLASS = CustomPixmapSymbol - - -class CustomPathSymbol(qwt.QwtSymbol): - def __init__(self): - super(CustomPathSymbol, self).__init__(qwt.QwtSymbol.Path) - - path = QG.QPainterPath() - path.moveTo(0, -10) # Top vertex of the triangle - path.lineTo(-10, 10) # Bottom-left vertex - path.lineTo(10, 10) # Bottom-right vertex - path.closeSubpath() # Close the triangle - - self.setPath(path) - self.setSize(20, 20) - - -class PathPlot(BaseSymbolPlot): - TITLE = "Custom Path Symbol Example" - SYMBOL_CLASS = CustomPathSymbol - - -class CustomSvgSymbol(qwt.QwtSymbol): - FNAME = osp.join(osp.dirname(__file__), "data", "symbol.svg") - - def __init__(self): - super(CustomSvgSymbol, self).__init__(qwt.QwtSymbol.SvgDocument) - - # Load the SVG document from the given file - self.setSvgDocument(self.FNAME) - - -class SvgDocumentPlot(BaseSymbolPlot): - TITLE = "Custom SVG Symbol Example" - SYMBOL_CLASS = CustomSvgSymbol - - -def test_base(): - """Base symbol test""" - utils.test_widget(BaseSymbolPlot, size=(600, 400)) - - -def test_builtin(): - """Built-in symbol test""" - utils.test_widget(BuiltinSymbolPlot, size=(600, 400)) - - -def test_graphic(): - """Graphic symbol test""" - utils.test_widget(GraphicPlot, size=(600, 400)) - - -def test_pixmap(): - """Pixmap test""" - utils.test_widget(PixmapPlot, size=(600, 400)) - - -def test_path(): - """Path symbol test""" - utils.test_widget(PathPlot, size=(600, 400)) - - -def test_svg(): - """SVG test""" - utils.test_widget(SvgDocumentPlot, size=(600, 400)) - - -if __name__ == "__main__": - # test_base() - test_builtin() - # test_graphic() - # test_pixmap() - # test_path() - # test_svg() diff --git a/qwt/tests/test_vertical.py b/qwt/tests/test_vertical.py deleted file mode 100644 index eadb18d..0000000 --- a/qwt/tests/test_vertical.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -"""Simple plot without margins""" - -SHOW = True # Show test in GUI-based test launcher - -import numpy as np -from qtpy.QtCore import Qt -from qtpy.QtGui import QColor, QPalette, QPen - -from qwt import QwtPlot, QwtPlotCurve, QwtPlotMarker, QwtText -from qwt.tests import utils - - -class VerticalPlot(QwtPlot): - def __init__(self, parent=None): - super(VerticalPlot, self).__init__(parent) - self.setWindowTitle("PythonQwt") - self.enableAxis(self.xTop, True) - self.enableAxis(self.yRight, True) - y = np.linspace(0, 10, 500) - curve1 = QwtPlotCurve.make(np.sin(y), y, title="Test Curve 1") - curve2 = QwtPlotCurve.make(y**3, y, title="Test Curve 2") - curve2.setAxes(self.xTop, self.yRight) - - for item, col, xa, ya in ( - (curve1, Qt.green, self.xBottom, self.yLeft), - (curve2, Qt.red, self.xTop, self.yRight), - ): - item.attach(self) - item.setPen(QPen(col)) - for axis_id in xa, ya: - palette = self.axisWidget(axis_id).palette() - palette.setColor(QPalette.WindowText, QColor(col)) - palette.setColor(QPalette.Text, QColor(col)) - self.axisWidget(axis_id).setPalette(palette) - ticks_font = self.axisFont(axis_id) - self.setAxisFont(axis_id, ticks_font) - - self.marker = QwtPlotMarker.make(0, 5, plot=self) - - def resizeEvent(self, event): - super(VerticalPlot, self).resizeEvent(event) - self.show_layout_details() - - def show_layout_details(self): - text = ( - "plotLayout().canvasRect():\n%r\n\n" - "canvas().geometry():\n%r\n\n" - "plotLayout().scaleRect(QwtPlot.yLeft):\n%r\n\n" - "axisWidget(QwtPlot.yLeft).geometry():\n%r\n\n" - "plotLayout().scaleRect(QwtPlot.yRight):\n%r\n\n" - "axisWidget(QwtPlot.yRight).geometry():\n%r\n\n" - "plotLayout().scaleRect(QwtPlot.xBottom):\n%r\n\n" - "axisWidget(QwtPlot.xBottom).geometry():\n%r\n\n" - "plotLayout().scaleRect(QwtPlot.xTop):\n%r\n\n" - "axisWidget(QwtPlot.xTop).geometry():\n%r\n\n" - % ( - self.plotLayout().canvasRect().getCoords(), - self.canvas().geometry().getCoords(), - self.plotLayout().scaleRect(QwtPlot.yLeft).getCoords(), - self.axisWidget(QwtPlot.yLeft).geometry().getCoords(), - self.plotLayout().scaleRect(QwtPlot.yRight).getCoords(), - self.axisWidget(QwtPlot.yRight).geometry().getCoords(), - self.plotLayout().scaleRect(QwtPlot.xBottom).getCoords(), - self.axisWidget(QwtPlot.xBottom).geometry().getCoords(), - self.plotLayout().scaleRect(QwtPlot.xTop).getCoords(), - self.axisWidget(QwtPlot.xTop).geometry().getCoords(), - ) - ) - self.marker.setLabel(QwtText.make(text, family="Courier New", color=Qt.blue)) - - -def test_vertical(): - """Vertical plot example""" - utils.test_widget(VerticalPlot, size=(300, 650)) - - -if __name__ == "__main__": - test_vertical() diff --git a/qwt/tests/utils.py b/qwt/tests/utils.py deleted file mode 100644 index 9f4d147..0000000 --- a/qwt/tests/utils.py +++ /dev/null @@ -1,323 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# Copyright (c) 2015 Pierre Raybaut -# (see LICENSE file for more details) - -""" -PythonQwt test utilities ------------------------- -""" - -import argparse -import inspect -import os -import os.path as osp -import platform -import subprocess -import sys - -from qtpy import QtCore as QC -from qtpy import QtGui as QG -from qtpy import QtWidgets as QW - -import qwt -from qwt import QwtPlot -from qwt import qthelpers as qth - -QT_API = os.environ["QT_API"] - -if QT_API.startswith("pyside"): - from qtpy import PYSIDE_VERSION - - PYTHON_QT_API = "PySide v" + PYSIDE_VERSION -else: - from qtpy import PYQT_VERSION - - PYTHON_QT_API = "PyQt v" + PYQT_VERSION - - -TEST_PATH = osp.abspath(osp.dirname(__file__)) - - -class TestEnvironment: - UNATTENDED_ARG = "unattended" - SCREENSHOTS_ARG = "screenshots" - UNATTENDED_ENV = "PYTHONQWT_UNATTENDED_TESTS" - SCREENSHOTS_ENV = "PYTHONQWT_TAKE_SCREENSHOTS" - - def __init__(self): - self.parse_args() - - @property - def unattended(self): - return os.environ.get(self.UNATTENDED_ENV) is not None - - @property - def screenshots(self): - return os.environ.get(self.SCREENSHOTS_ENV) is not None - - def parse_args(self): - """Parse command line arguments""" - parser = argparse.ArgumentParser(description="Run PythonQwt tests") - parser.add_argument( - "--mode", - choices=[self.UNATTENDED_ARG, self.SCREENSHOTS_ARG], - required=False, - ) - args, _unknown = parser.parse_known_args() - if args.mode is not None: - self.set_env_from_args(args) - - def set_env_from_args(self, args): - """Set appropriate environment variables""" - for name in (self.UNATTENDED_ENV, self.SCREENSHOTS_ENV): - if name in os.environ: - os.environ.pop(name) - if args.mode == self.UNATTENDED_ARG: - os.environ[self.UNATTENDED_ENV] = "1" - if args.mode == self.SCREENSHOTS_ARG: - os.environ[self.SCREENSHOTS_ENV] = os.environ[self.UNATTENDED_ENV] = "1" - - -def get_tests(package): - """Return list of test filenames""" - test_package_name = "%s.tests" % package.__name__ - _temp = __import__(test_package_name) - test_package = sys.modules[test_package_name] - tests = [] - test_path = osp.dirname(osp.realpath(test_package.__file__)) - for fname in sorted( - [ - name - for name in os.listdir(test_path) - if name.endswith((".py", ".pyw")) and not name.startswith(("_", "conftest")) - ] - ): - module_name = osp.splitext(fname)[0] - _temp = __import__(test_package.__name__, fromlist=[module_name]) - module = getattr(_temp, module_name) - if hasattr(module, "SHOW") and module.SHOW: - tests.append(osp.abspath(osp.join(test_path, fname))) - return tests - - -def run_test(fname, wait=True): - """Run test""" - os.environ["PYTHONPATH"] = os.pathsep.join(sys.path) - args = " ".join([sys.executable, '"' + fname + '"']) - if TestEnvironment().unattended: - print(" " + args) - (subprocess.call if wait else subprocess.Popen)(args, shell=True) - - -def run_all_tests(wait=True): - """Run all PythonQwt tests""" - for fname in get_tests(qwt): - run_test(fname, wait=wait) - - -def get_lib_versions(): - """Return string containing Python-Qt versions""" - from qtpy.QtCore import __version__ as qt_version - - return "Python %s, Qt %s, %s on %s" % ( - platform.python_version(), - qt_version, - PYTHON_QT_API, - platform.system(), - ) - - -class TestLauncher(QW.QMainWindow): - """PythonQwt Test Launcher main window""" - - COLUMNS = 5 - - def __init__(self, parent=None): - super(TestLauncher, self).__init__(parent) - self.setObjectName("TestLauncher") - icon = QG.QIcon(osp.join(TEST_PATH, "data", "PythonQwt.svg")) - self.setWindowIcon(icon) - self.setWindowTitle("PythonQwt %s - Test Launcher" % qwt.__version__) - self.setCentralWidget(QW.QWidget()) - self.grid_layout = QW.QGridLayout() - self.centralWidget().setLayout(self.grid_layout) - self.test_nb = None - self.fill_layout() - self.statusBar().show() - self.setStatusTip("Click on any button to run a test") - - def get_std_icon(self, name): - """Return Qt standard icon""" - return self.style().standardIcon(getattr(QW.QStyle, "SP_" + name)) - - def fill_layout(self): - """Fill grid layout""" - for fname in get_tests(qwt): - self.add_test(fname) - toolbar = QW.QToolBar(self) - all_act = QW.QAction(self.get_std_icon("DialogYesButton"), "", self) - all_act.setIconText("Run all tests") - all_act.triggered.connect(lambda checked: run_all_tests(wait=False)) - folder_act = QW.QAction(self.get_std_icon("DirOpenIcon"), "", self) - folder_act.setIconText("Open tests folder") - - def open_test_folder(checked): - return os.startfile(TEST_PATH) - - folder_act.triggered.connect(open_test_folder) - about_act = QW.QAction(self.get_std_icon("FileDialogInfoView"), "", self) - about_act.setIconText("About") - about_act.triggered.connect(self.about) - for action in (all_act, folder_act, None, about_act): - if action is None: - toolbar.addSeparator() - else: - toolbar.addAction(action) - toolbar.setToolButtonStyle(QC.Qt.ToolButtonTextBesideIcon) - self.addToolBar(toolbar) - - def add_test(self, fname): - """Add new test""" - if self.test_nb is None: - self.test_nb = 0 - self.test_nb += 1 - column = (self.test_nb - 1) % self.COLUMNS - row = (self.test_nb - 1) // self.COLUMNS - bname = osp.basename(fname) - button = QW.QToolButton(self) - button.setToolButtonStyle(QC.Qt.ToolButtonTextUnderIcon) - shot = osp.join( - TEST_PATH, "data", bname.replace(".py", ".png").replace("test_", "") - ) - if osp.isfile(shot): - button.setIcon(QG.QIcon(shot)) - else: - button.setIcon(self.get_std_icon("DialogYesButton")) - button.setText(bname) - button.setToolTip(fname) - button.setIconSize(QC.QSize(130, 80)) - button.clicked.connect(lambda checked=None, fname=fname: run_test(fname)) - self.grid_layout.addWidget(button, row, column) - - def about(self): - """About test launcher""" - QW.QMessageBox.about( - self, - "About " + self.windowTitle(), - """%s

Developped by Pierre Raybaut -
Copyright © 2020 Pierre Raybaut -

%s""" - % (self.windowTitle(), get_lib_versions()), - ) - - -class TestOptions(QW.QGroupBox): - """Test options groupbox""" - - def __init__(self, parent=None): - super(TestOptions, self).__init__("Test options", parent) - self.setLayout(QW.QFormLayout()) - self.hide() - - def add_checkbox(self, title, label, slot): - """Add new checkbox to option panel""" - widget = QW.QCheckBox(label, self) - widget.stateChanged.connect(slot) - self.layout().addRow(title, widget) - self.show() - return widget - - -class TestCentralWidget(QW.QWidget): - """Test central widget""" - - def __init__(self, widget_name, parent=None): - super(TestCentralWidget, self).__init__(parent) - self.widget_name = widget_name - self.plots = None - self.setLayout(QW.QVBoxLayout()) - self.options = TestOptions(self) - self.add_widget(self.options) - - def get_widget_of_interest(self): - """Return widget of interest""" - if self.plots is not None and len(self.plots) == 1: - return self.plots[0] - return self.parent() - - def add_widget(self, widget): - """Add new sub-widget""" - self.layout().addWidget(widget) - if isinstance(widget, QwtPlot): - self.plots = [widget] - else: - self.plots = widget.findChildren(QwtPlot) - for index, plot in enumerate(self.plots): - plot_name = plot.objectName() - if not plot_name: - plot_name = "Plot #%d" % (index + 1) - widget = self.options.add_checkbox( - plot_name, "Enable new flat style option", plot.setFlatStyle - ) - widget.setChecked(plot.flatStyle()) - - -def take_screenshot(widget): - """Take screenshot and save it to the data folder""" - bname = (widget.objectName().lower() + ".png").replace("window", "") - bname = bname.replace("plot", "").replace("widget", "") - qth.take_screenshot(widget, osp.join(TEST_PATH, "data", bname), quit=True) - - -def close_widgets_and_quit() -> None: - """Close Qt top level widgets and quit Qt event loop""" - QW.QApplication.processEvents() - for widget in QW.QApplication.instance().topLevelWidgets(): - assert widget.close() - QC.QTimer.singleShot(0, QW.QApplication.instance().quit) - - -def test_widget(widget_class, size=None, title=None, options=True): - """Test widget""" - widget_name = widget_class.__name__ - app = QW.QApplication.instance() - if app is None: - app = QW.QApplication([]) - test_env = TestEnvironment() - if inspect.signature(widget_class).parameters.get("unattended") is None: - widget = widget_class() - else: - widget = widget_class(unattended=test_env.unattended) - window = widget - if options: - if isinstance(widget, QW.QMainWindow): - widget = window.centralWidget() - widget.setParent(None) - else: - window = QW.QMainWindow() - central_widget = TestCentralWidget(widget_name, parent=window) - central_widget.add_widget(widget) - window.setCentralWidget(central_widget) - widget_of_interest = central_widget.get_widget_of_interest() - else: - widget_of_interest = window - widget_of_interest.setObjectName(widget_name) - if title is None: - title = 'Test "%s" - PythonQwt %s' % (widget_name, qwt.__version__) - window.setWindowTitle(title) - if size is not None: - width, height = size - window.resize(width, height) - - window.show() - if test_env.screenshots: - QC.QTimer.singleShot(1000, lambda: take_screenshot(widget_of_interest)) - elif test_env.unattended: - QC.QTimer.singleShot(0, close_widgets_and_quit) - if QT_API == "pyside6": - app.exec() - else: - app.exec_() - return app diff --git a/qwt/text.py b/qwt/text.py deleted file mode 100644 index 2fb0f9a..0000000 --- a/qwt/text.py +++ /dev/null @@ -1,1524 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -Text widgets ------------- - -QwtText -~~~~~~~ - -.. autoclass:: QwtText - :members: - -QwtTextLabel -~~~~~~~~~~~~ - -.. autoclass:: QwtTextLabel - :members: - -Text engines ------------- - -QwtTextEngine -~~~~~~~~~~~~~ - -.. autoclass:: QwtTextEngine - :members: - -QwtPlainTextEngine -~~~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtPlainTextEngine - :members: - -QwtRichTextEngine -~~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtRichTextEngine - :members: -""" - -import math -import os -import struct - -from qtpy.QtCore import QObject, QRectF, QSize, QSizeF, Qt -from qtpy.QtGui import ( - QAbstractTextDocumentLayout, - QColor, - QFont, - QFontInfo, - QFontMetrics, - QFontMetricsF, - QPainter, - QPalette, - QPixmap, - QTextDocument, - QTextOption, - QTransform, -) -from qtpy.QtWidgets import QApplication, QFrame, QSizePolicy, QWidget - -from qwt.painter import QwtPainter -from qwt.qthelpers import qcolor_from_str - -QWIDGETSIZE_MAX = (1 << 24) - 1 - -QT_API = os.environ["QT_API"] - - -# Cache Qt alignment flags as plain ints once at import time. On PyQt6 these -# are ``Qt.AlignmentFlag`` enum members and every bitwise test goes through -# ``enum.__and__`` (~6 us each). The test code below combines them in hot -# paths called per-tick / per-label / per-paint event. -def _flag_int(flag): - """Return the integer value of a Qt enum/flag (PyQt5 and PyQt6).""" - try: - return flag.value - except AttributeError: - return int(flag) - - -_ALIGN_LEFT = _flag_int(Qt.AlignLeft) -_ALIGN_RIGHT = _flag_int(Qt.AlignRight) -_ALIGN_TOP = _flag_int(Qt.AlignTop) -_ALIGN_BOTTOM = _flag_int(Qt.AlignBottom) -_ALIGN_HCENTER = _flag_int(Qt.AlignHCenter) -_ALIGN_JUSTIFY = _flag_int(Qt.AlignJustify) -_ALIGN_CENTER = _flag_int(Qt.AlignCenter) - - -def taggedRichText(text, flags): - richText = text - if flags & _ALIGN_JUSTIFY: - richText = '

' + richText + "
" - elif flags & _ALIGN_RIGHT: - richText = '
' + richText + "
" - elif flags & _ALIGN_HCENTER: - richText = '
' + richText + "
" - return richText - - -class QwtRichTextDocument(QTextDocument): - def __init__(self, text, flags, font): - super(QwtRichTextDocument, self).__init__(None) - self.setUndoRedoEnabled(False) - self.setDefaultFont(font) - self.setHtml(text) - - option = self.defaultTextOption() - if flags & Qt.TextWordWrap: - option.setWrapMode(QTextOption.WordWrap) - else: - option.setWrapMode(QTextOption.NoWrap) - - option.setAlignment(flags) - self.setDefaultTextOption(option) - - root = self.rootFrame() - fm = root.frameFormat() - fm.setBorder(0) - fm.setMargin(0) - fm.setPadding(0) - fm.setBottomMargin(0) - fm.setLeftMargin(0) - root.setFrameFormat(fm) - - self.adjustSize() - - -class QwtTextEngine(object): - """ - Abstract base class for rendering text strings - - A text engine is responsible for rendering texts for a - specific text format. They are used by `QwtText` to render a text. - - `QwtPlainTextEngine` and `QwtRichTextEngine` are part of the - `PythonQwt` library. - - .. seealso:: - - :py:meth:`qwt.text.QwtText.setTextEngine()` - """ - - def __init__(self): - pass - - def heightForWidth(self, font, flags, text, width): - """ - Find the height for a given width - - :param QFont font: Font of the text - :param int flags: Bitwise OR of the flags used like in QPainter::drawText - :param str text: Text to be rendered - :param float width: Width - :return: Calculated height - """ - pass - - def textSize(self, font, flags, text): - """ - Returns the size, that is needed to render text - - :param QFont font: Font of the text - :param int flags: Bitwise OR of the flags like in for QPainter::drawText - :param str text: Text to be rendered - :return: Calculated size - """ - pass - - def mightRender(self, text): - """ - Test if a string can be rendered by this text engine - - :param str text: Text to be tested - :return: True, if it can be rendered - """ - pass - - def textMargins(self, font): - """ - Return margins around the texts - - The textSize might include margins around the - text, like QFontMetrics::descent(). In situations - where texts need to be aligned in detail, knowing - these margins might improve the layout calculations. - - :param QFont font: Font of the text - :return: tuple (left, right, top, bottom) representing margins - """ - pass - - def draw(self, painter, rect, flags, text): - """ - Draw the text in a clipping rectangle - - :param QPainter painter: Painter - :param QRectF rect: Clipping rectangle - :param int flags: Bitwise OR of the flags like in for QPainter::drawText() - :param str text: Text to be rendered - """ - pass - - -ASCENTCACHE = {} - -# Module-level cache: ``id(font) -> tuple_key`` (fast path) and -# ``tuple_key -> tuple_key`` (slow path). The tuple key is built from a -# handful of QFont attributes that uniquely identify the *logical* font for -# metrics purposes. Tick-rendering uses very few distinct fonts in practice -# so both dicts stay tiny. -# -# This replaces the previous ``id(font) -> font.key()`` design. Two reasons: -# -# 1. ``QFont.key()`` is a sip dispatch that costs ~3.3 us/call on PyQt5 and -# ~9.3 us/call on PyQt6 -- it became the single biggest residual hotspot -# in ``QwtText.textSize`` on PyQt6. -# 2. PyQt6 returns a fresh Python wrapper around the same QFont on most -# calls, so ``id(font)`` changes between calls and the id-keyed fast path -# misses ~92% of the time. The tuple-key second level recovers the hits -# those misses would have produced, without paying for ``font.key()``. -# -# The tuple key uses ``(family, pixelSize-or-pointSizeF, weight, italic, -# stretch, styleStrategy)``. This is what determines ``QFontMetrics`` output -# in practice; if two QFonts share these values they share metrics. - -_FONT_KEY_CACHE: dict = {} # id(font) -> tuple_key (fast path) -_FONT_TUPLE_CACHE: dict = {} # tuple_key -> tuple_key (interning, also acts -# as the "have we seen this logical font" set) -_FONT_KEY_CACHE_LIMIT = 1024 - - -def _font_tuple_key(font): - """Build a hashable tuple identifying the logical font.""" - px = font.pixelSize() - return ( - font.family(), - px if px > 0 else font.pointSizeF(), - font.weight(), - font.italic(), - font.stretch(), - font.styleStrategy(), - ) - - -def font_key_cached(font): - """Return a hashable cache key uniquely identifying ``font`` for metrics. - - The returned value is **not** ``QFont.key()`` -- it is a tuple computed - from a handful of QFont attributes. It is safe to use as a dict key for - metrics caches (callers in this module always compare by ``==`` only). - """ - fid = id(font) - entry = _FONT_KEY_CACHE.get(fid) - if entry is not None: - return entry[1] - tkey = _font_tuple_key(font) - # Intern: reuse the same tuple object across all id() variants so dict - # lookups in caller-side caches benefit from object-identity hash hits. - interned = _FONT_TUPLE_CACHE.setdefault(tkey, tkey) - if len(_FONT_KEY_CACHE) >= _FONT_KEY_CACHE_LIMIT: - _FONT_KEY_CACHE.clear() - _FONT_KEY_CACHE[fid] = (font, interned) - return interned - - -def get_screen_resolution(): - """Return screen resolution: tuple of floats (DPIx, DPIy)""" - try: - desktop = QApplication.desktop() - return (desktop.logicalDpiX(), desktop.logicalDpiY()) - except AttributeError: - screen = QApplication.primaryScreen() - return (screen.logicalDotsPerInchX(), screen.logicalDotsPerInchY()) - - -def qwtUnscaleFont(painter): - if painter.font().pixelSize() >= 0: - return - dpix, dpiy = get_screen_resolution() - pd = painter.device() - if pd.logicalDpiX() != dpix or pd.logicalDpiY() != dpiy: - try: - pixelFont = QFont(painter.font(), QApplication.desktop()) - except AttributeError: - pixelFont = QFont(painter.font()) - pixelFont.setPixelSize(QFontInfo(pixelFont).pixelSize()) - painter.setFont(pixelFont) - - -class QwtPlainTextEngine(QwtTextEngine): - """ - A text engine for plain texts - - `QwtPlainTextEngine` renders texts using the basic `Qt` classes - `QPainter` and `QFontMetrics`. - """ - - def __init__(self): - self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) - self._fm_cache = {} - self._fm_cache_f = {} - self._margins_cache = {} - # Fast path: when textMargins is called repeatedly with the same - # QFont instance, skip the (expensive) font.key() Qt call. - self._margins_last_id = -1 - self._margins_last_value = None - - def fontmetrics(self, font): - fid = font_key_cached(font) - try: - return self._fm_cache[fid] - except KeyError: - return self._fm_cache.setdefault(fid, QFontMetrics(font)) - - def fontmetrics_f(self, font): - fid = font_key_cached(font) - try: - return self._fm_cache_f[fid] - except KeyError: - return self._fm_cache_f.setdefault(fid, QFontMetricsF(font)) - - def heightForWidth(self, font, flags, text, width): - """ - Find the height for a given width - - :param QFont font: Font of the text - :param int flags: Bitwise OR of the flags used like in QPainter::drawText - :param str text: Text to be rendered - :param float width: Width - :return: Calculated height - """ - fm = self.fontmetrics_f(font) - rect = fm.boundingRect(QRectF(0, 0, width, QWIDGETSIZE_MAX), flags, text) - return rect.height() - - def textSize(self, font, flags, text): - """ - Returns the size, that is needed to render text - - :param QFont font: Font of the text - :param int flags: Bitwise OR of the flags like in for QPainter::drawText - :param str text: Text to be rendered - :return: Calculated size - """ - fm = self.fontmetrics_f(font) - rect = fm.boundingRect(self.qrectf_max, flags, text) - return rect.size() - - def effectiveAscent(self, font): - global ASCENTCACHE - fontKey = font_key_cached(font) - ascent = ASCENTCACHE.get(fontKey) - if ascent is not None: - return ascent - return ASCENTCACHE.setdefault(fontKey, self.findAscent(font)) - - def findAscent(self, font): - dummy = "E" - white = QColor(Qt.white) - - fm = self.fontmetrics(font) - boundingr = fm.boundingRect(dummy) - pm = QPixmap(boundingr.width(), boundingr.height()) - pm.fill(white) - - p = QPainter(pm) - p.setFont(font) - p.drawText(0, 0, pm.width(), pm.height(), 0, dummy) - p.end() - - img = pm.toImage() - - w = pm.width() - linebytes = w * 4 - for row in range(img.height()): - if QT_API.startswith("pyside"): - line = bytes(img.scanLine(row)) - else: - line = img.scanLine(row).asstring(linebytes) - for col in range(w): - color = struct.unpack("I", line[col * 4 : (col + 1) * 4])[0] - if color != white.rgb(): - return fm.ascent() - row + 1 - return fm.ascent() - - def textMargins(self, font): - """ - Return margins around the texts - - The textSize might include margins around the - text, like QFontMetrics::descent(). In situations - where texts need to be aligned in detail, knowing - these margins might improve the layout calculations. - - :param QFont font: Font of the text - :return: tuple (left, right, top, bottom) representing margins - """ - # Fast path: same QFont object as the previous call. - font_id = id(font) - if font_id == self._margins_last_id: - return self._margins_last_value - fkey = font_key_cached(font) - cached = self._margins_cache.get(fkey) - if cached is None: - fm = self.fontmetrics(font) - cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent()) - self._margins_cache[fkey] = cached - self._margins_last_id = font_id - self._margins_last_value = cached - return cached - - def draw(self, painter, rect, flags, text): - """ - Draw the text in a clipping rectangle - - :param QPainter painter: Painter - :param QRectF rect: Clipping rectangle - :param int flags: Bitwise OR of the flags like in for QPainter::drawText() - :param str text: Text to be rendered - """ - painter.save() - - # Get and configure font for better rendering of rotated text - font = painter.font() - # Disable hinting to avoid character misalignment in rotated text - font.setHintingPreference(QFont.PreferNoHinting) - painter.setFont(font) - - qwtUnscaleFont(painter) - painter.drawText(rect, flags, text) - painter.restore() - - def mightRender(self, text): - """ - Test if a string can be rendered by this text engine - - :param str text: Text to be tested - :return: True, if it can be rendered - """ - return True - - -class QwtRichTextEngine(QwtTextEngine): - """ - A text engine for `Qt` rich texts - - `QwtRichTextEngine` renders `Qt` rich texts using the classes - of the Scribe framework of `Qt`. - """ - - def __init__(self): - pass - - def heightForWidth(self, font, flags, text, width): - """ - Find the height for a given width - - :param QFont font: Font of the text - :param int flags: Bitwise OR of the flags used like in QPainter::drawText - :param str text: Text to be rendered - :param float width: Width - :return: Calculated height - """ - doc = QwtRichTextDocument(text, flags, font) - doc.setPageSize(QSizeF(width, QWIDGETSIZE_MAX)) - return doc.documentLayout().documentSize().height() - - def textSize(self, font, flags, text): - """ - Returns the size, that is needed to render text - - :param QFont font: Font of the text - :param int flags: Bitwise OR of the flags like in for QPainter::drawText - :param str text: Text to be rendered - :return: Calculated size - """ - doc = QwtRichTextDocument(text, flags, font) - option = doc.defaultTextOption() - if option.wrapMode() != QTextOption.NoWrap: - option.setWrapMode(QTextOption.NoWrap) - doc.setDefaultTextOption(option) - doc.adjustSize() - return doc.size() - - def draw(self, painter, rect, flags, text): - """ - Draw the text in a clipping rectangle - - :param QPainter painter: Painter - :param QRectF rect: Clipping rectangle - :param int flags: Bitwise OR of the flags like in for QPainter::drawText() - :param str text: Text to be rendered - """ - txt = QwtRichTextDocument(text, flags, painter.font()) - painter.save() - unscaledRect = QRectF(rect) - if painter.font().pixelSize() < 0: - dpix, dpiy = get_screen_resolution() - pd = painter.device() - if pd.logicalDpiX() != dpix or pd.logicalDpiY() != dpiy: - transform = QTransform() - transform.scale( - dpix / float(pd.logicalDpiX()), dpiy / float(pd.logicalDpiY()) - ) - painter.setWorldTransform(transform, True) - invtrans, _ok = transform.inverted() - unscaledRect = invtrans.mapRect(rect) - txt.setDefaultFont(painter.font()) - txt.setPageSize(QSizeF(unscaledRect.width(), QWIDGETSIZE_MAX)) - layout = txt.documentLayout() - height = layout.documentSize().height() - y = unscaledRect.y() - if flags & Qt.AlignBottom: - y += unscaledRect.height() - height - elif flags & Qt.AlignVCenter: - y += (unscaledRect.height() - height) / 2 - context = QAbstractTextDocumentLayout.PaintContext() - context.palette.setColor(QPalette.Text, painter.pen().color()) - painter.translate(unscaledRect.x(), y) - layout.draw(painter, context) - painter.restore() - - def taggedText(self, text, flags): - return taggedRichText(text, flags) - - def mightRender(self, text): - """ - Test if a string can be rendered by this text engine - - :param str text: Text to be tested - :return: True, if it can be rendered - """ - try: - return Qt.mightBeRichText(text) - except AttributeError: - return True - - def textMargins(self, font): - """ - Return margins around the texts - - The textSize might include margins around the - text, like QFontMetrics::descent(). In situations - where texts need to be aligned in detail, knowing - these margins might improve the layout calculations. - - :param QFont font: Font of the text - :return: tuple (left, right, top, bottom) representing margins - """ - return 0, 0, 0, 0 - - -class QwtText_PrivateData(object): - # ``QObject`` was previously used as the base class but no Qt signals - # or events are ever emitted from ``_PrivateData`` containers and the - # ``QObject.__init__`` call dominates ``QwtText.__init__`` (it is the - # single most expensive line for tick-label-heavy renders, see - # https://github.com/PlotPyStack/PythonQwt/issues/93). - __slots__ = ( - "renderFlags", - "borderRadius", - "borderPen", - "backgroundBrush", - "paintAttributes", - "layoutAttributes", - "textEngine", - "text", - "font", - "color", - ) - - def __init__(self): - self.renderFlags = Qt.AlignCenter - self.borderRadius = 0 - self.borderPen = Qt.NoPen - self.backgroundBrush = Qt.NoBrush - self.paintAttributes = 0 - self.layoutAttributes = 0 - self.textEngine = None - - self.text = None - self.font = None - self.color = None - - -class QwtText_LayoutCache(object): - def __init__(self): - self.textSize = None - self.fontKey = None - self.fontId = -1 - - def invalidate(self): - self.textSize = None - self.fontKey = None - self.fontId = -1 - - -class QwtText(object): - """ - A class representing a text - - A `QwtText` is a text including a set of attributes how to render it. - - - Format: - - A text might include control sequences (f.e tags) describing - how to render it. Each format (f.e MathML, TeX, Qt Rich Text) - has its own set of control sequences, that can be handles by - a special `QwtTextEngine` for this format. - - - Background: - - A text might have a background, defined by a `QPen` and `QBrush` - to improve its visibility. The corners of the background might - be rounded. - - - Font: - - A text might have an individual font. - - - Color - - A text might have an individual color. - - - Render Flags - - Flags from `Qt.AlignmentFlag` and `Qt.TextFlag` used like in - `QPainter.drawText()`. - - ..seealso:: - - :py:meth:`qwt.text.QwtTextEngine`, - :py:meth:`qwt.text.QwtTextLabel` - - Text formats: - - * `QwtText.AutoText`: - - The text format is determined using `QwtTextEngine.mightRender()` for - all available text engines in increasing order > PlainText. - If none of the text engines can render the text is rendered - like `QwtText.PlainText`. - - * `QwtText.PlainText`: - - Draw the text as it is, using a QwtPlainTextEngine. - - * `QwtText.RichText`: - - Use the Scribe framework (Qt Rich Text) to render the text. - - * `QwtText.OtherFormat`: - - The number of text formats can be extended using `setTextEngine`. - Formats >= `QwtText.OtherFormat` are not used by Qwt. - - Paint attributes: - - * `QwtText.PaintUsingTextFont`: The text has an individual font. - * `QwtText.PaintUsingTextColor`: The text has an individual color. - * `QwtText.PaintBackground`: The text has an individual background. - - Layout attributes: - - * `QwtText.MinimumLayout`: - - Layout the text without its margins. This mode is useful if a - text needs to be aligned accurately, like the tick labels of a scale. - If `QwtTextEngine.textMargins` is not implemented for the format - of the text, `MinimumLayout` has no effect. - - .. py:class:: QwtText([text=None], [textFormat=None], [other=None]) - - :param str text: Text content - :param int textFormat: Text format - :param qwt.text.QwtText other: Object to copy (text and textFormat arguments are ignored) - """ - - # enum TextFormat - AutoText, PlainText, RichText = list(range(3)) - OtherFormat = 100 - - # enum PaintAttribute - PaintUsingTextFont = 0x01 - PaintUsingTextColor = 0x02 - PaintBackground = 0x04 - - # enum LayoutAttribute - MinimumLayout = 0x01 - - # Optimization: a single text engine for all QwtText objects - # (this is not how it's implemented in Qwt6 C++ library) - __map = {PlainText: QwtPlainTextEngine(), RichText: QwtRichTextEngine()} - - def __init__(self, text=None, textFormat=None, other=None): - if text is None: - text = "" - if textFormat is None: - textFormat = self.AutoText - if other is not None: - text = other - if isinstance(text, QwtText): - self.__data = text.__data - self.__layoutCache = text.__layoutCache - else: - self.__data = QwtText_PrivateData() - self.__data.text = text - self.__data.textEngine = self.textEngine(text, textFormat) - self.__layoutCache = QwtText_LayoutCache() - - @classmethod - def make( - cls, - text=None, - textformat=None, - renderflags=None, - font=None, - family=None, - pointsize=None, - weight=None, - color=None, - borderradius=None, - borderpen=None, - brush=None, - ): - """ - Create and setup a new `QwtText` object (convenience function). - - :param str text: Text content - :param int textformat: Text format - :param int renderflags: Flags from `Qt.AlignmentFlag` and `Qt.TextFlag` - :param font: Font - :type font: QFont or None - :param family: Font family (default: Helvetica) - :type family: str or None - :param pointsize: Font point size (default: 10) - :type pointsize: int or None - :param weight: Font weight (default: QFont.Normal) - :type weight: int or None - :param color: Pen color - :type color: QColor or str or None - :param borderradius: Radius for the corners of the border frame - :type borderradius: float or None - :param borderpen: Background pen - :type borderpen: QPen or None - :param brush: Background brush - :type brush: QBrush or None - - .. seealso:: - - :py:meth:`setText()` - """ - item = cls(text=text, textFormat=textformat) - if renderflags is not None: - item.setRenderFlags(renderflags) - if font is not None: - item.setFont(font) - elif family is not None or pointsize is not None or weight is not None: - family = "Helvetica" if family is None else family - pointsize = 10 if pointsize is None else pointsize - weight = QFont.Normal if weight is None else weight - item.setFont(QFont(family, pointsize, weight)) - if color is not None: - item.setColor(qcolor_from_str(color, Qt.black)) - if borderradius is not None: - item.setBorderRadius(borderradius) - if borderpen is not None: - item.setBorderPen(borderpen) - if brush is not None: - item.setBackgroundBrush(brush) - return item - - def __eq__(self, other): - return ( - self.__data.renderFlags == other.__data.renderFlags - and self.__data.text == other.__data.text - and self.__data.font == other.__data.font - and self.__data.color == other.__data.color - and self.__data.borderRadius == other.__data.borderRadius - and self.__data.borderPen == other.__data.borderPen - and self.__data.backgroundBrush == other.__data.backgroundBrush - and self.__data.paintAttributes == other.__data.paintAttributes - and self.__data.textEngine == other.__data.textEngine - ) - - def __ne__(self, other): - return not self.__eq__(other) - - def isEmpty(self): - """ - :return: True if text is empty - """ - return len(self.text()) == 0 - - def setText(self, text, textFormat=None): - """ - Assign a new text content - - :param str text: Text content - :param int textFormat: Text format - - .. seealso:: - - :py:meth:`text()` - """ - if textFormat is None: - textFormat = self.AutoText - self.__data.text = text - self.__data.textEngine = self.textEngine(text, textFormat) - self.__layoutCache.invalidate() - - def text(self): - """ - :return: Text content - - .. seealso:: - - :py:meth:`setText()` - """ - return self.__data.text - - def setRenderFlags(self, renderFlags): - """ - Change the render flags - - The default setting is `Qt.AlignCenter` - - :param int renderFlags: Bitwise OR of the flags used like in `QPainter.drawText()` - - .. seealso:: - - :py:meth:`renderFlags()`, - :py:meth:`qwt.text.QwtTextEngine.draw()` - """ - # Wrap into Qt.AlignmentFlag so that downstream Qt APIs (notably - # ``QTextOption.setAlignment``, ``QPainter.drawText``, - # ``QFontMetrics.boundingRect``) that strictly require an enum on - # PyQt6 keep working. Hot bitwise-test sites locally cast back to - # int to avoid the per-test enum.__and__ cost. - if not isinstance(renderFlags, Qt.AlignmentFlag): - renderFlags = Qt.AlignmentFlag(renderFlags) - if renderFlags != self.__data.renderFlags: - self.__data.renderFlags = renderFlags - self.__layoutCache.invalidate() - - def renderFlags(self): - """ - :return: Render flags - - .. seealso:: - - :py:meth:`setRenderFlags()` - """ - return self.__data.renderFlags - - def setFont(self, font): - """ - Set the font. - - :param QFont font: Font - - .. note:: - - Setting the font might have no effect, when - the text contains control sequences for setting fonts. - - .. seealso:: - - :py:meth:`font()`, :py:meth:`usedFont()` - """ - self.__data.font = font - self.setPaintAttribute(self.PaintUsingTextFont) - - def font(self): - """ - :return: Return the font - - .. seealso:: - - :py:meth:`setFont()`, :py:meth:`usedFont()` - """ - return self.__data.font - - def usedFont(self, defaultFont): - """ - Return the font of the text, if it has one. - Otherwise return defaultFont. - - :param QFont defaultFont: Default font - :return: Font used for drawing the text - - .. seealso:: - - :py:meth:`setFont()`, :py:meth:`font()` - """ - if self.__data.paintAttributes & self.PaintUsingTextFont: - return self.__data.font - return defaultFont - - def setColor(self, color): - """ - Set the pen color used for drawing the text. - - :param QColor color: Color - - .. note:: - - Setting the color might have no effect, when - the text contains control sequences for setting colors. - - .. seealso:: - - :py:meth:`color()`, :py:meth:`usedColor()` - """ - self.__data.color = QColor(color) - self.setPaintAttribute(self.PaintUsingTextColor) - - def color(self): - """ - :return: Return the pen color, used for painting the text - - .. seealso:: - - :py:meth:`setColor()`, :py:meth:`usedColor()` - """ - return self.__data.color - - def usedColor(self, defaultColor): - """ - Return the color of the text, if it has one. - Otherwise return defaultColor. - - :param QColor defaultColor: Default color - :return: Color used for drawing the text - - .. seealso:: - - :py:meth:`setColor()`, :py:meth:`color()` - """ - if self.__data.paintAttributes & self.PaintUsingTextColor: - return self.__data.color - return defaultColor - - def setBorderRadius(self, radius): - """ - Set the radius for the corners of the border frame - - :param float radius: Radius of a rounded corner - - .. seealso:: - - :py:meth:`borderRadius()`, :py:meth:`setBorderPen()`, - :py:meth:`setBackgroundBrush()` - """ - self.__data.borderRadius = max([0.0, radius]) - - def borderRadius(self): - """ - :return: Radius for the corners of the border frame - - .. seealso:: - - :py:meth:`setBorderRadius()`, :py:meth:`borderPen()`, - :py:meth:`backgroundBrush()` - """ - return self.__data.borderRadius - - def setBorderPen(self, pen): - """ - Set the background pen - - :param QPen pen: Background pen - - .. seealso:: - - :py:meth:`borderPen()`, :py:meth:`setBackgroundBrush()` - """ - self.__data.borderPen = pen - self.setPaintAttribute(self.PaintBackground) - - def borderPen(self): - """ - :return: Background pen - - .. seealso:: - - :py:meth:`setBorderPen()`, :py:meth:`backgroundBrush()` - """ - return self.__data.borderPen - - def setBackgroundBrush(self, brush): - """ - Set the background brush - - :param QBrush brush: Background brush - - .. seealso:: - - :py:meth:`backgroundBrush()`, :py:meth:`setBorderPen()` - """ - self.__data.backgroundBrush = brush - self.setPaintAttribute(self.PaintBackground) - - def backgroundBrush(self): - """ - :return: Background brush - - .. seealso:: - - :py:meth:`setBackgroundBrush()`, :py:meth:`borderPen()` - """ - return self.__data.backgroundBrush - - def setPaintAttribute(self, attribute, on=True): - """ - Change a paint attribute - - :param int attribute: Paint attribute - :param bool on: On/Off - - .. note:: - - Used by `setFont()`, `setColor()`, `setBorderPen()` - and `setBackgroundBrush()` - - .. seealso:: - - :py:meth:`testPaintAttribute()` - """ - if on: - self.__data.paintAttributes |= attribute - else: - self.__data.paintAttributes &= ~attribute - - def testPaintAttribute(self, attribute): - """ - Test a paint attribute - - :param int attribute: Paint attribute - :return: True, if attribute is enabled - - .. seealso:: - - :py:meth:`setPaintAttribute()` - """ - return self.__data.paintAttributes & attribute - - def setLayoutAttribute(self, attribute, on=True): - """ - Change a layout attribute - - :param int attribute: Layout attribute - :param bool on: On/Off - - .. seealso:: - - :py:meth:`testLayoutAttribute()` - """ - if on: - self.__data.layoutAttributes |= attribute - else: - self.__data.layoutAttributes &= ~attribute - - def testLayoutAttribute(self, attribute): - """ - Test a layout attribute - - :param int attribute: Layout attribute - :return: True, if attribute is enabled - - .. seealso:: - - :py:meth:`setLayoutAttribute()` - """ - return self.__data.layoutAttributes & attribute - - def heightForWidth(self, width, defaultFont=None): - """ - Find the height for a given width - - :param float width: Width - :param QFont defaultFont: Font, used for the calculation if the text has no font - :return: Calculated height - """ - if defaultFont is None: - defaultFont = QFont() - font = QFont(self.usedFont(defaultFont)) - h = 0 - if self.__data.layoutAttributes & self.MinimumLayout: - (left, right, top, bottom) = self.__data.textEngine.textMargins(font) - h = self.__data.textEngine.heightForWidth( - font, self.__data.renderFlags, self.__data.text, width + left + right - ) - h -= top + bottom - else: - h = self.__data.textEngine.heightForWidth( - font, self.__data.renderFlags, self.__data.text, width - ) - return h - - def textSize(self, defaultFont): - """ - Returns the size, that is needed to render text - - :param QFont defaultFont Font, used for the calculation if the text has no font - :return: Caluclated size - """ - font = self.usedFont(defaultFont) - cache = self.__layoutCache - font_id = id(font) - if cache.textSize is not None and cache.fontId == font_id: - sz = QSizeF(cache.textSize) - else: - fkey = font_key_cached(font) - if ( - cache.textSize is None - or not cache.textSize.isValid() - or cache.fontKey != fkey - ): - cache.textSize = self.__data.textEngine.textSize( - font, self.__data.renderFlags, self.__data.text - ) - cache.fontKey = fkey - cache.fontId = font_id - sz = QSizeF(cache.textSize) - if self.__data.layoutAttributes & self.MinimumLayout: - (left, right, top, bottom) = self.__data.textEngine.textMargins(font) - sz -= QSizeF(left + right, top + bottom) - return sz - - def draw(self, painter, rect): - """ - Draw a text into a rectangle - - :param QPainter painter: Painter - :param QRectF rect: Rectangle - """ - if self.__data.paintAttributes & self.PaintBackground: - if ( - self.__data.borderPen != Qt.NoPen - or self.__data.backgroundBrush != Qt.NoBrush - ): - painter.save() - painter.setPen(self.__data.borderPen) - painter.setBrush(self.__data.backgroundBrush) - if self.__data.borderRadius == 0: - painter.drawRect(rect) - else: - painter.setRenderHint(QPainter.Antialiasing, True) - painter.drawRoundedRect( - rect, self.__data.borderRadius, self.__data.borderRadius - ) - painter.restore() - painter.save() - if self.__data.paintAttributes & self.PaintUsingTextFont: - painter.setFont(self.__data.font) - if self.__data.paintAttributes & self.PaintUsingTextColor: - if self.__data.color.isValid(): - painter.setPen(self.__data.color) - expandedRect = rect - if self.__data.layoutAttributes & self.MinimumLayout: - font = QFont(painter.font()) - (left, right, top, bottom) = self.__data.textEngine.textMargins(font) - expandedRect.setTop(rect.top() - top) - expandedRect.setBottom(rect.bottom() + bottom) - expandedRect.setLeft(rect.left() - left) - expandedRect.setRight(rect.right() + right) - self.__data.textEngine.draw( - painter, expandedRect, self.__data.renderFlags, self.__data.text - ) - painter.restore() - - def textEngine(self, text=None, format_=None): - """ - Find the text engine for a text format - - In case of `QwtText.AutoText` the first text engine - (beside `QwtPlainTextEngine`) is returned, where - `QwtTextEngine.mightRender` returns true. - If there is none `QwtPlainTextEngine` is returned. - - If no text engine is registered for the format `QwtPlainTextEngine` - is returned. - - :param str text: Text, needed in case of AutoText - :param int format: Text format - :return: Corresponding text engine - """ - if text is None: - return self.__map.get(format_) - elif format_ is not None: - if format_ == QwtText.AutoText: - # Fast path: a string with no ``<`` cannot be rich text, so - # we can return the plain engine without iterating the map - # and calling Qt.mightBeRichText (which is a hot Qt call - # for tick labels like " 1.5"). - if "<" not in text: - return self.__map[QwtText.PlainText] - for key, engine in self.__map.items(): - if key != QwtText.PlainText: - if engine and engine.mightRender(text): - return engine - engine = self.__map.get(format_) - if engine is not None: - return engine - return self.__map[QwtText.PlainText] - else: - raise TypeError( - "%s().textEngine() takes 1 or 2 argument(s) (none" - " given)" % self.__class__.__name__ - ) - - def setTextEngine(self, format_, engine): - """ - Assign/Replace a text engine for a text format - - With setTextEngine it is possible to extend `PythonQwt` with - other types of text formats. - - For `QwtText.PlainText` it is not allowed to assign a engine to None. - - :param int format_: Text format - :param qwt.text.QwtTextEngine engine: Text engine - - .. seealso:: - - :py:meth:`setPaintAttribute()` - - .. warning:: - - Using `QwtText.AutoText` does nothing. - """ - if format_ == QwtText.AutoText: - return - if format_ == QwtText.PlainText and engine is None: - return - self.__map.setdefault(format_, engine) - - -class QwtTextLabel_PrivateData(QObject): - def __init__(self): - QObject.__init__(self) - - self.indent = 4 - self.margin = 0 - self.text = QwtText() - - -class QwtTextLabel(QFrame): - """ - A Widget which displays a QwtText - - .. py:class:: QwtTextLabel(parent) - - :param QWidget parent: Parent widget - - .. py:class:: QwtTextLabel([text=None], [parent=None]) - :noindex: - - :param str text: Text - :param QWidget parent: Parent widget - """ - - def __init__(self, *args): - if len(args) == 0: - text, parent = None, None - elif len(args) == 1: - if isinstance(args[0], QWidget): - text = None - (parent,) = args - else: - parent = None - (text,) = args - elif len(args) == 2: - text, parent = args - else: - raise TypeError( - "%s() takes 0, 1 or 2 argument(s) (%s given)" - % (self.__class__.__name__, len(args)) - ) - super(QwtTextLabel, self).__init__(parent) - self.init() - if text is not None: - self.__data.text = text - - def init(self): - self.__data = QwtTextLabel_PrivateData() - self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) - - def setPlainText(self, text): - """ - Interface for the designer plugin - does the same as setText() - - :param str text: Text - - .. seealso:: - - :py:meth:`plainText()` - """ - self.setText(QwtText(text)) - - def plainText(self): - """ - Interface for the designer plugin - - :return: Text as plain text - - .. seealso:: - - :py:meth:`setPlainText()` - """ - return self.__data.text.text() - - def setText(self, text, textFormat=QwtText.AutoText): - """ - Change the label's text, keeping all other QwtText attributes - - :param text: New text - :type text: qwt.text.QwtText or str - :param int textFormat: Format of text - - .. seealso:: - - :py:meth:`text()` - """ - if isinstance(text, QwtText): - self.__data.text = text - else: - self.__data.text.setText(text, textFormat) - self.update() - self.updateGeometry() - - def text(self): - """ - :return: Return the text - - .. seealso:: - - :py:meth:`setText()` - """ - return self.__data.text - - def clear(self): - """ - Clear the text and all `QwtText` attributes - """ - self.__data.text = QwtText() - self.update() - self.updateGeometry() - - def indent(self): - """ - :return: Label's text indent in pixels - - .. seealso:: - - :py:meth:`setIndent()` - """ - return self.__data.indent - - def setIndent(self, indent): - """ - Set label's text indent in pixels - - :param int indent: Indentation in pixels - - .. seealso:: - - :py:meth:`indent()` - """ - if indent < 0: - indent = 0 - self.__data.indent = indent - self.update() - self.updateGeometry() - - def margin(self): - """ - :return: Label's text indent in pixels - - .. seealso:: - - :py:meth:`setMargin()` - """ - return self.__data.margin - - def setMargin(self, margin): - """ - Set label's margin in pixels - - :param int margin: Margin in pixels - - .. seealso:: - - :py:meth:`margin()` - """ - self.__data.margin = margin - self.update() - self.updateGeometry() - - def sizeHint(self): - """ - Return a size hint - """ - return self.minimumSizeHint() - - def minimumSizeHint(self): - """ - Return a minimum size hint - """ - sz = self.__data.text.textSize(self.font()) - mw = 2 * (self.frameWidth() + self.__data.margin) - mh = mw - indent = self.__data.indent - if indent <= 0: - indent = self.defaultIndent() - if indent > 0: - align = _flag_int(self.__data.text.renderFlags()) - if align & (_ALIGN_LEFT | _ALIGN_RIGHT): - mw += self.__data.indent - elif align & (_ALIGN_TOP | _ALIGN_BOTTOM): - mh += self.__data.indent - sz += QSizeF(mw, mh) - return QSize(math.ceil(sz.width()), math.ceil(sz.height())) - - def heightForWidth(self, width): - """ - :param int width: Width - :return: Preferred height for this widget, given the width. - """ - renderFlags = _flag_int(self.__data.text.renderFlags()) - indent = self.__data.indent - if indent <= 0: - indent = self.defaultIndent() - width -= 2 * self.frameWidth() - if renderFlags & (_ALIGN_LEFT | _ALIGN_RIGHT): - width -= indent - height = math.ceil(self.__data.text.heightForWidth(width, self.font())) - if renderFlags & (_ALIGN_TOP | _ALIGN_BOTTOM): - height += indent - height += 2 * self.frameWidth() - return height - - def paintEvent(self, event): - painter = QPainter(self) - if not self.contentsRect().contains(event.rect()): - painter.save() - painter.setClipRegion(event.region() & self.frameRect()) - self.drawFrame(painter) - painter.restore() - painter.setClipRegion(event.region() & self.contentsRect()) - self.drawContents(painter) - - def drawContents(self, painter): - """ - Redraw the text and focus indicator - - :param QPainter painter: Painter - """ - r = self.textRect() - if r.isEmpty(): - return - painter.setFont(self.font()) - painter.setPen(self.palette().color(QPalette.Active, QPalette.Text)) - self.drawText(painter, QRectF(r)) - if self.hasFocus(): - m = 2 - focusRect = self.contentsRect().adjusted(m, m, -m + 1, -m + 1) - QwtPainter.drawFocusRect(painter, self, focusRect) - - def drawText(self, painter, textRect): - """ - Redraw the text - - :param QPainter painter: Painter - :param QRectF textRect: Text rectangle - """ - self.__data.text.draw(painter, textRect) - - def textRect(self): - """ - Calculate geometry for the text in widget coordinates - - :return: Geometry for the text - """ - r = self.contentsRect() - if not r.isEmpty() and self.__data.margin > 0: - r.setRect( - r.x() + self.__data.margin, - r.y() + self.__data.margin, - r.width() - 2 * self.__data.margin, - r.height() - 2 * self.__data.margin, - ) - if not r.isEmpty(): - indent = self.__data.indent - if indent <= 0: - indent = self.defaultIndent() - if indent > 0: - renderFlags = _flag_int(self.__data.text.renderFlags()) - if renderFlags & _ALIGN_LEFT: - r.setX(r.x() + indent) - elif renderFlags & _ALIGN_RIGHT: - r.setWidth(r.width() - indent) - elif renderFlags & _ALIGN_TOP: - r.setY(r.y() + indent) - elif renderFlags & _ALIGN_BOTTOM: - r.setHeight(r.height() - indent) - return r - - def defaultIndent(self): - if self.frameWidth() <= 0: - return 0 - if self.__data.text.testPaintAttribute(QwtText.PaintUsingTextFont): - fnt = self.__data.text.font() - else: - fnt = self.font() - return QFontMetrics(fnt).boundingRect("x").width() / 2 diff --git a/qwt/toqimage.py b/qwt/toqimage.py deleted file mode 100644 index b788901..0000000 --- a/qwt/toqimage.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the MIT License -# (see LICENSE file for more details) - -""" -NumPy array to QImage ---------------------- - -.. autofunction:: array_to_qimage -""" - -import numpy as np -from qtpy.QtGui import QImage - - -def array_to_qimage(arr, copy=False): - """ - Convert NumPy array to QImage object - - :param numpy.array arr: NumPy array - :param bool copy: if True, make a copy of the array - :return: QImage object - """ - # https://gist.githubusercontent.com/smex/5287589/raw/toQImage.py - if arr is None: - return QImage() - if len(arr.shape) not in (2, 3): - raise NotImplementedError("Unsupported array shape %r" % arr.shape) - data = arr.data - ny, nx = arr.shape[:2] - stride = arr.strides[0] # bytes per line - color_dim = None - if len(arr.shape) == 3: - color_dim = arr.shape[2] - if arr.dtype == np.uint8: - if color_dim is None: - qimage = QImage(data, nx, ny, stride, QImage.Format_Indexed8) - # qimage.setColorTable([qRgb(i, i, i) for i in range(256)]) - qimage.setColorCount(256) - elif color_dim == 3: - qimage = QImage(data, nx, ny, stride, QImage.Format_RGB888) - elif color_dim == 4: - qimage = QImage(data, nx, ny, stride, QImage.Format_ARGB32) - else: - raise TypeError("Invalid third axis dimension (%r)" % color_dim) - elif arr.dtype == np.uint32: - qimage = QImage(data, nx, ny, stride, QImage.Format_ARGB32) - else: - raise NotImplementedError("Unsupported array data type %r" % arr.dtype) - if copy: - return qimage.copy() - return qimage diff --git a/qwt/transform.py b/qwt/transform.py deleted file mode 100644 index 00b72a7..0000000 --- a/qwt/transform.py +++ /dev/null @@ -1,258 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Licensed under the terms of the Qwt License -# Copyright (c) 2002 Uwe Rathmann, for the original C++ code -# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization -# (see LICENSE file for more details) - -""" -Coordinate tranformations -------------------------- - -QwtTransform -~~~~~~~~~~~~ - -.. autoclass:: QwtTransform - :members: - -QwtNullTransform -~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtNullTransform - :members: - -QwtLogTransform -~~~~~~~~~~~~~~~ - -.. autoclass:: QwtLogTransform - :members: - -QwtPowerTransform -~~~~~~~~~~~~~~~~~ - -.. autoclass:: QwtPowerTransform - :members: -""" - -import numpy as np - - -class QwtTransform(object): - """ - A transformation between coordinate systems - - QwtTransform manipulates values, when being mapped between - the scale and the paint device coordinate system. - - A transformation consists of 2 methods: - - - transform - - invTransform - - where one is is the inverse function of the other. - - When p1, p2 are the boundaries of the paint device coordinates - and s1, s2 the boundaries of the scale, QwtScaleMap uses the - following calculations:: - - p = p1 + (p2 - p1) * ( T(s) - T(s1) / (T(s2) - T(s1)) ) - s = invT( T(s1) + ( T(s2) - T(s1) ) * (p - p1) / (p2 - p1) ) - """ - - def __init__(self): - pass - - def bounded(self, value): - """ - Modify value to be a valid value for the transformation. - The default implementation does nothing. - """ - return value - - def transform(self, value): - """ - Transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`invTransform()` - """ - raise NotImplementedError - - def invTransform(self, value): - """ - Inverse transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`transform()` - """ - raise NotImplementedError - - def copy(self): - """ - :return: Clone of the transformation - - The default implementation does nothing. - """ - raise NotImplementedError - - -class QwtNullTransform(QwtTransform): - def transform(self, value): - """ - Transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`invTransform()` - """ - return value - - def invTransform(self, value): - """ - Inverse transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`transform()` - """ - return value - - def copy(self): - """ - :return: Clone of the transformation - """ - return QwtNullTransform() - - -class QwtLogTransform(QwtTransform): - """ - Logarithmic transformation - - `QwtLogTransform` modifies the values using `numpy.log()` and - `numpy.exp()`. - - .. note:: - - In the calculations of `QwtScaleMap` the base of the log function - has no effect on the mapping. So `QwtLogTransform` can be used - for logarithmic scale in base 2 or base 10 or any other base. - - Extremum values: - - * `QwtLogTransform.LogMin`: Smallest allowed value for logarithmic - scales: 1.0e-150 - * `QwtLogTransform.LogMax`: Largest allowed value for logarithmic - scales: 1.0e150 - """ - - LogMin = 1.0e-150 - LogMax = 1.0e150 - - def bounded(self, value): - """ - Modify value to be a valid value for the transformation. - - :param float value: Value to be bounded - :return: Value modified - """ - bval = np.clip(np.asarray(value, dtype=np.float64), self.LogMin, self.LogMax) - return bval.item() if bval.ndim == 0 else bval - - def transform(self, value): - """ - Transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`invTransform()` - """ - return np.log(self.bounded(value)) - - def invTransform(self, value): - """ - Inverse transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`transform()` - """ - return np.exp(value) - - def copy(self): - """ - :return: Clone of the transformation - """ - return QwtLogTransform() - - -class QwtPowerTransform(QwtTransform): - """ - A transformation using `numpy.pow()` - - `QwtPowerTransform` preserves the sign of a value. - F.e. a transformation with a factor of 2 - transforms a value of -3 to -9 and v.v. Thus `QwtPowerTransform` - can be used for scales including negative values. - """ - - def __init__(self, exponent): - self.__exponent = exponent - super(QwtPowerTransform, self).__init__() - - def transform(self, value): - """ - Transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`invTransform()` - """ - if value < 0.0: - return -np.pow(-value, 1.0 / self.__exponent) - else: - return np.pow(value, 1.0 / self.__exponent) - - def invTransform(self, value): - """ - Inverse transformation function - - :param float value: Value - :return: Modified value - - .. seealso:: - - :py:meth:`transform()` - """ - if value < 0.0: - return -np.pow(-value, self.__exponent) - else: - return np.pow(value, self.__exponent) - - def copy(self): - """ - :return: Clone of the transformation - """ - return QwtPowerTransform(self.__exponent) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 18abc82..0000000 --- a/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -numpy -QtPy -PyQt5 -pre-commit -pylint -pytest -pytest-xvfb -coverage -build -ruff -sphinx -python-docs-theme diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 6594835..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,136 +0,0 @@ -# PythonQwt performance & visual-regression scripts - -This folder collects the tooling that supports performance investigations and visual-regression checks on PythonQwt. It was assembled while working on [issue #93](https://github.com/PlotPyStack/PythonQwt/issues/93) ("Performance degradation with Qt6") and is meant to be reused whenever someone needs to either: - -- **Optimize a hot path** — without flying blind on which binding pays the cost. -- **Validate that a refactor did not regress performance** — for any of the supported Qt bindings. -- **Validate that a refactor did not regress rendered output** — pixel-comparison across the 22 visual tests. - -The full case study that produced these scripts is documented in [`doc/issue93_optimization_summary.md`](../doc/issue93_optimization_summary.md). Read it first if you want to see the scripts in action and understand the kinds of findings they enable. - -## Prerequisites: per-binding virtualenvs - -PythonQwt supports PyQt5, PyQt6 and PySide6, and the *whole point* of this tooling is to compare them. The scripts assume three sibling virtual environments under `.venvs/`: - -``` -PythonQwt/ -├── .venvs/ -│ ├── pyqt5/ # contains PyQt5 -│ ├── pyqt6/ # contains PyQt6 -│ └── pyside6/ # contains PySide6 -``` - -`.venvs/` is git-ignored (see [`.gitignore`](../.gitignore)). One-shot bootstrap (PowerShell): - -```powershell -foreach ($b in "pyqt5","pyqt6","pyside6") { - & py -3.11 -m venv ".venvs/$b" - & ".\.venvs\$b\Scripts\python.exe" -m pip install --upgrade pip - # Core: Qt binding + qtpy + numpy + test runner + tools used by the scripts - & ".\.venvs\$b\Scripts\python.exe" -m pip install $b qtpy numpy pytest pillow line_profiler - # PythonQwt itself (editable, current checkout) - & ".\.venvs\$b\Scripts\python.exe" -m pip install -e . - # Optional: needed only by bench_plotpy_loadtest.py - & ".\.venvs\$b\Scripts\python.exe" -m pip install h5py scipy scikit-image opencv-python-headless tqdm -} -``` - -If you keep `PlotPy` and `guidata` as sibling editable checkouts, point `PYTHONPATH` at them when running the PlotPy bench (see workflow 3 below) instead of `pip install`-ing them. - -## Scripts at a glance - -| Script | Use case | Output | -|---|---|---| -| [`bench_qt.ps1`](bench_qt.ps1) | Quick **PythonQwt-only** load test across one or several bindings | `Average elapsed time: ms` per run | -| [`bench_plotpy_loadtest.py`](bench_plotpy_loadtest.py) | The **PlotPy** load test cited in issue #93 | Same format as `bench_qt.ps1` (parser-compatible) | -| [`profile_loadtest.py`](profile_loadtest.py) | First-pass profiling: who eats CPU? (`cProfile`) | Top-40 by cumulative & total time, plus `qwt/`-only view | -| [`lineprofile_loadtest.py`](lineprofile_loadtest.py) | Second-pass profiling: line-by-line on a curated `HOTSPOTS` set (`line_profiler`) | Annotated source listing | -| [`capture_screenshots.py`](capture_screenshots.py) | Run all 22 visual tests, copy PNGs into `shots///` | One PNG per test | -| [`diff_screenshots.py`](diff_screenshots.py) | Pixel-compare two screenshot folders | Markdown table; non-zero exit on `DIFFER` | - -`run_with_env.py` is unrelated to performance work; it is a generic local-development helper used elsewhere in the project. - -## Workflow 1 — "Did I regress performance?" - -Run before *and* after the change you want to validate, on the *same* machine, with no other heavy process competing for the CPU: - -```powershell -.\scripts\bench_qt.ps1 -Repeat 5 # PythonQwt-only micro load test -# Optional, slower, more representative of real-world usage: -$env:PYTHONPATH = "c:\Dev\PlotPy;c:\Dev\guidata" -foreach ($b in "pyqt5","pyqt6","pyside6") { - & ".\.venvs\$b\Scripts\python.exe" scripts\bench_plotpy_loadtest.py --repeat 3 --nplots 60 -} -``` - -Use the median across the 5 runs (the first run is usually slower due to warm-up) and **compare across all three bindings**: an optimization that helps PyQt5 only, or that helps PyQt6 only, is rarely a good trade. - -## Workflow 2 — "Where is time being spent?" - -Two-pass approach. cProfile first (cheap, broad), then line_profiler on the families it surfaces (focused, deep): - -```powershell -# First pass: who eats CPU? -$env:QT_API = "pyside6" -& .\.venvs\pyside6\Scripts\python.exe scripts\profile_loadtest.py pyside6.prof - -# Second pass: which line of which method? Edit HOTSPOTS in lineprofile_loadtest.py -# to add/remove functions of interest, then: -& .\.venvs\pyside6\Scripts\python.exe scripts\lineprofile_loadtest.py > lineprofile.txt -``` - -Comparing the cProfile output of two bindings (PyQt5 vs PySide6 typically) is the fastest way to spot Python-side overhead that the Qt6 bindings amplify; that diff is what guided the cleanups in [issue #93](https://github.com/PlotPyStack/PythonQwt/issues/93). - -## Workflow 3 — "Did I regress rendered output?" - -```powershell -# 1. Capture before (master) and after (your branch) for each binding. -$env:QT_API = "pyqt5" -& .\.venvs\pyqt5\Scripts\python.exe scripts\capture_screenshots.py shots\master\pyqt5 -# ... checkout your branch, repeat ... -& .\.venvs\pyqt5\Scripts\python.exe scripts\capture_screenshots.py shots\fix\pyqt5 - -# 2. (Recommended) capture each side TWICE (master-vs-master2 and fix-vs-fix2) -# to identify *flaky* tests that have intrinsically random output. Otherwise -# the diff cannot tell a regression from random data. - -# 3. Diff. PIL/numpy do not depend on Qt, so any venv with them works. -& .\.venvs\pyside6\Scripts\python.exe scripts\diff_screenshots.py shots\master\pyqt5 shots\fix\pyqt5 -``` - -After running `capture_screenshots.py`, the test PNGs accumulated under `qwt/tests/data/` should be cleaned up — only the tracked PNGs are kept (one-liner): - -```powershell -git checkout -- qwt/tests/data -$tracked = git ls-files qwt/tests/data/*.png | ForEach-Object { Split-Path $_ -Leaf } -Get-ChildItem qwt\tests\data\*.png | Where-Object { $_.Name -notin $tracked } | Remove-Item -``` - -The classification rule used in the issue #93 summary (✅ / ⚠️ / ❌) crosses two diff runs per test (master self-compare baseline + master-vs-fix). It is described in detail in [`doc/issue93_optimization_summary.md`](../doc/issue93_optimization_summary.md#per-test-screenshot-status-master-vs-phase-2-fix-all-bindings). - -## Reference numbers (from issue #93) - -So future investigations have a yardstick. Windows 11, Python 3.11.9, real desktop session (not `offscreen`). - -PythonQwt micro `test_loadtest` (`scripts/bench_qt.ps1 -Repeat 5`): - -| Binding | Master at the time | After issue #93 | -|---|---:|---:| -| PyQt5 | ~1 900 ms | ~450–550 ms | -| PyQt6 | ~2 300 ms | ~450–675 ms | -| PySide6 | ~2 900 ms | ~580–795 ms | - -PlotPy `test_loadtest`, 60 plots (`scripts/bench_plotpy_loadtest.py --repeat 3 --nplots 60`): - -| Binding | Master at the time | After issue #93 | -|---|---:|---:| -| PyQt5 | 25 134 ms | 16 169 ms | -| PyQt6 | 42 202 ms | 21 387 ms | -| PySide6 | 53 160 ms | 24 849 ms | - -If your absolute numbers differ by more than ~30% from these on a typical dev machine, suspect environmental drift before assuming a regression / improvement. - -## See also - -- [`doc/issue93_optimization_summary.md`](../doc/issue93_optimization_summary.md) — the case study these scripts came out of. -- [Issue #93](https://github.com/PlotPyStack/PythonQwt/issues/93) on GitHub. diff --git a/scripts/bench_plotpy_loadtest.py b/scripts/bench_plotpy_loadtest.py deleted file mode 100644 index c0f0ca1..0000000 --- a/scripts/bench_plotpy_loadtest.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Benchmark PlotPy's load test (test_loadtest) against the current PythonQwt. - -This reproduces the load test cited in the PlotPy/PythonQwt performance issue -(https://github.com/PlotPyStack/PythonQwt/issues/93): instantiating a large -grid of plot widgets and measuring construction time. See also -``scripts/README.md`` and ``doc/issue93_optimization_summary.md``. - -Prerequisites -------------- -A Python interpreter with **all** of the following importable: - -* a Qt binding (``PyQt5``, ``PyQt6`` or ``PySide6``) selected via ``QT_API``; -* ``qtpy``, ``numpy``, plus the usual PlotPy stack (``h5py``, ``scipy``, - ``scikit-image``, ``opencv-python-headless``, ``tqdm``); -* ``plotpy`` and ``guidata`` (typically as editable installs from sibling - checkouts: set ``PYTHONPATH=<...>\\PlotPy;<...>\\guidata``); -* the ``PythonQwt`` checkout under test (current working directory or in the - same ``PYTHONPATH``). - -The script does **not** force ``QT_QPA_PLATFORM=offscreen``: numbers are taken -with the real Qt paint pipeline so they include composite cost. - -Usage ------ -:: - - # PowerShell, with the PyQt5 venv prepared as described in scripts/README.md - $env:QT_API = "pyqt5" - $env:PYTHONPATH = "c:\\Dev\\PlotPy;c:\\Dev\\guidata" - & .\\.venvs\\pyqt5\\Scripts\\python.exe scripts\bench_plotpy_loadtest.py --repeat 3 --nplots 60 - -Output contains a line compatible with ``scripts/bench_qt.ps1``'s parser:: - - Average elapsed time: ms -""" - -from __future__ import annotations - -import argparse -import os -import time - -# Avoid PlotPy's "first run" wizard / dialogs in headless mode -os.environ.setdefault("GUIDATA_TEST_MODE", "1") - - -def run_once(nplots: int, ncols: int, nrows: int) -> float: - """Run one LoadTest construction and return elapsed seconds.""" - # Imports happen inside the function so the Qt binding is fully selected - # via QT_API by the time PlotPy / guidata import qtpy. - from guidata.qthelpers import qt_app_context # noqa: WPS433 - from plotpy.tests.benchmarks.test_loadtest import LoadTest # noqa: WPS433 - from qtpy import QtWidgets as QW # noqa: WPS433 - - with qt_app_context(exec_loop=False): - t0 = time.perf_counter() - win = LoadTest(nplots=nplots, ncols=ncols, nrows=nrows) - win.show() - QW.QApplication.processEvents() - elapsed = time.perf_counter() - t0 - win.close() - return elapsed - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--repeat", type=int, default=3) - parser.add_argument("--nplots", type=int, default=60) - parser.add_argument("--ncols", type=int, default=6) - parser.add_argument("--nrows", type=int, default=5) - args = parser.parse_args() - - # Print binding banner like PythonQwt's own loadtest does - import sys - - from qtpy import API_NAME, PYQT_VERSION, QT_VERSION # noqa: WPS433 - - pyver = ".".join(str(v) for v in sys.version_info[:3]) - print( - f"PlotPy load test [Python {pyver}, Qt {QT_VERSION}, " - f"{API_NAME} v{PYQT_VERSION}, nplots={args.nplots}]" - ) - - times = [] - for i in range(args.repeat): - t = run_once(args.nplots, args.ncols, args.nrows) - times.append(t) - print(f" run {i + 1}/{args.repeat}: {t * 1000:.0f} ms") - avg_ms = sum(times) / len(times) * 1000 - print(f"Average elapsed time: {avg_ms:.0f} ms") - - -if __name__ == "__main__": - main() diff --git a/scripts/bench_qt.ps1 b/scripts/bench_qt.ps1 deleted file mode 100644 index ba99c9d..0000000 --- a/scripts/bench_qt.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -# Run the PythonQwt micro load test (qwt/tests/test_loadtest.py) across one -# or several Qt bindings, using per-binding venvs under .venvs//. -# -# Primary use case: detect performance regressions in PythonQwt itself, -# isolated from PlotPy. For the PlotPy load test (the one cited in -# https://github.com/PlotPyStack/PythonQwt/issues/93) use -# scripts/bench_plotpy_loadtest.py instead. -# -# See scripts/README.md and doc/issue93_optimization_summary.md for the -# full performance-investigation workflow and reference numbers. -# -# Prerequisites: -# .venvs//Scripts/python.exe must exist for each binding listed in -# $Bindings, with the corresponding Qt binding + numpy + qtpy + pytest -# installed, and PythonQwt installed in editable mode (pip install -e .). -# See scripts/README.md for a one-shot bootstrap snippet. -# -# Usage: -# .\scripts\bench_qt.ps1 # run all three bindings, 1 run each -# .\scripts\bench_qt.ps1 pyqt5 # run a single binding -# .\scripts\bench_qt.ps1 pyqt5,pyside6 # run a subset -# .\scripts\bench_qt.ps1 -Repeat 5 # repeat each run N times (recommended) -# -# The script sets PYTHONQWT_UNATTENDED_TESTS=1 and QT_API=, then -# invokes qwt/tests/test_loadtest.py via the binding-specific venv. It -# captures the "Average elapsed time" line printed by the benchmark. - -[CmdletBinding()] -param( - [Parameter(Position = 0)] - [string[]] $Bindings = @("pyqt5", "pyqt6", "pyside6"), - [int] $Repeat = 1 -) - -$ErrorActionPreference = "Stop" -$repoRoot = Split-Path -Parent $PSScriptRoot -Push-Location $repoRoot -try { - foreach ($binding in $Bindings) { - $py = Join-Path $repoRoot ".venvs\$binding\Scripts\python.exe" - if (-not (Test-Path $py)) { - Write-Warning "Skipping $binding (venv not found at $py)" - continue - } - $env:PYTHONQWT_UNATTENDED_TESTS = "1" - $env:QT_API = $binding - for ($i = 1; $i -le $Repeat; $i++) { - $output = & $py "qwt\tests\test_loadtest.py" 2>&1 - $avg = $output | Select-String -Pattern "Average elapsed time" | Select-Object -Last 1 - $tag = "[{0}]" -f $binding - if ($Repeat -gt 1) { $tag = "{0} run {1}/{2}" -f $tag, $i, $Repeat } - if ($avg) { - Write-Host ("{0} {1}" -f $tag, $avg.Line.Trim()) - } - else { - Write-Host ("{0} (no result)" -f $tag) - Write-Host ($output -join [Environment]::NewLine) - } - } - Remove-Item Env:PYTHONQWT_UNATTENDED_TESTS -ErrorAction SilentlyContinue - Remove-Item Env:QT_API -ErrorAction SilentlyContinue - } -} -finally { - Pop-Location -} diff --git a/scripts/capture_screenshots.py b/scripts/capture_screenshots.py deleted file mode 100644 index ed96552..0000000 --- a/scripts/capture_screenshots.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Capture screenshots from every PythonQwt visual test into a directory. - -Used together with :mod:`diff_screenshots` to detect visual regressions -introduced by performance optimizations. See ``scripts/README.md`` and -``doc/issue93_optimization_summary.md`` for the broader workflow. - -What it does ------------- -For each entry in the hard-coded ``tests`` list, runs the corresponding test -in a *subprocess* with ``PYTHONQWT_TAKE_SCREENSHOTS=1`` and -``PYTHONQWT_UNATTENDED_TESTS=1`` set, watches the test data directory for -newly-written PNGs and copies them into ```` with names of the -form ``__.png`` (so different tests producing the -same PNG basename do not overwrite each other). - -Prerequisites -------------- -* A Qt binding selected via ``QT_API`` (``pyqt5`` / ``pyqt6`` / ``pyside6``). -* PythonQwt importable (typically the editable install of the local checkout). -* The Qt platform plug-in able to render to a real display surface - (offscreen also works but produces slightly different antialiasing). - -Usage ------ -:: - - $env:QT_API = "pyqt5" - & .\\.venvs\\pyqt5\\Scripts\\python.exe scripts\\capture_screenshots.py shots\fix\\pyqt5 - -The output directory is created if missing. After the run, leftover PNGs in -``qwt/tests/data/`` should be cleaned up (only newly-untracked PNGs are -produced by tests; see ``scripts/README.md`` for the cleanup snippet). - -Note: the ``tests`` list maps each test *module* (``test_xxx``) to the *test -function* defined inside it. Some modules use a slightly different function -name (``test_relativemargin`` -> ``test_relative_margin``, -``test_symbols`` -> ``test_base``); update this mapping if new visual tests -are added. -""" - -from __future__ import annotations - -import os -import os.path as osp -import shutil -import subprocess -import sys -import time -import traceback - - -def main() -> int: - if len(sys.argv) != 2: - print(__doc__) - return 2 - out_dir = osp.abspath(sys.argv[1]) - os.makedirs(out_dir, exist_ok=True) - - os.environ["PYTHONQWT_TAKE_SCREENSHOTS"] = "1" - os.environ["PYTHONQWT_UNATTENDED_TESTS"] = "1" - - import qwt # noqa: F401 - from qwt.tests import utils as test_utils - - data_dir = osp.join(test_utils.TEST_PATH, "data") - - tests = [ - ("test_backingstore", "test_backingstore"), - ("test_bodedemo", "test_bodedemo"), - ("test_cartesian", "test_cartesian"), - ("test_cpudemo", "test_cpudemo"), - ("test_curvebenchmark1", "test_curvebenchmark1"), - ("test_curvebenchmark2", "test_curvebenchmark2"), - ("test_curvedemo1", "test_curvedemo1"), - ("test_curvedemo2", "test_curvedemo2"), - ("test_data", "test_data"), - ("test_errorbar", "test_errorbar"), - ("test_eventfilter", "test_eventfilter"), - ("test_highdpi", "test_highdpi"), - ("test_image", "test_image"), - ("test_loadtest", "test_loadtest"), - ("test_logcurve", "test_logcurve"), - ("test_mapdemo", "test_mapdemo"), - ("test_multidemo", "test_multidemo"), - ("test_relativemargin", "test_relative_margin"), - ("test_simple", "test_simple"), - ("test_stylesheet", "test_stylesheet"), - ("test_symbols", "test_base"), - ("test_vertical", "test_vertical"), - ] - - failed: list[str] = [] - no_screenshot: list[str] = [] - - for module_name, func_name in tests: - before = { - f: os.path.getmtime(osp.join(data_dir, f)) - for f in os.listdir(data_dir) - if f.lower().endswith(".png") - } - marker = time.time() - - cmd = [ - sys.executable, - "-c", - f"import qwt.tests.{module_name} as m; m.{func_name}()", - ] - try: - proc = subprocess.run( - cmd, - env=os.environ.copy(), - capture_output=True, - text=True, - timeout=180, - ) - except subprocess.TimeoutExpired: - print(f"[TIMEOUT] {module_name}") - failed.append(module_name) - continue - - if proc.returncode != 0: - print(f"[FAIL] {module_name}: rc={proc.returncode}") - if proc.stderr: - print(proc.stderr.strip()[-500:]) - failed.append(module_name) - continue - - produced = [] - for f in os.listdir(data_dir): - if not f.lower().endswith(".png"): - continue - full = osp.join(data_dir, f) - mt = os.path.getmtime(full) - if mt >= marker - 0.5 and (f not in before or mt > before[f] + 0.001): - produced.append(f) - - if not produced: - print(f"[NO SCREENSHOT] {module_name}") - no_screenshot.append(module_name) - continue - - for png in produced: - src = osp.join(data_dir, png) - dst = osp.join(out_dir, f"{module_name}__{png}") - shutil.copy2(src, dst) - print(f"[OK] {module_name} -> {osp.basename(dst)}") - - print() - print(f"Captured screenshots into: {out_dir}") - if failed: - print(f"Failed ({len(failed)}): {', '.join(failed)}") - if no_screenshot: - print(f"No screenshot ({len(no_screenshot)}): {', '.join(no_screenshot)}") - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except Exception: - traceback.print_exc() - sys.exit(1) diff --git a/scripts/diff_screenshots.py b/scripts/diff_screenshots.py deleted file mode 100644 index dfe06b5..0000000 --- a/scripts/diff_screenshots.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Pixel-compare two directories of screenshots produced by -:mod:`capture_screenshots`. See ``scripts/README.md`` and -``doc/issue93_optimization_summary.md`` for the broader workflow. - -For each PNG present in both directories, classifies the pair as one of: - -* ``IDENTICAL`` - byte-equal files; -* ``EQUAL_PIXELS`` - bytes differ but decoded pixel arrays match; -* ``DIFFER`` - size or pixel mismatch (with ``n/total`` differing pixels, - pixel percentage, max and mean per-channel magnitude); -* ``ERROR`` - PIL failed to decode one of the files. - -Files present in only one side are listed at the end. The exit code is ``0`` -if and only if no ``DIFFER`` row is produced (useful as a CI gate). - -A ``DIFFER`` baseline does not necessarily mean a regression: some PythonQwt -visual tests use random data, timestamps or live system stats and are -intrinsically non-reproducible. Always pair the run with a self-compare -baseline (e.g. ``shots/master/`` vs ``shots/master2/``) to -identify *flaky* tests; see the optimization summary for the classification -rule ("✅ / ⚠️ / ❌"). - -Prerequisites -------------- -``numpy`` and ``Pillow`` available in the Python that runs the comparison -(any binding-specific venv works - PIL/numpy do not depend on Qt). - -Usage ------ -:: - - python scripts/diff_screenshots.py shots\\master\\pyqt5 shots\fix\\pyqt5 - -Output is a Markdown table; pipe through ``Select-String DIFFER|Summary`` to -focus on regressions only. -""" - -from __future__ import annotations - -import os -import os.path as osp -import sys - -import numpy as np -from PIL import Image - - -def load_array(path): - img = Image.open(path).convert("RGBA") - return np.asarray(img) - - -def main() -> int: - if len(sys.argv) != 3: - print(__doc__) - return 2 - a, b = osp.abspath(sys.argv[1]), osp.abspath(sys.argv[2]) - files_a = {f for f in os.listdir(a) if f.lower().endswith(".png")} - files_b = {f for f in os.listdir(b) if f.lower().endswith(".png")} - common = sorted(files_a & files_b) - only_a = sorted(files_a - files_b) - only_b = sorted(files_b - files_a) - - rows = [] - for name in common: - pa, pb = osp.join(a, name), osp.join(b, name) - with open(pa, "rb") as f: - ba = f.read() - with open(pb, "rb") as f: - bb = f.read() - if ba == bb: - rows.append((name, "IDENTICAL", "", "")) - continue - try: - arr_a = load_array(pa) - arr_b = load_array(pb) - except Exception as exc: - rows.append((name, "ERROR", str(exc), "")) - continue - if arr_a.shape != arr_b.shape: - rows.append((name, "DIFFER", f"shape {arr_a.shape} vs {arr_b.shape}", "")) - continue - diff = np.abs(arr_a.astype(np.int16) - arr_b.astype(np.int16)) - n_diff = int(np.any(diff > 0, axis=-1).sum()) - total = arr_a.shape[0] * arr_a.shape[1] - pct = 100.0 * n_diff / total - max_diff = int(diff.max()) - mean_diff = float(diff.mean()) - if n_diff == 0: - rows.append((name, "EQUAL_PIXELS", "", "")) - else: - rows.append( - ( - name, - "DIFFER", - f"{n_diff}/{total} px ({pct:.2f}%)", - f"max={max_diff} mean={mean_diff:.2f}", - ) - ) - - # Print a markdown-style table - print(f"# Screenshot diff: {a} vs {b}") - print() - print("| Test (PNG) | Status | Pixels differ | Magnitude |") - print("|---|---|---|---|") - for name, status, info1, info2 in rows: - print(f"| `{name}` | {status} | {info1} | {info2} |") - - if only_a: - print(f"\nOnly in A: {', '.join(only_a)}") - if only_b: - print(f"\nOnly in B: {', '.join(only_b)}") - - n_diff = sum(1 for r in rows if r[1] == "DIFFER") - n_same = sum(1 for r in rows if r[1] in ("IDENTICAL", "EQUAL_PIXELS")) - print(f"\nSummary: {n_same} match, {n_diff} differ, {len(common)} total") - return 0 if n_diff == 0 else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/lineprofile_loadtest.py b/scripts/lineprofile_loadtest.py deleted file mode 100644 index cdba522..0000000 --- a/scripts/lineprofile_loadtest.py +++ /dev/null @@ -1,92 +0,0 @@ -r"""Line-profile a curated set of PythonQwt hot functions under the load test. - -Second-pass tool for performance investigation, used after -:mod:`profile_loadtest` has identified the hot families. Line-by-line -attribution often surfaces costs that are invisible to ``cProfile`` (e.g. a -single ``QFont.key()`` call inside a tight loop, or per-call QObject overhead -on Qt6). See ``scripts/README.md`` and ``doc/issue93_optimization_summary.md``. - -What it does ------------- -Runs ``qwt.tests.test_loadtest`` once with :mod:`line_profiler` instrumenting -each function listed in :data:`HOTSPOTS`, then prints the line-by-line stats -on stdout (units: microseconds). - -The ``HOTSPOTS`` mapping is a *curated* registry. When the surface area of an -optimization changes, **edit the mapping in this file** to add/remove -functions; passing names on the command-line only restricts which of the -registered hotspots are profiled in this run. - -Prerequisites -------------- -* A Qt binding selected via ``QT_API`` (``pyqt5`` / ``pyqt6`` / ``pyside6``). -* ``line_profiler`` (``pip install line_profiler``). -* PythonQwt importable. - -Usage ------ -:: - - # Full hotspot set - & .\.venvs\pyside6\Scripts\python.exe scripts\lineprofile_loadtest.py - - # Subset (whatever you are currently iterating on) - & .\.venvs\pyside6\Scripts\python.exe scripts\lineprofile_loadtest.py textSize labelRect - -Redirect to a file when iterating: ``> lineprofile.txt`` then diff between -commits to confirm a hot line dropped from N us to N/2. -""" - -from __future__ import annotations - -import os -import sys - -os.environ.setdefault("PYTHONQWT_UNATTENDED_TESTS", "1") - -from line_profiler import LineProfiler # noqa: E402 - -import qwt.scale_div # noqa: E402 -import qwt.scale_draw # noqa: E402 -import qwt.scale_engine # noqa: E402 -import qwt.scale_map # noqa: E402 -import qwt.text # noqa: E402 -from qwt.tests import test_loadtest # noqa: E402 - -# (module, qualified-name) — only methods listed here are line-profiled. -HOTSPOTS = { - "textSize": qwt.text.QwtText.textSize, - "textEngine": qwt.text.QwtText.textEngine, - "QwtText.__init__": qwt.text.QwtText.__init__, - "PlainTextEngine.textMargins": qwt.text.QwtPlainTextEngine.textMargins, - "labelRect": qwt.scale_draw.QwtScaleDraw.labelRect, - "labelPosition": qwt.scale_draw.QwtScaleDraw.labelPosition, - "labelTransformation": qwt.scale_draw.QwtScaleDraw.labelTransformation, - "getBorderDistHint": qwt.scale_draw.QwtScaleDraw.getBorderDistHint, - "draw": qwt.scale_draw.QwtScaleDraw.draw, - "drawLabel": qwt.scale_draw.QwtScaleDraw.drawLabel, - "drawTick": qwt.scale_draw.QwtScaleDraw.drawTick, - "drawBackbone": qwt.scale_draw.QwtScaleDraw.drawBackbone, - "scale_map.transform": qwt.scale_map.QwtScaleMap.transform, - "scale_engine.strip": qwt.scale_engine.QwtScaleEngine.strip, - "scale_engine.contains": qwt.scale_engine.QwtScaleEngine.contains, - "scale_div.contains": qwt.scale_div.QwtScaleDiv.contains, - "orientation": qwt.scale_draw.QwtScaleDraw.orientation, -} - - -def main() -> None: - selected = sys.argv[1:] or list(HOTSPOTS) - profiler = LineProfiler() - for name in selected: - if name not in HOTSPOTS: - print(f"Unknown hotspot: {name!r}", file=sys.stderr) - continue - profiler.add_function(HOTSPOTS[name]) - - profiler.runcall(test_loadtest.test_loadtest) - profiler.print_stats(stream=sys.stdout, output_unit=1e-6) - - -if __name__ == "__main__": - main() diff --git a/scripts/profile_loadtest.py b/scripts/profile_loadtest.py deleted file mode 100644 index a49cbd4..0000000 --- a/scripts/profile_loadtest.py +++ /dev/null @@ -1,78 +0,0 @@ -r"""Profile ``qwt.tests.test_loadtest`` under :mod:`cProfile`. - -First-pass tool for performance investigation: identifies which functions -dominate cumulative time and total time. Use :mod:`lineprofile_loadtest` for -the second pass once a hot family of functions has been spotted. See -``scripts/README.md`` and ``doc/issue93_optimization_summary.md``. - -What it does ------------- -Runs the PythonQwt micro load test once under ``cProfile``, then prints three -reports to stdout (and dumps the raw stats to ````): - -1. Top 40 by cumulative time (``--cumulative``). -2. Top 40 by total time (``--tottime``). -3. Top 40 by total time, restricted to ``qwt/`` internals. - -Prerequisites -------------- -* A Qt binding selected via ``QT_API`` (``pyqt5`` / ``pyqt6`` / ``pyside6``); - numbers are most informative when collected for each binding in turn. -* PythonQwt importable. - -Usage ------ -:: - - $env:QT_API = "pyside6" - & .\.venvs\pyside6\Scripts\python.exe scripts\profile_loadtest.py pyside6.prof - -Open the dumped ``.prof`` file with ``snakeviz`` (``pip install snakeviz``) -or ``gprof2dot`` for a graphical view. Diff the per-binding reports to spot -overhead that scales with binding cost. -""" - -from __future__ import annotations - -import cProfile -import os -import pstats -import sys -from io import StringIO - - -def main() -> int: - os.environ["PYTHONQWT_UNATTENDED_TESTS"] = "1" - - # Import lazily so PYTHONQWT_UNATTENDED_TESTS is honored - from qwt.tests import test_loadtest - - pr = cProfile.Profile() - pr.enable() - test_loadtest.test_loadtest() - pr.disable() - - out_path = sys.argv[1] if len(sys.argv) > 1 else "profile.out" - pr.dump_stats(out_path) - - buf = StringIO() - stats = pstats.Stats(pr, stream=buf).sort_stats("cumulative") - stats.print_stats(40) - print(buf.getvalue()) - - buf2 = StringIO() - stats2 = pstats.Stats(pr, stream=buf2).sort_stats("tottime") - stats2.print_stats(40) - print("==== TOTTIME TOP 40 ====") - print(buf2.getvalue()) - - buf3 = StringIO() - stats3 = pstats.Stats(pr, stream=buf3).sort_stats("tottime") - stats3.print_stats(r"qwt[\\/].*", 40) - print("==== TOTTIME (qwt only) ====") - print(buf3.getvalue()) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/run_with_env.py b/scripts/run_with_env.py deleted file mode 100644 index 6688cb6..0000000 --- a/scripts/run_with_env.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. - -"""Run a command with environment variables loaded from a .env file. - -This script automatically detects the best Python interpreter to use: - -1. ``PYTHON`` variable in ``.env`` file (e.g. for WinPython distributions) -2. ``WINPYDIRBASE`` variable (legacy WinPython base directory) -3. ``VENV_DIR`` variable (explicit virtual environment directory) -4. A local virtual environment (``.venv*`` directory in the project root) -5. Falls back to ``sys.executable`` (the Python that launched this script) - -This ensures that VS Code tasks always use the correct Python environment -regardless of which interpreter is configured globally or in VS Code. -""" - -from __future__ import annotations - -import glob -import os -import subprocess -import sys -from pathlib import Path - - -def _find_venv_python(project_root: Path) -> str | None: - """Find a Python executable in a ``.venv*`` directory. - - Searches for directories matching ``.venv*`` in the project root and - returns the first valid Python executable found. - - Args: - project_root: The root directory of the project. - - Returns: - Absolute path to the venv Python executable, or None if not found. - """ - # Sort to prefer ".venv" over ".venv-xyz" etc. - venv_dirs = sorted(glob.glob(str(project_root / ".venv*"))) - for venv_dir in venv_dirs: - venv_path = Path(venv_dir) - if not venv_path.is_dir(): - continue - result = _get_venv_python(venv_path) - if result: - return result - return None - - -def _get_venv_python(venv_dir: Path) -> str | None: - """Get the Python executable from a specific venv directory. - - Args: - venv_dir: Path to the virtual environment directory. - - Returns: - Absolute path to the Python executable, or None if not found. - """ - if not venv_dir.is_dir(): - return None - # Windows: Scripts/python.exe — Unix: bin/python - candidates = [ - venv_dir / "Scripts" / "python.exe", - venv_dir / "bin" / "python", - ] - for candidate in candidates: - if candidate.is_file(): - # Keep the venv-local executable path without resolving symlinks: - # on Linux/WSL, ``bin/python`` is often a symlink to a global - # interpreter (e.g. /usr/bin/python3.x). Resolving it would lose - # venv context and site-packages selection. - return str(candidate.absolute()) - return None - - -def resolve_python(project_root: Path) -> str: - """Resolve the best Python interpreter for the project. - - Priority order: - - 1. ``PYTHON`` environment variable (set in ``.env`` or externally) - 2. ``WINPYDIRBASE`` environment variable (legacy WinPython base directory) - 3. ``VENV_DIR`` environment variable (explicit venv directory) - 4. ``.venv*`` directory in *project_root* (auto-discovery) - 5. ``sys.executable`` (the interpreter running this script) - - Args: - project_root: The root directory of the project. - - Returns: - Absolute path to the Python executable to use. - """ - # 1. Explicit PYTHON variable (e.g. WinPython distribution) - python_env = os.environ.get("PYTHON") - if python_env: - python_path = Path(python_env) - if python_path.is_file(): - # Do not resolve symlinks for the same reason as in - # ``_get_venv_python``. - resolved = str(python_path.absolute()) - print(f" 🐍 Using PYTHON from .env: {resolved}") - return resolved - print(f" ⚠️ PYTHON variable set but not found: {python_env}") - - # 2. Legacy WINPYDIRBASE variable (WinPython distribution) - winpy_base = os.environ.get("WINPYDIRBASE") - if winpy_base and Path(winpy_base).is_dir(): - # Search for python.exe in the WinPython directory structure - # Patterns: python-3.11.5.amd64/python.exe (old) or python/python.exe (new) - for pattern in ("python-*/python.exe", "python/python.exe"): - for candidate in sorted(Path(winpy_base).glob(pattern)): - if candidate.is_file(): - resolved = str(candidate.absolute()) - print(f" 🐍 Using WINPYDIRBASE (legacy): {resolved}") - return resolved - # Also try direct python.exe in the base directory - direct = Path(winpy_base) / "python.exe" - if direct.is_file(): - resolved = str(direct.absolute()) - print(f" 🐍 Using WINPYDIRBASE (legacy): {resolved}") - return resolved - print(f" ⚠️ WINPYDIRBASE set but no Python found in: {winpy_base}") - - # 3. Explicit VENV_DIR variable (e.g. for multiple local venvs) - venv_dir_env = os.environ.get("VENV_DIR") - if venv_dir_env: - venv_dir = Path(venv_dir_env) - if not venv_dir.is_absolute(): - venv_dir = project_root / venv_dir - venv_python = _get_venv_python(venv_dir) - if venv_python: - print(f" 🐍 Using VENV_DIR from .env: {venv_python}") - return venv_python - print(f" ⚠️ VENV_DIR set but no Python found in: {venv_dir}") - - # 4. Auto-discover local venv - venv_python = _find_venv_python(project_root) - if venv_python: - print(f" 🐍 Using venv Python: {venv_python}") - return venv_python - - # 5. Fallback - print(f" 🐍 Using caller Python: {sys.executable}") - return sys.executable - - -def load_env_file(env_path: str | None = None) -> None: - """Load environment variables from a .env file.""" - if env_path is None: - env_path = Path.cwd() / ".env" - if not Path(env_path).is_file(): - raise FileNotFoundError(f"Environment file not found: {env_path}") - print(f"Loading environment variables from: {env_path}") - with open(env_path, encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - value = value.strip().strip('"').strip("'") - os.environ[key.strip()] = value - print(f" Loaded variable: {key.strip()}={value}") - - -def execute_command(command: list[str], python_exe: str) -> int: - """Execute a command, replacing ``python`` placeholders. - - Any argument that is the bare word ``python`` or that points to a Python - executable (checked via filename) is replaced by *python_exe* so that the - subprocess uses the resolved interpreter rather than the global one. - - Args: - command: The command and its arguments. - python_exe: The resolved Python interpreter path. - - Returns: - The subprocess exit code. - """ - resolved: list[str] = [] - for arg in command: - if arg.lower() == "python" or ( - Path(arg).name.lower().startswith("python") - and Path(arg).is_file() - and arg.lower() != python_exe.lower() - ): - resolved.append(python_exe) - else: - resolved.append(arg) - print("Executing command:") - print(" ".join(resolved)) - print("") - result = subprocess.call(resolved) - print(f"Process exited with code {result}") - return result - - -def main() -> None: - """Main function to load environment variables and execute a command.""" - if len(sys.argv) < 2: - print("Usage: python run_with_env.py [args ...]") - sys.exit(1) - print("🏃 Running with environment variables") - project_root = Path.cwd() - load_env_file() - python_exe = resolve_python(project_root) - return execute_command(sys.argv[1:], python_exe) - - -if __name__ == "__main__": - main()