Merge branch 'master' into develop

This commit is contained in:
Ogi Moore 2020-06-22 20:37:55 -07:00 committed by GitHub
commit 1fe3731ec2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
186 changed files with 4310 additions and 1732 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,189 +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
env:
# Enable python 2 and python 3 builds
# Note that the python 2.6 support ended.
- PYTHON=2.7 QT=pyqt4 TEST=extra
- PYTHON=2.7 QT=pyside TEST=standard
- PYTHON=3.5 QT=pyqt5 TEST=standard
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
#- PYTHON=3.2 QT=pyqt5 TEST=standard
services:
- xvfb
before_install:
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi
- 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
- pip install pytest-faulthandler # activate faulthandler
# 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
- /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

View File

@ -1,6 +1,6 @@
# Contributing to PyQtGraph
Contributions to pyqtgraph are welcome!
Contributions to pyqtgraph are welcome!
Please use the following guidelines when preparing changes:
@ -9,15 +9,20 @@ 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.
* 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 nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code.
* 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
@ -33,9 +38,15 @@ Please use the following guidelines when preparing changes:
============== ========================================================
```
QObject subclasses that implement new signals should also describe
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
@ -45,9 +56,10 @@ Please use the following guidelines when preparing changes:
* pytest
* pytest-cov
* pytest-xdist
* pytest-faulthandler
* 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.
@ -57,13 +69,4 @@ As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make
### Continous Integration
For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations
* Python2.7 with PyQt4
* Python2.7 with PySide
* Python3.6 with PyQt5-5.9
* Python3.6 with PySide2-5.9
* Python3.7 with PyQt5-5.12
* Python3.7 with PySide2-5.12
More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1)
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,13 +1,13 @@
[![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/PyQt5/PySide2
Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill
Copyright 2020 Luke Campagnola, University of North Carolina at Chapel Hill
<http://www.pyqtgraph.org>
@ -19,29 +19,31 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for
Requirements
------------
* PyQt 4.8+, PySide, PyQt5, or PySide2
* python 2.7, or 3.x
* Python 2.7, or 3.x
* Required
* `numpy`, `scipy`
* PyQt 4.8+, PySide, PyQt5, or PySide2
* `numpy`
* Optional
* `scipy` for image processing
* `pyopengl` for 3D graphics
* `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy`
* `hdf5` for large hdf5 binary format support
* Known to run on Windows, Linux, and macOS.
Qt Bindings Test Matrix
-----------------------
Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings.
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.
| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 |
| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: |
| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: |
| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| 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 incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible.
* 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
-------
@ -54,18 +56,17 @@ Installation Methods
* From PyPI:
* Last released version: `pip install pyqtgraph`
* Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop`
* Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@master`
* From conda
* Last released version: `conda install pyqtgraph`
* 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)
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 https://pyqtgraph.readthedocs.io
The official documentation lives at http://pyqtgraph.org/documentation
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` to launch the examples application.

View File

@ -1,7 +1,3 @@
############################################################################################
# This config was rectrieved in no small part from https://github.com/slaclab/pydm
############################################################################################
trigger:
branches:
include:
@ -20,19 +16,84 @@ pr:
variables:
OFFICIAL_REPO: 'pyqtgraph/pyqtgraph'
DEFAULT_MERGE_BRANCH: 'develop'
disable.coverage.autogenerate: 'true'
jobs:
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 16.04'
name: linux
vmImage: 'Ubuntu 18.04'
- template: azure-test-template.yml
parameters:
name: Windows
vmImage: 'vs2017-win2016'
name: windows
vmImage: 'windows-2019'
- template: azure-test-template.yml
parameters:
name: MacOS
vmImage: 'macOS-10.13'
name: macOS
vmImage: 'macOS-10.15'

View File

@ -18,24 +18,30 @@ jobs:
python.version: '2.7'
qt.bindings: "pyside"
install.method: "conda"
Python36-PyQt-5.9:
Python36-PyQt5-5.9:
python.version: "3.6"
qt.bindings: "pyqt"
install.method: "conda"
Python36-PySide2-5.9:
python.version: "3.6"
Python37-PySide2-5.13:
python.version: "3.7"
qt.bindings: "pyside2"
install.method: "conda"
Python37-PyQt-5.12:
python.version: '3.7'
Python38-PyQt5-Latest:
python.version: '3.8'
qt.bindings: "PyQt5"
install.method: "pip"
Python37-PySide2-5.12:
python.version: "3.7"
Python38-PySide2-Latest:
python.version: '3.8'
qt.bindings: "PySide2"
install.method: "pip"
install.method: "pip"
steps:
- task: DownloadPipelineArtifact@2
inputs:
source: 'current'
artifact: wheel
path: 'dist'
- task: ScreenResolutionUtility@1
inputs:
displaySettings: 'specific'
@ -43,6 +49,11 @@ jobs:
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
@ -60,81 +71,81 @@ jobs:
displayName: "Install Windows-Mesa OpenGL DLL"
condition: eq(variables['agent.os'], 'Windows_NT')
- task: UsePythonVersion@0
inputs:
versionSpec: $(python.version)
condition: eq(variables['install.method'], 'pip')
- bash: |
if [ $(agent.os) == 'Linux' ]
then
echo '##vso[task.prependpath]/usr/share/miniconda/bin'
echo "##vso[task.prependpath]$CONDA/bin"
elif [ $(agent.os) == 'Darwin' ]
then
echo '##vso[task.prependpath]$CONDA/bin'
sudo install -d -m 0777 /usr/local/miniconda/envs
sudo chown -R $USER $CONDA
echo "##vso[task.prependpath]$CONDA/bin"
elif [ $(agent.os) == 'Windows_NT' ]
then
echo "##vso[task.prependpath]$env:CONDA\Scripts"
echo "##vso[task.prependpath]$CONDA/Scripts"
else
echo 'Just what OS are you using?'
fi
displayName: 'Add Conda to $PATH'
displayName: 'Add Conda To $PATH'
condition: eq(variables['install.method'], 'conda' )
- task: CondaEnvironment@0
displayName: 'Create Conda Environment'
condition: eq(variables['install.method'], 'conda')
inputs:
environmentName: 'test-environment-$(python.version)'
packageSpecs: 'python=$(python.version)'
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 install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet
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 pytest flake8 six coverage
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
pip install pytest-xdist pytest-cov pytest-faulthandler
displayName: "Install Dependencies"
- bash: |
if [ $(install.method) == "conda" ]
then
source activate test-environment-$(python.version)
fi
pip install setuptools wheel
python setup.py bdist_wheel
pip install dist/*.whl
displayName: 'Build Wheel and Install'
- task: CopyFiles@2
inputs:
contents: 'dist/**'
targetFolder: $(Build.ArtifactStagingDirectory)
cleanTargetFolder: true
displayName: "Copy Binary Wheel Distribution To Artifacts"
- task: PublishBuildArtifacts@1
displayName: 'Publish Binary Wheel'
condition: always()
inputs:
pathtoPublish: $(Build.ArtifactStagingDirectory)/dist
artifactName: Distributions
python -m pip install --no-index --find-links=dist pyqtgraph
displayName: 'Install Wheel'
- bash: |
sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm
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
pip install pytest-xvfb
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)
@ -167,9 +178,10 @@ jobs:
mkdir -p "$SCREENSHOT_DIR"
# echo "If Screenshots are generated, they may be downloaded from:"
# echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0"
pytest . -sv \
pytest . -v \
-n 1 \
--junitxml=junit/test-results.xml \
-n 1 --cov pyqtgraph --cov-report=xml --cov-report=html
--cov pyqtgraph --cov-report=xml --cov-report=html
displayName: 'Unit tests'
env:
AZURE: 1
@ -193,4 +205,4 @@ jobs:
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
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,10 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
import time
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 +22,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 +47,19 @@ master_doc = 'index'
# General information about the project.
project = 'pyqtgraph'
copyright = '2011, Luke Campagnola'
now = datetime.utcfromtimestamp(
int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
)
copyright = '2011 - {}, Luke Campagnola'.format(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 +95,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 +137,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 +233,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,55 +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 you have three choice::
1. Clone pyqtgraph from github::
$ git clone https://github.com/pyqtgraph/pyqtgraph
Now you can install pyqtgraph from the source::
$ python setup.py install
2. Directly install from GitHub repo::
$ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop
You can change to ``develop`` of the above command to the branch
name or the commit you prefer.
3.
You can simply place the pyqtgraph folder someplace importable, such as
inside the root of another project. PyQtGraph does not need to be "built" or
compiled in any way.
* Packages for pyqtgraph are also available in a few other forms:
* **Anaconda**: ``conda install pyqtgraph``
* **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:

View File

@ -30,7 +30,7 @@ p2 = cw.addPlot(row=1, col=0)
## variety of arrow shapes
a1 = pg.ArrowItem(angle=-160, tipAngle=60, headLen=40, tailLen=40, tailWidth=20, pen={'color': 'w', 'width': 3})
a2 = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, headLen=40, tailLen=40, tailWidth=8, pen=None, brush='y')
a3 = pg.ArrowItem(angle=-60, tipAngle=30, baseAngle=20, headLen=40, tailLen=None, brush=None)
a3 = pg.ArrowItem(angle=-60, baseAngle=20, headLen=40, headWidth=20, tailLen=None, brush=None)
a4 = pg.ArrowItem(angle=-20, tipAngle=30, baseAngle=-30, headLen=40, tailLen=None)
a2.setPos(10,0)
a3.setPos(20,0)

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

@ -7,17 +7,37 @@ import initExample ## Add path to library (just for examples; you do not need th
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
plt = pg.plot()
plt.setWindowTitle('pyqtgraph example: Legend')
plt.addLegend()
#l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset)
#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case
win = pg.plot()
win.setWindowTitle('pyqtgraph example: BarGraphItem')
c1 = plt.plot([1,3,2,4], pen='r', symbol='o', symbolPen='r', symbolBrush=0.5, name='red plot')
c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot')
#l.addItem(c1, 'red plot')
#l.addItem(c2, 'green plot')
# # option1: only for .plot(), following c1,c2 for example-----------------------
# win.addLegend(frame=False, rowCount=1, colCount=2)
# bar graph
x = np.arange(10)
y = np.sin(x+2) * 3
bg1 = pg.BarGraphItem(x=x, height=y, width=0.3, brush='b', pen='w', name='bar')
win.addItem(bg1)
# curve
c1 = win.plot([np.random.randint(0,8) for i in range(10)], pen='r', symbol='t', symbolPen='r', symbolBrush='g', name='curve1')
c2 = win.plot([2,1,4,3,1,3,2,4,3,2], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='curve2')
# scatter plot
s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120), name='scatter')
spots = [{'pos': [i, np.random.randint(-3, 3)], 'data': 1} for i in range(10)]
s1.addPoints(spots)
win.addItem(s1)
# # option2: generic method------------------------------------------------
legend = pg.LegendItem((80,60), offset=(70,20))
legend.setParentItem(win.graphicsItem())
legend.addItem(bg1, 'bar')
legend.addItem(c1, 'curve1')
legend.addItem(c2, 'curve2')
legend.addItem(s1, 'scatter')
## Start Qt event loop unless running in interactive mode or using pyside.

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

@ -83,7 +83,7 @@ random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True)
pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)]
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%10, 'size': 5+i/10.} for i in range(n)]
s2.addPoints(spots)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]]
s2.addPoints(spots)
@ -120,4 +120,3 @@ if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

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

@ -29,6 +29,10 @@ plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen
plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'")
plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'")
plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'")
plot.plot([11, 12, 13, 14, 15], pen=(253, 216, 53), symbolBrush=(253, 216, 53), symbolPen='w', symbol='arrow_down', symbolSize=22, name="symbol='arrow_down'")
plot.plot([12, 13, 14, 15, 16], pen=(189, 189, 189), symbolBrush=(189, 189, 189), symbolPen='w', symbol='arrow_left', symbolSize=22, name="symbol='arrow_left'")
plot.plot([13, 14, 15, 16, 17], pen=(187, 26, 95), symbolBrush=(187, 26, 95), symbolPen='w', symbol='arrow_up', symbolSize=22, name="symbol='arrow_up'")
plot.plot([14, 15, 16, 17, 18], pen=(248, 187, 208), symbolBrush=(248, 187, 208), symbolPen='w', symbol='arrow_right', symbolSize=22, name="symbol='arrow_right'")
plot.setXRange(-2, 4)
## Start Qt event loop unless running in interactive mode or using pyside.

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

@ -10,6 +10,7 @@ from pyqtgraph.python2_3 import basestring
from pyqtgraph.Qt import QtGui, QT_LIB
from .utils import buildFileList, path, examples
from .syntax import PythonHighlighter
if QT_LIB == 'PySide':
@ -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,7 +151,7 @@ class ExampleLoader(QtGui.QMainWindow):
self.loadFile(edited=True)
def run():
app = QtGui.QApplication([])
app = App([])
loader = ExampleLoader()
app.exec_()

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,11 @@
# -*- 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 pkgutil
import pytest
import os, sys
import subprocess
@ -41,7 +41,12 @@ if os.getenv('TRAVIS') is not None:
files = sorted(set(utils.buildFileList(utils.examples)))
frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False}
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:
@ -50,41 +55,142 @@ for frontend in frontends.keys():
except ImportError:
pass
installedFrontends = sorted([frontend for frontend, isPresent in frontends.items() if isPresent])
installedFrontends = sorted([
frontend for frontend, isPresent in frontends.items() if isPresent
])
exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"])
conditionalExampleTests = {
"hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"),
"RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"),
"optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671")
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",
[
pytest.param(
frontend,
"frontend, f",
[
pytest.param(
frontend,
f,
marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False,
reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (),
)
for frontend, f, in itertools.product(installedFrontends, files)
],
ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)]
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)
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
graphicsSystem = (
'' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
)
code = """
try:
%s
@ -116,7 +222,7 @@ except:
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)
process.stdin.write(code.encode('UTF-8'))
process.stdin.close() ##?
process.stdin.close()
output = ''
fail = False
while True:
@ -139,10 +245,14 @@ except:
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():
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)
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

@ -14,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'),

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
@ -365,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):
@ -451,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
@ -551,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
@ -104,24 +105,38 @@ def _loadUiType(uiFile):
if QT_LIB == "PYSIDE":
import pysideuic
else:
import pyside2uic as pysideuic
import xml.etree.ElementTree as xml
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
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
@ -329,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

@ -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 *
@ -414,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>`
@ -430,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
@ -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
@ -39,10 +39,10 @@ class ParseError(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
@ -55,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]
@ -73,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=''):
@ -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

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from ..Qt import QtCore, QtGui
from .DockDrop import *
@ -5,10 +6,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 +69,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 +83,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 +110,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 +131,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 +139,12 @@ 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
# setOrientation may be called before the container is set in some cases
# (via resizeEvent), so there's no need to do anything here until called
# again by containerChanged
if self.container() is None:
return
if o == 'auto' and self.autoOrient:
if self.container().type() == 'tab':
o = 'horizontal'
@ -162,22 +156,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 +194,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 +210,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 +230,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 +255,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 +285,7 @@ class DockLabel(VerticalLabel):
fg = '#fff'
bg = '#66c'
border = '#55B'
if self.orientation == 'vertical':
self.vStyle = """DockLabel {
background-color : %s;
@ -329,7 +319,7 @@ class DockLabel(VerticalLabel):
if self.dim != d:
self.dim = d
self.updateStyle()
def setOrientation(self, o):
VerticalLabel.setOrientation(self, o)
self.updateStyle()
@ -339,12 +329,12 @@ 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:
@ -353,7 +343,7 @@ class DockLabel(VerticalLabel):
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

@ -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={}):

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

@ -508,7 +508,7 @@ class Flowchart(Node):
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:
@ -763,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"""
@ -834,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:
@ -890,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'):
@ -938,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

@ -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):
@ -387,14 +389,15 @@ def glColor(*args, **kargs):
def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0):
def makeArrowPath(headLen=20, headWidth=None, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0):
"""
Construct a path outlining an arrow with the given dimensions.
The arrow points in the -x direction with tip positioned at 0,0.
If *tipAngle* is supplied (in degrees), it overrides *headWidth*.
If *headWidth* is supplied, it overrides *tipAngle* (in degrees).
If *tailLen* is None, no tail will be drawn.
"""
headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.)
if headWidth is None:
headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.)
path = QtGui.QPainterPath()
path.moveTo(0,0)
path.lineTo(headLen, -headWidth)
@ -424,6 +427,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 +445,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 +936,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 +1064,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:
@ -1074,7 +1102,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.
@ -1083,7 +1111,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:
@ -1105,11 +1140,11 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
if minVal != 0 or maxVal != scale:
if minVal == maxVal:
maxVal = np.nextafter(maxVal, 2*maxVal)
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
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)
@ -1152,7 +1187,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
@ -1223,30 +1263,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:
@ -1259,16 +1279,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):
"""
@ -2312,14 +2322,62 @@ def invertQTransform(tr):
raise Exception("Transform is not invertible.")
return inv[0]
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False, method='exact'):
"""Return an array of position values needed to make beeswarm or column scatter plots.
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
"""
Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots.
Used for examining the distribution of values in an array.
Given a list of x-values, construct a set of y-values such that an x,y scatter-plot
Given an array of x-values, construct an array of y-values such that an x,y scatter-plot
will not have overlapping points (it will look similar to a histogram).
"""
if method == 'exact':
return _pseudoScatterExact(data, spacing=spacing, shuffle=shuffle, bidir=bidir)
elif method == 'histogram':
return _pseudoScatterHistogram(data, spacing=spacing, shuffle=shuffle, bidir=bidir)
def _pseudoScatterHistogram(data, spacing=None, shuffle=True, bidir=False):
"""Works by binning points into a histogram and spreading them out to fill the bin.
Faster method, but can produce blocky results.
"""
inds = np.arange(len(data))
if shuffle:
np.random.shuffle(inds)
data = data[inds]
if spacing is None:
spacing = 2.*np.std(data)/len(data)**0.5
yvals = np.empty(len(data))
dmin = data.min()
dmax = data.max()
nbins = int((dmax-dmin) / spacing) + 1
bins = np.linspace(dmin, dmax, nbins)
dx = bins[1] - bins[0]
dbins = ((data - bins[0]) / dx).astype(int)
binCounts = {}
for i,j in enumerate(dbins):
c = binCounts.get(j, -1) + 1
binCounts[j] = c
yvals[i] = c
if bidir is True:
for i in range(nbins):
yvals[dbins==i] -= binCounts.get(i, 0) * 0.5
return yvals[np.argsort(inds)] ## un-shuffle values before returning
def _pseudoScatterExact(data, spacing=None, shuffle=True, bidir=False):
"""Works by stacking points up one at a time, searching for the lowest position available at each point.
This method produces nice, smooth results but can be prohibitively slow for large datasets.
"""
inds = np.arange(len(data))
if shuffle:
np.random.shuffle(inds)
@ -2469,6 +2527,3 @@ class SignalBlock(object):
def __exit__(self, *args):
if self.reconnect:
self.signal.connect(self.slot)

View File

@ -28,6 +28,7 @@ class ArrowItem(QtGui.QGraphicsPathItem):
'angle': -150, ## If the angle is 0, the arrow points left
'pos': (0,0),
'headLen': 20,
'headWidth': None,
'tipAngle': 25,
'baseAngle': 0,
'tailLen': None,
@ -52,10 +53,10 @@ class ArrowItem(QtGui.QGraphicsPathItem):
0; arrow pointing to the left.
headLen Length of the arrow head, from tip to base.
default=20
headWidth Width of the arrow head at its base.
headWidth Width of the arrow head at its base. If
headWidth is specified, it overrides tipAngle.
tipAngle Angle of the tip of the arrow in degrees. Smaller
values make a 'sharper' arrow. If tipAngle is
specified, ot overrides headWidth. default=25
values make a 'sharper' arrow. default=25
baseAngle Angle of the base of the arrow head. Default is
0, which means that the base of the arrow head
is perpendicular to the arrow tail.
@ -70,7 +71,7 @@ class ArrowItem(QtGui.QGraphicsPathItem):
"""
self.opts.update(opts)
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
opt = dict([(k,self.opts[k]) for k in ['headLen', 'headWidth', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
tr = QtGui.QTransform()
tr.rotate(self.opts['angle'])
self.path = tr.map(fn.makeArrowPath(**opt))

View File

@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from ..Qt import QtGui, QtCore
from ..python2_3 import asUnicode
import numpy as np
from ..Point import Point
from .. import debug as debug
import sys
import weakref
from .. import functions as fn
from .. import getConfigOption
@ -17,7 +19,7 @@ class AxisItem(GraphicsWidget):
If maxTickLength is negative, ticks point into the plot.
"""
def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
def __init__(self, orientation, pen=None, textPen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
"""
============== ===============================================================
**Arguments:**
@ -28,13 +30,14 @@ class AxisItem(GraphicsWidget):
to be linked to the visible range of a ViewBox.
showValues (bool) Whether to display values adjacent to ticks
pen (QPen) Pen used when drawing ticks.
textPen (QPen) Pen used when drawing tick labels.
text The text (excluding units) to display on the label for this
axis.
units The units for this axis. Units should generally be given
without any scaling prefix (eg, 'V' instead of 'mV'). The
scaling prefix will be automatically prepended based on the
range of data displayed.
**args All extra keyword arguments become CSS style options for
args All extra keyword arguments become CSS style options for
the <span> tag which will surround the axis label and units.
============== ===============================================================
"""
@ -80,7 +83,6 @@ class AxisItem(GraphicsWidget):
self.labelUnitPrefix = unitPrefix
self.labelStyle = args
self.logMode = False
self.tickFont = None
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
self._tickSpacing = None # used to override default tickSpacing method
@ -97,6 +99,11 @@ class AxisItem(GraphicsWidget):
else:
self.setPen(pen)
if textPen is None:
self.setTextPen()
else:
self.setTextPen(pen)
self._linkedView = None
if linkView is not None:
self.linkToView(linkView)
@ -198,7 +205,11 @@ class AxisItem(GraphicsWidget):
self.update()
def setTickFont(self, font):
self.tickFont = font
"""
(QFont or None) Determines the font used for tick values.
Use None for the default font.
"""
self.style['tickFont'] = font
self.picture = None
self.prepareGeometryChange()
## Need to re-allocate space depending on font size?
@ -249,7 +260,7 @@ class AxisItem(GraphicsWidget):
without any scaling prefix (eg, 'V' instead of 'mV'). The
scaling prefix will be automatically prepended based on the
range of data displayed.
**args All extra keyword arguments become CSS style options for
args All extra keyword arguments become CSS style options for
the <span> tag which will surround the axis label and units.
============== =============================================================
@ -405,6 +416,25 @@ class AxisItem(GraphicsWidget):
self.setLabel()
self.update()
def textPen(self):
if self._textPen is None:
return fn.mkPen(getConfigOption('foreground'))
return fn.mkPen(self._textPen)
def setTextPen(self, *args, **kwargs):
"""
Set the pen used for drawing text.
If no arguments are given, the default foreground color will be used.
"""
self.picture = None
if args or kwargs:
self._textPen = fn.mkPen(*args, **kwargs)
else:
self._textPen = fn.mkPen(getConfigOption('foreground'))
self.labelStyle['color'] = '#' + fn.colorStr(self._textPen.color())[:6]
self.setLabel()
self.update()
def setScale(self, scale=None):
"""
Set the value scaling for this axis.
@ -444,15 +474,19 @@ class AxisItem(GraphicsWidget):
def updateAutoSIPrefix(self):
if self.label.isVisible():
(scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale)))
if self.logMode:
_range = 10**np.array(self.range)
else:
_range = self.range
(scale, prefix) = fn.siScale(max(abs(_range[0]*self.scale), abs(_range[1]*self.scale)))
if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling.
scale = 1.0
prefix = ''
self.autoSIPrefixScale = scale
self.setLabel(unitPrefix=prefix)
else:
scale = 1.0
self.autoSIPrefixScale = 1.0
self.autoSIPrefixScale = scale
self.picture = None
self.update()
@ -477,20 +511,29 @@ class AxisItem(GraphicsWidget):
def linkToView(self, view):
"""Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
oldView = self.linkedView()
self.unlinkFromView()
self._linkedView = weakref.ref(view)
if self.orientation in ['right', 'left']:
view.sigYRangeChanged.connect(self.linkedViewChanged)
else:
view.sigXRangeChanged.connect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def unlinkFromView(self):
"""Unlink this axis from a ViewBox."""
oldView = self.linkedView()
self._linkedView = None
if self.orientation in ['right', 'left']:
if oldView is not None:
oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
view.sigYRangeChanged.connect(self.linkedViewChanged)
else:
if oldView is not None:
oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
view.sigXRangeChanged.connect(self.linkedViewChanged)
if oldView is not None:
oldView.sigResized.disconnect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def linkedViewChanged(self, view, newRange=None):
if self.orientation in ['right', 'left']:
@ -533,6 +576,8 @@ class AxisItem(GraphicsWidget):
try:
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
if self.style["tickFont"]:
painter.setFont(self.style["tickFont"])
specs = self.generateDrawSpecs(painter)
profiler('generate specs')
if specs is not None:
@ -771,7 +816,37 @@ class AxisItem(GraphicsWidget):
return strings
def logTickStrings(self, values, scale, spacing):
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
estrings = ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)]
if sys.version_info < (3, 0):
# python 2 does not support unicode strings like that
return estrings
else: # python 3+
convdict = {"0": "",
"1": "¹",
"2": "²",
"3": "³",
"4": "",
"5": "",
"6": "",
"7": "",
"8": "",
"9": "",
}
dstrings = []
for e in estrings:
if e.count("e"):
v, p = e.split("e")
sign = "" if p[0] == "-" else ""
pot = "".join([convdict[pp] for pp in p[1:].lstrip("0")])
if v == "1":
v = ""
else:
v = v + "·"
dstrings.append(v + "10" + sign + pot)
else:
dstrings.append(e)
return dstrings
def generateDrawSpecs(self, p):
"""
@ -780,8 +855,8 @@ class AxisItem(GraphicsWidget):
interpreted by drawPicture().
"""
profiler = debug.Profiler()
#bounds = self.boundingRect()
if self.style['tickFont'] is not None:
p.setFont(self.style['tickFont'])
bounds = self.mapRectFromParent(self.geometry())
linkedView = self.linkedView()
@ -892,7 +967,7 @@ class AxisItem(GraphicsWidget):
p2[axis] += tickLength*tickDir
tickPen = self.pen()
color = tickPen.color()
color.setAlpha(lineAlpha)
color.setAlpha(int(lineAlpha))
tickPen.setColor(color)
tickSpecs.append((tickPen, Point(p1), Point(p2)))
profiler('compute ticks')
@ -1044,13 +1119,13 @@ class AxisItem(GraphicsWidget):
p.drawLine(p1, p2)
profiler('draw ticks')
## Draw all text
if self.tickFont is not None:
p.setFont(self.tickFont)
p.setPen(self.pen())
# Draw all text
if self.style['tickFont'] is not None:
p.setFont(self.style['tickFont'])
p.setPen(self.textPen())
for rect, flags, text in textSpecs:
p.drawText(rect, flags, text)
#p.drawRect(rect)
p.drawText(rect, int(flags), text)
profiler('draw text')
def show(self):
@ -1068,23 +1143,26 @@ class AxisItem(GraphicsWidget):
self._updateHeight()
def wheelEvent(self, ev):
if self.linkedView() is None:
lv = self.linkedView()
if lv is None:
return
if self.orientation in ['left', 'right']:
self.linkedView().wheelEvent(ev, axis=1)
lv.wheelEvent(ev, axis=1)
else:
self.linkedView().wheelEvent(ev, axis=0)
lv.wheelEvent(ev, axis=0)
ev.accept()
def mouseDragEvent(self, event):
if self.linkedView() is None:
lv = self.linkedView()
if lv is None:
return
if self.orientation in ['left', 'right']:
return self.linkedView().mouseDragEvent(event, axis=1)
return lv.mouseDragEvent(event, axis=1)
else:
return self.linkedView().mouseDragEvent(event, axis=0)
return lv.mouseDragEvent(event, axis=0)
def mouseClickEvent(self, event):
if self.linkedView() is None:
lv = self.linkedView()
if lv is None:
return
return self.linkedView().mouseClickEvent(event)
return lv.mouseClickEvent(event)

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