Merge remote-tracking branch 'upstream/develop' into patch-3
This commit is contained in:
commit
262004e2ec
17
.travis.yml
17
.travis.yml
|
@ -9,21 +9,17 @@ sudo: false
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
||||||
virtualenv:
|
|
||||||
system_site_packages: true
|
|
||||||
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Enable python 2 and python 3 builds
|
# Enable python 2 and python 3 builds
|
||||||
# Note that the 2.6 build doesn't get flake8, and runs old versions of
|
# Note that the python 2.6 support ended.
|
||||||
# Pyglet and GLFW to make sure we deal with those correctly
|
|
||||||
#- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended
|
|
||||||
- PYTHON=2.7 QT=pyqt4 TEST=extra
|
- PYTHON=2.7 QT=pyqt4 TEST=extra
|
||||||
- PYTHON=2.7 QT=pyside TEST=standard
|
- PYTHON=2.7 QT=pyside TEST=standard
|
||||||
- PYTHON=3.5 QT=pyqt5 TEST=standard
|
- PYTHON=3.5 QT=pyqt5 TEST=standard
|
||||||
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
|
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
|
||||||
#- PYTHON=3.2 QT=pyqt5 TEST=standard
|
#- PYTHON=3.2 QT=pyqt5 TEST=standard
|
||||||
|
|
||||||
|
services:
|
||||||
|
- xvfb
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi
|
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi
|
||||||
|
@ -67,11 +63,7 @@ install:
|
||||||
fi;
|
fi;
|
||||||
- pip install pytest-xdist # multi-thread py.test
|
- pip install pytest-xdist # multi-thread py.test
|
||||||
- pip install pytest-cov # add coverage stats
|
- pip install pytest-cov # add coverage stats
|
||||||
|
- pip install pytest-faulthandler # activate faulthandler
|
||||||
# required for example testing on python 2.6
|
|
||||||
- if [ "${PYTHON}" == "2.6" ]; then
|
|
||||||
pip install importlib;
|
|
||||||
fi;
|
|
||||||
|
|
||||||
# Debugging helpers
|
# Debugging helpers
|
||||||
- uname -a
|
- uname -a
|
||||||
|
@ -85,7 +77,6 @@ install:
|
||||||
before_script:
|
before_script:
|
||||||
# We need to create a (fake) display on Travis, let's use a funny resolution
|
# We need to create a (fake) display on Travis, let's use a funny resolution
|
||||||
- export DISPLAY=:99.0
|
- export DISPLAY=:99.0
|
||||||
- "sh -e /etc/init.d/xvfb start"
|
|
||||||
- /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render
|
- /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render
|
||||||
|
|
||||||
# Make sure everyone uses the correct python (this is handled by conda)
|
# Make sure everyone uses the correct python (this is handled by conda)
|
||||||
|
|
69
CONTRIBUTING.md
Normal file
69
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
# Contributing to PyQtGraph
|
||||||
|
|
||||||
|
Contributions to pyqtgraph are welcome!
|
||||||
|
|
||||||
|
Please use the following guidelines when preparing changes:
|
||||||
|
|
||||||
|
## Submitting Code Changes
|
||||||
|
|
||||||
|
* The preferred method for submitting changes is by github pull request against the "develop" branch.
|
||||||
|
* Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected.
|
||||||
|
* For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort.
|
||||||
|
* Along these lines, please note that `pyqtgraph.opengl` will be deprecated soon and replaced with VisPy.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code.
|
||||||
|
* Documentation is generated with sphinx; please check that docstring changes compile correctly
|
||||||
|
|
||||||
|
## Style guidelines
|
||||||
|
|
||||||
|
* PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable.
|
||||||
|
* Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8.
|
||||||
|
* Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt
|
||||||
|
* Exception 2: Function docstrings use ReStructuredText tables for describing arguments:
|
||||||
|
|
||||||
|
```text
|
||||||
|
============== ========================================================
|
||||||
|
**Arguments:**
|
||||||
|
argName1 (type) Description of argument
|
||||||
|
argName2 (type) Description of argument. Longer descriptions must
|
||||||
|
be wrapped within the column guidelines defined by the
|
||||||
|
"====" header and footer.
|
||||||
|
============== ========================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
QObject subclasses that implement new signals should also describe
|
||||||
|
these in a similar table.
|
||||||
|
|
||||||
|
## Testing Setting up a test environment
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
* tox
|
||||||
|
* tox-conda
|
||||||
|
* pytest
|
||||||
|
* pytest-cov
|
||||||
|
* pytest-xdist
|
||||||
|
* pytest-faulthandler
|
||||||
|
* Optional: pytest-xvfb
|
||||||
|
|
||||||
|
### Tox
|
||||||
|
|
||||||
|
As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin.
|
||||||
|
|
||||||
|
* Tests for a module should ideally cover all code in that module, i.e., statement coverage should be at 100%.
|
||||||
|
* To measure the test coverage, un `pytest --cov -n 4` to run the test suite with coverage on 4 cores.
|
||||||
|
|
||||||
|
### Continous Integration
|
||||||
|
|
||||||
|
For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations
|
||||||
|
|
||||||
|
* Python2.7 with PyQt4
|
||||||
|
* Python2.7 with PySide
|
||||||
|
* Python3.6 with PyQt5-5.9
|
||||||
|
* Python3.6 with PySide2-5.9
|
||||||
|
* Python3.7 with PyQt5-5.12
|
||||||
|
* Python3.7 with PySide2-5.12
|
||||||
|
|
||||||
|
More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1)
|
|
@ -1,58 +0,0 @@
|
||||||
Contributions to pyqtgraph are welcome!
|
|
||||||
|
|
||||||
Please use the following guidelines when preparing changes:
|
|
||||||
|
|
||||||
* The preferred method for submitting changes is by github pull request
|
|
||||||
against the "develop" branch.
|
|
||||||
|
|
||||||
* Pull requests should include only a focused and related set of changes.
|
|
||||||
Mixed features and unrelated changes may be rejected.
|
|
||||||
|
|
||||||
* For major changes, it is recommended to discuss your plans on the mailing
|
|
||||||
list or in a github issue before putting in too much effort.
|
|
||||||
|
|
||||||
* Along these lines, please note that pyqtgraph.opengl will be deprecated
|
|
||||||
soon and replaced with VisPy.
|
|
||||||
|
|
||||||
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph
|
|
||||||
uses nose / py.test style testing, so tests should usually be included in a
|
|
||||||
tests/ directory adjacent to the relevant code.
|
|
||||||
|
|
||||||
* Documentation is generated with sphinx; please check that docstring changes
|
|
||||||
compile correctly.
|
|
||||||
|
|
||||||
* Style guidelines:
|
|
||||||
|
|
||||||
* PyQtGraph prefers PEP8 for most style issues, but this is not enforced
|
|
||||||
rigorously as long as the code is clean and readable.
|
|
||||||
|
|
||||||
* Use `python setup.py style` to see whether your code follows
|
|
||||||
the mandatory style guidelines checked by flake8.
|
|
||||||
|
|
||||||
* Exception 1: All variable names should use camelCase rather than
|
|
||||||
underscore_separation. This is done for consistency with Qt
|
|
||||||
|
|
||||||
* Exception 2: Function docstrings use ReStructuredText tables for
|
|
||||||
describing arguments:
|
|
||||||
|
|
||||||
```
|
|
||||||
============== ========================================================
|
|
||||||
**Arguments:**
|
|
||||||
argName1 (type) Description of argument
|
|
||||||
argName2 (type) Description of argument. Longer descriptions must
|
|
||||||
be wrapped within the column guidelines defined by the
|
|
||||||
"====" header and footer.
|
|
||||||
============== ========================================================
|
|
||||||
```
|
|
||||||
|
|
||||||
QObject subclasses that implement new signals should also describe
|
|
||||||
these in a similar table.
|
|
||||||
|
|
||||||
* Setting up a test environment.
|
|
||||||
|
|
||||||
Tests for a module should ideally cover all code in that module,
|
|
||||||
i.e., statement coverage should be at 100%.
|
|
||||||
|
|
||||||
To measure the test coverage, install py.test, pytest-cov and pytest-xdist.
|
|
||||||
Then run 'py.test --cov -n 4' to run the test suite with coverage on 4 cores.
|
|
||||||
|
|
53
README.md
53
README.md
|
@ -1,12 +1,13 @@
|
||||||
[![Build Status](https://travis-ci.org/pyqtgraph/pyqtgraph.svg?branch=develop)](https://travis-ci.org/pyqtgraph/pyqtgraph)
|
|
||||||
[![codecov.io](http://codecov.io/github/pyqtgraph/pyqtgraph/coverage.svg?branch=develop)](http://codecov.io/github/pyqtgraph/pyqtgraph?branch=develop)
|
[![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop)
|
||||||
|
|
||||||
|
|
||||||
PyQtGraph
|
PyQtGraph
|
||||||
=========
|
=========
|
||||||
|
|
||||||
A pure-Python graphics library for PyQt/PySide
|
A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2
|
||||||
|
|
||||||
Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill
|
Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill
|
||||||
|
|
||||||
<http://www.pyqtgraph.org>
|
<http://www.pyqtgraph.org>
|
||||||
|
|
||||||
|
@ -15,32 +16,51 @@ Despite being written entirely in python, the library is fast due to its
|
||||||
heavy leverage of numpy for number crunching, Qt's GraphicsView framework for
|
heavy leverage of numpy for number crunching, Qt's GraphicsView framework for
|
||||||
2D display, and OpenGL for 3D display.
|
2D display, and OpenGL for 3D display.
|
||||||
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* PyQt 4.7+, PySide, PyQt5, or PySide2
|
* PyQt 4.8+, PySide, PyQt5, or PySide2
|
||||||
* python 2.7, or 3.x
|
* python 2.7, or 3.x
|
||||||
* NumPy
|
* Required
|
||||||
* For 3D graphics: pyopengl and qt-opengl
|
* `numpy`, `scipy`
|
||||||
* Known to run on Windows, Linux, and Mac.
|
* Optional
|
||||||
|
* `pyopengl` for 3D graphics
|
||||||
|
* `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy`
|
||||||
|
* `hdf5` for large hdf5 binary format support
|
||||||
|
* Known to run on Windows, Linux, and macOS.
|
||||||
|
|
||||||
|
Qt Bindings Test Matrix
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings.
|
||||||
|
|
||||||
|
| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 |
|
||||||
|
| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: |
|
||||||
|
| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
|
||||||
|
| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: |
|
||||||
|
| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible.
|
||||||
|
|
||||||
Support
|
Support
|
||||||
-------
|
-------
|
||||||
|
|
||||||
* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
|
* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
|
||||||
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
|
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
|
||||||
|
|
||||||
Installation Methods
|
Installation Methods
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
* From pypi:
|
* From PyPI:
|
||||||
- Last released version: `pip install pyqtgraph`
|
* Last released version: `pip install pyqtgraph`
|
||||||
- Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph`
|
* Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop`
|
||||||
|
* From conda
|
||||||
|
* Last released version: `conda install pyqtgraph`
|
||||||
* To install system-wide from source distribution: `python setup.py install`
|
* To install system-wide from source distribution: `python setup.py install`
|
||||||
* Many linux package repositories have release versions.
|
* Many linux package repositories have release versions.
|
||||||
* To use with a specific project, simply copy the pyqtgraph subdirectory
|
* To use with a specific project, simply copy the pyqtgraph subdirectory
|
||||||
anywhere that is importable from your project.
|
anywhere that is importable from your project.
|
||||||
* For installation packages, see the website (pyqtgraph.org)
|
* For installation packages, see the website (pyqtgraph.org)
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
|
@ -49,4 +69,3 @@ Documentation
|
||||||
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu.
|
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu.
|
||||||
|
|
||||||
The official documentation lives at http://pyqtgraph.org/documentation
|
The official documentation lives at http://pyqtgraph.org/documentation
|
||||||
|
|
||||||
|
|
38
azure-pipelines.yml
Normal file
38
azure-pipelines.yml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
############################################################################################
|
||||||
|
# This config was rectrieved in no small part from https://github.com/slaclab/pydm
|
||||||
|
############################################################################################
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- '*' # Build for all branches if they have a azure-pipelines.yml file.
|
||||||
|
tags:
|
||||||
|
include:
|
||||||
|
- 'v*' # Ensure that we are building for tags starting with 'v' (Official Versions)
|
||||||
|
|
||||||
|
# Build only for PRs for master branch
|
||||||
|
pr:
|
||||||
|
autoCancel: true
|
||||||
|
branches:
|
||||||
|
include:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
|
||||||
|
variables:
|
||||||
|
OFFICIAL_REPO: 'pyqtgraph/pyqtgraph'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- template: azure-test-template.yml
|
||||||
|
parameters:
|
||||||
|
name: Linux
|
||||||
|
vmImage: 'Ubuntu 16.04'
|
||||||
|
|
||||||
|
- template: azure-test-template.yml
|
||||||
|
parameters:
|
||||||
|
name: Windows
|
||||||
|
vmImage: 'vs2017-win2016'
|
||||||
|
|
||||||
|
- template: azure-test-template.yml
|
||||||
|
parameters:
|
||||||
|
name: MacOS
|
||||||
|
vmImage: 'macOS-10.13'
|
196
azure-test-template.yml
Normal file
196
azure-test-template.yml
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
# Azure Pipelines CI job template for PyDM Tests
|
||||||
|
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/anaconda?view=azure-devops
|
||||||
|
parameters:
|
||||||
|
name: ''
|
||||||
|
vmImage: ''
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- job: ${{ parameters.name }}
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ parameters.vmImage }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
Python27-PyQt4-4.8:
|
||||||
|
python.version: '2.7'
|
||||||
|
qt.bindings: "pyqt=4"
|
||||||
|
install.method: "conda"
|
||||||
|
Python27-PySide-4.8:
|
||||||
|
python.version: '2.7'
|
||||||
|
qt.bindings: "pyside"
|
||||||
|
install.method: "conda"
|
||||||
|
Python36-PyQt-5.9:
|
||||||
|
python.version: "3.6"
|
||||||
|
qt.bindings: "pyqt"
|
||||||
|
install.method: "conda"
|
||||||
|
Python36-PySide2-5.9:
|
||||||
|
python.version: "3.6"
|
||||||
|
qt.bindings: "pyside2"
|
||||||
|
install.method: "conda"
|
||||||
|
Python37-PyQt-5.12:
|
||||||
|
python.version: '3.7'
|
||||||
|
qt.bindings: "PyQt5"
|
||||||
|
install.method: "pip"
|
||||||
|
Python37-PySide2-5.12:
|
||||||
|
python.version: "3.7"
|
||||||
|
qt.bindings: "PySide2"
|
||||||
|
install.method: "pip"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: ScreenResolutionUtility@1
|
||||||
|
inputs:
|
||||||
|
displaySettings: 'specific'
|
||||||
|
width: '1920'
|
||||||
|
height: '1080'
|
||||||
|
condition: eq(variables['agent.os'], 'Windows_NT' )
|
||||||
|
|
||||||
|
- script: |
|
||||||
|
curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe
|
||||||
|
7z x mesa3d-19.1.0-release-msvc.exe
|
||||||
|
cd x64
|
||||||
|
xcopy opengl32.dll C:\windows\system32\mesadrv.dll*
|
||||||
|
xcopy opengl32.dll C:\windows\syswow64\mesadrv.dll*
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f
|
||||||
|
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f
|
||||||
|
displayName: "Install Windows-Mesa OpenGL DLL"
|
||||||
|
condition: eq(variables['agent.os'], 'Windows_NT')
|
||||||
|
|
||||||
|
- task: UsePythonVersion@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: $(python.version)
|
||||||
|
condition: eq(variables['install.method'], 'pip')
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
if [ $(agent.os) == 'Linux' ]
|
||||||
|
then
|
||||||
|
echo '##vso[task.prependpath]/usr/share/miniconda/bin'
|
||||||
|
elif [ $(agent.os) == 'Darwin' ]
|
||||||
|
then
|
||||||
|
echo '##vso[task.prependpath]$CONDA/bin'
|
||||||
|
sudo install -d -m 0777 /usr/local/miniconda/envs
|
||||||
|
elif [ $(agent.os) == 'Windows_NT' ]
|
||||||
|
then
|
||||||
|
echo "##vso[task.prependpath]$env:CONDA\Scripts"
|
||||||
|
else
|
||||||
|
echo 'Just what OS are you using?'
|
||||||
|
fi
|
||||||
|
displayName: 'Add Conda to $PATH'
|
||||||
|
condition: eq(variables['install.method'], 'conda' )
|
||||||
|
|
||||||
|
- task: CondaEnvironment@0
|
||||||
|
displayName: 'Create Conda Environment'
|
||||||
|
condition: eq(variables['install.method'], 'conda')
|
||||||
|
inputs:
|
||||||
|
environmentName: 'test-environment-$(python.version)'
|
||||||
|
packageSpecs: 'python=$(python.version)'
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
if [ $(install.method) == "conda" ]
|
||||||
|
then
|
||||||
|
source activate test-environment-$(python.version)
|
||||||
|
conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet
|
||||||
|
else
|
||||||
|
pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage
|
||||||
|
fi
|
||||||
|
pip install pytest-xdist pytest-cov pytest-faulthandler
|
||||||
|
displayName: "Install Dependencies"
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
if [ $(install.method) == "conda" ]
|
||||||
|
then
|
||||||
|
source activate test-environment-$(python.version)
|
||||||
|
fi
|
||||||
|
pip install setuptools wheel
|
||||||
|
python setup.py bdist_wheel
|
||||||
|
pip install dist/*.whl
|
||||||
|
displayName: 'Build Wheel and Install'
|
||||||
|
|
||||||
|
- task: CopyFiles@2
|
||||||
|
inputs:
|
||||||
|
contents: 'dist/**'
|
||||||
|
targetFolder: $(Build.ArtifactStagingDirectory)
|
||||||
|
cleanTargetFolder: true
|
||||||
|
displayName: "Copy Binary Wheel Distribution To Artifacts"
|
||||||
|
|
||||||
|
- task: PublishBuildArtifacts@1
|
||||||
|
displayName: 'Publish Binary Wheel'
|
||||||
|
condition: always()
|
||||||
|
inputs:
|
||||||
|
pathtoPublish: $(Build.ArtifactStagingDirectory)/dist
|
||||||
|
artifactName: Distributions
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm
|
||||||
|
if [ $(install.method) == "conda" ]
|
||||||
|
then
|
||||||
|
source activate test-environment-$(python.version)
|
||||||
|
fi
|
||||||
|
pip install pytest-xvfb
|
||||||
|
displayName: "Virtual Display Setup"
|
||||||
|
condition: eq(variables['agent.os'], 'Linux' )
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
if [ $(install.method) == "conda" ]
|
||||||
|
then
|
||||||
|
source activate test-environment-$(python.version)
|
||||||
|
fi
|
||||||
|
echo python location: `which python`
|
||||||
|
echo python version: `python --version`
|
||||||
|
echo pytest location: `which pytest`
|
||||||
|
echo installed packages
|
||||||
|
pip list
|
||||||
|
echo pyqtgraph system info
|
||||||
|
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||||
|
echo display information
|
||||||
|
if [ $(agent.os) == 'Linux' ]
|
||||||
|
then
|
||||||
|
export DISPLAY=:99.0
|
||||||
|
Xvfb :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset &
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
python -m pyqtgraph.util.get_resolution
|
||||||
|
echo openGL information
|
||||||
|
python -c "from pyqtgraph.opengl.glInfo import GLTest"
|
||||||
|
displayName: 'Debug Info'
|
||||||
|
continueOnError: false
|
||||||
|
|
||||||
|
- bash: |
|
||||||
|
if [ $(install.method) == "conda" ]
|
||||||
|
then
|
||||||
|
source activate test-environment-$(python.version)
|
||||||
|
fi
|
||||||
|
mkdir -p "$SCREENSHOT_DIR"
|
||||||
|
# echo "If Screenshots are generated, they may be downloaded from:"
|
||||||
|
# echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0"
|
||||||
|
pytest . -sv \
|
||||||
|
--junitxml=junit/test-results.xml \
|
||||||
|
-n 1 --cov pyqtgraph --cov-report=xml --cov-report=html
|
||||||
|
displayName: 'Unit tests'
|
||||||
|
env:
|
||||||
|
AZURE: 1
|
||||||
|
SCREENSHOT_DIR: $(Build.ArtifactStagingDirectory)/screenshots
|
||||||
|
|
||||||
|
- task: PublishBuildArtifacts@1
|
||||||
|
displayName: 'Publish Screenshots'
|
||||||
|
condition: failed()
|
||||||
|
inputs:
|
||||||
|
pathtoPublish: $(Build.ArtifactStagingDirectory)/screenshots
|
||||||
|
artifactName: Screenshots
|
||||||
|
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
condition: succeededOrFailed()
|
||||||
|
inputs:
|
||||||
|
testResultsFiles: '**/test-*.xml'
|
||||||
|
testRunTitle: 'Test Results for $(agent.os) - $(python.version) - $(qt.bindings) - $(install.method)'
|
||||||
|
publishRunAttachments: true
|
||||||
|
|
||||||
|
- task: PublishCodeCoverageResults@1
|
||||||
|
inputs:
|
||||||
|
codeCoverageTool: Cobertura
|
||||||
|
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
|
||||||
|
reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
|
|
@ -9,18 +9,28 @@ There are many different ways to install pyqtgraph, depending on your needs:
|
||||||
|
|
||||||
Some users may need to call ``pip3`` instead. This method should work on
|
Some users may need to call ``pip3`` instead. This method should work on
|
||||||
all platforms.
|
all platforms.
|
||||||
* To get access to the very latest features and bugfixes, clone pyqtgraph from
|
* To get access to the very latest features and bugfixes you have three choice::
|
||||||
github::
|
|
||||||
|
1. Clone pyqtgraph from github::
|
||||||
|
|
||||||
$ git clone https://github.com/pyqtgraph/pyqtgraph
|
$ git clone https://github.com/pyqtgraph/pyqtgraph
|
||||||
|
|
||||||
Now you can install pyqtgraph from the source::
|
Now you can install pyqtgraph from the source::
|
||||||
|
|
||||||
$ python setup.py install
|
$ python setup.py install
|
||||||
|
|
||||||
..or you can simply place the pyqtgraph folder someplace importable, such as
|
2. Directly install from GitHub repo::
|
||||||
inside the root of another project. PyQtGraph does not need to be "built" or
|
|
||||||
compiled in any way.
|
$ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop
|
||||||
|
|
||||||
|
You can change to ``develop`` of the above command to the branch
|
||||||
|
name or the commit you prefer.
|
||||||
|
|
||||||
|
3.
|
||||||
|
You can simply place the pyqtgraph folder someplace importable, such as
|
||||||
|
inside the root of another project. PyQtGraph does not need to be "built" or
|
||||||
|
compiled in any way.
|
||||||
|
|
||||||
* Packages for pyqtgraph are also available in a few other forms:
|
* Packages for pyqtgraph are also available in a few other forms:
|
||||||
|
|
||||||
* **Anaconda**: ``conda install pyqtgraph``
|
* **Anaconda**: ``conda install pyqtgraph``
|
||||||
|
|
|
@ -92,10 +92,10 @@ def updateRoiPlot(roi, data=None):
|
||||||
rois = []
|
rois = []
|
||||||
rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9)))
|
rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9)))
|
||||||
rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9)))
|
rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9)))
|
||||||
rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9)))
|
rois.append(pg.MultiRectROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9)))
|
||||||
rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9)))
|
rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9)))
|
||||||
rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9)))
|
rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9)))
|
||||||
rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9)))
|
rois.append(pg.PolyLineROI([[2,0], [2.1,0], [2,.1]], pen=(5,9)))
|
||||||
#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0)))
|
#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0)))
|
||||||
|
|
||||||
## Add each ROI to the scene and link its data to a plot curve with the same color
|
## Add each ROI to the scene and link its data to a plot curve with the same color
|
||||||
|
|
|
@ -9,8 +9,8 @@ import subprocess
|
||||||
from pyqtgraph.python2_3 import basestring
|
from pyqtgraph.python2_3 import basestring
|
||||||
from pyqtgraph.Qt import QtGui, QT_LIB
|
from pyqtgraph.Qt import QtGui, QT_LIB
|
||||||
|
|
||||||
|
from .utils import buildFileList, path, examples
|
||||||
|
|
||||||
from .utils import buildFileList, testFile, path, examples
|
|
||||||
|
|
||||||
if QT_LIB == 'PySide':
|
if QT_LIB == 'PySide':
|
||||||
from .exampleLoaderTemplate_pyside import Ui_Form
|
from .exampleLoaderTemplate_pyside import Ui_Form
|
||||||
|
@ -117,30 +117,7 @@ class ExampleLoader(QtGui.QMainWindow):
|
||||||
def run():
|
def run():
|
||||||
app = QtGui.QApplication([])
|
app = QtGui.QApplication([])
|
||||||
loader = ExampleLoader()
|
loader = ExampleLoader()
|
||||||
|
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
run()
|
||||||
args = sys.argv[1:]
|
|
||||||
|
|
||||||
if '--test' in args:
|
|
||||||
# get rid of orphaned cache files first
|
|
||||||
pg.renamePyc(path)
|
|
||||||
|
|
||||||
files = buildFileList(examples)
|
|
||||||
if '--pyside' in args:
|
|
||||||
lib = 'PySide'
|
|
||||||
elif '--pyqt' in args or '--pyqt4' in args:
|
|
||||||
lib = 'PyQt4'
|
|
||||||
elif '--pyqt5' in args:
|
|
||||||
lib = 'PyQt5'
|
|
||||||
else:
|
|
||||||
lib = ''
|
|
||||||
|
|
||||||
exe = sys.executable
|
|
||||||
print("Running tests:", lib, sys.executable)
|
|
||||||
for f in files:
|
|
||||||
testFile(f[0], f[1], exe, lib)
|
|
||||||
else:
|
|
||||||
run()
|
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
from __future__ import print_function, division, absolute_import
|
from __future__ import print_function, division, absolute_import
|
||||||
from pyqtgraph import Qt
|
from pyqtgraph import Qt
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from collections import namedtuple
|
||||||
|
import errno
|
||||||
|
import importlib
|
||||||
import itertools
|
import itertools
|
||||||
|
import pkgutil
|
||||||
import pytest
|
import pytest
|
||||||
import os, sys
|
import os, sys
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
# printing on travis ci frequently leads to "interrupted system call" errors.
|
# printing on travis ci frequently leads to "interrupted system call" errors.
|
||||||
# as a workaround, we overwrite the built-in print function (bleh)
|
# as a workaround, we overwrite the built-in print function (bleh)
|
||||||
if os.getenv('TRAVIS') is not None:
|
if os.getenv('TRAVIS') is not None:
|
||||||
|
@ -32,17 +40,8 @@ if os.getenv('TRAVIS') is not None:
|
||||||
print("Installed wrapper for flaky print.")
|
print("Installed wrapper for flaky print.")
|
||||||
|
|
||||||
|
|
||||||
# apparently importlib does not exist in python 2.6...
|
files = sorted(set(utils.buildFileList(utils.examples)))
|
||||||
try:
|
frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False}
|
||||||
import importlib
|
|
||||||
except ImportError:
|
|
||||||
# we are on python 2.6
|
|
||||||
print("If you want to test the examples, please install importlib from "
|
|
||||||
"pypi\n\npip install importlib\n\n")
|
|
||||||
pass
|
|
||||||
|
|
||||||
files = utils.buildFileList(utils.examples)
|
|
||||||
frontends = {Qt.PYQT4: False, Qt.PYSIDE: False}
|
|
||||||
# sort out which of the front ends are available
|
# sort out which of the front ends are available
|
||||||
for frontend in frontends.keys():
|
for frontend in frontends.keys():
|
||||||
try:
|
try:
|
||||||
|
@ -51,15 +50,99 @@ for frontend in frontends.keys():
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
installedFrontends = sorted([frontend for frontend, isPresent in frontends.items() if isPresent])
|
||||||
|
|
||||||
|
exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"])
|
||||||
|
conditionalExampleTests = {
|
||||||
|
"hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"),
|
||||||
|
"RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"),
|
||||||
|
"optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"frontend, f", itertools.product(sorted(list(frontends.keys())), files))
|
"frontend, f",
|
||||||
def test_examples(frontend, f):
|
[
|
||||||
# Test the examples with all available front-ends
|
pytest.param(
|
||||||
print('frontend = %s. f = %s' % (frontend, f))
|
frontend,
|
||||||
if not frontends[frontend]:
|
f,
|
||||||
pytest.skip('%s is not installed. Skipping tests' % frontend)
|
marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False,
|
||||||
utils.testFile(f[0], f[1], utils.sys.executable, frontend)
|
reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (),
|
||||||
|
)
|
||||||
|
for frontend, f, in itertools.product(installedFrontends, files)
|
||||||
|
],
|
||||||
|
ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)]
|
||||||
|
)
|
||||||
|
def testExamples(frontend, f, graphicsSystem=None):
|
||||||
|
# runExampleFile(f[0], f[1], sys.executable, frontend)
|
||||||
|
|
||||||
|
name, file = f
|
||||||
|
global path
|
||||||
|
fn = os.path.join(path,file)
|
||||||
|
os.chdir(path)
|
||||||
|
sys.stdout.write("{} ".format(name))
|
||||||
|
sys.stdout.flush()
|
||||||
|
import1 = "import %s" % frontend if frontend != '' else ''
|
||||||
|
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||||
|
graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
|
||||||
|
code = """
|
||||||
|
try:
|
||||||
|
%s
|
||||||
|
import initExample
|
||||||
|
import pyqtgraph as pg
|
||||||
|
%s
|
||||||
|
import %s
|
||||||
|
import sys
|
||||||
|
print("test complete")
|
||||||
|
sys.stdout.flush()
|
||||||
|
import time
|
||||||
|
while True: ## run a little event loop
|
||||||
|
pg.QtGui.QApplication.processEvents()
|
||||||
|
time.sleep(0.01)
|
||||||
|
except:
|
||||||
|
print("test failed")
|
||||||
|
raise
|
||||||
|
|
||||||
|
""" % (import1, graphicsSystem, import2)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
process = subprocess.Popen([sys.executable],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
else:
|
||||||
|
process = subprocess.Popen(['exec %s -i' % (sys.executable)],
|
||||||
|
shell=True,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
process.stdin.write(code.encode('UTF-8'))
|
||||||
|
process.stdin.close() ##?
|
||||||
|
output = ''
|
||||||
|
fail = False
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
c = process.stdout.read(1).decode()
|
||||||
|
except IOError as err:
|
||||||
|
if err.errno == errno.EINTR:
|
||||||
|
# Interrupted system call; just try again.
|
||||||
|
c = ''
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
output += c
|
||||||
|
|
||||||
|
if output.endswith('test complete'):
|
||||||
|
break
|
||||||
|
if output.endswith('test failed'):
|
||||||
|
fail = True
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
process.kill()
|
||||||
|
#res = process.communicate()
|
||||||
|
res = (process.stdout.read(), process.stderr.read())
|
||||||
|
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
|
||||||
|
print(res[0].decode())
|
||||||
|
print(res[1].decode())
|
||||||
|
pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.cmdline.main()
|
pytest.cmdline.main()
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
from __future__ import division, print_function, absolute_import
|
from __future__ import division, print_function, absolute_import
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import errno
|
|
||||||
from pyqtgraph.pgcollections import OrderedDict
|
from pyqtgraph.pgcollections import OrderedDict
|
||||||
from pyqtgraph.python2_3 import basestring
|
from pyqtgraph.python2_3 import basestring
|
||||||
|
|
||||||
|
@ -86,7 +82,6 @@ examples = OrderedDict([
|
||||||
#('VerticalLabel', '../widgets/VerticalLabel.py'),
|
#('VerticalLabel', '../widgets/VerticalLabel.py'),
|
||||||
('JoystickButton', 'JoystickButton.py'),
|
('JoystickButton', 'JoystickButton.py'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
('Flowcharts', 'Flowchart.py'),
|
('Flowcharts', 'Flowchart.py'),
|
||||||
('Custom Flowchart Nodes', 'FlowchartCustomNode.py'),
|
('Custom Flowchart Nodes', 'FlowchartCustomNode.py'),
|
||||||
])
|
])
|
||||||
|
@ -103,73 +98,3 @@ def buildFileList(examples, files=None):
|
||||||
else:
|
else:
|
||||||
buildFileList(val, files)
|
buildFileList(val, files)
|
||||||
return files
|
return files
|
||||||
|
|
||||||
def testFile(name, f, exe, lib, graphicsSystem=None):
|
|
||||||
global path
|
|
||||||
fn = os.path.join(path,f)
|
|
||||||
#print "starting process: ", fn
|
|
||||||
os.chdir(path)
|
|
||||||
sys.stdout.write(name)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
import1 = "import %s" % lib if lib != '' else ''
|
|
||||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
|
||||||
graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
|
|
||||||
code = """
|
|
||||||
try:
|
|
||||||
%s
|
|
||||||
import initExample
|
|
||||||
import pyqtgraph as pg
|
|
||||||
%s
|
|
||||||
import %s
|
|
||||||
import sys
|
|
||||||
print("test complete")
|
|
||||||
sys.stdout.flush()
|
|
||||||
import time
|
|
||||||
while True: ## run a little event loop
|
|
||||||
pg.QtGui.QApplication.processEvents()
|
|
||||||
time.sleep(0.01)
|
|
||||||
except:
|
|
||||||
print("test failed")
|
|
||||||
raise
|
|
||||||
|
|
||||||
""" % (import1, graphicsSystem, import2)
|
|
||||||
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
process = subprocess.Popen([exe], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
process.stdin.write(code.encode('UTF-8'))
|
|
||||||
process.stdin.close()
|
|
||||||
else:
|
|
||||||
process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
process.stdin.write(code.encode('UTF-8'))
|
|
||||||
process.stdin.close() ##?
|
|
||||||
output = ''
|
|
||||||
fail = False
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
c = process.stdout.read(1).decode()
|
|
||||||
except IOError as err:
|
|
||||||
if err.errno == errno.EINTR:
|
|
||||||
# Interrupted system call; just try again.
|
|
||||||
c = ''
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
output += c
|
|
||||||
#sys.stdout.write(c)
|
|
||||||
#sys.stdout.flush()
|
|
||||||
if output.endswith('test complete'):
|
|
||||||
break
|
|
||||||
if output.endswith('test failed'):
|
|
||||||
fail = True
|
|
||||||
break
|
|
||||||
time.sleep(1)
|
|
||||||
process.kill()
|
|
||||||
#res = process.communicate()
|
|
||||||
res = (process.stdout.read(), process.stderr.read())
|
|
||||||
|
|
||||||
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
|
|
||||||
print('.' * (50-len(name)) + 'FAILED')
|
|
||||||
print(res[0].decode())
|
|
||||||
print(res[1].decode())
|
|
||||||
else:
|
|
||||||
print('.' * (50-len(name)) + 'passed')
|
|
||||||
|
|
|
@ -263,7 +263,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||||
for item in prevItems:
|
for item in prevItems:
|
||||||
event.currentItem = item
|
event.currentItem = item
|
||||||
try:
|
try:
|
||||||
item.hoverEvent(event)
|
if item.scene() is self:
|
||||||
|
item.hoverEvent(event)
|
||||||
except:
|
except:
|
||||||
debug.printExc("Error sending hover exit event:")
|
debug.printExc("Error sending hover exit event:")
|
||||||
finally:
|
finally:
|
||||||
|
@ -288,7 +289,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||||
else:
|
else:
|
||||||
acceptedItem = None
|
acceptedItem = None
|
||||||
|
|
||||||
if acceptedItem is not None:
|
if acceptedItem is not None and acceptedItem.scene() is self:
|
||||||
#print "Drag -> pre-selected item:", acceptedItem
|
#print "Drag -> pre-selected item:", acceptedItem
|
||||||
self.dragItem = acceptedItem
|
self.dragItem = acceptedItem
|
||||||
event.currentItem = self.dragItem
|
event.currentItem = self.dragItem
|
||||||
|
@ -435,6 +436,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||||
for item in items:
|
for item in items:
|
||||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||||
continue
|
continue
|
||||||
|
if item.scene() is not self:
|
||||||
|
continue
|
||||||
shape = item.shape() # Note: default shape() returns boundingRect()
|
shape = item.shape() # Note: default shape() returns boundingRect()
|
||||||
if shape is None:
|
if shape is None:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -100,10 +100,13 @@ def _loadUiType(uiFile):
|
||||||
how to make PyQt4 and pyside look the same...
|
how to make PyQt4 and pyside look the same...
|
||||||
http://stackoverflow.com/a/8717832
|
http://stackoverflow.com/a/8717832
|
||||||
"""
|
"""
|
||||||
import pysideuic
|
|
||||||
|
if QT_LIB == "PYSIDE":
|
||||||
|
import pysideuic
|
||||||
|
else:
|
||||||
|
import pyside2uic as pysideuic
|
||||||
import xml.etree.ElementTree as xml
|
import xml.etree.ElementTree as xml
|
||||||
#from io import StringIO
|
|
||||||
|
|
||||||
parsed = xml.parse(uiFile)
|
parsed = xml.parse(uiFile)
|
||||||
widget_class = parsed.find('widget').get('class')
|
widget_class = parsed.find('widget').get('class')
|
||||||
form_class = parsed.find('class').text
|
form_class = parsed.find('class').text
|
||||||
|
@ -216,8 +219,12 @@ elif QT_LIB == PYSIDE2:
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
QtTest = FailedImport(err)
|
QtTest = FailedImport(err)
|
||||||
|
|
||||||
isQObjectAlive = _isQObjectAlive
|
try:
|
||||||
|
import shiboken2
|
||||||
|
isQObjectAlive = shiboken2.isValid
|
||||||
|
except ImportError:
|
||||||
|
# use approximate version
|
||||||
|
isQObjectAlive = _isQObjectAlive
|
||||||
import PySide2
|
import PySide2
|
||||||
VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
|
VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
|
||||||
|
|
||||||
|
|
|
@ -67,11 +67,11 @@ class SignalProxy(QtCore.QObject):
|
||||||
"""If there is a signal queued up, send it now."""
|
"""If there is a signal queued up, send it now."""
|
||||||
if self.args is None or self.block:
|
if self.args is None or self.block:
|
||||||
return False
|
return False
|
||||||
#self.emit(self.signal, *self.args)
|
args, self.args = self.args, None
|
||||||
self.sigDelayed.emit(self.args)
|
|
||||||
self.args = None
|
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
self.lastFlushTime = time()
|
self.lastFlushTime = time()
|
||||||
|
#self.emit(self.signal, *self.args)
|
||||||
|
self.sigDelayed.emit(args)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
|
|
|
@ -141,7 +141,7 @@ class ColorMap(object):
|
||||||
|
|
||||||
pos, color = self.getStops(mode=self.BYTE)
|
pos, color = self.getStops(mode=self.BYTE)
|
||||||
color = [QtGui.QColor(*x) for x in color]
|
color = [QtGui.QColor(*x) for x in color]
|
||||||
g.setStops(zip(pos, color))
|
g.setStops(list(zip(pos, color)))
|
||||||
|
|
||||||
#if self.colorMode == 'rgb':
|
#if self.colorMode == 'rgb':
|
||||||
#ticks = self.listTicks()
|
#ticks = self.listTicks()
|
||||||
|
|
|
@ -33,9 +33,8 @@ class ParseError(Exception):
|
||||||
msg = "Error parsing string at line %d:\n" % self.lineNum
|
msg = "Error parsing string at line %d:\n" % self.lineNum
|
||||||
else:
|
else:
|
||||||
msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum)
|
msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum)
|
||||||
msg += "%s\n%s" % (self.line, self.message)
|
msg += "%s\n%s" % (self.line, Exception.__str__(self))
|
||||||
return msg
|
return msg
|
||||||
#raise Exception()
|
|
||||||
|
|
||||||
|
|
||||||
def writeConfigFile(data, fname):
|
def writeConfigFile(data, fname):
|
||||||
|
@ -93,13 +92,14 @@ def genString(data, indent=''):
|
||||||
s += indent + sk + ':\n'
|
s += indent + sk + ':\n'
|
||||||
s += genString(data[k], indent + ' ')
|
s += genString(data[k], indent + ' ')
|
||||||
else:
|
else:
|
||||||
s += indent + sk + ': ' + repr(data[k]) + '\n'
|
s += indent + sk + ': ' + repr(data[k]).replace("\n", "\\\n") + '\n'
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def parseString(lines, start=0):
|
def parseString(lines, start=0):
|
||||||
|
|
||||||
data = OrderedDict()
|
data = OrderedDict()
|
||||||
if isinstance(lines, basestring):
|
if isinstance(lines, basestring):
|
||||||
|
lines = lines.replace("\\\n", "")
|
||||||
lines = lines.split('\n')
|
lines = lines.split('\n')
|
||||||
lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines
|
lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines
|
||||||
|
|
||||||
|
|
|
@ -346,9 +346,9 @@ class DockLabel(VerticalLabel):
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
|
ev.accept()
|
||||||
if not self.startedDrag:
|
if not self.startedDrag:
|
||||||
self.sigClicked.emit(self, ev)
|
self.sigClicked.emit(self, ev)
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, ev):
|
def mouseDoubleClickEvent(self, ev):
|
||||||
if ev.button() == QtCore.Qt.LeftButton:
|
if ev.button() == QtCore.Qt.LeftButton:
|
||||||
|
|
|
@ -124,5 +124,4 @@ class MatplotlibWindow(QtGui.QMainWindow):
|
||||||
|
|
||||||
def closeEvent(self, ev):
|
def closeEvent(self, ev):
|
||||||
MatplotlibExporter.windows.remove(self)
|
MatplotlibExporter.windows.remove(self)
|
||||||
|
self.deleteLater()
|
||||||
|
|
||||||
|
|
|
@ -190,12 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}):
|
||||||
## this is taken care of in generateSvg instead.
|
## this is taken care of in generateSvg instead.
|
||||||
#if hasattr(item, 'setExportMode'):
|
#if hasattr(item, 'setExportMode'):
|
||||||
#item.setExportMode(False)
|
#item.setExportMode(False)
|
||||||
|
doc = xml.parseString(arr.data())
|
||||||
if QT_LIB in ['PySide', 'PySide2']:
|
|
||||||
xmlStr = str(arr)
|
|
||||||
else:
|
|
||||||
xmlStr = bytes(arr).decode('utf-8')
|
|
||||||
doc = xml.parseString(xmlStr.encode('utf-8'))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
## Get top-level group for this item
|
## Get top-level group for this item
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
SVG export test
|
CSV export test
|
||||||
"""
|
"""
|
||||||
from __future__ import division, print_function, absolute_import
|
from __future__ import division, print_function, absolute_import
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
@ -33,8 +33,9 @@ def test_CSVExporter():
|
||||||
ex = pg.exporters.CSVExporter(plt.plotItem)
|
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||||
ex.export(fileName=tempfilename)
|
ex.export(fileName=tempfilename)
|
||||||
|
|
||||||
r = csv.reader(open(tempfilename, 'r'))
|
with open(tempfilename, 'r') as csv_file:
|
||||||
lines = [line for line in r]
|
r = csv.reader(csv_file)
|
||||||
|
lines = [line for line in r]
|
||||||
header = lines.pop(0)
|
header = lines.pop(0)
|
||||||
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ from .. import configfile as configfile
|
||||||
from .. import dockarea as dockarea
|
from .. import dockarea as dockarea
|
||||||
from . import FlowchartGraphicsView
|
from . import FlowchartGraphicsView
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
|
from ..python2_3 import asUnicode
|
||||||
|
|
||||||
def strDict(d):
|
def strDict(d):
|
||||||
return dict([(str(k), v) for k, v in d.items()])
|
return dict([(str(k), v) for k, v in d.items()])
|
||||||
|
@ -502,8 +503,8 @@ class Flowchart(Node):
|
||||||
finally:
|
finally:
|
||||||
self.blockSignals(False)
|
self.blockSignals(False)
|
||||||
|
|
||||||
self.sigChartLoaded.emit()
|
|
||||||
self.outputChanged()
|
self.outputChanged()
|
||||||
|
self.sigChartLoaded.emit()
|
||||||
self.sigStateChanged.emit()
|
self.sigStateChanged.emit()
|
||||||
|
|
||||||
def loadFile(self, fileName=None, startDir=None):
|
def loadFile(self, fileName=None, startDir=None):
|
||||||
|
@ -519,12 +520,12 @@ class Flowchart(Node):
|
||||||
self.fileDialog.fileSelected.connect(self.loadFile)
|
self.fileDialog.fileSelected.connect(self.loadFile)
|
||||||
return
|
return
|
||||||
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
|
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
|
||||||
fileName = unicode(fileName)
|
fileName = asUnicode(fileName)
|
||||||
state = configfile.readConfigFile(fileName)
|
state = configfile.readConfigFile(fileName)
|
||||||
self.restoreState(state, clear=True)
|
self.restoreState(state, clear=True)
|
||||||
self.viewBox.autoRange()
|
self.viewBox.autoRange()
|
||||||
self.sigFileLoaded.emit(fileName)
|
self.sigFileLoaded.emit(fileName)
|
||||||
|
|
||||||
def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'):
|
def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'):
|
||||||
"""Save this flowchart to a .fc file
|
"""Save this flowchart to a .fc file
|
||||||
"""
|
"""
|
||||||
|
@ -534,11 +535,12 @@ class Flowchart(Node):
|
||||||
if startDir is None:
|
if startDir is None:
|
||||||
startDir = '.'
|
startDir = '.'
|
||||||
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
||||||
|
self.fileDialog.setDefaultSuffix("fc")
|
||||||
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||||
self.fileDialog.show()
|
self.fileDialog.show()
|
||||||
self.fileDialog.fileSelected.connect(self.saveFile)
|
self.fileDialog.fileSelected.connect(self.saveFile)
|
||||||
return
|
return
|
||||||
fileName = unicode(fileName)
|
fileName = asUnicode(fileName)
|
||||||
configfile.writeConfigFile(self.saveState(), fileName)
|
configfile.writeConfigFile(self.saveState(), fileName)
|
||||||
self.sigFileSaved.emit(fileName)
|
self.sigFileSaved.emit(fileName)
|
||||||
|
|
||||||
|
@ -662,7 +664,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||||
#self.setCurrentFile(newFile)
|
#self.setCurrentFile(newFile)
|
||||||
|
|
||||||
def fileSaved(self, fileName):
|
def fileSaved(self, fileName):
|
||||||
self.setCurrentFile(unicode(fileName))
|
self.setCurrentFile(asUnicode(fileName))
|
||||||
self.ui.saveBtn.success("Saved.")
|
self.ui.saveBtn.success("Saved.")
|
||||||
|
|
||||||
def saveClicked(self):
|
def saveClicked(self):
|
||||||
|
@ -691,7 +693,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||||
#self.setCurrentFile(newFile)
|
#self.setCurrentFile(newFile)
|
||||||
|
|
||||||
def setCurrentFile(self, fileName):
|
def setCurrentFile(self, fileName):
|
||||||
self.currentFileName = unicode(fileName)
|
self.currentFileName = asUnicode(fileName)
|
||||||
if fileName is None:
|
if fileName is None:
|
||||||
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
|
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -373,7 +373,7 @@ class Node(QtCore.QObject):
|
||||||
pos = self.graphicsItem().pos()
|
pos = self.graphicsItem().pos()
|
||||||
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||||
termsEditable = self._allowAddInput | self._allowAddOutput
|
termsEditable = self._allowAddInput | self._allowAddOutput
|
||||||
for term in self._inputs.values() + self._outputs.values():
|
for term in list(self._inputs.values()) + list(self._outputs.values()):
|
||||||
termsEditable |= term._renamable | term._removable | term._multiable
|
termsEditable |= term._renamable | term._removable | term._multiable
|
||||||
if termsEditable:
|
if termsEditable:
|
||||||
state['terminals'] = self.saveTerminals()
|
state['terminals'] = self.saveTerminals()
|
||||||
|
|
|
@ -1057,6 +1057,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||||
raise Exception('levels argument is required for float input types')
|
raise Exception('levels argument is required for float input types')
|
||||||
if not isinstance(levels, np.ndarray):
|
if not isinstance(levels, np.ndarray):
|
||||||
levels = np.array(levels)
|
levels = np.array(levels)
|
||||||
|
levels = levels.astype(np.float)
|
||||||
if levels.ndim == 1:
|
if levels.ndim == 1:
|
||||||
if levels.shape[0] != 2:
|
if levels.shape[0] != 2:
|
||||||
raise Exception('levels argument must have length 2')
|
raise Exception('levels argument must have length 2')
|
||||||
|
@ -1093,7 +1094,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||||
for i in range(data.shape[-1]):
|
for i in range(data.shape[-1]):
|
||||||
minVal, maxVal = levels[i]
|
minVal, maxVal = levels[i]
|
||||||
if minVal == maxVal:
|
if minVal == maxVal:
|
||||||
maxVal += 1e-16
|
maxVal = np.nextafter(maxVal, 2*maxVal)
|
||||||
rng = maxVal-minVal
|
rng = maxVal-minVal
|
||||||
rng = 1 if rng == 0 else rng
|
rng = 1 if rng == 0 else rng
|
||||||
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
|
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
|
||||||
|
@ -1103,7 +1104,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||||
minVal, maxVal = levels
|
minVal, maxVal = levels
|
||||||
if minVal != 0 or maxVal != scale:
|
if minVal != 0 or maxVal != scale:
|
||||||
if minVal == maxVal:
|
if minVal == maxVal:
|
||||||
maxVal += 1e-16
|
maxVal = np.nextafter(maxVal, 2*maxVal)
|
||||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
|
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1380,7 +1381,7 @@ def gaussianFilter(data, sigma):
|
||||||
# clip off extra data
|
# clip off extra data
|
||||||
sl = [slice(None)] * data.ndim
|
sl = [slice(None)] * data.ndim
|
||||||
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
|
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
|
||||||
filtered = filtered[sl]
|
filtered = filtered[tuple(sl)]
|
||||||
return filtered + baseline
|
return filtered + baseline
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@ class AxisItem(GraphicsWidget):
|
||||||
GraphicsItem showing a single plot axis with ticks, values, and label.
|
GraphicsItem showing a single plot axis with ticks, values, and label.
|
||||||
Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items.
|
Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items.
|
||||||
Ticks can be extended to draw a grid.
|
Ticks can be extended to draw a grid.
|
||||||
If maxTickLength is negative, ticks point into the plot.
|
If maxTickLength is negative, ticks point into the plot.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True):
|
def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
|
||||||
"""
|
"""
|
||||||
============== ===============================================================
|
============== ===============================================================
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
|
@ -26,11 +26,19 @@ class AxisItem(GraphicsWidget):
|
||||||
into the plot, positive values draw outward.
|
into the plot, positive values draw outward.
|
||||||
linkView (ViewBox) causes the range of values displayed in the axis
|
linkView (ViewBox) causes the range of values displayed in the axis
|
||||||
to be linked to the visible range of a ViewBox.
|
to be linked to the visible range of a ViewBox.
|
||||||
showValues (bool) Whether to display values adjacent to ticks
|
showValues (bool) Whether to display values adjacent to ticks
|
||||||
pen (QPen) Pen used when drawing ticks.
|
pen (QPen) Pen used when drawing ticks.
|
||||||
|
text The text (excluding units) to display on the label for this
|
||||||
|
axis.
|
||||||
|
units The units for this axis. Units should generally be given
|
||||||
|
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
||||||
|
scaling prefix will be automatically prepended based on the
|
||||||
|
range of data displayed.
|
||||||
|
**args All extra keyword arguments become CSS style options for
|
||||||
|
the <span> tag which will surround the axis label and units.
|
||||||
============== ===============================================================
|
============== ===============================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GraphicsWidget.__init__(self, parent)
|
GraphicsWidget.__init__(self, parent)
|
||||||
self.label = QtGui.QGraphicsTextItem(self)
|
self.label = QtGui.QGraphicsTextItem(self)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
@ -39,15 +47,15 @@ class AxisItem(GraphicsWidget):
|
||||||
raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
|
raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
|
||||||
if orientation in ['left', 'right']:
|
if orientation in ['left', 'right']:
|
||||||
self.label.rotate(-90)
|
self.label.rotate(-90)
|
||||||
|
|
||||||
self.style = {
|
self.style = {
|
||||||
'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis
|
'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis
|
||||||
'tickTextWidth': 30, ## space reserved for tick text
|
'tickTextWidth': 30, ## space reserved for tick text
|
||||||
'tickTextHeight': 18,
|
'tickTextHeight': 18,
|
||||||
'autoExpandTextSpace': True, ## automatically expand text space if needed
|
'autoExpandTextSpace': True, ## automatically expand text space if needed
|
||||||
'tickFont': None,
|
'tickFont': None,
|
||||||
'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick
|
'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick
|
||||||
'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally.
|
'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally.
|
||||||
(0, 0.8), ## never fill more than 80% of the axis
|
(0, 0.8), ## never fill more than 80% of the axis
|
||||||
(2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis
|
(2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis
|
||||||
(4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis
|
(4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis
|
||||||
|
@ -58,93 +66,93 @@ class AxisItem(GraphicsWidget):
|
||||||
'maxTickLevel': 2,
|
'maxTickLevel': 2,
|
||||||
'maxTextLevel': 2,
|
'maxTextLevel': 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
||||||
self.textHeight = 18
|
self.textHeight = 18
|
||||||
|
|
||||||
# If the user specifies a width / height, remember that setting
|
# If the user specifies a width / height, remember that setting
|
||||||
# indefinitely.
|
# indefinitely.
|
||||||
self.fixedWidth = None
|
self.fixedWidth = None
|
||||||
self.fixedHeight = None
|
self.fixedHeight = None
|
||||||
|
|
||||||
self.labelText = ''
|
self.labelText = text
|
||||||
self.labelUnits = ''
|
self.labelUnits = units
|
||||||
self.labelUnitPrefix=''
|
self.labelUnitPrefix = unitPrefix
|
||||||
self.labelStyle = {}
|
self.labelStyle = args
|
||||||
self.logMode = False
|
self.logMode = False
|
||||||
self.tickFont = None
|
self.tickFont = None
|
||||||
|
|
||||||
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
||||||
self._tickSpacing = None # used to override default tickSpacing method
|
self._tickSpacing = None # used to override default tickSpacing method
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
self.autoSIPrefix = True
|
self.autoSIPrefix = True
|
||||||
self.autoSIPrefixScale = 1.0
|
self.autoSIPrefixScale = 1.0
|
||||||
|
|
||||||
|
self.showLabel(False)
|
||||||
|
|
||||||
self.setRange(0, 1)
|
self.setRange(0, 1)
|
||||||
|
|
||||||
if pen is None:
|
if pen is None:
|
||||||
self.setPen()
|
self.setPen()
|
||||||
else:
|
else:
|
||||||
self.setPen(pen)
|
self.setPen(pen)
|
||||||
|
|
||||||
self._linkedView = None
|
self._linkedView = None
|
||||||
if linkView is not None:
|
if linkView is not None:
|
||||||
self.linkToView(linkView)
|
self.linkToView(linkView)
|
||||||
|
|
||||||
self.showLabel(False)
|
|
||||||
|
|
||||||
self.grid = False
|
self.grid = False
|
||||||
#self.setCacheMode(self.DeviceCoordinateCache)
|
#self.setCacheMode(self.DeviceCoordinateCache)
|
||||||
|
|
||||||
def setStyle(self, **kwds):
|
def setStyle(self, **kwds):
|
||||||
"""
|
"""
|
||||||
Set various style options.
|
Set various style options.
|
||||||
|
|
||||||
=================== =======================================================
|
=================== =======================================================
|
||||||
Keyword Arguments:
|
Keyword Arguments:
|
||||||
tickLength (int) The maximum length of ticks in pixels.
|
tickLength (int) The maximum length of ticks in pixels.
|
||||||
Positive values point toward the text; negative
|
Positive values point toward the text; negative
|
||||||
values point away.
|
values point away.
|
||||||
tickTextOffset (int) reserved spacing between text and axis in px
|
tickTextOffset (int) reserved spacing between text and axis in px
|
||||||
tickTextWidth (int) Horizontal space reserved for tick text in px
|
tickTextWidth (int) Horizontal space reserved for tick text in px
|
||||||
tickTextHeight (int) Vertical space reserved for tick text in px
|
tickTextHeight (int) Vertical space reserved for tick text in px
|
||||||
autoExpandTextSpace (bool) Automatically expand text space if the tick
|
autoExpandTextSpace (bool) Automatically expand text space if the tick
|
||||||
strings become too long.
|
strings become too long.
|
||||||
tickFont (QFont or None) Determines the font used for tick
|
tickFont (QFont or None) Determines the font used for tick
|
||||||
values. Use None for the default font.
|
values. Use None for the default font.
|
||||||
stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis
|
stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis
|
||||||
line is drawn only as far as the last tick.
|
line is drawn only as far as the last tick.
|
||||||
Otherwise, the line is drawn to the edge of the
|
Otherwise, the line is drawn to the edge of the
|
||||||
AxisItem boundary.
|
AxisItem boundary.
|
||||||
textFillLimits (list of (tick #, % fill) tuples). This structure
|
textFillLimits (list of (tick #, % fill) tuples). This structure
|
||||||
determines how the AxisItem decides how many ticks
|
determines how the AxisItem decides how many ticks
|
||||||
should have text appear next to them. Each tuple in
|
should have text appear next to them. Each tuple in
|
||||||
the list specifies what fraction of the axis length
|
the list specifies what fraction of the axis length
|
||||||
may be occupied by text, given the number of ticks
|
may be occupied by text, given the number of ticks
|
||||||
that already have text displayed. For example::
|
that already have text displayed. For example::
|
||||||
|
|
||||||
[(0, 0.8), # Never fill more than 80% of the axis
|
[(0, 0.8), # Never fill more than 80% of the axis
|
||||||
(2, 0.6), # If we already have 2 ticks with text,
|
(2, 0.6), # If we already have 2 ticks with text,
|
||||||
# fill no more than 60% of the axis
|
# fill no more than 60% of the axis
|
||||||
(4, 0.4), # If we already have 4 ticks with text,
|
(4, 0.4), # If we already have 4 ticks with text,
|
||||||
# fill no more than 40% of the axis
|
# fill no more than 40% of the axis
|
||||||
(6, 0.2)] # If we already have 6 ticks with text,
|
(6, 0.2)] # If we already have 6 ticks with text,
|
||||||
# fill no more than 20% of the axis
|
# fill no more than 20% of the axis
|
||||||
|
|
||||||
showValues (bool) indicates whether text is displayed adjacent
|
showValues (bool) indicates whether text is displayed adjacent
|
||||||
to ticks.
|
to ticks.
|
||||||
=================== =======================================================
|
=================== =======================================================
|
||||||
|
|
||||||
Added in version 0.9.9
|
Added in version 0.9.9
|
||||||
"""
|
"""
|
||||||
for kwd,value in kwds.items():
|
for kwd,value in kwds.items():
|
||||||
if kwd not in self.style:
|
if kwd not in self.style:
|
||||||
raise NameError("%s is not a valid style argument." % kwd)
|
raise NameError("%s is not a valid style argument." % kwd)
|
||||||
|
|
||||||
if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'):
|
if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'):
|
||||||
if not isinstance(value, int):
|
if not isinstance(value, int):
|
||||||
raise ValueError("Argument '%s' must be int" % kwd)
|
raise ValueError("Argument '%s' must be int" % kwd)
|
||||||
|
|
||||||
if kwd == 'tickTextOffset':
|
if kwd == 'tickTextOffset':
|
||||||
if self.orientation in ('left', 'right'):
|
if self.orientation in ('left', 'right'):
|
||||||
self.style['tickTextOffset'][0] = value
|
self.style['tickTextOffset'][0] = value
|
||||||
|
@ -158,19 +166,19 @@ class AxisItem(GraphicsWidget):
|
||||||
self.style[kwd] = value
|
self.style[kwd] = value
|
||||||
else:
|
else:
|
||||||
self.style[kwd] = value
|
self.style[kwd] = value
|
||||||
|
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self._adjustSize()
|
self._adjustSize()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.scene().removeItem(self.label)
|
self.scene().removeItem(self.label)
|
||||||
self.label = None
|
self.label = None
|
||||||
self.scene().removeItem(self)
|
self.scene().removeItem(self)
|
||||||
|
|
||||||
def setGrid(self, grid):
|
def setGrid(self, grid):
|
||||||
"""Set the alpha value (0-255) for the grid, or False to disable.
|
"""Set the alpha value (0-255) for the grid, or False to disable.
|
||||||
|
|
||||||
When grid lines are enabled, the axis tick lines are extended to cover
|
When grid lines are enabled, the axis tick lines are extended to cover
|
||||||
the extent of the linked ViewBox, if any.
|
the extent of the linked ViewBox, if any.
|
||||||
"""
|
"""
|
||||||
|
@ -178,28 +186,28 @@ class AxisItem(GraphicsWidget):
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setLogMode(self, log):
|
def setLogMode(self, log):
|
||||||
"""
|
"""
|
||||||
If *log* is True, then ticks are displayed on a logarithmic scale and values
|
If *log* is True, then ticks are displayed on a logarithmic scale and values
|
||||||
are adjusted accordingly. (This is usually accessed by changing the log mode
|
are adjusted accordingly. (This is usually accessed by changing the log mode
|
||||||
of a :func:`PlotItem <pyqtgraph.PlotItem.setLogMode>`)
|
of a :func:`PlotItem <pyqtgraph.PlotItem.setLogMode>`)
|
||||||
"""
|
"""
|
||||||
self.logMode = log
|
self.logMode = log
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setTickFont(self, font):
|
def setTickFont(self, font):
|
||||||
self.tickFont = font
|
self.tickFont = font
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
## Need to re-allocate space depending on font size?
|
## Need to re-allocate space depending on font size?
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def resizeEvent(self, ev=None):
|
def resizeEvent(self, ev=None):
|
||||||
#s = self.size()
|
#s = self.size()
|
||||||
|
|
||||||
## Set the position of the label
|
## Set the position of the label
|
||||||
nudge = 5
|
nudge = 5
|
||||||
br = self.label.boundingRect()
|
br = self.label.boundingRect()
|
||||||
|
@ -218,7 +226,7 @@ class AxisItem(GraphicsWidget):
|
||||||
p.setY(int(self.size().height()-br.height()+nudge))
|
p.setY(int(self.size().height()-br.height()+nudge))
|
||||||
self.label.setPos(p)
|
self.label.setPos(p)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
|
||||||
def showLabel(self, show=True):
|
def showLabel(self, show=True):
|
||||||
"""Show/hide the label text for this axis."""
|
"""Show/hide the label text for this axis."""
|
||||||
#self.drawLabel = show
|
#self.drawLabel = show
|
||||||
|
@ -229,10 +237,10 @@ class AxisItem(GraphicsWidget):
|
||||||
self._updateHeight()
|
self._updateHeight()
|
||||||
if self.autoSIPrefix:
|
if self.autoSIPrefix:
|
||||||
self.updateAutoSIPrefix()
|
self.updateAutoSIPrefix()
|
||||||
|
|
||||||
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
|
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
|
||||||
"""Set the text displayed adjacent to the axis.
|
"""Set the text displayed adjacent to the axis.
|
||||||
|
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
text The text (excluding units) to display on the label for this
|
text The text (excluding units) to display on the label for this
|
||||||
|
@ -244,23 +252,26 @@ class AxisItem(GraphicsWidget):
|
||||||
**args All extra keyword arguments become CSS style options for
|
**args All extra keyword arguments become CSS style options for
|
||||||
the <span> tag which will surround the axis label and units.
|
the <span> tag which will surround the axis label and units.
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
|
|
||||||
The final text generated for the label will look like::
|
The final text generated for the label will look like::
|
||||||
|
|
||||||
<span style="...options...">{text} (prefix{units})</span>
|
<span style="...options...">{text} (prefix{units})</span>
|
||||||
|
|
||||||
Each extra keyword argument will become a CSS option in the above template.
|
Each extra keyword argument will become a CSS option in the above template.
|
||||||
For example, you can set the font size and color of the label::
|
For example, you can set the font size and color of the label::
|
||||||
|
|
||||||
labelStyle = {'color': '#FFF', 'font-size': '14pt'}
|
labelStyle = {'color': '#FFF', 'font-size': '14pt'}
|
||||||
axis.setLabel('label text', units='V', **labelStyle)
|
axis.setLabel('label text', units='V', **labelStyle)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
show_label = False
|
||||||
if text is not None:
|
if text is not None:
|
||||||
self.labelText = text
|
self.labelText = text
|
||||||
self.showLabel()
|
show_label = True
|
||||||
if units is not None:
|
if units is not None:
|
||||||
self.labelUnits = units
|
self.labelUnits = units
|
||||||
|
show_label = True
|
||||||
|
if show_label:
|
||||||
self.showLabel()
|
self.showLabel()
|
||||||
if unitPrefix is not None:
|
if unitPrefix is not None:
|
||||||
self.labelUnitPrefix = unitPrefix
|
self.labelUnitPrefix = unitPrefix
|
||||||
|
@ -270,7 +281,7 @@ class AxisItem(GraphicsWidget):
|
||||||
self._adjustSize()
|
self._adjustSize()
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def labelString(self):
|
def labelString(self):
|
||||||
if self.labelUnits == '':
|
if self.labelUnits == '':
|
||||||
if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0:
|
if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0:
|
||||||
|
@ -280,13 +291,13 @@ class AxisItem(GraphicsWidget):
|
||||||
else:
|
else:
|
||||||
#print repr(self.labelUnitPrefix), repr(self.labelUnits)
|
#print repr(self.labelUnitPrefix), repr(self.labelUnits)
|
||||||
units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits))
|
units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits))
|
||||||
|
|
||||||
s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units))
|
s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units))
|
||||||
|
|
||||||
style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
|
style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
|
||||||
|
|
||||||
return asUnicode("<span style='%s'>%s</span>") % (style, asUnicode(s))
|
return asUnicode("<span style='%s'>%s</span>") % (style, asUnicode(s))
|
||||||
|
|
||||||
def _updateMaxTextSize(self, x):
|
def _updateMaxTextSize(self, x):
|
||||||
## Informs that the maximum tick size orthogonal to the axis has
|
## Informs that the maximum tick size orthogonal to the axis has
|
||||||
## changed; we use this to decide whether the item needs to be resized
|
## changed; we use this to decide whether the item needs to be resized
|
||||||
|
@ -305,22 +316,22 @@ class AxisItem(GraphicsWidget):
|
||||||
if self.style['autoExpandTextSpace'] is True:
|
if self.style['autoExpandTextSpace'] is True:
|
||||||
self._updateHeight()
|
self._updateHeight()
|
||||||
#return True ## size has changed
|
#return True ## size has changed
|
||||||
|
|
||||||
def _adjustSize(self):
|
def _adjustSize(self):
|
||||||
if self.orientation in ['left', 'right']:
|
if self.orientation in ['left', 'right']:
|
||||||
self._updateWidth()
|
self._updateWidth()
|
||||||
else:
|
else:
|
||||||
self._updateHeight()
|
self._updateHeight()
|
||||||
|
|
||||||
def setHeight(self, h=None):
|
def setHeight(self, h=None):
|
||||||
"""Set the height of this axis reserved for ticks and tick labels.
|
"""Set the height of this axis reserved for ticks and tick labels.
|
||||||
The height of the axis label is automatically added.
|
The height of the axis label is automatically added.
|
||||||
|
|
||||||
If *height* is None, then the value will be determined automatically
|
If *height* is None, then the value will be determined automatically
|
||||||
based on the size of the tick text."""
|
based on the size of the tick text."""
|
||||||
self.fixedHeight = h
|
self.fixedHeight = h
|
||||||
self._updateHeight()
|
self._updateHeight()
|
||||||
|
|
||||||
def _updateHeight(self):
|
def _updateHeight(self):
|
||||||
if not self.isVisible():
|
if not self.isVisible():
|
||||||
h = 0
|
h = 0
|
||||||
|
@ -338,20 +349,20 @@ class AxisItem(GraphicsWidget):
|
||||||
h += self.label.boundingRect().height() * 0.8
|
h += self.label.boundingRect().height() * 0.8
|
||||||
else:
|
else:
|
||||||
h = self.fixedHeight
|
h = self.fixedHeight
|
||||||
|
|
||||||
self.setMaximumHeight(h)
|
self.setMaximumHeight(h)
|
||||||
self.setMinimumHeight(h)
|
self.setMinimumHeight(h)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
|
||||||
def setWidth(self, w=None):
|
def setWidth(self, w=None):
|
||||||
"""Set the width of this axis reserved for ticks and tick labels.
|
"""Set the width of this axis reserved for ticks and tick labels.
|
||||||
The width of the axis label is automatically added.
|
The width of the axis label is automatically added.
|
||||||
|
|
||||||
If *width* is None, then the value will be determined automatically
|
If *width* is None, then the value will be determined automatically
|
||||||
based on the size of the tick text."""
|
based on the size of the tick text."""
|
||||||
self.fixedWidth = w
|
self.fixedWidth = w
|
||||||
self._updateWidth()
|
self._updateWidth()
|
||||||
|
|
||||||
def _updateWidth(self):
|
def _updateWidth(self):
|
||||||
if not self.isVisible():
|
if not self.isVisible():
|
||||||
w = 0
|
w = 0
|
||||||
|
@ -369,20 +380,20 @@ class AxisItem(GraphicsWidget):
|
||||||
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
|
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
|
||||||
else:
|
else:
|
||||||
w = self.fixedWidth
|
w = self.fixedWidth
|
||||||
|
|
||||||
self.setMaximumWidth(w)
|
self.setMaximumWidth(w)
|
||||||
self.setMinimumWidth(w)
|
self.setMinimumWidth(w)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
|
||||||
def pen(self):
|
def pen(self):
|
||||||
if self._pen is None:
|
if self._pen is None:
|
||||||
return fn.mkPen(getConfigOption('foreground'))
|
return fn.mkPen(getConfigOption('foreground'))
|
||||||
return fn.mkPen(self._pen)
|
return fn.mkPen(self._pen)
|
||||||
|
|
||||||
def setPen(self, *args, **kwargs):
|
def setPen(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Set the pen used for drawing text, axes, ticks, and grid lines.
|
Set the pen used for drawing text, axes, ticks, and grid lines.
|
||||||
If no arguments are given, the default foreground color will be used
|
If no arguments are given, the default foreground color will be used
|
||||||
(see :func:`setConfigOption <pyqtgraph.setConfigOption>`).
|
(see :func:`setConfigOption <pyqtgraph.setConfigOption>`).
|
||||||
"""
|
"""
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
@ -393,44 +404,44 @@ class AxisItem(GraphicsWidget):
|
||||||
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
||||||
self.setLabel()
|
self.setLabel()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setScale(self, scale=None):
|
def setScale(self, scale=None):
|
||||||
"""
|
"""
|
||||||
Set the value scaling for this axis.
|
Set the value scaling for this axis.
|
||||||
|
|
||||||
Setting this value causes the axis to draw ticks and tick labels as if
|
Setting this value causes the axis to draw ticks and tick labels as if
|
||||||
the view coordinate system were scaled. By default, the axis scaling is
|
the view coordinate system were scaled. By default, the axis scaling is
|
||||||
1.0.
|
1.0.
|
||||||
"""
|
"""
|
||||||
# Deprecated usage, kept for backward compatibility
|
# Deprecated usage, kept for backward compatibility
|
||||||
if scale is None:
|
if scale is None:
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
self.enableAutoSIPrefix(True)
|
self.enableAutoSIPrefix(True)
|
||||||
|
|
||||||
if scale != self.scale:
|
if scale != self.scale:
|
||||||
self.scale = scale
|
self.scale = scale
|
||||||
self.setLabel()
|
self.setLabel()
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def enableAutoSIPrefix(self, enable=True):
|
def enableAutoSIPrefix(self, enable=True):
|
||||||
"""
|
"""
|
||||||
Enable (or disable) automatic SI prefix scaling on this axis.
|
Enable (or disable) automatic SI prefix scaling on this axis.
|
||||||
|
|
||||||
When enabled, this feature automatically determines the best SI prefix
|
When enabled, this feature automatically determines the best SI prefix
|
||||||
to prepend to the label units, while ensuring that axis values are scaled
|
to prepend to the label units, while ensuring that axis values are scaled
|
||||||
accordingly.
|
accordingly.
|
||||||
|
|
||||||
For example, if the axis spans values from -0.1 to 0.1 and has units set
|
For example, if the axis spans values from -0.1 to 0.1 and has units set
|
||||||
to 'V' then the axis would display values -100 to 100
|
to 'V' then the axis would display values -100 to 100
|
||||||
and the units would appear as 'mV'
|
and the units would appear as 'mV'
|
||||||
|
|
||||||
This feature is enabled by default, and is only available when a suffix
|
This feature is enabled by default, and is only available when a suffix
|
||||||
(unit string) is provided to display on the label.
|
(unit string) is provided to display on the label.
|
||||||
"""
|
"""
|
||||||
self.autoSIPrefix = enable
|
self.autoSIPrefix = enable
|
||||||
self.updateAutoSIPrefix()
|
self.updateAutoSIPrefix()
|
||||||
|
|
||||||
def updateAutoSIPrefix(self):
|
def updateAutoSIPrefix(self):
|
||||||
if self.label.isVisible():
|
if self.label.isVisible():
|
||||||
(scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale)))
|
(scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale)))
|
||||||
|
@ -440,12 +451,12 @@ class AxisItem(GraphicsWidget):
|
||||||
self.setLabel(unitPrefix=prefix)
|
self.setLabel(unitPrefix=prefix)
|
||||||
else:
|
else:
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
|
|
||||||
self.autoSIPrefixScale = scale
|
self.autoSIPrefixScale = scale
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
def setRange(self, mn, mx):
|
def setRange(self, mn, mx):
|
||||||
"""Set the range of values displayed by the axis.
|
"""Set the range of values displayed by the axis.
|
||||||
Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView <pyqtgraph.AxisItem.linkToView>`"""
|
Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView <pyqtgraph.AxisItem.linkToView>`"""
|
||||||
|
@ -456,14 +467,14 @@ class AxisItem(GraphicsWidget):
|
||||||
self.updateAutoSIPrefix()
|
self.updateAutoSIPrefix()
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def linkedView(self):
|
def linkedView(self):
|
||||||
"""Return the ViewBox this axis is linked to"""
|
"""Return the ViewBox this axis is linked to"""
|
||||||
if self._linkedView is None:
|
if self._linkedView is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return self._linkedView()
|
return self._linkedView()
|
||||||
|
|
||||||
def linkToView(self, view):
|
def linkToView(self, view):
|
||||||
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
|
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
|
||||||
oldView = self.linkedView()
|
oldView = self.linkedView()
|
||||||
|
@ -476,11 +487,11 @@ class AxisItem(GraphicsWidget):
|
||||||
if oldView is not None:
|
if oldView is not None:
|
||||||
oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
|
oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
|
||||||
view.sigXRangeChanged.connect(self.linkedViewChanged)
|
view.sigXRangeChanged.connect(self.linkedViewChanged)
|
||||||
|
|
||||||
if oldView is not None:
|
if oldView is not None:
|
||||||
oldView.sigResized.disconnect(self.linkedViewChanged)
|
oldView.sigResized.disconnect(self.linkedViewChanged)
|
||||||
view.sigResized.connect(self.linkedViewChanged)
|
view.sigResized.connect(self.linkedViewChanged)
|
||||||
|
|
||||||
def linkedViewChanged(self, view, newRange=None):
|
def linkedViewChanged(self, view, newRange=None):
|
||||||
if self.orientation in ['right', 'left']:
|
if self.orientation in ['right', 'left']:
|
||||||
if newRange is None:
|
if newRange is None:
|
||||||
|
@ -496,7 +507,7 @@ class AxisItem(GraphicsWidget):
|
||||||
self.setRange(*newRange[::-1])
|
self.setRange(*newRange[::-1])
|
||||||
else:
|
else:
|
||||||
self.setRange(*newRange)
|
self.setRange(*newRange)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
linkedView = self.linkedView()
|
linkedView = self.linkedView()
|
||||||
if linkedView is None or self.grid is False:
|
if linkedView is None or self.grid is False:
|
||||||
|
@ -515,7 +526,7 @@ class AxisItem(GraphicsWidget):
|
||||||
return rect
|
return rect
|
||||||
else:
|
else:
|
||||||
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
|
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
|
||||||
|
|
||||||
def paint(self, p, opt, widget):
|
def paint(self, p, opt, widget):
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
if self.picture is None:
|
if self.picture is None:
|
||||||
|
@ -544,26 +555,26 @@ class AxisItem(GraphicsWidget):
|
||||||
[ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ],
|
[ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ],
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
|
||||||
If *ticks* is None, then the default tick system will be used instead.
|
If *ticks* is None, then the default tick system will be used instead.
|
||||||
"""
|
"""
|
||||||
self._tickLevels = ticks
|
self._tickLevels = ticks
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setTickSpacing(self, major=None, minor=None, levels=None):
|
def setTickSpacing(self, major=None, minor=None, levels=None):
|
||||||
"""
|
"""
|
||||||
Explicitly determine the spacing of major and minor ticks. This
|
Explicitly determine the spacing of major and minor ticks. This
|
||||||
overrides the default behavior of the tickSpacing method, and disables
|
overrides the default behavior of the tickSpacing method, and disables
|
||||||
the effect of setTicks(). Arguments may be either *major* and *minor*,
|
the effect of setTicks(). Arguments may be either *major* and *minor*,
|
||||||
or *levels* which is a list of (spacing, offset) tuples for each
|
or *levels* which is a list of (spacing, offset) tuples for each
|
||||||
tick level desired.
|
tick level desired.
|
||||||
|
|
||||||
If no arguments are given, then the default behavior of tickSpacing
|
If no arguments are given, then the default behavior of tickSpacing
|
||||||
is enabled.
|
is enabled.
|
||||||
|
|
||||||
Examples::
|
Examples::
|
||||||
|
|
||||||
# two levels, all offsets = 0
|
# two levels, all offsets = 0
|
||||||
axis.setTickSpacing(5, 1)
|
axis.setTickSpacing(5, 1)
|
||||||
# three levels, all offsets = 0
|
# three levels, all offsets = 0
|
||||||
|
@ -571,7 +582,7 @@ class AxisItem(GraphicsWidget):
|
||||||
# reset to default
|
# reset to default
|
||||||
axis.setTickSpacing()
|
axis.setTickSpacing()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if levels is None:
|
if levels is None:
|
||||||
if major is None:
|
if major is None:
|
||||||
levels = None
|
levels = None
|
||||||
|
@ -580,16 +591,16 @@ class AxisItem(GraphicsWidget):
|
||||||
self._tickSpacing = levels
|
self._tickSpacing = levels
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
def tickSpacing(self, minVal, maxVal, size):
|
def tickSpacing(self, minVal, maxVal, size):
|
||||||
"""Return values describing the desired spacing and offset of ticks.
|
"""Return values describing the desired spacing and offset of ticks.
|
||||||
|
|
||||||
This method is called whenever the axis needs to be redrawn and is a
|
This method is called whenever the axis needs to be redrawn and is a
|
||||||
good method to override in subclasses that require control over tick locations.
|
good method to override in subclasses that require control over tick locations.
|
||||||
|
|
||||||
The return value must be a list of tuples, one for each set of ticks::
|
The return value must be a list of tuples, one for each set of ticks::
|
||||||
|
|
||||||
[
|
[
|
||||||
(major tick spacing, offset),
|
(major tick spacing, offset),
|
||||||
(minor tick spacing, offset),
|
(minor tick spacing, offset),
|
||||||
|
@ -600,41 +611,40 @@ class AxisItem(GraphicsWidget):
|
||||||
# First check for override tick spacing
|
# First check for override tick spacing
|
||||||
if self._tickSpacing is not None:
|
if self._tickSpacing is not None:
|
||||||
return self._tickSpacing
|
return self._tickSpacing
|
||||||
|
|
||||||
dif = abs(maxVal - minVal)
|
dif = abs(maxVal - minVal)
|
||||||
if dif == 0:
|
if dif == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
## decide optimal minor tick spacing in pixels (this is just aesthetics)
|
||||||
optimalTickCount = max(2., np.log(size))
|
optimalTickCount = max(2., np.log(size))
|
||||||
|
|
||||||
## optimal minor tick spacing
|
## optimal minor tick spacing
|
||||||
optimalSpacing = dif / optimalTickCount
|
optimalSpacing = dif / optimalTickCount
|
||||||
|
|
||||||
## the largest power-of-10 spacing which is smaller than optimal
|
## the largest power-of-10 spacing which is smaller than optimal
|
||||||
p10unit = 10 ** np.floor(np.log10(optimalSpacing))
|
p10unit = 10 ** np.floor(np.log10(optimalSpacing))
|
||||||
|
|
||||||
## Determine major/minor tick spacings which flank the optimal spacing.
|
## Determine major/minor tick spacings which flank the optimal spacing.
|
||||||
intervals = np.array([1., 2., 10., 20., 100.]) * p10unit
|
intervals = np.array([1., 2., 10., 20., 100.]) * p10unit
|
||||||
minorIndex = 0
|
minorIndex = 0
|
||||||
while intervals[minorIndex+1] <= optimalSpacing:
|
while intervals[minorIndex+1] <= optimalSpacing:
|
||||||
minorIndex += 1
|
minorIndex += 1
|
||||||
|
|
||||||
levels = [
|
levels = [
|
||||||
(intervals[minorIndex+2], 0),
|
(intervals[minorIndex+2], 0),
|
||||||
(intervals[minorIndex+1], 0),
|
(intervals[minorIndex+1], 0),
|
||||||
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
|
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.style['maxTickLevel'] >= 2:
|
if self.style['maxTickLevel'] >= 2:
|
||||||
## decide whether to include the last level of ticks
|
## decide whether to include the last level of ticks
|
||||||
minSpacing = min(size / 20., 30.)
|
minSpacing = min(size / 20., 30.)
|
||||||
maxTickCount = size / minSpacing
|
maxTickCount = size / minSpacing
|
||||||
if dif / intervals[minorIndex] <= maxTickCount:
|
if dif / intervals[minorIndex] <= maxTickCount:
|
||||||
levels.append((intervals[minorIndex], 0))
|
levels.append((intervals[minorIndex], 0))
|
||||||
return levels
|
|
||||||
|
return levels
|
||||||
|
|
||||||
|
|
||||||
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
|
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
|
||||||
### Determine major/minor tick spacings which flank the optimal spacing.
|
### Determine major/minor tick spacings which flank the optimal spacing.
|
||||||
|
@ -642,7 +652,7 @@ class AxisItem(GraphicsWidget):
|
||||||
#minorIndex = 0
|
#minorIndex = 0
|
||||||
#while intervals[minorIndex+1] <= optimalSpacing:
|
#while intervals[minorIndex+1] <= optimalSpacing:
|
||||||
#minorIndex += 1
|
#minorIndex += 1
|
||||||
|
|
||||||
### make sure we never see 5 and 2 at the same time
|
### make sure we never see 5 and 2 at the same time
|
||||||
#intIndexes = [
|
#intIndexes = [
|
||||||
#[0,1,3],
|
#[0,1,3],
|
||||||
|
@ -651,42 +661,42 @@ class AxisItem(GraphicsWidget):
|
||||||
#[3,4,6],
|
#[3,4,6],
|
||||||
#[3,5,6],
|
#[3,5,6],
|
||||||
#][minorIndex]
|
#][minorIndex]
|
||||||
|
|
||||||
#return [
|
#return [
|
||||||
#(intervals[intIndexes[2]], 0),
|
#(intervals[intIndexes[2]], 0),
|
||||||
#(intervals[intIndexes[1]], 0),
|
#(intervals[intIndexes[1]], 0),
|
||||||
#(intervals[intIndexes[0]], 0)
|
#(intervals[intIndexes[0]], 0)
|
||||||
#]
|
#]
|
||||||
|
|
||||||
def tickValues(self, minVal, maxVal, size):
|
def tickValues(self, minVal, maxVal, size):
|
||||||
"""
|
"""
|
||||||
Return the values and spacing of ticks to draw::
|
Return the values and spacing of ticks to draw::
|
||||||
|
|
||||||
[
|
[
|
||||||
(spacing, [major ticks]),
|
(spacing, [major ticks]),
|
||||||
(spacing, [minor ticks]),
|
(spacing, [minor ticks]),
|
||||||
...
|
...
|
||||||
]
|
]
|
||||||
|
|
||||||
By default, this method calls tickSpacing to determine the correct tick locations.
|
By default, this method calls tickSpacing to determine the correct tick locations.
|
||||||
This is a good method to override in subclasses.
|
This is a good method to override in subclasses.
|
||||||
"""
|
"""
|
||||||
minVal, maxVal = sorted((minVal, maxVal))
|
minVal, maxVal = sorted((minVal, maxVal))
|
||||||
|
|
||||||
|
|
||||||
minVal *= self.scale
|
|
||||||
|
minVal *= self.scale
|
||||||
maxVal *= self.scale
|
maxVal *= self.scale
|
||||||
#size *= self.scale
|
#size *= self.scale
|
||||||
|
|
||||||
ticks = []
|
ticks = []
|
||||||
tickLevels = self.tickSpacing(minVal, maxVal, size)
|
tickLevels = self.tickSpacing(minVal, maxVal, size)
|
||||||
allValues = np.array([])
|
allValues = np.array([])
|
||||||
for i in range(len(tickLevels)):
|
for i in range(len(tickLevels)):
|
||||||
spacing, offset = tickLevels[i]
|
spacing, offset = tickLevels[i]
|
||||||
|
|
||||||
## determine starting tick
|
## determine starting tick
|
||||||
start = (np.ceil((minVal-offset) / spacing) * spacing) + offset
|
start = (np.ceil((minVal-offset) / spacing) * spacing) + offset
|
||||||
|
|
||||||
## determine number of ticks
|
## determine number of ticks
|
||||||
num = int((maxVal-start) / spacing) + 1
|
num = int((maxVal-start) / spacing) + 1
|
||||||
values = (np.arange(num) * spacing + start) / self.scale
|
values = (np.arange(num) * spacing + start) / self.scale
|
||||||
|
@ -696,11 +706,11 @@ class AxisItem(GraphicsWidget):
|
||||||
values = list(filter(lambda x: all(np.abs(allValues-x) > spacing/self.scale*0.01), values))
|
values = list(filter(lambda x: all(np.abs(allValues-x) > spacing/self.scale*0.01), values))
|
||||||
allValues = np.concatenate([allValues, values])
|
allValues = np.concatenate([allValues, values])
|
||||||
ticks.append((spacing/self.scale, values))
|
ticks.append((spacing/self.scale, values))
|
||||||
|
|
||||||
if self.logMode:
|
if self.logMode:
|
||||||
return self.logTickValues(minVal, maxVal, size, ticks)
|
return self.logTickValues(minVal, maxVal, size, ticks)
|
||||||
|
|
||||||
|
|
||||||
#nticks = []
|
#nticks = []
|
||||||
#for t in ticks:
|
#for t in ticks:
|
||||||
#nvals = []
|
#nvals = []
|
||||||
|
@ -708,24 +718,24 @@ class AxisItem(GraphicsWidget):
|
||||||
#nvals.append(v/self.scale)
|
#nvals.append(v/self.scale)
|
||||||
#nticks.append((t[0]/self.scale,nvals))
|
#nticks.append((t[0]/self.scale,nvals))
|
||||||
#ticks = nticks
|
#ticks = nticks
|
||||||
|
|
||||||
return ticks
|
return ticks
|
||||||
|
|
||||||
def logTickValues(self, minVal, maxVal, size, stdTicks):
|
def logTickValues(self, minVal, maxVal, size, stdTicks):
|
||||||
|
|
||||||
## start with the tick spacing given by tickValues().
|
## start with the tick spacing given by tickValues().
|
||||||
## Any level whose spacing is < 1 needs to be converted to log scale
|
## Any level whose spacing is < 1 needs to be converted to log scale
|
||||||
|
|
||||||
ticks = []
|
ticks = []
|
||||||
for (spacing, t) in stdTicks:
|
for (spacing, t) in stdTicks:
|
||||||
if spacing >= 1.0:
|
if spacing >= 1.0:
|
||||||
ticks.append((spacing, t))
|
ticks.append((spacing, t))
|
||||||
|
|
||||||
if len(ticks) < 3:
|
if len(ticks) < 3:
|
||||||
v1 = int(np.floor(minVal))
|
v1 = int(np.floor(minVal))
|
||||||
v2 = int(np.ceil(maxVal))
|
v2 = int(np.ceil(maxVal))
|
||||||
#major = list(range(v1+1, v2))
|
#major = list(range(v1+1, v2))
|
||||||
|
|
||||||
minor = []
|
minor = []
|
||||||
for v in range(v1, v2):
|
for v in range(v1, v2):
|
||||||
minor.extend(v + np.log10(np.arange(1, 10)))
|
minor.extend(v + np.log10(np.arange(1, 10)))
|
||||||
|
@ -734,21 +744,21 @@ class AxisItem(GraphicsWidget):
|
||||||
return ticks
|
return ticks
|
||||||
|
|
||||||
def tickStrings(self, values, scale, spacing):
|
def tickStrings(self, values, scale, spacing):
|
||||||
"""Return the strings that should be placed next to ticks. This method is called
|
"""Return the strings that should be placed next to ticks. This method is called
|
||||||
when redrawing the axis and is a good method to override in subclasses.
|
when redrawing the axis and is a good method to override in subclasses.
|
||||||
The method is called with a list of tick values, a scaling factor (see below), and the
|
The method is called with a list of tick values, a scaling factor (see below), and the
|
||||||
spacing between ticks (this is required since, in some instances, there may be only
|
spacing between ticks (this is required since, in some instances, there may be only
|
||||||
one tick and thus no other way to determine the tick spacing)
|
one tick and thus no other way to determine the tick spacing)
|
||||||
|
|
||||||
The scale argument is used when the axis label is displaying units which may have an SI scaling prefix.
|
The scale argument is used when the axis label is displaying units which may have an SI scaling prefix.
|
||||||
When determining the text to display, use value*scale to correctly account for this prefix.
|
When determining the text to display, use value*scale to correctly account for this prefix.
|
||||||
For example, if the axis label's units are set to 'V', then a tick value of 0.001 might
|
For example, if the axis label's units are set to 'V', then a tick value of 0.001 might
|
||||||
be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and
|
be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and
|
||||||
thus the tick should display 0.001 * 1000 = 1.
|
thus the tick should display 0.001 * 1000 = 1.
|
||||||
"""
|
"""
|
||||||
if self.logMode:
|
if self.logMode:
|
||||||
return self.logTickStrings(values, scale, spacing)
|
return self.logTickStrings(values, scale, spacing)
|
||||||
|
|
||||||
places = max(0, np.ceil(-np.log10(spacing*scale)))
|
places = max(0, np.ceil(-np.log10(spacing*scale)))
|
||||||
strings = []
|
strings = []
|
||||||
for v in values:
|
for v in values:
|
||||||
|
@ -759,27 +769,27 @@ class AxisItem(GraphicsWidget):
|
||||||
vstr = ("%%0.%df" % places) % vs
|
vstr = ("%%0.%df" % places) % vs
|
||||||
strings.append(vstr)
|
strings.append(vstr)
|
||||||
return strings
|
return strings
|
||||||
|
|
||||||
def logTickStrings(self, values, scale, spacing):
|
def logTickStrings(self, values, scale, spacing):
|
||||||
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
|
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
|
||||||
|
|
||||||
def generateDrawSpecs(self, p):
|
def generateDrawSpecs(self, p):
|
||||||
"""
|
"""
|
||||||
Calls tickValues() and tickStrings() to determine where and how ticks should
|
Calls tickValues() and tickStrings() to determine where and how ticks should
|
||||||
be drawn, then generates from this a set of drawing commands to be
|
be drawn, then generates from this a set of drawing commands to be
|
||||||
interpreted by drawPicture().
|
interpreted by drawPicture().
|
||||||
"""
|
"""
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
|
|
||||||
#bounds = self.boundingRect()
|
#bounds = self.boundingRect()
|
||||||
bounds = self.mapRectFromParent(self.geometry())
|
bounds = self.mapRectFromParent(self.geometry())
|
||||||
|
|
||||||
linkedView = self.linkedView()
|
linkedView = self.linkedView()
|
||||||
if linkedView is None or self.grid is False:
|
if linkedView is None or self.grid is False:
|
||||||
tickBounds = bounds
|
tickBounds = bounds
|
||||||
else:
|
else:
|
||||||
tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
|
tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
|
||||||
|
|
||||||
if self.orientation == 'left':
|
if self.orientation == 'left':
|
||||||
span = (bounds.topRight(), bounds.bottomRight())
|
span = (bounds.topRight(), bounds.bottomRight())
|
||||||
tickStart = tickBounds.right()
|
tickStart = tickBounds.right()
|
||||||
|
@ -805,7 +815,7 @@ class AxisItem(GraphicsWidget):
|
||||||
tickDir = 1
|
tickDir = 1
|
||||||
axis = 1
|
axis = 1
|
||||||
#print tickStart, tickStop, span
|
#print tickStart, tickStop, span
|
||||||
|
|
||||||
## determine size of this item in pixels
|
## determine size of this item in pixels
|
||||||
points = list(map(self.mapToDevice, span))
|
points = list(map(self.mapToDevice, span))
|
||||||
if None in points:
|
if None in points:
|
||||||
|
@ -830,7 +840,7 @@ class AxisItem(GraphicsWidget):
|
||||||
for val, strn in level:
|
for val, strn in level:
|
||||||
values.append(val)
|
values.append(val)
|
||||||
strings.append(strn)
|
strings.append(strn)
|
||||||
|
|
||||||
## determine mapping between tick values and local coordinates
|
## determine mapping between tick values and local coordinates
|
||||||
dif = self.range[1] - self.range[0]
|
dif = self.range[1] - self.range[0]
|
||||||
if dif == 0:
|
if dif == 0:
|
||||||
|
@ -843,29 +853,29 @@ class AxisItem(GraphicsWidget):
|
||||||
else:
|
else:
|
||||||
xScale = bounds.width() / dif
|
xScale = bounds.width() / dif
|
||||||
offset = self.range[0] * xScale
|
offset = self.range[0] * xScale
|
||||||
|
|
||||||
xRange = [x * xScale - offset for x in self.range]
|
xRange = [x * xScale - offset for x in self.range]
|
||||||
xMin = min(xRange)
|
xMin = min(xRange)
|
||||||
xMax = max(xRange)
|
xMax = max(xRange)
|
||||||
|
|
||||||
profiler('init')
|
profiler('init')
|
||||||
|
|
||||||
tickPositions = [] # remembers positions of previously drawn ticks
|
tickPositions = [] # remembers positions of previously drawn ticks
|
||||||
|
|
||||||
## compute coordinates to draw ticks
|
## compute coordinates to draw ticks
|
||||||
## draw three different intervals, long ticks first
|
## draw three different intervals, long ticks first
|
||||||
tickSpecs = []
|
tickSpecs = []
|
||||||
for i in range(len(tickLevels)):
|
for i in range(len(tickLevels)):
|
||||||
tickPositions.append([])
|
tickPositions.append([])
|
||||||
ticks = tickLevels[i][1]
|
ticks = tickLevels[i][1]
|
||||||
|
|
||||||
## length of tick
|
## length of tick
|
||||||
tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
|
tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
|
||||||
|
|
||||||
lineAlpha = 255 / (i+1)
|
lineAlpha = 255 / (i+1)
|
||||||
if self.grid is not False:
|
if self.grid is not False:
|
||||||
lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.)
|
lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.)
|
||||||
|
|
||||||
for v in ticks:
|
for v in ticks:
|
||||||
## determine actual position to draw this tick
|
## determine actual position to draw this tick
|
||||||
x = (v * xScale) - offset
|
x = (v * xScale) - offset
|
||||||
|
@ -873,7 +883,7 @@ class AxisItem(GraphicsWidget):
|
||||||
tickPositions[i].append(None)
|
tickPositions[i].append(None)
|
||||||
continue
|
continue
|
||||||
tickPositions[i].append(x)
|
tickPositions[i].append(x)
|
||||||
|
|
||||||
p1 = [x, x]
|
p1 = [x, x]
|
||||||
p2 = [x, x]
|
p2 = [x, x]
|
||||||
p1[axis] = tickStart
|
p1[axis] = tickStart
|
||||||
|
@ -887,22 +897,26 @@ class AxisItem(GraphicsWidget):
|
||||||
tickSpecs.append((tickPen, Point(p1), Point(p2)))
|
tickSpecs.append((tickPen, Point(p1), Point(p2)))
|
||||||
profiler('compute ticks')
|
profiler('compute ticks')
|
||||||
|
|
||||||
|
|
||||||
if self.style['stopAxisAtTick'][0] is True:
|
if self.style['stopAxisAtTick'][0] is True:
|
||||||
stop = max(span[0].y(), min(map(min, tickPositions)))
|
minTickPosition = min(map(min, tickPositions))
|
||||||
if axis == 0:
|
if axis == 0:
|
||||||
|
stop = max(span[0].y(), minTickPosition)
|
||||||
span[0].setY(stop)
|
span[0].setY(stop)
|
||||||
else:
|
else:
|
||||||
|
stop = max(span[0].x(), minTickPosition)
|
||||||
span[0].setX(stop)
|
span[0].setX(stop)
|
||||||
if self.style['stopAxisAtTick'][1] is True:
|
if self.style['stopAxisAtTick'][1] is True:
|
||||||
stop = min(span[1].y(), max(map(max, tickPositions)))
|
maxTickPosition = max(map(max, tickPositions))
|
||||||
if axis == 0:
|
if axis == 0:
|
||||||
|
stop = min(span[1].y(), maxTickPosition)
|
||||||
span[1].setY(stop)
|
span[1].setY(stop)
|
||||||
else:
|
else:
|
||||||
|
stop = min(span[1].x(), maxTickPosition)
|
||||||
span[1].setX(stop)
|
span[1].setX(stop)
|
||||||
axisSpec = (self.pen(), span[0], span[1])
|
axisSpec = (self.pen(), span[0], span[1])
|
||||||
|
|
||||||
|
|
||||||
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
|
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
|
||||||
#if self.style['autoExpandTextSpace'] is True:
|
#if self.style['autoExpandTextSpace'] is True:
|
||||||
#textWidth = self.textWidth
|
#textWidth = self.textWidth
|
||||||
|
@ -910,15 +924,15 @@ class AxisItem(GraphicsWidget):
|
||||||
#else:
|
#else:
|
||||||
#textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text
|
#textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text
|
||||||
#textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text
|
#textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text
|
||||||
|
|
||||||
textSize2 = 0
|
textSize2 = 0
|
||||||
textRects = []
|
textRects = []
|
||||||
textSpecs = [] ## list of draw
|
textSpecs = [] ## list of draw
|
||||||
|
|
||||||
# If values are hidden, return early
|
# If values are hidden, return early
|
||||||
if not self.style['showValues']:
|
if not self.style['showValues']:
|
||||||
return (axisSpec, tickSpecs, textSpecs)
|
return (axisSpec, tickSpecs, textSpecs)
|
||||||
|
|
||||||
for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
|
for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
|
||||||
## Get the list of strings to display for this level
|
## Get the list of strings to display for this level
|
||||||
if tickStrings is None:
|
if tickStrings is None:
|
||||||
|
@ -926,10 +940,10 @@ class AxisItem(GraphicsWidget):
|
||||||
strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing)
|
strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing)
|
||||||
else:
|
else:
|
||||||
strings = tickStrings[i]
|
strings = tickStrings[i]
|
||||||
|
|
||||||
if len(strings) == 0:
|
if len(strings) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
## ignore strings belonging to ticks that were previously ignored
|
## ignore strings belonging to ticks that were previously ignored
|
||||||
for j in range(len(strings)):
|
for j in range(len(strings)):
|
||||||
if tickPositions[i][j] is None:
|
if tickPositions[i][j] is None:
|
||||||
|
@ -945,10 +959,10 @@ class AxisItem(GraphicsWidget):
|
||||||
## boundingRect is usually just a bit too large
|
## boundingRect is usually just a bit too large
|
||||||
## (but this probably depends on per-font metrics?)
|
## (but this probably depends on per-font metrics?)
|
||||||
br.setHeight(br.height() * 0.8)
|
br.setHeight(br.height() * 0.8)
|
||||||
|
|
||||||
rects.append(br)
|
rects.append(br)
|
||||||
textRects.append(rects[-1])
|
textRects.append(rects[-1])
|
||||||
|
|
||||||
if len(textRects) > 0:
|
if len(textRects) > 0:
|
||||||
## measure all text, make sure there's enough room
|
## measure all text, make sure there's enough room
|
||||||
if axis == 0:
|
if axis == 0:
|
||||||
|
@ -973,7 +987,7 @@ class AxisItem(GraphicsWidget):
|
||||||
break
|
break
|
||||||
if finished:
|
if finished:
|
||||||
break
|
break
|
||||||
|
|
||||||
#spacing, values = tickLevels[best]
|
#spacing, values = tickLevels[best]
|
||||||
#strings = self.tickStrings(values, self.scale, spacing)
|
#strings = self.tickStrings(values, self.scale, spacing)
|
||||||
# Determine exactly where tick text should be drawn
|
# Determine exactly where tick text should be drawn
|
||||||
|
@ -1006,24 +1020,24 @@ class AxisItem(GraphicsWidget):
|
||||||
#p.drawText(rect, textFlags, vstr)
|
#p.drawText(rect, textFlags, vstr)
|
||||||
textSpecs.append((rect, textFlags, vstr))
|
textSpecs.append((rect, textFlags, vstr))
|
||||||
profiler('compute text')
|
profiler('compute text')
|
||||||
|
|
||||||
## update max text size if needed.
|
## update max text size if needed.
|
||||||
self._updateMaxTextSize(textSize2)
|
self._updateMaxTextSize(textSize2)
|
||||||
|
|
||||||
return (axisSpec, tickSpecs, textSpecs)
|
return (axisSpec, tickSpecs, textSpecs)
|
||||||
|
|
||||||
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
|
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
|
|
||||||
p.setRenderHint(p.Antialiasing, False)
|
p.setRenderHint(p.Antialiasing, False)
|
||||||
p.setRenderHint(p.TextAntialiasing, True)
|
p.setRenderHint(p.TextAntialiasing, True)
|
||||||
|
|
||||||
## draw long line along axis
|
## draw long line along axis
|
||||||
pen, p1, p2 = axisSpec
|
pen, p1, p2 = axisSpec
|
||||||
p.setPen(pen)
|
p.setPen(pen)
|
||||||
p.drawLine(p1, p2)
|
p.drawLine(p1, p2)
|
||||||
p.translate(0.5,0) ## resolves some damn pixel ambiguity
|
p.translate(0.5,0) ## resolves some damn pixel ambiguity
|
||||||
|
|
||||||
## draw ticks
|
## draw ticks
|
||||||
for pen, p1, p2 in tickSpecs:
|
for pen, p1, p2 in tickSpecs:
|
||||||
p.setPen(pen)
|
p.setPen(pen)
|
||||||
|
@ -1045,7 +1059,7 @@ class AxisItem(GraphicsWidget):
|
||||||
self._updateWidth()
|
self._updateWidth()
|
||||||
else:
|
else:
|
||||||
self._updateHeight()
|
self._updateHeight()
|
||||||
|
|
||||||
def hide(self):
|
def hide(self):
|
||||||
GraphicsWidget.hide(self)
|
GraphicsWidget.hide(self)
|
||||||
if self.orientation in ['left', 'right']:
|
if self.orientation in ['left', 'right']:
|
||||||
|
@ -1054,23 +1068,23 @@ class AxisItem(GraphicsWidget):
|
||||||
self._updateHeight()
|
self._updateHeight()
|
||||||
|
|
||||||
def wheelEvent(self, ev):
|
def wheelEvent(self, ev):
|
||||||
if self.linkedView() is None:
|
if self.linkedView() is None:
|
||||||
return
|
return
|
||||||
if self.orientation in ['left', 'right']:
|
if self.orientation in ['left', 'right']:
|
||||||
self.linkedView().wheelEvent(ev, axis=1)
|
self.linkedView().wheelEvent(ev, axis=1)
|
||||||
else:
|
else:
|
||||||
self.linkedView().wheelEvent(ev, axis=0)
|
self.linkedView().wheelEvent(ev, axis=0)
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
|
||||||
def mouseDragEvent(self, event):
|
def mouseDragEvent(self, event):
|
||||||
if self.linkedView() is None:
|
if self.linkedView() is None:
|
||||||
return
|
return
|
||||||
if self.orientation in ['left', 'right']:
|
if self.orientation in ['left', 'right']:
|
||||||
return self.linkedView().mouseDragEvent(event, axis=1)
|
return self.linkedView().mouseDragEvent(event, axis=1)
|
||||||
else:
|
else:
|
||||||
return self.linkedView().mouseDragEvent(event, axis=0)
|
return self.linkedView().mouseDragEvent(event, axis=0)
|
||||||
|
|
||||||
def mouseClickEvent(self, event):
|
def mouseClickEvent(self, event):
|
||||||
if self.linkedView() is None:
|
if self.linkedView() is None:
|
||||||
return
|
return
|
||||||
return self.linkedView().mouseClickEvent(event)
|
return self.linkedView().mouseClickEvent(event)
|
||||||
|
|
|
@ -23,6 +23,7 @@ class ErrorBarItem(GraphicsObject):
|
||||||
beam=None,
|
beam=None,
|
||||||
pen=None
|
pen=None
|
||||||
)
|
)
|
||||||
|
self.setVisible(False)
|
||||||
self.setData(**opts)
|
self.setData(**opts)
|
||||||
|
|
||||||
def setData(self, **opts):
|
def setData(self, **opts):
|
||||||
|
@ -45,6 +46,7 @@ class ErrorBarItem(GraphicsObject):
|
||||||
This method was added in version 0.9.9. For prior versions, use setOpts.
|
This method was added in version 0.9.9. For prior versions, use setOpts.
|
||||||
"""
|
"""
|
||||||
self.opts.update(opts)
|
self.opts.update(opts)
|
||||||
|
self.setVisible(all(self.opts[ax] is not None for ax in ['x', 'y']))
|
||||||
self.path = None
|
self.path = None
|
||||||
self.update()
|
self.update()
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
|
@ -59,6 +61,7 @@ class ErrorBarItem(GraphicsObject):
|
||||||
|
|
||||||
x, y = self.opts['x'], self.opts['y']
|
x, y = self.opts['x'], self.opts['y']
|
||||||
if x is None or y is None:
|
if x is None or y is None:
|
||||||
|
self.path = p
|
||||||
return
|
return
|
||||||
|
|
||||||
beam = self.opts['beam']
|
beam = self.opts['beam']
|
||||||
|
@ -146,4 +149,4 @@ class ErrorBarItem(GraphicsObject):
|
||||||
self.drawPath()
|
self.drawPath()
|
||||||
return self.path.boundingRect()
|
return self.path.boundingRect()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -186,7 +186,7 @@ class HistogramLUTItem(GraphicsWidget):
|
||||||
"""Return a lookup table from the color gradient defined by this
|
"""Return a lookup table from the color gradient defined by this
|
||||||
HistogramLUTItem.
|
HistogramLUTItem.
|
||||||
"""
|
"""
|
||||||
if self.levelMode is not 'mono':
|
if self.levelMode != 'mono':
|
||||||
return None
|
return None
|
||||||
if n is None:
|
if n is None:
|
||||||
if img.dtype == np.uint8:
|
if img.dtype == np.uint8:
|
||||||
|
@ -205,8 +205,8 @@ class HistogramLUTItem(GraphicsWidget):
|
||||||
def regionChanging(self):
|
def regionChanging(self):
|
||||||
if self.imageItem() is not None:
|
if self.imageItem() is not None:
|
||||||
self.imageItem().setLevels(self.getLevels())
|
self.imageItem().setLevels(self.getLevels())
|
||||||
self.sigLevelsChanged.emit(self)
|
|
||||||
self.update()
|
self.update()
|
||||||
|
self.sigLevelsChanged.emit(self)
|
||||||
|
|
||||||
def imageChanged(self, autoLevel=False, autoRange=False):
|
def imageChanged(self, autoLevel=False, autoRange=False):
|
||||||
if self.imageItem() is None:
|
if self.imageItem() is None:
|
||||||
|
|
|
@ -2,13 +2,17 @@ from __future__ import division
|
||||||
|
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import collections
|
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
from .. import debug as debug
|
from .. import debug as debug
|
||||||
from .GraphicsObject import GraphicsObject
|
from .GraphicsObject import GraphicsObject
|
||||||
from ..Point import Point
|
from ..Point import Point
|
||||||
from .. import getConfigOption
|
from .. import getConfigOption
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections.abc import Callable
|
||||||
|
except ImportError:
|
||||||
|
# fallback for python < 3.3
|
||||||
|
from collections import Callable
|
||||||
|
|
||||||
__all__ = ['ImageItem']
|
__all__ = ['ImageItem']
|
||||||
|
|
||||||
|
@ -16,23 +20,23 @@ __all__ = ['ImageItem']
|
||||||
class ImageItem(GraphicsObject):
|
class ImageItem(GraphicsObject):
|
||||||
"""
|
"""
|
||||||
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
|
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
|
||||||
|
|
||||||
GraphicsObject displaying an image. Optimized for rapid update (ie video display).
|
GraphicsObject displaying an image. Optimized for rapid update (ie video display).
|
||||||
This item displays either a 2D numpy array (height, width) or
|
This item displays either a 2D numpy array (height, width) or
|
||||||
a 3D array (height, width, RGBa). This array is optionally scaled (see
|
a 3D array (height, width, RGBa). This array is optionally scaled (see
|
||||||
:func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
|
:func:`setLevels <pyqtgraph.ImageItem.setLevels>`) and/or colored
|
||||||
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
|
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
|
||||||
before being displayed.
|
before being displayed.
|
||||||
|
|
||||||
ImageItem is frequently used in conjunction with
|
ImageItem is frequently used in conjunction with
|
||||||
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
|
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
|
||||||
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
|
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
|
||||||
for controlling the levels and lookup table used to display the image.
|
for controlling the levels and lookup table used to display the image.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sigImageChanged = QtCore.Signal()
|
sigImageChanged = QtCore.Signal()
|
||||||
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
|
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
|
||||||
|
|
||||||
def __init__(self, image=None, **kargs):
|
def __init__(self, image=None, **kargs):
|
||||||
"""
|
"""
|
||||||
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
||||||
|
@ -41,23 +45,23 @@ class ImageItem(GraphicsObject):
|
||||||
self.menu = None
|
self.menu = None
|
||||||
self.image = None ## original image data
|
self.image = None ## original image data
|
||||||
self.qimage = None ## rendered image for display
|
self.qimage = None ## rendered image for display
|
||||||
|
|
||||||
self.paintMode = None
|
self.paintMode = None
|
||||||
|
|
||||||
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
||||||
self.lut = None
|
self.lut = None
|
||||||
self.autoDownsample = False
|
self.autoDownsample = False
|
||||||
|
|
||||||
self.axisOrder = getConfigOption('imageAxisOrder')
|
self.axisOrder = getConfigOption('imageAxisOrder')
|
||||||
|
|
||||||
# In some cases, we use a modified lookup table to handle both rescaling
|
# In some cases, we use a modified lookup table to handle both rescaling
|
||||||
# and LUT more efficiently
|
# and LUT more efficiently
|
||||||
self._effectiveLut = None
|
self._effectiveLut = None
|
||||||
|
|
||||||
self.drawKernel = None
|
self.drawKernel = None
|
||||||
self.border = None
|
self.border = None
|
||||||
self.removable = False
|
self.removable = False
|
||||||
|
|
||||||
if image is not None:
|
if image is not None:
|
||||||
self.setImage(image, **kargs)
|
self.setImage(image, **kargs)
|
||||||
else:
|
else:
|
||||||
|
@ -66,32 +70,32 @@ class ImageItem(GraphicsObject):
|
||||||
def setCompositionMode(self, mode):
|
def setCompositionMode(self, mode):
|
||||||
"""Change the composition mode of the item (see QPainter::CompositionMode
|
"""Change the composition mode of the item (see QPainter::CompositionMode
|
||||||
in the Qt documentation). This is useful when overlaying multiple ImageItems.
|
in the Qt documentation). This is useful when overlaying multiple ImageItems.
|
||||||
|
|
||||||
============================================ ============================================================
|
============================================ ============================================================
|
||||||
**Most common arguments:**
|
**Most common arguments:**
|
||||||
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
|
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
|
||||||
is opaque. Otherwise, it uses the alpha channel to blend
|
is opaque. Otherwise, it uses the alpha channel to blend
|
||||||
the image with the background.
|
the image with the background.
|
||||||
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
|
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
|
||||||
reflect the lightness or darkness of the background.
|
reflect the lightness or darkness of the background.
|
||||||
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
|
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
|
||||||
are added together.
|
are added together.
|
||||||
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
|
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
|
||||||
============================================ ============================================================
|
============================================ ============================================================
|
||||||
"""
|
"""
|
||||||
self.paintMode = mode
|
self.paintMode = mode
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setBorder(self, b):
|
def setBorder(self, b):
|
||||||
self.border = fn.mkPen(b)
|
self.border = fn.mkPen(b)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def width(self):
|
def width(self):
|
||||||
if self.image is None:
|
if self.image is None:
|
||||||
return None
|
return None
|
||||||
axis = 0 if self.axisOrder == 'col-major' else 1
|
axis = 0 if self.axisOrder == 'col-major' else 1
|
||||||
return self.image.shape[axis]
|
return self.image.shape[axis]
|
||||||
|
|
||||||
def height(self):
|
def height(self):
|
||||||
if self.image is None:
|
if self.image is None:
|
||||||
return None
|
return None
|
||||||
|
@ -111,10 +115,10 @@ class ImageItem(GraphicsObject):
|
||||||
def setLevels(self, levels, update=True):
|
def setLevels(self, levels, update=True):
|
||||||
"""
|
"""
|
||||||
Set image scaling levels. Can be one of:
|
Set image scaling levels. Can be one of:
|
||||||
|
|
||||||
* [blackLevel, whiteLevel]
|
* [blackLevel, whiteLevel]
|
||||||
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
|
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
|
||||||
|
|
||||||
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
|
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
|
||||||
for more details on how levels are applied.
|
for more details on how levels are applied.
|
||||||
"""
|
"""
|
||||||
|
@ -125,18 +129,18 @@ class ImageItem(GraphicsObject):
|
||||||
self._effectiveLut = None
|
self._effectiveLut = None
|
||||||
if update:
|
if update:
|
||||||
self.updateImage()
|
self.updateImage()
|
||||||
|
|
||||||
def getLevels(self):
|
def getLevels(self):
|
||||||
return self.levels
|
return self.levels
|
||||||
#return self.whiteLevel, self.blackLevel
|
#return self.whiteLevel, self.blackLevel
|
||||||
|
|
||||||
def setLookupTable(self, lut, update=True):
|
def setLookupTable(self, lut, update=True):
|
||||||
"""
|
"""
|
||||||
Set the lookup table (numpy array) to use for this image. (see
|
Set the lookup table (numpy array) to use for this image. (see
|
||||||
:func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used).
|
:func:`makeARGB <pyqtgraph.makeARGB>` for more information on how this is used).
|
||||||
Optionally, lut can be a callable that accepts the current image as an
|
Optionally, lut can be a callable that accepts the current image as an
|
||||||
argument and returns the lookup table to use.
|
argument and returns the lookup table to use.
|
||||||
|
|
||||||
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
|
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
|
||||||
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
|
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
|
||||||
"""
|
"""
|
||||||
|
@ -149,7 +153,7 @@ class ImageItem(GraphicsObject):
|
||||||
def setAutoDownsample(self, ads):
|
def setAutoDownsample(self, ads):
|
||||||
"""
|
"""
|
||||||
Set the automatic downsampling mode for this ImageItem.
|
Set the automatic downsampling mode for this ImageItem.
|
||||||
|
|
||||||
Added in version 0.9.9
|
Added in version 0.9.9
|
||||||
"""
|
"""
|
||||||
self.autoDownsample = ads
|
self.autoDownsample = ads
|
||||||
|
@ -198,44 +202,44 @@ class ImageItem(GraphicsObject):
|
||||||
"""
|
"""
|
||||||
Update the image displayed by this item. For more information on how the image
|
Update the image displayed by this item. For more information on how the image
|
||||||
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
|
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
|
||||||
|
|
||||||
================= =========================================================================
|
================= =========================================================================
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
image (numpy array) Specifies the image data. May be 2D (width, height) or
|
image (numpy array) Specifies the image data. May be 2D (width, height) or
|
||||||
3D (width, height, RGBa). The array dtype must be integer or floating
|
3D (width, height, RGBa). The array dtype must be integer or floating
|
||||||
point of any bit depth. For 3D arrays, the third dimension must
|
point of any bit depth. For 3D arrays, the third dimension must
|
||||||
be of length 3 (RGB) or 4 (RGBA). See *notes* below.
|
be of length 3 (RGB) or 4 (RGBA). See *notes* below.
|
||||||
autoLevels (bool) If True, this forces the image to automatically select
|
autoLevels (bool) If True, this forces the image to automatically select
|
||||||
levels based on the maximum and minimum values in the data.
|
levels based on the maximum and minimum values in the data.
|
||||||
By default, this argument is true unless the levels argument is
|
By default, this argument is true unless the levels argument is
|
||||||
given.
|
given.
|
||||||
lut (numpy array) The color lookup table to use when displaying the image.
|
lut (numpy array) The color lookup table to use when displaying the image.
|
||||||
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
|
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
|
||||||
levels (min, max) The minimum and maximum values to use when rescaling the image
|
levels (min, max) The minimum and maximum values to use when rescaling the image
|
||||||
data. By default, this will be set to the minimum and maximum values
|
data. By default, this will be set to the minimum and maximum values
|
||||||
in the image. If the image array has dtype uint8, no rescaling is necessary.
|
in the image. If the image array has dtype uint8, no rescaling is necessary.
|
||||||
opacity (float 0.0-1.0)
|
opacity (float 0.0-1.0)
|
||||||
compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
||||||
border Sets the pen used when drawing the image border. Default is None.
|
border Sets the pen used when drawing the image border. Default is None.
|
||||||
autoDownsample (bool) If True, the image is automatically downsampled to match the
|
autoDownsample (bool) If True, the image is automatically downsampled to match the
|
||||||
screen resolution. This improves performance for large images and
|
screen resolution. This improves performance for large images and
|
||||||
reduces aliasing. If autoDownsample is not specified, then ImageItem will
|
reduces aliasing. If autoDownsample is not specified, then ImageItem will
|
||||||
choose whether to downsample the image based on its size.
|
choose whether to downsample the image based on its size.
|
||||||
================= =========================================================================
|
================= =========================================================================
|
||||||
|
|
||||||
|
|
||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
For backward compatibility, image data is assumed to be in column-major order (column, row).
|
For backward compatibility, image data is assumed to be in column-major order (column, row).
|
||||||
However, most image data is stored in row-major order (row, column) and will need to be
|
However, most image data is stored in row-major order (row, column) and will need to be
|
||||||
transposed before calling setImage()::
|
transposed before calling setImage()::
|
||||||
|
|
||||||
imageitem.setImage(imagedata.T)
|
imageitem.setImage(imagedata.T)
|
||||||
|
|
||||||
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
|
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
|
||||||
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
|
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
profile = debug.Profiler()
|
profile = debug.Profiler()
|
||||||
|
|
||||||
|
@ -292,7 +296,7 @@ class ImageItem(GraphicsObject):
|
||||||
def dataTransform(self):
|
def dataTransform(self):
|
||||||
"""Return the transform that maps from this image's input array to its
|
"""Return the transform that maps from this image's input array to its
|
||||||
local coordinate system.
|
local coordinate system.
|
||||||
|
|
||||||
This transform corrects for the transposition that occurs when image data
|
This transform corrects for the transposition that occurs when image data
|
||||||
is interpreted in row-major order.
|
is interpreted in row-major order.
|
||||||
"""
|
"""
|
||||||
|
@ -307,7 +311,7 @@ class ImageItem(GraphicsObject):
|
||||||
def inverseDataTransform(self):
|
def inverseDataTransform(self):
|
||||||
"""Return the transform that maps from this image's local coordinate
|
"""Return the transform that maps from this image's local coordinate
|
||||||
system to its input array.
|
system to its input array.
|
||||||
|
|
||||||
See dataTransform() for more information.
|
See dataTransform() for more information.
|
||||||
"""
|
"""
|
||||||
tr = QtGui.QTransform()
|
tr = QtGui.QTransform()
|
||||||
|
@ -339,7 +343,7 @@ class ImageItem(GraphicsObject):
|
||||||
|
|
||||||
def updateImage(self, *args, **kargs):
|
def updateImage(self, *args, **kargs):
|
||||||
## used for re-rendering qimage from self.image.
|
## used for re-rendering qimage from self.image.
|
||||||
|
|
||||||
## can we make any assumptions here that speed things up?
|
## can we make any assumptions here that speed things up?
|
||||||
## dtype, range, size are all the same?
|
## dtype, range, size are all the same?
|
||||||
defaults = {
|
defaults = {
|
||||||
|
@ -350,14 +354,14 @@ class ImageItem(GraphicsObject):
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
# Convert data to QImage for display.
|
# Convert data to QImage for display.
|
||||||
|
|
||||||
profile = debug.Profiler()
|
profile = debug.Profiler()
|
||||||
if self.image is None or self.image.size == 0:
|
if self.image is None or self.image.size == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Request a lookup table if this image has only one channel
|
# Request a lookup table if this image has only one channel
|
||||||
if self.image.ndim == 2 or self.image.shape[2] == 1:
|
if self.image.ndim == 2 or self.image.shape[2] == 1:
|
||||||
if isinstance(self.lut, collections.Callable):
|
if isinstance(self.lut, Callable):
|
||||||
lut = self.lut(self.image)
|
lut = self.lut(self.image)
|
||||||
else:
|
else:
|
||||||
lut = self.lut
|
lut = self.lut
|
||||||
|
@ -385,7 +389,7 @@ class ImageItem(GraphicsObject):
|
||||||
image = fn.downsample(self.image, xds, axis=axes[0])
|
image = fn.downsample(self.image, xds, axis=axes[0])
|
||||||
image = fn.downsample(image, yds, axis=axes[1])
|
image = fn.downsample(image, yds, axis=axes[1])
|
||||||
self._lastDownsample = (xds, yds)
|
self._lastDownsample = (xds, yds)
|
||||||
|
|
||||||
# Check if downsampling reduced the image size to zero due to inf values.
|
# Check if downsampling reduced the image size to zero due to inf values.
|
||||||
if image.size == 0:
|
if image.size == 0:
|
||||||
return
|
return
|
||||||
|
@ -403,27 +407,27 @@ class ImageItem(GraphicsObject):
|
||||||
levdiff = maxlev - minlev
|
levdiff = maxlev - minlev
|
||||||
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
|
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
|
||||||
if lut is None:
|
if lut is None:
|
||||||
efflut = fn.rescaleData(ind, scale=255./levdiff,
|
efflut = fn.rescaleData(ind, scale=255./levdiff,
|
||||||
offset=minlev, dtype=np.ubyte)
|
offset=minlev, dtype=np.ubyte)
|
||||||
else:
|
else:
|
||||||
lutdtype = np.min_scalar_type(lut.shape[0]-1)
|
lutdtype = np.min_scalar_type(lut.shape[0]-1)
|
||||||
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff,
|
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff,
|
||||||
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
|
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
|
||||||
efflut = lut[efflut]
|
efflut = lut[efflut]
|
||||||
|
|
||||||
self._effectiveLut = efflut
|
self._effectiveLut = efflut
|
||||||
lut = self._effectiveLut
|
lut = self._effectiveLut
|
||||||
levels = None
|
levels = None
|
||||||
|
|
||||||
# Convert single-channel image to 2D array
|
# Convert single-channel image to 2D array
|
||||||
if image.ndim == 3 and image.shape[-1] == 1:
|
if image.ndim == 3 and image.shape[-1] == 1:
|
||||||
image = image[..., 0]
|
image = image[..., 0]
|
||||||
|
|
||||||
# Assume images are in column-major order for backward compatibility
|
# Assume images are in column-major order for backward compatibility
|
||||||
# (most images are in row-major order)
|
# (most images are in row-major order)
|
||||||
if self.axisOrder == 'col-major':
|
if self.axisOrder == 'col-major':
|
||||||
image = image.transpose((1, 0, 2)[:image.ndim])
|
image = image.transpose((1, 0, 2)[:image.ndim])
|
||||||
|
|
||||||
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
|
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
|
||||||
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
||||||
|
|
||||||
|
@ -453,26 +457,26 @@ class ImageItem(GraphicsObject):
|
||||||
self.render()
|
self.render()
|
||||||
self.qimage.save(fileName, *args)
|
self.qimage.save(fileName, *args)
|
||||||
|
|
||||||
def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
|
def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
|
||||||
targetHistogramSize=500, **kwds):
|
targetHistogramSize=500, **kwds):
|
||||||
"""Returns x and y arrays containing the histogram values for the current image.
|
"""Returns x and y arrays containing the histogram values for the current image.
|
||||||
For an explanation of the return format, see numpy.histogram().
|
For an explanation of the return format, see numpy.histogram().
|
||||||
|
|
||||||
The *step* argument causes pixels to be skipped when computing the histogram to save time.
|
The *step* argument causes pixels to be skipped when computing the histogram to save time.
|
||||||
If *step* is 'auto', then a step is chosen such that the analyzed data has
|
If *step* is 'auto', then a step is chosen such that the analyzed data has
|
||||||
dimensions roughly *targetImageSize* for each axis.
|
dimensions roughly *targetImageSize* for each axis.
|
||||||
|
|
||||||
The *bins* argument and any extra keyword arguments are passed to
|
The *bins* argument and any extra keyword arguments are passed to
|
||||||
np.histogram(). If *bins* is 'auto', then a bin number is automatically
|
np.histogram(). If *bins* is 'auto', then a bin number is automatically
|
||||||
chosen based on the image characteristics:
|
chosen based on the image characteristics:
|
||||||
|
|
||||||
* Integer images will have approximately *targetHistogramSize* bins,
|
* Integer images will have approximately *targetHistogramSize* bins,
|
||||||
with each bin having an integer width.
|
with each bin having an integer width.
|
||||||
* All other types will have *targetHistogramSize* bins.
|
* All other types will have *targetHistogramSize* bins.
|
||||||
|
|
||||||
If *perChannel* is True, then the histogram is computed once per channel
|
If *perChannel* is True, then the histogram is computed once per channel
|
||||||
and the output is a list of the results.
|
and the output is a list of the results.
|
||||||
|
|
||||||
This method is also used when automatically computing levels.
|
This method is also used when automatically computing levels.
|
||||||
"""
|
"""
|
||||||
if self.image is None or self.image.size == 0:
|
if self.image is None or self.image.size == 0:
|
||||||
|
@ -483,10 +487,13 @@ class ImageItem(GraphicsObject):
|
||||||
if np.isscalar(step):
|
if np.isscalar(step):
|
||||||
step = (step, step)
|
step = (step, step)
|
||||||
stepData = self.image[::step[0], ::step[1]]
|
stepData = self.image[::step[0], ::step[1]]
|
||||||
|
|
||||||
if bins == 'auto':
|
if 'auto' == bins:
|
||||||
mn = np.nanmin(stepData)
|
mn = np.nanmin(stepData)
|
||||||
mx = np.nanmax(stepData)
|
mx = np.nanmax(stepData)
|
||||||
|
if mx == mn:
|
||||||
|
# degenerate image, arange will fail
|
||||||
|
mx += 1
|
||||||
if np.isnan(mn) or np.isnan(mx):
|
if np.isnan(mn) or np.isnan(mx):
|
||||||
# the data are all-nan
|
# the data are all-nan
|
||||||
return None, None
|
return None, None
|
||||||
|
@ -497,7 +504,7 @@ class ImageItem(GraphicsObject):
|
||||||
else:
|
else:
|
||||||
# for float data, let numpy select the bins.
|
# for float data, let numpy select the bins.
|
||||||
bins = np.linspace(mn, mx, 500)
|
bins = np.linspace(mn, mx, 500)
|
||||||
|
|
||||||
if len(bins) == 0:
|
if len(bins) == 0:
|
||||||
bins = [mn, mx]
|
bins = [mn, mx]
|
||||||
|
|
||||||
|
@ -524,7 +531,7 @@ class ImageItem(GraphicsObject):
|
||||||
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
|
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
|
||||||
"""
|
"""
|
||||||
self.setFlag(self.ItemIgnoresTransformations, b)
|
self.setFlag(self.ItemIgnoresTransformations, b)
|
||||||
|
|
||||||
def setScaledMode(self):
|
def setScaledMode(self):
|
||||||
self.setPxMode(False)
|
self.setPxMode(False)
|
||||||
|
|
||||||
|
@ -534,14 +541,14 @@ class ImageItem(GraphicsObject):
|
||||||
if self.qimage is None:
|
if self.qimage is None:
|
||||||
return None
|
return None
|
||||||
return QtGui.QPixmap.fromImage(self.qimage)
|
return QtGui.QPixmap.fromImage(self.qimage)
|
||||||
|
|
||||||
def pixelSize(self):
|
def pixelSize(self):
|
||||||
"""return scene-size of a single pixel in the image"""
|
"""return scene-size of a single pixel in the image"""
|
||||||
br = self.sceneBoundingRect()
|
br = self.sceneBoundingRect()
|
||||||
if self.image is None:
|
if self.image is None:
|
||||||
return 1,1
|
return 1,1
|
||||||
return br.width()/self.width(), br.height()/self.height()
|
return br.width()/self.width(), br.height()/self.height()
|
||||||
|
|
||||||
def viewTransformChanged(self):
|
def viewTransformChanged(self):
|
||||||
if self.autoDownsample:
|
if self.autoDownsample:
|
||||||
self.qimage = None
|
self.qimage = None
|
||||||
|
@ -582,7 +589,7 @@ class ImageItem(GraphicsObject):
|
||||||
self.menu.addAction(remAct)
|
self.menu.addAction(remAct)
|
||||||
self.menu.remAct = remAct
|
self.menu.remAct = remAct
|
||||||
return self.menu
|
return self.menu
|
||||||
|
|
||||||
def hoverEvent(self, ev):
|
def hoverEvent(self, ev):
|
||||||
if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton):
|
if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton):
|
||||||
ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
|
ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it.
|
||||||
|
@ -595,7 +602,7 @@ class ImageItem(GraphicsObject):
|
||||||
#print(ev.device())
|
#print(ev.device())
|
||||||
#print(ev.pointerType())
|
#print(ev.pointerType())
|
||||||
#print(ev.pressure())
|
#print(ev.pressure())
|
||||||
|
|
||||||
def drawAt(self, pos, ev=None):
|
def drawAt(self, pos, ev=None):
|
||||||
pos = [int(pos.x()), int(pos.y())]
|
pos = [int(pos.x()), int(pos.y())]
|
||||||
dk = self.drawKernel
|
dk = self.drawKernel
|
||||||
|
@ -604,7 +611,7 @@ class ImageItem(GraphicsObject):
|
||||||
sy = [0,dk.shape[1]]
|
sy = [0,dk.shape[1]]
|
||||||
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
|
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
|
||||||
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
|
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
|
||||||
|
|
||||||
for i in [0,1]:
|
for i in [0,1]:
|
||||||
dx1 = -min(0, tx[i])
|
dx1 = -min(0, tx[i])
|
||||||
dx2 = min(0, self.image.shape[0]-tx[i])
|
dx2 = min(0, self.image.shape[0]-tx[i])
|
||||||
|
@ -620,8 +627,8 @@ class ImageItem(GraphicsObject):
|
||||||
ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
|
ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1]))
|
||||||
mask = self.drawMask
|
mask = self.drawMask
|
||||||
src = dk
|
src = dk
|
||||||
|
|
||||||
if isinstance(self.drawMode, collections.Callable):
|
if isinstance(self.drawMode, Callable):
|
||||||
self.drawMode(dk, self.image, mask, ss, ts, ev)
|
self.drawMode(dk, self.image, mask, ss, ts, ev)
|
||||||
else:
|
else:
|
||||||
src = src[ss]
|
src = src[ss]
|
||||||
|
@ -636,7 +643,7 @@ class ImageItem(GraphicsObject):
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown draw mode '%s'" % self.drawMode)
|
raise Exception("Unknown draw mode '%s'" % self.drawMode)
|
||||||
self.updateImage()
|
self.updateImage()
|
||||||
|
|
||||||
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
|
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
|
||||||
self.drawKernel = kernel
|
self.drawKernel = kernel
|
||||||
self.drawKernelCenter = center
|
self.drawKernelCenter = center
|
||||||
|
|
|
@ -4,7 +4,7 @@ try:
|
||||||
HAVE_OPENGL = True
|
HAVE_OPENGL = True
|
||||||
except:
|
except:
|
||||||
HAVE_OPENGL = False
|
HAVE_OPENGL = False
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from .GraphicsObject import GraphicsObject
|
from .GraphicsObject import GraphicsObject
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
|
@ -15,51 +15,50 @@ from .. import debug
|
||||||
|
|
||||||
__all__ = ['PlotCurveItem']
|
__all__ = ['PlotCurveItem']
|
||||||
class PlotCurveItem(GraphicsObject):
|
class PlotCurveItem(GraphicsObject):
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Class representing a single plot curve. Instances of this class are created
|
Class representing a single plot curve. Instances of this class are created
|
||||||
automatically as part of PlotDataItem; these rarely need to be instantiated
|
automatically as part of PlotDataItem; these rarely need to be instantiated
|
||||||
directly.
|
directly.
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
|
||||||
- Fast data update
|
- Fast data update
|
||||||
- Fill under curve
|
- Fill under curve
|
||||||
- Mouse interaction
|
- Mouse interaction
|
||||||
|
|
||||||
==================== ===============================================
|
==================== ===============================================
|
||||||
**Signals:**
|
**Signals:**
|
||||||
sigPlotChanged(self) Emitted when the data being plotted has changed
|
sigPlotChanged(self) Emitted when the data being plotted has changed
|
||||||
sigClicked(self) Emitted when the curve is clicked
|
sigClicked(self) Emitted when the curve is clicked
|
||||||
==================== ===============================================
|
==================== ===============================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sigPlotChanged = QtCore.Signal(object)
|
sigPlotChanged = QtCore.Signal(object)
|
||||||
sigClicked = QtCore.Signal(object)
|
sigClicked = QtCore.Signal(object)
|
||||||
|
|
||||||
def __init__(self, *args, **kargs):
|
def __init__(self, *args, **kargs):
|
||||||
"""
|
"""
|
||||||
Forwards all arguments to :func:`setData <pyqtgraph.PlotCurveItem.setData>`.
|
Forwards all arguments to :func:`setData <pyqtgraph.PlotCurveItem.setData>`.
|
||||||
|
|
||||||
Some extra arguments are accepted as well:
|
Some extra arguments are accepted as well:
|
||||||
|
|
||||||
============== =======================================================
|
============== =======================================================
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
parent The parent GraphicsObject (optional)
|
parent The parent GraphicsObject (optional)
|
||||||
clickable If True, the item will emit sigClicked when it is
|
clickable If True, the item will emit sigClicked when it is
|
||||||
clicked on. Defaults to False.
|
clicked on. Defaults to False.
|
||||||
============== =======================================================
|
============== =======================================================
|
||||||
"""
|
"""
|
||||||
GraphicsObject.__init__(self, kargs.get('parent', None))
|
GraphicsObject.__init__(self, kargs.get('parent', None))
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
## this is disastrous for performance.
|
## this is disastrous for performance.
|
||||||
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
|
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
|
||||||
|
|
||||||
self.metaData = {}
|
self.metaData = {}
|
||||||
self.opts = {
|
self.opts = {
|
||||||
'pen': fn.mkPen('w'),
|
|
||||||
'shadowPen': None,
|
'shadowPen': None,
|
||||||
'fillLevel': None,
|
'fillLevel': None,
|
||||||
'brush': None,
|
'brush': None,
|
||||||
|
@ -70,21 +69,23 @@ class PlotCurveItem(GraphicsObject):
|
||||||
'mouseWidth': 8, # width of shape responding to mouse click
|
'mouseWidth': 8, # width of shape responding to mouse click
|
||||||
'compositionMode': None,
|
'compositionMode': None,
|
||||||
}
|
}
|
||||||
|
if 'pen' not in kargs:
|
||||||
|
self.opts['pen'] = fn.mkPen('w')
|
||||||
self.setClickable(kargs.get('clickable', False))
|
self.setClickable(kargs.get('clickable', False))
|
||||||
self.setData(*args, **kargs)
|
self.setData(*args, **kargs)
|
||||||
|
|
||||||
def implements(self, interface=None):
|
def implements(self, interface=None):
|
||||||
ints = ['plotData']
|
ints = ['plotData']
|
||||||
if interface is None:
|
if interface is None:
|
||||||
return ints
|
return ints
|
||||||
return interface in ints
|
return interface in ints
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
return self.opts.get('name', None)
|
return self.opts.get('name', None)
|
||||||
|
|
||||||
def setClickable(self, s, width=None):
|
def setClickable(self, s, width=None):
|
||||||
"""Sets whether the item responds to mouse clicks.
|
"""Sets whether the item responds to mouse clicks.
|
||||||
|
|
||||||
The *width* argument specifies the width in pixels orthogonal to the
|
The *width* argument specifies the width in pixels orthogonal to the
|
||||||
curve that will respond to a mouse click.
|
curve that will respond to a mouse click.
|
||||||
"""
|
"""
|
||||||
|
@ -92,41 +93,41 @@ class PlotCurveItem(GraphicsObject):
|
||||||
if width is not None:
|
if width is not None:
|
||||||
self.opts['mouseWidth'] = width
|
self.opts['mouseWidth'] = width
|
||||||
self._mouseShape = None
|
self._mouseShape = None
|
||||||
self._boundingRect = None
|
self._boundingRect = None
|
||||||
|
|
||||||
def setCompositionMode(self, mode):
|
def setCompositionMode(self, mode):
|
||||||
"""Change the composition mode of the item (see QPainter::CompositionMode
|
"""Change the composition mode of the item (see QPainter::CompositionMode
|
||||||
in the Qt documentation). This is useful when overlaying multiple items.
|
in the Qt documentation). This is useful when overlaying multiple items.
|
||||||
|
|
||||||
============================================ ============================================================
|
============================================ ============================================================
|
||||||
**Most common arguments:**
|
**Most common arguments:**
|
||||||
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
|
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
|
||||||
is opaque. Otherwise, it uses the alpha channel to blend
|
is opaque. Otherwise, it uses the alpha channel to blend
|
||||||
the image with the background.
|
the image with the background.
|
||||||
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
|
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
|
||||||
reflect the lightness or darkness of the background.
|
reflect the lightness or darkness of the background.
|
||||||
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
|
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
|
||||||
are added together.
|
are added together.
|
||||||
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
|
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
|
||||||
============================================ ============================================================
|
============================================ ============================================================
|
||||||
"""
|
"""
|
||||||
self.opts['compositionMode'] = mode
|
self.opts['compositionMode'] = mode
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def getData(self):
|
def getData(self):
|
||||||
return self.xData, self.yData
|
return self.xData, self.yData
|
||||||
|
|
||||||
def dataBounds(self, ax, frac=1.0, orthoRange=None):
|
def dataBounds(self, ax, frac=1.0, orthoRange=None):
|
||||||
## Need this to run as fast as possible.
|
## Need this to run as fast as possible.
|
||||||
## check cache first:
|
## check cache first:
|
||||||
cache = self._boundsCache[ax]
|
cache = self._boundsCache[ax]
|
||||||
if cache is not None and cache[0] == (frac, orthoRange):
|
if cache is not None and cache[0] == (frac, orthoRange):
|
||||||
return cache[1]
|
return cache[1]
|
||||||
|
|
||||||
(x, y) = self.getData()
|
(x, y) = self.getData()
|
||||||
if x is None or len(x) == 0:
|
if x is None or len(x) == 0:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
if ax == 0:
|
if ax == 0:
|
||||||
d = x
|
d = x
|
||||||
d2 = y
|
d2 = y
|
||||||
|
@ -139,7 +140,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
|
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
|
||||||
d = d[mask]
|
d = d[mask]
|
||||||
#d2 = d2[mask]
|
#d2 = d2[mask]
|
||||||
|
|
||||||
if len(d) == 0:
|
if len(d) == 0:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
|
|
||||||
|
@ -154,7 +155,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
if len(d) == 0:
|
if len(d) == 0:
|
||||||
return (None, None)
|
return (None, None)
|
||||||
b = (d.min(), d.max())
|
b = (d.min(), d.max())
|
||||||
|
|
||||||
elif frac <= 0.0:
|
elif frac <= 0.0:
|
||||||
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
||||||
else:
|
else:
|
||||||
|
@ -166,7 +167,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
## adjust for fill level
|
## adjust for fill level
|
||||||
if ax == 1 and self.opts['fillLevel'] is not None:
|
if ax == 1 and self.opts['fillLevel'] is not None:
|
||||||
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
|
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
|
||||||
|
|
||||||
## Add pen width only if it is non-cosmetic.
|
## Add pen width only if it is non-cosmetic.
|
||||||
pen = self.opts['pen']
|
pen = self.opts['pen']
|
||||||
spen = self.opts['shadowPen']
|
spen = self.opts['shadowPen']
|
||||||
|
@ -174,10 +175,10 @@ class PlotCurveItem(GraphicsObject):
|
||||||
b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072)
|
b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072)
|
||||||
if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
|
if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen:
|
||||||
b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072)
|
b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072)
|
||||||
|
|
||||||
self._boundsCache[ax] = [(frac, orthoRange), b]
|
self._boundsCache[ax] = [(frac, orthoRange), b]
|
||||||
return b
|
return b
|
||||||
|
|
||||||
def pixelPadding(self):
|
def pixelPadding(self):
|
||||||
pen = self.opts['pen']
|
pen = self.opts['pen']
|
||||||
spen = self.opts['shadowPen']
|
spen = self.opts['shadowPen']
|
||||||
|
@ -196,11 +197,11 @@ class PlotCurveItem(GraphicsObject):
|
||||||
(ymn, ymx) = self.dataBounds(ax=1)
|
(ymn, ymx) = self.dataBounds(ax=1)
|
||||||
if xmn is None or ymn is None:
|
if xmn is None or ymn is None:
|
||||||
return QtCore.QRectF()
|
return QtCore.QRectF()
|
||||||
|
|
||||||
px = py = 0.0
|
px = py = 0.0
|
||||||
pxPad = self.pixelPadding()
|
pxPad = self.pixelPadding()
|
||||||
if pxPad > 0:
|
if pxPad > 0:
|
||||||
# determine length of pixel in local x, y directions
|
# determine length of pixel in local x, y directions
|
||||||
px, py = self.pixelVectors()
|
px, py = self.pixelVectors()
|
||||||
try:
|
try:
|
||||||
px = 0 if px is None else px.length()
|
px = 0 if px is None else px.length()
|
||||||
|
@ -210,68 +211,68 @@ class PlotCurveItem(GraphicsObject):
|
||||||
py = 0 if py is None else py.length()
|
py = 0 if py is None else py.length()
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
py = 0
|
py = 0
|
||||||
|
|
||||||
# return bounds expanded by pixel size
|
# return bounds expanded by pixel size
|
||||||
px *= pxPad
|
px *= pxPad
|
||||||
py *= pxPad
|
py *= pxPad
|
||||||
#px += self._maxSpotWidth * 0.5
|
#px += self._maxSpotWidth * 0.5
|
||||||
#py += self._maxSpotWidth * 0.5
|
#py += self._maxSpotWidth * 0.5
|
||||||
self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
|
self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn)
|
||||||
|
|
||||||
return self._boundingRect
|
return self._boundingRect
|
||||||
|
|
||||||
def viewTransformChanged(self):
|
def viewTransformChanged(self):
|
||||||
self.invalidateBounds()
|
self.invalidateBounds()
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
|
|
||||||
#def boundingRect(self):
|
#def boundingRect(self):
|
||||||
#if self._boundingRect is None:
|
#if self._boundingRect is None:
|
||||||
#(x, y) = self.getData()
|
#(x, y) = self.getData()
|
||||||
#if x is None or y is None or len(x) == 0 or len(y) == 0:
|
#if x is None or y is None or len(x) == 0 or len(y) == 0:
|
||||||
#return QtCore.QRectF()
|
#return QtCore.QRectF()
|
||||||
|
|
||||||
|
|
||||||
#if self.opts['shadowPen'] is not None:
|
#if self.opts['shadowPen'] is not None:
|
||||||
#lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
|
#lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
|
||||||
#else:
|
#else:
|
||||||
#lineWidth = (self.opts['pen'].width()+1)
|
#lineWidth = (self.opts['pen'].width()+1)
|
||||||
|
|
||||||
|
|
||||||
#pixels = self.pixelVectors()
|
#pixels = self.pixelVectors()
|
||||||
#if pixels == (None, None):
|
#if pixels == (None, None):
|
||||||
#pixels = [Point(0,0), Point(0,0)]
|
#pixels = [Point(0,0), Point(0,0)]
|
||||||
|
|
||||||
#xmin = x.min()
|
#xmin = x.min()
|
||||||
#xmax = x.max()
|
#xmax = x.max()
|
||||||
#ymin = y.min()
|
#ymin = y.min()
|
||||||
#ymax = y.max()
|
#ymax = y.max()
|
||||||
|
|
||||||
#if self.opts['fillLevel'] is not None:
|
#if self.opts['fillLevel'] is not None:
|
||||||
#ymin = min(ymin, self.opts['fillLevel'])
|
#ymin = min(ymin, self.opts['fillLevel'])
|
||||||
#ymax = max(ymax, self.opts['fillLevel'])
|
#ymax = max(ymax, self.opts['fillLevel'])
|
||||||
|
|
||||||
#xmin -= pixels[0].x() * lineWidth
|
#xmin -= pixels[0].x() * lineWidth
|
||||||
#xmax += pixels[0].x() * lineWidth
|
#xmax += pixels[0].x() * lineWidth
|
||||||
#ymin -= abs(pixels[1].y()) * lineWidth
|
#ymin -= abs(pixels[1].y()) * lineWidth
|
||||||
#ymax += abs(pixels[1].y()) * lineWidth
|
#ymax += abs(pixels[1].y()) * lineWidth
|
||||||
|
|
||||||
#self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
|
#self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
|
||||||
#return self._boundingRect
|
#return self._boundingRect
|
||||||
|
|
||||||
|
|
||||||
def invalidateBounds(self):
|
def invalidateBounds(self):
|
||||||
self._boundingRect = None
|
self._boundingRect = None
|
||||||
self._boundsCache = [None, None]
|
self._boundsCache = [None, None]
|
||||||
|
|
||||||
def setPen(self, *args, **kargs):
|
def setPen(self, *args, **kargs):
|
||||||
"""Set the pen used to draw the curve."""
|
"""Set the pen used to draw the curve."""
|
||||||
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
||||||
self.invalidateBounds()
|
self.invalidateBounds()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setShadowPen(self, *args, **kargs):
|
def setShadowPen(self, *args, **kargs):
|
||||||
"""Set the shadow pen used to draw behind tyhe primary pen.
|
"""Set the shadow pen used to draw behind tyhe primary pen.
|
||||||
This pen must have a larger width than the primary
|
This pen must have a larger width than the primary
|
||||||
pen to be visible.
|
pen to be visible.
|
||||||
"""
|
"""
|
||||||
self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
|
self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
|
||||||
|
@ -283,7 +284,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
||||||
self.invalidateBounds()
|
self.invalidateBounds()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setFillLevel(self, level):
|
def setFillLevel(self, level):
|
||||||
"""Set the level filled to when filling under the curve"""
|
"""Set the level filled to when filling under the curve"""
|
||||||
self.opts['fillLevel'] = level
|
self.opts['fillLevel'] = level
|
||||||
|
@ -295,11 +296,11 @@ class PlotCurveItem(GraphicsObject):
|
||||||
"""
|
"""
|
||||||
=============== ========================================================
|
=============== ========================================================
|
||||||
**Arguments:**
|
**Arguments:**
|
||||||
x, y (numpy arrays) Data to show
|
x, y (numpy arrays) Data to show
|
||||||
pen Pen to use when drawing. Any single argument accepted by
|
pen Pen to use when drawing. Any single argument accepted by
|
||||||
:func:`mkPen <pyqtgraph.mkPen>` is allowed.
|
:func:`mkPen <pyqtgraph.mkPen>` is allowed.
|
||||||
shadowPen Pen for drawing behind the primary pen. Usually this
|
shadowPen Pen for drawing behind the primary pen. Usually this
|
||||||
is used to emphasize the curve by providing a
|
is used to emphasize the curve by providing a
|
||||||
high-contrast border. Any single argument accepted by
|
high-contrast border. Any single argument accepted by
|
||||||
:func:`mkPen <pyqtgraph.mkPen>` is allowed.
|
:func:`mkPen <pyqtgraph.mkPen>` is allowed.
|
||||||
fillLevel (float or None) Fill the area 'under' the curve to
|
fillLevel (float or None) Fill the area 'under' the curve to
|
||||||
|
@ -317,18 +318,18 @@ class PlotCurveItem(GraphicsObject):
|
||||||
to be drawn. "finite" causes segments to be omitted if
|
to be drawn. "finite" causes segments to be omitted if
|
||||||
they are attached to nan or inf values. For any other
|
they are attached to nan or inf values. For any other
|
||||||
connectivity, specify an array of boolean values.
|
connectivity, specify an array of boolean values.
|
||||||
compositionMode See :func:`setCompositionMode
|
compositionMode See :func:`setCompositionMode
|
||||||
<pyqtgraph.PlotCurveItem.setCompositionMode>`.
|
<pyqtgraph.PlotCurveItem.setCompositionMode>`.
|
||||||
=============== ========================================================
|
=============== ========================================================
|
||||||
|
|
||||||
If non-keyword arguments are used, they will be interpreted as
|
If non-keyword arguments are used, they will be interpreted as
|
||||||
setData(y) for a single argument and setData(x, y) for two
|
setData(y) for a single argument and setData(x, y) for two
|
||||||
arguments.
|
arguments.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.updateData(*args, **kargs)
|
self.updateData(*args, **kargs)
|
||||||
|
|
||||||
def updateData(self, *args, **kargs):
|
def updateData(self, *args, **kargs):
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
|
|
||||||
|
@ -340,12 +341,12 @@ class PlotCurveItem(GraphicsObject):
|
||||||
elif len(args) == 2:
|
elif len(args) == 2:
|
||||||
kargs['x'] = args[0]
|
kargs['x'] = args[0]
|
||||||
kargs['y'] = args[1]
|
kargs['y'] = args[1]
|
||||||
|
|
||||||
if 'y' not in kargs or kargs['y'] is None:
|
if 'y' not in kargs or kargs['y'] is None:
|
||||||
kargs['y'] = np.array([])
|
kargs['y'] = np.array([])
|
||||||
if 'x' not in kargs or kargs['x'] is None:
|
if 'x' not in kargs or kargs['x'] is None:
|
||||||
kargs['x'] = np.arange(len(kargs['y']))
|
kargs['x'] = np.arange(len(kargs['y']))
|
||||||
|
|
||||||
for k in ['x', 'y']:
|
for k in ['x', 'y']:
|
||||||
data = kargs[k]
|
data = kargs[k]
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
|
@ -355,9 +356,9 @@ class PlotCurveItem(GraphicsObject):
|
||||||
raise Exception("Plot data must be 1D ndarray.")
|
raise Exception("Plot data must be 1D ndarray.")
|
||||||
if 'complex' in str(data.dtype):
|
if 'complex' in str(data.dtype):
|
||||||
raise Exception("Can not plot complex data types.")
|
raise Exception("Can not plot complex data types.")
|
||||||
|
|
||||||
profiler("data checks")
|
profiler("data checks")
|
||||||
|
|
||||||
#self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly
|
#self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly
|
||||||
## Test this bug with test_PlotWidget and zoom in on the animated plot
|
## Test this bug with test_PlotWidget and zoom in on the animated plot
|
||||||
self.invalidateBounds()
|
self.invalidateBounds()
|
||||||
|
@ -365,24 +366,24 @@ class PlotCurveItem(GraphicsObject):
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
self.yData = kargs['y'].view(np.ndarray)
|
self.yData = kargs['y'].view(np.ndarray)
|
||||||
self.xData = kargs['x'].view(np.ndarray)
|
self.xData = kargs['x'].view(np.ndarray)
|
||||||
|
|
||||||
profiler('copy')
|
profiler('copy')
|
||||||
|
|
||||||
if 'stepMode' in kargs:
|
if 'stepMode' in kargs:
|
||||||
self.opts['stepMode'] = kargs['stepMode']
|
self.opts['stepMode'] = kargs['stepMode']
|
||||||
|
|
||||||
if self.opts['stepMode'] is True:
|
if self.opts['stepMode'] is True:
|
||||||
if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots
|
if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots
|
||||||
raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape))
|
raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape))
|
||||||
else:
|
else:
|
||||||
if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots
|
if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots
|
||||||
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (self.xData.shape, self.yData.shape))
|
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (self.xData.shape, self.yData.shape))
|
||||||
|
|
||||||
self.path = None
|
self.path = None
|
||||||
self.fillPath = None
|
self.fillPath = None
|
||||||
self._mouseShape = None
|
self._mouseShape = None
|
||||||
#self.xDisp = self.yDisp = None
|
#self.xDisp = self.yDisp = None
|
||||||
|
|
||||||
if 'name' in kargs:
|
if 'name' in kargs:
|
||||||
self.opts['name'] = kargs['name']
|
self.opts['name'] = kargs['name']
|
||||||
if 'connect' in kargs:
|
if 'connect' in kargs:
|
||||||
|
@ -397,14 +398,14 @@ class PlotCurveItem(GraphicsObject):
|
||||||
self.setBrush(kargs['brush'])
|
self.setBrush(kargs['brush'])
|
||||||
if 'antialias' in kargs:
|
if 'antialias' in kargs:
|
||||||
self.opts['antialias'] = kargs['antialias']
|
self.opts['antialias'] = kargs['antialias']
|
||||||
|
|
||||||
|
|
||||||
profiler('set')
|
profiler('set')
|
||||||
self.update()
|
self.update()
|
||||||
profiler('update')
|
profiler('update')
|
||||||
self.sigPlotChanged.emit(self)
|
self.sigPlotChanged.emit(self)
|
||||||
profiler('emit')
|
profiler('emit')
|
||||||
|
|
||||||
def generatePath(self, x, y):
|
def generatePath(self, x, y):
|
||||||
if self.opts['stepMode']:
|
if self.opts['stepMode']:
|
||||||
## each value in the x/y arrays generates 2 points.
|
## each value in the x/y arrays generates 2 points.
|
||||||
|
@ -423,9 +424,9 @@ class PlotCurveItem(GraphicsObject):
|
||||||
y = y2.reshape(y2.size)[1:-1]
|
y = y2.reshape(y2.size)[1:-1]
|
||||||
y[0] = self.opts['fillLevel']
|
y[0] = self.opts['fillLevel']
|
||||||
y[-1] = self.opts['fillLevel']
|
y[-1] = self.opts['fillLevel']
|
||||||
|
|
||||||
path = fn.arrayToQPath(x, y, connect=self.opts['connect'])
|
path = fn.arrayToQPath(x, y, connect=self.opts['connect'])
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
@ -438,7 +439,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
self.path = self.generatePath(*self.getData())
|
self.path = self.generatePath(*self.getData())
|
||||||
self.fillPath = None
|
self.fillPath = None
|
||||||
self._mouseShape = None
|
self._mouseShape = None
|
||||||
|
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
@debug.warnOnException ## raising an exception here causes crash
|
@debug.warnOnException ## raising an exception here causes crash
|
||||||
|
@ -446,27 +447,27 @@ class PlotCurveItem(GraphicsObject):
|
||||||
profiler = debug.Profiler()
|
profiler = debug.Profiler()
|
||||||
if self.xData is None or len(self.xData) == 0:
|
if self.xData is None or len(self.xData) == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
|
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
|
||||||
self.paintGL(p, opt, widget)
|
self.paintGL(p, opt, widget)
|
||||||
return
|
return
|
||||||
|
|
||||||
x = None
|
x = None
|
||||||
y = None
|
y = None
|
||||||
path = self.getPath()
|
path = self.getPath()
|
||||||
profiler('generate path')
|
profiler('generate path')
|
||||||
|
|
||||||
if self._exportOpts is not False:
|
if self._exportOpts is not False:
|
||||||
aa = self._exportOpts.get('antialias', True)
|
aa = self._exportOpts.get('antialias', True)
|
||||||
else:
|
else:
|
||||||
aa = self.opts['antialias']
|
aa = self.opts['antialias']
|
||||||
|
|
||||||
p.setRenderHint(p.Antialiasing, aa)
|
p.setRenderHint(p.Antialiasing, aa)
|
||||||
|
|
||||||
cmode = self.opts['compositionMode']
|
cmode = self.opts['compositionMode']
|
||||||
if cmode is not None:
|
if cmode is not None:
|
||||||
p.setCompositionMode(cmode)
|
p.setCompositionMode(cmode)
|
||||||
|
|
||||||
if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
|
if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
|
||||||
if self.fillPath is None:
|
if self.fillPath is None:
|
||||||
if x is None:
|
if x is None:
|
||||||
|
@ -477,14 +478,14 @@ class PlotCurveItem(GraphicsObject):
|
||||||
p2.lineTo(x[0], y[0])
|
p2.lineTo(x[0], y[0])
|
||||||
p2.closeSubpath()
|
p2.closeSubpath()
|
||||||
self.fillPath = p2
|
self.fillPath = p2
|
||||||
|
|
||||||
profiler('generate fill path')
|
profiler('generate fill path')
|
||||||
p.fillPath(self.fillPath, self.opts['brush'])
|
p.fillPath(self.fillPath, self.opts['brush'])
|
||||||
profiler('draw fill path')
|
profiler('draw fill path')
|
||||||
|
|
||||||
sp = fn.mkPen(self.opts['shadowPen'])
|
sp = self.opts['shadowPen']
|
||||||
cp = fn.mkPen(self.opts['pen'])
|
cp = self.opts['pen']
|
||||||
|
|
||||||
## Copy pens and apply alpha adjustment
|
## Copy pens and apply alpha adjustment
|
||||||
#sp = QtGui.QPen(self.opts['shadowPen'])
|
#sp = QtGui.QPen(self.opts['shadowPen'])
|
||||||
#cp = QtGui.QPen(self.opts['pen'])
|
#cp = QtGui.QPen(self.opts['pen'])
|
||||||
|
@ -495,38 +496,39 @@ class PlotCurveItem(GraphicsObject):
|
||||||
#c.setAlpha(c.alpha() * self.opts['alphaHint'])
|
#c.setAlpha(c.alpha() * self.opts['alphaHint'])
|
||||||
#pen.setColor(c)
|
#pen.setColor(c)
|
||||||
##pen.setCosmetic(True)
|
##pen.setCosmetic(True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if sp is not None and sp.style() != QtCore.Qt.NoPen:
|
if sp is not None and sp.style() != QtCore.Qt.NoPen:
|
||||||
p.setPen(sp)
|
p.setPen(sp)
|
||||||
p.drawPath(path)
|
p.drawPath(path)
|
||||||
p.setPen(cp)
|
p.setPen(cp)
|
||||||
p.drawPath(path)
|
if self.fillPath is not None:
|
||||||
|
p.drawPath(self.fillPath)
|
||||||
|
else:
|
||||||
|
p.drawPath(path)
|
||||||
profiler('drawPath')
|
profiler('drawPath')
|
||||||
|
|
||||||
#print "Render hints:", int(p.renderHints())
|
#print "Render hints:", int(p.renderHints())
|
||||||
#p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
|
#p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
|
||||||
#p.drawRect(self.boundingRect())
|
#p.drawRect(self.boundingRect())
|
||||||
|
|
||||||
def paintGL(self, p, opt, widget):
|
def paintGL(self, p, opt, widget):
|
||||||
p.beginNativePainting()
|
p.beginNativePainting()
|
||||||
import OpenGL.GL as gl
|
import OpenGL.GL as gl
|
||||||
|
|
||||||
## set clipping viewport
|
## set clipping viewport
|
||||||
view = self.getViewBox()
|
view = self.getViewBox()
|
||||||
if view is not None:
|
if view is not None:
|
||||||
rect = view.mapRectToItem(self, view.boundingRect())
|
rect = view.mapRectToItem(self, view.boundingRect())
|
||||||
#gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height()))
|
#gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height()))
|
||||||
|
|
||||||
#gl.glTranslate(-rect.x(), -rect.y(), 0)
|
#gl.glTranslate(-rect.x(), -rect.y(), 0)
|
||||||
|
|
||||||
gl.glEnable(gl.GL_STENCIL_TEST)
|
gl.glEnable(gl.GL_STENCIL_TEST)
|
||||||
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) # disable drawing to frame buffer
|
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) # disable drawing to frame buffer
|
||||||
gl.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer
|
gl.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer
|
||||||
gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF)
|
gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF)
|
||||||
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
|
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
|
||||||
|
|
||||||
## draw stencil pattern
|
## draw stencil pattern
|
||||||
gl.glStencilMask(0xFF)
|
gl.glStencilMask(0xFF)
|
||||||
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
||||||
|
@ -538,12 +540,12 @@ class PlotCurveItem(GraphicsObject):
|
||||||
gl.glVertex2f(rect.x()+rect.width(), rect.y())
|
gl.glVertex2f(rect.x()+rect.width(), rect.y())
|
||||||
gl.glVertex2f(rect.x(), rect.y()+rect.height())
|
gl.glVertex2f(rect.x(), rect.y()+rect.height())
|
||||||
gl.glEnd()
|
gl.glEnd()
|
||||||
|
|
||||||
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
|
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
|
||||||
gl.glDepthMask(gl.GL_TRUE)
|
gl.glDepthMask(gl.GL_TRUE)
|
||||||
gl.glStencilMask(0x00)
|
gl.glStencilMask(0x00)
|
||||||
gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF)
|
gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
x, y = self.getData()
|
x, y = self.getData()
|
||||||
pos = np.empty((len(x), 2))
|
pos = np.empty((len(x), 2))
|
||||||
|
@ -568,7 +570,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
|
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
|
||||||
finally:
|
finally:
|
||||||
p.endNativePainting()
|
p.endNativePainting()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.xData = None ## raw values
|
self.xData = None ## raw values
|
||||||
self.yData = None
|
self.yData = None
|
||||||
|
@ -584,7 +586,7 @@ class PlotCurveItem(GraphicsObject):
|
||||||
def mouseShape(self):
|
def mouseShape(self):
|
||||||
"""
|
"""
|
||||||
Return a QPainterPath representing the clickable shape of the curve
|
Return a QPainterPath representing the clickable shape of the curve
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self._mouseShape is None:
|
if self._mouseShape is None:
|
||||||
view = self.getViewBox()
|
view = self.getViewBox()
|
||||||
|
@ -597,14 +599,14 @@ class PlotCurveItem(GraphicsObject):
|
||||||
mousePath = stroker.createStroke(path)
|
mousePath = stroker.createStroke(path)
|
||||||
self._mouseShape = self.mapFromItem(view, mousePath)
|
self._mouseShape = self.mapFromItem(view, mousePath)
|
||||||
return self._mouseShape
|
return self._mouseShape
|
||||||
|
|
||||||
def mouseClickEvent(self, ev):
|
def mouseClickEvent(self, ev):
|
||||||
if not self.clickable or ev.button() != QtCore.Qt.LeftButton:
|
if not self.clickable or ev.button() != QtCore.Qt.LeftButton:
|
||||||
return
|
return
|
||||||
if self.mouseShape().contains(ev.pos()):
|
if self.mouseShape().contains(ev.pos()):
|
||||||
ev.accept()
|
ev.accept()
|
||||||
self.sigClicked.emit(self)
|
self.sigClicked.emit(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ROIPlotItem(PlotCurveItem):
|
class ROIPlotItem(PlotCurveItem):
|
||||||
|
@ -619,7 +621,7 @@ class ROIPlotItem(PlotCurveItem):
|
||||||
#roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent)
|
#roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent)
|
||||||
roi.sigRegionChanged.connect(self.roiChangedEvent)
|
roi.sigRegionChanged.connect(self.roiChangedEvent)
|
||||||
#self.roiChangedEvent()
|
#self.roiChangedEvent()
|
||||||
|
|
||||||
def getRoiData(self):
|
def getRoiData(self):
|
||||||
d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes)
|
d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes)
|
||||||
if d is None:
|
if d is None:
|
||||||
|
@ -627,7 +629,7 @@ class ROIPlotItem(PlotCurveItem):
|
||||||
while d.ndim > 1:
|
while d.ndim > 1:
|
||||||
d = d.mean(axis=1)
|
d = d.mean(axis=1)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def roiChangedEvent(self):
|
def roiChangedEvent(self):
|
||||||
d = self.getRoiData()
|
d = self.getRoiData()
|
||||||
self.updateData(d, self.xVals)
|
self.updateData(d, self.xVals)
|
||||||
|
|
|
@ -514,11 +514,13 @@ class PlotDataItem(GraphicsObject):
|
||||||
# Ignore the first bin for fft data if we have a logx scale
|
# Ignore the first bin for fft data if we have a logx scale
|
||||||
if self.opts['logMode'][0]:
|
if self.opts['logMode'][0]:
|
||||||
x=x[1:]
|
x=x[1:]
|
||||||
y=y[1:]
|
y=y[1:]
|
||||||
if self.opts['logMode'][0]:
|
|
||||||
x = np.log10(x)
|
with np.errstate(divide='ignore'):
|
||||||
if self.opts['logMode'][1]:
|
if self.opts['logMode'][0]:
|
||||||
y = np.log10(y)
|
x = np.log10(x)
|
||||||
|
if self.opts['logMode'][1]:
|
||||||
|
y = np.log10(y)
|
||||||
|
|
||||||
ds = self.opts['downsample']
|
ds = self.opts['downsample']
|
||||||
if not isinstance(ds, int):
|
if not isinstance(ds, int):
|
||||||
|
@ -643,9 +645,9 @@ class PlotDataItem(GraphicsObject):
|
||||||
#self.yClean = None
|
#self.yClean = None
|
||||||
self.xDisp = None
|
self.xDisp = None
|
||||||
self.yDisp = None
|
self.yDisp = None
|
||||||
self.curve.setData([])
|
self.curve.clear()
|
||||||
self.scatter.setData([])
|
self.scatter.clear()
|
||||||
|
|
||||||
def appendData(self, *args, **kargs):
|
def appendData(self, *args, **kargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -545,9 +545,9 @@ class PlotItem(GraphicsWidget):
|
||||||
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
|
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
|
||||||
Returns the item created.
|
Returns the item created.
|
||||||
"""
|
"""
|
||||||
pos = kwds.get('pos', x if x is not None else y)
|
kwds['pos'] = kwds.get('pos', x if x is not None else y)
|
||||||
angle = kwds.get('angle', 0 if x is None else 90)
|
kwds['angle'] = kwds.get('angle', 0 if x is None else 90)
|
||||||
line = InfiniteLine(pos, angle, **kwds)
|
line = InfiniteLine(**kwds)
|
||||||
self.addItem(line)
|
self.addItem(line)
|
||||||
if z is not None:
|
if z is not None:
|
||||||
line.setZValue(z)
|
line.setZValue(z)
|
||||||
|
@ -986,8 +986,8 @@ class PlotItem(GraphicsWidget):
|
||||||
self._menuEnabled = enableMenu
|
self._menuEnabled = enableMenu
|
||||||
if enableViewBoxMenu is None:
|
if enableViewBoxMenu is None:
|
||||||
return
|
return
|
||||||
if enableViewBoxMenu is 'same':
|
if enableViewBoxMenu == 'same':
|
||||||
enableViewBoxMenu = enableMenu
|
enableViewBoxMenu = enableMenu
|
||||||
self.vb.setMenuEnabled(enableViewBoxMenu)
|
self.vb.setMenuEnabled(enableViewBoxMenu)
|
||||||
|
|
||||||
def menuEnabled(self):
|
def menuEnabled(self):
|
||||||
|
|
|
@ -428,6 +428,7 @@ class ROI(GraphicsObject):
|
||||||
|
|
||||||
def handleMoveStarted(self):
|
def handleMoveStarted(self):
|
||||||
self.preMoveState = self.getState()
|
self.preMoveState = self.getState()
|
||||||
|
self.sigRegionChangeStarted.emit(self)
|
||||||
|
|
||||||
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
|
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
|
||||||
"""
|
"""
|
||||||
|
@ -711,10 +712,10 @@ class ROI(GraphicsObject):
|
||||||
|
|
||||||
if hover:
|
if hover:
|
||||||
self.setMouseHover(True)
|
self.setMouseHover(True)
|
||||||
self.sigHoverEvent.emit(self)
|
|
||||||
ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion.
|
ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion.
|
||||||
ev.acceptClicks(QtCore.Qt.RightButton)
|
ev.acceptClicks(QtCore.Qt.RightButton)
|
||||||
ev.acceptClicks(QtCore.Qt.MidButton)
|
ev.acceptClicks(QtCore.Qt.MidButton)
|
||||||
|
self.sigHoverEvent.emit(self)
|
||||||
else:
|
else:
|
||||||
self.setMouseHover(False)
|
self.setMouseHover(False)
|
||||||
|
|
||||||
|
@ -928,6 +929,7 @@ class ROI(GraphicsObject):
|
||||||
|
|
||||||
if h['type'] == 'rf':
|
if h['type'] == 'rf':
|
||||||
h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle
|
h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle
|
||||||
|
h['pos'] = self.mapFromParent(p1)
|
||||||
|
|
||||||
elif h['type'] == 'sr':
|
elif h['type'] == 'sr':
|
||||||
if h['center'][0] == h['pos'][0]:
|
if h['center'][0] == h['pos'][0]:
|
||||||
|
@ -1102,9 +1104,9 @@ class ROI(GraphicsObject):
|
||||||
return bounds, tr
|
return bounds, tr
|
||||||
|
|
||||||
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
||||||
"""Use the position and orientation of this ROI relative to an imageItem
|
r"""Use the position and orientation of this ROI relative to an imageItem
|
||||||
to pull a slice from an array.
|
to pull a slice from an array.
|
||||||
|
|
||||||
=================== ====================================================
|
=================== ====================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
data The array to slice from. Note that this array does
|
data The array to slice from. Note that this array does
|
||||||
|
@ -1524,9 +1526,9 @@ class TestROI(ROI):
|
||||||
|
|
||||||
|
|
||||||
class RectROI(ROI):
|
class RectROI(ROI):
|
||||||
"""
|
r"""
|
||||||
Rectangular ROI subclass with a single scale handle at the top-right corner.
|
Rectangular ROI subclass with a single scale handle at the top-right corner.
|
||||||
|
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
pos (length-2 sequence) The position of the ROI origin.
|
pos (length-2 sequence) The position of the ROI origin.
|
||||||
|
@ -1553,7 +1555,7 @@ class RectROI(ROI):
|
||||||
self.addScaleHandle([0.5, 1], [0.5, center[1]])
|
self.addScaleHandle([0.5, 1], [0.5, center[1]])
|
||||||
|
|
||||||
class LineROI(ROI):
|
class LineROI(ROI):
|
||||||
"""
|
r"""
|
||||||
Rectangular ROI subclass with scale-rotate handles on either side. This
|
Rectangular ROI subclass with scale-rotate handles on either side. This
|
||||||
allows the ROI to be positioned as if moving the ends of a line segment.
|
allows the ROI to be positioned as if moving the ends of a line segment.
|
||||||
A third handle controls the width of the ROI orthogonal to its "line" axis.
|
A third handle controls the width of the ROI orthogonal to its "line" axis.
|
||||||
|
@ -1586,11 +1588,13 @@ class LineROI(ROI):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MultiRectROI(QtGui.QGraphicsObject):
|
class MultiRectROI(QtGui.QGraphicsObject):
|
||||||
"""
|
r"""
|
||||||
Chain of rectangular ROIs connected by handles.
|
Chain of rectangular ROIs connected by handles.
|
||||||
|
|
||||||
This is generally used to mark a curved path through
|
This is generally used to mark a curved path through
|
||||||
an image similarly to PolyLineROI. It differs in that each segment
|
an image similarly to PolyLineROI. It differs in that each segment
|
||||||
of the chain is rectangular instead of linear and thus has width.
|
of the chain is rectangular instead of linear and thus has width.
|
||||||
|
|
||||||
|
@ -1665,7 +1669,7 @@ class MultiRectROI(QtGui.QGraphicsObject):
|
||||||
ms = min([r.shape[axes[1]] for r in rgns])
|
ms = min([r.shape[axes[1]] for r in rgns])
|
||||||
sl = [slice(None)] * rgns[0].ndim
|
sl = [slice(None)] * rgns[0].ndim
|
||||||
sl[axes[1]] = slice(0,ms)
|
sl[axes[1]] = slice(0,ms)
|
||||||
rgns = [r[sl] for r in rgns]
|
rgns = [r[tuple(sl)] for r in rgns]
|
||||||
#print [r.shape for r in rgns], axes
|
#print [r.shape for r in rgns], axes
|
||||||
|
|
||||||
return np.concatenate(rgns, axis=axes[0])
|
return np.concatenate(rgns, axis=axes[0])
|
||||||
|
@ -1724,12 +1728,12 @@ class MultiLineROI(MultiRectROI):
|
||||||
MultiRectROI.__init__(self, *args, **kwds)
|
MultiRectROI.__init__(self, *args, **kwds)
|
||||||
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
|
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
|
||||||
|
|
||||||
|
|
||||||
class EllipseROI(ROI):
|
class EllipseROI(ROI):
|
||||||
"""
|
r"""
|
||||||
Elliptical ROI subclass with one scale handle and one rotation handle.
|
Elliptical ROI subclass with one scale handle and one rotation handle.
|
||||||
|
|
||||||
|
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
pos (length-2 sequence) The position of the ROI's origin.
|
pos (length-2 sequence) The position of the ROI's origin.
|
||||||
|
@ -1810,8 +1814,9 @@ class EllipseROI(ROI):
|
||||||
return self.path
|
return self.path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CircleROI(EllipseROI):
|
class CircleROI(EllipseROI):
|
||||||
"""
|
r"""
|
||||||
Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled
|
Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled
|
||||||
proportionally to maintain its aspect ratio.
|
proportionally to maintain its aspect ratio.
|
||||||
|
|
||||||
|
@ -1878,13 +1883,13 @@ class PolygonROI(ROI):
|
||||||
sc['angle'] = self.state['angle']
|
sc['angle'] = self.state['angle']
|
||||||
return sc
|
return sc
|
||||||
|
|
||||||
|
|
||||||
class PolyLineROI(ROI):
|
class PolyLineROI(ROI):
|
||||||
"""
|
r"""
|
||||||
Container class for multiple connected LineSegmentROIs.
|
Container class for multiple connected LineSegmentROIs.
|
||||||
|
|
||||||
This class allows the user to draw paths of multiple line segments.
|
This class allows the user to draw paths of multiple line segments.
|
||||||
|
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
positions (list of length-2 sequences) The list of points in the path.
|
positions (list of length-2 sequences) The list of points in the path.
|
||||||
|
@ -2076,9 +2081,9 @@ class PolyLineROI(ROI):
|
||||||
|
|
||||||
|
|
||||||
class LineSegmentROI(ROI):
|
class LineSegmentROI(ROI):
|
||||||
"""
|
r"""
|
||||||
ROI subclass with two freely-moving handles defining a line.
|
ROI subclass with two freely-moving handles defining a line.
|
||||||
|
|
||||||
============== =============================================================
|
============== =============================================================
|
||||||
**Arguments**
|
**Arguments**
|
||||||
positions (list of two length-2 sequences) The endpoints of the line
|
positions (list of two length-2 sequences) The endpoints of the line
|
||||||
|
|
|
@ -834,8 +834,8 @@ class ScatterPlotItem(GraphicsObject):
|
||||||
pts = self.pointsAt(ev.pos())
|
pts = self.pointsAt(ev.pos())
|
||||||
if len(pts) > 0:
|
if len(pts) > 0:
|
||||||
self.ptsClicked = pts
|
self.ptsClicked = pts
|
||||||
self.sigClicked.emit(self, self.ptsClicked)
|
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
self.sigClicked.emit(self, self.ptsClicked)
|
||||||
else:
|
else:
|
||||||
#print "no spots"
|
#print "no spots"
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -31,7 +31,6 @@ def init_viewbox():
|
||||||
|
|
||||||
g = pg.GridItem()
|
g = pg.GridItem()
|
||||||
vb.addItem(g)
|
vb.addItem(g)
|
||||||
|
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
|
|
||||||
def test_ViewBox():
|
def test_ViewBox():
|
||||||
|
|
30
pyqtgraph/graphicsItems/tests/test_AxisItem.py
Normal file
30
pyqtgraph/graphicsItems/tests/test_AxisItem.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
def test_AxisItem_stopAxisAtTick(monkeypatch):
|
||||||
|
def test_bottom(p, axisSpec, tickSpecs, textSpecs):
|
||||||
|
assert view.mapToView(axisSpec[1]).x() == 0.25
|
||||||
|
assert view.mapToView(axisSpec[2]).x() == 0.75
|
||||||
|
|
||||||
|
def test_left(p, axisSpec, tickSpecs, textSpecs):
|
||||||
|
assert view.mapToView(axisSpec[1]).y() == 0.875
|
||||||
|
assert view.mapToView(axisSpec[2]).y() == 0.125
|
||||||
|
|
||||||
|
plot = pg.PlotWidget()
|
||||||
|
view = plot.plotItem.getViewBox()
|
||||||
|
bottom = plot.getAxis("bottom")
|
||||||
|
bottom.setRange(0, 1)
|
||||||
|
bticks = [(0.25, "a"), (0.6, "b"), (0.75, "c")]
|
||||||
|
bottom.setTicks([bticks, bticks])
|
||||||
|
bottom.setStyle(stopAxisAtTick=(True, True))
|
||||||
|
monkeypatch.setattr(bottom, "drawPicture", test_bottom)
|
||||||
|
|
||||||
|
left = plot.getAxis("left")
|
||||||
|
lticks = [(0.125, "a"), (0.55, "b"), (0.875, "c")]
|
||||||
|
left.setTicks([lticks, lticks])
|
||||||
|
left.setRange(0, 1)
|
||||||
|
left.setStyle(stopAxisAtTick=(True, True))
|
||||||
|
monkeypatch.setattr(left, "drawPicture", test_left)
|
||||||
|
|
||||||
|
plot.show()
|
37
pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py
Normal file
37
pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import pyqtgraph as pg
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
app = pg.mkQApp()
|
||||||
|
|
||||||
|
|
||||||
|
def test_ErrorBarItem_defer_data():
|
||||||
|
plot = pg.PlotWidget()
|
||||||
|
plot.show()
|
||||||
|
|
||||||
|
# plot some data away from the origin to set the view rect
|
||||||
|
x = np.arange(5) + 10
|
||||||
|
curve = pg.PlotCurveItem(x=x, y=x)
|
||||||
|
plot.addItem(curve)
|
||||||
|
app.processEvents()
|
||||||
|
r_no_ebi = plot.viewRect()
|
||||||
|
|
||||||
|
# ErrorBarItem with no data shouldn't affect the view rect
|
||||||
|
err = pg.ErrorBarItem()
|
||||||
|
plot.addItem(err)
|
||||||
|
app.processEvents()
|
||||||
|
r_empty_ebi = plot.viewRect()
|
||||||
|
|
||||||
|
assert r_no_ebi == r_empty_ebi
|
||||||
|
|
||||||
|
err.setData(x=x, y=x, bottom=x, top=x)
|
||||||
|
app.processEvents()
|
||||||
|
r_ebi = plot.viewRect()
|
||||||
|
|
||||||
|
assert r_empty_ebi != r_ebi
|
||||||
|
|
||||||
|
# unset data, ErrorBarItem disappears and view rect goes back to original
|
||||||
|
err.setData(x=None, y=None)
|
||||||
|
app.processEvents()
|
||||||
|
r_clear_ebi = plot.viewRect()
|
||||||
|
|
||||||
|
assert r_clear_ebi == r_no_ebi
|
|
@ -9,16 +9,16 @@ def test_fft():
|
||||||
x = np.linspace(0, 1, 1000)
|
x = np.linspace(0, 1, 1000)
|
||||||
y = np.sin(2 * np.pi * f * x)
|
y = np.sin(2 * np.pi * f * x)
|
||||||
pd = pg.PlotDataItem(x, y)
|
pd = pg.PlotDataItem(x, y)
|
||||||
pd.setFftMode(True)
|
pd.setFftMode(True)
|
||||||
x, y = pd.getData()
|
x, y = pd.getData()
|
||||||
assert abs(x[np.argmax(y)] - f) < 0.03
|
assert abs(x[np.argmax(y)] - f) < 0.03
|
||||||
|
|
||||||
x = np.linspace(0, 1, 1001)
|
x = np.linspace(0, 1, 1001)
|
||||||
y = np.sin(2 * np.pi * f * x)
|
y = np.sin(2 * np.pi * f * x)
|
||||||
pd.setData(x, y)
|
pd.setData(x, y)
|
||||||
x, y = pd.getData()
|
x, y = pd.getData()
|
||||||
assert abs(x[np.argmax(y)]- f) < 0.03
|
assert abs(x[np.argmax(y)]- f) < 0.03
|
||||||
|
|
||||||
pd.setLogMode(True, False)
|
pd.setLogMode(True, False)
|
||||||
x, y = pd.getData()
|
x, y = pd.getData()
|
||||||
assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01
|
assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01
|
||||||
|
@ -58,3 +58,9 @@ def test_clear():
|
||||||
|
|
||||||
assert pdi.xData == None
|
assert pdi.xData == None
|
||||||
assert pdi.yData == None
|
assert pdi.yData == None
|
||||||
|
|
||||||
|
def test_clear_in_step_mode():
|
||||||
|
w = pg.PlotWidget()
|
||||||
|
c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True)
|
||||||
|
w.addItem(c)
|
||||||
|
c.clear()
|
||||||
|
|
|
@ -633,7 +633,7 @@ class ImageView(QtGui.QWidget):
|
||||||
ax = np.argmax(data.shape)
|
ax = np.argmax(data.shape)
|
||||||
sl = [slice(None)] * data.ndim
|
sl = [slice(None)] * data.ndim
|
||||||
sl[ax] = slice(None, None, 2)
|
sl[ax] = slice(None, None, 2)
|
||||||
data = data[sl]
|
data = data[tuple(sl)]
|
||||||
|
|
||||||
cax = self.axes['c']
|
cax = self.axes['c']
|
||||||
if cax is None:
|
if cax is None:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os, time, sys, traceback, weakref
|
import os, time, sys, traceback, weakref
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import threading
|
import threading
|
||||||
|
import warnings
|
||||||
try:
|
try:
|
||||||
import __builtin__ as builtins
|
import __builtin__ as builtins
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
|
@ -21,6 +22,9 @@ class NoResultError(Exception):
|
||||||
because the call has not yet returned."""
|
because the call has not yet returned."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class RemoteExceptionWarning(UserWarning):
|
||||||
|
"""Emitted when a request to a remote object results in an Exception """
|
||||||
|
pass
|
||||||
|
|
||||||
class RemoteEventHandler(object):
|
class RemoteEventHandler(object):
|
||||||
"""
|
"""
|
||||||
|
@ -499,9 +503,9 @@ class RemoteEventHandler(object):
|
||||||
#print ''.join(result)
|
#print ''.join(result)
|
||||||
exc, excStr = result
|
exc, excStr = result
|
||||||
if exc is not None:
|
if exc is not None:
|
||||||
print("===== Remote process raised exception on request: =====")
|
warnings.warn("===== Remote process raised exception on request: =====", RemoteExceptionWarning)
|
||||||
print(''.join(excStr))
|
warnings.warn(''.join(excStr), RemoteExceptionWarning)
|
||||||
print("===== Local Traceback to request follows: =====")
|
warnings.warn("===== Local Traceback to request follows: =====", RemoteExceptionWarning)
|
||||||
raise exc
|
raise exc
|
||||||
else:
|
else:
|
||||||
print(''.join(excStr))
|
print(''.join(excStr))
|
||||||
|
|
|
@ -236,6 +236,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||||
glPopMatrix()
|
glPopMatrix()
|
||||||
|
|
||||||
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
|
def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None):
|
||||||
|
if pos is not None:
|
||||||
|
self.opts['center'] = pos
|
||||||
if distance is not None:
|
if distance is not None:
|
||||||
self.opts['distance'] = distance
|
self.opts['distance'] = distance
|
||||||
if elevation is not None:
|
if elevation is not None:
|
||||||
|
@ -427,7 +429,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||||
ver = glGetString(GL_VERSION).split()[0]
|
ver = glGetString(GL_VERSION).split()[0]
|
||||||
if int(ver.split('.')[0]) < 2:
|
if int(ver.split('.')[0]) < 2:
|
||||||
from .. import debug
|
from .. import debug
|
||||||
pyqtgraph.debug.printExc()
|
debug.printExc()
|
||||||
raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
|
raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QtOpenGL.QGLWidget.__init__(self)
|
QtOpenGL.QGLWidget.__init__(self)
|
||||||
self.makeCurrent()
|
self.makeCurrent()
|
||||||
print("GL version:" + glGetString(GL_VERSION))
|
print("GL version:" + glGetString(GL_VERSION).decode("utf-8"))
|
||||||
print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE))
|
print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE))
|
||||||
print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE))
|
print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE))
|
||||||
print("Extensions: " + glGetString(GL_EXTENSIONS))
|
print("Extensions: " + glGetString(GL_EXTENSIONS).decode("utf-8").replace(" ", "\n"))
|
||||||
|
|
||||||
GLTest()
|
GLTest()
|
||||||
|
|
||||||
|
|
|
@ -559,8 +559,8 @@ class Parameter(QtCore.QObject):
|
||||||
self.childs.insert(pos, child)
|
self.childs.insert(pos, child)
|
||||||
|
|
||||||
child.parentChanged(self)
|
child.parentChanged(self)
|
||||||
self.sigChildAdded.emit(self, child, pos)
|
|
||||||
child.sigTreeStateChanged.connect(self.treeStateChanged)
|
child.sigTreeStateChanged.connect(self.treeStateChanged)
|
||||||
|
self.sigChildAdded.emit(self, child, pos)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
def removeChild(self, child):
|
def removeChild(self, child):
|
||||||
|
@ -571,11 +571,11 @@ class Parameter(QtCore.QObject):
|
||||||
del self.names[name]
|
del self.names[name]
|
||||||
self.childs.pop(self.childs.index(child))
|
self.childs.pop(self.childs.index(child))
|
||||||
child.parentChanged(None)
|
child.parentChanged(None)
|
||||||
self.sigChildRemoved.emit(self, child)
|
|
||||||
try:
|
try:
|
||||||
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
|
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
|
||||||
except (TypeError, RuntimeError): ## already disconnected
|
except (TypeError, RuntimeError): ## already disconnected
|
||||||
pass
|
pass
|
||||||
|
self.sigChildRemoved.emit(self, child)
|
||||||
|
|
||||||
def clearChildren(self):
|
def clearChildren(self):
|
||||||
"""Remove all child parameters."""
|
"""Remove all child parameters."""
|
||||||
|
@ -612,7 +612,7 @@ class Parameter(QtCore.QObject):
|
||||||
|
|
||||||
def incrementName(self, name):
|
def incrementName(self, name):
|
||||||
## return an unused name by adding a number to the name given
|
## return an unused name by adding a number to the name given
|
||||||
base, num = re.match('(.*)(\d*)', name).groups()
|
base, num = re.match(r'(.*)(\d*)', name).groups()
|
||||||
numLen = len(num)
|
numLen = len(num)
|
||||||
if numLen == 0:
|
if numLen == 0:
|
||||||
num = 2
|
num = 2
|
||||||
|
|
|
@ -10,15 +10,22 @@ Includes:
|
||||||
- ThreadsafeDict, ThreadsafeList - Self-mutexed data structures
|
- ThreadsafeDict, ThreadsafeList - Self-mutexed data structures
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import threading, sys, copy, collections
|
import threading
|
||||||
#from debug import *
|
import sys
|
||||||
|
import copy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# fallback: try to use the ordereddict backport when using python 2.6
|
# fallback: try to use the ordereddict backport when using python 2.6
|
||||||
from ordereddict import OrderedDict
|
from ordereddict import OrderedDict
|
||||||
|
|
||||||
|
try:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
except ImportError:
|
||||||
|
# fallback for python < 3.3
|
||||||
|
from collections import Sequence
|
||||||
|
|
||||||
|
|
||||||
class ReverseDict(dict):
|
class ReverseDict(dict):
|
||||||
"""extends dict so that reverse lookups are possible by requesting the key as a list of length 1:
|
"""extends dict so that reverse lookups are possible by requesting the key as a list of length 1:
|
||||||
|
@ -326,7 +333,7 @@ class ProtectedDict(dict):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProtectedList(collections.Sequence):
|
class ProtectedList(Sequence):
|
||||||
"""
|
"""
|
||||||
A class allowing read-only 'view' of a list or dict.
|
A class allowing read-only 'view' of a list or dict.
|
||||||
The object can be treated like a normal list, but will never modify the original list it points to.
|
The object can be treated like a normal list, but will never modify the original list it points to.
|
||||||
|
@ -408,7 +415,7 @@ class ProtectedList(collections.Sequence):
|
||||||
raise Exception("This is a list. It does not poop.")
|
raise Exception("This is a list. It does not poop.")
|
||||||
|
|
||||||
|
|
||||||
class ProtectedTuple(collections.Sequence):
|
class ProtectedTuple(Sequence):
|
||||||
"""
|
"""
|
||||||
A class allowing read-only 'view' of a tuple.
|
A class allowing read-only 'view' of a tuple.
|
||||||
The object can be treated like a normal tuple, but its contents will be returned as protected objects.
|
The object can be treated like a normal tuple, but its contents will be returned as protected objects.
|
||||||
|
|
|
@ -47,7 +47,7 @@ def reloadAll(prefix=None, debug=False):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
## Ignore if the file name does not start with prefix
|
## Ignore if the file name does not start with prefix
|
||||||
if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']:
|
if not hasattr(mod, '__file__') or mod.__file__ is None or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']:
|
||||||
continue
|
continue
|
||||||
if prefix is not None and mod.__file__[:len(prefix)] != prefix:
|
if prefix is not None and mod.__file__[:len(prefix)] != prefix:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||||
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
|
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
|
||||||
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
|
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
||||||
if stdFileName in gitStatus(dataPath):
|
if stdFileName in gitStatus(dataPath):
|
||||||
print("\n\nWARNING: unit test failed against modified standard "
|
print("\n\nWARNING: unit test failed against modified standard "
|
||||||
"image %s.\nTo revert this file, run `cd %s; git checkout "
|
"image %s.\nTo revert this file, run `cd %s; git checkout "
|
||||||
|
@ -210,6 +211,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||||
"PYQTGRAPH_AUDIT=1 to add this image." % stdFileName)
|
"PYQTGRAPH_AUDIT=1 to add this image." % stdFileName)
|
||||||
else:
|
else:
|
||||||
if os.getenv('TRAVIS') is not None:
|
if os.getenv('TRAVIS') is not None:
|
||||||
|
saveFailedTest(image, stdImage, standardFile, upload=True)
|
||||||
|
elif os.getenv('AZURE') is not None:
|
||||||
|
standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile)
|
||||||
saveFailedTest(image, stdImage, standardFile)
|
saveFailedTest(image, stdImage, standardFile)
|
||||||
print(graphstate)
|
print(graphstate)
|
||||||
raise
|
raise
|
||||||
|
@ -253,7 +257,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
||||||
assert im1.dtype == im2.dtype
|
assert im1.dtype == im2.dtype
|
||||||
|
|
||||||
if pxCount == -1:
|
if pxCount == -1:
|
||||||
if QT_LIB == 'PyQt5':
|
if QT_LIB in {'PyQt5', 'PySide2'}:
|
||||||
# Qt5 generates slightly different results; relax the tolerance
|
# Qt5 generates slightly different results; relax the tolerance
|
||||||
# until test images are updated.
|
# until test images are updated.
|
||||||
pxCount = int(im1.shape[0] * im1.shape[1] * 0.01)
|
pxCount = int(im1.shape[0] * im1.shape[1] * 0.01)
|
||||||
|
@ -281,15 +285,9 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
|
||||||
assert corr >= minCorr
|
assert corr >= minCorr
|
||||||
|
|
||||||
|
|
||||||
def saveFailedTest(data, expect, filename):
|
def saveFailedTest(data, expect, filename, upload=False):
|
||||||
"""Upload failed test images to web server to allow CI test debugging.
|
"""Upload failed test images to web server to allow CI test debugging.
|
||||||
"""
|
"""
|
||||||
commit = runSubprocess(['git', 'rev-parse', 'HEAD'])
|
|
||||||
name = filename.split('/')
|
|
||||||
name.insert(-1, commit.strip())
|
|
||||||
filename = '/'.join(name)
|
|
||||||
host = 'data.pyqtgraph.org'
|
|
||||||
|
|
||||||
# concatenate data, expect, and diff into a single image
|
# concatenate data, expect, and diff into a single image
|
||||||
ds = data.shape
|
ds = data.shape
|
||||||
es = expect.shape
|
es = expect.shape
|
||||||
|
@ -306,15 +304,31 @@ def saveFailedTest(data, expect, filename):
|
||||||
img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
|
img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff
|
||||||
|
|
||||||
png = makePng(img)
|
png = makePng(img)
|
||||||
|
directory = os.path.dirname(filename)
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
with open(filename + ".png", "wb") as png_file:
|
||||||
|
png_file.write(png)
|
||||||
|
print("\nImage comparison failed. Test result: %s %s Expected result: "
|
||||||
|
"%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype))
|
||||||
|
if upload:
|
||||||
|
uploadFailedTest(filename, png)
|
||||||
|
|
||||||
|
|
||||||
|
def uploadFailedTest(filename, png):
|
||||||
|
commit = runSubprocess(['git', 'rev-parse', 'HEAD'])
|
||||||
|
name = filename.split(os.path.sep)
|
||||||
|
name.insert(-1, commit.strip())
|
||||||
|
filename = os.path.sep.join(name)
|
||||||
|
|
||||||
|
host = 'data.pyqtgraph.org'
|
||||||
conn = httplib.HTTPConnection(host)
|
conn = httplib.HTTPConnection(host)
|
||||||
req = urllib.urlencode({'name': filename,
|
req = urllib.urlencode({'name': filename,
|
||||||
'data': base64.b64encode(png)})
|
'data': base64.b64encode(png)})
|
||||||
conn.request('POST', '/upload.py', req)
|
conn.request('POST', '/upload.py', req)
|
||||||
response = conn.getresponse().read()
|
response = conn.getresponse().read()
|
||||||
conn.close()
|
conn.close()
|
||||||
print("\nImage comparison failed. Test result: %s %s Expected result: "
|
|
||||||
"%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype))
|
|
||||||
print("Uploaded to: \nhttp://%s/data/%s" % (host, filename))
|
print("Uploaded to: \nhttp://%s/data/%s" % (host, filename))
|
||||||
if not response.startswith(b'OK'):
|
if not response.startswith(b'OK'):
|
||||||
print("WARNING: Error uploading data to %s" % host)
|
print("WARNING: Error uploading data to %s" % host)
|
||||||
|
@ -495,7 +509,7 @@ def getTestDataRepo():
|
||||||
if not os.path.isdir(parentPath):
|
if not os.path.isdir(parentPath):
|
||||||
os.makedirs(parentPath)
|
os.makedirs(parentPath)
|
||||||
|
|
||||||
if os.getenv('TRAVIS') is not None:
|
if os.getenv('TRAVIS') is not None or os.getenv('AZURE') is not None:
|
||||||
# Create a shallow clone of the test-data repository (to avoid
|
# Create a shallow clone of the test-data repository (to avoid
|
||||||
# downloading more data than is necessary)
|
# downloading more data than is necessary)
|
||||||
os.makedirs(dataPath)
|
os.makedirs(dataPath)
|
||||||
|
|
36
pyqtgraph/tests/test_configparser.py
Normal file
36
pyqtgraph/tests/test_configparser.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
from pyqtgraph import configfile
|
||||||
|
import numpy as np
|
||||||
|
import tempfile, os
|
||||||
|
|
||||||
|
def test_longArrays():
|
||||||
|
"""
|
||||||
|
Test config saving and loading of long arrays.
|
||||||
|
"""
|
||||||
|
tmp = tempfile.mktemp(".cfg")
|
||||||
|
|
||||||
|
arr = np.arange(20)
|
||||||
|
configfile.writeConfigFile({'arr':arr}, tmp)
|
||||||
|
config = configfile.readConfigFile(tmp)
|
||||||
|
|
||||||
|
assert all(config['arr'] == arr)
|
||||||
|
|
||||||
|
os.remove(tmp)
|
||||||
|
|
||||||
|
def test_multipleParameters():
|
||||||
|
"""
|
||||||
|
Test config saving and loading of multiple parameters.
|
||||||
|
"""
|
||||||
|
tmp = tempfile.mktemp(".cfg")
|
||||||
|
|
||||||
|
par1 = [1,2,3]
|
||||||
|
par2 = "Test"
|
||||||
|
par3 = {'a':3,'b':'c'}
|
||||||
|
|
||||||
|
configfile.writeConfigFile({'par1':par1, 'par2':par2, 'par3':par3}, tmp)
|
||||||
|
config = configfile.readConfigFile(tmp)
|
||||||
|
|
||||||
|
assert config['par1'] == par1
|
||||||
|
assert config['par2'] == par2
|
||||||
|
assert config['par3'] == par3
|
||||||
|
|
||||||
|
os.remove(tmp)
|
|
@ -10,7 +10,6 @@ def test_isQObjectAlive():
|
||||||
o2 = pg.QtCore.QObject()
|
o2 = pg.QtCore.QObject()
|
||||||
o2.setParent(o1)
|
o2.setParent(o1)
|
||||||
del o1
|
del o1
|
||||||
gc.collect()
|
|
||||||
assert not pg.Qt.isQObjectAlive(o2)
|
assert not pg.Qt.isQObjectAlive(o2)
|
||||||
|
|
||||||
@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be '
|
@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be '
|
||||||
|
|
|
@ -4,6 +4,7 @@ import pyqtgraph.reload
|
||||||
|
|
||||||
|
|
||||||
pgpath = os.path.join(os.path.dirname(pg.__file__), '..')
|
pgpath = os.path.join(os.path.dirname(pg.__file__), '..')
|
||||||
|
pgpath_repr = repr(pgpath)
|
||||||
|
|
||||||
# make temporary directory to write module code
|
# make temporary directory to write module code
|
||||||
path = None
|
path = None
|
||||||
|
@ -22,7 +23,7 @@ def teardown_module():
|
||||||
|
|
||||||
code = """
|
code = """
|
||||||
import sys
|
import sys
|
||||||
sys.path.append('{path}')
|
sys.path.append({path_repr})
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ def test_reload():
|
||||||
# write a module
|
# write a module
|
||||||
mod = os.path.join(path, 'reload_test_mod.py')
|
mod = os.path.join(path, 'reload_test_mod.py')
|
||||||
print("\nRELOAD FILE:", mod)
|
print("\nRELOAD FILE:", mod)
|
||||||
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1"))
|
open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version1"))
|
||||||
|
|
||||||
# import the new module
|
# import the new module
|
||||||
import reload_test_mod
|
import reload_test_mod
|
||||||
|
@ -63,7 +64,7 @@ def test_reload():
|
||||||
|
|
||||||
|
|
||||||
# write again and reload
|
# write again and reload
|
||||||
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
|
open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2"))
|
||||||
remove_cache(mod)
|
remove_cache(mod)
|
||||||
pg.reload.reloadAll(path, debug=True)
|
pg.reload.reloadAll(path, debug=True)
|
||||||
if py3:
|
if py3:
|
||||||
|
@ -87,7 +88,7 @@ def test_reload():
|
||||||
|
|
||||||
|
|
||||||
# write again and reload
|
# write again and reload
|
||||||
open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2"))
|
open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2"))
|
||||||
remove_cache(mod)
|
remove_cache(mod)
|
||||||
pg.reload.reloadAll(path, debug=True)
|
pg.reload.reloadAll(path, debug=True)
|
||||||
if py3:
|
if py3:
|
||||||
|
|
15
pyqtgraph/util/get_resolution.py
Normal file
15
pyqtgraph/util/get_resolution.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from .. import mkQApp
|
||||||
|
|
||||||
|
def test_screenInformation():
|
||||||
|
qApp = mkQApp()
|
||||||
|
desktop = qApp.desktop()
|
||||||
|
resolution = desktop.screenGeometry()
|
||||||
|
availableResolution = desktop.availableGeometry()
|
||||||
|
print("Screen resolution: {}x{}".format(resolution.width(), resolution.height()))
|
||||||
|
print("Available geometry: {}x{}".format(availableResolution.width(), availableResolution.height()))
|
||||||
|
print("Number of Screens: {}".format(desktop.screenCount()))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_screenInformation()
|
|
@ -50,11 +50,11 @@ class ColorButton(QtGui.QPushButton):
|
||||||
def setColor(self, color, finished=True):
|
def setColor(self, color, finished=True):
|
||||||
"""Sets the button's color and emits both sigColorChanged and sigColorChanging."""
|
"""Sets the button's color and emits both sigColorChanged and sigColorChanging."""
|
||||||
self._color = functions.mkColor(color)
|
self._color = functions.mkColor(color)
|
||||||
|
self.update()
|
||||||
if finished:
|
if finished:
|
||||||
self.sigColorChanged.emit(self)
|
self.sigColorChanged.emit(self)
|
||||||
else:
|
else:
|
||||||
self.sigColorChanging.emit(self)
|
self.sigColorChanging.emit(self)
|
||||||
self.update()
|
|
||||||
|
|
||||||
def selectColor(self):
|
def selectColor(self):
|
||||||
self.origColor = self.color()
|
self.origColor = self.color()
|
||||||
|
|
|
@ -227,12 +227,12 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||||
else:
|
else:
|
||||||
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
||||||
|
|
||||||
self.sigDeviceRangeChanged.emit(self, self.range)
|
|
||||||
self.sigDeviceTransformChanged.emit(self)
|
|
||||||
|
|
||||||
if propagate:
|
if propagate:
|
||||||
for v in self.lockedViewports:
|
for v in self.lockedViewports:
|
||||||
v.setXRange(self.range, padding=0)
|
v.setXRange(self.range, padding=0)
|
||||||
|
|
||||||
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||||
|
self.sigDeviceTransformChanged.emit(self)
|
||||||
|
|
||||||
def viewRect(self):
|
def viewRect(self):
|
||||||
"""Return the boundaries of the view in scene coordinates"""
|
"""Return the boundaries of the view in scene coordinates"""
|
||||||
|
@ -262,7 +262,6 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||||
h = self.range.height() / scale[1]
|
h = self.range.height() / scale[1]
|
||||||
self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h)
|
self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h)
|
||||||
|
|
||||||
|
|
||||||
self.updateMatrix()
|
self.updateMatrix()
|
||||||
self.sigScaleChanged.emit(self)
|
self.sigScaleChanged.emit(self)
|
||||||
|
|
||||||
|
@ -362,7 +361,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||||
def mouseMoveEvent(self, ev):
|
def mouseMoveEvent(self, ev):
|
||||||
if self.lastMousePos is None:
|
if self.lastMousePos is None:
|
||||||
self.lastMousePos = Point(ev.pos())
|
self.lastMousePos = Point(ev.pos())
|
||||||
delta = Point(ev.pos() - self.lastMousePos)
|
delta = Point(ev.pos() - QtCore.QPoint(*self.lastMousePos))
|
||||||
self.lastMousePos = Point(ev.pos())
|
self.lastMousePos = Point(ev.pos())
|
||||||
|
|
||||||
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
|
||||||
|
|
|
@ -39,7 +39,7 @@ class LayoutWidget(QtGui.QWidget):
|
||||||
Returns the created widget.
|
Returns the created widget.
|
||||||
"""
|
"""
|
||||||
text = QtGui.QLabel(text, **kargs)
|
text = QtGui.QLabel(text, **kargs)
|
||||||
self.addItem(text, row, col, rowspan, colspan)
|
self.addWidget(text, row, col, rowspan, colspan)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
|
def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
|
||||||
|
@ -49,7 +49,7 @@ class LayoutWidget(QtGui.QWidget):
|
||||||
Returns the created widget.
|
Returns the created widget.
|
||||||
"""
|
"""
|
||||||
layout = LayoutWidget(**kargs)
|
layout = LayoutWidget(**kargs)
|
||||||
self.addItem(layout, row, col, rowspan, colspan)
|
self.addWidget(layout, row, col, rowspan, colspan)
|
||||||
return layout
|
return layout
|
||||||
|
|
||||||
def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1):
|
def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1):
|
||||||
|
@ -75,7 +75,7 @@ class LayoutWidget(QtGui.QWidget):
|
||||||
|
|
||||||
def getWidget(self, row, col):
|
def getWidget(self, row, col):
|
||||||
"""Return the widget in (*row*, *col*)"""
|
"""Return the widget in (*row*, *col*)"""
|
||||||
return self.row[row][col]
|
return self.rows[row][col]
|
||||||
|
|
||||||
#def itemIndex(self, item):
|
#def itemIndex(self, item):
|
||||||
#for i in range(self.layout.count()):
|
#for i in range(self.layout.count()):
|
||||||
|
|
|
@ -201,7 +201,7 @@ class TreeWidget(QtGui.QTreeWidget):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def topLevelItems(self):
|
def topLevelItems(self):
|
||||||
return map(self.topLevelItem, xrange(self.topLevelItemCount()))
|
return [self.topLevelItem(i) for i in range(self.topLevelItemCount())]
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
items = self.topLevelItems()
|
items = self.topLevelItems()
|
||||||
|
|
15
pytest.ini
Normal file
15
pytest.ini
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[pytest]
|
||||||
|
xvfb_width = 1920
|
||||||
|
xvfb_height = 1080
|
||||||
|
# use this due to some issues with ndarray reshape errors on CI systems
|
||||||
|
xvfb_colordepth = 24
|
||||||
|
xvfb_args=-ac +extension GLX +render
|
||||||
|
addopts = --faulthandler-timeout=15
|
||||||
|
|
||||||
|
filterwarnings =
|
||||||
|
# comfortable skipping these warnings runtime warnings
|
||||||
|
# https://stackoverflow.com/questions/40845304/runtimewarning-numpy-dtype-size-changed-may-indicate-binary-incompatibility
|
||||||
|
ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning
|
||||||
|
# Warnings generated from PyQt5.9
|
||||||
|
ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning
|
||||||
|
ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning
|
6
test.py
6
test.py
|
@ -15,10 +15,10 @@ elif '--pyqt4' in args:
|
||||||
elif '--pyqt5' in args:
|
elif '--pyqt5' in args:
|
||||||
args.remove('--pyqt5')
|
args.remove('--pyqt5')
|
||||||
import PyQt5
|
import PyQt5
|
||||||
|
elif '--pyside2' in args:
|
||||||
|
args.remove('--pyside2')
|
||||||
|
import PySide2
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
pg.systemInfo()
|
pg.systemInfo()
|
||||||
|
|
||||||
pytest.main(args)
|
pytest.main(args)
|
||||||
|
|
||||||
|
|
51
tox.ini
Normal file
51
tox.ini
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
[tox]
|
||||||
|
envlist =
|
||||||
|
; qt 5.12.x
|
||||||
|
py{27,37}-pyside2-pip
|
||||||
|
py{35,37}-pyqt5-pip
|
||||||
|
|
||||||
|
; qt 5.9.7
|
||||||
|
py{27,37}-pyqt5-conda
|
||||||
|
py{27,37}-pyside2-conda
|
||||||
|
|
||||||
|
; qt 5.6.2
|
||||||
|
py35-pyqt5-conda
|
||||||
|
; consider dropping support...
|
||||||
|
; py35-pyside2-conda
|
||||||
|
|
||||||
|
; qt 4.8.7
|
||||||
|
py{27,36}-pyqt4-conda
|
||||||
|
py{27,36}-pyside-conda
|
||||||
|
|
||||||
|
|
||||||
|
[base]
|
||||||
|
deps =
|
||||||
|
pytest
|
||||||
|
numpy
|
||||||
|
scipy
|
||||||
|
pyopengl
|
||||||
|
flake8
|
||||||
|
six
|
||||||
|
coverage
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
passenv = DISPLAY XAUTHORITY
|
||||||
|
deps=
|
||||||
|
{[base]deps}
|
||||||
|
pytest-cov
|
||||||
|
pytest-xdist
|
||||||
|
pytest-faulthandler
|
||||||
|
pyside2-pip: pyside2
|
||||||
|
pyqt5-pip: pyqt5
|
||||||
|
|
||||||
|
conda_deps=
|
||||||
|
pyside2-conda: pyside2
|
||||||
|
pyside-conda: pyside
|
||||||
|
pyqt5-conda: pyqt
|
||||||
|
pyqt4-conda: pyqt=4
|
||||||
|
|
||||||
|
conda_channels=
|
||||||
|
conda-forge
|
||||||
|
commands=
|
||||||
|
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||||
|
pytest {posargs:.}
|
Loading…
Reference in New Issue
Block a user