Merge branch 'master' into develop

This commit is contained in:
Ogi Moore 2020-06-09 20:56:51 -07:00 committed by GitHub
commit 517adc87c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
196 changed files with 5319 additions and 2433 deletions

49
.flake8 Normal file
View File

@ -0,0 +1,49 @@
[flake8]
exclude = .git,.tox,__pycache__,doc,old,build,dist
show_source = True
statistics = True
verbose = 2
select =
E101,
E112,
E122,
E125,
E133,
E223,
E224,
E242,
E273,
E274,
E901,
E902,
W191,
W601,
W602,
W603,
W604,
E124,
E231,
E211,
E261,
E271,
E272,
E304,
F401,
F402,
F403,
F404,
E501,
E502,
E702,
E703,
E711,
E712,
E721,
F811,
F812,
F821,
F822,
F823,
F831,
F841,
W292

44
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,44 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
<!-- In the following, please describe your issue in detail! -->
<!-- If some of the sections do not apply, just remove them. -->
### Short description
<!-- This should summarize the issue. -->
### Code to reproduce
<!-- Please provide a minimal working example that reproduces the issue in the code block below.
Ideally, this should be a full example someone else could run without additional setup. -->
```python
import pyqtgraph as pg
import numpy as np
```
### Expected behavior
<!-- What should happen? -->
### Real behavior
<!-- What happens? -->
```
An error occurred?
Post the full traceback inside these 'code fences'!
```
### Tested environment(s)
* PyQtGraph version: <!-- output of pyqtgraph.__version__ -->
* Qt Python binding: <!-- output of pyqtgraph.Qt.VERSION_INFO -->
* Python version:
* NumPy version: <!-- output of numpy.__version__ -->
* Operating system:
* Installation method: <!-- e.g. pip, conda, system packages, ... -->
### Additional context

View File

@ -1,12 +0,0 @@
Luke Campagnola <lcampagn@email.unc.edu> Luke Campagnola <>
Luke Campagnola <lcampagn@email.unc.edu> Luke Campagnola <luke.campagnola@gmail.com>
Megan Kratz <meganbkratz@gmail.com> meganbkratz@gmail.com <>
Megan Kratz <meganbkratz@gmail.com> Megan Kratz <megankratz@megancomputer.local>
Megan Kratz <meganbkratz@gmail.com> Megan Kratz <megankratz@wireless152023024102.med.unc.edu>
Megan Kratz <meganbkratz@gmail.com> Megan Kratz <megankratz@wireless152023025209.med.unc.edu>
Megan Kratz <meganbkratz@gmail.com> Megan Kratz <megankratz@p152023031037.med.unc.edu>
Megan Kratz <meganbkratz@gmail.com> Megan Kratz <megankratz@wire152019114033.med.unc.edu>
Megan Kratz <meganbkratz@gmail.com> Megan Kratz <megankratz@wireless152023024078.med.unc.edu>
Ingo Breßler <dev@ingobressler.net> Ingo Breßler <ingo.bressler@bam.de>
Ingo Breßler <dev@ingobressler.net> Ingo B. <dev@ingobressler.net>

11
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,11 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
sha: master
hooks:
- id: check-added-large-files
args: ['--maxkb=100']
- id: check-case-conflict
- id: end-of-file-fixer
- id: fix-encoding-pragma
- id: mixed-line-ending
args: [--fix=lf]

12
.readthedocs.yml Normal file
View File

@ -0,0 +1,12 @@
# Read the Docs configuration file
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
python:
version: 3
install:
- requirements: doc/requirements.txt
sphinx:
fail_on_warning: true

View File

@ -1,198 +0,0 @@
language: python
sudo: false
# Credit: Original .travis.yml lifted from VisPy
# Here we use anaconda for 2.6 and 3.3, since it provides the simplest
# interface for running different versions of Python. We could also use
# it for 2.7, but the Ubuntu system has installable 2.7 Qt4-GL, which
# allows for more complete testing.
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
- 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
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
- chmod +x miniconda.sh
- ./miniconda.sh -b -p /home/travis/mc
- export PATH=/home/travis/mc/bin:$PATH
# not sure what is if block is for
- if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then
GIT_TARGET_EXTRA="+refs/heads/${TRAVIS_BRANCH}";
GIT_SOURCE_EXTRA="+refs/pull/${TRAVIS_PULL_REQUEST}/merge";
else
GIT_TARGET_EXTRA="";
GIT_SOURCE_EXTRA="";
fi;
# to aid in debugging
- echo ${TRAVIS_BRANCH}
- echo ${TRAVIS_REPO_SLUG}
- echo ${GIT_TARGET_EXTRA}
- echo ${GIT_SOURCE_EXTRA}
install:
- export GIT_FULL_HASH=`git rev-parse HEAD`
- conda update conda --yes
- conda create -n test_env python=${PYTHON} --yes
- source activate test_env
- conda install numpy scipy pyopengl pytest flake8 six coverage --yes
- echo ${QT}
- echo ${TEST}
- echo ${PYTHON}
- if [ "${QT}" == "pyqt5" ]; then
conda install pyqt --yes;
fi;
- if [ "${QT}" == "pyqt4" ]; then
conda install pyqt=4 --yes;
fi;
- if [ "${QT}" == "pyside" ]; then
conda install pyside --yes;
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;
# Debugging helpers
- uname -a
- cat /etc/issue
- if [ "${PYTHON}" == "2.7" ]; then
python --version;
else
python3 --version;
fi;
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)
- which python
- python --version
- pwd
- ls
# Help color output from each test
- RESET='\033[0m';
RED='\033[00;31m';
GREEN='\033[00;32m';
YELLOW='\033[00;33m';
BLUE='\033[00;34m';
PURPLE='\033[00;35m';
CYAN='\033[00;36m';
WHITE='\033[00;37m';
start_test() {
echo -e "${BLUE}======== Starting $1 ========${RESET}";
};
check_output() {
ret=$?;
if [ $ret == 0 ]; then
echo -e "${GREEN}>>>>>> $1 passed <<<<<<${RESET}";
else
echo -e "${RED}>>>>>> $1 FAILED <<<<<<${RESET}";
fi;
return $ret;
};
- if [ "${TEST}" == "extra" ]; then
start_test "repo size check";
mkdir ~/repo-clone && cd ~/repo-clone &&
git init && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git &&
git fetch origin ${GIT_TARGET_EXTRA} &&
git checkout -qf FETCH_HEAD &&
git tag travis-merge-target &&
git gc --aggressive &&
TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` &&
git pull origin ${GIT_SOURCE_EXTRA} &&
git gc --aggressive &&
MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` &&
if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then
SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`;
else
SIZE_DIFF=0;
fi;
fi;
script:
- source activate test_env
# Check system info
- python -c "import pyqtgraph as pg; pg.systemInfo()"
# Run unit tests
- start_test "unit tests";
PYTHONPATH=. py.test --cov pyqtgraph -sv;
check_output "unit tests";
- echo "test script finished. Current directory:"
- pwd
# check line endings
- if [ "${TEST}" == "extra" ]; then
start_test "line ending check";
! find ./ -name "*.py" | xargs file | grep CRLF &&
! find ./ -name "*.rst" | xargs file | grep CRLF;
check_output "line ending check";
fi;
# Check repo size does not expand too much
- if [ "${TEST}" == "extra" ]; then
start_test "repo size check";
echo -e "Estimated content size difference = ${SIZE_DIFF} kB" &&
test ${SIZE_DIFF} -lt 100;
check_output "repo size check";
fi;
# Check for style issues
- if [ "${TEST}" == "extra" ]; then
start_test "style check";
cd ~/repo-clone &&
git reset -q travis-merge-target &&
python setup.py style &&
check_output "style check";
fi;
# Check install works
- start_test "install test";
python setup.py --quiet install;
check_output "install test";
# Check double-install fails
# Note the bash -c is because travis strips off the ! otherwise.
- start_test "double install test";
bash -c "! python setup.py --quiet install";
check_output "double install test";
# Check we can import pg
- start_test "import test";
echo "import sys; print(sys.path)" | python &&
cd /; echo "import pyqtgraph.examples" | python;
check_output "import test";
after_success:
- cd /home/travis/build/pyqtgraph/pyqtgraph
- pip install codecov --upgrade # add coverage integration service
- codecov
- pip install coveralls --upgrade # add another coverage integration service
- coveralls

159
CHANGELOG
View File

@ -1,6 +1,9 @@
pyqtgraph-0.11.0 (in development)
pyqtgraph-0.11.0
NOTICE: This is the _last_ feature release to support Python 2 and Qt 4 (PyQt4 or pyside 1)
New Features:
- #101: GridItem formatting options
- #410: SpinBox custom formatting options
- #415: ROI.getArrayRegion supports nearest-neighbor interpolation (especially handy for label images)
- #428: DataTreeWidget:
@ -56,10 +59,24 @@ pyqtgraph-0.11.0 (in development)
- #683: Allow data filter entries to be updated after they are created
- #685: Add option to set enum default values in DataFilterWidget
- #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers)
- #813,814,817: Performance improvements
- #837: Added options for field variables in ColorMapWidget
- #840, 932: Improve clipping behavior
- #841: Set color of tick-labels separately
- #922: Curve fill for fill-patches
- #996: Allow the update of LegendItem
- #1023: Add bookkeeping exporter parameters
- #1072: HDF5Exporter handling of ragged curves with tests
- #1124: Syntax highlighting for examples.
- #1154: Date axis item
- #393: NEW show/hide gradient ticks NEW link gradientEditor to others
- #1211: Add support for running pyside2-uic binary to dynamically compile ui files
API / behavior changes:
- Deprecated graphicsWindow classes; these have been unnecessary for many years because
widgets can be placed into a new window just by calling show().
- #158: Make DockArea compatible with Qt Designer
- #406: Applying alpha mask on numpy.nan data values
- #566: ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system.
The result is visually the same, but children of ArrowItem are no longer rotated
(this allows screen-aligned text to be attached more easily).
@ -76,8 +93,50 @@ pyqtgraph-0.11.0 (in development)
- #589: Remove SpiralROI (this was unintentionally added in the first case)
- #593: Override qAbort on slot exceptions for PyQt>=5.5
- #657: When a floating Dock window is closed, the dock is now returned home
- #771: Suppress RuntimeWarning for arrays containing zeros in logscale
- #942: If the visible GraphicsView is garbage collected, a warning is issued.
- #958: Nicer Legend
- #963: Last image in image-stack can now be selected with the z-slider
- #992: Added a setter for GlGridItem.color.
- #999: Make outline around fillLevel optional.
- #1014: Enable various arguments as color in colormap.
- #1044: Raise AttributeError in __getattr__ in graphicsWindows (deprecated)
- #1055: Remove global for CONFIG_OPTIONS in setConfigOption
- #1066: Add RemoteGraphicsView to __init__.py
- #1069: Allow actions to display title instead of name
- #1074: Validate min/max text inputs in ViewBoxMenu
- #1076: Reset currentRow and currentCol on GraphicsLayout.clear()
- #1079: Improve performance of updateData PlotCurveItem
- #1082: Allow MetaArray.__array__ to accept an optional dtype arg
- #841: set color of tick-labels separately
- #1111: Add name label to GradientEditorItem
- #1145: Pass showAxRect keyword arguments to setRange
- #1184: improve SymbolAtlas.getSymbolCoords performance
- #1198: improve SymbolAtlas.getSymbolCoords and ScatterPlotItem.plot performance
- #1197: Disable remove ROI menu action in handle context menu
- #1188: Added support for plot curve to handle both fill and connect args
- #801: Remove use of GraphicsScene._addressCache in translateGraphicsItem
- Deprecates registerObject meethod of GraphicsScene
- Deprecates regstar argument to GraphicsScene.__init__
- #1166: pg.mkQApp: Pass non-empty string array to QApplication() as default
- #1199: Pass non-empty sys.argv to QApplication
- #1090: dump ExportDialog.exporterParameters
- #1173: GraphicsLayout: Always call layout.activate() after adding items
- #1097: pretty-print log-scale axes labels
- #755: Check lastDownsample in viewTransformChanged
- #1216: Add cache for mapRectFromView
- #444: Fix duplicate menus in GradientEditorItem
- #151: Optionally provide custom PlotItem to PlotWidget
- #1093: Fix aspectRatio and zoom range issues when zooming
- #390: moved some functionality from method 'export' to new method
- #468: Patch/window handling
- #392: new method 'getAxpectRatio' with code taken from 'setAspectLocked'
- #1206: Added context menu option to parametertree
- #1228: Minor improvements to LegendItem
Bugfixes:
- #88: Fixed image scatterplot export
- #356: Fix some NumPy warnings
- #408: Fix `cleanup` when the running qt application is not a QApplication
- #410: SpinBox fixes
- fixed bug with exponents disappearing after edit
@ -93,7 +152,7 @@ pyqtgraph-0.11.0 (in development)
- fixed spinbox height too small for font size
- ROI subclass getArrayRegion methods are a bit more consistent (still need work)
- #424: Fix crash when running pyqtgraph with python -OO
- #429: fix fft premature slicing away of 0 freq bin
- #429: Fix fft premature slicing away of 0 freq bin
- #458: Fixed image export problems with new numpy API
- #478: Fixed PySide image memory leak
- #475: Fixed unicode error when exporting to SVG with non-ascii symbols
@ -126,17 +185,18 @@ pyqtgraph-0.11.0 (in development)
- #592,595: Fix InvisibleRootItem issues introduced in #518
- #596: Fix polyline click causing lines to bedrawn to the wrong node
- #598: Better ParameterTree support for dark themes
- #599: Prevent invalid list access in GraphicsScene
- #623: Fix PyQt5 / ScatterPlot issue with custom symbols
- #626: Fix OpenGL texture state leaking to wrong items
- #627: Fix ConsoleWidget stack handling on python 3.5
- #633: Fix OpenGL cylinder geometry
- #637: Fix TypeError in isosurface
- #641,642: Fix SVG export on Qt5 / high-DPI displays
- #645: scatterplotwidget behaves nicely when data contains infs
- #645: ScatterPlotWidget behaves nicely when data contains infs
- #653: ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581
- #648: fix color ignored in GLGridItem
- #671: fixed SVG export failing if the first value of a plot is nan
- #674: fixed parallelizer leaking file handles
- #671: Fixed SVG export failing if the first value of a plot is nan
- #674: Fixed parallelizer leaking file handles
- #675: Gracefully handle case where image data has size==0
- #679: Fix overflow in Point.length()
- #682: Fix: mkQApp returned None if a QApplication was already created elsewhere
@ -153,14 +213,103 @@ pyqtgraph-0.11.0 (in development)
it was causing auto range to be disabled.
- #723: Fix axis ticks when using self.scale
- #739: Fix handling of 2-axis mouse wheel events
- #742: Fix Metaarray in python 3
- #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX.
- #763: Fix OverflowError when using Auto Downsampling.
- #767: Fix Image display for images with the same value everywhere.
- #770: Fix GLVieWidget.setCameraPosition ignoring first parameter.
- #782: Fix missing FileForwarder thread termination.
- #787: Fix encoding errors in checkOpenGLVersion.
- #793: Fix wrong default scaling in makeARGB
- #815: Fixed mirroring of x-axis with "invert Axis" submenu.
- #824: Fix several issues related with mouse movement and GraphicsView.
- #832: Fix Permission error in tests due to unclosed filehandle.
- #836: Fix tickSpacing bug that lead to axis not being drawn.
- #861: Fix crash of PlotWidget if empty ErrorBarItem is added.
- #868: Fix segfault on repeated closing of matplotlib exporter.
- #875,876,887,934,947,980: Fix deprecation warnings.
- #886: Fix flowchart saving on python3.
- #888: Fix TreeWidget.topLevelItems in python3.
- #924: Fix QWheelEvent in RemoteGraphicsView with pyqt5.
- #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter.
- #949: Fix multiline parameters (such as arrays) reading from config files.
- #951: Fix event firing from scale handler.
- #952: Fix RotateFree handle dragging
- #953: Fix HistogramLUTWidget with background parameter
- #968: Fix Si units in AxisItem leading to an incorrect unit.
- #970: Always update transform when setting angle of a TextItem
- #971: Fix a segfault stemming from incorrect signal disconnection.
- #972: Correctly include SI units for log AxisItems
- #974: Fix recursion error when instancing CtrlNode.
- #987: Fix visibility reset when PlotItems are removed.
- #998: Fix QtProcess proxy being unable to handle numpy arrays with dtype uint8.
- #1010: Fix matplotlib/CSV export.
- #1012: Fix circular texture centering
- #1015: Iterators are now converted to NumPy arrays.
- #1016: Fix synchronisation of multiple ImageViews with time axis.
- #1017: Fix duplicate paint calls emitted by Items on ViewBox.
- #1019: Fix disappearing GLGridItems when PlotItems are removed and readded.
- #1024: Prevent element-wise string comparison
- #1031: Reset ParentItem to None on removing from PlotItem/ViewBox
- #1044: Fix PlotCurveItem.paintGL
- #1048: Fix bounding box for InfiniteLine
- #1062: Fix flowchart context menu redundant menu
- #1062: Fix a typo
- #1073: Fix Python3 compatibility
- #1083: Fix SVG export of scatter plots
- #1085: Fix ofset when drawing symbol
- #1101: Fix small oversight in LegendItem
- #1113: Correctly call hasFaceIndexedData function
- #1139: Bug fix in LegendItem for `setPen`, `setBrush` etc (Call update instead of paint)
- #1110: fix for makeARGB error after #955
- #1063: Fix: AttributeError in ViewBox.setEnableMenu
- #1151: ImageExporter py2-pyside fix with test
- #1133: compatibility-fix for py2/pyside
- #1152: Nanmask fix in makeARGB
- #1159: Fix: Update axes after data is set
- #1156: SVGExporter: Correct image pixelation
- #1169: Replace default list arg with None
- #770: Do not ignore pos argument of setCameraPosition
- #1180: Fix: AxisItem tickFont is defined in two places while only one is used
- #1168: GroupParameterItem: Did not pass changed options to ParameterItem
- #1174: Fixed a possible race condition with linked views
- #809: Fix selection of FlowchartWidget input/output nodes
- #1071: Fix py3 execution in flowchart
- #1212: Fix PixelVectors cache
- #1161: Correctly import numpy where needed
- #1218: Fix ParameterTree.clear()
- #1175: Fix: Parameter tree ignores user-set 'expanded' state
- #1219: Encode csv export header as unicode
- #507: Fix Dock close event QLabel still running with no parent
- #1222: py3 fix for ScatterPlotWidget.setSelectedFields
- #1203: Image axis order bugfix
- #1225: ParameterTree: Fix custom context menu
Maintenance:
- Lots of new unit tests
- Lots of code cleanup
- A lot of work on CI pipelines, test coverage and test passing (see e.g. #903,911)
- #546: Add check for EINTR during example testing to avoid sporadic test failures on travis
- #624: TravisCI no longer running python 2.6 tests
- #695: "dev0" added to version string
- #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI
- #991: Use Azure Pipelines to do style checks, Add .pre-commit-config.yaml
- #1042: Close windows at the end of test functions
- #1046: Establish minimum numpy version, remove legacy workarounds
- #1067: Make scipy dependency optional
- #1114: doc: Fix small mistake in introduction
- #1131: Update CI/tox and Enable More Tests
- #1142: Miscellaneous doc fixups
- #1179: DateAxisItem: AxisItem unlinking tests and doc fixed
- #1201: Get readthedocs working
- #1214: Pin PyVirtualDisplay Version
- #1215: Skip test when on qt 5.9
- #1221: Identify pyqt5 5.15 ci issue
- #1223: Remove workaround for memory leak in QImage
- #1217: Get docs version and copyright year dynamically
- #1229: Wrap text in tables in docs
- #1231: Update readme for 0.11 release
pyqtgraph-0.10.0

72
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,72 @@
# 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.
* The following deprecations are being considered by the maintainers
* `pyqtgraph.opengl` may be deprecated and replaced with `VisPy` functionality
* After v0.11, pyqtgraph will adopt [NEP-29](https://numpy.org/neps/nep-0029-deprecation_policy.html) which will effectively mean that python2 support will be deprecated
* Qt4 will be deprecated shortly, as well as Qt5<5.9 (and potentially <5.12)
## Documentation
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses 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
### Rules
* 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.
### Pre-Commit
PyQtGraph developers are highly encouraged to (but not required) to use [`pre-commit`](https://pre-commit.com/). `pre-commit` does a number of checks when attempting to commit the code to ensure it conforms to various standards, such as `flake8`, utf-8 encoding pragma, line-ending fixers, and so on. If any of the checks fail, the commit will be rejected, and you will have the opportunity to make the necessary fixes before adding and committing a file again. This ensures that every commit made conforms to (most) of the styling standards that the library enforces; and you will most likely pass the code style checks by the CI.
To make use of `pre-commit`, have it available in your `$PATH` and run `pre-commit install` from the root directory of PyQtGraph.
## Testing Setting up a test environment
### Dependencies
* tox
* tox-conda
* pytest
* pytest-cov
* pytest-xdist
* Optional: pytest-xvfb
If you have `pytest<5` (used in python2), you may also want to install `pytest-faulthandler==1.6` plugin to output extra debugging information in case of test failures. This isn't necessary with `pytest>=5`
### 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. Tested configurations are visible on [README](README.md). 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)
[![Documentation Status](https://readthedocs.org/projects/pyqtgraph/badge/?version=latest)](https://pyqtgraph.readthedocs.io/en/latest/?badge=latest)
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 2020 Luke Campagnola, University of North Carolina at Chapel Hill
<http://www.pyqtgraph.org>
@ -15,38 +16,57 @@ 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.
* Python 2.7, or 3.x
* Required
* PyQt 4.8+, PySide, PyQt5, or PySide2
* `numpy`
* Optional
* `scipy` for image processing
* `pyopengl` for 3D graphics
* `hdf5` for large hdf5 binary format support
Qt Bindings Test Matrix
-----------------------
The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 18.04, Windows Server 2019, and macOS 10.15 base images.
| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 |
| :------------- | :----------------: | :----------------: | :----------------: | :----------------: |
| PyQt-4 | :white_check_mark: | :x: | :x: | :x: |
| PySide1 | :white_check_mark: | :x: | :x: | :x: |
| PyQt5-5.9 | :x: | :white_check_mark: | :x: | :x: |
| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: |
| PyQt5-Latest | :x: | :x: | :x: | :white_check_mark: |
| PySide2-Latest | :x: | :x: | :x: | :white_check_mark: |
* pyqtgraph has had some incompatibilities with PySide2 versions 5.6-5.11, and we recommend you avoid those versions if possible
* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work reliably
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@master`
* From conda
* Last released version: `conda install -c conda-forge 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.
* For installation packages, see the website (pyqtgraph.org)
anywhere that is importable from your project.
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
The official documentation lives at https://pyqtgraph.readthedocs.io
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` to launch the examples application.

99
azure-pipelines.yml Normal file
View File

@ -0,0 +1,99 @@
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'
DEFAULT_MERGE_BRANCH: 'develop'
disable.coverage.autogenerate: 'true'
stages:
- stage: "pre_test"
jobs:
- job: check_diff_size
pool:
vmImage: 'Ubuntu 18.04'
steps:
- bash: |
git config --global advice.detachedHead false
mkdir ~/repo-clone && cd ~/repo-clone
git init
git remote add -t $(Build.SourceBranchName) origin $(Build.Repository.Uri)
git remote add -t ${DEFAULT_MERGE_BRANCH} upstream https://github.com/${OFFICIAL_REPO}.git
git fetch origin $(Build.SourceBranchName)
git fetch upstream ${DEFAULT_MERGE_BRANCH}
git checkout $(Build.SourceBranchName)
MERGE_SIZE=`du -s . | sed -e "s/\t.*//"`
echo -e "Merge Size ${MERGE_SIZE}"
git checkout ${DEFAULT_MERGE_BRANCH}
TARGET_SIZE=`du -s . | sed -e "s/\t.*//"`
echo -e "Target Size ${TARGET_SIZE}"
if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then
SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`;
else
SIZE_DIFF=0;
fi;
echo -e "Estimated content size difference = ${SIZE_DIFF} kB" &&
test ${SIZE_DIFF} -lt 100;
displayName: 'Diff Size Check'
continueOnError: true
- job: "style_check"
pool:
vmImage: "Ubuntu 18.04"
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.7
- bash: |
pip install flake8
python setup.py style
displayName: 'flake8 check'
continueOnError: true
- job: "build_wheel"
pool:
vmImage: 'Ubuntu 18.04'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: 3.8
- script: |
python -m pip install setuptools wheel
python setup.py bdist_wheel --universal
displayName: "Build Python Wheel"
continueOnError: false
- publish: dist
artifact: wheel
- stage: "test"
jobs:
- template: azure-test-template.yml
parameters:
name: linux
vmImage: 'Ubuntu 18.04'
- template: azure-test-template.yml
parameters:
name: windows
vmImage: 'windows-2019'
- template: azure-test-template.yml
parameters:
name: macOS
vmImage: 'macOS-10.15'

208
azure-test-template.yml Normal file
View File

@ -0,0 +1,208 @@
# 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-PyQt5-5.9:
python.version: "3.6"
qt.bindings: "pyqt"
install.method: "conda"
Python37-PySide2-5.13:
python.version: "3.7"
qt.bindings: "pyside2"
install.method: "conda"
Python38-PyQt5-Latest:
python.version: '3.8'
qt.bindings: "PyQt5"
install.method: "pip"
Python38-PySide2-Latest:
python.version: '3.8'
qt.bindings: "PySide2"
install.method: "pip"
steps:
- task: DownloadPipelineArtifact@2
inputs:
source: 'current'
artifact: wheel
path: 'dist'
- task: ScreenResolutionUtility@1
inputs:
displaySettings: 'specific'
width: '1920'
height: '1080'
condition: eq(variables['agent.os'], 'Windows_NT' )
- task: UsePythonVersion@0
inputs:
versionSpec: $(python.version)
condition: eq(variables['install.method'], 'pip')
- 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')
- bash: |
if [ $(agent.os) == 'Linux' ]
then
echo "##vso[task.prependpath]$CONDA/bin"
elif [ $(agent.os) == 'Darwin' ]
then
sudo chown -R $USER $CONDA
echo "##vso[task.prependpath]$CONDA/bin"
elif [ $(agent.os) == 'Windows_NT' ]
then
echo "##vso[task.prependpath]$CONDA/Scripts"
else
echo 'Just what OS are you using?'
fi
displayName: 'Add Conda To $PATH'
condition: eq(variables['install.method'], 'conda' )
continueOnError: false
- bash: |
if [ $(install.method) == "conda" ]
then
conda update --all --yes --quiet
conda create --name test-environment-$(python.version) python=$(python.version) --yes --quiet
source activate test-environment-$(python.version)
conda config --env --set always_yes true
if [ $(python.version) == '2.7' ]
then
conda config --set restore_free_channel true
fi
if [ $(qt.bindings) == "pyside2" ] || ([ $(qt.bindings) == 'pyside' ] && [ $(agent.os) == 'Darwin' ])
then
conda config --prepend channels conda-forge
fi
conda info
conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet
else
pip install $(qt.bindings) numpy scipy pyopengl h5py six
fi
pip install pytest pytest-cov coverage pytest-xdist
if [ $(python.version) == "2.7" ]
then
pip install pytest-faulthandler==1.6.0
export PYTEST_ADDOPTS="--faulthandler-timeout=15"
else
pip install pytest pytest-cov coverage
fi
displayName: "Install Dependencies"
- bash: |
if [ $(install.method) == "conda" ]
then
source activate test-environment-$(python.version)
fi
python -m pip install --no-index --find-links=dist pyqtgraph
displayName: 'Install Wheel'
- bash: |
sudo apt-get install -y libxkbcommon-x11-dev
# workaround for QTBUG-84489
sudo apt-get install -y libxcb-xfixes0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0
if [ $(install.method) == "conda" ]
then
source activate test-environment-$(python.version)
fi
if [ $(python.version) == "2.7" ]
then
pip install PyVirtualDisplay==0.2.5 pytest-xvfb==1.2.0
else
pip install pytest-xvfb
fi
displayName: "Virtual Display Setup"
condition: eq(variables['agent.os'], 'Linux' )
- bash: |
export QT_DEBUG_PLUGINS=1
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 . -v \
-n 1 \
--junitxml=junit/test-results.xml \
--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'

5
doc/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
pyside2
numpy
pyopengl
sphinx
sphinx_rtd_theme

View File

@ -0,0 +1,14 @@
/* Customizations to the theme */
/* override table width restrictions */
/* https://github.com/readthedocs/sphinx_rtd_theme/issues/117 */
@media screen and (min-width: 768px) {
.wy-table-responsive table td, .wy-table-responsive table th {
white-space: normal !important;
}
.wy-table-responsive {
overflow: visible !important;
max-width: 100%;
}
}

View File

@ -13,5 +13,7 @@ Contents:
3dgraphics/index
colormap
parametertree/index
dockarea
graphicsscene/index
flowchart/index
graphicswindow

View File

@ -11,7 +11,9 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import sys
import os
from datetime import datetime
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@ -19,6 +21,7 @@ import sys, os
path = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(path, '..', '..'))
sys.path.insert(0, os.path.join(path, '..', 'extensions'))
import pyqtgraph
# -- General configuration -----------------------------------------------------
@ -43,16 +46,16 @@ master_doc = 'index'
# General information about the project.
project = 'pyqtgraph'
copyright = '2011, Luke Campagnola'
copyright = '2011 - {}, Luke Campagnola'.format(datetime.now().year)
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.10.0'
version = pyqtgraph.__version__
# The full version, including alpha/beta/rc tags.
release = '0.10.0'
release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -88,12 +91,18 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
autodoc_inherit_docstrings = False
autodoc_mock_imports = [
"scipy",
"h5py",
"matplotlib",
]
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@ -124,6 +133,10 @@ html_theme = 'default'
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# add the theme customizations
def setup(app):
app.add_stylesheet("custom.css")
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
@ -216,4 +229,3 @@ man_pages = [
('index', 'pyqtgraph', 'pyqtgraph Documentation',
['Luke Campagnola'], 1)
]

11
doc/source/dockarea.rst Normal file
View File

@ -0,0 +1,11 @@
Dock Area Module
================
.. automodule:: pyqtgraph.dockarea
:members:
.. autoclass:: pyqtgraph.dockarea.DockArea
:members:
.. autoclass:: pyqtgraph.dockarea.Dock
:members:

View File

@ -30,8 +30,13 @@ Export Formats
for export.
* Printer - Exports to the operating system's printing service. This exporter is provided for completeness,
but is not well supported due to problems with Qt's printing system.
* HDF5 - Exports data from a :class:`~pyqtgraph.PlotItem` to a HDF5 file if
h5py_ is installed. This exporter supports :class:`~pyqtgraph.PlotItem`
objects containing multiple curves, stacking the data into a single HDF5
dataset based on the ``columnMode`` parameter. If data items aren't the same
size, each one is given its own dataset.
.. _h5py: https://www.h5py.org/
Exporting from the API
----------------------

View File

@ -0,0 +1,8 @@
BarGraphItem
============
.. autoclass:: pyqtgraph.BarGraphItem
:members:
.. automethod:: pyqtgraph.BarGraphItem.__init__

View File

@ -0,0 +1,8 @@
DateAxisItem
============
.. autoclass:: pyqtgraph.DateAxisItem
:members:
.. automethod:: pyqtgraph.DateAxisItem.__init__

View File

@ -24,6 +24,7 @@ Contents:
axisitem
textitem
errorbaritem
bargraphitem
arrowitem
fillbetweenitem
curvepoint
@ -42,4 +43,4 @@ Contents:
graphicsitem
uigraphicsitem
graphicswidgetanchor
dateaxisitem

View File

@ -2,6 +2,7 @@ files = """ArrowItem
AxisItem
ButtonItem
CurvePoint
DateAxisItem
GradientEditorItem
GradientLegend
GraphicsLayout

View File

@ -1,8 +1,16 @@
Basic display widgets
=====================
Deprecated Window Classes
=========================
- GraphicsWindow
- GraphicsView
- GraphicsLayoutItem
- ViewBox
.. automodule:: pyqtgraph.graphicsWindows
.. autoclass:: pyqtgraph.GraphicsWindow
:members:
.. autoclass:: pyqtgraph.TabWindow
:members:
.. autoclass:: pyqtgraph.PlotWindow
:members:
.. autoclass:: pyqtgraph.ImageWindow
:members:

View File

@ -1,45 +1,70 @@
Installation
============
There are many different ways to install pyqtgraph, depending on your needs:
* The most common way to install pyqtgraph is with pip::
$ pip install pyqtgraph
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::
$ git clone https://github.com/pyqtgraph/pyqtgraph
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.
* Packages for pyqtgraph are also available in a few other forms:
* **Anaconda**: ``conda install pyqtgraph``
* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or
download the .deb file linked at the top of the pyqtgraph web page.
* **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577)
* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page.
Requirements
============
PyQtGraph depends on:
* Python 2.7 or Python 3.x
* A Qt library such as PyQt4, PyQt5, PySide, or PySide2
* numpy
The easiest way to meet these dependencies is with ``pip`` or with a scientific python
distribution like Anaconda.
The easiest way to meet these dependencies is with ``pip`` or with a scientific
python distribution like Anaconda.
.. _pyqtgraph: http://www.pyqtgraph.org/
There are many different ways to install pyqtgraph, depending on your needs:
pip
---
The most common way to install pyqtgraph is with pip::
$ pip install pyqtgraph
Some users may need to call ``pip3`` instead. This method should work on all
platforms.
conda
-----
pyqtgraph is on the default Anaconda channel::
$ conda install pyqtgraph
It is also available in the conda-forge channel::
$ conda install -c conda-forge pyqtgraph
From Source
-----------
To get access to the very latest features and bugfixes you have three choices:
1. Clone pyqtgraph from github::
$ git clone https://github.com/pyqtgraph/pyqtgraph
$ cd pyqtgraph
Now you can install pyqtgraph from the source::
$ pip install .
2. Directly install from GitHub repo::
$ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop
You can change ``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.
Other Packages
--------------
Packages for pyqtgraph are also available in a few other forms:
* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or
download the .deb file linked at the top of the pyqtgraph web page.
* **Arch Linux:** https://www.archlinux.org/packages/community/any/python-pyqtgraph/
* **Windows:** Download and run the .exe installer file linked at the top of the
pyqtgraph web page: http://pyqtgraph.org

View File

@ -73,7 +73,7 @@ How does it compare to...
such as image interaction, volumetric rendering, parameter trees,
flowcharts, etc.
* pyqwt5: About as fast as pyqwt5, but not quite as complete for plotting
* pyqwt5: About as fast as pyqtgraph, but not quite as complete for plotting
functionality. Image handling in pyqtgraph is much more complete (again, no
ROI widgets in qwt). Also, pyqtgraph is written in pure python, so it is
more portable than pyqwt, which often lags behind pyqt in development (I

View File

@ -10,7 +10,7 @@ Most applications that use pyqtgraph's data visualization will generate widgets
In pyqtgraph, most 2D visualizations follow the following mouse interaction:
* **Left button:** Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead.
* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis.
* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes visible in the scene, then right-dragging over the axis will _only_ affect that axis.
* **Right button click:** Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor.
* **Middle button (or wheel) drag:** Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene).
* **Wheel spin:** Zooms the scene in and out.

View File

@ -41,7 +41,7 @@ There are several classes invloved in displaying plot data. Most of these classe
* :class:`AxisItem <pyqtgraph.AxisItem>` - Displays axis values, ticks, and labels. Most commonly used with PlotItem.
* Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs)
* :class:`PlotWidget <pyqtgraph.PlotWidget>` - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget.
* :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>` - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget.
* :class:`GraphicsLayoutWidget <pyqtgraph.GraphicsLayoutWidget>` - QWidget subclass displaying a single :class:`~pyqtgraph.GraphicsLayout`. Most of the methods provided by :class:`~pyqtgraph.GraphicsLayout` are also available through GraphicsLayoutWidget.
.. image:: images/plottingClasses.png
@ -69,5 +69,3 @@ Create/show a plot widget, display three data curves::
for i in range(3):
plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens

View File

@ -1,5 +0,0 @@
dockarea module
===============
.. automodule:: pyqtgraph.dockarea
:members:

View File

@ -12,11 +12,9 @@ Contents:
plotwidget
imageview
dockarea
spinbox
gradientwidget
histogramlutwidget
parametertree
consolewidget
colormapwidget
scatterplotwidget

View File

@ -1,5 +0,0 @@
parametertree module
====================
.. automodule:: pyqtgraph.parametertree
:members:

33
examples/DateAxisItem.py Normal file
View File

@ -0,0 +1,33 @@
"""
Demonstrates the usage of DateAxisItem to display properly-formatted
timestamps on x-axis which automatically adapt to current zoom level.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import time
from datetime import datetime, timedelta
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui
app = QtGui.QApplication([])
# Create a plot with a date-time axis
w = pg.PlotWidget(axisItems = {'bottom': pg.DateAxisItem()})
w.showGrid(x=True, y=True)
# Plot sin(1/x^2) with timestamps in the last 100 years
now = time.time()
x = np.linspace(2*np.pi, 1000*2*np.pi, 8301)
w.plot(now-(2*np.pi/x)**2*100*np.pi*1e7, np.sin(x), symbol='o')
w.setWindowTitle('pyqtgraph example: DateAxisItem')
w.show()
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
app.exec_()

View File

@ -0,0 +1,48 @@
"""
Demonstrates the usage of DateAxisItem in a layout created with Qt Designer.
The spotlight here is on the 'setAxisItems' method, without which
one would have to subclass plotWidget in order to attach a dateaxis to it.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import sys
import time
import numpy as np
from PyQt5 import QtWidgets, QtCore, uic
import pyqtgraph as pg
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
BLUE = pg.mkPen('#1f77b4')
Design, _ = uic.loadUiType('DateAxisItem_QtDesigner.ui')
class ExampleApp(QtWidgets.QMainWindow, Design):
def __init__(self):
super().__init__()
self.setupUi(self)
now = time.time()
# Plot random values with timestamps in the last 6 months
timestamps = np.linspace(now - 6*30*24*3600, now, 100)
self.curve = self.plotWidget.plot(x=timestamps, y=np.random.rand(100),
symbol='o', symbolSize=5, pen=BLUE)
# 'o' circle 't' triangle 'd' diamond '+' plus 's' square
self.plotWidget.setAxisItems({'bottom': pg.DateAxisItem()})
self.plotWidget.showGrid(x=True, y=True)
app = QtWidgets.QApplication(sys.argv)
app.setStyle(QtWidgets.QStyleFactory.create('Fusion'))
app.setPalette(QtWidgets.QApplication.style().standardPalette())
window = ExampleApp()
window.setWindowTitle('pyqtgraph example: DateAxisItem_QtDesigner')
window.show()
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
app.exec_()

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>536</width>
<height>381</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="PlotWidget" name="plotWidget"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>536</width>
<height>18</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>PlotWidget</class>
<extends>QGraphicsView</extends>
<header>pyqtgraph</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -3,8 +3,7 @@
## Add path to library (just for examples; you do not need this)
import initExample
from scipy import random
import numpy as np
from numpy import linspace
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
@ -22,7 +21,7 @@ pw = MultiPlotWidget()
mw.setCentralWidget(pw)
mw.show()
data = random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]])
data = np.random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]])
ma = MetaArray(data, info=[
{'name': 'Signal', 'cols': [
{'name': 'Col1', 'units': 'V'},

View File

@ -13,7 +13,7 @@ import numpy as np
import pyqtgraph as pg
#QtGui.QApplication.setGraphicsSystem('raster')
app = QtGui.QApplication([])
app = pg.mkQApp()
mw = QtGui.QMainWindow()
mw.setWindowTitle('pyqtgraph example: PlotWidget')
mw.resize(800,800)

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

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="1">

View File

@ -41,7 +41,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8))
self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8))
self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -36,7 +36,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.pixelModeCheck.setText(QtGui.QApplication.translate("Form", "pixel mode", None, QtGui.QApplication.UnicodeUTF8))
self.label.setText(QtGui.QApplication.translate("Form", "Size", None, QtGui.QApplication.UnicodeUTF8))
self.randCheck.setText(QtGui.QApplication.translate("Form", "Randomize", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -103,6 +103,7 @@ def mkData():
dt = np.float
loc = 1.0
scale = 0.1
mx = 1.0
if ui.rgbCheck.isChecked():
data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale)

View File

@ -9,8 +9,9 @@ import subprocess
from pyqtgraph.python2_3 import basestring
from pyqtgraph.Qt import QtGui, QT_LIB
from .utils import buildFileList, path, examples
from .syntax import PythonHighlighter
from .utils import buildFileList, testFile, path, examples
if QT_LIB == 'PySide':
from .exampleLoaderTemplate_pyside import Ui_Form
@ -21,6 +22,16 @@ elif QT_LIB == 'PyQt5':
else:
from .exampleLoaderTemplate_pyqt import Ui_Form
class App(QtGui.QApplication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.paletteChanged.connect(self.onPaletteChange)
self.onPaletteChange(self.palette())
def onPaletteChange(self, palette):
self.dark_mode = palette.base().color().name().lower() != "#ffffff"
class ExampleLoader(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
@ -33,6 +44,9 @@ class ExampleLoader(QtGui.QMainWindow):
self.codeBtn = QtGui.QPushButton('Run Edited Code')
self.codeLayout = QtGui.QGridLayout()
self.ui.codeView.setLayout(self.codeLayout)
self.hl = PythonHighlighter(self.ui.codeView.document())
app = QtGui.QApplication.instance()
app.paletteChanged.connect(self.updateTheme)
self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0)
self.codeLayout.addWidget(self.codeBtn, 1, 1)
self.codeBtn.hide()
@ -51,6 +65,28 @@ class ExampleLoader(QtGui.QMainWindow):
self.ui.codeView.textChanged.connect(self.codeEdited)
self.codeBtn.clicked.connect(self.runEditedCode)
def simulate_black_mode(self):
"""
used to simulate MacOS "black mode" on other platforms
intended for debug only, as it manage only the QPlainTextEdit
"""
# first, a dark background
c = QtGui.QColor('#171717')
p = self.ui.codeView.palette()
p.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, c)
p.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base, c)
self.ui.codeView.setPalette(p)
# then, a light font
f = QtGui.QTextCharFormat()
f.setForeground(QtGui.QColor('white'))
self.ui.codeView.setCurrentCharFormat(f)
# finally, override application automatic detection
app = QtGui.QApplication.instance()
app.dark_mode = True
def updateTheme(self):
self.hl = PythonHighlighter(self.ui.codeView.document())
def populateTree(self, root, examples):
for key, val in examples.items():
item = QtGui.QTreeWidgetItem([key])
@ -115,32 +151,9 @@ class ExampleLoader(QtGui.QMainWindow):
self.loadFile(edited=True)
def run():
app = QtGui.QApplication([])
app = App([])
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,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
This example demonstrates the creation of a plot with a customized
AxisItem and ViewBox.
This example demonstrates the creation of a plot with
DateAxisItem and a customized ViewBox.
"""
@ -12,40 +12,6 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import time
class DateAxis(pg.AxisItem):
def tickStrings(self, values, scale, spacing):
strns = []
rng = max(values)-min(values)
#if rng < 120:
# return pg.AxisItem.tickStrings(self, values, scale, spacing)
if rng < 3600*24:
string = '%H:%M:%S'
label1 = '%b %d -'
label2 = ' %b %d, %Y'
elif rng >= 3600*24 and rng < 3600*24*30:
string = '%d'
label1 = '%b - '
label2 = '%b, %Y'
elif rng >= 3600*24*30 and rng < 3600*24*30*24:
string = '%b'
label1 = '%Y -'
label2 = ' %Y'
elif rng >=3600*24*30*24:
string = '%Y'
label1 = ''
label2 = ''
for x in values:
try:
strns.append(time.strftime(string, time.localtime(x)))
except ValueError: ## Windows can't handle dates before 1970
strns.append('')
try:
label = time.strftime(label1, time.localtime(min(values)))+time.strftime(label2, time.localtime(max(values)))
except ValueError:
label = ''
#self.setLabel(text=label)
return strns
class CustomViewBox(pg.ViewBox):
def __init__(self, *args, **kwds):
pg.ViewBox.__init__(self, *args, **kwds)
@ -65,10 +31,10 @@ class CustomViewBox(pg.ViewBox):
app = pg.mkQApp()
axis = DateAxis(orientation='bottom')
axis = pg.DateAxisItem(orientation='bottom')
vb = CustomViewBox()
pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with DateAxisItem and custom ViewBox<br>Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom")
dates = np.arange(8) * (3600*24*356)
pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o')
pw.show()

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">

View File

@ -89,7 +89,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.graphicsSystemCombo.setItemText(0, _translate("Form", "default", None))
self.graphicsSystemCombo.setItemText(1, _translate("Form", "native", None))
self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster", None))

View File

@ -78,7 +78,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.graphicsSystemCombo.setItemText(0, _translate("Form", "default"))
self.graphicsSystemCombo.setItemText(1, _translate("Form", "native"))
self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster"))

View File

@ -78,7 +78,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsSystemCombo.setItemText(0, QtGui.QApplication.translate("Form", "default", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsSystemCombo.setItemText(1, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8))
self.graphicsSystemCombo.setItemText(2, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -22,7 +22,7 @@ y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40))
## Using stepMode=True causes the plot to draw two lines for each sample.
## notice that len(x) == len(y)+1
plt1.plot(x, y, stepMode=True, fillLevel=0, brush=(0,0,255,150))
plt1.plot(x, y, stepMode=True, fillLevel=0, fillOutline=True, brush=(0,0,255,150))
## Now draw all points as a nicely-spaced scatter plot
y = pg.pseudoScatter(vals, spacing=0.15)

View File

@ -96,6 +96,16 @@ params = [
{'name': 'Renamable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'renamable': True},
{'name': 'Removable', 'type': 'float', 'value': 1.2e6, 'siPrefix': True, 'suffix': 'Hz', 'removable': True},
]},
{'name': 'Custom context menu', 'type': 'group', 'children': [
{'name': 'List contextMenu', 'type': 'float', 'value': 0, 'context': [
'menu1',
'menu2'
]},
{'name': 'Dict contextMenu', 'type': 'float', 'value': 0, 'context': {
'changeName': 'Title',
'internal': 'What the user sees',
}},
]},
ComplexParameter(name='Custom parameter group (reciprocal values)'),
ScalableGroup(name="Expandable Parameter Group", children=[
{'name': 'ScalableParam 1', 'type': 'str', 'value': "default param 1"},

250
examples/syntax.py Normal file
View File

@ -0,0 +1,250 @@
# based on https://github.com/art1415926535/PyQt5-syntax-highlighting
from pyqtgraph.Qt import QtCore, QtGui
QRegExp = QtCore.QRegExp
QFont = QtGui.QFont
QColor = QtGui.QColor
QTextCharFormat = QtGui.QTextCharFormat
QSyntaxHighlighter = QtGui.QSyntaxHighlighter
def format(color, style=''):
"""
Return a QTextCharFormat with the given attributes.
"""
_color = QColor()
if type(color) is not str:
_color.setRgb(color[0], color[1], color[2])
else:
_color.setNamedColor(color)
_format = QTextCharFormat()
_format.setForeground(_color)
if 'bold' in style:
_format.setFontWeight(QFont.Bold)
if 'italic' in style:
_format.setFontItalic(True)
return _format
class LightThemeColors:
Red = "#B71C1C"
Pink = "#FCE4EC"
Purple = "#4A148C"
DeepPurple = "#311B92"
Indigo = "#1A237E"
Blue = "#0D47A1"
LightBlue = "#01579B"
Cyan = "#006064"
Teal = "#004D40"
Green = "#1B5E20"
LightGreen = "#33691E"
Lime = "#827717"
Yellow = "#F57F17"
Amber = "#FF6F00"
Orange = "#E65100"
DeepOrange = "#BF360C"
Brown = "#3E2723"
Grey = "#212121"
BlueGrey = "#263238"
class DarkThemeColors:
Red = "#F44336"
Pink = "#F48FB1"
Purple = "#CE93D8"
DeepPurple = "#B39DDB"
Indigo = "#9FA8DA"
Blue = "#90CAF9"
LightBlue = "#81D4FA"
Cyan = "#80DEEA"
Teal = "#80CBC4"
Green = "#A5D6A7"
LightGreen = "#C5E1A5"
Lime = "#E6EE9C"
Yellow = "#FFF59D"
Amber = "#FFE082"
Orange = "#FFCC80"
DeepOrange = "#FFAB91"
Brown = "#BCAAA4"
Grey = "#EEEEEE"
BlueGrey = "#B0BEC5"
LIGHT_STYLES = {
'keyword': format(LightThemeColors.Blue, 'bold'),
'operator': format(LightThemeColors.Red, 'bold'),
'brace': format(LightThemeColors.Purple),
'defclass': format(LightThemeColors.Indigo, 'bold'),
'string': format(LightThemeColors.Amber),
'string2': format(LightThemeColors.DeepPurple),
'comment': format(LightThemeColors.Green, 'italic'),
'self': format(LightThemeColors.Blue, 'bold'),
'numbers': format(LightThemeColors.Teal),
}
DARK_STYLES = {
'keyword': format(DarkThemeColors.Blue, 'bold'),
'operator': format(DarkThemeColors.Red, 'bold'),
'brace': format(DarkThemeColors.Purple),
'defclass': format(DarkThemeColors.Indigo, 'bold'),
'string': format(DarkThemeColors.Amber),
'string2': format(DarkThemeColors.DeepPurple),
'comment': format(DarkThemeColors.Green, 'italic'),
'self': format(DarkThemeColors.Blue, 'bold'),
'numbers': format(DarkThemeColors.Teal),
}
class PythonHighlighter(QSyntaxHighlighter):
"""Syntax highlighter for the Python language.
"""
# Python keywords
keywords = [
'and', 'assert', 'break', 'class', 'continue', 'def',
'del', 'elif', 'else', 'except', 'exec', 'finally',
'for', 'from', 'global', 'if', 'import', 'in',
'is', 'lambda', 'not', 'or', 'pass', 'print',
'raise', 'return', 'try', 'while', 'yield',
'None', 'True', 'False', 'async', 'await',
]
# Python operators
operators = [
'=',
# Comparison
'==', '!=', '<', '<=', '>', '>=',
# Arithmetic
'\+', '-', '\*', '/', '//', '\%', '\*\*',
# In-place
'\+=', '-=', '\*=', '/=', '\%=',
# Bitwise
'\^', '\|', '\&', '\~', '>>', '<<',
]
# Python braces
braces = [
'\{', '\}', '\(', '\)', '\[', '\]',
]
def __init__(self, document):
QSyntaxHighlighter.__init__(self, document)
# Multi-line strings (expression, flag, style)
# FIXME: The triple-quotes in these two lines will mess up the
# syntax highlighting from this point onward
self.tri_single = (QRegExp("'''"), 1, 'string2')
self.tri_double = (QRegExp('"""'), 2, 'string2')
rules = []
# Keyword, operator, and brace rules
rules += [(r'\b%s\b' % w, 0, 'keyword')
for w in PythonHighlighter.keywords]
rules += [(r'%s' % o, 0, 'operator')
for o in PythonHighlighter.operators]
rules += [(r'%s' % b, 0, 'brace')
for b in PythonHighlighter.braces]
# All other rules
rules += [
# 'self'
(r'\bself\b', 0, 'self'),
# 'def' followed by an identifier
(r'\bdef\b\s*(\w+)', 1, 'defclass'),
# 'class' followed by an identifier
(r'\bclass\b\s*(\w+)', 1, 'defclass'),
# Numeric literals
(r'\b[+-]?[0-9]+[lL]?\b', 0, 'numbers'),
(r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, 'numbers'),
(r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, 'numbers'),
# Double-quoted string, possibly containing escape sequences
(r'"[^"\\]*(\\.[^"\\]*)*"', 0, 'string'),
# Single-quoted string, possibly containing escape sequences
(r"'[^'\\]*(\\.[^'\\]*)*'", 0, 'string'),
# From '#' until a newline
(r'#[^\n]*', 0, 'comment'),
]
# Build a QRegExp for each pattern
self.rules = [(QRegExp(pat), index, fmt)
for (pat, index, fmt) in rules]
@property
def styles(self):
app = QtGui.QApplication.instance()
return DARK_STYLES if app.dark_mode else LIGHT_STYLES
def highlightBlock(self, text):
"""Apply syntax highlighting to the given block of text.
"""
# Do other syntax formatting
for expression, nth, format in self.rules:
index = expression.indexIn(text, 0)
format = self.styles[format]
while index >= 0:
# We actually want the index of the nth match
index = expression.pos(nth)
length = len(expression.cap(nth))
self.setFormat(index, length, format)
index = expression.indexIn(text, index + length)
self.setCurrentBlockState(0)
# Do multi-line strings
in_multiline = self.match_multiline(text, *self.tri_single)
if not in_multiline:
in_multiline = self.match_multiline(text, *self.tri_double)
def match_multiline(self, text, delimiter, in_state, style):
"""Do highlighting of multi-line strings. ``delimiter`` should be a
``QRegExp`` for triple-single-quotes or triple-double-quotes, and
``in_state`` should be a unique integer to represent the corresponding
state changes when inside those strings. Returns True if we're still
inside a multi-line string when this function is finished.
"""
# If inside triple-single quotes, start at 0
if self.previousBlockState() == in_state:
start = 0
add = 0
# Otherwise, look for the delimiter on this line
else:
start = delimiter.indexIn(text)
# Move past this match
add = delimiter.matchedLength()
# As long as there's a delimiter match on this line...
while start >= 0:
# Look for the ending delimiter
end = delimiter.indexIn(text, start + add)
# Ending delimiter on this line?
if end >= add:
length = end - start + add + delimiter.matchedLength()
self.setCurrentBlockState(0)
# No; multi-line string
else:
self.setCurrentBlockState(in_state)
length = len(text) - start + add
# Apply formatting
self.setFormat(start, length, self.styles[style])
# Look for the next match
start = delimiter.indexIn(text, start + length)
# Return True if still inside a multi-line string, False otherwise
if self.currentBlockState() == in_state:
return True
else:
return False

View File

@ -1,11 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import print_function, division, absolute_import
from pyqtgraph import Qt
from . import utils
from collections import namedtuple
from pyqtgraph import Qt
import errno
import importlib
import itertools
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,13 @@ 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 +55,204 @@ for frontend in frontends.keys():
except ImportError:
pass
installedFrontends = sorted([
frontend for frontend, isPresent in frontends.items() if isPresent
])
exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"])
conditionalExamples = {
"hdf5.py": exceptionCondition(
False,
reason="Example requires user interaction"
),
"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"
)
),
'GLVolumeItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLIsosurface.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLSurfacePlot.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLScatterPlotItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLshaders.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLLinePlotItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLMeshItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
),
'GLImageItem.py': exceptionCondition(
not(sys.platform == "darwin" and
sys.version_info[0] == 2 and
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
reason=(
"glClear does not work on macOS + Python2.7 + Qt4: ",
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
)
)
}
@pytest.mark.skipif(
Qt.QT_LIB == "PySide2"
and tuple(map(int, Qt.PySide2.__version__.split("."))) >= (5, 14)
and tuple(map(int, Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2),
reason="new PySide2 doesn't have loadUi functionality"
)
@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(
conditionalExamples[f[1]].condition is False,
reason=conditionalExamples[f[1]].reason
) if f[1] in conditionalExamples.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
@ -18,6 +14,7 @@ examples = OrderedDict([
('Crosshair / Mouse interaction', 'crosshair.py'),
('Data Slicing', 'DataSlicing.py'),
('Plot Customization', 'customPlot.py'),
('Timestamps on x axis', 'DateAxisItem.py'),
('Image Analysis', 'imageAnalysis.py'),
('ViewBox Features', 'ViewBoxFeatures.py'),
('Dock widgets', 'dockarea.py'),
@ -86,7 +83,6 @@ examples = OrderedDict([
#('VerticalLabel', '../widgets/VerticalLabel.py'),
('JoystickButton', 'JoystickButton.py'),
])),
('Flowcharts', 'Flowchart.py'),
('Custom Flowchart Nodes', 'FlowchartCustomNode.py'),
])
@ -103,73 +99,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

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import weakref
import warnings
from ..Qt import QtCore, QtGui
from ..python2_3 import sortList, cmp
from ..Point import Point
from .. import functions as fn
from .. import ptime as ptime
@ -88,15 +90,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
@classmethod
def registerObject(cls, obj):
"""
Workaround for PyQt bug in qgraphicsscene.items()
All subclasses of QGraphicsObject must register themselves with this function.
(otherwise, mouse interaction with those objects will likely fail)
"""
if HAVE_SIP and isinstance(obj, sip.wrapper):
cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj
warnings.warn(
"'registerObject' is deprecated and does nothing.",
DeprecationWarning, stacklevel=2
)
def __init__(self, clickRadius=2, moveDistance=5, parent=None):
QtGui.QGraphicsScene.__init__(self, parent)
self.setClickRadius(clickRadius)
@ -183,12 +181,14 @@ class GraphicsScene(QtGui.QGraphicsScene):
if int(ev.buttons() & btn) == 0:
continue
if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0]
dist = Point(ev.scenePos() - cev.scenePos()).length()
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
continue
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
self.dragButtons.append(int(btn))
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)]
if cev:
cev = cev[0]
dist = Point(ev.scenePos() - cev.scenePos()).length()
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
continue
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
self.dragButtons.append(int(btn))
## If we have dragged buttons, deliver a drag event
if len(self.dragButtons) > 0:
@ -208,10 +208,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
self.dragButtons.remove(ev.button())
else:
cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())]
if self.sendClickEvent(cev[0]):
#print "sent click event"
ev.accept()
self.clickEvents.remove(cev[0])
if cev:
if self.sendClickEvent(cev[0]):
#print "sent click event"
ev.accept()
self.clickEvents.remove(cev[0])
if int(ev.buttons()) == 0:
self.dragItem = None
@ -263,7 +264,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 +290,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
@ -364,46 +366,15 @@ class GraphicsScene(QtGui.QGraphicsScene):
return ev.isAccepted()
def items(self, *args):
#print 'args:', args
items = QtGui.QGraphicsScene.items(self, *args)
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
## then the object returned will be different than the actual item that was originally added to the scene
items2 = list(map(self.translateGraphicsItem, items))
#if HAVE_SIP and isinstance(self, sip.wrapper):
#items2 = []
#for i in items:
#addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem))
#i2 = GraphicsScene._addressCache.get(addr, i)
##print i, "==>", i2
#items2.append(i2)
#print 'items:', items
return items2
return self.translateGraphicsItems(items)
def selectedItems(self, *args):
items = QtGui.QGraphicsScene.selectedItems(self, *args)
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
## then the object returned will be different than the actual item that was originally added to the scene
#if HAVE_SIP and isinstance(self, sip.wrapper):
#items2 = []
#for i in items:
#addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem))
#i2 = GraphicsScene._addressCache.get(addr, i)
##print i, "==>", i2
#items2.append(i2)
items2 = list(map(self.translateGraphicsItem, items))
#print 'items:', items
return items2
return self.translateGraphicsItems(items)
def itemAt(self, *args):
item = QtGui.QGraphicsScene.itemAt(self, *args)
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
## then the object returned will be different than the actual item that was originally added to the scene
#if HAVE_SIP and isinstance(self, sip.wrapper):
#addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem))
#item = GraphicsScene._addressCache.get(addr, item)
#return item
return self.translateGraphicsItem(item)
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False):
@ -435,6 +406,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
@ -448,7 +421,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
return 0
return item.zValue() + absZValue(item.parentItem())
sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a)))
items2.sort(key=absZValue, reverse=True)
return items2
@ -548,15 +521,16 @@ class GraphicsScene(QtGui.QGraphicsScene):
@staticmethod
def translateGraphicsItem(item):
## for fixing pyqt bugs where the wrong item is returned
# This function is intended as a workaround for a problem with older
# versions of PyQt (< 4.9?), where methods returning 'QGraphicsItem *'
# lose the type of the QGraphicsObject subclasses and instead return
# generic QGraphicsItem wrappers.
if HAVE_SIP and isinstance(item, sip.wrapper):
addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem))
item = GraphicsScene._addressCache.get(addr, item)
obj = item.toGraphicsObject()
if obj is not None:
item = obj
return item
@staticmethod
def translateGraphicsItems(items):
return list(map(GraphicsScene.translateGraphicsItem, items))

View File

@ -22,7 +22,7 @@ class ExportDialog(QtGui.QWidget):
self.shown = False
self.currentExporter = None
self.scene = scene
self.selectBox = QtGui.QGraphicsRectItem()
self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine))
self.selectBox.hide()
@ -121,7 +121,9 @@ class ExportDialog(QtGui.QWidget):
return
expClass = self.exporterClasses[str(item.text())]
exp = expClass(item=self.ui.itemTree.currentItem().gitem)
params = exp.parameters()
if params is None:
self.ui.paramTree.clear()
else:

View File

@ -1,3 +1,4 @@
import numpy as np
class PlotData(object):
@ -50,7 +51,3 @@ class PlotData(object):
mn = np.min(self[field])
self.minVals[field] = mn
return mn

View File

@ -2,7 +2,7 @@
"""
Point.py - Extension of QPointF which adds a few missing methods.
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Distributed under MIT/X11 license. See license.txt for more information.
"""
from .Qt import QtCore

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
This module exists to smooth out some of the differences between PySide and PyQt4:
@ -9,7 +10,7 @@ This module exists to smooth out some of the differences between PySide and PyQt
"""
import os, sys, re, time
import os, sys, re, time, subprocess, warnings
from .python2_3 import asUnicode
@ -100,25 +101,42 @@ 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:
try:
import pyside2uic as pysideuic
except ImportError:
# later vserions of pyside2 have dropped pysideuic; use the uic binary instead.
pysideuic = None
# get class names from ui file
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
with open(uiFile, 'r') as f:
# convert ui file to python code
if pysideuic is None:
pyside2version = tuple(map(int, PySide2.__version__.split(".")))
if pyside2version >= (5, 14) and pyside2version < (5, 14, 2, 2):
warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15')
uipy = subprocess.check_output(['pyside2-uic', uiFile])
else:
o = _StringIO()
frame = {}
with open(uiFile, 'r') as f:
pysideuic.compileUi(f, o, indent=0)
uipy = o.getvalue()
pysideuic.compileUi(f, o, indent=0)
pyc = compile(o.getvalue(), '<string>', 'exec')
exec(pyc, frame)
# exceute python code
pyc = compile(uipy, '<string>', 'exec')
frame = {}
exec(pyc, frame)
#Fetch the base_class and form class based on their type in the xml from designer
form_class = frame['Ui_%s'%form_class]
base_class = eval('QtGui.%s'%widget_class)
# fetch the base_class and form class based on their type in the xml from designer
form_class = frame['Ui_%s'%form_class]
base_class = eval('QtGui.%s'%widget_class)
return form_class, base_class
@ -216,8 +234,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__
@ -322,9 +344,19 @@ if m is not None and list(map(int, m.groups())) < versionReq:
QAPP = None
def mkQApp():
global QAPP
def mkQApp(name=None):
"""
Creates new QApplication or returns current instance if existing.
============== ========================================================
**Arguments:**
name (str) Application name, passed to Qt
============== ========================================================
"""
global QAPP
QAPP = QtGui.QApplication.instance()
if QAPP is None:
QAPP = QtGui.QApplication([])
QAPP = QtGui.QApplication(sys.argv or ["pyqtgraph"])
if name is not None:
QAPP.setApplicationName(name)
return QAPP

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):
@ -81,7 +81,7 @@ class SignalProxy(QtCore.QObject):
except:
pass
try:
self.sigDelayed.disconnect(self.slot())
self.sigDelayed.disconnect(self.slot)
except:
pass

View File

@ -2,7 +2,7 @@
"""
Vector.py - Extension of QVector3D which adds a few missing methods.
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Distributed under MIT/X11 license. See license.txt for more information.
"""
from .Qt import QtGui, QtCore, QT_LIB

View File

@ -2,7 +2,7 @@
"""
WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Distributed under MIT/X11 license. See license.txt for more information.
This class addresses the problem of having to save and restore the state
of a large group of widgets.

View File

@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python
www.pyqtgraph.org
"""
__version__ = '0.11.0.dev0'
__version__ = '0.11.0'
### import all the goodies and add some helper functions for easy CLI use
@ -29,9 +29,6 @@ if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1]
## helpers for 2/3 compatibility
from . import python2_3
## install workarounds for numpy bugs
from . import numpy_fix
## in general openGL is poorly supported with Qt+GraphicsView.
## we only enable it where the performance benefit is critical.
## Note this only applies to 2D graphics; 3D graphics always use OpenGL.
@ -67,7 +64,6 @@ CONFIG_OPTIONS = {
def setConfigOption(opt, value):
global CONFIG_OPTIONS
if opt not in CONFIG_OPTIONS:
raise KeyError('Unknown configuration option "%s"' % opt)
if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'):
@ -99,7 +95,8 @@ def systemInfo():
if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file
lastRevFile = os.path.join(os.path.dirname(__file__), '..', '.bzr', 'branch', 'last-revision')
if os.path.exists(lastRevFile):
rev = open(lastRevFile, 'r').read().strip()
with open(lastRevFile, 'r') as fd:
rev = fd.read().strip()
print("pyqtgraph: %s; %s" % (__version__, rev))
print("config:")
@ -222,6 +219,7 @@ from .graphicsItems.ViewBox import *
from .graphicsItems.ArrowItem import *
from .graphicsItems.ImageItem import *
from .graphicsItems.AxisItem import *
from .graphicsItems.DateAxisItem import *
from .graphicsItems.LabelItem import *
from .graphicsItems.CurvePoint import *
from .graphicsItems.GraphicsWidgetAnchor import *
@ -264,6 +262,7 @@ from .widgets.LayoutWidget import *
from .widgets.TableWidget import *
from .widgets.ProgressDialog import *
from .widgets.GroupBox import GroupBox
from .widgets.RemoteGraphicsView import RemoteGraphicsView
from .imageview import *
from .WidgetGroup import *
@ -369,8 +368,12 @@ def exit():
## close file handles
if sys.platform == 'darwin':
for fd in range(3, 4096):
if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac.
if fd in [7]: # trying to close 7 produces an illegal instruction on the Mac.
continue
try:
os.close(fd)
except OSError:
pass
else:
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
@ -410,12 +413,20 @@ def plot(*args, **kargs):
dataArgs[k] = kargs[k]
w = PlotWindow(**pwArgs)
w.sigClosed.connect(_plotWindowClosed)
if len(args) > 0 or len(dataArgs) > 0:
w.plot(*args, **dataArgs)
plots.append(w)
w.show()
return w
def _plotWindowClosed(w):
w.close()
try:
plots.remove(w)
except ValueError:
pass
def image(*args, **kargs):
"""
Create and return an :class:`ImageWindow <pyqtgraph.ImageWindow>`
@ -426,11 +437,19 @@ def image(*args, **kargs):
"""
mkQApp()
w = ImageWindow(*args, **kargs)
w.sigClosed.connect(_imageWindowClosed)
images.append(w)
w.show()
return w
show = image ## for backward compatibility
def _imageWindowClosed(w):
w.close()
try:
images.remove(w)
except ValueError:
pass
def dbg(*args, **kwds):
"""
Create a console window and begin watching for exceptions.

View File

@ -1,8 +1,4 @@
# -*- coding: utf-8 -*-
if __name__ == '__main__':
import sys, os
md = os.path.dirname(os.path.abspath(__file__))
sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path
from ..Qt import QtGui, QtCore, QT_LIB
from ..graphicsItems.ROI import ROI

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="margin">

View File

@ -91,7 +91,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.autoRangeBtn.setText(_translate("Form", "Auto Range", None))
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None))
self.redirectCheck.setText(_translate("Form", "Redirect", None))

View File

@ -79,7 +79,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.autoRangeBtn.setText(_translate("Form", "Auto Range"))
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas."))
self.redirectCheck.setText(_translate("Form", "Redirect"))

View File

@ -80,7 +80,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -17,7 +17,7 @@
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">

View File

@ -59,7 +59,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.translateLabel.setText(_translate("Form", "Translate:", None))
self.rotateLabel.setText(_translate("Form", "Rotate:", None))
self.scaleLabel.setText(_translate("Form", "Scale:", None))

View File

@ -46,7 +46,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.translateLabel.setText(_translate("Form", "Translate:"))
self.rotateLabel.setText(_translate("Form", "Rotate:"))
self.scaleLabel.setText(_translate("Form", "Scale:"))

View File

@ -46,7 +46,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8))
self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8))
self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -1,6 +1,7 @@
import numpy as np
from .Qt import QtGui, QtCore
from .python2_3 import basestring
from .functions import mkColor
class ColorMap(object):
@ -56,9 +57,9 @@ class ColorMap(object):
=============== ==============================================================
**Arguments:**
pos Array of positions where each color is defined
color Array of RGBA colors.
Integer data types are interpreted as 0-255; float data types
are interpreted as 0.0-1.0
color Array of colors.
Values are interpreted via
:func:`mkColor() <pyqtgraph.mkColor>`.
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
indicating the color space that should be used when
interpolating between stops. Note that the last mode value is
@ -68,7 +69,11 @@ class ColorMap(object):
self.pos = np.array(pos)
order = np.argsort(self.pos)
self.pos = self.pos[order]
self.color = np.array(color)[order]
self.color = np.apply_along_axis(
func1d = lambda x: mkColor(x).getRgb(),
axis = -1,
arr = color,
)[order]
if mode is None:
mode = np.ones(len(pos))
self.mode = mode
@ -141,7 +146,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()
@ -225,7 +230,7 @@ class ColorMap(object):
x = np.linspace(start, stop, nPts)
table = self.map(x, mode)
if not alpha:
if not alpha and mode != self.QCOLOR:
return table[:,:3]
else:
return table

View File

@ -2,7 +2,7 @@
"""
configfile.py - Human-readable text configuration file library
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Distributed under MIT/X11 license. See license.txt for more information.
Used for reading and writing dictionary objects to a python-like configuration
file format. Data structures may be nested and contain any data type as long
@ -33,17 +33,16 @@ 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):
s = genString(data)
fd = open(fname, 'w')
fd.write(s)
fd.close()
with open(fname, 'w') as fd:
fd.write(s)
def readConfigFile(fname):
#cwd = os.getcwd()
global GLOBAL_PATH
@ -56,9 +55,8 @@ def readConfigFile(fname):
try:
#os.chdir(newDir) ## bad.
fd = open(fname)
s = asUnicode(fd.read())
fd.close()
with open(fname) as fd:
s = asUnicode(fd.read())
s = s.replace("\r\n", "\n")
s = s.replace("\r", "\n")
data = parseString(s)[1]
@ -74,9 +72,8 @@ def readConfigFile(fname):
def appendConfigFile(data, fname):
s = genString(data)
fd = open(fname, 'a')
fd.write(s)
fd.close()
with open(fname, 'a') as fd:
fd.write(s)
def genString(data, indent=''):
@ -93,13 +90,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
@ -194,8 +192,6 @@ def measureIndent(s):
if __name__ == '__main__':
import tempfile
fn = tempfile.mktemp()
tf = open(fn, 'w')
cf = """
key: 'value'
key2: ##comment
@ -205,8 +201,9 @@ key2: ##comment
key22: [1,2,3]
key23: 234 #comment
"""
tf.write(cf)
tf.close()
fn = tempfile.mktemp()
with open(fn, 'w') as tf:
tf.write(cf)
print("=== Test:===")
num = 1
for line in cf.split('\n'):

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import sys, re, os, time, traceback, subprocess
import pickle
@ -98,12 +99,14 @@ class ConsoleWidget(QtGui.QWidget):
def loadHistory(self):
"""Return the list of previously-invoked command strings (or None)."""
if self.historyFile is not None:
return pickle.load(open(self.historyFile, 'rb'))
with open(self.historyFile, 'rb') as pf:
return pickle.load(pf)
def saveHistory(self, history):
"""Store the list of previously-invoked command strings."""
if self.historyFile is not None:
pickle.dump(open(self.historyFile, 'wb'), history)
with open(self.historyFile, 'wb') as pf:
pickle.dump(pf, history)
def runCmd(self, cmd):
self.stdout = sys.stdout

View File

@ -2,7 +2,7 @@
"""
debug.py - Functions to aid in debugging
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Distributed under MIT/X11 license. See license.txt for more information.
"""
from __future__ import print_function

View File

@ -5,10 +5,10 @@ from ..widgets.VerticalLabel import VerticalLabel
from ..python2_3 import asUnicode
class Dock(QtGui.QWidget, DockDrop):
sigStretchChanged = QtCore.Signal()
sigClosed = QtCore.Signal(object)
def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False):
QtGui.QWidget.__init__(self)
DockDrop.__init__(self)
@ -68,9 +68,9 @@ class Dock(QtGui.QWidget, DockDrop):
}"""
self.setAutoFillBackground(False)
self.widgetArea.setStyleSheet(self.hStyle)
self.setStretch(*size)
if widget is not None:
self.addWidget(widget)
@ -82,34 +82,22 @@ class Dock(QtGui.QWidget, DockDrop):
return ['dock']
else:
return name == 'dock'
def setStretch(self, x=None, y=None):
"""
Set the 'target' size for this Dock.
The actual size will be determined by comparing this Dock's
stretch value to the rest of the docks it shares space with.
"""
#print "setStretch", self, x, y
#self._stretch = (x, y)
if x is None:
x = 0
if y is None:
y = 0
#policy = self.sizePolicy()
#policy.setHorizontalStretch(x)
#policy.setVerticalStretch(y)
#self.setSizePolicy(policy)
self._stretch = (x, y)
self.sigStretchChanged.emit()
#print "setStretch", self, x, y, self.stretch()
def stretch(self):
#policy = self.sizePolicy()
#return policy.horizontalStretch(), policy.verticalStretch()
return self._stretch
#def stretch(self):
#return self._stretch
def hideTitleBar(self):
"""
@ -121,7 +109,7 @@ class Dock(QtGui.QWidget, DockDrop):
if 'center' in self.allowedAreas:
self.allowedAreas.remove('center')
self.updateStyle()
def showTitleBar(self):
"""
Show the title bar for this Dock.
@ -142,7 +130,7 @@ class Dock(QtGui.QWidget, DockDrop):
Sets the text displayed in title bar for this Dock.
"""
self.label.setText(text)
def setOrientation(self, o='auto', force=False):
"""
Sets the orientation of the title bar for this Dock.
@ -150,7 +138,6 @@ class Dock(QtGui.QWidget, DockDrop):
By default ('auto'), the orientation is determined
based on the aspect ratio of the Dock.
"""
#print self.name(), "setOrientation", o, force
if o == 'auto' and self.autoOrient:
if self.container().type() == 'tab':
o = 'horizontal'
@ -162,22 +149,19 @@ class Dock(QtGui.QWidget, DockDrop):
self.orientation = o
self.label.setOrientation(o)
self.updateStyle()
def updateStyle(self):
## updates orientation and appearance of title bar
#print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible()
if self.labelHidden:
self.widgetArea.setStyleSheet(self.nStyle)
elif self.orientation == 'vertical':
self.label.setOrientation('vertical')
if self.moveLabel:
#print self.name(), "reclaim label"
self.topLayout.addWidget(self.label, 1, 0)
self.widgetArea.setStyleSheet(self.vStyle)
else:
self.label.setOrientation('horizontal')
if self.moveLabel:
#print self.name(), "reclaim label"
self.topLayout.addWidget(self.label, 0, 1)
self.widgetArea.setStyleSheet(self.hStyle)
@ -203,13 +187,12 @@ class Dock(QtGui.QWidget, DockDrop):
def startDrag(self):
self.drag = QtGui.QDrag(self)
mime = QtCore.QMimeData()
#mime.setPlainText("asd")
self.drag.setMimeData(mime)
self.widgetArea.setStyleSheet(self.dragStyle)
self.update()
action = self.drag.exec_()
self.updateStyle()
def float(self):
self.area.floatDock(self)
@ -220,7 +203,6 @@ class Dock(QtGui.QWidget, DockDrop):
if self._container is not None:
# ask old container to close itself if it is no longer needed
self._container.apoptose()
#print self.name(), "container changed"
self._container = c
if c is None:
self.area = None
@ -241,6 +223,7 @@ class Dock(QtGui.QWidget, DockDrop):
def close(self):
"""Remove this dock from the DockArea it lives inside."""
self.setParent(None)
QtGui.QLabel.close(self.label)
self.label.setParent(None)
self._container.apoptose()
self._container = None
@ -265,10 +248,10 @@ class Dock(QtGui.QWidget, DockDrop):
class DockLabel(VerticalLabel):
sigClicked = QtCore.Signal(object, object)
sigCloseClicked = QtCore.Signal()
def __init__(self, text, dock, showCloseButton):
self.dim = False
self.fixedWidth = False
@ -295,7 +278,7 @@ class DockLabel(VerticalLabel):
fg = '#fff'
bg = '#66c'
border = '#55B'
if self.orientation == 'vertical':
self.vStyle = """DockLabel {
background-color : %s;
@ -329,7 +312,7 @@ class DockLabel(VerticalLabel):
if self.dim != d:
self.dim = d
self.updateStyle()
def setOrientation(self, o):
VerticalLabel.setOrientation(self, o)
self.updateStyle()
@ -339,21 +322,21 @@ class DockLabel(VerticalLabel):
self.pressPos = ev.pos()
self.startedDrag = False
ev.accept()
def mouseMoveEvent(self, ev):
if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
self.dock.startDrag()
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:
self.dock.float()
def resizeEvent (self, ev):
if self.closeButton:
if self.orientation == 'vertical':

View File

@ -9,9 +9,9 @@ from ..python2_3 import basestring
class DockArea(Container, QtGui.QWidget, DockDrop):
def __init__(self, temporary=False, home=None):
def __init__(self, parent=None, temporary=False, home=None):
Container.__init__(self, self)
QtGui.QWidget.__init__(self)
QtGui.QWidget.__init__(self, parent=parent)
DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom'])
self.layout = QtGui.QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)

View File

@ -14,3 +14,15 @@ def test_dock():
assert dock.name() == name
# no surprises in return type.
assert type(dock.name()) == type(name)
def test_closable_dock():
name = "Test close dock"
dock = da.Dock(name=name, closable=True)
assert dock.label.closeButton != None
def test_hide_title_dock():
name = "Test hide title dock"
dock = da.Dock(name=name, hideTitle=True)
assert dock.labelHidden == True

View File

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore
from .Exporter import Exporter
from ..parametertree import Parameter
from .. import PlotItem
from ..python2_3 import asUnicode
__all__ = ['CSVExporter']
@ -29,7 +31,6 @@ class CSVExporter(Exporter):
self.fileSaveDialog(filter=["*.csv", "*.tsv"])
return
fd = open(fileName, 'w')
data = []
header = []
@ -55,28 +56,29 @@ class CSVExporter(Exporter):
sep = ','
else:
sep = '\t'
fd.write(sep.join(header) + '\n')
i = 0
numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data])
for i in range(numRows):
for j, d in enumerate(data):
# write x value if this is the first column, or if we want x
# for all rows
if appendAllX or j == 0:
if d is not None and i < len(d[0]):
fd.write(numFormat % d[0][i] + sep)
with open(fileName, 'w') as fd:
fd.write(sep.join(map(asUnicode, header)) + '\n')
i = 0
numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data])
for i in range(numRows):
for j, d in enumerate(data):
# write x value if this is the first column, or if we want
# x for all rows
if appendAllX or j == 0:
if d is not None and i < len(d[0]):
fd.write(numFormat % d[0][i] + sep)
else:
fd.write(' %s' % sep)
# write y value
if d is not None and i < len(d[1]):
fd.write(numFormat % d[1][i] + sep)
else:
fd.write(' %s' % sep)
# write y value
if d is not None and i < len(d[1]):
fd.write(numFormat % d[1][i] + sep)
else:
fd.write(' %s' % sep)
fd.write('\n')
fd.close()
fd.write('\n')
CSVExporter.register()

View File

@ -44,20 +44,27 @@ class HDF5Exporter(Exporter):
data = []
appendAllX = self.params['columnMode'] == '(x,y) per plot'
#print dir(self.item.curves[0])
tlen = 0
for i, c in enumerate(self.item.curves):
d = c.getData()
if i > 0 and len(d[0]) != tlen:
raise ValueError ("HDF5 Export requires all curves in plot to have same length")
if appendAllX or i == 0:
data.append(d[0])
tlen = len(d[0])
data.append(d[1])
# Check if the arrays are ragged
len_first = len(self.item.curves[0].getData()[0]) if self.item.curves[0] else None
ragged = any(len(i.getData()[0]) != len_first for i in self.item.curves)
if ragged:
dgroup = fd.create_group(dsname)
for i, c in enumerate(self.item.curves):
d = c.getData()
fdata = numpy.array([d[0], d[1]]).astype('double')
cname = c.name() if c.name() is not None else str(i)
dset = dgroup.create_dataset(cname, data=fdata)
else:
for i, c in enumerate(self.item.curves):
d = c.getData()
if appendAllX or i == 0:
data.append(d[0])
data.append(d[1])
fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata)
fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata)
fd.close()
if HAVE_HDF5:

View File

@ -44,31 +44,33 @@ class ImageExporter(Exporter):
def parameters(self):
return self.params
@staticmethod
def getSupportedImageFormats():
filter = ["*."+f.data().decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]:
if p in filter:
filter.remove(p)
filter.insert(0, p)
return filter
def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None and not toBytes and not copy:
if QT_LIB in ['PySide', 'PySide2']:
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
else:
filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]:
if p in filter:
filter.remove(p)
filter.insert(0, p)
filter = self.getSupportedImageFormats()
self.fileSaveDialog(filter=filter)
return
targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height'])
sourceRect = self.getSourceRect()
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
#self.png.fill(pyqtgraph.mkColor(self.params['background']))
w, h = self.params['width'], self.params['height']
w = int(self.params['width'])
h = int(self.params['height'])
if w == 0 or h == 0:
raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h))
bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte)
raise Exception("Cannot export image with size=0 (requested "
"export size is %dx%d)" % (w, h))
targetRect = QtCore.QRect(0, 0, w, h)
sourceRect = self.getSourceRect()
bg = np.empty((h, w, 4), dtype=np.ubyte)
color = self.params['background']
bg[:,:,0] = color.blue()
bg[:,:,1] = color.green()
@ -105,7 +107,7 @@ class ImageExporter(Exporter):
elif toBytes:
return self.png
else:
self.png.save(fileName)
return self.png.save(fileName)
ImageExporter.register()

View File

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

View File

@ -69,6 +69,13 @@ xmlHeader = """\
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
<title>pyqtgraph SVG export</title>
<desc>Generated with Qt and pyqtgraph</desc>
<style>
image {
image-rendering: crisp-edges;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
}
</style>
"""
def generateSvg(item, options={}):
@ -190,12 +197,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

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
import pytest
import pyqtgraph as pg
from pyqtgraph.exporters import HDF5Exporter
import numpy as np
from numpy.testing import assert_equal
import h5py
import os
@pytest.fixture
def tmp_h5(tmp_path):
yield tmp_path / "data.h5"
@pytest.mark.parametrize("combine", [False, True])
def test_HDF5Exporter(tmp_h5, combine):
# Basic test of functionality: multiple curves with shared x array. Tests
# both options for stacking the data (columnMode).
x = np.linspace(0, 1, 100)
y1 = np.sin(x)
y2 = np.cos(x)
plt = pg.plot()
plt.plot(x=x, y=y1)
plt.plot(x=x, y=y2)
ex = HDF5Exporter(plt.plotItem)
if combine:
ex.parameters()['columnMode'] = '(x,y,y,y) for all plots'
ex.export(fileName=tmp_h5)
with h5py.File(tmp_h5, 'r') as f:
# should be a single dataset with the name of the exporter
dset = f[ex.parameters()['Name']]
assert isinstance(dset, h5py.Dataset)
if combine:
assert_equal(np.array([x, y1, y2]), dset)
else:
assert_equal(np.array([x, y1, x, y2]), dset)
def test_HDF5Exporter_unequal_lengths(tmp_h5):
# Test export with multiple curves of different size. The exporter should
# detect this and create multiple hdf5 datasets under a group.
x1 = np.linspace(0, 1, 10)
y1 = np.sin(x1)
x2 = np.linspace(0, 1, 100)
y2 = np.cos(x2)
plt = pg.plot()
plt.plot(x=x1, y=y1, name='plot0')
plt.plot(x=x2, y=y2)
ex = HDF5Exporter(plt.plotItem)
ex.export(fileName=tmp_h5)
with h5py.File(tmp_h5, 'r') as f:
# should be a group with the name of the exporter
group = f[ex.parameters()['Name']]
assert isinstance(group, h5py.Group)
# should be a dataset under the group with the name of the PlotItem
assert_equal(np.array([x1, y1]), group['plot0'])
# should be a dataset under the group with a default name that's the
# index of the curve in the PlotItem
assert_equal(np.array([x2, y2]), group['1'])

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
import pyqtgraph as pg
from pyqtgraph.exporters import ImageExporter
app = pg.mkQApp()
def test_ImageExporter_filename_dialog():
"""Tests ImageExporter code path that opens a file dialog. Regression test
for pull request 1133."""
p = pg.plot()
exp = ImageExporter(p.getPlotItem())
exp.export()

View File

@ -28,6 +28,7 @@ def test_plotscene():
ex.export(fileName=tempfilename)
# clean up after the test is done
os.unlink(tempfilename)
w.close()
def test_simple():
tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name

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,12 +503,12 @@ 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):
"""Load a flowchart (*.fc) file.
"""Load a flowchart (``*.fc``) file.
"""
if fileName is None:
if startDir is 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:
@ -761,6 +763,9 @@ class FlowchartCtrlWidget(QtGui.QWidget):
item = self.items[node]
self.ui.ctrlList.setCurrentItem(item)
def clearSelection(self):
self.ui.ctrlList.selectionModel().clearSelection()
class FlowchartWidget(dockarea.DockArea):
"""Includes the actual graphical flowchart and debugging interface"""
@ -832,9 +837,9 @@ class FlowchartWidget(dockarea.DockArea):
def buildMenu(self, pos=None):
def buildSubMenu(node, rootMenu, subMenus, pos=None):
for section, node in node.items():
menu = QtGui.QMenu(section)
rootMenu.addMenu(menu)
if isinstance(node, OrderedDict):
if isinstance(node, OrderedDict):
menu = QtGui.QMenu(section)
rootMenu.addMenu(menu)
buildSubMenu(node, menu, subMenus, pos=pos)
subMenus.append(menu)
else:
@ -888,7 +893,10 @@ class FlowchartWidget(dockarea.DockArea):
item = items[0]
if hasattr(item, 'node') and isinstance(item.node, Node):
n = item.node
self.ctrl.select(n)
if n in self.ctrl.items:
self.ctrl.select(n)
else:
self.ctrl.clearSelection()
data = {'outputs': n.outputValues(), 'inputs': n.inputValues()}
self.selNameLabel.setText(n.name())
if hasattr(n, 'nodeName'):
@ -936,4 +944,3 @@ class FlowchartWidget(dockarea.DockArea):
class FlowchartNode(Node):
pass

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="verticalSpacing">

View File

@ -69,7 +69,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
self.loadBtn.setText(_translate("Form", "Load..", None))
self.saveBtn.setText(_translate("Form", "Save", None))
self.saveAsBtn.setText(_translate("Form", "As..", None))

View File

@ -56,7 +56,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
self.loadBtn.setText(_translate("Form", "Load.."))
self.saveBtn.setText(_translate("Form", "Save"))
self.saveAsBtn.setText(_translate("Form", "As.."))

View File

@ -55,7 +55,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8))
self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8))
self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
<string>PyQtGraph</string>
</property>
<widget class="QWidget" name="selInfoWidget" native="true">
<property name="geometry">

View File

@ -62,7 +62,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None))
Form.setWindowTitle(_translate("Form", "PyQtGraph", None))
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
from ..widgets.DataTreeWidget import DataTreeWidget

View File

@ -49,7 +49,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Form"))
Form.setWindowTitle(_translate("Form", "PyQtGraph"))
from ..widgets.DataTreeWidget import DataTreeWidget
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView

View File

@ -48,7 +48,7 @@ class Ui_Form(object):
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQtGraph", None, QtGui.QApplication.UnicodeUTF8))
from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView
from ..widgets.DataTreeWidget import DataTreeWidget

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

@ -2,6 +2,7 @@
from ..Node import Node
from ...Qt import QtGui, QtCore
import numpy as np
import sys
from .common import *
from ...SRTTransform import SRTTransform
from ...Point import Point
@ -238,7 +239,12 @@ class EvalNode(Node):
fn = "def fn(**args):\n"
run = "\noutput=fn(**args)\n"
text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run
exec(text)
if sys.version_info.major == 2:
exec(text)
elif sys.version_info.major == 3:
ldict = locals()
exec(text, globals(), ldict)
output = ldict['output']
except:
print("Error processing node: %s" % self.name())
raise

View File

@ -91,14 +91,15 @@ class CtrlNode(Node):
sigStateChanged = QtCore.Signal(object)
def __init__(self, name, ui=None, terminals=None):
if terminals is None:
terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}}
Node.__init__(self, name=name, terminals=terminals)
if ui is None:
if hasattr(self, 'uiTemplate'):
ui = self.uiTemplate
else:
ui = []
if terminals is None:
terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}}
Node.__init__(self, name=name, terminals=terminals)
self.ui, self.stateGroup, self.ctrls = generateUi(ui)
self.stateGroup.sigChanged.connect(self.changed)

View File

@ -2,7 +2,7 @@
"""
functions.py - Miscellaneous functions with no other home
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
Distributed under MIT/X11 license. See license.txt for more information.
"""
from __future__ import division
@ -11,6 +11,7 @@ import numpy as np
import decimal, re
import ctypes
import sys, struct
from .pgcollections import OrderedDict
from .python2_3 import asUnicode, basestring
from .Qt import QtGui, QtCore, QT_LIB
from . import getConfigOption, setConfigOptions
@ -76,9 +77,10 @@ def siScale(x, minVal=1e-25, allowUnicode=True):
pref = SI_PREFIXES[m+8]
else:
pref = SI_PREFIXES_ASCII[m+8]
p = .001**m
m1 = -3*m
p = 10.**m1
return (p, pref)
return (p, pref)
def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True):
@ -424,6 +426,8 @@ def eq(a, b):
3. When comparing arrays, returns False if the array shapes are not the same.
4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
the == operator would return a boolean array).
5. Collections (dict, list, etc.) must have the same type to be considered equal. One
consequence is that comparing a dict to an OrderedDict will always return False.
"""
if a is b:
return True
@ -440,6 +444,28 @@ def eq(a, b):
if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype):
return False
# Recursively handle common containers
if isinstance(a, dict) and isinstance(b, dict):
if type(a) != type(b) or len(a) != len(b):
return False
if set(a.keys()) != set(b.keys()):
return False
for k, v in a.items():
if not eq(v, b[k]):
return False
if isinstance(a, OrderedDict) or sys.version_info >= (3, 7):
for a_item, b_item in zip(a.items(), b.items()):
if not eq(a_item, b_item):
return False
return True
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
if type(a) != type(b) or len(a) != len(b):
return False
for v1,v2 in zip(a, b):
if not eq(v1, v2):
return False
return True
# Test for equivalence.
# If the test raises a recognized exception, then return Falase
try:
@ -909,10 +935,12 @@ def solveBilinearTransform(points1, points2):
return matrix
def rescaleData(data, scale, offset, dtype=None, clip=None):
"""Return data rescaled and optionally cast to a new dtype::
"""Return data rescaled and optionally cast to a new dtype.
The scaling operation is::
data => (data-offset) * scale
"""
if dtype is None:
dtype = data.dtype
@ -1035,7 +1063,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
============== ==================================================================================
"""
profile = debug.Profiler()
if data.ndim not in (2, 3):
raise TypeError("data must be 2D or 3D")
if data.ndim == 3 and data.shape[2] > 4:
@ -1057,6 +1084,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')
@ -1073,7 +1101,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
# Decide on maximum scaled value
if scale is None:
if lut is not None:
scale = lut.shape[0] - 1
scale = lut.shape[0]
else:
scale = 255.
@ -1082,7 +1110,14 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
dtype = np.ubyte
else:
dtype = np.min_scalar_type(lut.shape[0]-1)
# awkward, but fastest numpy native nan evaluation
#
nanMask = None
if data.dtype.kind == 'f' and np.isnan(data.min()):
nanMask = np.isnan(data)
if data.ndim > 2:
nanMask = np.any(nanMask, axis=-1)
# Apply levels if given
if levels is not None:
if isinstance(levels, np.ndarray) and levels.ndim == 2:
@ -1093,7 +1128,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,12 +1138,12 @@ 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
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
maxVal = np.nextafter(maxVal, 2*maxVal)
rng = maxVal-minVal
rng = 1 if rng == 0 else rng
data = rescaleData(data, scale/rng, minVal, dtype=dtype)
profile()
# apply LUT if given
if lut is not None:
data = applyLookupTable(data, lut)
@ -1151,7 +1186,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
imgData[..., 3] = 255
else:
alpha = True
# apply nan mask through alpha channel
if nanMask is not None:
alpha = True
imgData[nanMask, 3] = 0
profile()
return imgData, alpha
@ -1222,30 +1262,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
if QT_LIB in ['PySide', 'PySide2']:
ch = ctypes.c_char.from_buffer(imgData, 0)
# Bug in PySide + Python 3 causes refcount for image data to be improperly
# incremented, which leads to leaked memory. As a workaround, we manually
# reset the reference count after creating the QImage.
# See: https://bugreports.qt.io/browse/PYSIDE-140
# Get initial reference count (PyObject struct has ob_refcnt as first element)
rcount = ctypes.c_long.from_address(id(ch)).value
img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat)
if sys.version[0] == '3':
# Reset refcount only on python 3. Technically this would have no effect
# on python 2, but this is a nasty hack, and checking for version here
# helps to mitigate possible unforseen consequences.
ctypes.c_long.from_address(id(ch)).value = rcount
else:
#addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was)
## So we first attempt the 4.9.6 API, then fall back to 4.9.3
#addr = ctypes.c_char.from_buffer(imgData, 0)
#try:
#img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
#except TypeError:
#addr = ctypes.addressof(addr)
#img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
try:
img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat)
except:
@ -1258,16 +1278,6 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
img.data = imgData
return img
#try:
#buf = imgData.data
#except AttributeError: ## happens when image data is non-contiguous
#buf = imgData.data
#profiler()
#qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
#profiler()
#qimage.data = imgData
#return qimage
def imageToArray(img, copy=False, transpose=True):
"""
@ -1380,7 +1390,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
@ -2468,6 +2478,3 @@ class SignalBlock(object):
def __exit__(self, *args):
if self.reconnect:
self.signal.connect(self.slot)

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More