Merge branch 'master' into develop
This commit is contained in:
commit
517adc87c0
49
.flake8
Normal file
49
.flake8
Normal 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
44
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
|
12
.mailmap
12
.mailmap
@ -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
11
.pre-commit-config.yaml
Normal 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
12
.readthedocs.yml
Normal 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
|
198
.travis.yml
198
.travis.yml
@ -1,198 +0,0 @@
|
||||
language: python
|
||||
sudo: false
|
||||
# Credit: Original .travis.yml lifted from VisPy
|
||||
|
||||
# Here we use anaconda for 2.6 and 3.3, since it provides the simplest
|
||||
# interface for running different versions of Python. We could also use
|
||||
# it for 2.7, but the Ubuntu system has installable 2.7 Qt4-GL, which
|
||||
# allows for more complete testing.
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
virtualenv:
|
||||
system_site_packages: true
|
||||
|
||||
|
||||
env:
|
||||
# Enable python 2 and python 3 builds
|
||||
# Note that the 2.6 build doesn't get flake8, and runs old versions of
|
||||
# Pyglet and GLFW to make sure we deal with those correctly
|
||||
#- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended
|
||||
- PYTHON=2.7 QT=pyqt4 TEST=extra
|
||||
- PYTHON=2.7 QT=pyside TEST=standard
|
||||
- PYTHON=3.5 QT=pyqt5 TEST=standard
|
||||
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
|
||||
#- PYTHON=3.2 QT=pyqt5 TEST=standard
|
||||
|
||||
|
||||
before_install:
|
||||
- if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi
|
||||
- chmod +x miniconda.sh
|
||||
- ./miniconda.sh -b -p /home/travis/mc
|
||||
- export PATH=/home/travis/mc/bin:$PATH
|
||||
|
||||
# not sure what is if block is for
|
||||
- if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then
|
||||
GIT_TARGET_EXTRA="+refs/heads/${TRAVIS_BRANCH}";
|
||||
GIT_SOURCE_EXTRA="+refs/pull/${TRAVIS_PULL_REQUEST}/merge";
|
||||
else
|
||||
GIT_TARGET_EXTRA="";
|
||||
GIT_SOURCE_EXTRA="";
|
||||
fi;
|
||||
|
||||
# to aid in debugging
|
||||
- echo ${TRAVIS_BRANCH}
|
||||
- echo ${TRAVIS_REPO_SLUG}
|
||||
- echo ${GIT_TARGET_EXTRA}
|
||||
- echo ${GIT_SOURCE_EXTRA}
|
||||
|
||||
install:
|
||||
- export GIT_FULL_HASH=`git rev-parse HEAD`
|
||||
- conda update conda --yes
|
||||
- conda create -n test_env python=${PYTHON} --yes
|
||||
- source activate test_env
|
||||
- conda install numpy scipy pyopengl pytest flake8 six coverage --yes
|
||||
- echo ${QT}
|
||||
- echo ${TEST}
|
||||
- echo ${PYTHON}
|
||||
|
||||
- if [ "${QT}" == "pyqt5" ]; then
|
||||
conda install pyqt --yes;
|
||||
fi;
|
||||
- if [ "${QT}" == "pyqt4" ]; then
|
||||
conda install pyqt=4 --yes;
|
||||
fi;
|
||||
- if [ "${QT}" == "pyside" ]; then
|
||||
conda install pyside --yes;
|
||||
fi;
|
||||
- pip install pytest-xdist # multi-thread py.test
|
||||
- pip install pytest-cov # add coverage stats
|
||||
|
||||
# required for example testing on python 2.6
|
||||
- if [ "${PYTHON}" == "2.6" ]; then
|
||||
pip install importlib;
|
||||
fi;
|
||||
|
||||
# Debugging helpers
|
||||
- uname -a
|
||||
- cat /etc/issue
|
||||
- if [ "${PYTHON}" == "2.7" ]; then
|
||||
python --version;
|
||||
else
|
||||
python3 --version;
|
||||
fi;
|
||||
|
||||
before_script:
|
||||
# We need to create a (fake) display on Travis, let's use a funny resolution
|
||||
- export DISPLAY=:99.0
|
||||
- "sh -e /etc/init.d/xvfb start"
|
||||
- /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render
|
||||
|
||||
# Make sure everyone uses the correct python (this is handled by conda)
|
||||
- which python
|
||||
- python --version
|
||||
- pwd
|
||||
- ls
|
||||
# Help color output from each test
|
||||
- RESET='\033[0m';
|
||||
RED='\033[00;31m';
|
||||
GREEN='\033[00;32m';
|
||||
YELLOW='\033[00;33m';
|
||||
BLUE='\033[00;34m';
|
||||
PURPLE='\033[00;35m';
|
||||
CYAN='\033[00;36m';
|
||||
WHITE='\033[00;37m';
|
||||
start_test() {
|
||||
echo -e "${BLUE}======== Starting $1 ========${RESET}";
|
||||
};
|
||||
check_output() {
|
||||
ret=$?;
|
||||
if [ $ret == 0 ]; then
|
||||
echo -e "${GREEN}>>>>>> $1 passed <<<<<<${RESET}";
|
||||
else
|
||||
echo -e "${RED}>>>>>> $1 FAILED <<<<<<${RESET}";
|
||||
fi;
|
||||
return $ret;
|
||||
};
|
||||
|
||||
- if [ "${TEST}" == "extra" ]; then
|
||||
start_test "repo size check";
|
||||
mkdir ~/repo-clone && cd ~/repo-clone &&
|
||||
git init && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git &&
|
||||
git fetch origin ${GIT_TARGET_EXTRA} &&
|
||||
git checkout -qf FETCH_HEAD &&
|
||||
git tag travis-merge-target &&
|
||||
git gc --aggressive &&
|
||||
TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` &&
|
||||
git pull origin ${GIT_SOURCE_EXTRA} &&
|
||||
git gc --aggressive &&
|
||||
MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` &&
|
||||
if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then
|
||||
SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`;
|
||||
else
|
||||
SIZE_DIFF=0;
|
||||
fi;
|
||||
fi;
|
||||
|
||||
script:
|
||||
|
||||
- source activate test_env
|
||||
|
||||
# Check system info
|
||||
- python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||
|
||||
# Run unit tests
|
||||
- start_test "unit tests";
|
||||
PYTHONPATH=. py.test --cov pyqtgraph -sv;
|
||||
check_output "unit tests";
|
||||
- echo "test script finished. Current directory:"
|
||||
- pwd
|
||||
|
||||
# check line endings
|
||||
- if [ "${TEST}" == "extra" ]; then
|
||||
start_test "line ending check";
|
||||
! find ./ -name "*.py" | xargs file | grep CRLF &&
|
||||
! find ./ -name "*.rst" | xargs file | grep CRLF;
|
||||
check_output "line ending check";
|
||||
fi;
|
||||
|
||||
# Check repo size does not expand too much
|
||||
- if [ "${TEST}" == "extra" ]; then
|
||||
start_test "repo size check";
|
||||
echo -e "Estimated content size difference = ${SIZE_DIFF} kB" &&
|
||||
test ${SIZE_DIFF} -lt 100;
|
||||
check_output "repo size check";
|
||||
fi;
|
||||
|
||||
# Check for style issues
|
||||
- if [ "${TEST}" == "extra" ]; then
|
||||
start_test "style check";
|
||||
cd ~/repo-clone &&
|
||||
git reset -q travis-merge-target &&
|
||||
python setup.py style &&
|
||||
check_output "style check";
|
||||
fi;
|
||||
|
||||
# Check install works
|
||||
- start_test "install test";
|
||||
python setup.py --quiet install;
|
||||
check_output "install test";
|
||||
|
||||
# Check double-install fails
|
||||
# Note the bash -c is because travis strips off the ! otherwise.
|
||||
- start_test "double install test";
|
||||
bash -c "! python setup.py --quiet install";
|
||||
check_output "double install test";
|
||||
|
||||
# Check we can import pg
|
||||
- start_test "import test";
|
||||
echo "import sys; print(sys.path)" | python &&
|
||||
cd /; echo "import pyqtgraph.examples" | python;
|
||||
check_output "import test";
|
||||
|
||||
after_success:
|
||||
- cd /home/travis/build/pyqtgraph/pyqtgraph
|
||||
- pip install codecov --upgrade # add coverage integration service
|
||||
- codecov
|
||||
- pip install coveralls --upgrade # add another coverage integration service
|
||||
- coveralls
|
159
CHANGELOG
159
CHANGELOG
@ -1,6 +1,9 @@
|
||||
pyqtgraph-0.11.0 (in development)
|
||||
pyqtgraph-0.11.0
|
||||
|
||||
NOTICE: This is the _last_ feature release to support Python 2 and Qt 4 (PyQt4 or pyside 1)
|
||||
|
||||
New Features:
|
||||
- #101: GridItem formatting options
|
||||
- #410: SpinBox custom formatting options
|
||||
- #415: ROI.getArrayRegion supports nearest-neighbor interpolation (especially handy for label images)
|
||||
- #428: DataTreeWidget:
|
||||
@ -56,10 +59,24 @@ pyqtgraph-0.11.0 (in development)
|
||||
- #683: Allow data filter entries to be updated after they are created
|
||||
- #685: Add option to set enum default values in DataFilterWidget
|
||||
- #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers)
|
||||
- #813,814,817: Performance improvements
|
||||
- #837: Added options for field variables in ColorMapWidget
|
||||
- #840, 932: Improve clipping behavior
|
||||
- #841: Set color of tick-labels separately
|
||||
- #922: Curve fill for fill-patches
|
||||
- #996: Allow the update of LegendItem
|
||||
- #1023: Add bookkeeping exporter parameters
|
||||
- #1072: HDF5Exporter handling of ragged curves with tests
|
||||
- #1124: Syntax highlighting for examples.
|
||||
- #1154: Date axis item
|
||||
- #393: NEW show/hide gradient ticks NEW link gradientEditor to others
|
||||
- #1211: Add support for running pyside2-uic binary to dynamically compile ui files
|
||||
|
||||
API / behavior changes:
|
||||
- Deprecated graphicsWindow classes; these have been unnecessary for many years because
|
||||
widgets can be placed into a new window just by calling show().
|
||||
- #158: Make DockArea compatible with Qt Designer
|
||||
- #406: Applying alpha mask on numpy.nan data values
|
||||
- #566: ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system.
|
||||
The result is visually the same, but children of ArrowItem are no longer rotated
|
||||
(this allows screen-aligned text to be attached more easily).
|
||||
@ -76,8 +93,50 @@ pyqtgraph-0.11.0 (in development)
|
||||
- #589: Remove SpiralROI (this was unintentionally added in the first case)
|
||||
- #593: Override qAbort on slot exceptions for PyQt>=5.5
|
||||
- #657: When a floating Dock window is closed, the dock is now returned home
|
||||
- #771: Suppress RuntimeWarning for arrays containing zeros in logscale
|
||||
- #942: If the visible GraphicsView is garbage collected, a warning is issued.
|
||||
- #958: Nicer Legend
|
||||
- #963: Last image in image-stack can now be selected with the z-slider
|
||||
- #992: Added a setter for GlGridItem.color.
|
||||
- #999: Make outline around fillLevel optional.
|
||||
- #1014: Enable various arguments as color in colormap.
|
||||
- #1044: Raise AttributeError in __getattr__ in graphicsWindows (deprecated)
|
||||
- #1055: Remove global for CONFIG_OPTIONS in setConfigOption
|
||||
- #1066: Add RemoteGraphicsView to __init__.py
|
||||
- #1069: Allow actions to display title instead of name
|
||||
- #1074: Validate min/max text inputs in ViewBoxMenu
|
||||
- #1076: Reset currentRow and currentCol on GraphicsLayout.clear()
|
||||
- #1079: Improve performance of updateData PlotCurveItem
|
||||
- #1082: Allow MetaArray.__array__ to accept an optional dtype arg
|
||||
- #841: set color of tick-labels separately
|
||||
- #1111: Add name label to GradientEditorItem
|
||||
- #1145: Pass showAxRect keyword arguments to setRange
|
||||
- #1184: improve SymbolAtlas.getSymbolCoords performance
|
||||
- #1198: improve SymbolAtlas.getSymbolCoords and ScatterPlotItem.plot performance
|
||||
- #1197: Disable remove ROI menu action in handle context menu
|
||||
- #1188: Added support for plot curve to handle both fill and connect args
|
||||
- #801: Remove use of GraphicsScene._addressCache in translateGraphicsItem
|
||||
- Deprecates registerObject meethod of GraphicsScene
|
||||
- Deprecates regstar argument to GraphicsScene.__init__
|
||||
- #1166: pg.mkQApp: Pass non-empty string array to QApplication() as default
|
||||
- #1199: Pass non-empty sys.argv to QApplication
|
||||
- #1090: dump ExportDialog.exporterParameters
|
||||
- #1173: GraphicsLayout: Always call layout.activate() after adding items
|
||||
- #1097: pretty-print log-scale axes labels
|
||||
- #755: Check lastDownsample in viewTransformChanged
|
||||
- #1216: Add cache for mapRectFromView
|
||||
- #444: Fix duplicate menus in GradientEditorItem
|
||||
- #151: Optionally provide custom PlotItem to PlotWidget
|
||||
- #1093: Fix aspectRatio and zoom range issues when zooming
|
||||
- #390: moved some functionality from method 'export' to new method
|
||||
- #468: Patch/window handling
|
||||
- #392: new method 'getAxpectRatio' with code taken from 'setAspectLocked'
|
||||
- #1206: Added context menu option to parametertree
|
||||
- #1228: Minor improvements to LegendItem
|
||||
|
||||
Bugfixes:
|
||||
- #88: Fixed image scatterplot export
|
||||
- #356: Fix some NumPy warnings
|
||||
- #408: Fix `cleanup` when the running qt application is not a QApplication
|
||||
- #410: SpinBox fixes
|
||||
- fixed bug with exponents disappearing after edit
|
||||
@ -93,7 +152,7 @@ pyqtgraph-0.11.0 (in development)
|
||||
- fixed spinbox height too small for font size
|
||||
- ROI subclass getArrayRegion methods are a bit more consistent (still need work)
|
||||
- #424: Fix crash when running pyqtgraph with python -OO
|
||||
- #429: fix fft premature slicing away of 0 freq bin
|
||||
- #429: Fix fft premature slicing away of 0 freq bin
|
||||
- #458: Fixed image export problems with new numpy API
|
||||
- #478: Fixed PySide image memory leak
|
||||
- #475: Fixed unicode error when exporting to SVG with non-ascii symbols
|
||||
@ -126,17 +185,18 @@ pyqtgraph-0.11.0 (in development)
|
||||
- #592,595: Fix InvisibleRootItem issues introduced in #518
|
||||
- #596: Fix polyline click causing lines to bedrawn to the wrong node
|
||||
- #598: Better ParameterTree support for dark themes
|
||||
- #599: Prevent invalid list access in GraphicsScene
|
||||
- #623: Fix PyQt5 / ScatterPlot issue with custom symbols
|
||||
- #626: Fix OpenGL texture state leaking to wrong items
|
||||
- #627: Fix ConsoleWidget stack handling on python 3.5
|
||||
- #633: Fix OpenGL cylinder geometry
|
||||
- #637: Fix TypeError in isosurface
|
||||
- #641,642: Fix SVG export on Qt5 / high-DPI displays
|
||||
- #645: scatterplotwidget behaves nicely when data contains infs
|
||||
- #645: ScatterPlotWidget behaves nicely when data contains infs
|
||||
- #653: ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581
|
||||
- #648: fix color ignored in GLGridItem
|
||||
- #671: fixed SVG export failing if the first value of a plot is nan
|
||||
- #674: fixed parallelizer leaking file handles
|
||||
- #671: Fixed SVG export failing if the first value of a plot is nan
|
||||
- #674: Fixed parallelizer leaking file handles
|
||||
- #675: Gracefully handle case where image data has size==0
|
||||
- #679: Fix overflow in Point.length()
|
||||
- #682: Fix: mkQApp returned None if a QApplication was already created elsewhere
|
||||
@ -153,14 +213,103 @@ pyqtgraph-0.11.0 (in development)
|
||||
it was causing auto range to be disabled.
|
||||
- #723: Fix axis ticks when using self.scale
|
||||
- #739: Fix handling of 2-axis mouse wheel events
|
||||
- #742: Fix Metaarray in python 3
|
||||
- #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX.
|
||||
- #763: Fix OverflowError when using Auto Downsampling.
|
||||
- #767: Fix Image display for images with the same value everywhere.
|
||||
- #770: Fix GLVieWidget.setCameraPosition ignoring first parameter.
|
||||
- #782: Fix missing FileForwarder thread termination.
|
||||
- #787: Fix encoding errors in checkOpenGLVersion.
|
||||
- #793: Fix wrong default scaling in makeARGB
|
||||
- #815: Fixed mirroring of x-axis with "invert Axis" submenu.
|
||||
- #824: Fix several issues related with mouse movement and GraphicsView.
|
||||
- #832: Fix Permission error in tests due to unclosed filehandle.
|
||||
- #836: Fix tickSpacing bug that lead to axis not being drawn.
|
||||
- #861: Fix crash of PlotWidget if empty ErrorBarItem is added.
|
||||
- #868: Fix segfault on repeated closing of matplotlib exporter.
|
||||
- #875,876,887,934,947,980: Fix deprecation warnings.
|
||||
- #886: Fix flowchart saving on python3.
|
||||
- #888: Fix TreeWidget.topLevelItems in python3.
|
||||
- #924: Fix QWheelEvent in RemoteGraphicsView with pyqt5.
|
||||
- #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter.
|
||||
- #949: Fix multiline parameters (such as arrays) reading from config files.
|
||||
- #951: Fix event firing from scale handler.
|
||||
- #952: Fix RotateFree handle dragging
|
||||
- #953: Fix HistogramLUTWidget with background parameter
|
||||
- #968: Fix Si units in AxisItem leading to an incorrect unit.
|
||||
- #970: Always update transform when setting angle of a TextItem
|
||||
- #971: Fix a segfault stemming from incorrect signal disconnection.
|
||||
- #972: Correctly include SI units for log AxisItems
|
||||
- #974: Fix recursion error when instancing CtrlNode.
|
||||
- #987: Fix visibility reset when PlotItems are removed.
|
||||
- #998: Fix QtProcess proxy being unable to handle numpy arrays with dtype uint8.
|
||||
- #1010: Fix matplotlib/CSV export.
|
||||
- #1012: Fix circular texture centering
|
||||
- #1015: Iterators are now converted to NumPy arrays.
|
||||
- #1016: Fix synchronisation of multiple ImageViews with time axis.
|
||||
- #1017: Fix duplicate paint calls emitted by Items on ViewBox.
|
||||
- #1019: Fix disappearing GLGridItems when PlotItems are removed and readded.
|
||||
- #1024: Prevent element-wise string comparison
|
||||
- #1031: Reset ParentItem to None on removing from PlotItem/ViewBox
|
||||
- #1044: Fix PlotCurveItem.paintGL
|
||||
- #1048: Fix bounding box for InfiniteLine
|
||||
- #1062: Fix flowchart context menu redundant menu
|
||||
- #1062: Fix a typo
|
||||
- #1073: Fix Python3 compatibility
|
||||
- #1083: Fix SVG export of scatter plots
|
||||
- #1085: Fix ofset when drawing symbol
|
||||
- #1101: Fix small oversight in LegendItem
|
||||
- #1113: Correctly call hasFaceIndexedData function
|
||||
- #1139: Bug fix in LegendItem for `setPen`, `setBrush` etc (Call update instead of paint)
|
||||
- #1110: fix for makeARGB error after #955
|
||||
- #1063: Fix: AttributeError in ViewBox.setEnableMenu
|
||||
- #1151: ImageExporter py2-pyside fix with test
|
||||
- #1133: compatibility-fix for py2/pyside
|
||||
- #1152: Nanmask fix in makeARGB
|
||||
- #1159: Fix: Update axes after data is set
|
||||
- #1156: SVGExporter: Correct image pixelation
|
||||
- #1169: Replace default list arg with None
|
||||
- #770: Do not ignore pos argument of setCameraPosition
|
||||
- #1180: Fix: AxisItem tickFont is defined in two places while only one is used
|
||||
- #1168: GroupParameterItem: Did not pass changed options to ParameterItem
|
||||
- #1174: Fixed a possible race condition with linked views
|
||||
- #809: Fix selection of FlowchartWidget input/output nodes
|
||||
- #1071: Fix py3 execution in flowchart
|
||||
- #1212: Fix PixelVectors cache
|
||||
- #1161: Correctly import numpy where needed
|
||||
- #1218: Fix ParameterTree.clear()
|
||||
- #1175: Fix: Parameter tree ignores user-set 'expanded' state
|
||||
- #1219: Encode csv export header as unicode
|
||||
- #507: Fix Dock close event QLabel still running with no parent
|
||||
- #1222: py3 fix for ScatterPlotWidget.setSelectedFields
|
||||
- #1203: Image axis order bugfix
|
||||
- #1225: ParameterTree: Fix custom context menu
|
||||
|
||||
|
||||
Maintenance:
|
||||
- Lots of new unit tests
|
||||
- Lots of code cleanup
|
||||
- A lot of work on CI pipelines, test coverage and test passing (see e.g. #903,911)
|
||||
- #546: Add check for EINTR during example testing to avoid sporadic test failures on travis
|
||||
- #624: TravisCI no longer running python 2.6 tests
|
||||
- #695: "dev0" added to version string
|
||||
- #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI
|
||||
- #991: Use Azure Pipelines to do style checks, Add .pre-commit-config.yaml
|
||||
- #1042: Close windows at the end of test functions
|
||||
- #1046: Establish minimum numpy version, remove legacy workarounds
|
||||
- #1067: Make scipy dependency optional
|
||||
- #1114: doc: Fix small mistake in introduction
|
||||
- #1131: Update CI/tox and Enable More Tests
|
||||
- #1142: Miscellaneous doc fixups
|
||||
- #1179: DateAxisItem: AxisItem unlinking tests and doc fixed
|
||||
- #1201: Get readthedocs working
|
||||
- #1214: Pin PyVirtualDisplay Version
|
||||
- #1215: Skip test when on qt 5.9
|
||||
- #1221: Identify pyqt5 5.15 ci issue
|
||||
- #1223: Remove workaround for memory leak in QImage
|
||||
- #1217: Get docs version and copyright year dynamically
|
||||
- #1229: Wrap text in tables in docs
|
||||
- #1231: Update readme for 0.11 release
|
||||
|
||||
|
||||
pyqtgraph-0.10.0
|
||||
|
72
CONTRIBUTING.md
Normal file
72
CONTRIBUTING.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Contributing to PyQtGraph
|
||||
|
||||
Contributions to pyqtgraph are welcome!
|
||||
|
||||
Please use the following guidelines when preparing changes:
|
||||
|
||||
## Submitting Code Changes
|
||||
|
||||
* The preferred method for submitting changes is by github pull request against the "develop" branch.
|
||||
* Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected.
|
||||
* For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort.
|
||||
* The following deprecations are being considered by the maintainers
|
||||
* `pyqtgraph.opengl` may be deprecated and replaced with `VisPy` functionality
|
||||
* After v0.11, pyqtgraph will adopt [NEP-29](https://numpy.org/neps/nep-0029-deprecation_policy.html) which will effectively mean that python2 support will be deprecated
|
||||
* Qt4 will be deprecated shortly, as well as Qt5<5.9 (and potentially <5.12)
|
||||
|
||||
## Documentation
|
||||
|
||||
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code.
|
||||
* Documentation is generated with sphinx; please check that docstring changes compile correctly
|
||||
|
||||
## Style guidelines
|
||||
|
||||
### Rules
|
||||
|
||||
* PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable.
|
||||
* Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8.
|
||||
* Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt
|
||||
* Exception 2: Function docstrings use ReStructuredText tables for describing arguments:
|
||||
|
||||
```text
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
argName1 (type) Description of argument
|
||||
argName2 (type) Description of argument. Longer descriptions must
|
||||
be wrapped within the column guidelines defined by the
|
||||
"====" header and footer.
|
||||
============== ========================================================
|
||||
```
|
||||
|
||||
QObject subclasses that implement new signals should also describe
|
||||
these in a similar table.
|
||||
|
||||
### Pre-Commit
|
||||
|
||||
PyQtGraph developers are highly encouraged to (but not required) to use [`pre-commit`](https://pre-commit.com/). `pre-commit` does a number of checks when attempting to commit the code to ensure it conforms to various standards, such as `flake8`, utf-8 encoding pragma, line-ending fixers, and so on. If any of the checks fail, the commit will be rejected, and you will have the opportunity to make the necessary fixes before adding and committing a file again. This ensures that every commit made conforms to (most) of the styling standards that the library enforces; and you will most likely pass the code style checks by the CI.
|
||||
|
||||
To make use of `pre-commit`, have it available in your `$PATH` and run `pre-commit install` from the root directory of PyQtGraph.
|
||||
|
||||
## Testing Setting up a test environment
|
||||
|
||||
### Dependencies
|
||||
|
||||
* tox
|
||||
* tox-conda
|
||||
* pytest
|
||||
* pytest-cov
|
||||
* pytest-xdist
|
||||
* Optional: pytest-xvfb
|
||||
|
||||
If you have `pytest<5` (used in python2), you may also want to install `pytest-faulthandler==1.6` plugin to output extra debugging information in case of test failures. This isn't necessary with `pytest>=5`
|
||||
|
||||
### Tox
|
||||
|
||||
As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin.
|
||||
|
||||
* Tests for a module should ideally cover all code in that module, i.e., statement coverage should be at 100%.
|
||||
* To measure the test coverage, un `pytest --cov -n 4` to run the test suite with coverage on 4 cores.
|
||||
|
||||
### Continous Integration
|
||||
|
||||
For our Continuous Integration, we utilize Azure Pipelines. Tested configurations are visible on [README](README.md). More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1)
|
@ -1,58 +0,0 @@
|
||||
Contributions to pyqtgraph are welcome!
|
||||
|
||||
Please use the following guidelines when preparing changes:
|
||||
|
||||
* The preferred method for submitting changes is by github pull request
|
||||
against the "develop" branch.
|
||||
|
||||
* Pull requests should include only a focused and related set of changes.
|
||||
Mixed features and unrelated changes may be rejected.
|
||||
|
||||
* For major changes, it is recommended to discuss your plans on the mailing
|
||||
list or in a github issue before putting in too much effort.
|
||||
|
||||
* Along these lines, please note that pyqtgraph.opengl will be deprecated
|
||||
soon and replaced with VisPy.
|
||||
|
||||
* Writing proper documentation and unit tests is highly encouraged. PyQtGraph
|
||||
uses nose / py.test style testing, so tests should usually be included in a
|
||||
tests/ directory adjacent to the relevant code.
|
||||
|
||||
* Documentation is generated with sphinx; please check that docstring changes
|
||||
compile correctly.
|
||||
|
||||
* Style guidelines:
|
||||
|
||||
* PyQtGraph prefers PEP8 for most style issues, but this is not enforced
|
||||
rigorously as long as the code is clean and readable.
|
||||
|
||||
* Use `python setup.py style` to see whether your code follows
|
||||
the mandatory style guidelines checked by flake8.
|
||||
|
||||
* Exception 1: All variable names should use camelCase rather than
|
||||
underscore_separation. This is done for consistency with Qt
|
||||
|
||||
* Exception 2: Function docstrings use ReStructuredText tables for
|
||||
describing arguments:
|
||||
|
||||
```
|
||||
============== ========================================================
|
||||
**Arguments:**
|
||||
argName1 (type) Description of argument
|
||||
argName2 (type) Description of argument. Longer descriptions must
|
||||
be wrapped within the column guidelines defined by the
|
||||
"====" header and footer.
|
||||
============== ========================================================
|
||||
```
|
||||
|
||||
QObject subclasses that implement new signals should also describe
|
||||
these in a similar table.
|
||||
|
||||
* Setting up a test environment.
|
||||
|
||||
Tests for a module should ideally cover all code in that module,
|
||||
i.e., statement coverage should be at 100%.
|
||||
|
||||
To measure the test coverage, install py.test, pytest-cov and pytest-xdist.
|
||||
Then run 'py.test --cov -n 4' to run the test suite with coverage on 4 cores.
|
||||
|
58
README.md
58
README.md
@ -1,12 +1,13 @@
|
||||
[![Build Status](https://travis-ci.org/pyqtgraph/pyqtgraph.svg?branch=develop)](https://travis-ci.org/pyqtgraph/pyqtgraph)
|
||||
[![codecov.io](http://codecov.io/github/pyqtgraph/pyqtgraph/coverage.svg?branch=develop)](http://codecov.io/github/pyqtgraph/pyqtgraph?branch=develop)
|
||||
|
||||
[![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop)
|
||||
[![Documentation Status](https://readthedocs.org/projects/pyqtgraph/badge/?version=latest)](https://pyqtgraph.readthedocs.io/en/latest/?badge=latest)
|
||||
|
||||
PyQtGraph
|
||||
=========
|
||||
|
||||
A pure-Python graphics library for PyQt/PySide
|
||||
A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2
|
||||
|
||||
Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill
|
||||
Copyright 2020 Luke Campagnola, University of North Carolina at Chapel Hill
|
||||
|
||||
<http://www.pyqtgraph.org>
|
||||
|
||||
@ -15,38 +16,57 @@ Despite being written entirely in python, the library is fast due to its
|
||||
heavy leverage of numpy for number crunching, Qt's GraphicsView framework for
|
||||
2D display, and OpenGL for 3D display.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* PyQt 4.7+, PySide, PyQt5, or PySide2
|
||||
* python 2.7, or 3.x
|
||||
* NumPy
|
||||
* For 3D graphics: pyopengl and qt-opengl
|
||||
* Known to run on Windows, Linux, and Mac.
|
||||
* Python 2.7, or 3.x
|
||||
* Required
|
||||
* PyQt 4.8+, PySide, PyQt5, or PySide2
|
||||
* `numpy`
|
||||
* Optional
|
||||
* `scipy` for image processing
|
||||
* `pyopengl` for 3D graphics
|
||||
* `hdf5` for large hdf5 binary format support
|
||||
|
||||
Qt Bindings Test Matrix
|
||||
-----------------------
|
||||
|
||||
The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 18.04, Windows Server 2019, and macOS 10.15 base images.
|
||||
|
||||
| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 |
|
||||
| :------------- | :----------------: | :----------------: | :----------------: | :----------------: |
|
||||
| PyQt-4 | :white_check_mark: | :x: | :x: | :x: |
|
||||
| PySide1 | :white_check_mark: | :x: | :x: | :x: |
|
||||
| PyQt5-5.9 | :x: | :white_check_mark: | :x: | :x: |
|
||||
| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: |
|
||||
| PyQt5-Latest | :x: | :x: | :x: | :white_check_mark: |
|
||||
| PySide2-Latest | :x: | :x: | :x: | :white_check_mark: |
|
||||
|
||||
* pyqtgraph has had some incompatibilities with PySide2 versions 5.6-5.11, and we recommend you avoid those versions if possible
|
||||
* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work reliably
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
|
||||
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
|
||||
* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues)
|
||||
* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph)
|
||||
|
||||
Installation Methods
|
||||
--------------------
|
||||
|
||||
* From pypi:
|
||||
- Last released version: `pip install pyqtgraph`
|
||||
- Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph`
|
||||
* From PyPI:
|
||||
* Last released version: `pip install pyqtgraph`
|
||||
* Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@master`
|
||||
* From conda
|
||||
* Last released version: `conda install -c conda-forge pyqtgraph`
|
||||
* To install system-wide from source distribution: `python setup.py install`
|
||||
* Many linux package repositories have release versions.
|
||||
* To use with a specific project, simply copy the pyqtgraph subdirectory
|
||||
anywhere that is importable from your project.
|
||||
* For installation packages, see the website (pyqtgraph.org)
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu.
|
||||
|
||||
The official documentation lives at http://pyqtgraph.org/documentation
|
||||
The official documentation lives at https://pyqtgraph.readthedocs.io
|
||||
|
||||
The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` to launch the examples application.
|
||||
|
99
azure-pipelines.yml
Normal file
99
azure-pipelines.yml
Normal file
@ -0,0 +1,99 @@
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- '*' # Build for all branches if they have a azure-pipelines.yml file.
|
||||
tags:
|
||||
include:
|
||||
- 'v*' # Ensure that we are building for tags starting with 'v' (Official Versions)
|
||||
|
||||
# Build only for PRs for master branch
|
||||
pr:
|
||||
autoCancel: true
|
||||
branches:
|
||||
include:
|
||||
- master
|
||||
- develop
|
||||
|
||||
variables:
|
||||
OFFICIAL_REPO: 'pyqtgraph/pyqtgraph'
|
||||
DEFAULT_MERGE_BRANCH: 'develop'
|
||||
disable.coverage.autogenerate: 'true'
|
||||
|
||||
stages:
|
||||
- stage: "pre_test"
|
||||
jobs:
|
||||
- job: check_diff_size
|
||||
pool:
|
||||
vmImage: 'Ubuntu 18.04'
|
||||
steps:
|
||||
- bash: |
|
||||
git config --global advice.detachedHead false
|
||||
mkdir ~/repo-clone && cd ~/repo-clone
|
||||
git init
|
||||
|
||||
git remote add -t $(Build.SourceBranchName) origin $(Build.Repository.Uri)
|
||||
git remote add -t ${DEFAULT_MERGE_BRANCH} upstream https://github.com/${OFFICIAL_REPO}.git
|
||||
|
||||
git fetch origin $(Build.SourceBranchName)
|
||||
git fetch upstream ${DEFAULT_MERGE_BRANCH}
|
||||
|
||||
git checkout $(Build.SourceBranchName)
|
||||
MERGE_SIZE=`du -s . | sed -e "s/\t.*//"`
|
||||
echo -e "Merge Size ${MERGE_SIZE}"
|
||||
|
||||
git checkout ${DEFAULT_MERGE_BRANCH}
|
||||
TARGET_SIZE=`du -s . | sed -e "s/\t.*//"`
|
||||
echo -e "Target Size ${TARGET_SIZE}"
|
||||
|
||||
if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then
|
||||
SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`;
|
||||
else
|
||||
SIZE_DIFF=0;
|
||||
fi;
|
||||
echo -e "Estimated content size difference = ${SIZE_DIFF} kB" &&
|
||||
test ${SIZE_DIFF} -lt 100;
|
||||
displayName: 'Diff Size Check'
|
||||
continueOnError: true
|
||||
|
||||
- job: "style_check"
|
||||
pool:
|
||||
vmImage: "Ubuntu 18.04"
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: 3.7
|
||||
- bash: |
|
||||
pip install flake8
|
||||
python setup.py style
|
||||
displayName: 'flake8 check'
|
||||
continueOnError: true
|
||||
|
||||
- job: "build_wheel"
|
||||
pool:
|
||||
vmImage: 'Ubuntu 18.04'
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: 3.8
|
||||
- script: |
|
||||
python -m pip install setuptools wheel
|
||||
python setup.py bdist_wheel --universal
|
||||
displayName: "Build Python Wheel"
|
||||
continueOnError: false
|
||||
- publish: dist
|
||||
artifact: wheel
|
||||
|
||||
- stage: "test"
|
||||
jobs:
|
||||
- template: azure-test-template.yml
|
||||
parameters:
|
||||
name: linux
|
||||
vmImage: 'Ubuntu 18.04'
|
||||
- template: azure-test-template.yml
|
||||
parameters:
|
||||
name: windows
|
||||
vmImage: 'windows-2019'
|
||||
- template: azure-test-template.yml
|
||||
parameters:
|
||||
name: macOS
|
||||
vmImage: 'macOS-10.15'
|
208
azure-test-template.yml
Normal file
208
azure-test-template.yml
Normal file
@ -0,0 +1,208 @@
|
||||
# Azure Pipelines CI job template for PyDM Tests
|
||||
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/anaconda?view=azure-devops
|
||||
parameters:
|
||||
name: ''
|
||||
vmImage: ''
|
||||
|
||||
jobs:
|
||||
- job: ${{ parameters.name }}
|
||||
pool:
|
||||
vmImage: ${{ parameters.vmImage }}
|
||||
strategy:
|
||||
matrix:
|
||||
Python27-PyQt4-4.8:
|
||||
python.version: '2.7'
|
||||
qt.bindings: "pyqt=4"
|
||||
install.method: "conda"
|
||||
Python27-PySide-4.8:
|
||||
python.version: '2.7'
|
||||
qt.bindings: "pyside"
|
||||
install.method: "conda"
|
||||
Python36-PyQt5-5.9:
|
||||
python.version: "3.6"
|
||||
qt.bindings: "pyqt"
|
||||
install.method: "conda"
|
||||
Python37-PySide2-5.13:
|
||||
python.version: "3.7"
|
||||
qt.bindings: "pyside2"
|
||||
install.method: "conda"
|
||||
Python38-PyQt5-Latest:
|
||||
python.version: '3.8'
|
||||
qt.bindings: "PyQt5"
|
||||
install.method: "pip"
|
||||
Python38-PySide2-Latest:
|
||||
python.version: '3.8'
|
||||
qt.bindings: "PySide2"
|
||||
install.method: "pip"
|
||||
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
source: 'current'
|
||||
artifact: wheel
|
||||
path: 'dist'
|
||||
|
||||
- task: ScreenResolutionUtility@1
|
||||
inputs:
|
||||
displaySettings: 'specific'
|
||||
width: '1920'
|
||||
height: '1080'
|
||||
condition: eq(variables['agent.os'], 'Windows_NT' )
|
||||
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: $(python.version)
|
||||
condition: eq(variables['install.method'], 'pip')
|
||||
|
||||
- script: |
|
||||
curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe
|
||||
7z x mesa3d-19.1.0-release-msvc.exe
|
||||
cd x64
|
||||
xcopy opengl32.dll C:\windows\system32\mesadrv.dll*
|
||||
xcopy opengl32.dll C:\windows\syswow64\mesadrv.dll*
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f
|
||||
REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f
|
||||
displayName: "Install Windows-Mesa OpenGL DLL"
|
||||
condition: eq(variables['agent.os'], 'Windows_NT')
|
||||
|
||||
- bash: |
|
||||
if [ $(agent.os) == 'Linux' ]
|
||||
then
|
||||
echo "##vso[task.prependpath]$CONDA/bin"
|
||||
elif [ $(agent.os) == 'Darwin' ]
|
||||
then
|
||||
sudo chown -R $USER $CONDA
|
||||
echo "##vso[task.prependpath]$CONDA/bin"
|
||||
elif [ $(agent.os) == 'Windows_NT' ]
|
||||
then
|
||||
echo "##vso[task.prependpath]$CONDA/Scripts"
|
||||
else
|
||||
echo 'Just what OS are you using?'
|
||||
fi
|
||||
displayName: 'Add Conda To $PATH'
|
||||
condition: eq(variables['install.method'], 'conda' )
|
||||
continueOnError: false
|
||||
|
||||
- bash: |
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
conda update --all --yes --quiet
|
||||
conda create --name test-environment-$(python.version) python=$(python.version) --yes --quiet
|
||||
source activate test-environment-$(python.version)
|
||||
conda config --env --set always_yes true
|
||||
if [ $(python.version) == '2.7' ]
|
||||
then
|
||||
conda config --set restore_free_channel true
|
||||
fi
|
||||
if [ $(qt.bindings) == "pyside2" ] || ([ $(qt.bindings) == 'pyside' ] && [ $(agent.os) == 'Darwin' ])
|
||||
then
|
||||
conda config --prepend channels conda-forge
|
||||
fi
|
||||
conda info
|
||||
conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet
|
||||
else
|
||||
pip install $(qt.bindings) numpy scipy pyopengl h5py six
|
||||
fi
|
||||
pip install pytest pytest-cov coverage pytest-xdist
|
||||
if [ $(python.version) == "2.7" ]
|
||||
then
|
||||
pip install pytest-faulthandler==1.6.0
|
||||
export PYTEST_ADDOPTS="--faulthandler-timeout=15"
|
||||
else
|
||||
pip install pytest pytest-cov coverage
|
||||
fi
|
||||
displayName: "Install Dependencies"
|
||||
|
||||
- bash: |
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
python -m pip install --no-index --find-links=dist pyqtgraph
|
||||
displayName: 'Install Wheel'
|
||||
|
||||
- bash: |
|
||||
sudo apt-get install -y libxkbcommon-x11-dev
|
||||
# workaround for QTBUG-84489
|
||||
sudo apt-get install -y libxcb-xfixes0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
if [ $(python.version) == "2.7" ]
|
||||
then
|
||||
pip install PyVirtualDisplay==0.2.5 pytest-xvfb==1.2.0
|
||||
else
|
||||
pip install pytest-xvfb
|
||||
fi
|
||||
displayName: "Virtual Display Setup"
|
||||
condition: eq(variables['agent.os'], 'Linux' )
|
||||
|
||||
- bash: |
|
||||
export QT_DEBUG_PLUGINS=1
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
echo python location: `which python`
|
||||
echo python version: `python --version`
|
||||
echo pytest location: `which pytest`
|
||||
echo installed packages
|
||||
pip list
|
||||
echo pyqtgraph system info
|
||||
python -c "import pyqtgraph as pg; pg.systemInfo()"
|
||||
echo display information
|
||||
if [ $(agent.os) == 'Linux' ]
|
||||
then
|
||||
export DISPLAY=:99.0
|
||||
Xvfb :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset &
|
||||
sleep 3
|
||||
fi
|
||||
python -m pyqtgraph.util.get_resolution
|
||||
echo openGL information
|
||||
python -c "from pyqtgraph.opengl.glInfo import GLTest"
|
||||
displayName: 'Debug Info'
|
||||
continueOnError: false
|
||||
|
||||
- bash: |
|
||||
if [ $(install.method) == "conda" ]
|
||||
then
|
||||
source activate test-environment-$(python.version)
|
||||
fi
|
||||
mkdir -p "$SCREENSHOT_DIR"
|
||||
# echo "If Screenshots are generated, they may be downloaded from:"
|
||||
# echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0"
|
||||
pytest . -v \
|
||||
-n 1 \
|
||||
--junitxml=junit/test-results.xml \
|
||||
--cov pyqtgraph --cov-report=xml --cov-report=html
|
||||
displayName: 'Unit tests'
|
||||
env:
|
||||
AZURE: 1
|
||||
SCREENSHOT_DIR: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Screenshots'
|
||||
condition: failed()
|
||||
inputs:
|
||||
pathtoPublish: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
artifactName: Screenshots
|
||||
|
||||
- task: PublishTestResults@2
|
||||
condition: succeededOrFailed()
|
||||
inputs:
|
||||
testResultsFiles: '**/test-*.xml'
|
||||
testRunTitle: 'Test Results for $(agent.os) - $(python.version) - $(qt.bindings) - $(install.method)'
|
||||
publishRunAttachments: true
|
||||
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: Cobertura
|
||||
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
|
||||
reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
|
5
doc/requirements.txt
Normal file
5
doc/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
pyside2
|
||||
numpy
|
||||
pyopengl
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
14
doc/source/_static/custom.css
Normal file
14
doc/source/_static/custom.css
Normal 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%;
|
||||
}
|
||||
}
|
@ -13,5 +13,7 @@ Contents:
|
||||
3dgraphics/index
|
||||
colormap
|
||||
parametertree/index
|
||||
dockarea
|
||||
graphicsscene/index
|
||||
flowchart/index
|
||||
graphicswindow
|
||||
|
@ -11,7 +11,9 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
@ -19,6 +21,7 @@ import sys, os
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.join(path, '..', '..'))
|
||||
sys.path.insert(0, os.path.join(path, '..', 'extensions'))
|
||||
import pyqtgraph
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
@ -43,16 +46,16 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'pyqtgraph'
|
||||
copyright = '2011, Luke Campagnola'
|
||||
copyright = '2011 - {}, Luke Campagnola'.format(datetime.now().year)
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.10.0'
|
||||
version = pyqtgraph.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.10.0'
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -88,12 +91,18 @@ pygments_style = 'sphinx'
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
autodoc_inherit_docstrings = False
|
||||
autodoc_mock_imports = [
|
||||
"scipy",
|
||||
"h5py",
|
||||
"matplotlib",
|
||||
]
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@ -124,6 +133,10 @@ html_theme = 'default'
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# add the theme customizations
|
||||
def setup(app):
|
||||
app.add_stylesheet("custom.css")
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
@ -216,4 +229,3 @@ man_pages = [
|
||||
('index', 'pyqtgraph', 'pyqtgraph Documentation',
|
||||
['Luke Campagnola'], 1)
|
||||
]
|
||||
|
||||
|
11
doc/source/dockarea.rst
Normal file
11
doc/source/dockarea.rst
Normal file
@ -0,0 +1,11 @@
|
||||
Dock Area Module
|
||||
================
|
||||
|
||||
.. automodule:: pyqtgraph.dockarea
|
||||
:members:
|
||||
|
||||
.. autoclass:: pyqtgraph.dockarea.DockArea
|
||||
:members:
|
||||
|
||||
.. autoclass:: pyqtgraph.dockarea.Dock
|
||||
:members:
|
@ -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
|
||||
----------------------
|
||||
|
8
doc/source/graphicsItems/bargraphitem.rst
Normal file
8
doc/source/graphicsItems/bargraphitem.rst
Normal file
@ -0,0 +1,8 @@
|
||||
BarGraphItem
|
||||
============
|
||||
|
||||
.. autoclass:: pyqtgraph.BarGraphItem
|
||||
:members:
|
||||
|
||||
.. automethod:: pyqtgraph.BarGraphItem.__init__
|
||||
|
8
doc/source/graphicsItems/dateaxisitem.rst
Normal file
8
doc/source/graphicsItems/dateaxisitem.rst
Normal file
@ -0,0 +1,8 @@
|
||||
DateAxisItem
|
||||
============
|
||||
|
||||
.. autoclass:: pyqtgraph.DateAxisItem
|
||||
:members:
|
||||
|
||||
.. automethod:: pyqtgraph.DateAxisItem.__init__
|
||||
|
@ -24,6 +24,7 @@ Contents:
|
||||
axisitem
|
||||
textitem
|
||||
errorbaritem
|
||||
bargraphitem
|
||||
arrowitem
|
||||
fillbetweenitem
|
||||
curvepoint
|
||||
@ -42,4 +43,4 @@ Contents:
|
||||
graphicsitem
|
||||
uigraphicsitem
|
||||
graphicswidgetanchor
|
||||
|
||||
dateaxisitem
|
||||
|
@ -2,6 +2,7 @@ files = """ArrowItem
|
||||
AxisItem
|
||||
ButtonItem
|
||||
CurvePoint
|
||||
DateAxisItem
|
||||
GradientEditorItem
|
||||
GradientLegend
|
||||
GraphicsLayout
|
||||
|
@ -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:
|
||||
|
@ -1,45 +1,70 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
There are many different ways to install pyqtgraph, depending on your needs:
|
||||
|
||||
* The most common way to install pyqtgraph is with pip::
|
||||
|
||||
$ pip install pyqtgraph
|
||||
|
||||
Some users may need to call ``pip3`` instead. This method should work on
|
||||
all platforms.
|
||||
* To get access to the very latest features and bugfixes, clone pyqtgraph from
|
||||
github::
|
||||
|
||||
$ git clone https://github.com/pyqtgraph/pyqtgraph
|
||||
|
||||
Now you can install pyqtgraph from the source::
|
||||
|
||||
$ python setup.py install
|
||||
|
||||
..or you can simply place the pyqtgraph folder someplace importable, such as
|
||||
inside the root of another project. PyQtGraph does not need to be "built" or
|
||||
compiled in any way.
|
||||
* Packages for pyqtgraph are also available in a few other forms:
|
||||
|
||||
* **Anaconda**: ``conda install pyqtgraph``
|
||||
* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or
|
||||
download the .deb file linked at the top of the pyqtgraph web page.
|
||||
* **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577)
|
||||
* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page.
|
||||
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
PyQtGraph depends on:
|
||||
|
||||
* Python 2.7 or Python 3.x
|
||||
* A Qt library such as PyQt4, PyQt5, PySide, or PySide2
|
||||
* numpy
|
||||
|
||||
The easiest way to meet these dependencies is with ``pip`` or with a scientific python
|
||||
distribution like Anaconda.
|
||||
The easiest way to meet these dependencies is with ``pip`` or with a scientific
|
||||
python distribution like Anaconda.
|
||||
|
||||
.. _pyqtgraph: http://www.pyqtgraph.org/
|
||||
There are many different ways to install pyqtgraph, depending on your needs:
|
||||
|
||||
pip
|
||||
---
|
||||
|
||||
The most common way to install pyqtgraph is with pip::
|
||||
|
||||
$ pip install pyqtgraph
|
||||
|
||||
Some users may need to call ``pip3`` instead. This method should work on all
|
||||
platforms.
|
||||
|
||||
conda
|
||||
-----
|
||||
|
||||
pyqtgraph is on the default Anaconda channel::
|
||||
|
||||
$ conda install pyqtgraph
|
||||
|
||||
It is also available in the conda-forge channel::
|
||||
|
||||
$ conda install -c conda-forge pyqtgraph
|
||||
|
||||
From Source
|
||||
-----------
|
||||
|
||||
To get access to the very latest features and bugfixes you have three choices:
|
||||
|
||||
1. Clone pyqtgraph from github::
|
||||
|
||||
$ git clone https://github.com/pyqtgraph/pyqtgraph
|
||||
$ cd pyqtgraph
|
||||
|
||||
Now you can install pyqtgraph from the source::
|
||||
|
||||
$ pip install .
|
||||
|
||||
2. Directly install from GitHub repo::
|
||||
|
||||
$ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop
|
||||
|
||||
You can change ``develop`` of the above command to the branch name or the
|
||||
commit you prefer.
|
||||
|
||||
3. You can simply place the pyqtgraph folder someplace importable, such as
|
||||
inside the root of another project. PyQtGraph does not need to be "built" or
|
||||
compiled in any way.
|
||||
|
||||
Other Packages
|
||||
--------------
|
||||
|
||||
Packages for pyqtgraph are also available in a few other forms:
|
||||
|
||||
* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or
|
||||
download the .deb file linked at the top of the pyqtgraph web page.
|
||||
* **Arch Linux:** https://www.archlinux.org/packages/community/any/python-pyqtgraph/
|
||||
* **Windows:** Download and run the .exe installer file linked at the top of the
|
||||
pyqtgraph web page: http://pyqtgraph.org
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
dockarea module
|
||||
===============
|
||||
|
||||
.. automodule:: pyqtgraph.dockarea
|
||||
:members:
|
@ -12,11 +12,9 @@ Contents:
|
||||
|
||||
plotwidget
|
||||
imageview
|
||||
dockarea
|
||||
spinbox
|
||||
gradientwidget
|
||||
histogramlutwidget
|
||||
parametertree
|
||||
consolewidget
|
||||
colormapwidget
|
||||
scatterplotwidget
|
||||
|
@ -1,5 +0,0 @@
|
||||
parametertree module
|
||||
====================
|
||||
|
||||
.. automodule:: pyqtgraph.parametertree
|
||||
:members:
|
33
examples/DateAxisItem.py
Normal file
33
examples/DateAxisItem.py
Normal 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_()
|
48
examples/DateAxisItem_QtDesigner.py
Normal file
48
examples/DateAxisItem_QtDesigner.py
Normal 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_()
|
44
examples/DateAxisItem_QtDesigner.ui
Normal file
44
examples/DateAxisItem_QtDesigner.ui
Normal 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>
|
@ -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'},
|
||||
|
@ -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)
|
||||
|
@ -92,10 +92,10 @@ def updateRoiPlot(roi, data=None):
|
||||
rois = []
|
||||
rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9)))
|
||||
rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9)))
|
||||
rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9)))
|
||||
rois.append(pg.MultiRectROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9)))
|
||||
rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9)))
|
||||
rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9)))
|
||||
rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9)))
|
||||
rois.append(pg.PolyLineROI([[2,0], [2.1,0], [2,.1]], pen=(5,9)))
|
||||
#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0)))
|
||||
|
||||
## Add each ROI to the scene and link its data to a plot curve with the same color
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -9,8 +9,9 @@ import subprocess
|
||||
from pyqtgraph.python2_3 import basestring
|
||||
from pyqtgraph.Qt import QtGui, QT_LIB
|
||||
|
||||
from .utils import buildFileList, path, examples
|
||||
from .syntax import PythonHighlighter
|
||||
|
||||
from .utils import buildFileList, testFile, path, examples
|
||||
|
||||
if QT_LIB == 'PySide':
|
||||
from .exampleLoaderTemplate_pyside import Ui_Form
|
||||
@ -21,6 +22,16 @@ elif QT_LIB == 'PyQt5':
|
||||
else:
|
||||
from .exampleLoaderTemplate_pyqt import Ui_Form
|
||||
|
||||
class App(QtGui.QApplication):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.paletteChanged.connect(self.onPaletteChange)
|
||||
self.onPaletteChange(self.palette())
|
||||
|
||||
def onPaletteChange(self, palette):
|
||||
self.dark_mode = palette.base().color().name().lower() != "#ffffff"
|
||||
|
||||
class ExampleLoader(QtGui.QMainWindow):
|
||||
def __init__(self):
|
||||
QtGui.QMainWindow.__init__(self)
|
||||
@ -33,6 +44,9 @@ class ExampleLoader(QtGui.QMainWindow):
|
||||
self.codeBtn = QtGui.QPushButton('Run Edited Code')
|
||||
self.codeLayout = QtGui.QGridLayout()
|
||||
self.ui.codeView.setLayout(self.codeLayout)
|
||||
self.hl = PythonHighlighter(self.ui.codeView.document())
|
||||
app = QtGui.QApplication.instance()
|
||||
app.paletteChanged.connect(self.updateTheme)
|
||||
self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0)
|
||||
self.codeLayout.addWidget(self.codeBtn, 1, 1)
|
||||
self.codeBtn.hide()
|
||||
@ -51,6 +65,28 @@ class ExampleLoader(QtGui.QMainWindow):
|
||||
self.ui.codeView.textChanged.connect(self.codeEdited)
|
||||
self.codeBtn.clicked.connect(self.runEditedCode)
|
||||
|
||||
def simulate_black_mode(self):
|
||||
"""
|
||||
used to simulate MacOS "black mode" on other platforms
|
||||
intended for debug only, as it manage only the QPlainTextEdit
|
||||
"""
|
||||
# first, a dark background
|
||||
c = QtGui.QColor('#171717')
|
||||
p = self.ui.codeView.palette()
|
||||
p.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, c)
|
||||
p.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base, c)
|
||||
self.ui.codeView.setPalette(p)
|
||||
# then, a light font
|
||||
f = QtGui.QTextCharFormat()
|
||||
f.setForeground(QtGui.QColor('white'))
|
||||
self.ui.codeView.setCurrentCharFormat(f)
|
||||
# finally, override application automatic detection
|
||||
app = QtGui.QApplication.instance()
|
||||
app.dark_mode = True
|
||||
|
||||
def updateTheme(self):
|
||||
self.hl = PythonHighlighter(self.ui.codeView.document())
|
||||
|
||||
def populateTree(self, root, examples):
|
||||
for key, val in examples.items():
|
||||
item = QtGui.QTreeWidgetItem([key])
|
||||
@ -115,32 +151,9 @@ class ExampleLoader(QtGui.QMainWindow):
|
||||
self.loadFile(edited=True)
|
||||
|
||||
def run():
|
||||
app = QtGui.QApplication([])
|
||||
app = App([])
|
||||
loader = ExampleLoader()
|
||||
|
||||
app.exec_()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
if '--test' in args:
|
||||
# get rid of orphaned cache files first
|
||||
pg.renamePyc(path)
|
||||
|
||||
files = buildFileList(examples)
|
||||
if '--pyside' in args:
|
||||
lib = 'PySide'
|
||||
elif '--pyqt' in args or '--pyqt4' in args:
|
||||
lib = 'PyQt4'
|
||||
elif '--pyqt5' in args:
|
||||
lib = 'PyQt5'
|
||||
else:
|
||||
lib = ''
|
||||
|
||||
exe = sys.executable
|
||||
print("Running tests:", lib, sys.executable)
|
||||
for f in files:
|
||||
testFile(f[0], f[1], exe, lib)
|
||||
else:
|
||||
run()
|
||||
run()
|
||||
|
@ -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()
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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
250
examples/syntax.py
Normal 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
|
@ -1,11 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from pyqtgraph import Qt
|
||||
from . import utils
|
||||
from collections import namedtuple
|
||||
from pyqtgraph import Qt
|
||||
import errno
|
||||
import importlib
|
||||
import itertools
|
||||
import pytest
|
||||
import os, sys
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
# printing on travis ci frequently leads to "interrupted system call" errors.
|
||||
# as a workaround, we overwrite the built-in print function (bleh)
|
||||
if os.getenv('TRAVIS') is not None:
|
||||
@ -32,17 +40,13 @@ if os.getenv('TRAVIS') is not None:
|
||||
print("Installed wrapper for flaky print.")
|
||||
|
||||
|
||||
# apparently importlib does not exist in python 2.6...
|
||||
try:
|
||||
import importlib
|
||||
except ImportError:
|
||||
# we are on python 2.6
|
||||
print("If you want to test the examples, please install importlib from "
|
||||
"pypi\n\npip install importlib\n\n")
|
||||
pass
|
||||
|
||||
files = utils.buildFileList(utils.examples)
|
||||
frontends = {Qt.PYQT4: False, Qt.PYSIDE: False}
|
||||
files = sorted(set(utils.buildFileList(utils.examples)))
|
||||
frontends = {
|
||||
Qt.PYQT4: False,
|
||||
Qt.PYQT5: False,
|
||||
Qt.PYSIDE: False,
|
||||
Qt.PYSIDE2: False
|
||||
}
|
||||
# sort out which of the front ends are available
|
||||
for frontend in frontends.keys():
|
||||
try:
|
||||
@ -51,15 +55,204 @@ for frontend in frontends.keys():
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
installedFrontends = sorted([
|
||||
frontend for frontend, isPresent in frontends.items() if isPresent
|
||||
])
|
||||
|
||||
exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"])
|
||||
conditionalExamples = {
|
||||
"hdf5.py": exceptionCondition(
|
||||
False,
|
||||
reason="Example requires user interaction"
|
||||
),
|
||||
"RemoteSpeedTest.py": exceptionCondition(
|
||||
False,
|
||||
reason="Test is being problematic on CI machines"
|
||||
),
|
||||
"optics_demos.py": exceptionCondition(
|
||||
not frontends[Qt.PYSIDE],
|
||||
reason=(
|
||||
"Test fails due to PySide bug: ",
|
||||
"https://bugreports.qt.io/browse/PYSIDE-671"
|
||||
)
|
||||
),
|
||||
'GLVolumeItem.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLIsosurface.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLSurfacePlot.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLScatterPlotItem.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLshaders.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLLinePlotItem.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLMeshItem.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
),
|
||||
'GLImageItem.py': exceptionCondition(
|
||||
not(sys.platform == "darwin" and
|
||||
sys.version_info[0] == 2 and
|
||||
(frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])),
|
||||
reason=(
|
||||
"glClear does not work on macOS + Python2.7 + Qt4: ",
|
||||
"https://github.com/pyqtgraph/pyqtgraph/issues/939"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@pytest.mark.skipif(
|
||||
Qt.QT_LIB == "PySide2"
|
||||
and tuple(map(int, Qt.PySide2.__version__.split("."))) >= (5, 14)
|
||||
and tuple(map(int, Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2),
|
||||
reason="new PySide2 doesn't have loadUi functionality"
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"frontend, f", itertools.product(sorted(list(frontends.keys())), files))
|
||||
def test_examples(frontend, f):
|
||||
# Test the examples with all available front-ends
|
||||
print('frontend = %s. f = %s' % (frontend, f))
|
||||
if not frontends[frontend]:
|
||||
pytest.skip('%s is not installed. Skipping tests' % frontend)
|
||||
utils.testFile(f[0], f[1], utils.sys.executable, frontend)
|
||||
"frontend, f",
|
||||
[
|
||||
pytest.param(
|
||||
frontend,
|
||||
f,
|
||||
marks=pytest.mark.skipif(
|
||||
conditionalExamples[f[1]].condition is False,
|
||||
reason=conditionalExamples[f[1]].reason
|
||||
) if f[1] in conditionalExamples.keys() else (),
|
||||
)
|
||||
for frontend, f, in itertools.product(installedFrontends, files)
|
||||
],
|
||||
ids = [
|
||||
" {} - {} ".format(f[1], frontend)
|
||||
for frontend, f in itertools.product(
|
||||
installedFrontends,
|
||||
files
|
||||
)
|
||||
]
|
||||
)
|
||||
def testExamples(frontend, f, graphicsSystem=None):
|
||||
# runExampleFile(f[0], f[1], sys.executable, frontend)
|
||||
|
||||
name, file = f
|
||||
global path
|
||||
fn = os.path.join(path, file)
|
||||
os.chdir(path)
|
||||
sys.stdout.write("{} ".format(name))
|
||||
sys.stdout.flush()
|
||||
import1 = "import %s" % frontend if frontend != '' else ''
|
||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||
graphicsSystem = (
|
||||
'' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
|
||||
)
|
||||
code = """
|
||||
try:
|
||||
%s
|
||||
import initExample
|
||||
import pyqtgraph as pg
|
||||
%s
|
||||
import %s
|
||||
import sys
|
||||
print("test complete")
|
||||
sys.stdout.flush()
|
||||
import time
|
||||
while True: ## run a little event loop
|
||||
pg.QtGui.QApplication.processEvents()
|
||||
time.sleep(0.01)
|
||||
except:
|
||||
print("test failed")
|
||||
raise
|
||||
|
||||
""" % (import1, graphicsSystem, import2)
|
||||
if sys.platform.startswith('win'):
|
||||
process = subprocess.Popen([sys.executable],
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
else:
|
||||
process = subprocess.Popen(['exec %s -i' % (sys.executable)],
|
||||
shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE)
|
||||
process.stdin.write(code.encode('UTF-8'))
|
||||
process.stdin.close()
|
||||
output = ''
|
||||
fail = False
|
||||
while True:
|
||||
try:
|
||||
c = process.stdout.read(1).decode()
|
||||
except IOError as err:
|
||||
if err.errno == errno.EINTR:
|
||||
# Interrupted system call; just try again.
|
||||
c = ''
|
||||
else:
|
||||
raise
|
||||
output += c
|
||||
|
||||
if output.endswith('test complete'):
|
||||
break
|
||||
if output.endswith('test failed'):
|
||||
fail = True
|
||||
break
|
||||
time.sleep(1)
|
||||
process.kill()
|
||||
#res = process.communicate()
|
||||
res = (process.stdout.read(), process.stderr.read())
|
||||
if (fail or
|
||||
'exception' in res[1].decode().lower() or
|
||||
'error' in res[1].decode().lower()):
|
||||
print(res[0].decode())
|
||||
print(res[1].decode())
|
||||
pytest.fail("{}\n{}\nFailed {} Example Test Located in {} "
|
||||
.format(res[0].decode(), res[1].decode(), name, file),
|
||||
pytrace=False)
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.cmdline.main()
|
||||
|
@ -1,9 +1,5 @@
|
||||
from __future__ import division, print_function, absolute_import
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
from pyqtgraph.pgcollections import OrderedDict
|
||||
from pyqtgraph.python2_3 import basestring
|
||||
|
||||
@ -18,6 +14,7 @@ examples = OrderedDict([
|
||||
('Crosshair / Mouse interaction', 'crosshair.py'),
|
||||
('Data Slicing', 'DataSlicing.py'),
|
||||
('Plot Customization', 'customPlot.py'),
|
||||
('Timestamps on x axis', 'DateAxisItem.py'),
|
||||
('Image Analysis', 'imageAnalysis.py'),
|
||||
('ViewBox Features', 'ViewBoxFeatures.py'),
|
||||
('Dock widgets', 'dockarea.py'),
|
||||
@ -86,7 +83,6 @@ examples = OrderedDict([
|
||||
#('VerticalLabel', '../widgets/VerticalLabel.py'),
|
||||
('JoystickButton', 'JoystickButton.py'),
|
||||
])),
|
||||
|
||||
('Flowcharts', 'Flowchart.py'),
|
||||
('Custom Flowchart Nodes', 'FlowchartCustomNode.py'),
|
||||
])
|
||||
@ -103,73 +99,3 @@ def buildFileList(examples, files=None):
|
||||
else:
|
||||
buildFileList(val, files)
|
||||
return files
|
||||
|
||||
def testFile(name, f, exe, lib, graphicsSystem=None):
|
||||
global path
|
||||
fn = os.path.join(path,f)
|
||||
#print "starting process: ", fn
|
||||
os.chdir(path)
|
||||
sys.stdout.write(name)
|
||||
sys.stdout.flush()
|
||||
|
||||
import1 = "import %s" % lib if lib != '' else ''
|
||||
import2 = os.path.splitext(os.path.split(fn)[1])[0]
|
||||
graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem
|
||||
code = """
|
||||
try:
|
||||
%s
|
||||
import initExample
|
||||
import pyqtgraph as pg
|
||||
%s
|
||||
import %s
|
||||
import sys
|
||||
print("test complete")
|
||||
sys.stdout.flush()
|
||||
import time
|
||||
while True: ## run a little event loop
|
||||
pg.QtGui.QApplication.processEvents()
|
||||
time.sleep(0.01)
|
||||
except:
|
||||
print("test failed")
|
||||
raise
|
||||
|
||||
""" % (import1, graphicsSystem, import2)
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
process = subprocess.Popen([exe], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
process.stdin.write(code.encode('UTF-8'))
|
||||
process.stdin.close()
|
||||
else:
|
||||
process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
process.stdin.write(code.encode('UTF-8'))
|
||||
process.stdin.close() ##?
|
||||
output = ''
|
||||
fail = False
|
||||
while True:
|
||||
try:
|
||||
c = process.stdout.read(1).decode()
|
||||
except IOError as err:
|
||||
if err.errno == errno.EINTR:
|
||||
# Interrupted system call; just try again.
|
||||
c = ''
|
||||
else:
|
||||
raise
|
||||
output += c
|
||||
#sys.stdout.write(c)
|
||||
#sys.stdout.flush()
|
||||
if output.endswith('test complete'):
|
||||
break
|
||||
if output.endswith('test failed'):
|
||||
fail = True
|
||||
break
|
||||
time.sleep(1)
|
||||
process.kill()
|
||||
#res = process.communicate()
|
||||
res = (process.stdout.read(), process.stderr.read())
|
||||
|
||||
if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower():
|
||||
print('.' * (50-len(name)) + 'FAILED')
|
||||
print(res[0].decode())
|
||||
print(res[1].decode())
|
||||
else:
|
||||
print('.' * (50-len(name)) + 'passed')
|
||||
|
@ -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,14 +90,10 @@ 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)
|
||||
@ -183,12 +181,14 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
if int(ev.buttons() & btn) == 0:
|
||||
continue
|
||||
if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet
|
||||
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0]
|
||||
dist = Point(ev.scenePos() - cev.scenePos()).length()
|
||||
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
|
||||
continue
|
||||
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
|
||||
self.dragButtons.append(int(btn))
|
||||
cev = [e for e in self.clickEvents if int(e.button()) == int(btn)]
|
||||
if cev:
|
||||
cev = cev[0]
|
||||
dist = Point(ev.scenePos() - cev.scenePos()).length()
|
||||
if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime):
|
||||
continue
|
||||
init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True
|
||||
self.dragButtons.append(int(btn))
|
||||
|
||||
## If we have dragged buttons, deliver a drag event
|
||||
if len(self.dragButtons) > 0:
|
||||
@ -208,10 +208,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
self.dragButtons.remove(ev.button())
|
||||
else:
|
||||
cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())]
|
||||
if self.sendClickEvent(cev[0]):
|
||||
#print "sent click event"
|
||||
ev.accept()
|
||||
self.clickEvents.remove(cev[0])
|
||||
if cev:
|
||||
if self.sendClickEvent(cev[0]):
|
||||
#print "sent click event"
|
||||
ev.accept()
|
||||
self.clickEvents.remove(cev[0])
|
||||
|
||||
if int(ev.buttons()) == 0:
|
||||
self.dragItem = None
|
||||
@ -263,7 +264,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
for item in prevItems:
|
||||
event.currentItem = item
|
||||
try:
|
||||
item.hoverEvent(event)
|
||||
if item.scene() is self:
|
||||
item.hoverEvent(event)
|
||||
except:
|
||||
debug.printExc("Error sending hover exit event:")
|
||||
finally:
|
||||
@ -288,7 +290,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
else:
|
||||
acceptedItem = None
|
||||
|
||||
if acceptedItem is not None:
|
||||
if acceptedItem is not None and acceptedItem.scene() is self:
|
||||
#print "Drag -> pre-selected item:", acceptedItem
|
||||
self.dragItem = acceptedItem
|
||||
event.currentItem = self.dragItem
|
||||
@ -364,46 +366,15 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
return ev.isAccepted()
|
||||
|
||||
def items(self, *args):
|
||||
#print 'args:', args
|
||||
items = QtGui.QGraphicsScene.items(self, *args)
|
||||
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
|
||||
## then the object returned will be different than the actual item that was originally added to the scene
|
||||
items2 = list(map(self.translateGraphicsItem, items))
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#items2 = []
|
||||
#for i in items:
|
||||
#addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem))
|
||||
#i2 = GraphicsScene._addressCache.get(addr, i)
|
||||
##print i, "==>", i2
|
||||
#items2.append(i2)
|
||||
#print 'items:', items
|
||||
return items2
|
||||
return self.translateGraphicsItems(items)
|
||||
|
||||
def selectedItems(self, *args):
|
||||
items = QtGui.QGraphicsScene.selectedItems(self, *args)
|
||||
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
|
||||
## then the object returned will be different than the actual item that was originally added to the scene
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#items2 = []
|
||||
#for i in items:
|
||||
#addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem))
|
||||
#i2 = GraphicsScene._addressCache.get(addr, i)
|
||||
##print i, "==>", i2
|
||||
#items2.append(i2)
|
||||
items2 = list(map(self.translateGraphicsItem, items))
|
||||
|
||||
#print 'items:', items
|
||||
return items2
|
||||
return self.translateGraphicsItems(items)
|
||||
|
||||
def itemAt(self, *args):
|
||||
item = QtGui.QGraphicsScene.itemAt(self, *args)
|
||||
|
||||
## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject,
|
||||
## then the object returned will be different than the actual item that was originally added to the scene
|
||||
#if HAVE_SIP and isinstance(self, sip.wrapper):
|
||||
#addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem))
|
||||
#item = GraphicsScene._addressCache.get(addr, item)
|
||||
#return item
|
||||
return self.translateGraphicsItem(item)
|
||||
|
||||
def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False):
|
||||
@ -435,6 +406,8 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
for item in items:
|
||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||
continue
|
||||
if item.scene() is not self:
|
||||
continue
|
||||
shape = item.shape() # Note: default shape() returns boundingRect()
|
||||
if shape is None:
|
||||
continue
|
||||
@ -448,7 +421,7 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
return 0
|
||||
return item.zValue() + absZValue(item.parentItem())
|
||||
|
||||
sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a)))
|
||||
items2.sort(key=absZValue, reverse=True)
|
||||
|
||||
return items2
|
||||
|
||||
@ -548,15 +521,16 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
||||
|
||||
@staticmethod
|
||||
def translateGraphicsItem(item):
|
||||
## for fixing pyqt bugs where the wrong item is returned
|
||||
# This function is intended as a workaround for a problem with older
|
||||
# versions of PyQt (< 4.9?), where methods returning 'QGraphicsItem *'
|
||||
# lose the type of the QGraphicsObject subclasses and instead return
|
||||
# generic QGraphicsItem wrappers.
|
||||
if HAVE_SIP and isinstance(item, sip.wrapper):
|
||||
addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem))
|
||||
item = GraphicsScene._addressCache.get(addr, item)
|
||||
obj = item.toGraphicsObject()
|
||||
if obj is not None:
|
||||
item = obj
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
def translateGraphicsItems(items):
|
||||
return list(map(GraphicsScene.translateGraphicsItem, items))
|
||||
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module exists to smooth out some of the differences between PySide and PyQt4:
|
||||
|
||||
@ -9,7 +10,7 @@ This module exists to smooth out some of the differences between PySide and PyQt
|
||||
|
||||
"""
|
||||
|
||||
import os, sys, re, time
|
||||
import os, sys, re, time, subprocess, warnings
|
||||
|
||||
from .python2_3 import asUnicode
|
||||
|
||||
@ -100,25 +101,42 @@ def _loadUiType(uiFile):
|
||||
how to make PyQt4 and pyside look the same...
|
||||
http://stackoverflow.com/a/8717832
|
||||
"""
|
||||
import pysideuic
|
||||
import xml.etree.ElementTree as xml
|
||||
#from io import StringIO
|
||||
|
||||
if QT_LIB == "PYSIDE":
|
||||
import pysideuic
|
||||
else:
|
||||
try:
|
||||
import pyside2uic as pysideuic
|
||||
except ImportError:
|
||||
# later vserions of pyside2 have dropped pysideuic; use the uic binary instead.
|
||||
pysideuic = None
|
||||
|
||||
# get class names from ui file
|
||||
import xml.etree.ElementTree as xml
|
||||
parsed = xml.parse(uiFile)
|
||||
widget_class = parsed.find('widget').get('class')
|
||||
form_class = parsed.find('class').text
|
||||
|
||||
with open(uiFile, 'r') as f:
|
||||
# convert ui file to python code
|
||||
if pysideuic is None:
|
||||
pyside2version = tuple(map(int, PySide2.__version__.split(".")))
|
||||
if pyside2version >= (5, 14) and pyside2version < (5, 14, 2, 2):
|
||||
warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15')
|
||||
uipy = subprocess.check_output(['pyside2-uic', uiFile])
|
||||
else:
|
||||
o = _StringIO()
|
||||
frame = {}
|
||||
with open(uiFile, 'r') as f:
|
||||
pysideuic.compileUi(f, o, indent=0)
|
||||
uipy = o.getvalue()
|
||||
|
||||
pysideuic.compileUi(f, o, indent=0)
|
||||
pyc = compile(o.getvalue(), '<string>', 'exec')
|
||||
exec(pyc, frame)
|
||||
# exceute python code
|
||||
pyc = compile(uipy, '<string>', 'exec')
|
||||
frame = {}
|
||||
exec(pyc, frame)
|
||||
|
||||
#Fetch the base_class and form class based on their type in the xml from designer
|
||||
form_class = frame['Ui_%s'%form_class]
|
||||
base_class = eval('QtGui.%s'%widget_class)
|
||||
# fetch the base_class and form class based on their type in the xml from designer
|
||||
form_class = frame['Ui_%s'%form_class]
|
||||
base_class = eval('QtGui.%s'%widget_class)
|
||||
|
||||
return form_class, base_class
|
||||
|
||||
@ -216,8 +234,12 @@ elif QT_LIB == PYSIDE2:
|
||||
except ImportError as err:
|
||||
QtTest = FailedImport(err)
|
||||
|
||||
isQObjectAlive = _isQObjectAlive
|
||||
|
||||
try:
|
||||
import shiboken2
|
||||
isQObjectAlive = shiboken2.isValid
|
||||
except ImportError:
|
||||
# use approximate version
|
||||
isQObjectAlive = _isQObjectAlive
|
||||
import PySide2
|
||||
VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
|
||||
|
||||
@ -322,9 +344,19 @@ if m is not None and list(map(int, m.groups())) < versionReq:
|
||||
|
||||
|
||||
QAPP = None
|
||||
def mkQApp():
|
||||
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
|
||||
|
@ -67,11 +67,11 @@ class SignalProxy(QtCore.QObject):
|
||||
"""If there is a signal queued up, send it now."""
|
||||
if self.args is None or self.block:
|
||||
return False
|
||||
#self.emit(self.signal, *self.args)
|
||||
self.sigDelayed.emit(self.args)
|
||||
self.args = None
|
||||
args, self.args = self.args, None
|
||||
self.timer.stop()
|
||||
self.lastFlushTime = time()
|
||||
#self.emit(self.signal, *self.args)
|
||||
self.sigDelayed.emit(args)
|
||||
return True
|
||||
|
||||
def disconnect(self):
|
||||
@ -81,7 +81,7 @@ class SignalProxy(QtCore.QObject):
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
self.sigDelayed.disconnect(self.slot())
|
||||
self.sigDelayed.disconnect(self.slot)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python
|
||||
www.pyqtgraph.org
|
||||
"""
|
||||
|
||||
__version__ = '0.11.0.dev0'
|
||||
__version__ = '0.11.0'
|
||||
|
||||
### import all the goodies and add some helper functions for easy CLI use
|
||||
|
||||
@ -29,9 +29,6 @@ if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1]
|
||||
## helpers for 2/3 compatibility
|
||||
from . import python2_3
|
||||
|
||||
## install workarounds for numpy bugs
|
||||
from . import numpy_fix
|
||||
|
||||
## in general openGL is poorly supported with Qt+GraphicsView.
|
||||
## we only enable it where the performance benefit is critical.
|
||||
## Note this only applies to 2D graphics; 3D graphics always use OpenGL.
|
||||
@ -67,7 +64,6 @@ CONFIG_OPTIONS = {
|
||||
|
||||
|
||||
def setConfigOption(opt, value):
|
||||
global CONFIG_OPTIONS
|
||||
if opt not in CONFIG_OPTIONS:
|
||||
raise KeyError('Unknown configuration option "%s"' % opt)
|
||||
if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'):
|
||||
@ -99,7 +95,8 @@ def systemInfo():
|
||||
if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file
|
||||
lastRevFile = os.path.join(os.path.dirname(__file__), '..', '.bzr', 'branch', 'last-revision')
|
||||
if os.path.exists(lastRevFile):
|
||||
rev = open(lastRevFile, 'r').read().strip()
|
||||
with open(lastRevFile, 'r') as fd:
|
||||
rev = fd.read().strip()
|
||||
|
||||
print("pyqtgraph: %s; %s" % (__version__, rev))
|
||||
print("config:")
|
||||
@ -222,6 +219,7 @@ from .graphicsItems.ViewBox import *
|
||||
from .graphicsItems.ArrowItem import *
|
||||
from .graphicsItems.ImageItem import *
|
||||
from .graphicsItems.AxisItem import *
|
||||
from .graphicsItems.DateAxisItem import *
|
||||
from .graphicsItems.LabelItem import *
|
||||
from .graphicsItems.CurvePoint import *
|
||||
from .graphicsItems.GraphicsWidgetAnchor import *
|
||||
@ -264,6 +262,7 @@ from .widgets.LayoutWidget import *
|
||||
from .widgets.TableWidget import *
|
||||
from .widgets.ProgressDialog import *
|
||||
from .widgets.GroupBox import GroupBox
|
||||
from .widgets.RemoteGraphicsView import RemoteGraphicsView
|
||||
|
||||
from .imageview import *
|
||||
from .WidgetGroup import *
|
||||
@ -369,8 +368,12 @@ def exit():
|
||||
## close file handles
|
||||
if sys.platform == 'darwin':
|
||||
for fd in range(3, 4096):
|
||||
if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac.
|
||||
if fd in [7]: # trying to close 7 produces an illegal instruction on the Mac.
|
||||
continue
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
|
||||
|
||||
@ -410,12 +413,20 @@ def plot(*args, **kargs):
|
||||
dataArgs[k] = kargs[k]
|
||||
|
||||
w = PlotWindow(**pwArgs)
|
||||
w.sigClosed.connect(_plotWindowClosed)
|
||||
if len(args) > 0 or len(dataArgs) > 0:
|
||||
w.plot(*args, **dataArgs)
|
||||
plots.append(w)
|
||||
w.show()
|
||||
return w
|
||||
|
||||
def _plotWindowClosed(w):
|
||||
w.close()
|
||||
try:
|
||||
plots.remove(w)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def image(*args, **kargs):
|
||||
"""
|
||||
Create and return an :class:`ImageWindow <pyqtgraph.ImageWindow>`
|
||||
@ -426,11 +437,19 @@ def image(*args, **kargs):
|
||||
"""
|
||||
mkQApp()
|
||||
w = ImageWindow(*args, **kargs)
|
||||
w.sigClosed.connect(_imageWindowClosed)
|
||||
images.append(w)
|
||||
w.show()
|
||||
return w
|
||||
show = image ## for backward compatibility
|
||||
|
||||
def _imageWindowClosed(w):
|
||||
w.close()
|
||||
try:
|
||||
images.remove(w)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def dbg(*args, **kwds):
|
||||
"""
|
||||
Create a console window and begin watching for exceptions.
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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))
|
||||
|
@ -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"))
|
||||
|
@ -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))
|
||||
|
@ -17,7 +17,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>PyQtGraph</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
|
@ -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))
|
||||
|
@ -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:"))
|
||||
|
@ -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))
|
||||
|
@ -1,6 +1,7 @@
|
||||
import numpy as np
|
||||
from .Qt import QtGui, QtCore
|
||||
from .python2_3 import basestring
|
||||
from .functions import mkColor
|
||||
|
||||
|
||||
class ColorMap(object):
|
||||
@ -56,9 +57,9 @@ class ColorMap(object):
|
||||
=============== ==============================================================
|
||||
**Arguments:**
|
||||
pos Array of positions where each color is defined
|
||||
color Array of RGBA colors.
|
||||
Integer data types are interpreted as 0-255; float data types
|
||||
are interpreted as 0.0-1.0
|
||||
color Array of colors.
|
||||
Values are interpreted via
|
||||
:func:`mkColor() <pyqtgraph.mkColor>`.
|
||||
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
|
||||
indicating the color space that should be used when
|
||||
interpolating between stops. Note that the last mode value is
|
||||
@ -68,7 +69,11 @@ class ColorMap(object):
|
||||
self.pos = np.array(pos)
|
||||
order = np.argsort(self.pos)
|
||||
self.pos = self.pos[order]
|
||||
self.color = np.array(color)[order]
|
||||
self.color = np.apply_along_axis(
|
||||
func1d = lambda x: mkColor(x).getRgb(),
|
||||
axis = -1,
|
||||
arr = color,
|
||||
)[order]
|
||||
if mode is None:
|
||||
mode = np.ones(len(pos))
|
||||
self.mode = mode
|
||||
@ -141,7 +146,7 @@ class ColorMap(object):
|
||||
|
||||
pos, color = self.getStops(mode=self.BYTE)
|
||||
color = [QtGui.QColor(*x) for x in color]
|
||||
g.setStops(zip(pos, color))
|
||||
g.setStops(list(zip(pos, color)))
|
||||
|
||||
#if self.colorMode == 'rgb':
|
||||
#ticks = self.listTicks()
|
||||
@ -225,7 +230,7 @@ class ColorMap(object):
|
||||
x = np.linspace(start, stop, nPts)
|
||||
table = self.map(x, mode)
|
||||
|
||||
if not alpha:
|
||||
if not alpha and mode != self.QCOLOR:
|
||||
return table[:,:3]
|
||||
else:
|
||||
return table
|
||||
|
@ -2,7 +2,7 @@
|
||||
"""
|
||||
configfile.py - Human-readable text configuration file library
|
||||
Copyright 2010 Luke Campagnola
|
||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||
Distributed under MIT/X11 license. See license.txt for more information.
|
||||
|
||||
Used for reading and writing dictionary objects to a python-like configuration
|
||||
file format. Data structures may be nested and contain any data type as long
|
||||
@ -33,16 +33,15 @@ class ParseError(Exception):
|
||||
msg = "Error parsing string at line %d:\n" % self.lineNum
|
||||
else:
|
||||
msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum)
|
||||
msg += "%s\n%s" % (self.line, self.message)
|
||||
msg += "%s\n%s" % (self.line, Exception.__str__(self))
|
||||
return msg
|
||||
#raise Exception()
|
||||
|
||||
|
||||
def writeConfigFile(data, fname):
|
||||
s = genString(data)
|
||||
fd = open(fname, 'w')
|
||||
fd.write(s)
|
||||
fd.close()
|
||||
with open(fname, 'w') as fd:
|
||||
fd.write(s)
|
||||
|
||||
|
||||
def readConfigFile(fname):
|
||||
#cwd = os.getcwd()
|
||||
@ -56,9 +55,8 @@ def readConfigFile(fname):
|
||||
|
||||
try:
|
||||
#os.chdir(newDir) ## bad.
|
||||
fd = open(fname)
|
||||
s = asUnicode(fd.read())
|
||||
fd.close()
|
||||
with open(fname) as fd:
|
||||
s = asUnicode(fd.read())
|
||||
s = s.replace("\r\n", "\n")
|
||||
s = s.replace("\r", "\n")
|
||||
data = parseString(s)[1]
|
||||
@ -74,9 +72,8 @@ def readConfigFile(fname):
|
||||
|
||||
def appendConfigFile(data, fname):
|
||||
s = genString(data)
|
||||
fd = open(fname, 'a')
|
||||
fd.write(s)
|
||||
fd.close()
|
||||
with open(fname, 'a') as fd:
|
||||
fd.write(s)
|
||||
|
||||
|
||||
def genString(data, indent=''):
|
||||
@ -93,13 +90,14 @@ def genString(data, indent=''):
|
||||
s += indent + sk + ':\n'
|
||||
s += genString(data[k], indent + ' ')
|
||||
else:
|
||||
s += indent + sk + ': ' + repr(data[k]) + '\n'
|
||||
s += indent + sk + ': ' + repr(data[k]).replace("\n", "\\\n") + '\n'
|
||||
return s
|
||||
|
||||
def parseString(lines, start=0):
|
||||
|
||||
data = OrderedDict()
|
||||
if isinstance(lines, basestring):
|
||||
lines = lines.replace("\\\n", "")
|
||||
lines = lines.split('\n')
|
||||
lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines
|
||||
|
||||
@ -194,8 +192,6 @@ def measureIndent(s):
|
||||
|
||||
if __name__ == '__main__':
|
||||
import tempfile
|
||||
fn = tempfile.mktemp()
|
||||
tf = open(fn, 'w')
|
||||
cf = """
|
||||
key: 'value'
|
||||
key2: ##comment
|
||||
@ -205,8 +201,9 @@ key2: ##comment
|
||||
key22: [1,2,3]
|
||||
key23: 234 #comment
|
||||
"""
|
||||
tf.write(cf)
|
||||
tf.close()
|
||||
fn = tempfile.mktemp()
|
||||
with open(fn, 'w') as tf:
|
||||
tf.write(cf)
|
||||
print("=== Test:===")
|
||||
num = 1
|
||||
for line in cf.split('\n'):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -89,28 +89,16 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
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):
|
||||
"""
|
||||
Hide the title bar for this Dock.
|
||||
@ -150,7 +138,6 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
By default ('auto'), the orientation is determined
|
||||
based on the aspect ratio of the Dock.
|
||||
"""
|
||||
#print self.name(), "setOrientation", o, force
|
||||
if o == 'auto' and self.autoOrient:
|
||||
if self.container().type() == 'tab':
|
||||
o = 'horizontal'
|
||||
@ -165,19 +152,16 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
|
||||
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,7 +187,6 @@ 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()
|
||||
@ -220,7 +203,6 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
if self._container is not None:
|
||||
# ask old container to close itself if it is no longer needed
|
||||
self._container.apoptose()
|
||||
#print self.name(), "container changed"
|
||||
self._container = c
|
||||
if c is None:
|
||||
self.area = None
|
||||
@ -241,6 +223,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
||||
def close(self):
|
||||
"""Remove this dock from the DockArea it lives inside."""
|
||||
self.setParent(None)
|
||||
QtGui.QLabel.close(self.label)
|
||||
self.label.setParent(None)
|
||||
self._container.apoptose()
|
||||
self._container = None
|
||||
@ -346,9 +329,9 @@ class DockLabel(VerticalLabel):
|
||||
ev.accept()
|
||||
|
||||
def mouseReleaseEvent(self, ev):
|
||||
ev.accept()
|
||||
if not self.startedDrag:
|
||||
self.sigClicked.emit(self, ev)
|
||||
ev.accept()
|
||||
|
||||
def mouseDoubleClickEvent(self, ev):
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 = []
|
||||
|
||||
@ -56,27 +57,28 @@ class CSVExporter(Exporter):
|
||||
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)
|
||||
fd.write('\n')
|
||||
|
||||
# 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()
|
||||
|
||||
CSVExporter.register()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -45,30 +45,32 @@ 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'])
|
||||
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))
|
||||
|
||||
targetRect = QtCore.QRect(0, 0, w, h)
|
||||
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']
|
||||
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)
|
||||
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()
|
||||
|
||||
|
@ -124,5 +124,4 @@ class MatplotlibWindow(QtGui.QMainWindow):
|
||||
|
||||
def closeEvent(self, ev):
|
||||
MatplotlibExporter.windows.remove(self)
|
||||
|
||||
|
||||
self.deleteLater()
|
||||
|
@ -69,6 +69,13 @@ xmlHeader = """\
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
|
||||
<title>pyqtgraph SVG export</title>
|
||||
<desc>Generated with Qt and pyqtgraph</desc>
|
||||
<style>
|
||||
image {
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
|
||||
def generateSvg(item, options={}):
|
||||
@ -190,12 +197,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}):
|
||||
## this is taken care of in generateSvg instead.
|
||||
#if hasattr(item, 'setExportMode'):
|
||||
#item.setExportMode(False)
|
||||
|
||||
if QT_LIB in ['PySide', 'PySide2']:
|
||||
xmlStr = str(arr)
|
||||
else:
|
||||
xmlStr = bytes(arr).decode('utf-8')
|
||||
doc = xml.parseString(xmlStr.encode('utf-8'))
|
||||
doc = xml.parseString(arr.data())
|
||||
|
||||
try:
|
||||
## Get top-level group for this item
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
SVG export test
|
||||
CSV export test
|
||||
"""
|
||||
from __future__ import division, print_function, absolute_import
|
||||
import pyqtgraph as pg
|
||||
@ -33,8 +33,9 @@ def test_CSVExporter():
|
||||
ex = pg.exporters.CSVExporter(plt.plotItem)
|
||||
ex.export(fileName=tempfilename)
|
||||
|
||||
r = csv.reader(open(tempfilename, 'r'))
|
||||
lines = [line for line in r]
|
||||
with open(tempfilename, 'r') as csv_file:
|
||||
r = csv.reader(csv_file)
|
||||
lines = [line for line in r]
|
||||
header = lines.pop(0)
|
||||
assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002']
|
||||
|
||||
|
71
pyqtgraph/exporters/tests/test_hdf5.py
Normal file
71
pyqtgraph/exporters/tests/test_hdf5.py
Normal 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'])
|
13
pyqtgraph/exporters/tests/test_image.py
Normal file
13
pyqtgraph/exporters/tests/test_image.py
Normal 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()
|
@ -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
|
||||
|
@ -27,6 +27,7 @@ from .. import configfile as configfile
|
||||
from .. import dockarea as dockarea
|
||||
from . import FlowchartGraphicsView
|
||||
from .. import functions as fn
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
@ -502,12 +503,12 @@ class Flowchart(Node):
|
||||
finally:
|
||||
self.blockSignals(False)
|
||||
|
||||
self.sigChartLoaded.emit()
|
||||
self.outputChanged()
|
||||
self.sigChartLoaded.emit()
|
||||
self.sigStateChanged.emit()
|
||||
|
||||
def loadFile(self, fileName=None, startDir=None):
|
||||
"""Load a flowchart (*.fc) file.
|
||||
"""Load a flowchart (``*.fc``) file.
|
||||
"""
|
||||
if fileName is None:
|
||||
if startDir is None:
|
||||
@ -519,7 +520,7 @@ class Flowchart(Node):
|
||||
self.fileDialog.fileSelected.connect(self.loadFile)
|
||||
return
|
||||
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
|
||||
fileName = unicode(fileName)
|
||||
fileName = asUnicode(fileName)
|
||||
state = configfile.readConfigFile(fileName)
|
||||
self.restoreState(state, clear=True)
|
||||
self.viewBox.autoRange()
|
||||
@ -534,11 +535,12 @@ class Flowchart(Node):
|
||||
if startDir is None:
|
||||
startDir = '.'
|
||||
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
self.fileDialog.setDefaultSuffix("fc")
|
||||
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
self.fileDialog.show()
|
||||
self.fileDialog.fileSelected.connect(self.saveFile)
|
||||
return
|
||||
fileName = unicode(fileName)
|
||||
fileName = asUnicode(fileName)
|
||||
configfile.writeConfigFile(self.saveState(), fileName)
|
||||
self.sigFileSaved.emit(fileName)
|
||||
|
||||
@ -662,7 +664,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
#self.setCurrentFile(newFile)
|
||||
|
||||
def fileSaved(self, fileName):
|
||||
self.setCurrentFile(unicode(fileName))
|
||||
self.setCurrentFile(asUnicode(fileName))
|
||||
self.ui.saveBtn.success("Saved.")
|
||||
|
||||
def saveClicked(self):
|
||||
@ -691,7 +693,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
#self.setCurrentFile(newFile)
|
||||
|
||||
def setCurrentFile(self, fileName):
|
||||
self.currentFileName = unicode(fileName)
|
||||
self.currentFileName = asUnicode(fileName)
|
||||
if fileName is None:
|
||||
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
|
||||
else:
|
||||
@ -761,6 +763,9 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
item = self.items[node]
|
||||
self.ui.ctrlList.setCurrentItem(item)
|
||||
|
||||
def clearSelection(self):
|
||||
self.ui.ctrlList.selectionModel().clearSelection()
|
||||
|
||||
|
||||
class FlowchartWidget(dockarea.DockArea):
|
||||
"""Includes the actual graphical flowchart and debugging interface"""
|
||||
@ -832,9 +837,9 @@ class FlowchartWidget(dockarea.DockArea):
|
||||
def buildMenu(self, pos=None):
|
||||
def buildSubMenu(node, rootMenu, subMenus, pos=None):
|
||||
for section, node in node.items():
|
||||
menu = QtGui.QMenu(section)
|
||||
rootMenu.addMenu(menu)
|
||||
if isinstance(node, OrderedDict):
|
||||
menu = QtGui.QMenu(section)
|
||||
rootMenu.addMenu(menu)
|
||||
buildSubMenu(node, menu, subMenus, pos=pos)
|
||||
subMenus.append(menu)
|
||||
else:
|
||||
@ -888,7 +893,10 @@ class FlowchartWidget(dockarea.DockArea):
|
||||
item = items[0]
|
||||
if hasattr(item, 'node') and isinstance(item.node, Node):
|
||||
n = item.node
|
||||
self.ctrl.select(n)
|
||||
if n in self.ctrl.items:
|
||||
self.ctrl.select(n)
|
||||
else:
|
||||
self.ctrl.clearSelection()
|
||||
data = {'outputs': n.outputValues(), 'inputs': n.inputValues()}
|
||||
self.selNameLabel.setText(n.name())
|
||||
if hasattr(n, 'nodeName'):
|
||||
@ -936,4 +944,3 @@ class FlowchartWidget(dockarea.DockArea):
|
||||
|
||||
class FlowchartNode(Node):
|
||||
pass
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
<string>PyQtGraph</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="verticalSpacing">
|
||||
|
@ -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))
|
||||
|
@ -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.."))
|
||||
|
@ -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))
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -373,7 +373,7 @@ class Node(QtCore.QObject):
|
||||
pos = self.graphicsItem().pos()
|
||||
state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()}
|
||||
termsEditable = self._allowAddInput | self._allowAddOutput
|
||||
for term in self._inputs.values() + self._outputs.values():
|
||||
for term in list(self._inputs.values()) + list(self._outputs.values()):
|
||||
termsEditable |= term._renamable | term._removable | term._multiable
|
||||
if termsEditable:
|
||||
state['terminals'] = self.saveTerminals()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,7 +77,8 @@ 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)
|
||||
|
||||
@ -424,6 +426,8 @@ def eq(a, b):
|
||||
3. When comparing arrays, returns False if the array shapes are not the same.
|
||||
4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
|
||||
the == operator would return a boolean array).
|
||||
5. Collections (dict, list, etc.) must have the same type to be considered equal. One
|
||||
consequence is that comparing a dict to an OrderedDict will always return False.
|
||||
"""
|
||||
if a is b:
|
||||
return True
|
||||
@ -440,6 +444,28 @@ def eq(a, b):
|
||||
if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype):
|
||||
return False
|
||||
|
||||
# Recursively handle common containers
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
if type(a) != type(b) or len(a) != len(b):
|
||||
return False
|
||||
if set(a.keys()) != set(b.keys()):
|
||||
return False
|
||||
for k, v in a.items():
|
||||
if not eq(v, b[k]):
|
||||
return False
|
||||
if isinstance(a, OrderedDict) or sys.version_info >= (3, 7):
|
||||
for a_item, b_item in zip(a.items(), b.items()):
|
||||
if not eq(a_item, b_item):
|
||||
return False
|
||||
return True
|
||||
if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
|
||||
if type(a) != type(b) or len(a) != len(b):
|
||||
return False
|
||||
for v1,v2 in zip(a, b):
|
||||
if not eq(v1, v2):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Test for equivalence.
|
||||
# If the test raises a recognized exception, then return Falase
|
||||
try:
|
||||
@ -909,7 +935,9 @@ 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
|
||||
|
||||
@ -1035,7 +1063,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
============== ==================================================================================
|
||||
"""
|
||||
profile = debug.Profiler()
|
||||
|
||||
if data.ndim not in (2, 3):
|
||||
raise TypeError("data must be 2D or 3D")
|
||||
if data.ndim == 3 and data.shape[2] > 4:
|
||||
@ -1057,6 +1084,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
raise Exception('levels argument is required for float input types')
|
||||
if not isinstance(levels, np.ndarray):
|
||||
levels = np.array(levels)
|
||||
levels = levels.astype(np.float)
|
||||
if levels.ndim == 1:
|
||||
if levels.shape[0] != 2:
|
||||
raise Exception('levels argument must have length 2')
|
||||
@ -1073,7 +1101,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
# Decide on maximum scaled value
|
||||
if scale is None:
|
||||
if lut is not None:
|
||||
scale = lut.shape[0] - 1
|
||||
scale = lut.shape[0]
|
||||
else:
|
||||
scale = 255.
|
||||
|
||||
@ -1083,6 +1111,13 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
else:
|
||||
dtype = np.min_scalar_type(lut.shape[0]-1)
|
||||
|
||||
# awkward, but fastest numpy native nan evaluation
|
||||
#
|
||||
nanMask = None
|
||||
if data.dtype.kind == 'f' and np.isnan(data.min()):
|
||||
nanMask = np.isnan(data)
|
||||
if data.ndim > 2:
|
||||
nanMask = np.any(nanMask, axis=-1)
|
||||
# Apply levels if given
|
||||
if levels is not None:
|
||||
if isinstance(levels, np.ndarray) and levels.ndim == 2:
|
||||
@ -1093,7 +1128,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
for i in range(data.shape[-1]):
|
||||
minVal, maxVal = levels[i]
|
||||
if minVal == maxVal:
|
||||
maxVal += 1e-16
|
||||
maxVal = np.nextafter(maxVal, 2*maxVal)
|
||||
rng = maxVal-minVal
|
||||
rng = 1 if rng == 0 else rng
|
||||
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
|
||||
@ -1103,12 +1138,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
minVal, maxVal = levels
|
||||
if minVal != 0 or maxVal != scale:
|
||||
if minVal == maxVal:
|
||||
maxVal += 1e-16
|
||||
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype)
|
||||
|
||||
maxVal = np.nextafter(maxVal, 2*maxVal)
|
||||
rng = maxVal-minVal
|
||||
rng = 1 if rng == 0 else rng
|
||||
data = rescaleData(data, scale/rng, minVal, dtype=dtype)
|
||||
|
||||
profile()
|
||||
|
||||
# apply LUT if given
|
||||
if lut is not None:
|
||||
data = applyLookupTable(data, lut)
|
||||
@ -1152,6 +1187,11 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
||||
else:
|
||||
alpha = True
|
||||
|
||||
# apply nan mask through alpha channel
|
||||
if nanMask is not None:
|
||||
alpha = True
|
||||
imgData[nanMask, 3] = 0
|
||||
|
||||
profile()
|
||||
return imgData, alpha
|
||||
|
||||
@ -1222,30 +1262,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
|
||||
if QT_LIB in ['PySide', 'PySide2']:
|
||||
ch = ctypes.c_char.from_buffer(imgData, 0)
|
||||
|
||||
# Bug in PySide + Python 3 causes refcount for image data to be improperly
|
||||
# incremented, which leads to leaked memory. As a workaround, we manually
|
||||
# reset the reference count after creating the QImage.
|
||||
# See: https://bugreports.qt.io/browse/PYSIDE-140
|
||||
|
||||
# Get initial reference count (PyObject struct has ob_refcnt as first element)
|
||||
rcount = ctypes.c_long.from_address(id(ch)).value
|
||||
img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
if sys.version[0] == '3':
|
||||
# Reset refcount only on python 3. Technically this would have no effect
|
||||
# on python 2, but this is a nasty hack, and checking for version here
|
||||
# helps to mitigate possible unforseen consequences.
|
||||
ctypes.c_long.from_address(id(ch)).value = rcount
|
||||
else:
|
||||
#addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
|
||||
## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was)
|
||||
## So we first attempt the 4.9.6 API, then fall back to 4.9.3
|
||||
#addr = ctypes.c_char.from_buffer(imgData, 0)
|
||||
#try:
|
||||
#img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
#except TypeError:
|
||||
#addr = ctypes.addressof(addr)
|
||||
#img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
try:
|
||||
img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
except:
|
||||
@ -1258,16 +1278,6 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
|
||||
img.data = imgData
|
||||
return img
|
||||
#try:
|
||||
#buf = imgData.data
|
||||
#except AttributeError: ## happens when image data is non-contiguous
|
||||
#buf = imgData.data
|
||||
|
||||
#profiler()
|
||||
#qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
|
||||
#profiler()
|
||||
#qimage.data = imgData
|
||||
#return qimage
|
||||
|
||||
def imageToArray(img, copy=False, transpose=True):
|
||||
"""
|
||||
@ -1380,7 +1390,7 @@ def gaussianFilter(data, sigma):
|
||||
# clip off extra data
|
||||
sl = [slice(None)] * data.ndim
|
||||
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
|
||||
filtered = filtered[sl]
|
||||
filtered = filtered[tuple(sl)]
|
||||
return filtered + baseline
|
||||
|
||||
|
||||
@ -2468,6 +2478,3 @@ class SignalBlock(object):
|
||||
def __exit__(self, *args):
|
||||
if self.reconnect:
|
||||
self.signal.connect(self.slot)
|
||||
|
||||
|
||||
|
||||
|
@ -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):
|
||||
def __init__(self, orientation, pen=None, textPen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args):
|
||||
"""
|
||||
============== ===============================================================
|
||||
**Arguments:**
|
||||
@ -28,6 +30,15 @@ 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
|
||||
the <span> tag which will surround the axis label and units.
|
||||
============== ===============================================================
|
||||
"""
|
||||
|
||||
@ -67,12 +78,11 @@ class AxisItem(GraphicsWidget):
|
||||
self.fixedWidth = None
|
||||
self.fixedHeight = None
|
||||
|
||||
self.labelText = ''
|
||||
self.labelUnits = ''
|
||||
self.labelUnitPrefix=''
|
||||
self.labelStyle = {}
|
||||
self.labelText = text
|
||||
self.labelUnits = units
|
||||
self.labelUnitPrefix = unitPrefix
|
||||
self.labelStyle = args
|
||||
self.logMode = False
|
||||
self.tickFont = None
|
||||
|
||||
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
||||
self._tickSpacing = None # used to override default tickSpacing method
|
||||
@ -80,6 +90,8 @@ class AxisItem(GraphicsWidget):
|
||||
self.autoSIPrefix = True
|
||||
self.autoSIPrefixScale = 1.0
|
||||
|
||||
self.showLabel(False)
|
||||
|
||||
self.setRange(0, 1)
|
||||
|
||||
if pen is None:
|
||||
@ -87,12 +99,15 @@ 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)
|
||||
|
||||
self.showLabel(False)
|
||||
|
||||
self.grid = False
|
||||
#self.setCacheMode(self.DeviceCoordinateCache)
|
||||
|
||||
@ -190,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?
|
||||
@ -241,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.
|
||||
============== =============================================================
|
||||
|
||||
@ -256,11 +275,14 @@ class AxisItem(GraphicsWidget):
|
||||
axis.setLabel('label text', units='V', **labelStyle)
|
||||
|
||||
"""
|
||||
show_label = False
|
||||
if text is not None:
|
||||
self.labelText = text
|
||||
self.showLabel()
|
||||
show_label = True
|
||||
if units is not None:
|
||||
self.labelUnits = units
|
||||
show_label = True
|
||||
if show_label:
|
||||
self.showLabel()
|
||||
if unitPrefix is not None:
|
||||
self.labelUnitPrefix = unitPrefix
|
||||
@ -394,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.
|
||||
@ -433,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()
|
||||
|
||||
@ -466,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']:
|
||||
@ -632,9 +686,8 @@ class AxisItem(GraphicsWidget):
|
||||
maxTickCount = size / minSpacing
|
||||
if dif / intervals[minorIndex] <= maxTickCount:
|
||||
levels.append((intervals[minorIndex], 0))
|
||||
return levels
|
||||
|
||||
|
||||
return levels
|
||||
|
||||
##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
|
||||
### Determine major/minor tick spacings which flank the optimal spacing.
|
||||
@ -761,7 +814,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):
|
||||
"""
|
||||
@ -882,23 +965,27 @@ 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')
|
||||
|
||||
|
||||
if self.style['stopAxisAtTick'][0] is True:
|
||||
stop = max(span[0].y(), min(map(min, tickPositions)))
|
||||
minTickPosition = min(map(min, tickPositions))
|
||||
if axis == 0:
|
||||
stop = max(span[0].y(), minTickPosition)
|
||||
span[0].setY(stop)
|
||||
else:
|
||||
stop = max(span[0].x(), minTickPosition)
|
||||
span[0].setX(stop)
|
||||
if self.style['stopAxisAtTick'][1] is True:
|
||||
stop = min(span[1].y(), max(map(max, tickPositions)))
|
||||
maxTickPosition = max(map(max, tickPositions))
|
||||
if axis == 0:
|
||||
stop = min(span[1].y(), maxTickPosition)
|
||||
span[1].setY(stop)
|
||||
else:
|
||||
stop = min(span[1].x(), maxTickPosition)
|
||||
span[1].setX(stop)
|
||||
axisSpec = (self.pen(), span[0], span[1])
|
||||
|
||||
@ -1030,13 +1117,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):
|
||||
@ -1054,23 +1141,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
Loading…
Reference in New Issue
Block a user