Merge remote-tracking branch 'upstream/develop' into patch-3

This commit is contained in:
Sam Schott 2019-06-24 13:54:08 +01:00
commit 262004e2ec
56 changed files with 1493 additions and 965 deletions

View File

@ -9,21 +9,17 @@ sudo: false
notifications:
email: false
virtualenv:
system_site_packages: true
env:
# Enable python 2 and python 3 builds
# Note that the 2.6 build doesn't get flake8, and runs old versions of
# Pyglet and GLFW to make sure we deal with those correctly
#- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended
# Note that the python 2.6 support ended.
- PYTHON=2.7 QT=pyqt4 TEST=extra
- PYTHON=2.7 QT=pyside 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.2 QT=pyqt5 TEST=standard
services:
- xvfb
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
@ -67,11 +63,7 @@ install:
fi;
- pip install pytest-xdist # multi-thread py.test
- pip install pytest-cov # add coverage stats
# required for example testing on python 2.6
- if [ "${PYTHON}" == "2.6" ]; then
pip install importlib;
fi;
- pip install pytest-faulthandler # activate faulthandler
# Debugging helpers
- uname -a
@ -85,7 +77,6 @@ install:
before_script:
# We need to create a (fake) display on Travis, let's use a funny resolution
- 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
# Make sure everyone uses the correct python (this is handled by conda)

69
CONTRIBUTING.md Normal file
View 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)

View File

@ -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.

View File

@ -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
=========
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>
@ -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
2D display, and OpenGL for 3D display.
Requirements
------------
* PyQt 4.7+, PySide, PyQt5, or PySide2
* python 2.7, or 3.x
* NumPy
* For 3D graphics: pyopengl and qt-opengl
* Known to run on Windows, Linux, and Mac.
* PyQt 4.8+, PySide, PyQt5, or PySide2
* python 2.7, or 3.x
* Required
* `numpy`, `scipy`
* 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
-------
* 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)
* 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)
Installation Methods
--------------------
* From pypi:
- Last released version: `pip install pyqtgraph`
- Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph`
* From PyPI:
* Last released version: `pip install 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`
* Many linux package repositories have release versions.
* 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)
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 official documentation lives at http://pyqtgraph.org/documentation

38
azure-pipelines.yml Normal file
View 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
View 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'

View File

@ -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
all platforms.
* To get access to the very latest features and bugfixes, clone pyqtgraph from
github::
* To get access to the very latest features and bugfixes you have three choice::
1. Clone pyqtgraph from github::
$ 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
..or 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.
2. Directly install from GitHub repo::
$ 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:
* **Anaconda**: ``conda install pyqtgraph``

View File

@ -92,10 +92,10 @@ def updateRoiPlot(roi, data=None):
rois = []
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.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.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)))
## Add each ROI to the scene and link its data to a plot curve with the same color

View File

@ -9,8 +9,8 @@ import subprocess
from pyqtgraph.python2_3 import basestring
from pyqtgraph.Qt import QtGui, QT_LIB
from .utils import buildFileList, path, examples
from .utils import buildFileList, testFile, path, examples
if QT_LIB == 'PySide':
from .exampleLoaderTemplate_pyside import Ui_Form
@ -117,30 +117,7 @@ class ExampleLoader(QtGui.QMainWindow):
def run():
app = QtGui.QApplication([])
loader = ExampleLoader()
app.exec_()
if __name__ == '__main__':
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()
run()

View File

@ -1,11 +1,19 @@
from __future__ import print_function, division, absolute_import
from pyqtgraph import Qt
from . import utils
from collections import namedtuple
import errno
import importlib
import itertools
import pkgutil
import pytest
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.
# as a workaround, we overwrite the built-in print function (bleh)
if os.getenv('TRAVIS') is not None:
@ -32,17 +40,8 @@ if os.getenv('TRAVIS') is not None:
print("Installed wrapper for flaky print.")
# apparently importlib does not exist in python 2.6...
try:
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}
files = sorted(set(utils.buildFileList(utils.examples)))
frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False}
# sort out which of the front ends are available
for frontend in frontends.keys():
try:
@ -51,15 +50,99 @@ for frontend in frontends.keys():
except ImportError:
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(
"frontend, f", itertools.product(sorted(list(frontends.keys())), files))
def test_examples(frontend, f):
# Test the examples with all available front-ends
print('frontend = %s. f = %s' % (frontend, f))
if not frontends[frontend]:
pytest.skip('%s is not installed. Skipping tests' % frontend)
utils.testFile(f[0], f[1], utils.sys.executable, frontend)
"frontend, f",
[
pytest.param(
frontend,
f,
marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False,
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__":
pytest.cmdline.main()

View File

@ -1,9 +1,5 @@
from __future__ import division, print_function, absolute_import
import subprocess
import time
import os
import sys
import errno
from pyqtgraph.pgcollections import OrderedDict
from pyqtgraph.python2_3 import basestring
@ -86,7 +82,6 @@ examples = OrderedDict([
#('VerticalLabel', '../widgets/VerticalLabel.py'),
('JoystickButton', 'JoystickButton.py'),
])),
('Flowcharts', 'Flowchart.py'),
('Custom Flowchart Nodes', 'FlowchartCustomNode.py'),
])
@ -103,73 +98,3 @@ def buildFileList(examples, files=None):
else:
buildFileList(val, 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')

View File

@ -263,7 +263,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
for item in prevItems:
event.currentItem = item
try:
item.hoverEvent(event)
if item.scene() is self:
item.hoverEvent(event)
except:
debug.printExc("Error sending hover exit event:")
finally:
@ -288,7 +289,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
else:
acceptedItem = None
if acceptedItem is not None:
if acceptedItem is not None and acceptedItem.scene() is self:
#print "Drag -> pre-selected item:", acceptedItem
self.dragItem = acceptedItem
event.currentItem = self.dragItem
@ -435,6 +436,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
for item in items:
if hoverable and not hasattr(item, 'hoverEvent'):
continue
if item.scene() is not self:
continue
shape = item.shape() # Note: default shape() returns boundingRect()
if shape is None:
continue

View File

@ -100,10 +100,13 @@ def _loadUiType(uiFile):
how to make PyQt4 and pyside look the same...
http://stackoverflow.com/a/8717832
"""
import pysideuic
if QT_LIB == "PYSIDE":
import pysideuic
else:
import pyside2uic as pysideuic
import xml.etree.ElementTree as xml
#from io import StringIO
parsed = xml.parse(uiFile)
widget_class = parsed.find('widget').get('class')
form_class = parsed.find('class').text
@ -216,8 +219,12 @@ elif QT_LIB == PYSIDE2:
except ImportError as err:
QtTest = FailedImport(err)
isQObjectAlive = _isQObjectAlive
try:
import shiboken2
isQObjectAlive = shiboken2.isValid
except ImportError:
# use approximate version
isQObjectAlive = _isQObjectAlive
import PySide2
VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__

View File

@ -67,11 +67,11 @@ class SignalProxy(QtCore.QObject):
"""If there is a signal queued up, send it now."""
if self.args is None or self.block:
return False
#self.emit(self.signal, *self.args)
self.sigDelayed.emit(self.args)
self.args = None
args, self.args = self.args, None
self.timer.stop()
self.lastFlushTime = time()
#self.emit(self.signal, *self.args)
self.sigDelayed.emit(args)
return True
def disconnect(self):

View File

@ -141,7 +141,7 @@ class ColorMap(object):
pos, color = self.getStops(mode=self.BYTE)
color = [QtGui.QColor(*x) for x in color]
g.setStops(zip(pos, color))
g.setStops(list(zip(pos, color)))
#if self.colorMode == 'rgb':
#ticks = self.listTicks()

View File

@ -33,9 +33,8 @@ class ParseError(Exception):
msg = "Error parsing string at line %d:\n" % self.lineNum
else:
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
#raise Exception()
def writeConfigFile(data, fname):
@ -93,13 +92,14 @@ def genString(data, indent=''):
s += indent + sk + ':\n'
s += genString(data[k], indent + ' ')
else:
s += indent + sk + ': ' + repr(data[k]) + '\n'
s += indent + sk + ': ' + repr(data[k]).replace("\n", "\\\n") + '\n'
return s
def parseString(lines, start=0):
data = OrderedDict()
if isinstance(lines, basestring):
lines = lines.replace("\\\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

View File

@ -346,9 +346,9 @@ class DockLabel(VerticalLabel):
ev.accept()
def mouseReleaseEvent(self, ev):
ev.accept()
if not self.startedDrag:
self.sigClicked.emit(self, ev)
ev.accept()
def mouseDoubleClickEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:

View File

@ -124,5 +124,4 @@ class MatplotlibWindow(QtGui.QMainWindow):
def closeEvent(self, ev):
MatplotlibExporter.windows.remove(self)
self.deleteLater()

View File

@ -190,12 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}):
## this is taken care of in generateSvg instead.
#if hasattr(item, 'setExportMode'):
#item.setExportMode(False)
if QT_LIB in ['PySide', 'PySide2']:
xmlStr = str(arr)
else:
xmlStr = bytes(arr).decode('utf-8')
doc = xml.parseString(xmlStr.encode('utf-8'))
doc = xml.parseString(arr.data())
try:
## Get top-level group for this item

View File

@ -1,5 +1,5 @@
"""
SVG export test
CSV export test
"""
from __future__ import division, print_function, absolute_import
import pyqtgraph as pg
@ -33,8 +33,9 @@ def test_CSVExporter():
ex = pg.exporters.CSVExporter(plt.plotItem)
ex.export(fileName=tempfilename)
r = csv.reader(open(tempfilename, 'r'))
lines = [line for line in r]
with open(tempfilename, 'r') as csv_file:
r = csv.reader(csv_file)
lines = [line for line in r]
header = lines.pop(0)
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']

View File

@ -27,6 +27,7 @@ from .. import configfile as configfile
from .. import dockarea as dockarea
from . import FlowchartGraphicsView
from .. import functions as fn
from ..python2_3 import asUnicode
def strDict(d):
return dict([(str(k), v) for k, v in d.items()])
@ -502,8 +503,8 @@ class Flowchart(Node):
finally:
self.blockSignals(False)
self.sigChartLoaded.emit()
self.outputChanged()
self.sigChartLoaded.emit()
self.sigStateChanged.emit()
def loadFile(self, fileName=None, startDir=None):
@ -519,12 +520,12 @@ class Flowchart(Node):
self.fileDialog.fileSelected.connect(self.loadFile)
return
## 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)
self.restoreState(state, clear=True)
self.viewBox.autoRange()
self.sigFileLoaded.emit(fileName)
def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'):
"""Save this flowchart to a .fc file
"""
@ -534,11 +535,12 @@ class Flowchart(Node):
if startDir is None:
startDir = '.'
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
self.fileDialog.setDefaultSuffix("fc")
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.saveFile)
return
fileName = unicode(fileName)
fileName = asUnicode(fileName)
configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName)
@ -662,7 +664,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def fileSaved(self, fileName):
self.setCurrentFile(unicode(fileName))
self.setCurrentFile(asUnicode(fileName))
self.ui.saveBtn.success("Saved.")
def saveClicked(self):
@ -691,7 +693,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def setCurrentFile(self, fileName):
self.currentFileName = unicode(fileName)
self.currentFileName = asUnicode(fileName)
if fileName is None:
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
else:

View File

@ -373,7 +373,7 @@ class Node(QtCore.QObject):
pos = self.graphicsItem().pos()
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
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
if termsEditable:
state['terminals'] = self.saveTerminals()

View File

@ -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')
if not isinstance(levels, np.ndarray):
levels = np.array(levels)
levels = levels.astype(np.float)
if levels.ndim == 1:
if levels.shape[0] != 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]):
minVal, maxVal = levels[i]
if minVal == maxVal:
maxVal += 1e-16
maxVal = np.nextafter(maxVal, 2*maxVal)
rng = maxVal-minVal
rng = 1 if rng == 0 else rng
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
if minVal != 0 or maxVal != scale:
if minVal == maxVal:
maxVal += 1e-16
maxVal = np.nextafter(maxVal, 2*maxVal)
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
@ -1380,7 +1381,7 @@ def gaussianFilter(data, sigma):
# clip off extra data
sl = [slice(None)] * data.ndim
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
filtered = filtered[sl]
filtered = filtered[tuple(sl)]
return filtered + baseline

View File

@ -14,10 +14,10 @@ class AxisItem(GraphicsWidget):
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.
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:**
@ -26,11 +26,19 @@ class AxisItem(GraphicsWidget):
into the plot, positive values draw outward.
linkView (ViewBox) causes the range of values displayed in the axis
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.
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)
self.label = QtGui.QGraphicsTextItem(self)
self.picture = None
@ -39,15 +47,15 @@ class AxisItem(GraphicsWidget):
raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
if orientation in ['left', 'right']:
self.label.rotate(-90)
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
'tickTextHeight': 18,
'tickTextHeight': 18,
'autoExpandTextSpace': True, ## automatically expand text space if needed
'tickFont': None,
'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.
'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.
(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
(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,
'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
# If the user specifies a width / height, remember that setting
# indefinitely.
self.fixedWidth = None
self.fixedHeight = None
self.labelText = ''
self.labelUnits = ''
self.labelUnitPrefix=''
self.labelStyle = {}
self.labelText = text
self.labelUnits = units
self.labelUnitPrefix = unitPrefix
self.labelStyle = args
self.logMode = False
self.tickFont = None
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
self._tickSpacing = None # used to override default tickSpacing method
self.scale = 1.0
self.autoSIPrefix = True
self.autoSIPrefixScale = 1.0
self.showLabel(False)
self.setRange(0, 1)
if pen is None:
self.setPen()
else:
self.setPen(pen)
self._linkedView = None
if linkView is not None:
self.linkToView(linkView)
self.showLabel(False)
self.grid = False
#self.setCacheMode(self.DeviceCoordinateCache)
def setStyle(self, **kwds):
"""
Set various style options.
=================== =======================================================
Keyword Arguments:
tickLength (int) The maximum length of ticks in pixels.
Positive values point toward the text; negative
tickLength (int) The maximum length of ticks in pixels.
Positive values point toward the text; negative
values point away.
tickTextOffset (int) reserved spacing between text and axis in px
tickTextWidth (int) Horizontal 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
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.
stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis
line is drawn only as far as the last tick.
Otherwise, the line is drawn to the edge of the
stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis
line is drawn only as far as the last tick.
Otherwise, the line is drawn to the edge of the
AxisItem boundary.
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
the list specifies what fraction of the axis length
may be occupied by text, given the number of ticks
that already have text displayed. For example::
[(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
(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
(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
showValues (bool) indicates whether text is displayed adjacent
to ticks.
=================== =======================================================
Added in version 0.9.9
"""
for kwd,value in kwds.items():
if kwd not in self.style:
raise NameError("%s is not a valid style argument." % kwd)
if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'):
if not isinstance(value, int):
raise ValueError("Argument '%s' must be int" % kwd)
if kwd == 'tickTextOffset':
if self.orientation in ('left', 'right'):
self.style['tickTextOffset'][0] = value
@ -158,19 +166,19 @@ class AxisItem(GraphicsWidget):
self.style[kwd] = value
else:
self.style[kwd] = value
self.picture = None
self._adjustSize()
self.update()
def close(self):
self.scene().removeItem(self.label)
self.label = None
self.scene().removeItem(self)
def setGrid(self, grid):
"""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
the extent of the linked ViewBox, if any.
"""
@ -178,28 +186,28 @@ class AxisItem(GraphicsWidget):
self.picture = None
self.prepareGeometryChange()
self.update()
def setLogMode(self, log):
"""
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>`)
"""
self.logMode = log
self.picture = None
self.update()
def setTickFont(self, font):
self.tickFont = font
self.picture = None
self.prepareGeometryChange()
## Need to re-allocate space depending on font size?
self.update()
def resizeEvent(self, ev=None):
#s = self.size()
## Set the position of the label
nudge = 5
br = self.label.boundingRect()
@ -218,7 +226,7 @@ class AxisItem(GraphicsWidget):
p.setY(int(self.size().height()-br.height()+nudge))
self.label.setPos(p)
self.picture = None
def showLabel(self, show=True):
"""Show/hide the label text for this axis."""
#self.drawLabel = show
@ -229,10 +237,10 @@ class AxisItem(GraphicsWidget):
self._updateHeight()
if self.autoSIPrefix:
self.updateAutoSIPrefix()
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
"""Set the text displayed adjacent to the axis.
============== =============================================================
**Arguments:**
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
the <span> tag which will surround the axis label and units.
============== =============================================================
The final text generated for the label will look like::
<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::
labelStyle = {'color': '#FFF', 'font-size': '14pt'}
axis.setLabel('label text', units='V', **labelStyle)
"""
show_label = False
if text is not None:
self.labelText = text
self.showLabel()
show_label = True
if units is not None:
self.labelUnits = units
show_label = True
if show_label:
self.showLabel()
if unitPrefix is not None:
self.labelUnitPrefix = unitPrefix
@ -270,7 +281,7 @@ class AxisItem(GraphicsWidget):
self._adjustSize()
self.picture = None
self.update()
def labelString(self):
if self.labelUnits == '':
if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0:
@ -280,13 +291,13 @@ class AxisItem(GraphicsWidget):
else:
#print repr(self.labelUnitPrefix), repr(self.labelUnits)
units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits))
s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units))
style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
return asUnicode("<span style='%s'>%s</span>") % (style, asUnicode(s))
def _updateMaxTextSize(self, x):
## Informs that the maximum tick size orthogonal to the axis has
## 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:
self._updateHeight()
#return True ## size has changed
def _adjustSize(self):
if self.orientation in ['left', 'right']:
self._updateWidth()
else:
self._updateHeight()
def setHeight(self, h=None):
"""Set the height of this axis reserved for ticks and tick labels.
The height of the axis label is automatically added.
If *height* is None, then the value will be determined automatically
based on the size of the tick text."""
self.fixedHeight = h
self._updateHeight()
def _updateHeight(self):
if not self.isVisible():
h = 0
@ -338,20 +349,20 @@ class AxisItem(GraphicsWidget):
h += self.label.boundingRect().height() * 0.8
else:
h = self.fixedHeight
self.setMaximumHeight(h)
self.setMinimumHeight(h)
self.picture = None
def setWidth(self, w=None):
"""Set the width of this axis reserved for ticks and tick labels.
The width of the axis label is automatically added.
If *width* is None, then the value will be determined automatically
based on the size of the tick text."""
self.fixedWidth = w
self._updateWidth()
def _updateWidth(self):
if not self.isVisible():
w = 0
@ -369,20 +380,20 @@ class AxisItem(GraphicsWidget):
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
else:
w = self.fixedWidth
self.setMaximumWidth(w)
self.setMinimumWidth(w)
self.picture = None
def pen(self):
if self._pen is None:
return fn.mkPen(getConfigOption('foreground'))
return fn.mkPen(self._pen)
def setPen(self, *args, **kwargs):
"""
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>`).
"""
self.picture = None
@ -393,44 +404,44 @@ class AxisItem(GraphicsWidget):
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
self.setLabel()
self.update()
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
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.
"""
# Deprecated usage, kept for backward compatibility
if scale is None:
if scale is None:
scale = 1.0
self.enableAutoSIPrefix(True)
if scale != self.scale:
self.scale = scale
self.setLabel()
self.picture = None
self.update()
def enableAutoSIPrefix(self, enable=True):
"""
Enable (or disable) automatic SI prefix scaling on this axis.
When enabled, this feature automatically determines the best SI prefix
Enable (or disable) automatic SI prefix scaling on this axis.
When enabled, this feature automatically determines the best SI prefix
to prepend to the label units, while ensuring that axis values are scaled
accordingly.
For example, if the axis spans values from -0.1 to 0.1 and has units set
accordingly.
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
and the units would appear as 'mV'
This feature is enabled by default, and is only available when a suffix
(unit string) is provided to display on the label.
"""
self.autoSIPrefix = enable
self.updateAutoSIPrefix()
def updateAutoSIPrefix(self):
if self.label.isVisible():
(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)
else:
scale = 1.0
self.autoSIPrefixScale = scale
self.picture = None
self.update()
def setRange(self, mn, mx):
"""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>`"""
@ -456,14 +467,14 @@ class AxisItem(GraphicsWidget):
self.updateAutoSIPrefix()
self.picture = None
self.update()
def linkedView(self):
"""Return the ViewBox this axis is linked to"""
if self._linkedView is None:
return None
else:
return self._linkedView()
def linkToView(self, view):
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
oldView = self.linkedView()
@ -476,11 +487,11 @@ class AxisItem(GraphicsWidget):
if oldView is not None:
oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
view.sigXRangeChanged.connect(self.linkedViewChanged)
if oldView is not None:
oldView.sigResized.disconnect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def linkedViewChanged(self, view, newRange=None):
if self.orientation in ['right', 'left']:
if newRange is None:
@ -496,7 +507,7 @@ class AxisItem(GraphicsWidget):
self.setRange(*newRange[::-1])
else:
self.setRange(*newRange)
def boundingRect(self):
linkedView = self.linkedView()
if linkedView is None or self.grid is False:
@ -515,7 +526,7 @@ class AxisItem(GraphicsWidget):
return rect
else:
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
def paint(self, p, opt, widget):
profiler = debug.Profiler()
if self.picture is None:
@ -544,26 +555,26 @@ class AxisItem(GraphicsWidget):
[ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ],
...
]
If *ticks* is None, then the default tick system will be used instead.
"""
self._tickLevels = ticks
self.picture = None
self.update()
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
the effect of setTicks(). Arguments may be either *major* and *minor*,
or *levels* which is a list of (spacing, offset) tuples for each
the effect of setTicks(). Arguments may be either *major* and *minor*,
or *levels* which is a list of (spacing, offset) tuples for each
tick level desired.
If no arguments are given, then the default behavior of tickSpacing
is enabled.
Examples::
# two levels, all offsets = 0
axis.setTickSpacing(5, 1)
# three levels, all offsets = 0
@ -571,7 +582,7 @@ class AxisItem(GraphicsWidget):
# reset to default
axis.setTickSpacing()
"""
if levels is None:
if major is None:
levels = None
@ -580,16 +591,16 @@ class AxisItem(GraphicsWidget):
self._tickSpacing = levels
self.picture = None
self.update()
def tickSpacing(self, minVal, maxVal, size):
"""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.
The return value must be a list of tuples, one for each set of ticks::
[
(major tick spacing, offset),
(minor tick spacing, offset),
@ -600,41 +611,40 @@ class AxisItem(GraphicsWidget):
# First check for override tick spacing
if self._tickSpacing is not None:
return self._tickSpacing
dif = abs(maxVal - minVal)
if dif == 0:
return []
## decide optimal minor tick spacing in pixels (this is just aesthetics)
optimalTickCount = max(2., np.log(size))
## optimal minor tick spacing
## optimal minor tick spacing
optimalSpacing = dif / optimalTickCount
## the largest power-of-10 spacing which is smaller than optimal
p10unit = 10 ** np.floor(np.log10(optimalSpacing))
## Determine major/minor tick spacings which flank the optimal spacing.
intervals = np.array([1., 2., 10., 20., 100.]) * p10unit
minorIndex = 0
while intervals[minorIndex+1] <= optimalSpacing:
minorIndex += 1
levels = [
(intervals[minorIndex+2], 0),
(intervals[minorIndex+1], 0),
#(intervals[minorIndex], 0) ## Pretty, but eats up CPU
]
if self.style['maxTickLevel'] >= 2:
## decide whether to include the last level of ticks
minSpacing = min(size / 20., 30.)
maxTickCount = size / minSpacing
if dif / intervals[minorIndex] <= maxTickCount:
levels.append((intervals[minorIndex], 0))
return levels
return levels
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
### Determine major/minor tick spacings which flank the optimal spacing.
@ -642,7 +652,7 @@ class AxisItem(GraphicsWidget):
#minorIndex = 0
#while intervals[minorIndex+1] <= optimalSpacing:
#minorIndex += 1
### make sure we never see 5 and 2 at the same time
#intIndexes = [
#[0,1,3],
@ -651,42 +661,42 @@ class AxisItem(GraphicsWidget):
#[3,4,6],
#[3,5,6],
#][minorIndex]
#return [
#(intervals[intIndexes[2]], 0),
#(intervals[intIndexes[1]], 0),
#(intervals[intIndexes[0]], 0)
#]
def tickValues(self, minVal, maxVal, size):
"""
Return the values and spacing of ticks to draw::
[
(spacing, [major ticks]),
(spacing, [minor ticks]),
...
[
(spacing, [major ticks]),
(spacing, [minor ticks]),
...
]
By default, this method calls tickSpacing to determine the correct tick locations.
This is a good method to override in subclasses.
"""
minVal, maxVal = sorted((minVal, maxVal))
minVal *= self.scale
minVal *= self.scale
maxVal *= self.scale
#size *= self.scale
ticks = []
tickLevels = self.tickSpacing(minVal, maxVal, size)
allValues = np.array([])
for i in range(len(tickLevels)):
spacing, offset = tickLevels[i]
## determine starting tick
start = (np.ceil((minVal-offset) / spacing) * spacing) + offset
## determine number of ticks
num = int((maxVal-start) / spacing) + 1
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))
allValues = np.concatenate([allValues, values])
ticks.append((spacing/self.scale, values))
if self.logMode:
return self.logTickValues(minVal, maxVal, size, ticks)
#nticks = []
#for t in ticks:
#nvals = []
@ -708,24 +718,24 @@ class AxisItem(GraphicsWidget):
#nvals.append(v/self.scale)
#nticks.append((t[0]/self.scale,nvals))
#ticks = nticks
return ticks
def logTickValues(self, minVal, maxVal, size, stdTicks):
## start with the tick spacing given by tickValues().
## Any level whose spacing is < 1 needs to be converted to log scale
ticks = []
for (spacing, t) in stdTicks:
if spacing >= 1.0:
ticks.append((spacing, t))
if len(ticks) < 3:
v1 = int(np.floor(minVal))
v2 = int(np.ceil(maxVal))
#major = list(range(v1+1, v2))
minor = []
for v in range(v1, v2):
minor.extend(v + np.log10(np.arange(1, 10)))
@ -734,21 +744,21 @@ class AxisItem(GraphicsWidget):
return ticks
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.
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
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
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.
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
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.
"""
if self.logMode:
return self.logTickStrings(values, scale, spacing)
places = max(0, np.ceil(-np.log10(spacing*scale)))
strings = []
for v in values:
@ -759,27 +769,27 @@ class AxisItem(GraphicsWidget):
vstr = ("%%0.%df" % places) % vs
strings.append(vstr)
return strings
def logTickStrings(self, values, scale, spacing):
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
def generateDrawSpecs(self, p):
"""
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().
"""
profiler = debug.Profiler()
#bounds = self.boundingRect()
bounds = self.mapRectFromParent(self.geometry())
linkedView = self.linkedView()
if linkedView is None or self.grid is False:
tickBounds = bounds
else:
tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
if self.orientation == 'left':
span = (bounds.topRight(), bounds.bottomRight())
tickStart = tickBounds.right()
@ -805,7 +815,7 @@ class AxisItem(GraphicsWidget):
tickDir = 1
axis = 1
#print tickStart, tickStop, span
## determine size of this item in pixels
points = list(map(self.mapToDevice, span))
if None in points:
@ -830,7 +840,7 @@ class AxisItem(GraphicsWidget):
for val, strn in level:
values.append(val)
strings.append(strn)
## determine mapping between tick values and local coordinates
dif = self.range[1] - self.range[0]
if dif == 0:
@ -843,29 +853,29 @@ class AxisItem(GraphicsWidget):
else:
xScale = bounds.width() / dif
offset = self.range[0] * xScale
xRange = [x * xScale - offset for x in self.range]
xMin = min(xRange)
xMax = max(xRange)
profiler('init')
tickPositions = [] # remembers positions of previously drawn ticks
## compute coordinates to draw ticks
## draw three different intervals, long ticks first
tickSpecs = []
for i in range(len(tickLevels)):
tickPositions.append([])
ticks = tickLevels[i][1]
## length of tick
tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
lineAlpha = 255 / (i+1)
if self.grid is not False:
lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.)
for v in ticks:
## determine actual position to draw this tick
x = (v * xScale) - offset
@ -873,7 +883,7 @@ class AxisItem(GraphicsWidget):
tickPositions[i].append(None)
continue
tickPositions[i].append(x)
p1 = [x, x]
p2 = [x, x]
p1[axis] = tickStart
@ -887,22 +897,26 @@ class AxisItem(GraphicsWidget):
tickSpecs.append((tickPen, Point(p1), Point(p2)))
profiler('compute ticks')
if self.style['stopAxisAtTick'][0] is True:
stop = max(span[0].y(), min(map(min, tickPositions)))
minTickPosition = min(map(min, tickPositions))
if axis == 0:
stop = max(span[0].y(), minTickPosition)
span[0].setY(stop)
else:
stop = max(span[0].x(), minTickPosition)
span[0].setX(stop)
if self.style['stopAxisAtTick'][1] is True:
stop = min(span[1].y(), max(map(max, tickPositions)))
maxTickPosition = max(map(max, tickPositions))
if axis == 0:
stop = min(span[1].y(), maxTickPosition)
span[1].setY(stop)
else:
stop = min(span[1].x(), maxTickPosition)
span[1].setX(stop)
axisSpec = (self.pen(), span[0], span[1])
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
#if self.style['autoExpandTextSpace'] is True:
#textWidth = self.textWidth
@ -910,15 +924,15 @@ class AxisItem(GraphicsWidget):
#else:
#textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text
#textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text
textSize2 = 0
textRects = []
textSpecs = [] ## list of draw
# If values are hidden, return early
if not self.style['showValues']:
return (axisSpec, tickSpecs, textSpecs)
for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
## Get the list of strings to display for this level
if tickStrings is None:
@ -926,10 +940,10 @@ class AxisItem(GraphicsWidget):
strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing)
else:
strings = tickStrings[i]
if len(strings) == 0:
continue
## ignore strings belonging to ticks that were previously ignored
for j in range(len(strings)):
if tickPositions[i][j] is None:
@ -945,10 +959,10 @@ class AxisItem(GraphicsWidget):
## boundingRect is usually just a bit too large
## (but this probably depends on per-font metrics?)
br.setHeight(br.height() * 0.8)
rects.append(br)
textRects.append(rects[-1])
if len(textRects) > 0:
## measure all text, make sure there's enough room
if axis == 0:
@ -973,7 +987,7 @@ class AxisItem(GraphicsWidget):
break
if finished:
break
#spacing, values = tickLevels[best]
#strings = self.tickStrings(values, self.scale, spacing)
# Determine exactly where tick text should be drawn
@ -1006,24 +1020,24 @@ class AxisItem(GraphicsWidget):
#p.drawText(rect, textFlags, vstr)
textSpecs.append((rect, textFlags, vstr))
profiler('compute text')
## update max text size if needed.
self._updateMaxTextSize(textSize2)
return (axisSpec, tickSpecs, textSpecs)
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
profiler = debug.Profiler()
p.setRenderHint(p.Antialiasing, False)
p.setRenderHint(p.TextAntialiasing, True)
## draw long line along axis
pen, p1, p2 = axisSpec
p.setPen(pen)
p.drawLine(p1, p2)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
## draw ticks
for pen, p1, p2 in tickSpecs:
p.setPen(pen)
@ -1045,7 +1059,7 @@ class AxisItem(GraphicsWidget):
self._updateWidth()
else:
self._updateHeight()
def hide(self):
GraphicsWidget.hide(self)
if self.orientation in ['left', 'right']:
@ -1054,23 +1068,23 @@ class AxisItem(GraphicsWidget):
self._updateHeight()
def wheelEvent(self, ev):
if self.linkedView() is None:
if self.linkedView() is None:
return
if self.orientation in ['left', 'right']:
self.linkedView().wheelEvent(ev, axis=1)
else:
self.linkedView().wheelEvent(ev, axis=0)
ev.accept()
def mouseDragEvent(self, event):
if self.linkedView() is None:
if self.linkedView() is None:
return
if self.orientation in ['left', 'right']:
return self.linkedView().mouseDragEvent(event, axis=1)
else:
return self.linkedView().mouseDragEvent(event, axis=0)
def mouseClickEvent(self, event):
if self.linkedView() is None:
if self.linkedView() is None:
return
return self.linkedView().mouseClickEvent(event)

View File

@ -23,6 +23,7 @@ class ErrorBarItem(GraphicsObject):
beam=None,
pen=None
)
self.setVisible(False)
self.setData(**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.
"""
self.opts.update(opts)
self.setVisible(all(self.opts[ax] is not None for ax in ['x', 'y']))
self.path = None
self.update()
self.prepareGeometryChange()
@ -59,6 +61,7 @@ class ErrorBarItem(GraphicsObject):
x, y = self.opts['x'], self.opts['y']
if x is None or y is None:
self.path = p
return
beam = self.opts['beam']
@ -146,4 +149,4 @@ class ErrorBarItem(GraphicsObject):
self.drawPath()
return self.path.boundingRect()

View File

@ -186,7 +186,7 @@ class HistogramLUTItem(GraphicsWidget):
"""Return a lookup table from the color gradient defined by this
HistogramLUTItem.
"""
if self.levelMode is not 'mono':
if self.levelMode != 'mono':
return None
if n is None:
if img.dtype == np.uint8:
@ -205,8 +205,8 @@ class HistogramLUTItem(GraphicsWidget):
def regionChanging(self):
if self.imageItem() is not None:
self.imageItem().setLevels(self.getLevels())
self.sigLevelsChanged.emit(self)
self.update()
self.sigLevelsChanged.emit(self)
def imageChanged(self, autoLevel=False, autoRange=False):
if self.imageItem() is None:

View File

@ -2,13 +2,17 @@ from __future__ import division
from ..Qt import QtGui, QtCore
import numpy as np
import collections
from .. import functions as fn
from .. import debug as debug
from .GraphicsObject import GraphicsObject
from ..Point import Point
from .. import getConfigOption
try:
from collections.abc import Callable
except ImportError:
# fallback for python < 3.3
from collections import Callable
__all__ = ['ImageItem']
@ -16,23 +20,23 @@ __all__ = ['ImageItem']
class ImageItem(GraphicsObject):
"""
**Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
GraphicsObject displaying an image. Optimized for rapid update (ie video display).
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
with a lookup table (see :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`)
before being displayed.
ImageItem is frequently used in conjunction with
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
ImageItem is frequently used in conjunction with
:class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or
:class:`HistogramLUTWidget <pyqtgraph.HistogramLUTWidget>` to provide a GUI
for controlling the levels and lookup table used to display the image.
"""
sigImageChanged = QtCore.Signal()
sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu
def __init__(self, image=None, **kargs):
"""
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
@ -41,23 +45,23 @@ class ImageItem(GraphicsObject):
self.menu = None
self.image = None ## original image data
self.qimage = None ## rendered image for display
self.paintMode = None
self.levels = None ## [min, max] or [[redMin, redMax], ...]
self.lut = None
self.autoDownsample = False
self.axisOrder = getConfigOption('imageAxisOrder')
# In some cases, we use a modified lookup table to handle both rescaling
# and LUT more efficiently
self._effectiveLut = None
self.drawKernel = None
self.border = None
self.removable = False
if image is not None:
self.setImage(image, **kargs)
else:
@ -66,32 +70,32 @@ class ImageItem(GraphicsObject):
def setCompositionMode(self, mode):
"""Change the composition mode of the item (see QPainter::CompositionMode
in the Qt documentation). This is useful when overlaying multiple ImageItems.
============================================ ============================================================
**Most common arguments:**
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
is opaque. Otherwise, it uses the alpha channel to blend
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.
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.
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
============================================ ============================================================
"""
self.paintMode = mode
self.update()
def setBorder(self, b):
self.border = fn.mkPen(b)
self.update()
def width(self):
if self.image is None:
return None
axis = 0 if self.axisOrder == 'col-major' else 1
return self.image.shape[axis]
def height(self):
if self.image is None:
return None
@ -111,10 +115,10 @@ class ImageItem(GraphicsObject):
def setLevels(self, levels, update=True):
"""
Set image scaling levels. Can be one of:
* [blackLevel, whiteLevel]
* [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>`
for more details on how levels are applied.
"""
@ -125,18 +129,18 @@ class ImageItem(GraphicsObject):
self._effectiveLut = None
if update:
self.updateImage()
def getLevels(self):
return self.levels
#return self.whiteLevel, self.blackLevel
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).
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.
Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>`
or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`.
"""
@ -149,7 +153,7 @@ class ImageItem(GraphicsObject):
def setAutoDownsample(self, ads):
"""
Set the automatic downsampling mode for this ImageItem.
Added in version 0.9.9
"""
self.autoDownsample = ads
@ -198,44 +202,44 @@ class ImageItem(GraphicsObject):
"""
Update the image displayed by this item. For more information on how the image
is processed before displaying, see :func:`makeARGB <pyqtgraph.makeARGB>`
================= =========================================================================
**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
point of any bit depth. For 3D arrays, the third dimension must
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.
By default, this argument is true unless the levels argument is
given.
lut (numpy array) The color lookup table to use when displaying the image.
See :func:`setLookupTable <pyqtgraph.ImageItem.setLookupTable>`.
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.
opacity (float 0.0-1.0)
compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
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
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
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).
However, most image data is stored in row-major order (row, column) and will need to be
transposed before calling setImage()::
imageitem.setImage(imagedata.T)
This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or
by changing the ``imageAxisOrder`` :ref:`global configuration option <apiref_config>`.
"""
profile = debug.Profiler()
@ -292,7 +296,7 @@ class ImageItem(GraphicsObject):
def dataTransform(self):
"""Return the transform that maps from this image's input array to its
local coordinate system.
This transform corrects for the transposition that occurs when image data
is interpreted in row-major order.
"""
@ -307,7 +311,7 @@ class ImageItem(GraphicsObject):
def inverseDataTransform(self):
"""Return the transform that maps from this image's local coordinate
system to its input array.
See dataTransform() for more information.
"""
tr = QtGui.QTransform()
@ -339,7 +343,7 @@ class ImageItem(GraphicsObject):
def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image.
## can we make any assumptions here that speed things up?
## dtype, range, size are all the same?
defaults = {
@ -350,14 +354,14 @@ class ImageItem(GraphicsObject):
def render(self):
# Convert data to QImage for display.
profile = debug.Profiler()
if self.image is None or self.image.size == 0:
return
# Request a lookup table if this image has only one channel
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)
else:
lut = self.lut
@ -385,7 +389,7 @@ class ImageItem(GraphicsObject):
image = fn.downsample(self.image, xds, axis=axes[0])
image = fn.downsample(image, yds, axis=axes[1])
self._lastDownsample = (xds, yds)
# Check if downsampling reduced the image size to zero due to inf values.
if image.size == 0:
return
@ -403,27 +407,27 @@ class ImageItem(GraphicsObject):
levdiff = maxlev - minlev
levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0
if lut is None:
efflut = fn.rescaleData(ind, scale=255./levdiff,
efflut = fn.rescaleData(ind, scale=255./levdiff,
offset=minlev, dtype=np.ubyte)
else:
lutdtype = np.min_scalar_type(lut.shape[0]-1)
efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff,
offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1))
efflut = lut[efflut]
self._effectiveLut = efflut
lut = self._effectiveLut
levels = None
# Convert single-channel image to 2D array
if image.ndim == 3 and image.shape[-1] == 1:
image = image[..., 0]
# Assume images are in column-major order for backward compatibility
# (most images are in row-major order)
if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim])
argb, alpha = fn.makeARGB(image, lut=lut, levels=levels)
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
@ -453,26 +457,26 @@ class ImageItem(GraphicsObject):
self.render()
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):
"""Returns x and y arrays containing the histogram values for the current image.
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.
If *step* is 'auto', then a step is chosen such that the analyzed data has
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
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.
* All other types will have *targetHistogramSize* bins.
If *perChannel* is True, then the histogram is computed once per channel
and the output is a list of the results.
This method is also used when automatically computing levels.
"""
if self.image is None or self.image.size == 0:
@ -483,10 +487,13 @@ class ImageItem(GraphicsObject):
if np.isscalar(step):
step = (step, step)
stepData = self.image[::step[0], ::step[1]]
if bins == 'auto':
if 'auto' == bins:
mn = np.nanmin(stepData)
mx = np.nanmax(stepData)
if mx == mn:
# degenerate image, arange will fail
mx += 1
if np.isnan(mn) or np.isnan(mx):
# the data are all-nan
return None, None
@ -497,7 +504,7 @@ class ImageItem(GraphicsObject):
else:
# for float data, let numpy select the bins.
bins = np.linspace(mn, mx, 500)
if len(bins) == 0:
bins = [mn, mx]
@ -524,7 +531,7 @@ class ImageItem(GraphicsObject):
(see GraphicsItem::ItemIgnoresTransformations in the Qt documentation)
"""
self.setFlag(self.ItemIgnoresTransformations, b)
def setScaledMode(self):
self.setPxMode(False)
@ -534,14 +541,14 @@ class ImageItem(GraphicsObject):
if self.qimage is None:
return None
return QtGui.QPixmap.fromImage(self.qimage)
def pixelSize(self):
"""return scene-size of a single pixel in the image"""
br = self.sceneBoundingRect()
if self.image is None:
return 1,1
return br.width()/self.width(), br.height()/self.height()
def viewTransformChanged(self):
if self.autoDownsample:
self.qimage = None
@ -582,7 +589,7 @@ class ImageItem(GraphicsObject):
self.menu.addAction(remAct)
self.menu.remAct = remAct
return self.menu
def hoverEvent(self, ev):
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.
@ -595,7 +602,7 @@ class ImageItem(GraphicsObject):
#print(ev.device())
#print(ev.pointerType())
#print(ev.pressure())
def drawAt(self, pos, ev=None):
pos = [int(pos.x()), int(pos.y())]
dk = self.drawKernel
@ -604,7 +611,7 @@ class ImageItem(GraphicsObject):
sy = [0,dk.shape[1]]
tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]]
ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]]
for i in [0,1]:
dx1 = -min(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]))
mask = self.drawMask
src = dk
if isinstance(self.drawMode, collections.Callable):
if isinstance(self.drawMode, Callable):
self.drawMode(dk, self.image, mask, ss, ts, ev)
else:
src = src[ss]
@ -636,7 +643,7 @@ class ImageItem(GraphicsObject):
else:
raise Exception("Unknown draw mode '%s'" % self.drawMode)
self.updateImage()
def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'):
self.drawKernel = kernel
self.drawKernelCenter = center

View File

@ -4,7 +4,7 @@ try:
HAVE_OPENGL = True
except:
HAVE_OPENGL = False
import numpy as np
from .GraphicsObject import GraphicsObject
from .. import functions as fn
@ -15,51 +15,50 @@ from .. import debug
__all__ = ['PlotCurveItem']
class PlotCurveItem(GraphicsObject):
"""
Class representing a single plot curve. Instances of this class are created
automatically as part of PlotDataItem; these rarely need to be instantiated
directly.
Features:
- Fast data update
- Fill under curve
- Mouse interaction
==================== ===============================================
**Signals:**
sigPlotChanged(self) Emitted when the data being plotted has changed
sigClicked(self) Emitted when the curve is clicked
==================== ===============================================
"""
sigPlotChanged = QtCore.Signal(object)
sigClicked = QtCore.Signal(object)
def __init__(self, *args, **kargs):
"""
Forwards all arguments to :func:`setData <pyqtgraph.PlotCurveItem.setData>`.
Some extra arguments are accepted as well:
============== =======================================================
**Arguments:**
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.
============== =======================================================
"""
GraphicsObject.__init__(self, kargs.get('parent', None))
self.clear()
## this is disastrous for performance.
#self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
self.metaData = {}
self.opts = {
'pen': fn.mkPen('w'),
'shadowPen': None,
'fillLevel': None,
'brush': None,
@ -70,21 +69,23 @@ class PlotCurveItem(GraphicsObject):
'mouseWidth': 8, # width of shape responding to mouse click
'compositionMode': None,
}
if 'pen' not in kargs:
self.opts['pen'] = fn.mkPen('w')
self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs)
def implements(self, interface=None):
ints = ['plotData']
if interface is None:
return ints
return interface in ints
def name(self):
return self.opts.get('name', None)
def setClickable(self, s, width=None):
"""Sets whether the item responds to mouse clicks.
The *width* argument specifies the width in pixels orthogonal to the
curve that will respond to a mouse click.
"""
@ -92,41 +93,41 @@ class PlotCurveItem(GraphicsObject):
if width is not None:
self.opts['mouseWidth'] = width
self._mouseShape = None
self._boundingRect = None
self._boundingRect = None
def setCompositionMode(self, mode):
"""Change the composition mode of the item (see QPainter::CompositionMode
in the Qt documentation). This is useful when overlaying multiple items.
============================================ ============================================================
**Most common arguments:**
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
is opaque. Otherwise, it uses the alpha channel to blend
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.
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.
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
============================================ ============================================================
"""
self.opts['compositionMode'] = mode
self.update()
def getData(self):
return self.xData, self.yData
def dataBounds(self, ax, frac=1.0, orthoRange=None):
## Need this to run as fast as possible.
## check cache first:
cache = self._boundsCache[ax]
if cache is not None and cache[0] == (frac, orthoRange):
return cache[1]
(x, y) = self.getData()
if x is None or len(x) == 0:
return (None, None)
if ax == 0:
d = x
d2 = y
@ -139,7 +140,7 @@ class PlotCurveItem(GraphicsObject):
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask]
#d2 = d2[mask]
if len(d) == 0:
return (None, None)
@ -154,7 +155,7 @@ class PlotCurveItem(GraphicsObject):
if len(d) == 0:
return (None, None)
b = (d.min(), d.max())
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
else:
@ -166,7 +167,7 @@ class PlotCurveItem(GraphicsObject):
## adjust for fill level
if ax == 1 and self.opts['fillLevel'] is not None:
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
## Add pen width only if it is non-cosmetic.
pen = self.opts['pen']
spen = self.opts['shadowPen']
@ -174,10 +175,10 @@ class PlotCurveItem(GraphicsObject):
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:
b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072)
self._boundsCache[ax] = [(frac, orthoRange), b]
return b
def pixelPadding(self):
pen = self.opts['pen']
spen = self.opts['shadowPen']
@ -196,11 +197,11 @@ class PlotCurveItem(GraphicsObject):
(ymn, ymx) = self.dataBounds(ax=1)
if xmn is None or ymn is None:
return QtCore.QRectF()
px = py = 0.0
pxPad = self.pixelPadding()
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()
try:
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()
except OverflowError:
py = 0
# return bounds expanded by pixel size
px *= pxPad
py *= pxPad
#px += 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)
return self._boundingRect
def viewTransformChanged(self):
self.invalidateBounds()
self.prepareGeometryChange()
#def boundingRect(self):
#if self._boundingRect is None:
#(x, y) = self.getData()
#if x is None or y is None or len(x) == 0 or len(y) == 0:
#return QtCore.QRectF()
#if self.opts['shadowPen'] is not None:
#lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1)
#else:
#lineWidth = (self.opts['pen'].width()+1)
#pixels = self.pixelVectors()
#if pixels == (None, None):
#pixels = [Point(0,0), Point(0,0)]
#xmin = x.min()
#xmax = x.max()
#ymin = y.min()
#ymax = y.max()
#if self.opts['fillLevel'] is not None:
#ymin = min(ymin, self.opts['fillLevel'])
#ymax = max(ymax, self.opts['fillLevel'])
#xmin -= pixels[0].x() * lineWidth
#xmax += pixels[0].x() * lineWidth
#ymin -= abs(pixels[1].y()) * lineWidth
#ymax += abs(pixels[1].y()) * lineWidth
#self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
#return self._boundingRect
def invalidateBounds(self):
self._boundingRect = None
self._boundsCache = [None, None]
def setPen(self, *args, **kargs):
"""Set the pen used to draw the curve."""
self.opts['pen'] = fn.mkPen(*args, **kargs)
self.invalidateBounds()
self.update()
def setShadowPen(self, *args, **kargs):
"""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.
"""
self.opts['shadowPen'] = fn.mkPen(*args, **kargs)
@ -283,7 +284,7 @@ class PlotCurveItem(GraphicsObject):
self.opts['brush'] = fn.mkBrush(*args, **kargs)
self.invalidateBounds()
self.update()
def setFillLevel(self, level):
"""Set the level filled to when filling under the curve"""
self.opts['fillLevel'] = level
@ -295,11 +296,11 @@ class PlotCurveItem(GraphicsObject):
"""
=============== ========================================================
**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
:func:`mkPen <pyqtgraph.mkPen>` is allowed.
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
:func:`mkPen <pyqtgraph.mkPen>` is allowed.
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
they are attached to nan or inf values. For any other
connectivity, specify an array of boolean values.
compositionMode See :func:`setCompositionMode
compositionMode See :func:`setCompositionMode
<pyqtgraph.PlotCurveItem.setCompositionMode>`.
=============== ========================================================
If non-keyword arguments are used, they will be interpreted as
setData(y) for a single argument and setData(x, y) for two
arguments.
"""
self.updateData(*args, **kargs)
def updateData(self, *args, **kargs):
profiler = debug.Profiler()
@ -340,12 +341,12 @@ class PlotCurveItem(GraphicsObject):
elif len(args) == 2:
kargs['x'] = args[0]
kargs['y'] = args[1]
if 'y' not in kargs or kargs['y'] is None:
kargs['y'] = np.array([])
if 'x' not in kargs or kargs['x'] is None:
kargs['x'] = np.arange(len(kargs['y']))
for k in ['x', 'y']:
data = kargs[k]
if isinstance(data, list):
@ -355,9 +356,9 @@ class PlotCurveItem(GraphicsObject):
raise Exception("Plot data must be 1D ndarray.")
if 'complex' in str(data.dtype):
raise Exception("Can not plot complex data types.")
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
## Test this bug with test_PlotWidget and zoom in on the animated plot
self.invalidateBounds()
@ -365,24 +366,24 @@ class PlotCurveItem(GraphicsObject):
self.informViewBoundsChanged()
self.yData = kargs['y'].view(np.ndarray)
self.xData = kargs['x'].view(np.ndarray)
profiler('copy')
if 'stepMode' in kargs:
self.opts['stepMode'] = kargs['stepMode']
if self.opts['stepMode'] is True:
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))
else:
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))
self.path = None
self.fillPath = None
self._mouseShape = None
#self.xDisp = self.yDisp = None
if 'name' in kargs:
self.opts['name'] = kargs['name']
if 'connect' in kargs:
@ -397,14 +398,14 @@ class PlotCurveItem(GraphicsObject):
self.setBrush(kargs['brush'])
if 'antialias' in kargs:
self.opts['antialias'] = kargs['antialias']
profiler('set')
self.update()
profiler('update')
self.sigPlotChanged.emit(self)
profiler('emit')
def generatePath(self, x, y):
if self.opts['stepMode']:
## 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[0] = self.opts['fillLevel']
y[-1] = self.opts['fillLevel']
path = fn.arrayToQPath(x, y, connect=self.opts['connect'])
return path
@ -438,7 +439,7 @@ class PlotCurveItem(GraphicsObject):
self.path = self.generatePath(*self.getData())
self.fillPath = None
self._mouseShape = None
return self.path
@debug.warnOnException ## raising an exception here causes crash
@ -446,27 +447,27 @@ class PlotCurveItem(GraphicsObject):
profiler = debug.Profiler()
if self.xData is None or len(self.xData) == 0:
return
if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
self.paintGL(p, opt, widget)
return
x = None
y = None
path = self.getPath()
profiler('generate path')
if self._exportOpts is not False:
aa = self._exportOpts.get('antialias', True)
else:
aa = self.opts['antialias']
p.setRenderHint(p.Antialiasing, aa)
cmode = self.opts['compositionMode']
if cmode is not None:
p.setCompositionMode(cmode)
if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
if self.fillPath is None:
if x is None:
@ -477,14 +478,14 @@ class PlotCurveItem(GraphicsObject):
p2.lineTo(x[0], y[0])
p2.closeSubpath()
self.fillPath = p2
profiler('generate fill path')
p.fillPath(self.fillPath, self.opts['brush'])
profiler('draw fill path')
sp = fn.mkPen(self.opts['shadowPen'])
cp = fn.mkPen(self.opts['pen'])
sp = self.opts['shadowPen']
cp = self.opts['pen']
## Copy pens and apply alpha adjustment
#sp = QtGui.QPen(self.opts['shadowPen'])
#cp = QtGui.QPen(self.opts['pen'])
@ -495,38 +496,39 @@ class PlotCurveItem(GraphicsObject):
#c.setAlpha(c.alpha() * self.opts['alphaHint'])
#pen.setColor(c)
##pen.setCosmetic(True)
if sp is not None and sp.style() != QtCore.Qt.NoPen:
p.setPen(sp)
p.drawPath(path)
p.setPen(cp)
p.drawPath(path)
if self.fillPath is not None:
p.drawPath(self.fillPath)
else:
p.drawPath(path)
profiler('drawPath')
#print "Render hints:", int(p.renderHints())
#p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
#p.drawRect(self.boundingRect())
def paintGL(self, p, opt, widget):
p.beginNativePainting()
import OpenGL.GL as gl
## set clipping viewport
view = self.getViewBox()
if view is not None:
rect = view.mapRectToItem(self, view.boundingRect())
#gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height()))
#gl.glTranslate(-rect.x(), -rect.y(), 0)
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.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer
gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF)
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF)
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
## draw stencil pattern
gl.glStencilMask(0xFF)
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.y()+rect.height())
gl.glEnd()
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_TRUE)
gl.glStencilMask(0x00)
gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF)
try:
x, y = self.getData()
pos = np.empty((len(x), 2))
@ -568,7 +570,7 @@ class PlotCurveItem(GraphicsObject):
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
finally:
p.endNativePainting()
def clear(self):
self.xData = None ## raw values
self.yData = None
@ -584,7 +586,7 @@ class PlotCurveItem(GraphicsObject):
def mouseShape(self):
"""
Return a QPainterPath representing the clickable shape of the curve
"""
if self._mouseShape is None:
view = self.getViewBox()
@ -597,14 +599,14 @@ class PlotCurveItem(GraphicsObject):
mousePath = stroker.createStroke(path)
self._mouseShape = self.mapFromItem(view, mousePath)
return self._mouseShape
def mouseClickEvent(self, ev):
if not self.clickable or ev.button() != QtCore.Qt.LeftButton:
return
if self.mouseShape().contains(ev.pos()):
ev.accept()
self.sigClicked.emit(self)
class ROIPlotItem(PlotCurveItem):
@ -619,7 +621,7 @@ class ROIPlotItem(PlotCurveItem):
#roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent)
roi.sigRegionChanged.connect(self.roiChangedEvent)
#self.roiChangedEvent()
def getRoiData(self):
d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes)
if d is None:
@ -627,7 +629,7 @@ class ROIPlotItem(PlotCurveItem):
while d.ndim > 1:
d = d.mean(axis=1)
return d
def roiChangedEvent(self):
d = self.getRoiData()
self.updateData(d, self.xVals)

View File

@ -514,11 +514,13 @@ class PlotDataItem(GraphicsObject):
# Ignore the first bin for fft data if we have a logx scale
if self.opts['logMode'][0]:
x=x[1:]
y=y[1:]
if self.opts['logMode'][0]:
x = np.log10(x)
if self.opts['logMode'][1]:
y = np.log10(y)
y=y[1:]
with np.errstate(divide='ignore'):
if self.opts['logMode'][0]:
x = np.log10(x)
if self.opts['logMode'][1]:
y = np.log10(y)
ds = self.opts['downsample']
if not isinstance(ds, int):
@ -643,9 +645,9 @@ class PlotDataItem(GraphicsObject):
#self.yClean = None
self.xDisp = None
self.yDisp = None
self.curve.setData([])
self.scatter.setData([])
self.curve.clear()
self.scatter.clear()
def appendData(self, *args, **kargs):
pass

View File

@ -545,9 +545,9 @@ class PlotItem(GraphicsWidget):
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
Returns the item created.
"""
pos = kwds.get('pos', x if x is not None else y)
angle = kwds.get('angle', 0 if x is None else 90)
line = InfiniteLine(pos, angle, **kwds)
kwds['pos'] = kwds.get('pos', x if x is not None else y)
kwds['angle'] = kwds.get('angle', 0 if x is None else 90)
line = InfiniteLine(**kwds)
self.addItem(line)
if z is not None:
line.setZValue(z)
@ -986,8 +986,8 @@ class PlotItem(GraphicsWidget):
self._menuEnabled = enableMenu
if enableViewBoxMenu is None:
return
if enableViewBoxMenu is 'same':
enableViewBoxMenu = enableMenu
if enableViewBoxMenu == 'same':
enableViewBoxMenu = enableMenu
self.vb.setMenuEnabled(enableViewBoxMenu)
def menuEnabled(self):

View File

@ -428,6 +428,7 @@ class ROI(GraphicsObject):
def handleMoveStarted(self):
self.preMoveState = self.getState()
self.sigRegionChangeStarted.emit(self)
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
"""
@ -711,10 +712,10 @@ class ROI(GraphicsObject):
if hover:
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.RightButton)
ev.acceptClicks(QtCore.Qt.MidButton)
self.sigHoverEvent.emit(self)
else:
self.setMouseHover(False)
@ -928,6 +929,7 @@ class ROI(GraphicsObject):
if h['type'] == 'rf':
h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle
h['pos'] = self.mapFromParent(p1)
elif h['type'] == 'sr':
if h['center'][0] == h['pos'][0]:
@ -1102,9 +1104,9 @@ class ROI(GraphicsObject):
return bounds, tr
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.
=================== ====================================================
**Arguments**
data The array to slice from. Note that this array does
@ -1524,9 +1526,9 @@ class TestROI(ROI):
class RectROI(ROI):
"""
r"""
Rectangular ROI subclass with a single scale handle at the top-right corner.
============== =============================================================
**Arguments**
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]])
class LineROI(ROI):
"""
r"""
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.
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):
"""
Chain of rectangular ROIs connected by handles.
This is generally used to mark a curved path through
r"""
Chain of rectangular ROIs connected by handles.
This is generally used to mark a curved path through
an image similarly to PolyLineROI. It differs in that each segment
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])
sl = [slice(None)] * rgns[0].ndim
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
return np.concatenate(rgns, axis=axes[0])
@ -1724,12 +1728,12 @@ class MultiLineROI(MultiRectROI):
MultiRectROI.__init__(self, *args, **kwds)
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
class EllipseROI(ROI):
"""
r"""
Elliptical ROI subclass with one scale handle and one rotation handle.
============== =============================================================
**Arguments**
pos (length-2 sequence) The position of the ROI's origin.
@ -1810,8 +1814,9 @@ class EllipseROI(ROI):
return self.path
class CircleROI(EllipseROI):
"""
r"""
Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled
proportionally to maintain its aspect ratio.
@ -1878,13 +1883,13 @@ class PolygonROI(ROI):
sc['angle'] = self.state['angle']
return sc
class PolyLineROI(ROI):
"""
r"""
Container class for multiple connected LineSegmentROIs.
This class allows the user to draw paths of multiple line segments.
============== =============================================================
**Arguments**
positions (list of length-2 sequences) The list of points in the path.
@ -2076,9 +2081,9 @@ class PolyLineROI(ROI):
class LineSegmentROI(ROI):
"""
r"""
ROI subclass with two freely-moving handles defining a line.
============== =============================================================
**Arguments**
positions (list of two length-2 sequences) The endpoints of the line

View File

@ -834,8 +834,8 @@ class ScatterPlotItem(GraphicsObject):
pts = self.pointsAt(ev.pos())
if len(pts) > 0:
self.ptsClicked = pts
self.sigClicked.emit(self, self.ptsClicked)
ev.accept()
self.sigClicked.emit(self, self.ptsClicked)
else:
#print "no spots"
ev.ignore()

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,6 @@ def init_viewbox():
g = pg.GridItem()
vb.addItem(g)
app.processEvents()
def test_ViewBox():

View 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()

View 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

View File

@ -9,16 +9,16 @@ def test_fft():
x = np.linspace(0, 1, 1000)
y = np.sin(2 * np.pi * f * x)
pd = pg.PlotDataItem(x, y)
pd.setFftMode(True)
pd.setFftMode(True)
x, y = pd.getData()
assert abs(x[np.argmax(y)] - f) < 0.03
x = np.linspace(0, 1, 1001)
y = np.sin(2 * np.pi * f * x)
pd.setData(x, y)
x, y = pd.getData()
assert abs(x[np.argmax(y)]- f) < 0.03
pd.setLogMode(True, False)
x, y = pd.getData()
assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01
@ -58,3 +58,9 @@ def test_clear():
assert pdi.xData == 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()

View File

@ -633,7 +633,7 @@ class ImageView(QtGui.QWidget):
ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2)
data = data[sl]
data = data[tuple(sl)]
cax = self.axes['c']
if cax is None:

View File

@ -1,6 +1,7 @@
import os, time, sys, traceback, weakref
import numpy as np
import threading
import warnings
try:
import __builtin__ as builtins
import cPickle as pickle
@ -21,6 +22,9 @@ class NoResultError(Exception):
because the call has not yet returned."""
pass
class RemoteExceptionWarning(UserWarning):
"""Emitted when a request to a remote object results in an Exception """
pass
class RemoteEventHandler(object):
"""
@ -499,9 +503,9 @@ class RemoteEventHandler(object):
#print ''.join(result)
exc, excStr = result
if exc is not None:
print("===== Remote process raised exception on request: =====")
print(''.join(excStr))
print("===== Local Traceback to request follows: =====")
warnings.warn("===== Remote process raised exception on request: =====", RemoteExceptionWarning)
warnings.warn(''.join(excStr), RemoteExceptionWarning)
warnings.warn("===== Local Traceback to request follows: =====", RemoteExceptionWarning)
raise exc
else:
print(''.join(excStr))

View File

@ -236,6 +236,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glPopMatrix()
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:
self.opts['distance'] = distance
if elevation is not None:
@ -427,7 +429,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
ver = glGetString(GL_VERSION).split()[0]
if int(ver.split('.')[0]) < 2:
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)
else:
raise

View File

@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget):
def __init__(self):
QtOpenGL.QGLWidget.__init__(self)
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_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()

View File

@ -559,8 +559,8 @@ class Parameter(QtCore.QObject):
self.childs.insert(pos, child)
child.parentChanged(self)
self.sigChildAdded.emit(self, child, pos)
child.sigTreeStateChanged.connect(self.treeStateChanged)
self.sigChildAdded.emit(self, child, pos)
return child
def removeChild(self, child):
@ -571,11 +571,11 @@ class Parameter(QtCore.QObject):
del self.names[name]
self.childs.pop(self.childs.index(child))
child.parentChanged(None)
self.sigChildRemoved.emit(self, child)
try:
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
except (TypeError, RuntimeError): ## already disconnected
pass
self.sigChildRemoved.emit(self, child)
def clearChildren(self):
"""Remove all child parameters."""
@ -612,7 +612,7 @@ class Parameter(QtCore.QObject):
def incrementName(self, name):
## 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)
if numLen == 0:
num = 2

View File

@ -10,15 +10,22 @@ Includes:
- ThreadsafeDict, ThreadsafeList - Self-mutexed data structures
"""
import threading, sys, copy, collections
#from debug import *
import threading
import sys
import copy
try:
from collections import OrderedDict
except ImportError:
# fallback: try to use the ordereddict backport when using python 2.6
from ordereddict import OrderedDict
try:
from collections.abc import Sequence
except ImportError:
# fallback for python < 3.3
from collections import Sequence
class ReverseDict(dict):
"""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.
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.")
class ProtectedTuple(collections.Sequence):
class ProtectedTuple(Sequence):
"""
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.

View File

@ -47,7 +47,7 @@ def reloadAll(prefix=None, debug=False):
continue
## 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
if prefix is not None and mod.__file__[:len(prefix)] != prefix:
continue

View File

@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1':
raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.")
except Exception:
if stdFileName in gitStatus(dataPath):
print("\n\nWARNING: unit test failed against modified standard "
"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)
else:
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)
print(graphstate)
raise
@ -253,7 +257,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50.,
assert im1.dtype == im2.dtype
if pxCount == -1:
if QT_LIB == 'PyQt5':
if QT_LIB in {'PyQt5', 'PySide2'}:
# Qt5 generates slightly different results; relax the tolerance
# until test images are updated.
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
def saveFailedTest(data, expect, filename):
def saveFailedTest(data, expect, filename, upload=False):
"""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
ds = data.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
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)
req = urllib.urlencode({'name': filename,
'data': base64.b64encode(png)})
conn.request('POST', '/upload.py', req)
response = conn.getresponse().read()
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))
if not response.startswith(b'OK'):
print("WARNING: Error uploading data to %s" % host)
@ -495,7 +509,7 @@ def getTestDataRepo():
if not os.path.isdir(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
# downloading more data than is necessary)
os.makedirs(dataPath)

View 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)

View File

@ -10,7 +10,6 @@ def test_isQObjectAlive():
o2 = pg.QtCore.QObject()
o2.setParent(o1)
del o1
gc.collect()
assert not pg.Qt.isQObjectAlive(o2)
@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be '

View File

@ -4,6 +4,7 @@ import pyqtgraph.reload
pgpath = os.path.join(os.path.dirname(pg.__file__), '..')
pgpath_repr = repr(pgpath)
# make temporary directory to write module code
path = None
@ -22,7 +23,7 @@ def teardown_module():
code = """
import sys
sys.path.append('{path}')
sys.path.append({path_repr})
import pyqtgraph as pg
@ -47,7 +48,7 @@ def test_reload():
# write a module
mod = os.path.join(path, 'reload_test_mod.py')
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 reload_test_mod
@ -63,7 +64,7 @@ def test_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)
pg.reload.reloadAll(path, debug=True)
if py3:
@ -87,7 +88,7 @@ def test_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)
pg.reload.reloadAll(path, debug=True)
if py3:

View 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()

View File

@ -50,11 +50,11 @@ class ColorButton(QtGui.QPushButton):
def setColor(self, color, finished=True):
"""Sets the button's color and emits both sigColorChanged and sigColorChanging."""
self._color = functions.mkColor(color)
self.update()
if finished:
self.sigColorChanged.emit(self)
else:
self.sigColorChanging.emit(self)
self.update()
def selectColor(self):
self.origColor = self.color()

View File

@ -227,12 +227,12 @@ class GraphicsView(QtGui.QGraphicsView):
else:
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
self.sigDeviceRangeChanged.emit(self, self.range)
self.sigDeviceTransformChanged.emit(self)
if propagate:
for v in self.lockedViewports:
v.setXRange(self.range, padding=0)
self.sigDeviceRangeChanged.emit(self, self.range)
self.sigDeviceTransformChanged.emit(self)
def viewRect(self):
"""Return the boundaries of the view in scene coordinates"""
@ -262,7 +262,6 @@ class GraphicsView(QtGui.QGraphicsView):
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.updateMatrix()
self.sigScaleChanged.emit(self)
@ -362,7 +361,7 @@ class GraphicsView(QtGui.QGraphicsView):
def mouseMoveEvent(self, ev):
if self.lastMousePos is None:
self.lastMousePos = Point(ev.pos())
delta = Point(ev.pos() - self.lastMousePos)
delta = Point(ev.pos() - QtCore.QPoint(*self.lastMousePos))
self.lastMousePos = Point(ev.pos())
QtGui.QGraphicsView.mouseMoveEvent(self, ev)

View File

@ -39,7 +39,7 @@ class LayoutWidget(QtGui.QWidget):
Returns the created widget.
"""
text = QtGui.QLabel(text, **kargs)
self.addItem(text, row, col, rowspan, colspan)
self.addWidget(text, row, col, rowspan, colspan)
return text
def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs):
@ -49,7 +49,7 @@ class LayoutWidget(QtGui.QWidget):
Returns the created widget.
"""
layout = LayoutWidget(**kargs)
self.addItem(layout, row, col, rowspan, colspan)
self.addWidget(layout, row, col, rowspan, colspan)
return layout
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):
"""Return the widget in (*row*, *col*)"""
return self.row[row][col]
return self.rows[row][col]
#def itemIndex(self, item):
#for i in range(self.layout.count()):

View File

@ -201,7 +201,7 @@ class TreeWidget(QtGui.QTreeWidget):
return item
def topLevelItems(self):
return map(self.topLevelItem, xrange(self.topLevelItemCount()))
return [self.topLevelItem(i) for i in range(self.topLevelItemCount())]
def clear(self):
items = self.topLevelItems()

15
pytest.ini Normal file
View 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

View File

@ -15,10 +15,10 @@ elif '--pyqt4' in args:
elif '--pyqt5' in args:
args.remove('--pyqt5')
import PyQt5
elif '--pyside2' in args:
args.remove('--pyside2')
import PySide2
import pyqtgraph as pg
pg.systemInfo()
pytest.main(args)

51
tox.ini Normal file
View 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:.}