diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..29e546b2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,19 @@ +[run] +source = pyqtgraph +branch = True +[report] +omit = + */python?.?/* + */site-packages/nose/* + *test* + */__pycache__/* + *.pyc +exclude_lines = + pragma: no cover + def __repr__ + if self\.debug + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: +ignore_errors = True diff --git a/.gitignore b/.gitignore index bd9cbb44..78309170 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,109 @@ -__pycache__ -build -*.pyc +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +doc/_build + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +cover/ +.coverage +.cache +nosetests.xml +coverage.xml +.coverage.* + +# Translations +*.mo +*.pot + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +#mac +.DS_Store +*~ + +#vim *.swp + +#pycharm +.idea/* + +#Dolphin browser files +.directory/ +.directory + +#Binary data files +*.volume +*.am +*.tiff +*.tif +*.dat +*.DAT + +#generated documntation files +doc/resource/api/generated/ + +# Enaml +__enamlcache__/ + + +# PyBuilder +target/ + +# sphinx docs +generated/ + MANIFEST deb_build -dist -.idea rtr.cvs + +# pytest parallel +.coverage + +# ctags +.tags* + diff --git a/.travis.yml b/.travis.yml index 80cd5067..c4a67ac3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ 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 @@ -17,25 +17,21 @@ 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=pyqt TEST=standard - - PYTHON=2.7 QT=pyqt TEST=extra + - PYTHON=2.6 QT=pyqt4 TEST=standard + - PYTHON=2.7 QT=pyqt4 TEST=extra - PYTHON=2.7 QT=pyside TEST=standard - - PYTHON=3.2 QT=pyqt TEST=standard - - PYTHON=3.2 QT=pyside TEST=standard + - PYTHON=3.4 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: - - TRAVIS_DIR=`pwd` - - travis_retry sudo apt-get update; -# - if [ "${PYTHON}" != "2.7" ]; then -# wget http://repo.continuum.io/miniconda/Miniconda-2.2.2-Linux-x86_64.sh -O miniconda.sh && -# chmod +x miniconda.sh && -# ./miniconda.sh -b && -# export PATH=/home/$USER/anaconda/bin:$PATH && -# conda update --yes conda && -# travis_retry sudo apt-get -qq -y install libgl1-mesa-dri; -# fi; + - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi + - 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"; @@ -51,60 +47,31 @@ before_install: - echo ${GIT_SOURCE_EXTRA} install: - # Dependencies - - if [ "${PYTHON}" == "2.7" ]; then - travis_retry sudo apt-get -qq -y install python-numpy && - export PIP=pip && - sudo ${PIP} install pytest && - sudo ${PIP} install flake8 && - export PYTEST=py.test; - else - travis_retry sudo apt-get -qq -y install python3-numpy && - curl http://python-distribute.org/distribute_setup.py | sudo python3 && - curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | sudo python3 && - export PIP=pip3.2 && - sudo ${PIP} install pytest && - sudo ${PIP} install flake8 && - export PYTEST=py.test-3.2; - fi; - - # Qt - - if [ "${PYTHON}" == "2.7" ]; then - if [ ${QT} == 'pyqt' ]; then - travis_retry sudo apt-get -qq -y install python-qt4 python-qt4-gl; - else - travis_retry sudo apt-get -qq -y install python-pyside.qtcore python-pyside.qtgui python-pyside.qtsvg python-pyside.qtopengl; - fi; - elif [ "${PYTHON}" == "3.2" ]; then - if [ ${QT} == 'pyqt' ]; then - travis_retry sudo apt-get -qq -y install python3-pyqt4; - elif [ ${QT} == 'pyside' ]; then - travis_retry sudo apt-get -qq -y install python3-pyside; - else - ${PIP} search PyQt5; - ${PIP} install PyQt5; - cat /home/travis/.pip/pip.log; - fi; - else - conda create -n testenv --yes --quiet pip python=$PYTHON && - source activate testenv && - if [ ${QT} == 'pyqt' ]; then - conda install --yes --quiet pyside; - else - conda install --yes --quiet pyside; - fi; - fi; + - 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} - # Install PyOpenGL - - if [ "${PYTHON}" == "2.7" ]; then - echo "Using OpenGL stable version (apt)"; - travis_retry sudo apt-get -qq -y install python-opengl; - else - echo "Using OpenGL stable version (pip)"; - ${PIP} install -q PyOpenGL; - cat /home/travis/.pip/pip.log; + - 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 @@ -114,23 +81,18 @@ install: else python3 --version; fi; - - apt-cache search python3-pyqt - - apt-cache search python3-pyside - - apt-cache search pytest - - apt-cache search python pip - - apt-cache search python qt5 - 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 - - mkdir ~/bin && ln -s `which python${PYTHON}` ~/bin/python - - export PATH=/home/travis/bin:$PATH + + # 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'; @@ -157,12 +119,12 @@ before_script: 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 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 pull origin ${GIT_SOURCE_EXTRA} && git gc --aggressive && MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` && if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then @@ -171,18 +133,21 @@ before_script: SIZE_DIFF=0; fi; fi; - - - cd $TRAVIS_DIR - script: + + - source activate test_env + # Check system info + - python -c "import pyqtgraph as pg; pg.systemInfo()" + # Run unit tests - start_test "unit tests"; - PYTHONPATH=. ${PYTEST} pyqtgraph/; + 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"; @@ -208,23 +173,26 @@ script: check_output "style check"; fi; - - cd $TRAVIS_DIR - # Check install works - start_test "install test"; - sudo python${PYTHON} setup.py --quiet install; + 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 "! sudo python${PYTHON} setup.py --quiet install"; + 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 diff --git a/CHANGELOG b/CHANGELOG index e574e479..388f51b9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,60 @@ -pyqtgraph-0.9.9 [unreleased] +pyqtgraph-0.10.0 + + New Features: + - PyQt5 support + - Options for interpreting image data as either row-major or col-major + - InfiniteLine and LinearRegionItem can have attached labels + - DockArea: + - Dock titles can be changed after creation + - Added Dock.sigClosed + - Added TextItem.setColor() + - FillBetweenItem supports finite-connected curves (those that exclude nan/inf) + + API / behavior changes: + - Improved ImageItem performance for some data types by scaling LUT instead of image + - Change the defaut color kwarg to None in TextItem.setText() to avoid changing + the color every time the text is changed. + - FFT plots skip first sample if x-axis uses log scaling + - Multiprocessing system adds bytes and unicode to the default list of no-proxy data types + - Version number scheme changed to be PEP440-compliant (only affects installations from non- + release git commits) + + Bugfixes: + - Fix for numpy API change that caused casting errors for inplace operations + - Fixed git version string generation on python3 + - Fixed setting default values for out-of-bound points in pg.interpolateArray + - Fixed plot downsampling bug on python 3 + - Fixed invalid slice in ImageItem.getHistogram + - DockArea: + - Fixed adding Docks to DockArea after all Docks have been removed + - Fixed DockArea save/restoreState when area is empty + - Properly remove select box when export dialog is closed using window decorations + - Remove all modifications to python builtins + - Better Python 2.6 compatibility + - Fix SpinBox decimals + - Fixed numerous issues with ImageItem automatic downsampling + - Fixed PlotItem average curves using incorrect stepMode + - Fixed TableWidget eating key events + - Prevent redundant updating of flowchart nodes with multiple inputs + - Ignore wheel events in GraphicsView if mouse interaction is disabled + - Correctly pass calls to QWidget.close() up the inheritance chain + - ColorMap forces color inputs to be sorted + - Fixed memory mapping for RemoteGraphicsView in OSX + - Fixed QPropertyAnimation str/bytes handling + - Fixed __version__ string update when using `setup.py install` with newer setuptools + + Maintenance: + - Image comparison system for unit testing plus tests for several graphics items + - Travis CI and coveralls/codecov support + - Add examples to unit tests + + + +pyqtgraph-0.9.10 + + Fixed installation issues with more recent pip versions. + +pyqtgraph-0.9.9 API / behavior changes: - Dynamic import system abandoned; pg now uses static imports throughout. @@ -33,7 +89,9 @@ pyqtgraph-0.9.9 [unreleased] - Essentially a graphical interface to dict; all items have text and value - Assigns previously-selected text after list is cleared and repopulated - Get, set current value - - Added Flowchart.sigChartChanged + - Flowchart updates + - Added Flowchart.sigChartChanged + - Custom nodes may now be registered in sub-menu trees - ImageItem.getHistogram is more clever about constructing histograms - Added FillBetweenItem.setCurves() - MultiPlotWidget now has setMinimumPlotHeight method and displays scroll bar @@ -95,6 +153,8 @@ pyqtgraph-0.9.9 [unreleased] - Removed a few cyclic references - Fixed Parameter 'readonly' option for bool, color, and text parameter types - Fixed alpha on GLScatterPlotItem spots (formerly maxed out at alpha=200) + - Fixed a few bugs causing exit crashes + pyqtgraph-0.9.8 2013-11-24 diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 0b4b1beb..5df9703f 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -3,12 +3,10 @@ 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. If this is inconvenient, don't hesitate to - submit by other means. + against the "develop" branch. * Pull requests should include only a focused and related set of changes. - Mixed features and unrelated changes (such as .gitignore) will usually be - rejected. + 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. @@ -49,3 +47,12 @@ Please use the following guidelines when preparing changes: 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. + diff --git a/MANIFEST.in b/MANIFEST.in index c6667d04..9b3331b3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ -recursive-include pyqtgraph *.py *.ui *.m README *.txt -recursive-include tests *.py *.ui -recursive-include examples *.py *.ui -recursive-include doc *.rst *.py *.svg *.png *.jpg +recursive-include pyqtgraph *.py *.ui *.m README.* *.txt +recursive-include examples *.py *.ui *.gz *.cfg +recursive-include doc *.rst *.py *.svg *.png recursive-include doc/build/html * recursive-include tools * include doc/Makefile doc/make.bat README.md LICENSE.txt CHANGELOG diff --git a/README.md b/README.md index 990664c0..30268796 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![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) + PyQtGraph ========= @@ -30,13 +33,20 @@ Contributors * Mikhail Terekhov * Pietro Zambelli * Stefan Holzmann + * Nicholas TJ + * John David Reaver + * David Kaplan + * Martin Fitzpatrick + * Daniel Lidstrom + * Eric Dill + * Vincent LeSaux Requirements ------------ - * PyQt 4.7+ or PySide + * PyQt 4.7+, PySide, or PyQt5 * python 2.6, 2.7, or 3.x - * NumPy + * NumPy * For 3D graphics: pyopengl and qt-opengl * Known to run on Windows, Linux, and Mac. diff --git a/doc/listmissing.py b/doc/listmissing.py index 28fcbcf2..6268d81e 100644 --- a/doc/listmissing.py +++ b/doc/listmissing.py @@ -9,6 +9,6 @@ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') for a, b in dirs: rst = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, 'documentation', 'source', a))] py = [os.path.splitext(x)[0].lower() for x in os.listdir(os.path.join(path, b))] - print a + print(a) for x in set(py) - set(rst): - print " ", x + print( " ", x) diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index 9742568a..c4dc64aa 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -6,6 +6,7 @@ Contents: .. toctree:: :maxdepth: 2 + config_options functions graphicsItems/index widgets/index diff --git a/doc/source/conf.py b/doc/source/conf.py index bf35651d..3ec48f75 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -50,9 +50,9 @@ copyright = '2011, Luke Campagnola' # built documents. # # The short X.Y version. -version = '0.9.8' +version = '0.10.0' # The full version, including alpha/beta/rc tags. -release = '0.9.8' +release = '0.10.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/source/config_options.rst b/doc/source/config_options.rst new file mode 100644 index 00000000..61b64499 --- /dev/null +++ b/doc/source/config_options.rst @@ -0,0 +1,41 @@ +.. currentmodule:: pyqtgraph + +.. _apiref_config: + +Global Configuration Options +============================ + +PyQtGraph has several global configuration options that allow you to change its +default behavior. These can be accessed using the :func:`setConfigOptions` and +:func:`getConfigOption` functions: + +================== =================== ================== ================================================================================ +**Option** **Type** **Default** +leftButtonPan bool True If True, dragging the left mouse button over a ViewBox + causes the view to be panned. If False, then dragging + the left mouse button draws a rectangle that the + ViewBox will zoom to. +foreground See :func:`mkColor` 'd' Default foreground color for text, lines, axes, etc. +background See :func:`mkColor` 'k' Default background for :class:`GraphicsView`. +antialias bool False Enabling antialiasing causes lines to be drawn with + smooth edges at the cost of reduced performance. +imageAxisOrder str 'col-major' For 'row-major', image data is expected in the standard row-major + (row, col) order. For 'col-major', image data is expected in + reversed column-major (col, row) order. + The default is 'col-major' for backward compatibility, but this may + change in the future. +editorCommand str or None None Command used to invoke code editor from ConsoleWidget. +exitCleanup bool True Attempt to work around some exit crash bugs in PyQt and PySide. +useWeave bool False Use weave to speed up some operations, if it is available. +weaveDebug bool False Print full error message if weave compile fails. +useOpenGL bool False Enable OpenGL in GraphicsView. This can have unpredictable effects on stability + and performance. +enableExperimental bool False Enable experimental features (the curious can search for this key in the code). +crashWarning bool False If True, print warnings about situations that may result in a crash. +================== =================== ================== ================================================================================ + + +.. autofunction:: pyqtgraph.setConfigOptions + +.. autofunction:: pyqtgraph.getConfigOption + diff --git a/doc/source/exporting.rst b/doc/source/exporting.rst index 137e6584..ccd017d7 100644 --- a/doc/source/exporting.rst +++ b/doc/source/exporting.rst @@ -39,13 +39,14 @@ Exporting from the API To export a file programatically, follow this example:: import pyqtgraph as pg + import pyqtgraph.exporters # generate something to export plt = pg.plot([1,5,2,4,3]) # create an exporter instance, as an argument give it # the item you wish to export - exporter = pg.exporters.ImageExporter.ImageExporter(plt.plotItem) + exporter = pg.exporters.ImageExporter(plt.plotItem) # set export parameters if needed exporter.parameters()['width'] = 100 # (note this also affects height parameter) diff --git a/doc/source/functions.rst b/doc/source/functions.rst index 5d328ad9..8ea67a69 100644 --- a/doc/source/functions.rst +++ b/doc/source/functions.rst @@ -91,6 +91,8 @@ Mesh Generation Functions Miscellaneous Functions ----------------------- +.. autofunction:: pyqtgraph.eq + .. autofunction:: pyqtgraph.arrayToQPath .. autofunction:: pyqtgraph.pseudoScatter diff --git a/doc/source/graphicsItems/make b/doc/source/graphicsItems/make index 2a990405..293db0d6 100644 --- a/doc/source/graphicsItems/make +++ b/doc/source/graphicsItems/make @@ -23,7 +23,7 @@ ViewBox VTickGroup""".split('\n') for f in files: - print f + print(f) fh = open(f.lower()+'.rst', 'w') fh.write( """%s diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst index 043ee6ba..70161173 100644 --- a/doc/source/introduction.rst +++ b/doc/source/introduction.rst @@ -6,9 +6,17 @@ Introduction What is pyqtgraph? ------------------ -PyQtGraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). +PyQtGraph is a graphics and user interface library for Python that provides +functionality commonly required in engineering and science applications. Its +primary goals are 1) to provide fast, interactive graphics for displaying data +(plots, video, etc.) and 2) to provide tools to aid in rapid application +development (for example, property trees such as used in Qt Designer). -PyQtGraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. +PyQtGraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its +high-performance graphics and numpy for heavy number crunching. In particular, +pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics +system on its own; we bring optimized and simplified primitives to this +framework to allow data visualization with minimal effort. It is known to run on Linux, Windows, and OSX @@ -22,10 +30,13 @@ Amongst the core features of pyqtgraph are: * Fast enough for realtime update of video/plot data * Interactive scaling/panning, averaging, FFTs, SVG/PNG export * Widgets for marking/selecting plot regions -* Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data +* Widgets for marking/selecting image region-of-interest and automatically + slicing multi-dimensional image data * Framework for building customized image region-of-interest widgets -* Docking system that replaces/complements Qt's dock system to allow more complex (and more predictable) docking arrangements -* ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications) +* Docking system that replaces/complements Qt's dock system to allow more + complex (and more predictable) docking arrangements +* ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to + the property trees in Qt Designer and many other applications) .. _examples: @@ -33,19 +44,43 @@ Amongst the core features of pyqtgraph are: Examples -------- -PyQtGraph includes an extensive set of examples that can be accessed by running:: - +PyQtGraph includes an extensive set of examples that can be accessed by +running:: + import pyqtgraph.examples pyqtgraph.examples.run() -This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example. +Or by running ``python examples/`` from the source root. + +This will start a launcher with a list of available examples. Select an item +from the list to view its source code and double-click an item to run the +example. + +Note If you have installed pyqtgraph with ``python setup.py develop`` +then the examples are incorrectly exposed as a top-level module. In this case, +use ``import examples; examples.run()``. How does it compare to... ------------------------- -* matplotlib: For plotting, pyqtgraph is not nearly as complete/mature as matplotlib, but runs much faster. Matplotlib is more aimed toward making publication-quality graphics, whereas pyqtgraph is intended for use in data acquisition and analysis applications. Matplotlib is more intuitive for matlab programmers; pyqtgraph is more intuitive for python/qt programmers. Matplotlib (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. +* matplotlib: For plotting, pyqtgraph is not nearly as complete/mature as + matplotlib, but runs much faster. Matplotlib is more aimed toward making + publication-quality graphics, whereas pyqtgraph is intended for use in data + acquisition and analysis applications. Matplotlib is more intuitive for + matlab programmers; pyqtgraph is more intuitive for python/qt programmers. + Matplotlib (to my knowledge) does not include many of pyqtgraph's features + such as image interaction, volumetric rendering, parameter trees, + flowcharts, etc. -* pyqwt5: About as fast as pyqwt5, 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 originally used pyqwt, but decided it was too much trouble to rely on it as a dependency in my projects). Like matplotlib, pyqwt (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. +* pyqwt5: About as fast as pyqwt5, 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 + originally used pyqwt, but decided it was too much trouble to rely on it + as a dependency in my projects). Like matplotlib, pyqwt (to my knowledge) + does not include many of pyqtgraph's features such as image interaction, + volumetric rendering, parameter trees, flowcharts, etc. -(My experience with these libraries is somewhat outdated; please correct me if I am wrong here) +(My experience with these libraries is somewhat outdated; please correct me if +I am wrong here) diff --git a/doc/source/widgets/make b/doc/source/widgets/make index 40d0e126..1c7d379e 100644 --- a/doc/source/widgets/make +++ b/doc/source/widgets/make @@ -17,7 +17,7 @@ TreeWidget VerticalLabel""".split('\n') for f in files: - print f + print(f) fh = open(f.lower()+'.rst', 'w') fh.write( """%s diff --git a/doc/source/widgets/rawimagewidget.rst b/doc/source/widgets/rawimagewidget.rst deleted file mode 100644 index 29fda791..00000000 --- a/doc/source/widgets/rawimagewidget.rst +++ /dev/null @@ -1,8 +0,0 @@ -RawImageWidget -============== - -.. autoclass:: pyqtgraph.RawImageWidget - :members: - - .. automethod:: pyqtgraph.RawImageWidget.__init__ - diff --git a/examples/FillBetweenItem.py b/examples/FillBetweenItem.py index 74dd89bc..fc91ee32 100644 --- a/examples/FillBetweenItem.py +++ b/examples/FillBetweenItem.py @@ -8,6 +8,8 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtGui, QtCore import numpy as np +#FIXME: When running on Qt5, not as perfect as on Qt4 + win = pg.plot() win.setWindowTitle('pyqtgraph example: FillBetweenItem') win.setXRange(-10, 10) diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 86c2564b..b911cec8 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -2,7 +2,7 @@ """ This example demonstrates a very basic use of flowcharts: filter data, displaying both the input and output of the filter. The behavior of -he filter can be reprogrammed by the user. +the filter can be reprogrammed by the user. Basic steps are: - create a flowchart and two plots diff --git a/examples/FlowchartCustomNode.py b/examples/FlowchartCustomNode.py index 54c56622..2b0819ab 100644 --- a/examples/FlowchartCustomNode.py +++ b/examples/FlowchartCustomNode.py @@ -89,11 +89,11 @@ class ImageViewNode(Node): ## CtrlNode is just a convenience class that automatically creates its ## control widget based on a simple data structure. class UnsharpMaskNode(CtrlNode): - """Return the input data passed through pg.gaussianFilter.""" + """Return the input data passed through an unsharp mask.""" nodeName = "UnsharpMask" uiTemplate = [ - ('sigma', 'spin', {'value': 1.0, 'step': 1.0, 'range': [0.0, None]}), - ('strength', 'spin', {'value': 1.0, 'dec': True, 'step': 0.5, 'minStep': 0.01, 'range': [0.0, None]}), + ('sigma', 'spin', {'value': 1.0, 'step': 1.0, 'bounds': [0.0, None]}), + ('strength', 'spin', {'value': 1.0, 'dec': True, 'step': 0.5, 'minStep': 0.01, 'bounds': [0.0, None]}), ] def __init__(self, name): ## Define the input / output terminals available on this node @@ -127,7 +127,10 @@ class UnsharpMaskNode(CtrlNode): ## NodeLibrary: library = fclib.LIBRARY.copy() # start with the default node set library.addNodeType(ImageViewNode, [('Display',)]) -library.addNodeType(UnsharpMaskNode, [('Image',)]) +# Add the unsharp mask node to two locations in the menu to demonstrate +# that we can create arbitrary menu structures +library.addNodeType(UnsharpMaskNode, [('Image',), + ('Submenu_test','submenu2','submenu3')]) fc.setLibrary(library) diff --git a/examples/GradientWidget.py b/examples/GradientWidget.py index ef7d0fa6..fa5253ba 100644 --- a/examples/GradientWidget.py +++ b/examples/GradientWidget.py @@ -16,7 +16,7 @@ app = QtGui.QApplication([]) w = QtGui.QMainWindow() w.show() w.setWindowTitle('pyqtgraph example: GradientWidget') -w.resize(400,400) +w.setGeometry(10, 50, 400, 400) cw = QtGui.QWidget() w.setCentralWidget(cw) diff --git a/examples/ImageView.py b/examples/ImageView.py index 22168409..3412f348 100644 --- a/examples/ImageView.py +++ b/examples/ImageView.py @@ -17,6 +17,9 @@ import numpy as np from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg +# Interpret image data as row-major instead of col-major +pg.setConfigOptions(imageAxisOrder='row-major') + app = QtGui.QApplication([]) ## Create window with ImageView widget @@ -42,12 +45,24 @@ sig[40:] += np.exp(-np.linspace(1,10, 60)) sig[70:] += np.exp(-np.linspace(1,10, 30)) sig = sig[:,np.newaxis,np.newaxis] * 3 -data[:,50:60,50:60] += sig +data[:,50:60,30:40] += sig ## Display the data and assign each frame a time value from 1.0 to 3.0 imv.setImage(data, xvals=np.linspace(1., 3., data.shape[0])) +## Set a custom color map +colors = [ + (0, 0, 0), + (45, 5, 61), + (84, 42, 55), + (150, 87, 60), + (208, 171, 141), + (255, 255, 255) +] +cmap = pg.ColorMap(pos=np.linspace(0.0, 1.0, 6), color=colors) +imv.setColorMap(cmap) + ## Start Qt event loop unless running in interactive mode. if __name__ == '__main__': import sys diff --git a/examples/InfiniteLine.py b/examples/InfiniteLine.py new file mode 100644 index 00000000..50efbd04 --- /dev/null +++ b/examples/InfiniteLine.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" +This example demonstrates some of the plotting items available in pyqtgraph. +""" + +import initExample ## Add path to library (just for examples; you do not need this) +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +win = pg.GraphicsWindow(title="Plotting items examples") +win.resize(1000,600) + +# Enable antialiasing for prettier plots +pg.setConfigOptions(antialias=True) + +# Create a plot with some random data +p1 = win.addPlot(title="Plot Items example", y=np.random.normal(size=100, scale=10), pen=0.5) +p1.setYRange(-40, 40) + +# Add three infinite lines with labels +inf1 = pg.InfiniteLine(movable=True, angle=90, label='x={value:0.2f}', + labelOpts={'position':0.1, 'color': (200,200,100), 'fill': (200,200,200,50), 'movable': True}) +inf2 = pg.InfiniteLine(movable=True, angle=0, pen=(0, 0, 200), bounds = [-20, 20], hoverPen=(0,200,0), label='y={value:0.2f}mm', + labelOpts={'color': (200,0,0), 'movable': True, 'fill': (0, 0, 200, 100)}) +inf3 = pg.InfiniteLine(movable=True, angle=45, pen='g', label='diagonal', + labelOpts={'rotateAxis': [1, 0], 'fill': (0, 200, 0, 100), 'movable': True}) +inf1.setPos([2,2]) +p1.addItem(inf1) +p1.addItem(inf2) +p1.addItem(inf3) + +# Add a linear region with a label +lr = pg.LinearRegionItem(values=[70, 80]) +p1.addItem(lr) +label = pg.InfLineLabel(lr.lines[1], "region 1", position=0.95, rotateAxis=(1,0), anchor=(1, 1)) + + +## 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'): + QtGui.QApplication.instance().exec_() diff --git a/examples/PlotWidget.py b/examples/PlotWidget.py index 88236ba0..e52a893d 100644 --- a/examples/PlotWidget.py +++ b/examples/PlotWidget.py @@ -37,7 +37,7 @@ p1.setPen((200,200,100)) ## Add in some extra graphics rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 5e-11)) -rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) +rect.setPen(pg.mkPen(100, 200, 100)) pw.addItem(rect) pw.setLabel('left', 'Value', units='V') diff --git a/examples/Plotting.py b/examples/Plotting.py index 8476eae8..44996ae5 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -28,8 +28,8 @@ p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100)) p2 = win.addPlot(title="Multiple curves") p2.plot(np.random.normal(size=100), pen=(255,0,0), name="Red curve") -p2.plot(np.random.normal(size=110)+5, pen=(0,255,0), name="Blue curve") -p2.plot(np.random.normal(size=120)+10, pen=(0,0,255), name="Green curve") +p2.plot(np.random.normal(size=110)+5, pen=(0,255,0), name="Green curve") +p2.plot(np.random.normal(size=120)+10, pen=(0,0,255), name="Blue curve") p3 = win.addPlot(title="Drawing with points") p3.plot(np.random.normal(size=100), pen=(200,200,200), symbolBrush=(255,0,0), symbolPen='w') diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 55c671ad..a48fa7b5 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -11,6 +11,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np +pg.setConfigOptions(imageAxisOrder='row-major') ## Create image to display arr = np.ones((100, 100), dtype=float) @@ -24,6 +25,11 @@ arr[:, 50] = 10 arr += np.sin(np.linspace(0, 20, 100)).reshape(1, 100) arr += np.random.normal(size=(100,100)) +# add an arrow for asymmetry +arr[10, :50] = 10 +arr[9:12, 44:48] = 10 +arr[8:13, 44:46] = 10 + ## create GUI app = QtGui.QApplication([]) diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 95b938cd..9e67ebe1 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -8,23 +8,15 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np import pyqtgraph as pg +pg.setConfigOptions(imageAxisOrder='row-major') + ## create GUI app = QtGui.QApplication([]) w = pg.GraphicsWindow(size=(800,800), border=True) - v = w.addViewBox(colspan=2) - -#w = QtGui.QMainWindow() -#w.resize(800,800) -#v = pg.GraphicsView() v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) -#v.enableMouse(True) -#v.autoPixelScale = False -#w.setCentralWidget(v) -#s = v.scene() -#v.setRange(QtCore.QRectF(-2, -2, 220, 220)) ## Create image to display @@ -37,6 +29,11 @@ arr[:, 75] = 5 arr[50, :] = 10 arr[:, 50] = 10 +# add an arrow for asymmetry +arr[10, :50] = 10 +arr[9:12, 44:48] = 10 +arr[8:13, 44:46] = 10 + ## Create image items, add to scene and set position im1 = pg.ImageItem(arr) im2 = pg.ImageItem(arr) @@ -44,6 +41,7 @@ v.addItem(im1) v.addItem(im2) im2.moveBy(110, 20) v.setRange(QtCore.QRectF(0, 0, 200, 120)) +im1.scale(0.8, 0.5) im3 = pg.ImageItem() v2 = w.addViewBox(1,0) diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index 4dbe57db..9cbf0c63 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -12,7 +12,7 @@ For testing rapid updates of ScatterPlotItem under various conditions. import initExample -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 import numpy as np import pyqtgraph as pg from pyqtgraph.ptime import time @@ -22,6 +22,8 @@ app = QtGui.QApplication([]) #mw.resize(800,800) if USE_PYSIDE: from ScatterPlotSpeedTestTemplate_pyside import Ui_Form +elif USE_PYQT5: + from ScatterPlotSpeedTestTemplate_pyqt5 import Ui_Form else: from ScatterPlotSpeedTestTemplate_pyqt import Ui_Form diff --git a/examples/ScatterPlotWidget.py b/examples/ScatterPlotWidget.py index 563667bd..33503cab 100644 --- a/examples/ScatterPlotWidget.py +++ b/examples/ScatterPlotWidget.py @@ -1,4 +1,22 @@ # -*- coding: utf-8 -*- +""" +Demonstration of ScatterPlotWidget for exploring structure in tabular data. + +The widget consists of four components: + +1) A list of column names from which the user may select 1 or 2 columns + to plot. If one column is selected, the data for that column will be + plotted in a histogram-like manner by using pg.pseudoScatter(). + If two columns are selected, then the + scatter plot will be generated with x determined by the first column + that was selected and y by the second. +2) A DataFilter that allows the user to select a subset of the data by + specifying multiple selection criteria. +3) A ColorMap that allows the user to determine how points are colored by + specifying multiple criteria. +4) A PlotWidget for displaying the data. + +""" import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg @@ -7,42 +25,42 @@ import numpy as np pg.mkQApp() +# Make up some tabular data with structure +data = np.empty(1000, dtype=[('x_pos', float), ('y_pos', float), + ('count', int), ('amplitude', float), + ('decay', float), ('type', 'S10')]) +strings = ['Type-A', 'Type-B', 'Type-C', 'Type-D', 'Type-E'] +typeInds = np.random.randint(5, size=1000) +data['type'] = np.array(strings)[typeInds] +data['x_pos'] = np.random.normal(size=1000) +data['x_pos'][data['type'] == 'Type-A'] -= 1 +data['x_pos'][data['type'] == 'Type-B'] -= 1 +data['x_pos'][data['type'] == 'Type-C'] += 2 +data['x_pos'][data['type'] == 'Type-D'] += 2 +data['x_pos'][data['type'] == 'Type-E'] += 2 +data['y_pos'] = np.random.normal(size=1000) + data['x_pos']*0.1 +data['y_pos'][data['type'] == 'Type-A'] += 3 +data['y_pos'][data['type'] == 'Type-B'] += 3 +data['amplitude'] = data['x_pos'] * 1.4 + data['y_pos'] + np.random.normal(size=1000, scale=0.4) +data['count'] = (np.random.exponential(size=1000, scale=100) * data['x_pos']).astype(int) +data['decay'] = np.random.normal(size=1000, scale=1e-3) + data['amplitude'] * 1e-4 +data['decay'][data['type'] == 'Type-A'] /= 2 +data['decay'][data['type'] == 'Type-E'] *= 3 + + +# Create ScatterPlotWidget and configure its fields spw = pg.ScatterPlotWidget() -spw.show() - -data = np.array([ - (1, 1, 3, 4, 'x'), - (2, 3, 3, 7, 'y'), - (3, 2, 5, 2, 'z'), - (4, 4, 6, 9, 'z'), - (5, 3, 6, 7, 'x'), - (6, 5, 4, 6, 'x'), - (7, 5, 8, 2, 'z'), - (8, 1, 2, 4, 'x'), - (9, 2, 3, 7, 'z'), - (0, 6, 0, 2, 'z'), - (1, 3, 1, 2, 'z'), - (2, 5, 4, 6, 'y'), - (3, 4, 8, 1, 'y'), - (4, 7, 6, 8, 'z'), - (5, 8, 7, 4, 'y'), - (6, 1, 2, 3, 'y'), - (7, 5, 3, 9, 'z'), - (8, 9, 3, 1, 'x'), - (9, 2, 6, 2, 'z'), - (0, 3, 4, 6, 'x'), - (1, 5, 9, 3, 'y'), - ], dtype=[('col1', float), ('col2', float), ('col3', int), ('col4', int), ('col5', 'S10')]) - spw.setFields([ - ('col1', {'units': 'm'}), - ('col2', {'units': 'm'}), - ('col3', {}), - ('col4', {}), - ('col5', {'mode': 'enum', 'values': ['x', 'y', 'z']}), + ('x_pos', {'units': 'm'}), + ('y_pos', {'units': 'm'}), + ('count', {}), + ('amplitude', {'units': 'V'}), + ('decay', {'units': 's'}), + ('type', {'mode': 'enum', 'values': strings}), ]) spw.setData(data) +spw.show() ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/examples/SpinBox.py b/examples/SpinBox.py index ef20e757..2faf10ee 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -13,18 +13,33 @@ import initExample ## Add path to library (just for examples; you do not need th import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np - +import ast app = QtGui.QApplication([]) spins = [ - ("Floating-point spin box, min=0, no maximum.", pg.SpinBox(value=5.0, bounds=[0, None])), - ("Integer spin box, dec stepping
(1-9, 10-90, 100-900, etc)", pg.SpinBox(value=10, int=True, dec=True, minStep=1, step=1)), - ("Float with SI-prefixed units
(n, u, m, k, M, etc)", pg.SpinBox(value=0.9, suffix='V', siPrefix=True)), - ("Float with SI-prefixed units,
dec step=0.1, minStep=0.1", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.1, minStep=0.1)), - ("Float with SI-prefixed units,
dec step=0.5, minStep=0.01", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), - ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)), + ("Floating-point spin box, min=0, no maximum.", + pg.SpinBox(value=5.0, bounds=[0, None])), + ("Integer spin box, dec stepping
(1-9, 10-90, 100-900, etc), decimals=4", + pg.SpinBox(value=10, int=True, dec=True, minStep=1, step=1, decimals=4)), + ("Float with SI-prefixed units
(n, u, m, k, M, etc)", + pg.SpinBox(value=0.9, suffix='V', siPrefix=True)), + ("Float with SI-prefixed units,
dec step=0.1, minStep=0.1", + pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.1, minStep=0.1)), + ("Float with SI-prefixed units,
dec step=0.5, minStep=0.01", + pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), + ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", + pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)), + ("Float with custom formatting", + pg.SpinBox(value=23.07, format='${value:0.02f}', + regex='\$?(?P(-?\d+(\.\d+)?)|(-?\.\d+))$')), + ("Int with custom formatting", + pg.SpinBox(value=4567, step=1, int=True, bounds=[0,None], format='0x{value:X}', + regex='(0x)?(?P[0-9a-fA-F]+)$', + evalFunc=lambda s: ast.literal_eval('0x'+s))), + ("Integer with bounds=[10, 20] and wrapping", + pg.SpinBox(value=10, bounds=[10, 20], int=False, minStep=1, step=1, wrapping=True)), ] diff --git a/examples/Symbols.py b/examples/Symbols.py new file mode 100755 index 00000000..3dd28e13 --- /dev/null +++ b/examples/Symbols.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +This example shows all the scatter plot symbols available in pyqtgraph. + +These symbols are used to mark point locations for scatter plots and some line +plots, similar to "markers" in matplotlib and vispy. +""" + +import initExample ## Add path to library (just for examples; you do not need this) +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + +app = QtGui.QApplication([]) +win = pg.GraphicsWindow(title="Scatter Plot Symbols") +win.resize(1000,600) + +pg.setConfigOptions(antialias=True) + +plot = win.addPlot(title="Plotting with symbols") +plot.addLegend() +plot.plot([0, 1, 2, 3, 4], pen=(0,0,200), symbolBrush=(0,0,200), symbolPen='w', symbol='o', symbolSize=14, name="symbol='o'") +plot.plot([1, 2, 3, 4, 5], pen=(0,128,0), symbolBrush=(0,128,0), symbolPen='w', symbol='t', symbolSize=14, name="symbol='t'") +plot.plot([2, 3, 4, 5, 6], pen=(19,234,201), symbolBrush=(19,234,201), symbolPen='w', symbol='t1', symbolSize=14, name="symbol='t1'") +plot.plot([3, 4, 5, 6, 7], pen=(195,46,212), symbolBrush=(195,46,212), symbolPen='w', symbol='t2', symbolSize=14, name="symbol='t2'") +plot.plot([4, 5, 6, 7, 8], pen=(250,194,5), symbolBrush=(250,194,5), symbolPen='w', symbol='t3', symbolSize=14, name="symbol='t3'") +plot.plot([5, 6, 7, 8, 9], pen=(54,55,55), symbolBrush=(55,55,55), symbolPen='w', symbol='s', symbolSize=14, name="symbol='s'") +plot.plot([6, 7, 8, 9, 10], pen=(0,114,189), symbolBrush=(0,114,189), symbolPen='w', symbol='p', symbolSize=14, name="symbol='p'") +plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen='w', symbol='h', symbolSize=14, name="symbol='h'") +plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'") +plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'") +plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'") +plot.setXRange(-2, 4) + +## 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'): + QtGui.QApplication.instance().exec_() diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 6fce8a86..e7189bf5 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -10,27 +10,37 @@ is used by the view widget import initExample ## Add path to library (just for examples; you do not need this) -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 import numpy as np import pyqtgraph as pg import pyqtgraph.ptime as ptime if USE_PYSIDE: import VideoTemplate_pyside as VideoTemplate +elif USE_PYQT5: + import VideoTemplate_pyqt5 as VideoTemplate else: import VideoTemplate_pyqt as VideoTemplate #QtGui.QApplication.setGraphicsSystem('raster') app = QtGui.QApplication([]) -#mw = QtGui.QMainWindow() -#mw.resize(800,800) win = QtGui.QMainWindow() win.setWindowTitle('pyqtgraph example: VideoSpeedTest') ui = VideoTemplate.Ui_MainWindow() ui.setupUi(win) win.show() + +try: + from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget +except ImportError: + ui.rawGLRadio.setEnabled(False) + ui.rawGLRadio.setText(ui.rawGLRadio.text() + " (OpenGL not available)") +else: + ui.rawGLImg = RawImageGLWidget() + ui.stack.addWidget(ui.rawGLImg) + ui.maxSpin1.setOpts(value=255, step=1) ui.minSpin1.setOpts(value=0, step=1) @@ -101,6 +111,9 @@ def mkData(): if dtype[0] != 'float': data = np.clip(data, 0, mx) data = data.astype(dt) + data[:, 10, 10:50] = mx + data[:, 9:12, 48] = mx + data[:, 8:13, 47] = mx cache = {dtype: data} # clear to save memory (but keep one to prevent unnecessary regeneration) data = cache[dtype] diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui index 6bde7fe2..7da18327 100644 --- a/examples/VideoTemplate.ui +++ b/examples/VideoTemplate.ui @@ -51,7 +51,7 @@ - 2 + 1 @@ -74,13 +74,6 @@ - - - - - - - @@ -340,12 +333,6 @@ QDoubleSpinBox
pyqtgraph
- - RawImageGLWidget - QWidget -
pyqtgraph.widgets.RawImageWidget
- 1 -
diff --git a/examples/VideoTemplate_pyqt.py b/examples/VideoTemplate_pyqt.py index e2481df7..b93bedeb 100644 --- a/examples/VideoTemplate_pyqt.py +++ b/examples/VideoTemplate_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './examples/VideoTemplate.ui' +# Form implementation generated from reading ui file 'examples/VideoTemplate.ui' # -# Created: Mon Feb 17 20:39:30 2014 -# by: PyQt4 UI code generator 4.10.3 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -69,14 +68,6 @@ class Ui_MainWindow(object): self.rawImg.setObjectName(_fromUtf8("rawImg")) self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) self.stack.addWidget(self.page_2) - self.page_3 = QtGui.QWidget() - self.page_3.setObjectName(_fromUtf8("page_3")) - self.gridLayout_5 = QtGui.QGridLayout(self.page_3) - self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) - self.rawGLImg = RawImageGLWidget(self.page_3) - self.rawGLImg.setObjectName(_fromUtf8("rawGLImg")) - self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) - self.stack.addWidget(self.page_3) self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) self.rawGLRadio = QtGui.QRadioButton(self.centralwidget) self.rawGLRadio.setObjectName(_fromUtf8("rawGLRadio")) @@ -193,7 +184,7 @@ class Ui_MainWindow(object): MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) - self.stack.setCurrentIndex(2) + self.stack.setCurrentIndex(1) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -217,5 +208,5 @@ class Ui_MainWindow(object): self.rgbCheck.setText(_translate("MainWindow", "RGB", None)) self.label_5.setText(_translate("MainWindow", "Image size", None)) -from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget -from pyqtgraph import GradientWidget, SpinBox, GraphicsView +from pyqtgraph import GradientWidget, GraphicsView, SpinBox +from pyqtgraph.widgets.RawImageWidget import RawImageWidget diff --git a/examples/VideoTemplate_pyqt5.py b/examples/VideoTemplate_pyqt5.py new file mode 100644 index 00000000..63153fb5 --- /dev/null +++ b/examples/VideoTemplate_pyqt5.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'examples/VideoTemplate.ui' +# +# Created by: PyQt5 UI code generator 5.5.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(695, 798) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) + self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName("scaleCheck") + self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.rawRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawRadio.setObjectName("rawRadio") + self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) + self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget) + self.gfxRadio.setChecked(True) + self.gfxRadio.setObjectName("gfxRadio") + self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) + self.stack = QtWidgets.QStackedWidget(self.centralwidget) + self.stack.setObjectName("stack") + self.page = QtWidgets.QWidget() + self.page.setObjectName("page") + self.gridLayout_3 = QtWidgets.QGridLayout(self.page) + self.gridLayout_3.setObjectName("gridLayout_3") + self.graphicsView = GraphicsView(self.page) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) + self.stack.addWidget(self.page) + self.page_2 = QtWidgets.QWidget() + self.page_2.setObjectName("page_2") + self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2) + self.gridLayout_4.setObjectName("gridLayout_4") + self.rawImg = RawImageWidget(self.page_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName("rawImg") + self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_2) + self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) + self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawGLRadio.setObjectName("rawGLRadio") + self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName("dtypeCombo") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1) + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1) + self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName("minSpin2") + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName("label_3") + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName("maxSpin2") + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName("minSpin1") + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName("maxSpin1") + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName("minSpin3") + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtWidgets.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName("label_4") + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName("maxSpin3") + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1) + self.lutCheck = QtWidgets.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName("lutCheck") + self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1) + self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName("alphaCheck") + self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName("gradient") + self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1) + self.fpsLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName("fpsLabel") + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName("rgbCheck") + self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.centralwidget) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.framesSpin = QtWidgets.QSpinBox(self.centralwidget) + self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.framesSpin.setProperty("value", 10) + self.framesSpin.setObjectName("framesSpin") + self.horizontalLayout_4.addWidget(self.framesSpin) + self.widthSpin = QtWidgets.QSpinBox(self.centralwidget) + self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus) + self.widthSpin.setMaximum(10000) + self.widthSpin.setProperty("value", 512) + self.widthSpin.setObjectName("widthSpin") + self.horizontalLayout_4.addWidget(self.widthSpin) + self.heightSpin = QtWidgets.QSpinBox(self.centralwidget) + self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.heightSpin.setMaximum(10000) + self.heightSpin.setProperty("value", 512) + self.heightSpin.setObjectName("heightSpin") + self.horizontalLayout_4.addWidget(self.heightSpin) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2) + self.sizeLabel = QtWidgets.QLabel(self.centralwidget) + self.sizeLabel.setText("") + self.sizeLabel.setObjectName("sizeLabel") + self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + self.stack.setCurrentIndex(1) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.downsampleCheck.setText(_translate("MainWindow", "Auto downsample")) + self.scaleCheck.setText(_translate("MainWindow", "Scale Data")) + self.rawRadio.setText(_translate("MainWindow", "RawImageWidget")) + self.gfxRadio.setText(_translate("MainWindow", "GraphicsView + ImageItem")) + self.rawGLRadio.setText(_translate("MainWindow", "RawGLImageWidget")) + self.dtypeCombo.setItemText(0, _translate("MainWindow", "uint8")) + self.dtypeCombo.setItemText(1, _translate("MainWindow", "uint16")) + self.dtypeCombo.setItemText(2, _translate("MainWindow", "float")) + self.label.setText(_translate("MainWindow", "Data type")) + self.rgbLevelsCheck.setText(_translate("MainWindow", "RGB")) + self.label_3.setText(_translate("MainWindow", "<--->")) + self.label_2.setText(_translate("MainWindow", "<--->")) + self.label_4.setText(_translate("MainWindow", "<--->")) + self.lutCheck.setText(_translate("MainWindow", "Use Lookup Table")) + self.alphaCheck.setText(_translate("MainWindow", "alpha")) + self.fpsLabel.setText(_translate("MainWindow", "FPS")) + self.rgbCheck.setText(_translate("MainWindow", "RGB")) + self.label_5.setText(_translate("MainWindow", "Image size")) + +from pyqtgraph import GradientWidget, GraphicsView, SpinBox +from pyqtgraph.widgets.RawImageWidget import RawImageWidget diff --git a/examples/VideoTemplate_pyside.py b/examples/VideoTemplate_pyside.py index faebd546..4af85249 100644 --- a/examples/VideoTemplate_pyside.py +++ b/examples/VideoTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './examples/VideoTemplate.ui' +# Form implementation generated from reading ui file 'examples/VideoTemplate.ui' # -# Created: Mon Feb 17 20:39:30 2014 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Wed Oct 26 09:21:01 2016 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -55,14 +55,6 @@ class Ui_MainWindow(object): self.rawImg.setObjectName("rawImg") self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) self.stack.addWidget(self.page_2) - self.page_3 = QtGui.QWidget() - self.page_3.setObjectName("page_3") - self.gridLayout_5 = QtGui.QGridLayout(self.page_3) - self.gridLayout_5.setObjectName("gridLayout_5") - self.rawGLImg = RawImageGLWidget(self.page_3) - self.rawGLImg.setObjectName("rawGLImg") - self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) - self.stack.addWidget(self.page_3) self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) self.rawGLRadio = QtGui.QRadioButton(self.centralwidget) self.rawGLRadio.setObjectName("rawGLRadio") @@ -179,7 +171,7 @@ class Ui_MainWindow(object): MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) - self.stack.setCurrentIndex(2) + self.stack.setCurrentIndex(1) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -203,5 +195,5 @@ class Ui_MainWindow(object): self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) self.label_5.setText(QtGui.QApplication.translate("MainWindow", "Image size", None, QtGui.QApplication.UnicodeUTF8)) -from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget -from pyqtgraph import GradientWidget, SpinBox, GraphicsView +from pyqtgraph.widgets.RawImageWidget import RawImageWidget +from pyqtgraph import SpinBox, GradientWidget, GraphicsView diff --git a/examples/ViewBox.py b/examples/ViewBox.py index 3a66afe3..2ba2094c 100644 --- a/examples/ViewBox.py +++ b/examples/ViewBox.py @@ -42,7 +42,7 @@ class movableRect(QtGui.QGraphicsRectItem): self.setAcceptHoverEvents(True) def hoverEnterEvent(self, ev): self.savedPen = self.pen() - self.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255))) + self.setPen(pg.mkPen(255, 255, 255)) ev.ignore() def hoverLeaveEvent(self, ev): self.setPen(self.savedPen) @@ -57,7 +57,7 @@ class movableRect(QtGui.QGraphicsRectItem): self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) rect = movableRect(QtCore.QRectF(0, 0, 1, 1)) -rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) +rect.setPen(pg.mkPen(100, 200, 100)) vb.addItem(rect) l.addItem(vb, 0, 1) diff --git a/examples/__main__.py b/examples/__main__.py index cb1b87a1..03c41119 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -1,101 +1,23 @@ -import sys, os, subprocess, time - +import sys, os if __name__ == "__main__" and (__package__ is None or __package__==''): parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, parent_dir) import examples __package__ = "examples" - -from . import initExample -from pyqtgraph.Qt import QtCore, QtGui, USE_PYSIDE import pyqtgraph as pg +import subprocess +from pyqtgraph.python2_3 import basestring +from pyqtgraph.Qt import QtGui, USE_PYSIDE, USE_PYQT5 + + +from .utils import buildFileList, testFile, path, examples if USE_PYSIDE: from .exampleLoaderTemplate_pyside import Ui_Form +elif USE_PYQT5: + from .exampleLoaderTemplate_pyqt5 import Ui_Form else: from .exampleLoaderTemplate_pyqt import Ui_Form - -import os, sys -from pyqtgraph.pgcollections import OrderedDict - -examples = OrderedDict([ - ('Command-line usage', 'CLIexample.py'), - ('Basic Plotting', 'Plotting.py'), - ('ImageView', 'ImageView.py'), - ('ParameterTree', 'parametertree.py'), - ('Crosshair / Mouse interaction', 'crosshair.py'), - ('Data Slicing', 'DataSlicing.py'), - ('Plot Customization', 'customPlot.py'), - ('Image Analysis', 'imageAnalysis.py'), - ('Dock widgets', 'dockarea.py'), - ('Console', 'ConsoleWidget.py'), - ('Histograms', 'histogram.py'), - ('Auto-range', 'PlotAutoRange.py'), - ('Remote Plotting', 'RemoteSpeedTest.py'), - ('Scrolling plots', 'scrollingPlots.py'), - ('HDF5 big data', 'hdf5.py'), - ('Demos', OrderedDict([ - ('Optics', 'optics_demos.py'), - ('Special relativity', 'relativity_demo.py'), - ('Verlet chain', 'verlet_chain_demo.py'), - ])), - ('GraphicsItems', OrderedDict([ - ('Scatter Plot', 'ScatterPlot.py'), - #('PlotItem', 'PlotItem.py'), - ('IsocurveItem', 'isocurve.py'), - ('GraphItem', 'GraphItem.py'), - ('ErrorBarItem', 'ErrorBarItem.py'), - ('FillBetweenItem', 'FillBetweenItem.py'), - ('ImageItem - video', 'ImageItem.py'), - ('ImageItem - draw', 'Draw.py'), - ('Region-of-Interest', 'ROIExamples.py'), - ('GraphicsLayout', 'GraphicsLayout.py'), - ('LegendItem', 'Legend.py'), - ('Text Item', 'text.py'), - ('Linked Views', 'linkedViews.py'), - ('Arrow', 'Arrow.py'), - ('ViewBox', 'ViewBox.py'), - ('Custom Graphics', 'customGraphicsItem.py'), - ])), - ('Benchmarks', OrderedDict([ - ('Video speed test', 'VideoSpeedTest.py'), - ('Line Plot update', 'PlotSpeedTest.py'), - ('Scatter Plot update', 'ScatterPlotSpeedTest.py'), - ('Multiple plots', 'MultiPlotSpeedTest.py'), - ])), - ('3D Graphics', OrderedDict([ - ('Volumetric', 'GLVolumeItem.py'), - ('Isosurface', 'GLIsosurface.py'), - ('Surface Plot', 'GLSurfacePlot.py'), - ('Scatter Plot', 'GLScatterPlotItem.py'), - ('Shaders', 'GLshaders.py'), - ('Line Plot', 'GLLinePlotItem.py'), - ('Mesh', 'GLMeshItem.py'), - ('Image', 'GLImageItem.py'), - ])), - ('Widgets', OrderedDict([ - ('PlotWidget', 'PlotWidget.py'), - ('SpinBox', 'SpinBox.py'), - ('ConsoleWidget', 'ConsoleWidget.py'), - ('Histogram / lookup table', 'HistogramLUT.py'), - ('TreeWidget', 'TreeWidget.py'), - ('DataTreeWidget', 'DataTreeWidget.py'), - ('GradientWidget', 'GradientWidget.py'), - ('TableWidget', 'TableWidget.py'), - ('ColorButton', 'ColorButton.py'), - #('CheckTable', '../widgets/CheckTable.py'), - #('VerticalLabel', '../widgets/VerticalLabel.py'), - ('JoystickButton', 'JoystickButton.py'), - ])), - - #('GraphicsScene', 'GraphicsScene.py'), - ('Flowcharts', 'Flowchart.py'), - ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), - #('Canvas', '../canvas'), - #('MultiPlotWidget', 'MultiPlotWidget.py'), -]) - -path = os.path.abspath(os.path.dirname(__file__)) class ExampleLoader(QtGui.QMainWindow): def __init__(self): @@ -104,39 +26,28 @@ class ExampleLoader(QtGui.QMainWindow): self.cw = QtGui.QWidget() self.setCentralWidget(self.cw) self.ui.setupUi(self.cw) - + self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) 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() - + global examples self.itemCache = [] self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) self.ui.exampleTree.expandAll() - + self.resize(1000,500) self.show() self.ui.splitter.setSizes([250,750]) self.ui.loadBtn.clicked.connect(self.loadFile) self.ui.exampleTree.currentItemChanged.connect(self.showFile) self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) - self.ui.pyqtCheck.toggled.connect(self.pyqtToggled) - self.ui.pysideCheck.toggled.connect(self.pysideToggled) self.ui.codeView.textChanged.connect(self.codeEdited) self.codeBtn.clicked.connect(self.runEditedCode) - def pyqtToggled(self, b): - if b: - self.ui.pysideCheck.setChecked(False) - - def pysideToggled(self, b): - if b: - self.ui.pyqtCheck.setChecked(False) - - def populateTree(self, root, examples): for key, val in examples.items(): item = QtGui.QTreeWidgetItem([key]) @@ -148,32 +59,25 @@ class ExampleLoader(QtGui.QMainWindow): else: self.populateTree(item, val) root.addChild(item) - - + def currentFile(self): item = self.ui.exampleTree.currentItem() if hasattr(item, 'file'): global path return os.path.join(path, item.file) return None - - def loadFile(self, edited=False): - - extra = [] - if self.ui.pyqtCheck.isChecked(): - extra.append('pyqt') - elif self.ui.pysideCheck.isChecked(): - extra.append('pyside') - - if self.ui.forceGraphicsCheck.isChecked(): - extra.append(str(self.ui.forceGraphicsCombo.currentText())) - - #if sys.platform.startswith('win'): - #os.spawnl(os.P_NOWAIT, sys.executable, '"'+sys.executable+'"', '"' + fn + '"', *extra) - #else: - #os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra) - + def loadFile(self, edited=False): + + extra = [] + qtLib = str(self.ui.qtLibCombo.currentText()) + gfxSys = str(self.ui.graphicsSystemCombo.currentText()) + + if qtLib != 'default': + extra.append(qtLib.lower()) + elif gfxSys != 'default': + extra.append(gfxSys) + if edited: path = os.path.abspath(os.path.dirname(__file__)) proc = subprocess.Popen([sys.executable, '-'] + extra, stdin=subprocess.PIPE, cwd=path) @@ -188,7 +92,7 @@ class ExampleLoader(QtGui.QMainWindow): os.spawnl(os.P_NOWAIT, sys.executable, '"'+sys.executable+'"', '"' + fn + '"', *extra) else: os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra) - + def showFile(self): fn = self.currentFile() if fn is None: @@ -200,106 +104,34 @@ class ExampleLoader(QtGui.QMainWindow): self.ui.codeView.setPlainText(text) self.ui.loadedFileLabel.setText(fn) self.codeBtn.hide() - + def codeEdited(self): self.codeBtn.show() - + def runEditedCode(self): self.loadFile(edited=True) def run(): app = QtGui.QApplication([]) loader = ExampleLoader() - + app.exec_() -def buildFileList(examples, files=None): - if files == None: - files = [] - for key, val in examples.items(): - #item = QtGui.QTreeWidgetItem([key]) - if isinstance(val, basestring): - #item.file = val - files.append((key,val)) - 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: - c = process.stdout.read(1).decode() - 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') - - - if __name__ == '__main__': - if '--test' in sys.argv[1:]: + + args = sys.argv[1:] + + if '--test' in args: # get rid of orphaned cache files first pg.renamePyc(path) - + files = buildFileList(examples) - if '--pyside' in sys.argv[1:]: + if '--pyside' in args: lib = 'PySide' - elif '--pyqt' in sys.argv[1:]: + elif '--pyqt' in args or '--pyqt4' in args: lib = 'PyQt4' + elif '--pyqt5' in args: + lib = 'PyQt5' else: lib = '' diff --git a/examples/exampleLoaderTemplate.ui b/examples/exampleLoaderTemplate.ui index 2da57800..a1d6bc19 100644 --- a/examples/exampleLoaderTemplate.ui +++ b/examples/exampleLoaderTemplate.ui @@ -6,28 +6,22 @@ 0 0 - 623 - 380 + 846 + 552 Form - - - 0 - - - 0 - + Qt::Horizontal - - + + false @@ -39,55 +33,69 @@ - - + + - - - Force PyQt - - + + default + - - - Force PySide - - + + native + - + + + raster + + + + + opengl + + + - - + + - - - Force Graphics System: - - + + default + - - - - native - - - - - raster - - - - - opengl - - - + + PyQt4 + - + + + PySide + + + + + PyQt5 + + + - + + + + Graphics System: + + + + + + + Qt Library: + + + + Run Example @@ -97,7 +105,7 @@ - + diff --git a/examples/exampleLoaderTemplate_pyqt.py b/examples/exampleLoaderTemplate_pyqt.py index 836640c6..708839f5 100644 --- a/examples/exampleLoaderTemplate_pyqt.py +++ b/examples/exampleLoaderTemplate_pyqt.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './exampleLoaderTemplate.ui' +# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' # -# Created: Mon Feb 25 09:02:09 2013 -# by: PyQt4 UI code generator 4.9.3 +# Created: Sat Feb 28 10:30:29 2015 +# by: PyQt4 UI code generator 4.10.4 # # WARNING! All changes made in this file will be lost! @@ -12,58 +12,64 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(623, 380) - self.gridLayout = QtGui.QGridLayout(Form) - self.gridLayout.setMargin(0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + Form.resize(846, 552) + self.gridLayout_2 = QtGui.QGridLayout(Form) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName(_fromUtf8("splitter")) self.widget = QtGui.QWidget(self.splitter) self.widget.setObjectName(_fromUtf8("widget")) - self.verticalLayout = QtGui.QVBoxLayout(self.widget) - self.verticalLayout.setMargin(0) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.gridLayout = QtGui.QGridLayout(self.widget) + self.gridLayout.setMargin(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.exampleTree = QtGui.QTreeWidget(self.widget) self.exampleTree.setObjectName(_fromUtf8("exampleTree")) self.exampleTree.headerItem().setText(0, _fromUtf8("1")) self.exampleTree.header().setVisible(False) - self.verticalLayout.addWidget(self.exampleTree) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.pyqtCheck = QtGui.QCheckBox(self.widget) - self.pyqtCheck.setObjectName(_fromUtf8("pyqtCheck")) - self.horizontalLayout.addWidget(self.pyqtCheck) - self.pysideCheck = QtGui.QCheckBox(self.widget) - self.pysideCheck.setObjectName(_fromUtf8("pysideCheck")) - self.horizontalLayout.addWidget(self.pysideCheck) - self.verticalLayout.addLayout(self.horizontalLayout) - self.horizontalLayout_2 = QtGui.QHBoxLayout() - self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) - self.forceGraphicsCheck = QtGui.QCheckBox(self.widget) - self.forceGraphicsCheck.setObjectName(_fromUtf8("forceGraphicsCheck")) - self.horizontalLayout_2.addWidget(self.forceGraphicsCheck) - self.forceGraphicsCombo = QtGui.QComboBox(self.widget) - self.forceGraphicsCombo.setObjectName(_fromUtf8("forceGraphicsCombo")) - self.forceGraphicsCombo.addItem(_fromUtf8("")) - self.forceGraphicsCombo.addItem(_fromUtf8("")) - self.forceGraphicsCombo.addItem(_fromUtf8("")) - self.horizontalLayout_2.addWidget(self.forceGraphicsCombo) - self.verticalLayout.addLayout(self.horizontalLayout_2) + self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) + self.graphicsSystemCombo = QtGui.QComboBox(self.widget) + self.graphicsSystemCombo.setObjectName(_fromUtf8("graphicsSystemCombo")) + self.graphicsSystemCombo.addItem(_fromUtf8("")) + self.graphicsSystemCombo.addItem(_fromUtf8("")) + self.graphicsSystemCombo.addItem(_fromUtf8("")) + self.graphicsSystemCombo.addItem(_fromUtf8("")) + self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) + self.qtLibCombo = QtGui.QComboBox(self.widget) + self.qtLibCombo.setObjectName(_fromUtf8("qtLibCombo")) + self.qtLibCombo.addItem(_fromUtf8("")) + self.qtLibCombo.addItem(_fromUtf8("")) + self.qtLibCombo.addItem(_fromUtf8("")) + self.qtLibCombo.addItem(_fromUtf8("")) + self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) + self.label_2 = QtGui.QLabel(self.widget) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.label = QtGui.QLabel(self.widget) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) self.loadBtn = QtGui.QPushButton(self.widget) self.loadBtn.setObjectName(_fromUtf8("loadBtn")) - self.verticalLayout.addWidget(self.loadBtn) + self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) self.widget1 = QtGui.QWidget(self.splitter) self.widget1.setObjectName(_fromUtf8("widget1")) - self.verticalLayout_2 = QtGui.QVBoxLayout(self.widget1) - self.verticalLayout_2.setMargin(0) - self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2")) + self.verticalLayout = QtGui.QVBoxLayout(self.widget1) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.loadedFileLabel = QtGui.QLabel(self.widget1) font = QtGui.QFont() font.setBold(True) @@ -72,25 +78,29 @@ class Ui_Form(object): self.loadedFileLabel.setText(_fromUtf8("")) self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) self.loadedFileLabel.setObjectName(_fromUtf8("loadedFileLabel")) - self.verticalLayout_2.addWidget(self.loadedFileLabel) + self.verticalLayout.addWidget(self.loadedFileLabel) self.codeView = QtGui.QPlainTextEdit(self.widget1) font = QtGui.QFont() font.setFamily(_fromUtf8("FreeMono")) self.codeView.setFont(font) self.codeView.setObjectName(_fromUtf8("codeView")) - self.verticalLayout_2.addWidget(self.codeView) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.verticalLayout.addWidget(self.codeView) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8)) - self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCheck.setText(QtGui.QApplication.translate("Form", "Force Graphics System:", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCombo.setItemText(0, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCombo.setItemText(1, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCombo.setItemText(2, QtGui.QApplication.translate("Form", "opengl", None, QtGui.QApplication.UnicodeUTF8)) - self.loadBtn.setText(QtGui.QApplication.translate("Form", "Run Example", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", 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)) + self.graphicsSystemCombo.setItemText(3, _translate("Form", "opengl", None)) + self.qtLibCombo.setItemText(0, _translate("Form", "default", None)) + self.qtLibCombo.setItemText(1, _translate("Form", "PyQt4", None)) + self.qtLibCombo.setItemText(2, _translate("Form", "PySide", None)) + self.qtLibCombo.setItemText(3, _translate("Form", "PyQt5", None)) + self.label_2.setText(_translate("Form", "Graphics System:", None)) + self.label.setText(_translate("Form", "Qt Library:", None)) + self.loadBtn.setText(_translate("Form", "Run Example", None)) diff --git a/examples/exampleLoaderTemplate_pyqt5.py b/examples/exampleLoaderTemplate_pyqt5.py new file mode 100644 index 00000000..29c00325 --- /dev/null +++ b/examples/exampleLoaderTemplate_pyqt5.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# +# Created: Sat Feb 28 10:28:50 2015 +# by: PyQt5 UI code generator 5.2.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(846, 552) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setObjectName("gridLayout_2") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.widget = QtWidgets.QWidget(self.splitter) + self.widget.setObjectName("widget") + self.gridLayout = QtWidgets.QGridLayout(self.widget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.exampleTree = QtWidgets.QTreeWidget(self.widget) + self.exampleTree.setObjectName("exampleTree") + self.exampleTree.headerItem().setText(0, "1") + self.exampleTree.header().setVisible(False) + self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) + self.graphicsSystemCombo = QtWidgets.QComboBox(self.widget) + self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) + self.qtLibCombo = QtWidgets.QComboBox(self.widget) + self.qtLibCombo.setObjectName("qtLibCombo") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(self.widget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.label = QtWidgets.QLabel(self.widget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.loadBtn = QtWidgets.QPushButton(self.widget) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) + self.widget1 = QtWidgets.QWidget(self.splitter) + self.widget1.setObjectName("widget1") + self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.loadedFileLabel = QtWidgets.QLabel(self.widget1) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.loadedFileLabel.setFont(font) + self.loadedFileLabel.setText("") + self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) + self.loadedFileLabel.setObjectName("loadedFileLabel") + self.verticalLayout.addWidget(self.loadedFileLabel) + self.codeView = QtWidgets.QPlainTextEdit(self.widget1) + font = QtGui.QFont() + font.setFamily("FreeMono") + self.codeView.setFont(font) + self.codeView.setObjectName("codeView") + self.verticalLayout.addWidget(self.codeView) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.graphicsSystemCombo.setItemText(0, _translate("Form", "default")) + self.graphicsSystemCombo.setItemText(1, _translate("Form", "native")) + self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster")) + self.graphicsSystemCombo.setItemText(3, _translate("Form", "opengl")) + self.qtLibCombo.setItemText(0, _translate("Form", "default")) + self.qtLibCombo.setItemText(1, _translate("Form", "PyQt4")) + self.qtLibCombo.setItemText(2, _translate("Form", "PySide")) + self.qtLibCombo.setItemText(3, _translate("Form", "PyQt5")) + self.label_2.setText(_translate("Form", "Graphics System:")) + self.label.setText(_translate("Form", "Qt Library:")) + self.loadBtn.setText(_translate("Form", "Run Example")) + diff --git a/examples/exampleLoaderTemplate_pyside.py b/examples/exampleLoaderTemplate_pyside.py index f596e566..61f1d09f 100644 --- a/examples/exampleLoaderTemplate_pyside.py +++ b/examples/exampleLoaderTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './exampleLoaderTemplate.ui' +# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' # -# Created: Mon Feb 25 09:02:09 2013 -# by: pyside-uic 0.2.13 running on PySide 1.1.1 +# Created: Sat Feb 28 10:31:57 2015 +# by: pyside-uic 0.2.15 running on PySide 1.2.1 # # WARNING! All changes made in this file will be lost! @@ -12,53 +12,50 @@ from PySide import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(623, 380) - self.gridLayout = QtGui.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") + Form.resize(846, 552) + self.gridLayout_2 = QtGui.QGridLayout(Form) + self.gridLayout_2.setObjectName("gridLayout_2") self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.widget = QtGui.QWidget(self.splitter) self.widget.setObjectName("widget") - self.verticalLayout = QtGui.QVBoxLayout(self.widget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") + self.gridLayout = QtGui.QGridLayout(self.widget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") self.exampleTree = QtGui.QTreeWidget(self.widget) self.exampleTree.setObjectName("exampleTree") self.exampleTree.headerItem().setText(0, "1") self.exampleTree.header().setVisible(False) - self.verticalLayout.addWidget(self.exampleTree) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.pyqtCheck = QtGui.QCheckBox(self.widget) - self.pyqtCheck.setObjectName("pyqtCheck") - self.horizontalLayout.addWidget(self.pyqtCheck) - self.pysideCheck = QtGui.QCheckBox(self.widget) - self.pysideCheck.setObjectName("pysideCheck") - self.horizontalLayout.addWidget(self.pysideCheck) - self.verticalLayout.addLayout(self.horizontalLayout) - self.horizontalLayout_2 = QtGui.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.forceGraphicsCheck = QtGui.QCheckBox(self.widget) - self.forceGraphicsCheck.setObjectName("forceGraphicsCheck") - self.horizontalLayout_2.addWidget(self.forceGraphicsCheck) - self.forceGraphicsCombo = QtGui.QComboBox(self.widget) - self.forceGraphicsCombo.setObjectName("forceGraphicsCombo") - self.forceGraphicsCombo.addItem("") - self.forceGraphicsCombo.addItem("") - self.forceGraphicsCombo.addItem("") - self.horizontalLayout_2.addWidget(self.forceGraphicsCombo) - self.verticalLayout.addLayout(self.horizontalLayout_2) + self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) + self.graphicsSystemCombo = QtGui.QComboBox(self.widget) + self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) + self.qtLibCombo = QtGui.QComboBox(self.widget) + self.qtLibCombo.setObjectName("qtLibCombo") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) + self.label_2 = QtGui.QLabel(self.widget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.label = QtGui.QLabel(self.widget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) self.loadBtn = QtGui.QPushButton(self.widget) self.loadBtn.setObjectName("loadBtn") - self.verticalLayout.addWidget(self.loadBtn) + self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) self.widget1 = QtGui.QWidget(self.splitter) self.widget1.setObjectName("widget1") - self.verticalLayout_2 = QtGui.QVBoxLayout(self.widget1) - self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_2.setObjectName("verticalLayout_2") + self.verticalLayout = QtGui.QVBoxLayout(self.widget1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") self.loadedFileLabel = QtGui.QLabel(self.widget1) font = QtGui.QFont() font.setWeight(75) @@ -67,25 +64,29 @@ class Ui_Form(object): self.loadedFileLabel.setText("") self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) self.loadedFileLabel.setObjectName("loadedFileLabel") - self.verticalLayout_2.addWidget(self.loadedFileLabel) + self.verticalLayout.addWidget(self.loadedFileLabel) self.codeView = QtGui.QPlainTextEdit(self.widget1) font = QtGui.QFont() font.setFamily("FreeMono") self.codeView.setFont(font) self.codeView.setObjectName("codeView") - self.verticalLayout_2.addWidget(self.codeView) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.verticalLayout.addWidget(self.codeView) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8)) - self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCheck.setText(QtGui.QApplication.translate("Form", "Force Graphics System:", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCombo.setItemText(0, QtGui.QApplication.translate("Form", "native", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCombo.setItemText(1, QtGui.QApplication.translate("Form", "raster", None, QtGui.QApplication.UnicodeUTF8)) - self.forceGraphicsCombo.setItemText(2, QtGui.QApplication.translate("Form", "opengl", 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)) + self.graphicsSystemCombo.setItemText(3, QtGui.QApplication.translate("Form", "opengl", None, QtGui.QApplication.UnicodeUTF8)) + self.qtLibCombo.setItemText(0, QtGui.QApplication.translate("Form", "default", None, QtGui.QApplication.UnicodeUTF8)) + self.qtLibCombo.setItemText(1, QtGui.QApplication.translate("Form", "PyQt4", None, QtGui.QApplication.UnicodeUTF8)) + self.qtLibCombo.setItemText(2, QtGui.QApplication.translate("Form", "PySide", None, QtGui.QApplication.UnicodeUTF8)) + self.qtLibCombo.setItemText(3, QtGui.QApplication.translate("Form", "PyQt5", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("Form", "Graphics System:", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Qt Library:", None, QtGui.QApplication.UnicodeUTF8)) self.loadBtn.setText(QtGui.QApplication.translate("Form", "Run Example", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/examples/hdf5.py b/examples/hdf5.py index b43ae24a..3cd5de29 100644 --- a/examples/hdf5.py +++ b/examples/hdf5.py @@ -14,11 +14,11 @@ to avoid re-reading the entire visible waveform at every update. import initExample ## Add path to library (just for examples; you do not need this) -import pyqtgraph as pg -from pyqtgraph.Qt import QtCore, QtGui +import sys, os import numpy as np import h5py -import sys, os +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui pg.mkQApp() diff --git a/examples/imageAnalysis.py b/examples/imageAnalysis.py index 8283144e..13adf5ac 100644 --- a/examples/imageAnalysis.py +++ b/examples/imageAnalysis.py @@ -12,8 +12,11 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -pg.mkQApp() +# Interpret image data as row-major instead of col-major +pg.setConfigOptions(imageAxisOrder='row-major') + +pg.mkQApp() win = pg.GraphicsLayoutWidget() win.setWindowTitle('pyqtgraph example: Image Analysis') @@ -57,10 +60,10 @@ win.show() # Generate image data -data = np.random.normal(size=(100, 200)) +data = np.random.normal(size=(200, 100)) data[20:80, 20:80] += 2. data = pg.gaussianFilter(data, (3, 3)) -data += np.random.normal(size=(100, 200)) * 0.1 +data += np.random.normal(size=(200, 100)) * 0.1 img.setImage(data) hist.setLevels(data.min(), data.max()) @@ -79,7 +82,7 @@ p1.autoRange() def updatePlot(): global img, roi, data, p2 selected = roi.getArrayRegion(data, img) - p2.plot(selected.mean(axis=1), clear=True) + p2.plot(selected.mean(axis=0), clear=True) roi.sigRegionChanged.connect(updatePlot) updatePlot() diff --git a/examples/infiniteline_performance.py b/examples/infiniteline_performance.py new file mode 100644 index 00000000..86264142 --- /dev/null +++ b/examples/infiniteline_performance.py @@ -0,0 +1,52 @@ +#!/usr/bin/python + +import initExample ## Add path to library (just for examples; you do not need this) +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg +from pyqtgraph.ptime import time +app = QtGui.QApplication([]) + +p = pg.plot() +p.setWindowTitle('pyqtgraph performance: InfiniteLine') +p.setRange(QtCore.QRectF(0, -10, 5000, 20)) +p.setLabel('bottom', 'Index', units='B') +curve = p.plot() + +# Add a large number of horizontal InfiniteLine to plot +for i in range(100): + line = pg.InfiniteLine(pos=np.random.randint(5000), movable=True) + p.addItem(line) + +data = np.random.normal(size=(50, 5000)) +ptr = 0 +lastTime = time() +fps = None + + +def update(): + global curve, data, ptr, p, lastTime, fps + curve.setData(data[ptr % 10]) + ptr += 1 + now = time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + p.setTitle('%0.2f fps' % fps) + app.processEvents() # force complete redraw for every plot + + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + +# Start Qt event loop unless running in interactive mode. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() \ No newline at end of file diff --git a/examples/initExample.py b/examples/initExample.py index b61b55cc..c10de84e 100644 --- a/examples/initExample.py +++ b/examples/initExample.py @@ -24,15 +24,23 @@ if 'pyside' in sys.argv: from PySide import QtGui elif 'pyqt' in sys.argv: from PyQt4 import QtGui +elif 'pyqt5' in sys.argv: + from PyQt5 import QtGui else: from pyqtgraph.Qt import QtGui + +import pyqtgraph as pg ## Force use of a specific graphics system +use_gs = 'default' for gs in ['raster', 'native', 'opengl']: if gs in sys.argv: + use_gs = gs QtGui.QApplication.setGraphicsSystem(gs) break +print("Using %s (%s graphics system)" % (pg.Qt.QT_LIB, use_gs)) + ## Enable fault handling to give more helpful error messages on crash. ## Only available in python 3.3+ try: diff --git a/examples/multiplePlotSpeedTest.py b/examples/multiplePlotSpeedTest.py index cea59a35..07df7522 100644 --- a/examples/multiplePlotSpeedTest.py +++ b/examples/multiplePlotSpeedTest.py @@ -23,8 +23,8 @@ def plot(): pts = 100 x = np.linspace(0, 0.8, pts) y = np.random.random(size=pts)*0.8 - for i in xrange(n): - for j in xrange(n): + for i in range(n): + for j in range(n): ## calling PlotWidget.plot() generates a PlotDataItem, which ## has a bit more overhead than PlotCurveItem, which is all ## we need here. This overhead adds up quickly and makes a big diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py index dc493568..c2cb2ba2 100644 --- a/examples/optics/pyoptic.py +++ b/examples/optics/pyoptic.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from PyQt4 import QtGui, QtCore import pyqtgraph as pg -#from pyqtgraph.canvas import Canvas, CanvasItem +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import csv, gzip, os from pyqtgraph import Point @@ -90,7 +89,7 @@ def wlPen(wl): return pen -class ParamObj: +class ParamObj(object): # Just a helper for tracking parameters and responding to changes def __init__(self): self.__params = {} @@ -110,7 +109,8 @@ class ParamObj: pass def __getitem__(self, item): - return self.getParam(item) + # bug in pyside 1.2.2 causes getitem to be called inside QGraphicsObject.parentItem: + return self.getParam(item) # PySide bug: https://bugreports.qt.io/browse/PYSIDE-441 def getParam(self, param): return self.__params[param] diff --git a/examples/parallelize.py b/examples/parallelize.py index 768d6f00..b309aa31 100644 --- a/examples/parallelize.py +++ b/examples/parallelize.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- import initExample ## Add path to library (just for examples; you do not need this) + +import time import numpy as np import pyqtgraph.multiprocess as mp import pyqtgraph as pg -import time +from pyqtgraph.python2_3 import xrange print( "\n=================\nParallelize") diff --git a/examples/parametertree.py b/examples/parametertree.py index 6e8e0dbd..8d8a7352 100644 --- a/examples/parametertree.py +++ b/examples/parametertree.py @@ -124,7 +124,7 @@ p.sigTreeStateChanged.connect(change) def valueChanging(param, value): - print("Value changing (not finalized):", param, value) + print("Value changing (not finalized): %s %s" % (param, value)) # Too lazy for recursion: for child in p.children(): diff --git a/examples/relativity/__init__.py b/examples/relativity/__init__.py index 093806ef..f1acf528 100644 --- a/examples/relativity/__init__.py +++ b/examples/relativity/__init__.py @@ -1 +1 @@ -from relativity import * +from .relativity import * diff --git a/examples/relativity/relativity.py b/examples/relativity/relativity.py index 80a56d64..e3f2c435 100644 --- a/examples/relativity/relativity.py +++ b/examples/relativity/relativity.py @@ -1,13 +1,12 @@ +import numpy as np +import collections +import sys, os import pyqtgraph as pg from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.parametertree import Parameter, ParameterTree from pyqtgraph.parametertree import types as pTypes import pyqtgraph.configfile -import numpy as np -import user -import collections -import sys, os - +from pyqtgraph.python2_3 import xrange class RelativityGUI(QtGui.QWidget): @@ -247,7 +246,7 @@ class GridParam(pTypes.GroupParameter): template = self.param('ClockTemplate') spacing = self['Spacing'] for i in range(self['Number of Clocks']): - c = template.buildClocks().values()[0] + c = list(template.buildClocks().values())[0] c.x0 += i * spacing clocks[self.name() + '%02d' % i] = c return clocks @@ -502,7 +501,7 @@ class Simulation: def run(self): nPts = int(self.duration/self.dt)+1 - for cl in self.clocks.itervalues(): + for cl in self.clocks.values(): cl.init(nPts) if self.ref is None: @@ -514,7 +513,7 @@ class Simulation: clocks = self.clocks dt = self.dt tVals = np.linspace(0, dt*(nPts-1), nPts) - for cl in self.clocks.itervalues(): + for cl in self.clocks.values(): for i in xrange(1,nPts): nextT = tVals[i] while True: @@ -549,7 +548,7 @@ class Simulation: ## make sure reference clock is not present in the list of clocks--this will be handled separately. clocks = clocks.copy() - for k,v in clocks.iteritems(): + for k,v in clocks.items(): if v is ref: del clocks[k] break @@ -586,7 +585,7 @@ class Simulation: ## update all other clocks - for cl in clocks.itervalues(): + for cl in clocks.values(): while True: g = cl.acceleration() tau1, tau2 = cl.accelLimits() @@ -635,7 +634,7 @@ class Simulation: def plot(self, plot): plot.clear() - for cl in self.clocks.itervalues(): + for cl in self.clocks.values(): c, p = cl.getCurve() plot.addItem(c) plot.addItem(p) diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py index 623b9ab1..313d4e8d 100644 --- a/examples/scrollingPlots.py +++ b/examples/scrollingPlots.py @@ -21,7 +21,7 @@ curve1 = p1.plot(data1) curve2 = p2.plot(data1) ptr1 = 0 def update1(): - global data1, curve1, ptr1 + global data1, ptr1 data1[:-1] = data1[1:] # shift data in the array one sample left # (see also: np.roll) data1[-1] = np.random.normal() diff --git a/examples/template.py b/examples/template.py index 1198e317..6b5e1f75 100644 --- a/examples/template.py +++ b/examples/template.py @@ -11,6 +11,8 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np +app = QtGui.QApplication([]) + # win.setWindowTitle('pyqtgraph example: ____') ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/examples/test_examples.py b/examples/test_examples.py new file mode 100644 index 00000000..ae88b087 --- /dev/null +++ b/examples/test_examples.py @@ -0,0 +1,65 @@ +from __future__ import print_function, division, absolute_import +from pyqtgraph import Qt +from . import utils +import itertools +import pytest +import os, sys + + +# 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: + if sys.version_info[0] < 3: + import __builtin__ as builtins + else: + import builtins + + def flaky_print(*args): + """Wrapper for print that retries in case of IOError. + """ + count = 0 + while count < 5: + count += 1 + try: + orig_print(*args) + break + except IOError: + if count >= 5: + raise + pass + orig_print = builtins.print + builtins.print = flaky_print + 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} +# sort out which of the front ends are available +for frontend in frontends.keys(): + try: + importlib.import_module(frontend) + frontends[frontend] = True + except ImportError: + pass + + +@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) + +if __name__ == "__main__": + pytest.cmdline.main() diff --git a/examples/text.py b/examples/text.py index 23f527e3..bf9bd6b9 100644 --- a/examples/text.py +++ b/examples/text.py @@ -23,7 +23,7 @@ plot.setWindowTitle('pyqtgraph example: text') curve = plot.plot(x,y) ## add a single curve ## Create text object, use HTML tags to specify color/size -text = pg.TextItem(html='
This is the
PEAK
', anchor=(-0.3,1.3), border='w', fill=(0, 0, 255, 100)) +text = pg.TextItem(html='
This is the
PEAK
', anchor=(-0.3,0.5), angle=45, border='w', fill=(0, 0, 255, 100)) plot.addItem(text) text.setPos(0, y.max()) @@ -46,7 +46,6 @@ def update(): global curvePoint, index index = (index + 1) % len(x) curvePoint.setPos(float(index)/(len(x)-1)) - #text2.viewRangeChanged() text2.setText('[%0.1f, %0.1f]' % (x[index], y[index])) timer = QtCore.QTimer() diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 00000000..cbdf69c6 --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,165 @@ +from __future__ import division, print_function, absolute_import +import subprocess +import time +import os +import sys +from pyqtgraph.pgcollections import OrderedDict +from pyqtgraph.python2_3 import basestring + +path = os.path.abspath(os.path.dirname(__file__)) + + +examples = OrderedDict([ + ('Command-line usage', 'CLIexample.py'), + ('Basic Plotting', 'Plotting.py'), + ('ImageView', 'ImageView.py'), + ('ParameterTree', 'parametertree.py'), + ('Crosshair / Mouse interaction', 'crosshair.py'), + ('Data Slicing', 'DataSlicing.py'), + ('Plot Customization', 'customPlot.py'), + ('Image Analysis', 'imageAnalysis.py'), + ('Dock widgets', 'dockarea.py'), + ('Console', 'ConsoleWidget.py'), + ('Histograms', 'histogram.py'), + ('Beeswarm plot', 'beeswarm.py'), + ('Symbols', 'Symbols.py'), + ('Auto-range', 'PlotAutoRange.py'), + ('Remote Plotting', 'RemoteSpeedTest.py'), + ('Scrolling plots', 'scrollingPlots.py'), + ('HDF5 big data', 'hdf5.py'), + ('Demos', OrderedDict([ + ('Optics', 'optics_demos.py'), + ('Special relativity', 'relativity_demo.py'), + ('Verlet chain', 'verlet_chain_demo.py'), + ])), + ('GraphicsItems', OrderedDict([ + ('Scatter Plot', 'ScatterPlot.py'), + #('PlotItem', 'PlotItem.py'), + ('IsocurveItem', 'isocurve.py'), + ('GraphItem', 'GraphItem.py'), + ('ErrorBarItem', 'ErrorBarItem.py'), + ('FillBetweenItem', 'FillBetweenItem.py'), + ('ImageItem - video', 'ImageItem.py'), + ('ImageItem - draw', 'Draw.py'), + ('Region-of-Interest', 'ROIExamples.py'), + ('Bar Graph', 'BarGraphItem.py'), + ('GraphicsLayout', 'GraphicsLayout.py'), + ('LegendItem', 'Legend.py'), + ('Text Item', 'text.py'), + ('Linked Views', 'linkedViews.py'), + ('Arrow', 'Arrow.py'), + ('ViewBox', 'ViewBox.py'), + ('Custom Graphics', 'customGraphicsItem.py'), + ('Labeled Graph', 'CustomGraphItem.py'), + ])), + ('Benchmarks', OrderedDict([ + ('Video speed test', 'VideoSpeedTest.py'), + ('Line Plot update', 'PlotSpeedTest.py'), + ('Scatter Plot update', 'ScatterPlotSpeedTest.py'), + ('Multiple plots', 'MultiPlotSpeedTest.py'), + ])), + ('3D Graphics', OrderedDict([ + ('Volumetric', 'GLVolumeItem.py'), + ('Isosurface', 'GLIsosurface.py'), + ('Surface Plot', 'GLSurfacePlot.py'), + ('Scatter Plot', 'GLScatterPlotItem.py'), + ('Shaders', 'GLshaders.py'), + ('Line Plot', 'GLLinePlotItem.py'), + ('Mesh', 'GLMeshItem.py'), + ('Image', 'GLImageItem.py'), + ])), + ('Widgets', OrderedDict([ + ('PlotWidget', 'PlotWidget.py'), + ('SpinBox', 'SpinBox.py'), + ('ConsoleWidget', 'ConsoleWidget.py'), + ('Histogram / lookup table', 'HistogramLUT.py'), + ('TreeWidget', 'TreeWidget.py'), + ('ScatterPlotWidget', 'ScatterPlotWidget.py'), + ('DataTreeWidget', 'DataTreeWidget.py'), + ('GradientWidget', 'GradientWidget.py'), + ('TableWidget', 'TableWidget.py'), + ('ColorButton', 'ColorButton.py'), + #('CheckTable', '../widgets/CheckTable.py'), + #('VerticalLabel', '../widgets/VerticalLabel.py'), + ('JoystickButton', 'JoystickButton.py'), + ])), + + ('Flowcharts', 'Flowchart.py'), + ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), +]) + + +def buildFileList(examples, files=None): + if files == None: + files = [] + for key, val in examples.items(): + #item = QtGui.QTreeWidgetItem([key]) + if isinstance(val, basestring): + #item.file = val + files.append((key,val)) + 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: + c = process.stdout.read(1).decode() + 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') diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 896505ac..6eb3501a 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -1,7 +1,7 @@ import pyqtgraph as pg import numpy as np import time -from .relax import relax +from . import relax class ChainSim(pg.QtCore.QObject): @@ -52,7 +52,7 @@ class ChainSim(pg.QtCore.QObject): self.mrel1[self.fixed[l2]] = 0 self.mrel2 = 1.0 - self.mrel1 - for i in range(100): + for i in range(10): self.relax(n=10) self.initialized = True @@ -75,6 +75,10 @@ class ChainSim(pg.QtCore.QObject): else: dt = now - self.lasttime self.lasttime = now + + # limit amount of work to be done between frames + if not relax.COMPILED: + dt = self.maxTimeStep if self.lastpos is None: self.lastpos = self.pos @@ -103,8 +107,9 @@ class ChainSim(pg.QtCore.QObject): def relax(self, n=50): - # speed up with C magic - relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) + # speed up with C magic if possible + relax.relax(self.pos, self.links, self.mrel1, self.mrel2, self.lengths, self.push, self.pull, n) self.relaxed.emit() + diff --git a/examples/verlet_chain/maths.so b/examples/verlet_chain/maths.so deleted file mode 100755 index 62aff321..00000000 Binary files a/examples/verlet_chain/maths.so and /dev/null differ diff --git a/examples/verlet_chain/relax.py b/examples/verlet_chain/relax.py index 95a2b7b3..22c54d62 100644 --- a/examples/verlet_chain/relax.py +++ b/examples/verlet_chain/relax.py @@ -2,22 +2,69 @@ import ctypes import os so = os.path.join(os.path.dirname(__file__), 'maths.so') -lib = ctypes.CDLL(so) +try: + lib = ctypes.CDLL(so) + COMPILED = True +except OSError: + COMPILED = False -lib.relax.argtypes = [ - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_void_p, - ctypes.c_int, - ctypes.c_int, - ] -def relax(pos, links, mrel1, mrel2, lengths, push, pull, iters): - nlinks = links.shape[0] - lib.relax(pos.ctypes, links.ctypes, mrel1.ctypes, mrel2.ctypes, lengths.ctypes, push.ctypes, pull.ctypes, nlinks, iters) - +if COMPILED: + lib.relax.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int, + ] + def relax(pos, links, mrel1, mrel2, lengths, push, pull, iters): + nlinks = links.shape[0] + lib.relax(pos.ctypes, links.ctypes, mrel1.ctypes, mrel2.ctypes, lengths.ctypes, push.ctypes, pull.ctypes, nlinks, iters) + +else: + def relax(pos, links, mrel1, mrel2, lengths, push, pull, iters): + lengths2 = lengths**2 + for i in range(iters): + #p1 = links[:, 0] + #p2 = links[:, 1] + #x1 = pos[p1] + #x2 = pos[p2] + + #dx = x2 - x1 + + #dist = (dx**2).sum(axis=1)**0.5 + + #mask = (npush & (dist < lengths)) | (npull & (dist > lengths)) + ##dist[mask] = lengths[mask] + #change = (lengths-dist) / dist + #change[mask] = 0 + + #dx *= change[:, np.newaxis] + #print dx + + ##pos[p1] -= mrel2 * dx + ##pos[p2] += mrel1 * dx + #for j in range(links.shape[0]): + #pos[links[j,0]] -= mrel2[j] * dx[j] + #pos[links[j,1]] += mrel1[j] * dx[j] + + + for l in range(links.shape[0]): + p1, p2 = links[l]; + x1 = pos[p1] + x2 = pos[p2] + + dx = x2 - x1 + dist2 = (dx**2).sum() + + if (push[l] and dist2 < lengths2[l]) or (pull[l] and dist2 > lengths2[l]): + dist = dist2 ** 0.5 + change = (lengths[l]-dist) / dist + dx *= change + pos[p1] -= mrel2[l] * dx + pos[p2] += mrel1[l] * dx diff --git a/examples/verlet_chain_demo.py b/examples/verlet_chain_demo.py index 6ed97d48..1197344d 100644 --- a/examples/verlet_chain_demo.py +++ b/examples/verlet_chain_demo.py @@ -1,26 +1,38 @@ """ Mechanical simulation of a chain using verlet integration. +Use the mouse to interact with one of the chains. +By default, this uses a slow, pure-python integrator to solve the chain link +positions. Unix users may compile a small math library to speed this up by +running the `examples/verlet_chain/make` script. """ + import initExample ## Add path to library (just for examples; you do not need this) import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -from verlet_chain import ChainSim +import verlet_chain -sim = ChainSim() +sim = verlet_chain.ChainSim() - -chlen1 = 80 -chlen2 = 60 +if verlet_chain.relax.COMPILED: + # Use more complex chain if compiled mad library is available. + chlen1 = 80 + chlen2 = 60 + linklen = 1 +else: + chlen1 = 10 + chlen2 = 8 + linklen = 8 + npts = chlen1 + chlen2 sim.mass = np.ones(npts) -sim.mass[chlen1-15] = 100 +sim.mass[int(chlen1 * 0.8)] = 100 sim.mass[chlen1-1] = 500 sim.mass[npts-1] = 200 @@ -31,8 +43,10 @@ sim.fixed[chlen1] = True sim.pos = np.empty((npts, 2)) sim.pos[:chlen1, 0] = 0 sim.pos[chlen1:, 0] = 10 -sim.pos[:chlen1, 1] = np.arange(chlen1) -sim.pos[chlen1:, 1] = np.arange(chlen2) +sim.pos[:chlen1, 1] = np.arange(chlen1) * linklen +sim.pos[chlen1:, 1] = np.arange(chlen2) * linklen +# to prevent miraculous balancing acts: +sim.pos += np.random.normal(size=sim.pos.shape, scale=1e-3) links1 = [(j, i+j+1) for i in range(chlen1) for j in range(chlen1-i-1)] links2 = [(j, i+j+1) for i in range(chlen2) for j in range(chlen2-i-1)] @@ -55,7 +69,8 @@ sim.push = np.concatenate([push1, push2, np.array([True], dtype=bool)]) sim.pull = np.ones(sim.links.shape[0], dtype=bool) sim.pull[-1] = False -mousepos = sim.pos[0] +# move chain initially just to generate some motion if the mouse is not over the window +mousepos = np.array([30, 20]) def display(): diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index c6afbe0f..952a2415 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,12 +1,13 @@ -from ..Qt import QtCore, QtGui -from ..python2_3 import sortList import weakref +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 from .mouseEvents import * from .. import debug as debug + if hasattr(QtCore, 'PYQT_VERSION'): try: import sip @@ -84,8 +85,8 @@ class GraphicsScene(QtGui.QGraphicsScene): cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj - def __init__(self, clickRadius=2, moveDistance=5): - QtGui.QGraphicsScene.__init__(self) + def __init__(self, clickRadius=2, moveDistance=5, parent=None): + QtGui.QGraphicsScene.__init__(self, parent) self.setClickRadius(clickRadius) self.setMoveDistance(moveDistance) self.exportDirectory = None @@ -97,6 +98,7 @@ class GraphicsScene(QtGui.QGraphicsScene): self.lastDrag = None self.hoverItems = weakref.WeakKeyDictionary() self.lastHoverEvent = None + self.minDragTime = 0.5 # drags shorter than 0.5 sec are interpreted as clicks self.contextMenu = [QtGui.QAction("Export...", self)] self.contextMenu[0].triggered.connect(self.showExportDialog) @@ -133,7 +135,6 @@ class GraphicsScene(QtGui.QGraphicsScene): self._moveDistance = d def mousePressEvent(self, ev): - #print 'scenePress' QtGui.QGraphicsScene.mousePressEvent(self, ev) if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events if self.lastHoverEvent is not None: @@ -171,8 +172,8 @@ class GraphicsScene(QtGui.QGraphicsScene): 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.screenPos() - cev.screenPos()) - if dist.length() < self._moveDistance and now - cev.time() < 0.5: + 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)) @@ -185,10 +186,8 @@ class GraphicsScene(QtGui.QGraphicsScene): def leaveEvent(self, ev): ## inform items that mouse is gone if len(self.dragButtons) == 0: self.sendHoverEvents(ev, exitOnly=True) - def mouseReleaseEvent(self, ev): - #print 'sceneRelease' if self.mouseGrabberItem() is None: if ev.button() in self.dragButtons: if self.sendDragEvent(ev, final=True): @@ -231,8 +230,6 @@ class GraphicsScene(QtGui.QGraphicsScene): prevItems = list(self.hoverItems.keys()) - #print "hover prev items:", prevItems - #print "hover test items:", items for item in items: if hasattr(item, 'hoverEvent'): event.currentItem = item @@ -247,7 +244,7 @@ class GraphicsScene(QtGui.QGraphicsScene): item.hoverEvent(event) except: debug.printExc("Error sending hover event:") - + event.enter = False event.exit = True #print "hover exit items:", prevItems diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 5efb7c44..2676a3b4 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5 from .. import exporters as exporters from .. import functions as fn from ..graphicsItems.ViewBox import ViewBox @@ -6,6 +6,8 @@ from ..graphicsItems.PlotItem import PlotItem if USE_PYSIDE: from . import exportDialogTemplate_pyside as exportDialogTemplate +elif USE_PYQT5: + from . import exportDialogTemplate_pyqt5 as exportDialogTemplate else: from . import exportDialogTemplate_pyqt as exportDialogTemplate @@ -137,5 +139,6 @@ class ExportDialog(QtGui.QWidget): self.selectBox.setVisible(False) self.setVisible(False) - - + def closeEvent(self, event): + self.close() + QtGui.QWidget.closeEvent(self, event) diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py new file mode 100644 index 00000000..418fd0f0 --- /dev/null +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui' +# +# Created: Wed Mar 26 15:09:29 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(241, 367) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtWidgets.QTreeWidget(Form) + self.itemTree.setObjectName("itemTree") + self.itemTree.headerItem().setText(0, "1") + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtWidgets.QLabel(Form) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtWidgets.QListWidget(Form) + self.formatList.setObjectName("formatList") + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtWidgets.QPushButton(Form) + self.exportBtn.setObjectName("exportBtn") + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtWidgets.QPushButton(Form) + self.closeBtn.setObjectName("closeBtn") + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName("paramTree") + self.paramTree.headerItem().setText(0, "1") + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtWidgets.QLabel(Form) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + self.copyBtn = QtWidgets.QPushButton(Form) + self.copyBtn.setObjectName("copyBtn") + self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Export")) + self.label.setText(_translate("Form", "Item to export:")) + self.label_2.setText(_translate("Form", "Export format")) + self.exportBtn.setText(_translate("Form", "Export")) + self.closeBtn.setText(_translate("Form", "Close")) + self.label_3.setText(_translate("Form", "Export options")) + self.copyBtn.setText(_translate("Form", "Copy")) + +from ..parametertree import ParameterTree diff --git a/pyqtgraph/GraphicsScene/mouseEvents.py b/pyqtgraph/GraphicsScene/mouseEvents.py index 2e472e04..fb9d3683 100644 --- a/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/pyqtgraph/GraphicsScene/mouseEvents.py @@ -276,8 +276,6 @@ class HoverEvent(object): self._modifiers = moveEvent.modifiers() else: self.exit = True - - def isEnter(self): """Returns True if the mouse has just entered the item's shape""" diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index efbe66c4..2ed9d6f9 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -4,37 +4,58 @@ This module exists to smooth out some of the differences between PySide and PyQt * Automatically import either PyQt4 or PySide depending on availability * Allow to import QtCore/QtGui pyqtgraph.Qt without specifying which Qt wrapper you want to use. -* Declare QtCore.Signal, .Slot in PyQt4 +* Declare QtCore.Signal, .Slot in PyQt4 * Declare loadUiType function for Pyside """ -import sys, re +import os, sys, re, time from .python2_3 import asUnicode -## Automatically determine whether to use PyQt or PySide. +PYSIDE = 'PySide' +PYQT4 = 'PyQt4' +PYQT5 = 'PyQt5' + +QT_LIB = os.getenv('PYQTGRAPH_QT_LIB') + +## Automatically determine whether to use PyQt or PySide (unless specified by +## environment variable). ## This is done by first checking to see whether one of the libraries ## is already imported. If not, then attempt to import PyQt4, then PySide. -if 'PyQt4' in sys.modules: - USE_PYSIDE = False -elif 'PySide' in sys.modules: - USE_PYSIDE = True -else: - try: - import PyQt4 - USE_PYSIDE = False - except ImportError: - try: - import PySide - USE_PYSIDE = True - except ImportError: - raise Exception("PyQtGraph requires either PyQt4 or PySide; neither package could be imported.") +if QT_LIB is None: + libOrder = [PYQT4, PYSIDE, PYQT5] -if USE_PYSIDE: + for lib in libOrder: + if lib in sys.modules: + QT_LIB = lib + break + +if QT_LIB is None: + for lib in libOrder: + try: + __import__(lib) + QT_LIB = lib + break + except ImportError: + pass + +if QT_LIB is None: + raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") + +if QT_LIB == PYSIDE: from PySide import QtGui, QtCore, QtOpenGL, QtSvg try: from PySide import QtTest + if not hasattr(QtTest.QTest, 'qWait'): + @staticmethod + def qWait(msec): + start = time.time() + QtGui.QApplication.processEvents() + while time.time() < start + msec * 0.001: + QtGui.QApplication.processEvents() + QtTest.QTest.qWait = qWait + except ImportError: pass import PySide @@ -59,7 +80,7 @@ if USE_PYSIDE: # Make a loadUiType function like PyQt has - # Credit: + # Credit: # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 class StringIO(object): @@ -75,7 +96,15 @@ if USE_PYSIDE: def loadUiType(uiFile): """ - Pyside "loadUiType" command like PyQt4 has one, so we have to convert the ui file to py code in-memory first and then execute it in a special frame to retrieve the form_class. + Pyside "loadUiType" command like PyQt4 has one, so we have to convert + the ui file to py code in-memory first and then execute it in a + special frame to retrieve the form_class. + + from stackoverflow: http://stackoverflow.com/a/14195313/3781327 + + seems like this might also be a legitimate solution, but I'm not sure + how to make PyQt4 and pyside look the same... + http://stackoverflow.com/a/8717832 """ import pysideuic import xml.etree.ElementTree as xml @@ -98,9 +127,9 @@ if USE_PYSIDE: base_class = eval('QtGui.%s'%widget_class) return form_class, base_class - - -else: + +elif QT_LIB == PYQT4: + from PyQt4 import QtGui, QtCore, uic try: from PyQt4 import QtSvg @@ -115,21 +144,98 @@ else: except ImportError: pass + VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR +elif QT_LIB == PYQT5: + + # We're using PyQt5 which has a different structure so we're going to use a shim to + # recreate the Qt4 structure for Qt5 + from PyQt5 import QtGui, QtCore, QtWidgets, uic + try: + from PyQt5 import QtSvg + except ImportError: + pass + try: + from PyQt5 import QtOpenGL + except ImportError: + pass + try: + from PyQt5 import QtTest + QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed + except ImportError: + pass + + # Re-implement deprecated APIs + + __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale + + def scale(self, *args): + if args: + sx, sy = args + tr = self.transform() + tr.scale(sx, sy) + self.setTransform(tr) + else: + return __QGraphicsItem_scale(self) + + QtWidgets.QGraphicsItem.scale = scale + + def rotate(self, angle): + tr = self.transform() + tr.rotate(angle) + self.setTransform(tr) + QtWidgets.QGraphicsItem.rotate = rotate + + def translate(self, dx, dy): + tr = self.transform() + tr.translate(dx, dy) + self.setTransform(tr) + QtWidgets.QGraphicsItem.translate = translate + + def setMargin(self, i): + self.setContentsMargins(i, i, i, i) + QtWidgets.QGridLayout.setMargin = setMargin + + def setResizeMode(self, *args): + self.setSectionResizeMode(*args) + QtWidgets.QHeaderView.setResizeMode = setResizeMode + + + QtGui.QApplication = QtWidgets.QApplication + QtGui.QGraphicsScene = QtWidgets.QGraphicsScene + QtGui.QGraphicsObject = QtWidgets.QGraphicsObject + QtGui.QGraphicsWidget = QtWidgets.QGraphicsWidget + + QtGui.QApplication.setGraphicsSystem = None + + # Import all QtWidgets objects into QtGui + for o in dir(QtWidgets): + if o.startswith('Q'): + setattr(QtGui, o, getattr(QtWidgets,o) ) + + VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR + +else: + raise ValueError("Invalid Qt lib '%s'" % QT_LIB) + +# Common to PyQt4 and 5 +if QT_LIB.startswith('PyQt'): import sip def isQObjectAlive(obj): return not sip.isdeleted(obj) loadUiType = uic.loadUiType QtCore.Signal = QtCore.pyqtSignal - VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR - + + ## Make sure we have Qt >= 4.7 versionReq = [4, 7] -QtVersion = PySide.QtCore.__version__ if USE_PYSIDE else QtCore.QT_VERSION_STR +USE_PYSIDE = QT_LIB == PYSIDE +USE_PYQT4 = QT_LIB == PYQT4 +USE_PYQT5 = QT_LIB == PYQT5 +QtVersion = PySide.QtCore.__version__ if QT_LIB == PYSIDE else QtCore.QT_VERSION_STR m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: print(list(map(int, m.groups()))) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) - diff --git a/pyqtgraph/SRTTransform.py b/pyqtgraph/SRTTransform.py index 23281343..b1aea297 100644 --- a/pyqtgraph/SRTTransform.py +++ b/pyqtgraph/SRTTransform.py @@ -3,6 +3,7 @@ from .Qt import QtCore, QtGui from .Point import Point import numpy as np + class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. @@ -165,6 +166,7 @@ class SRTTransform(QtGui.QTransform): def matrix(self): return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) + if __name__ == '__main__': from . import widgets diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index 29541454..d7e265c5 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -8,7 +8,7 @@ This class addresses the problem of having to save and restore the state of a large group of widgets. """ -from .Qt import QtCore, QtGui +from .Qt import QtCore, QtGui, USE_PYQT5 import weakref, inspect from .python2_3 import asUnicode @@ -60,9 +60,13 @@ def setComboState(w, v): class WidgetGroup(QtCore.QObject): - """This class takes a list of widgets and keeps an internal record of their state which is always up to date. Allows reading and writing from groups of widgets simultaneously.""" + """This class takes a list of widgets and keeps an internal record of their + state that is always up to date. - ## List of widget types which can be handled by WidgetGroup. + Allows reading and writing from groups of widgets simultaneously. + """ + + ## List of widget types that can be handled by WidgetGroup. ## The value for each type is a tuple (change signal function, get function, set function, [auto-add children]) ## The change signal function that takes an object and returns a signal that is emitted any time the state of the widget changes, not just ## when it is changed by user interaction. (for example, 'clicked' is not a valid signal here) @@ -200,51 +204,35 @@ class WidgetGroup(QtCore.QObject): if hasattr(obj, 'widgetGroupInterface'): return True return False - #return (type(obj) in WidgetGroup.classes) def setScale(self, widget, scale): val = self.readWidget(widget) self.scales[widget] = scale self.setWidget(widget, val) - #print "scaling %f to %f" % (val, self.readWidget(widget)) - def mkChangeCallback(self, w): return lambda *args: self.widgetChanged(w, *args) def widgetChanged(self, w, *args): - #print "widget changed" n = self.widgetList[w] v1 = self.cache[n] v2 = self.readWidget(w) if v1 != v2: - #print "widget", n, " = ", v2 - self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) + if not USE_PYQT5: + # Old signal kept for backward compatibility. + self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) self.sigChanged.emit(self.widgetList[w], v2) def state(self): for w in self.uncachedWidgets: self.readWidget(w) - - #cc = self.cache.copy() - #if 'averageGroup' in cc: - #val = cc['averageGroup'] - #w = self.findWidget('averageGroup') - #self.readWidget(w) - #if val != self.cache['averageGroup']: - #print " AverageGroup did not match cached value!" - #else: - #print " AverageGroup OK" return self.cache.copy() def setState(self, s): - #print "SET STATE", self, s for w in self.widgetList: n = self.widgetList[w] - #print " restore %s?" % n if n not in s: continue - #print " restore state", w, n, s[n] self.setWidget(w, s[n]) def readWidget(self, w): diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index de9fb278..3a0a7419 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = '0.9.8' +__version__ = '0.10.0' ### import all the goodies and add some helper functions for easy CLI use @@ -41,13 +41,15 @@ elif 'darwin' in sys.platform: ## openGL can have a major impact on mac, but als useOpenGL = False if QtGui.QApplication.instance() is not None: print('Warning: QApplication was created before pyqtgraph was imported; there may be problems (to avoid bugs, call QApplication.setGraphicsSystem("raster") before the QApplication is created).') - QtGui.QApplication.setGraphicsSystem('raster') ## work around a variety of bugs in the native graphics system + if QtGui.QApplication.setGraphicsSystem: + QtGui.QApplication.setGraphicsSystem('raster') ## work around a variety of bugs in the native graphics system else: useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. CONFIG_OPTIONS = { 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox + # foreground/background take any arguments to the 'mkColor' in /pyqtgraph/functions.py 'foreground': 'd', ## default foreground color for axes, labels, etc. 'background': 'k', ## default background for GraphicsWidget 'antialias': False, @@ -57,16 +59,32 @@ CONFIG_OPTIONS = { 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'crashWarning': False, # If True, print warnings about situations that may result in a crash + 'imageAxisOrder': 'col-major', # For 'row-major', image data is expected in the standard (row, col) order. + # For 'col-major', image data is expected in reversed (col, row) order. + # The default is 'col-major' for backward compatibility, but this may + # change in the future. } 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'): + raise ValueError('imageAxisOrder must be either "row-major" or "col-major"') CONFIG_OPTIONS[opt] = value def setConfigOptions(**opts): - CONFIG_OPTIONS.update(opts) + """Set global configuration options. + + Each keyword argument sets one global option. + """ + for k,v in opts.items(): + setConfigOption(k, v) def getConfigOption(opt): + """Return the value of a single global configuration option. + """ return CONFIG_OPTIONS[opt] @@ -271,7 +289,12 @@ from .Qt import isQObjectAlive ## Attempts to work around exit crashes: import atexit +_cleanupCalled = False def cleanup(): + global _cleanupCalled + if _cleanupCalled: + return + if not getConfigOption('exitCleanup'): return @@ -281,7 +304,10 @@ def cleanup(): ## ALL QGraphicsItems must have a scene before they are deleted. ## This is potentially very expensive, but preferred over crashing. ## Note: this appears to be fixed in PySide as of 2012.12, but it should be left in for a while longer.. - if QtGui.QApplication.instance() is None: + app = QtGui.QApplication.instance() + if app is None or not isinstance(app, QtGui.QApplication): + # app was never constructed is already deleted or is an + # QCoreApplication/QGuiApplication and not a full QApplication return import gc s = QtGui.QGraphicsScene() @@ -296,8 +322,22 @@ def cleanup(): s.addItem(o) except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object continue + _cleanupCalled = True + atexit.register(cleanup) +# Call cleanup when QApplication quits. This is necessary because sometimes +# the QApplication will quit before the atexit callbacks are invoked. +# Note: cannot connect this function until QApplication has been created, so +# instead we have GraphicsView.__init__ call this for us. +_cleanupConnected = False +def _connectCleanup(): + global _cleanupConnected + if _cleanupConnected: + return + QtGui.QApplication.instance().aboutToQuit.connect(cleanup) + _cleanupConnected = True + ## Optional function for exiting immediately (with some manual teardown) def exit(): @@ -327,7 +367,7 @@ def exit(): ## close file handles if sys.platform == 'darwin': - for fd in xrange(3, 4096): + for fd in range(3, 4096): if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac. os.close(fd) else: diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 4de891f7..5b5ce2f7 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -4,15 +4,17 @@ if __name__ == '__main__': 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, USE_PYSIDE +from ..Qt import QtGui, QtCore, QT_LIB from ..graphicsItems.ROI import ROI from ..graphicsItems.ViewBox import ViewBox from ..graphicsItems.GridItem import GridItem -if USE_PYSIDE: +if QT_LIB == 'PySide': from .CanvasTemplate_pyside import * -else: +elif QT_LIB == 'PyQt4': from .CanvasTemplate_pyqt import * +elif QT_LIB == 'PyQt5': + from .CanvasTemplate_pyqt5 import * import numpy as np from .. import debug @@ -378,7 +380,7 @@ class Canvas(QtGui.QWidget): z = citem.zValue() if z is None: zvals = [i.zValue() for i in siblings] - if parent == self.itemList.invisibleRootItem(): + if parent is self.itemList.invisibleRootItem(): if len(zvals) == 0: z = 0 else: diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index b6ecbb39..a06235b2 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- -from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from ..graphicsItems.ROI import ROI from .. import SRTTransform, ItemGroup -if USE_PYSIDE: +if QT_LIB == 'PySide': from . import TransformGuiTemplate_pyside as TransformGuiTemplate -else: +elif QT_LIB == 'PyQt4': from . import TransformGuiTemplate_pyqt as TransformGuiTemplate +elif QT_LIB == 'PyQt5': + from . import TransformGuiTemplate_pyqt5 as TransformGuiTemplate from .. import debug diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index 9bea8f89..b05c11cd 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -127,7 +127,7 @@ CanvasCombo QComboBox -
CanvasManager
+
.CanvasManager
diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index 557354e0..b65ef465 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'acq4/pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Thu Jan 2 11:13:07 2014 -# by: PyQt4 UI code generator 4.9 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -12,7 +11,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -30,7 +38,6 @@ class Ui_Form(object): self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout_2.setMargin(0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) @@ -79,14 +86,14 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) - self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) - self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) - self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", 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)) + self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms", None)) + self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection", None)) + self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY", None)) -from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo from ..widgets.GraphicsView import GraphicsView +from ..widgets.TreeWidget import TreeWidget +from .CanvasManager import CanvasCombo diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py new file mode 100644 index 00000000..20f5e339 --- /dev/null +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# +# Created by: PyQt5 UI code generator 5.5.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(490, 414) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.view = GraphicsView(self.splitter) + self.view.setObjectName("view") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName("autoRangeBtn") + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName("redirectCheck") + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName("redirectCombo") + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName("itemList") + self.itemList.headerItem().setText(0, "1") + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) + self.ctrlLayout = QtWidgets.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) + self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) + self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) + self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + 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")) + self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms")) + self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection")) + self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY")) + +from ..widgets.GraphicsView import GraphicsView +from ..widgets.TreeWidget import TreeWidget +from .CanvasManager import CanvasCombo diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index 56d1ff47..b0e05a07 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Wed Nov 9 18:02:00 2016 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -27,12 +27,6 @@ class Ui_Form(object): self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setObjectName("gridLayout_2") - self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) - self.storeSvgBtn.setObjectName("storeSvgBtn") - self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) - self.storePngBtn = QtGui.QPushButton(self.layoutWidget) - self.storePngBtn.setObjectName("storePngBtn") - self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -40,7 +34,7 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") @@ -50,7 +44,7 @@ class Ui_Form(object): self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) self.itemList = TreeWidget(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -60,20 +54,20 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) self.ctrlLayout = QtGui.QGridLayout() self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -81,8 +75,6 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) - self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", 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)) @@ -90,6 +82,6 @@ class Ui_Form(object): self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) +from .CanvasManager import CanvasCombo from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py index 75c694c0..c6cf82e4 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: PyQt4 UI code generator 4.10 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -33,8 +32,8 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) Form.setSizePolicy(sizePolicy) self.verticalLayout = QtGui.QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) self.verticalLayout.setMargin(0) + self.verticalLayout.setSpacing(1) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.translateLabel = QtGui.QLabel(Form) self.translateLabel.setObjectName(_fromUtf8("translateLabel")) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py new file mode 100644 index 00000000..6b1f239b --- /dev/null +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' +# +# Created by: PyQt5 UI code generator 5.5.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(224, 117) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setObjectName("verticalLayout") + self.translateLabel = QtWidgets.QLabel(Form) + self.translateLabel.setObjectName("translateLabel") + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtWidgets.QLabel(Form) + self.rotateLabel.setObjectName("rotateLabel") + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtWidgets.QLabel(Form) + self.scaleLabel.setObjectName("scaleLabel") + self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.mirrorImageBtn = QtWidgets.QPushButton(Form) + self.mirrorImageBtn.setToolTip("") + self.mirrorImageBtn.setObjectName("mirrorImageBtn") + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtWidgets.QPushButton(Form) + self.reflectImageBtn.setObjectName("reflectImageBtn") + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.translateLabel.setText(_translate("Form", "Translate:")) + self.rotateLabel.setText(_translate("Form", "Rotate:")) + self.scaleLabel.setText(_translate("Form", "Scale:")) + self.mirrorImageBtn.setText(_translate("Form", "Mirror")) + self.reflectImageBtn.setText(_translate("Form", "Reflect")) + diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py index bce7b511..e430b61a 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Wed Nov 9 17:57:16 2016 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index c0033708..f943e2fe 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -1,5 +1,7 @@ import numpy as np from .Qt import QtGui, QtCore +from .python2_3 import basestring + class ColorMap(object): """ @@ -64,7 +66,9 @@ class ColorMap(object): =============== ============================================================== """ self.pos = np.array(pos) - self.color = np.array(color) + order = np.argsort(self.pos) + self.pos = self.pos[order] + self.color = np.array(color)[order] if mode is None: mode = np.ones(len(pos)) self.mode = mode diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index c095bba3..7b20db1d 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -10,14 +10,15 @@ as it can be converted to/from a string using repr and eval. """ import re, os, sys +import numpy from .pgcollections import OrderedDict -GLOBAL_PATH = None # so not thread safe. from . import units -from .python2_3 import asUnicode +from .python2_3 import asUnicode, basestring from .Qt import QtCore from .Point import Point from .colormap import ColorMap -import numpy +GLOBAL_PATH = None # so not thread safe. + class ParseError(Exception): def __init__(self, message, lineNum, line, fileName=None): diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 896de924..ed4b7f08 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,14 +1,17 @@ - -from ..Qt import QtCore, QtGui, USE_PYSIDE import sys, re, os, time, traceback, subprocess +import pickle + +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5 +from ..python2_3 import basestring +from .. import exceptionHandling as exceptionHandling +from .. import getConfigOption if USE_PYSIDE: from . import template_pyside as template +elif USE_PYQT5: + from . import template_pyqt5 as template else: from . import template_pyqt as template - -from .. import exceptionHandling as exceptionHandling -import pickle -from .. import getConfigOption + class ConsoleWidget(QtGui.QWidget): """ @@ -45,6 +48,7 @@ class ConsoleWidget(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) if namespace is None: namespace = {} + namespace['__console__'] = self self.localNamespace = namespace self.editor = editor self.multiline = None @@ -131,7 +135,7 @@ class ConsoleWidget(QtGui.QWidget): if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): return self.currentFrame().tb_frame.f_globals else: - return globals() + return self.localNamespace def locals(self): frame = self.currentFrame() diff --git a/pyqtgraph/console/template_pyqt5.py b/pyqtgraph/console/template_pyqt5.py new file mode 100644 index 00000000..1fbc5bed --- /dev/null +++ b/pyqtgraph/console/template_pyqt5.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# +# Created: Wed Mar 26 15:09:29 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(710, 497) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) + font = QtGui.QFont() + font.setFamily("Monospace") + self.output.setFont(font) + self.output.setReadOnly(True) + self.output.setObjectName("output") + self.verticalLayout.addWidget(self.output) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.input = CmdInput(self.layoutWidget) + self.input.setObjectName("input") + self.horizontalLayout.addWidget(self.input) + self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) + self.historyBtn.setCheckable(True) + self.historyBtn.setObjectName("historyBtn") + self.horizontalLayout.addWidget(self.historyBtn) + self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.exceptionBtn.setCheckable(True) + self.exceptionBtn.setObjectName("exceptionBtn") + self.horizontalLayout.addWidget(self.exceptionBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + self.historyList = QtWidgets.QListWidget(self.splitter) + font = QtGui.QFont() + font.setFamily("Monospace") + self.historyList.setFont(font) + self.historyList.setObjectName("historyList") + self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) + self.exceptionGroup.setObjectName("exceptionGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchAllExceptionsBtn.setCheckable(True) + self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") + self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchNextExceptionBtn.setCheckable(True) + self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") + self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.onlyUncaughtCheck.setChecked(True) + self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) + self.exceptionStackList.setAlternatingRowColors(True) + self.exceptionStackList.setObjectName("exceptionStackList") + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.runSelectedFrameCheck.setChecked(True) + self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) + self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Console")) + self.historyBtn.setText(_translate("Form", "History..")) + self.exceptionBtn.setText(_translate("Form", "Exceptions..")) + self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) + self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) + self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) + self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) + self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) + self.exceptionInfoLabel.setText(_translate("Form", "Exception Info")) + self.clearExceptionBtn.setText(_translate("Form", "Clear Exception")) + +from .CmdInput import CmdInput diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 57c71bc8..0da24d7c 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -83,8 +83,9 @@ class Tracer(object): funcname = cls.__name__ + "." + funcname return "%s: %s %s: %s" % (callline, filename, lineno, funcname) + def warnOnException(func): - """Decorator which catches/ignores exceptions and prints a stack trace.""" + """Decorator that catches/ignores exceptions and prints a stack trace.""" def w(*args, **kwds): try: func(*args, **kwds) @@ -92,11 +93,9 @@ def warnOnException(func): printExc('Ignored exception:') return w + def getExc(indent=4, prefix='| ', skip=1): - lines = (traceback.format_stack()[:-skip] - + [" ---- exception caught ---->\n"] - + traceback.format_tb(sys.exc_info()[2]) - + traceback.format_exception_only(*sys.exc_info()[:2])) + lines = formatException(*sys.exc_info(), skip=skip) lines2 = [] for l in lines: lines2.extend(l.strip('\n').split('\n')) @@ -112,6 +111,7 @@ def printExc(msg='', indent=4, prefix='|'): print(" "*indent + prefix + '='*30 + '>>') print(exc) print(" "*indent + prefix + '='*30 + '<<') + def printTrace(msg='', indent=4, prefix='|'): """Print an error message followed by an indented stack trace""" @@ -126,7 +126,30 @@ def printTrace(msg='', indent=4, prefix='|'): def backtrace(skip=0): return ''.join(traceback.format_stack()[:-(skip+1)]) + + +def formatException(exctype, value, tb, skip=0): + """Return a list of formatted exception strings. + Similar to traceback.format_exception, but displays the entire stack trace + rather than just the portion downstream of the point where the exception is + caught. In particular, unhandled exceptions that occur during Qt signal + handling do not usually show the portion of the stack that emitted the + signal. + """ + lines = traceback.format_exception(exctype, value, tb) + lines = [lines[0]] + traceback.format_stack()[:-(skip+1)] + [' --- exception caught here ---\n'] + lines[1:] + return lines + + +def printException(exctype, value, traceback): + """Print an exception with its full traceback. + + Set `sys.excepthook = printException` to ensure that exceptions caught + inside Qt signal handlers are printed with their full stack trace. + """ + print(''.join(formatException(exctype, value, traceback, skip=1))) + def listObjs(regex='Q', typ=None): """List all objects managed by python gc with class name matching regex. @@ -723,7 +746,6 @@ class ObjTracker(object): for k in self.startCount: c1[k] = c1.get(k, 0) - self.startCount[k] typs = list(c1.keys()) - #typs.sort(lambda a,b: cmp(c1[a], c1[b])) typs.sort(key=lambda a: c1[a]) for t in typs: if c1[t] == 0: @@ -824,7 +846,6 @@ class ObjTracker(object): c = count.get(typ, [0,0]) count[typ] = [c[0]+1, c[1]+objectSize(obj)] typs = list(count.keys()) - #typs.sort(lambda a,b: cmp(count[a][1], count[b][1])) typs.sort(key=lambda a: count[a][1]) for t in typs: @@ -1097,46 +1118,44 @@ def pretty(data, indent=''): return ret -class PeriodicTrace(object): +class ThreadTrace(object): """ Used to debug freezing by starting a new thread that reports on the - location of the main thread periodically. + location of other threads periodically. """ - class ReportThread(QtCore.QThread): - def __init__(self): - self.frame = None - self.ind = 0 - self.lastInd = None - self.lock = Mutex() - QtCore.QThread.__init__(self) + def __init__(self, interval=10.0): + self.interval = interval + self.lock = Mutex() + self._stop = False + self.start() - def notify(self, frame): - with self.lock: - self.frame = frame - self.ind += 1 + def stop(self): + with self.lock: + self._stop = True - def run(self): - while True: - time.sleep(1) - with self.lock: - if self.lastInd != self.ind: - print("== Trace %d: ==" % self.ind) - traceback.print_stack(self.frame) - self.lastInd = self.ind - - def __init__(self): - self.mainThread = threading.current_thread() - self.thread = PeriodicTrace.ReportThread() + def start(self, interval=None): + if interval is not None: + self.interval = interval + self._stop = False + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True self.thread.start() - sys.settrace(self.trace) - - def trace(self, frame, event, arg): - if threading.current_thread() is self.mainThread: # and 'threading' not in frame.f_code.co_filename: - self.thread.notify(frame) - # print("== Trace ==", event, arg) - # traceback.print_stack(frame) - return self.trace + def run(self): + while True: + with self.lock: + if self._stop is True: + return + + print("\n============= THREAD FRAMES: ================") + for id, frame in sys._current_frames().items(): + if id == threading.current_thread().ident: + continue + print("<< thread %d >>" % id) + traceback.print_stack(frame) + print("===============================================\n") + + time.sleep(self.interval) class ThreadColor(object): diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 28d4244b..4493d075 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -7,10 +7,13 @@ from ..python2_3 import asUnicode class Dock(QtGui.QWidget, DockDrop): sigStretchChanged = QtCore.Signal() + sigClosed = QtCore.Signal(object) def __init__(self, name, area=None, size=(10, 10), widget=None, hideTitle=False, autoOrientation=True, closable=False): QtGui.QWidget.__init__(self) DockDrop.__init__(self) + self._container = None + self._name = name self.area = area self.label = DockLabel(name, self, closable) if closable: @@ -126,6 +129,18 @@ class Dock(QtGui.QWidget, DockDrop): self.labelHidden = False self.allowedAreas.add('center') self.updateStyle() + + def title(self): + """ + Gets the text displayed in the title bar for this dock. + """ + return asUnicode(self.label.text()) + + def setTitle(self, text): + """ + Sets the text displayed in title bar for this Dock. + """ + self.label.setText(text) def setOrientation(self, o='auto', force=False): """ @@ -170,7 +185,7 @@ class Dock(QtGui.QWidget, DockDrop): self.resizeOverlay(self.size()) def name(self): - return asUnicode(self.label.text()) + return self._name def container(self): return self._container @@ -223,6 +238,7 @@ class Dock(QtGui.QWidget, DockDrop): self.label.setParent(None) self._container.apoptose() self._container = None + self.sigClosed.emit(self) def __repr__(self): return "" % (self.name(), self.stretch()) diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index a75d881d..ffe75b61 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -1,17 +1,11 @@ # -*- coding: utf-8 -*- +import weakref from ..Qt import QtCore, QtGui from .Container import * from .DockDrop import * from .Dock import Dock from .. import debug as debug -import weakref - -## TODO: -# - containers should be drop areas, not docks. (but every slot within a container must have its own drop areas?) -# - drop between tabs -# - nest splitters inside tab boxes, etc. - - +from ..python2_3 import basestring class DockArea(Container, QtGui.QWidget, DockDrop): @@ -102,9 +96,12 @@ class DockArea(Container, QtGui.QWidget, DockDrop): 'below': 'after' }[position] #print "request insert", dock, insertPos, neighbor + old = dock.container() container.insert(dock, insertPos, neighbor) dock.area = self self.docks[dock.name()] = dock + if old is not None: + old.apoptose() return dock @@ -112,12 +109,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): """ Move an existing Dock to a new location. """ - old = dock.container() ## Moving to the edge of a tabbed dock causes a drop outside the tab box if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab': neighbor = neighbor.container() self.addDock(dock, position, neighbor) - old.apoptose() def getContainer(self, obj): if obj is None: @@ -171,8 +166,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): if self.home is None: area = DockArea(temporary=True, home=self) self.tempAreas.append(area) - win = QtGui.QMainWindow() - win.setCentralWidget(area) + win = TempAreaWindow(area) area.win = win win.show() else: @@ -196,7 +190,13 @@ class DockArea(Container, QtGui.QWidget, DockDrop): """ Return a serialized (storable) representation of the state of all Docks in this DockArea.""" - state = {'main': self.childState(self.topContainer), 'float': []} + + if self.topContainer is None: + main = None + else: + main = self.childState(self.topContainer) + + state = {'main': main, 'float': []} for a in self.tempAreas: geo = a.win.geometry() geo = (geo.x(), geo.y(), geo.width(), geo.height()) @@ -228,7 +228,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop): #print "found docks:", docks ## 2) create container structure, move docks into new containers - self.buildFromState(state['main'], docks, self) + if state['main'] is not None: + self.buildFromState(state['main'], docks, self) ## 3) create floating areas, populate for s in state['float']: @@ -296,10 +297,16 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def apoptose(self): #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count() - if self.temporary and self.topContainer.count() == 0: + if self.topContainer.count() == 0: self.topContainer = None - self.home.removeTempArea(self) - #self.close() + if self.temporary: + self.home.removeTempArea(self) + #self.close() + + def clear(self): + docks = self.findAll()[1] + for dock in docks.values(): + dock.close() ## PySide bug: We need to explicitly redefine these methods ## or else drag/drop events will not be delivered. @@ -315,5 +322,12 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def dropEvent(self, *args): DockDrop.dropEvent(self, *args) - - + +class TempAreaWindow(QtGui.QMainWindow): + def __init__(self, area, **kwargs): + QtGui.QMainWindow.__init__(self, **kwargs) + self.setCentralWidget(area) + + def closeEvent(self, *args, **kwargs): + self.centralWidget().clear() + QtGui.QMainWindow.closeEvent(self, *args, **kwargs) diff --git a/pyqtgraph/exporters/Exporter.py b/pyqtgraph/exporters/Exporter.py index 64a25294..792e36bd 100644 --- a/pyqtgraph/exporters/Exporter.py +++ b/pyqtgraph/exporters/Exporter.py @@ -1,6 +1,6 @@ from ..widgets.FileDialog import FileDialog from ..Qt import QtGui, QtCore, QtSvg -from ..python2_3 import asUnicode +from ..python2_3 import asUnicode, basestring from ..GraphicsScene import GraphicsScene import os, re LastExportDirectory = None diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index a91466c8..ccf92165 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -45,41 +45,6 @@ class SVGExporter(Exporter): if toBytes is False and copy is False and fileName is None: self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)") return - #self.svg = QtSvg.QSvgGenerator() - #self.svg.setFileName(fileName) - #dpi = QtGui.QDesktopWidget().physicalDpiX() - ### not really sure why this works, but it seems to be important: - #self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.)) - #self.svg.setResolution(dpi) - ##self.svg.setViewBox() - #targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) - #sourceRect = self.getSourceRect() - - #painter = QtGui.QPainter(self.svg) - #try: - #self.setExportMode(True) - #self.render(painter, QtCore.QRectF(targetRect), sourceRect) - #finally: - #self.setExportMode(False) - #painter.end() - - ## Workaround to set pen widths correctly - #data = open(fileName).readlines() - #for i in range(len(data)): - #line = data[i] - #m = re.match(r'(= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i]) assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i]) i += 1 - + + os.unlink(tempfilename) + if __name__ == '__main__': test_CSVExporter() - \ No newline at end of file diff --git a/pyqtgraph/exporters/tests/test_svg.py b/pyqtgraph/exporters/tests/test_svg.py index 871f43c2..2261f7df 100644 --- a/pyqtgraph/exporters/tests/test_svg.py +++ b/pyqtgraph/exporters/tests/test_svg.py @@ -1,11 +1,18 @@ """ SVG export test """ +from __future__ import division, print_function, absolute_import import pyqtgraph as pg -import pyqtgraph.exporters +import tempfile +import os + + app = pg.mkQApp() + def test_plotscene(): + tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name + print("using %s as a temporary file" % tempfilename) pg.setConfigOption('foreground', (0,0,0)) w = pg.GraphicsWindow() w.show() @@ -18,10 +25,13 @@ def test_plotscene(): app.processEvents() ex = pg.exporters.SVGExporter(w.scene()) - ex.export(fileName='test.svg') - + ex.export(fileName=tempfilename) + # clean up after the test is done + os.unlink(tempfilename) def test_simple(): + tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name + print("using %s as a temporary file" % tempfilename) scene = pg.QtGui.QGraphicsScene() #rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) #scene.addItem(rect) @@ -51,17 +61,17 @@ def test_simple(): #el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50) #el.translate(10,-5) #el.scale(0.5,2) + #el.setParentItem(rect2) - + grp2 = pg.ItemGroup() scene.addItem(grp2) grp2.scale(100,100) - + rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2) rect3.setPen(pg.mkPen(width=1, cosmetic=False)) grp2.addItem(rect3) - - ex = pg.exporters.SVGExporter(scene) - ex.export(fileName='test.svg') - + ex = pg.exporters.SVGExporter(scene) + ex.export(fileName=tempfilename) + os.unlink(tempfilename) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 878f86ae..b623f5c7 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from ..Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5 from .Node import * from ..pgcollections import OrderedDict from ..widgets.TreeWidget import * @@ -9,6 +9,9 @@ from .. import FileDialog, DataTreeWidget if USE_PYSIDE: from . import FlowchartTemplate_pyside as FlowchartTemplate from . import FlowchartCtrlTemplate_pyside as FlowchartCtrlTemplate +elif USE_PYQT5: + from . import FlowchartTemplate_pyqt5 as FlowchartTemplate + from . import FlowchartCtrlTemplate_pyqt5 as FlowchartCtrlTemplate else: from . import FlowchartTemplate_pyqt as FlowchartTemplate from . import FlowchartCtrlTemplate_pyqt as FlowchartCtrlTemplate @@ -349,7 +352,6 @@ class Flowchart(Node): #tdeps[t] = lastNode if lastInd is not None: dels.append((lastInd+1, t)) - #dels.sort(lambda a,b: cmp(b[0], a[0])) dels.sort(key=lambda a: a[0], reverse=True) for i, t in dels: ops.insert(i, ('d', t)) @@ -379,22 +381,22 @@ class Flowchart(Node): terms = set(startNode.outputs().values()) #print "======= Updating", startNode - #print "Order:", order + # print("Order:", order) for node in order[1:]: - #print "Processing node", node + # print("Processing node", node) + update = False for term in list(node.inputs().values()): - #print " checking terminal", term + # print(" checking terminal", term) deps = list(term.connections().keys()) - update = False for d in deps: if d in terms: - #print " ..input", d, "changed" - update = True + # print(" ..input", d, "changed") + update |= True term.inputChanged(d, process=False) - if update: - #print " processing.." - node.update() - terms |= set(node.outputs().values()) + if update: + # print(" processing..") + node.update() + terms |= set(node.outputs().values()) finally: self.processing = False @@ -464,7 +466,6 @@ class Flowchart(Node): self.clear() Node.restoreState(self, state) nodes = state['nodes'] - #nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) nodes.sort(key=lambda a: a['pos'][0]) for n in nodes: if n['name'] in self._nodes: @@ -619,7 +620,10 @@ class FlowchartCtrlWidget(QtGui.QWidget): self.cwWin.resize(1000,800) h = self.ui.ctrlList.header() - h.setResizeMode(0, h.Stretch) + if not USE_PYQT5: + h.setResizeMode(0, h.Stretch) + else: + h.setSectionResizeMode(0, h.Stretch) self.ui.ctrlList.itemChanged.connect(self.itemChanged) self.ui.loadBtn.clicked.connect(self.loadClicked) @@ -823,16 +827,20 @@ class FlowchartWidget(dockarea.DockArea): self.buildMenu() 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): + buildSubMenu(node, menu, subMenus, pos=pos) + subMenus.append(menu) + else: + act = rootMenu.addAction(section) + act.nodeType = section + act.pos = pos self.nodeMenu = QtGui.QMenu() - self.subMenus = [] - for section, nodes in self.chart.library.getNodeTree().items(): - menu = QtGui.QMenu(section) - self.nodeMenu.addMenu(menu) - for name in nodes: - act = menu.addAction(name) - act.nodeType = name - act.pos = pos - self.subMenus.append(menu) + self.subMenus = [] + buildSubMenu(self.chart.library.getNodeTree(), self.nodeMenu, self.subMenus, pos=pos) self.nodeMenu.triggered.connect(self.nodeMenuTriggered) return self.nodeMenu diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py new file mode 100644 index 00000000..b661918d --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(217, 499) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.loadBtn = QtWidgets.QPushButton(Form) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName("saveBtn") + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName("saveAsBtn") + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName("reloadBtn") + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtWidgets.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName("showChartBtn") + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName("ctrlList") + self.ctrlList.headerItem().setText(0, "1") + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtWidgets.QLabel(Form) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText("") + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName("fileNameLabel") + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.loadBtn.setText(_translate("Form", "Load..")) + self.saveBtn.setText(_translate("Form", "Save")) + self.saveAsBtn.setText(_translate("Form", "As..")) + self.reloadBtn.setText(_translate("Form", "Reload Libs")) + self.showChartBtn.setText(_translate("Form", "Flowchart")) + +from ..widgets.FeedbackButton import FeedbackButton +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartGraphicsView.py b/pyqtgraph/flowchart/FlowchartGraphicsView.py index ab4b2914..93011218 100644 --- a/pyqtgraph/flowchart/FlowchartGraphicsView.py +++ b/pyqtgraph/flowchart/FlowchartGraphicsView.py @@ -4,72 +4,27 @@ from ..widgets.GraphicsView import GraphicsView from ..GraphicsScene import GraphicsScene from ..graphicsItems.ViewBox import ViewBox -#class FlowchartGraphicsView(QtGui.QGraphicsView): + class FlowchartGraphicsView(GraphicsView): sigHoverOver = QtCore.Signal(object) sigClicked = QtCore.Signal(object) def __init__(self, widget, *args): - #QtGui.QGraphicsView.__init__(self, *args) GraphicsView.__init__(self, *args, useOpenGL=False) - #self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255))) self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True) self.setCentralItem(self._vb) - #self.scene().addItem(self.vb) - #self.setMouseTracking(True) - #self.lastPos = None - #self.setTransformationAnchor(self.AnchorViewCenter) - #self.setRenderHints(QtGui.QPainter.Antialiasing) self.setRenderHint(QtGui.QPainter.Antialiasing, True) - #self.setDragMode(QtGui.QGraphicsView.RubberBandDrag) - #self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect) def viewBox(self): return self._vb - - #def mousePressEvent(self, ev): - #self.moved = False - #self.lastPos = ev.pos() - #return QtGui.QGraphicsView.mousePressEvent(self, ev) - - #def mouseMoveEvent(self, ev): - #self.moved = True - #callSuper = False - #if ev.buttons() & QtCore.Qt.RightButton: - #if self.lastPos is not None: - #dif = ev.pos() - self.lastPos - #self.scale(1.01**-dif.y(), 1.01**-dif.y()) - #elif ev.buttons() & QtCore.Qt.MidButton: - #if self.lastPos is not None: - #dif = ev.pos() - self.lastPos - #self.translate(dif.x(), -dif.y()) - #else: - ##self.emit(QtCore.SIGNAL('hoverOver'), self.items(ev.pos())) - #self.sigHoverOver.emit(self.items(ev.pos())) - #callSuper = True - #self.lastPos = ev.pos() - - #if callSuper: - #QtGui.QGraphicsView.mouseMoveEvent(self, ev) - - #def mouseReleaseEvent(self, ev): - #if not self.moved: - ##self.emit(QtCore.SIGNAL('clicked'), ev) - #self.sigClicked.emit(ev) - #return QtGui.QGraphicsView.mouseReleaseEvent(self, ev) class FlowchartViewBox(ViewBox): def __init__(self, widget, *args, **kwargs): ViewBox.__init__(self, *args, **kwargs) self.widget = widget - #self.menu = None - #self._subMenus = None ## need a place to store the menus otherwise they dissappear (even though they've been added to other menus) ((yes, it doesn't make sense)) - - - def getMenu(self, ev): ## called by ViewBox to create a new context menu @@ -84,26 +39,3 @@ class FlowchartViewBox(ViewBox): menu = self.widget.buildMenu(ev.scenePos()) menu.setTitle("Add node") return [menu, ViewBox.getMenu(self, ev)] - - - - - - - - - - -##class FlowchartGraphicsScene(QtGui.QGraphicsScene): -#class FlowchartGraphicsScene(GraphicsScene): - - #sigContextMenuEvent = QtCore.Signal(object) - - #def __init__(self, *args): - ##QtGui.QGraphicsScene.__init__(self, *args) - #GraphicsScene.__init__(self, *args) - - #def mouseClickEvent(self, ev): - ##QtGui.QGraphicsScene.contextMenuEvent(self, ev) - #if not ev.button() in [QtCore.Qt.RightButton]: - #self.sigContextMenuEvent.emit(ev) \ No newline at end of file diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py new file mode 100644 index 00000000..ba754305 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(529, 329) + self.selInfoWidget = QtWidgets.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName("selInfoWidget") + self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) + self.selDescLabel.setText("") + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName("selDescLabel") + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.selNameLabel.setFont(font) + self.selNameLabel.setText("") + self.selNameLabel.setObjectName("selNameLabel") + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName("selectedTree") + self.selectedTree.headerItem().setText(0, "1") + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtWidgets.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName("hoverText") + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName("view") + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + +from ..widgets.DataTreeWidget import DataTreeWidget +from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index fc7b04d3..c450a9f3 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -6,7 +6,6 @@ from .Terminal import * from ..pgcollections import OrderedDict from ..debug import * import numpy as np -from .eq import * def strDict(d): @@ -261,7 +260,7 @@ class Node(QtCore.QObject): for k, v in args.items(): term = self._inputs[k] oldVal = term.value() - if not eq(oldVal, v): + if not fn.eq(oldVal, v): changed = True term.setValue(v, process=False) if changed and '_updatesHandled_' not in args: diff --git a/pyqtgraph/flowchart/Terminal.py b/pyqtgraph/flowchart/Terminal.py index 6a6db62e..016e2d30 100644 --- a/pyqtgraph/flowchart/Terminal.py +++ b/pyqtgraph/flowchart/Terminal.py @@ -4,8 +4,7 @@ import weakref from ..graphicsItems.GraphicsObject import GraphicsObject from .. import functions as fn from ..Point import Point -#from PySide import QtCore, QtGui -from .eq import * + class Terminal(object): def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None): @@ -29,9 +28,6 @@ class Terminal(object): ============== ================================================================================= """ self._io = io - #self._isOutput = opts[0] in ['out', 'io'] - #self._isInput = opts[0]] in ['in', 'io'] - #self._isIO = opts[0]=='io' self._optional = optional self._multi = multi self._node = weakref.ref(node) @@ -68,7 +64,7 @@ class Terminal(object): """If this is a single-value terminal, val should be a single value. If this is a multi-value terminal, val should be a dict of terminal:value pairs""" if not self.isMultiValue(): - if eq(val, self._value): + if fn.eq(val, self._value): return self._value = val else: @@ -81,11 +77,6 @@ class Terminal(object): if self.isInput() and process: self.node().update() - ## Let the flowchart handle this. - #if self.isOutput(): - #for c in self.connections(): - #if c.isInput(): - #c.inputChanged(self) self.recolor() def setOpts(self, **opts): @@ -94,7 +85,6 @@ class Terminal(object): self._multiable = opts.get('multiable', self._multiable) if 'multi' in opts: self.setMultiValue(opts['multi']) - def connected(self, term): """Called whenever this terminal has been connected to another. (note--this function is called on both terminals)""" @@ -109,12 +99,10 @@ class Terminal(object): if self.isMultiValue() and term in self._value: del self._value[term] self.node().update() - #self.recolor() else: if self.isInput(): self.setValue(None) self.node().disconnected(self, term) - #self.node().update() def inputChanged(self, term, process=True): """Called whenever there is a change to the input value to this terminal. @@ -178,7 +166,6 @@ class Terminal(object): return term in self.connections() def hasInput(self): - #conn = self.extendedConnections() for t in self.connections(): if t.isOutput(): return True @@ -186,17 +173,10 @@ class Terminal(object): def inputTerminals(self): """Return the terminal(s) that give input to this one.""" - #terms = self.extendedConnections() - #for t in terms: - #if t.isOutput(): - #return t return [t for t in self.connections() if t.isOutput()] - def dependentNodes(self): """Return the list of nodes which receive input from this terminal.""" - #conn = self.extendedConnections() - #del conn[self] return set([t.node() for t in self.connections() if t.isInput()]) def connectTo(self, term, connectionItem=None): @@ -210,12 +190,6 @@ class Terminal(object): for t in [self, term]: if t.isInput() and not t._multi and len(t.connections()) > 0: raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, list(t.connections().keys()))) - #if self.hasInput() and term.hasInput(): - #raise Exception('Target terminal already has input') - - #if term in self.node().terminals.values(): - #if self.isOutput() or term.isOutput(): - #raise Exception('Can not connect an output back to the same node.') except: if connectionItem is not None: connectionItem.close() @@ -223,18 +197,12 @@ class Terminal(object): if connectionItem is None: connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem()) - #self.graphicsItem().scene().addItem(connectionItem) self.graphicsItem().getViewBox().addItem(connectionItem) - #connectionItem.setParentItem(self.graphicsItem().parent().parent()) self._connections[term] = connectionItem term._connections[self] = connectionItem self.recolor() - #if self.isOutput() and term.isInput(): - #term.inputChanged(self) - #if term.isInput() and term.isOutput(): - #self.inputChanged(term) self.connected(term) term.connected(self) @@ -244,8 +212,6 @@ class Terminal(object): if not self.connectedTo(term): return item = self._connections[term] - #print "removing connection", item - #item.scene().removeItem(item) item.close() del self._connections[term] del term._connections[self] @@ -254,10 +220,6 @@ class Terminal(object): self.disconnected(term) term.disconnected(self) - #if self.isOutput() and term.isInput(): - #term.inputChanged(self) - #if term.isInput() and term.isOutput(): - #self.inputChanged(term) def disconnectAll(self): @@ -270,7 +232,7 @@ class Terminal(object): color = QtGui.QColor(0,0,0) elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals color = QtGui.QColor(200,200,0) - elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) + elif self._value is None or fn.eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) color = QtGui.QColor(255,255,255) elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok color = QtGui.QColor(200, 200, 0) @@ -283,7 +245,6 @@ class Terminal(object): if recurse: for t in self.connections(): t.recolor(color, recurse=False) - def rename(self, name): oldName = self._name @@ -294,17 +255,6 @@ class Terminal(object): def __repr__(self): return "" % (str(self.node().name()), str(self.name())) - #def extendedConnections(self, terms=None): - #"""Return list of terminals (including this one) that are directly or indirectly wired to this.""" - #if terms is None: - #terms = {} - #terms[self] = None - #for t in self._connections: - #if t in terms: - #continue - #terms.update(t.extendedConnections(terms)) - #return terms - def __hash__(self): return id(self) @@ -318,18 +268,15 @@ class Terminal(object): return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable} -#class TerminalGraphicsItem(QtGui.QGraphicsItem): class TerminalGraphicsItem(GraphicsObject): def __init__(self, term, parent=None): self.term = term - #QtGui.QGraphicsItem.__init__(self, parent) GraphicsObject.__init__(self, parent) self.brush = fn.mkBrush(0,0,0) self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self) self.label = QtGui.QGraphicsTextItem(self.term.name(), self) self.label.scale(0.7, 0.7) - #self.setAcceptHoverEvents(True) self.newConnection = None self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem if self.term.isRenamable(): @@ -338,7 +285,6 @@ class TerminalGraphicsItem(GraphicsObject): self.label.keyPressEvent = self.labelKeyPress self.setZValue(1) self.menu = None - def labelFocusOut(self, ev): QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev) @@ -471,8 +417,6 @@ class TerminalGraphicsItem(GraphicsObject): break if not gotTarget: - #print "remove unused connection" - #self.scene().removeItem(self.newConnection) self.newConnection.close() self.newConnection = None else: @@ -488,12 +432,6 @@ class TerminalGraphicsItem(GraphicsObject): self.box.setBrush(self.brush) self.update() - #def hoverEnterEvent(self, ev): - #self.hover = True - - #def hoverLeaveEvent(self, ev): - #self.hover = False - def connectPoint(self): ## return the connect position of this terminal in view coords return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center())) @@ -503,11 +441,9 @@ class TerminalGraphicsItem(GraphicsObject): item.updateLine() -#class ConnectionItem(QtGui.QGraphicsItem): class ConnectionItem(GraphicsObject): def __init__(self, source, target=None): - #QtGui.QGraphicsItem.__init__(self) GraphicsObject.__init__(self) self.setFlags( self.ItemIsSelectable | @@ -528,14 +464,12 @@ class ConnectionItem(GraphicsObject): 'selectedColor': (200, 200, 0), 'selectedWidth': 3.0, } - #self.line = QtGui.QGraphicsLineItem(self) self.source.getViewBox().addItem(self) self.updateLine() self.setZValue(0) def close(self): if self.scene() is not None: - #self.scene().removeItem(self.line) self.scene().removeItem(self) def setTarget(self, target): @@ -575,8 +509,11 @@ class ConnectionItem(GraphicsObject): return path def keyPressEvent(self, ev): + if not self.isSelected(): + ev.ignore() + return + if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: - #if isinstance(self.target, TerminalGraphicsItem): self.source.disconnect(self.target) ev.accept() else: @@ -590,6 +527,7 @@ class ConnectionItem(GraphicsObject): ev.accept() sel = self.isSelected() self.setSelected(True) + self.setFocus() if not sel and self.isSelected(): self.update() @@ -600,12 +538,9 @@ class ConnectionItem(GraphicsObject): self.hovered = False self.update() - def boundingRect(self): return self.shape().boundingRect() - ##return self.line.boundingRect() - #px = self.pixelWidth() - #return QtCore.QRectF(-5*px, 0, 10*px, self.length) + def viewRangeChanged(self): self.shapePath = None self.prepareGeometryChange() @@ -628,7 +563,5 @@ class ConnectionItem(GraphicsObject): p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth'])) else: p.setPen(fn.mkPen(self.style['color'], width=self.style['width'])) - - #p.drawLine(0, 0, 0, self.length) p.drawPath(self.path) diff --git a/pyqtgraph/flowchart/eq.py b/pyqtgraph/flowchart/eq.py deleted file mode 100644 index 554989b2..00000000 --- a/pyqtgraph/flowchart/eq.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -from numpy import ndarray, bool_ -from ..metaarray import MetaArray - -def eq(a, b): - """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" - if a is b: - return True - - try: - e = a==b - except ValueError: - return False - except AttributeError: - return False - except: - print("a:", str(type(a)), str(a)) - print("b:", str(type(b)), str(b)) - raise - t = type(e) - if t is bool: - return e - elif t is bool_: - return bool(e) - elif isinstance(e, ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')): - try: ## disaster: if a is an empty array and b is not, then e.all() is True - if a.shape != b.shape: - return False - except: - return False - if (hasattr(e, 'implements') and e.implements('MetaArray')): - return e.asarray().all() - else: - return e.all() - else: - raise Exception("== operator returned type %s" % str(type(e))) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index 88a2f6c5..9392b037 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- +import numpy as np from ...Qt import QtCore, QtGui from ..Node import Node from . import functions from ... import functions as pgfn from .common import * -import numpy as np - +from ...python2_3 import xrange from ... import PolyLineROI from ... import Point from ... import metaarray as metaarray @@ -164,8 +164,15 @@ class Gaussian(CtrlNode): import scipy.ndimage except ImportError: raise Exception("GaussianFilter node requires the package scipy.ndimage.") - return pgfn.gaussianFilter(data, self.ctrls['sigma'].value()) + if hasattr(data, 'implements') and data.implements('MetaArray'): + info = data.infoCopy() + filt = pgfn.gaussianFilter(data.asarray(), self.ctrls['sigma'].value()) + if 'values' in info[0]: + info[0]['values'] = info[0]['values'][:filt.shape[0]] + return metaarray.MetaArray(filt, info=info) + else: + return pgfn.gaussianFilter(data, self.ctrls['sigma'].value()) class Derivative(CtrlNode): """Returns the pointwise derivative of the input""" diff --git a/pyqtgraph/flowchart/library/functions.py b/pyqtgraph/flowchart/library/functions.py index 338d25c4..cb7fb41a 100644 --- a/pyqtgraph/flowchart/library/functions.py +++ b/pyqtgraph/flowchart/library/functions.py @@ -1,5 +1,7 @@ import numpy as np from ...metaarray import MetaArray +from ...python2_3 import basestring, xrange + def downsample(data, n, axis=0, xvals='subsample'): """Downsample by averaging points together across axis. diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 897a123d..839720d1 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -6,8 +6,18 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from __future__ import division -from .python2_3 import asUnicode +import warnings +import numpy as np +import decimal, re +import ctypes +import sys, struct +from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, USE_PYSIDE +from . import getConfigOption, setConfigOptions +from . import debug + + + Colors = { 'b': QtGui.QColor(0,0,255,255), 'g': QtGui.QColor(0,255,0,255), @@ -24,18 +34,13 @@ Colors = { SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' +SI_PREFIX_EXPONENTS = dict([(SI_PREFIXES[i], (i-8)*3) for i in range(len(SI_PREFIXES))]) +SI_PREFIX_EXPONENTS['u'] = -6 +FLOAT_REGEX = re.compile(r'(?P[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P[u' + SI_PREFIXES + r']?)(?P\w.*))?$') +INT_REGEX = re.compile(r'(?P[+-]?\d+)\s*(?P[u' + SI_PREFIXES + r']?)(?P.*)$') - -from .Qt import QtGui, QtCore, USE_PYSIDE -from . import getConfigOption, setConfigOptions -import numpy as np -import decimal, re -import ctypes -import sys, struct - -from . import debug - + def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. @@ -74,6 +79,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): return (p, pref) + def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): """ Return the number x formatted in engineering notation with SI prefix. @@ -102,31 +108,56 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al plusminus = " +/- " fmt = "%." + str(precision) + "g%s%s%s%s" return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) + + +def siParse(s, regex=FLOAT_REGEX): + """Convert a value written in SI notation to a tuple (number, si_prefix, suffix). -def siEval(s): + Example:: + + siParse('100 μV") # returns ('100', 'μ', 'V') """ - Convert a value written in SI notation to its equivalent prefixless value + s = asUnicode(s) + m = regex.match(s) + if m is None: + raise ValueError('Cannot parse number "%s"' % s) + try: + sip = m.group('siPrefix') + except IndexError: + sip = '' + try: + suf = m.group('suffix') + except IndexError: + suf = '' + + return m.group('number'), '' if sip is None else sip, '' if suf is None else suf + + +def siEval(s, typ=float, regex=FLOAT_REGEX): + """ + Convert a value written in SI notation to its equivalent prefixless value. + Example:: siEval("100 μV") # returns 0.0001 """ + val, siprefix, suffix = siParse(s, regex) + v = typ(val) + return siApply(val, siprefix) + - s = asUnicode(s) - m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?).*$', s) - if m is None: - raise Exception("Can't convert string '%s' to number." % s) - v = float(m.groups()[0]) - p = m.groups()[6] - #if p not in SI_PREFIXES: - #raise Exception("Can't convert string '%s' to number--unknown prefix." % s) - if p == '': - n = 0 - elif p == 'u': - n = -2 +def siApply(val, siprefix): + """ + """ + n = SI_PREFIX_EXPONENTS[siprefix] if siprefix != '' else 0 + if n > 0: + return val * 10**n + elif n < 0: + # this case makes it possible to use Decimal objects here + return val / 10**-n else: - n = SI_PREFIXES.index(p) - 8 - return v * 1000**n + return val class Color(QtGui.QColor): @@ -243,6 +274,7 @@ def mkBrush(*args, **kwds): color = args return QtGui.QBrush(mkColor(color)) + def mkPen(*args, **kargs): """ Convenience function for constructing QPen. @@ -292,6 +324,7 @@ def mkPen(*args, **kargs): pen.setDashPattern(dash) return pen + def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): """Generate a QColor from HSVa values. (all arguments are float 0.0-1.0)""" c = QtGui.QColor() @@ -303,10 +336,12 @@ def colorTuple(c): """Return a tuple (R,G,B,A) from a QColor""" return (c.red(), c.green(), c.blue(), c.alpha()) + def colorStr(c): """Generate a hex string code from a QColor""" return ('%02x'*4) % colorTuple(c) + def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): """ Creates a QColor from a single index. Useful for stepping through a predefined list of colors. @@ -331,6 +366,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi c.setAlpha(alpha) return c + def glColor(*args, **kargs): """ Convert a color to OpenGL color format (r,g,b,a) floats 0.0-1.0 @@ -367,12 +403,80 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) return path +def eq(a, b): + """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" + if a is b: + return True + + try: + with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10) + e = a==b + except ValueError: + return False + except AttributeError: + return False + except: + print('failed to evaluate equivalence for:') + print(" a:", str(type(a)), str(a)) + print(" b:", str(type(b)), str(b)) + raise + t = type(e) + if t is bool: + return e + elif t is np.bool_: + return bool(e) + elif isinstance(e, np.ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')): + try: ## disaster: if a is an empty array and b is not, then e.all() is True + if a.shape != b.shape: + return False + except: + return False + if (hasattr(e, 'implements') and e.implements('MetaArray')): + return e.asarray().all() + else: + return e.all() + else: + raise Exception("== operator returned type %s" % str(type(e))) + + +def affineSliceCoords(shape, origin, vectors, axes): + """Return the array of coordinates used to sample data arrays in affineSlice(). + """ + # sanity check + if len(shape) != len(vectors): + raise Exception("shape and vectors must have same length.") + if len(origin) != len(axes): + raise Exception("origin and axes must have same length.") + for v in vectors: + if len(v) != len(axes): + raise Exception("each vector must be same length as axes.") + + shape = list(map(np.ceil, shape)) + + ## make sure vectors are arrays + if not isinstance(vectors, np.ndarray): + vectors = np.array(vectors) + if not isinstance(origin, np.ndarray): + origin = np.array(origin) + origin.shape = (len(axes),) + (1,)*len(shape) + + ## Build array of sample locations. + grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes + x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic + x += origin + + return x + def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): """ - Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. + Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays + such as MRI images for viewing as 1D or 2D data. - The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used. + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is + possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger + datasets. The original data is interpolated onto a new array of coordinates using either interpolateArray if order<2 + or scipy.ndimage.map_coordinates otherwise. For a graphical interface to this function, see :func:`ROI.getArrayRegion ` @@ -411,49 +515,24 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) """ - try: - import scipy.ndimage - have_scipy = True - except ImportError: - have_scipy = False - have_scipy = False - - # sanity check - if len(shape) != len(vectors): - raise Exception("shape and vectors must have same length.") - if len(origin) != len(axes): - raise Exception("origin and axes must have same length.") - for v in vectors: - if len(v) != len(axes): - raise Exception("each vector must be same length as axes.") - - shape = list(map(np.ceil, shape)) + x = affineSliceCoords(shape, origin, vectors, axes) ## transpose data so slice axes come first trAx = list(range(data.ndim)) - for x in axes: - trAx.remove(x) + for ax in axes: + trAx.remove(ax) tr1 = tuple(axes) + tuple(trAx) data = data.transpose(tr1) #print "tr1:", tr1 ## dims are now [(slice axes), (other axes)] - - ## make sure vectors are arrays - if not isinstance(vectors, np.ndarray): - vectors = np.array(vectors) - if not isinstance(origin, np.ndarray): - origin = np.array(origin) - origin.shape = (len(axes),) + (1,)*len(shape) - - ## Build array of sample locations. - grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes - #print shape, grid.shape - x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic - x += origin - #print "X values:" - #print x - ## iterate manually over unused axes since map_coordinates won't do it for us - if have_scipy: + + if order > 1: + try: + import scipy.ndimage + except ImportError: + raise ImportError("Interpolating with order > 1 requires the scipy.ndimage module, but it could not be imported.") + + # iterate manually over unused axes since map_coordinates won't do it for us extraShape = data.shape[len(axes):] output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) for inds in np.ndindex(*extraShape): @@ -462,9 +541,8 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, else: # map_coordinates expects the indexes as the first axis, whereas # interpolateArray expects indexes at the last axis. - tr = tuple(range(1,x.ndim)) + (0,) - output = interpolateArray(data, x.transpose(tr)) - + tr = tuple(range(1, x.ndim)) + (0,) + output = interpolateArray(data, x.transpose(tr), order=order) tr = list(range(output.ndim)) trb = [] @@ -481,18 +559,26 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, else: return output -def interpolateArray(data, x, default=0.0): + +def interpolateArray(data, x, default=0.0, order=1): """ - N-dimensional interpolation similar scipy.ndimage.map_coordinates. + N-dimensional interpolation similar to scipy.ndimage.map_coordinates. This function returns linearly-interpolated values sampled from a regular - grid of data. + grid of data. It differs from `ndimage.map_coordinates` by allowing broadcasting + within the input array. - *data* is an array of any shape containing the values to be interpolated. - *x* is an array with (shape[-1] <= data.ndim) containing the locations - within *data* to interpolate. + ============== =========================================================================================== + **Arguments:** + *data* Array of any shape containing the values to be interpolated. + *x* Array with (shape[-1] <= data.ndim) containing the locations within *data* to interpolate. + (note: the axes for this argument are transposed relative to the same argument for + `ndimage.map_coordinates`). + *default* Value to return for locations in *x* that are outside the bounds of *data*. + *order* Order of interpolation: 0=nearest, 1=linear. + ============== =========================================================================================== - Returns array of shape (x.shape[:-1] + data.shape) + Returns array of shape (x.shape[:-1] + data.shape[x.shape[-1]:]) For example, assume we have the following 2D image data:: @@ -535,58 +621,75 @@ def interpolateArray(data, x, default=0.0): This is useful for interpolating from arrays of colors, vertexes, etc. """ - + if order not in (0, 1): + raise ValueError("interpolateArray requires order=0 or 1 (got %s)" % order) + prof = debug.Profiler() - + nd = data.ndim md = x.shape[-1] + if md > nd: + raise TypeError("x.shape[-1] must be less than or equal to data.ndim") - # First we generate arrays of indexes that are needed to - # extract the data surrounding each point - fields = np.mgrid[(slice(0,2),) * md] - xmin = np.floor(x).astype(int) - xmax = xmin + 1 - indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) - fieldInds = [] totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes - for ax in range(md): - mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1) - # keep track of points that need to be set to default - totalMask &= mask + if order == 0: + xinds = np.round(x).astype(int) # NOTE: for 0.5 this rounds to the nearest *even* number + for ax in range(md): + mask = (xinds[...,ax] >= 0) & (xinds[...,ax] <= data.shape[ax]-1) + xinds[...,ax][~mask] = 0 + # keep track of points that need to be set to default + totalMask &= mask + result = data[tuple([xinds[...,i] for i in range(xinds.shape[-1])])] - # ..and keep track of indexes that are out of bounds - # (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out - # of bounds, but the interpolation will work anyway) - mask &= (xmax[...,ax] < data.shape[ax]) - axisIndex = indexes[...,ax][fields[ax]] - #axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape) - axisIndex[axisIndex < 0] = 0 - axisIndex[axisIndex >= data.shape[ax]] = 0 - fieldInds.append(axisIndex) - prof() + elif order == 1: + # First we generate arrays of indexes that are needed to + # extract the data surrounding each point + fields = np.mgrid[(slice(0,order+1),) * md] + xmin = np.floor(x).astype(int) + xmax = xmin + 1 + indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) + fieldInds = [] + for ax in range(md): + mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1) + # keep track of points that need to be set to default + totalMask &= mask + + # ..and keep track of indexes that are out of bounds + # (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out + # of bounds, but the interpolation will work anyway) + mask &= (xmax[...,ax] < data.shape[ax]) + axisIndex = indexes[...,ax][fields[ax]] + axisIndex[axisIndex < 0] = 0 + axisIndex[axisIndex >= data.shape[ax]] = 0 + fieldInds.append(axisIndex) + prof() + + # Get data values surrounding each requested point + fieldData = data[tuple(fieldInds)] + prof() - # Get data values surrounding each requested point - # fieldData[..., i] contains all 2**nd values needed to interpolate x[i] - fieldData = data[tuple(fieldInds)] - prof() - - ## Interpolate - s = np.empty((md,) + fieldData.shape, dtype=float) - dx = x - xmin - # reshape fields for arithmetic against dx - for ax in range(md): - f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1)) - sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax]) - sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim)) - s[ax] = sax - s = np.product(s, axis=0) - result = fieldData * s - for i in range(md): - result = result.sum(axis=0) + ## Interpolate + s = np.empty((md,) + fieldData.shape, dtype=float) + dx = x - xmin + # reshape fields for arithmetic against dx + for ax in range(md): + f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1)) + sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax]) + sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim)) + s[ax] = sax + s = np.product(s, axis=0) + result = fieldData * s + for i in range(md): + result = result.sum(axis=0) prof() - totalMask.shape = totalMask.shape + (1,) * (nd - md) - result[~totalMask] = default + + if totalMask.ndim > 0: + result[~totalMask] = default + else: + if totalMask is False: + result[:] = default + prof() return result @@ -773,12 +876,11 @@ def solveBilinearTransform(points1, points2): return matrix -def rescaleData(data, scale, offset, dtype=None): +def rescaleData(data, scale, offset, dtype=None, clip=None): """Return data rescaled and optionally cast to a new dtype:: data => (data-offset) * scale - Uses scipy.weave (if available) to improve performance. """ if dtype is None: dtype = data.dtype @@ -823,9 +925,21 @@ def rescaleData(data, scale, offset, dtype=None): setConfigOptions(useWeave=False) #p = np.poly1d([scale, -offset*scale]) - #data = p(data).astype(dtype) - d2 = data-offset + #d2 = p(data) + d2 = data - float(offset) d2 *= scale + + # Clip before converting dtype to avoid overflow + if dtype.kind in 'ui': + lim = np.iinfo(dtype) + if clip is None: + # don't let rescale cause integer overflow + d2 = np.clip(d2, lim.min, lim.max) + else: + d2 = np.clip(d2, max(clip[0], lim.min), min(clip[1], lim.max)) + else: + if clip is not None: + d2 = np.clip(d2, *clip) data = d2.astype(dtype) return data @@ -847,15 +961,18 @@ def makeRGBA(*args, **kwds): kwds['useRGBA'] = True return makeARGB(*args, **kwds) + def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): """ - Convert an array of values into an ARGB array suitable for building QImages, OpenGL textures, etc. + Convert an array of values into an ARGB array suitable for building QImages, + OpenGL textures, etc. - Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. - This is a two stage process: + Returns the ARGB array (unsigned byte) and a boolean indicating whether + there is alpha channel data. This is a two stage process: 1) Rescale the data based on the values in the *levels* argument (min, max). - 2) Determine the final output by passing the rescaled values through a lookup table. + 2) Determine the final output by passing the rescaled values through a + lookup table. Both stages are optional. @@ -874,55 +991,70 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. scale The maximum value to which data will be rescaled before being passed through the lookup table (or returned if there is no lookup table). By default this will - be set to the length of the lookup table, or 256 is no lookup table is provided. - For OpenGL color specifications (as in GLColor4f) use scale=1.0 + be set to the length of the lookup table, or 255 if no lookup table is provided. lut Optional lookup table (array with dtype=ubyte). Values in data will be converted to color by indexing directly from lut. The output data shape will be input.shape + lut.shape[1:]. - - Note: the output of makeARGB will have the same dtype as the lookup table, so - for conversion to QImage, the dtype must be ubyte. - - Lookup tables can be built using GradientWidget. + Lookup tables can be built using ColorMap or GradientWidget. useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is False, which returns in ARGB order for use with QImage - (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order + (Note that 'ARGB' is a term used by the Qt documentation; the *actual* order is BGRA). ============== ================================================================================== """ 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: + raise TypeError("data.shape[2] must be <= 4") if lut is not None and not isinstance(lut, np.ndarray): lut = np.array(lut) - if levels is not None and not isinstance(levels, np.ndarray): - levels = np.array(levels) - if levels is not None: - if levels.ndim == 1: - if len(levels) != 2: - raise Exception('levels argument must have length 2') - elif levels.ndim == 2: - if lut is not None and lut.ndim > 1: - raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2') - if levels.shape != (data.shape[-1], 2): - raise Exception('levels must have shape (data.shape[-1], 2)') + if levels is None: + # automatically decide levels based on data dtype + if data.dtype.kind == 'u': + levels = np.array([0, 2**(data.itemsize*8)-1]) + elif data.dtype.kind == 'i': + s = 2**(data.itemsize*8 - 1) + levels = np.array([-s, s-1]) + elif data.dtype.kind == 'b': + levels = np.array([0,1]) else: - print(levels) - raise Exception("levels argument must be 1D or 2D.") + raise Exception('levels argument is required for float input types') + if not isinstance(levels, np.ndarray): + levels = np.array(levels) + if levels.ndim == 1: + if levels.shape[0] != 2: + raise Exception('levels argument must have length 2') + elif levels.ndim == 2: + if lut is not None and lut.ndim > 1: + raise Exception('Cannot make ARGB data when both levels and lut have ndim > 2') + if levels.shape != (data.shape[-1], 2): + raise Exception('levels must have shape (data.shape[-1], 2)') + else: + raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape)) profile() + # Decide on maximum scaled value if scale is None: if lut is not None: - scale = lut.shape[0] + scale = lut.shape[0] - 1 else: scale = 255. - ## Apply levels if given + # Decide on the dtype we want after scaling + if lut is None: + dtype = np.ubyte + else: + dtype = np.min_scalar_type(lut.shape[0]-1) + + # Apply levels if given if levels is not None: - if isinstance(levels, np.ndarray) and levels.ndim == 2: - ## we are going to rescale each channel independently + # we are going to rescale each channel independently if levels.shape[0] != data.shape[-1]: raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])") newData = np.empty(data.shape, dtype=int) @@ -930,20 +1062,20 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): minVal, maxVal = levels[i] if minVal == maxVal: maxVal += 1e-16 - newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int) + newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype) data = newData else: + # Apply level scaling unless it would have no effect on the data minVal, maxVal = levels - if minVal == maxVal: - maxVal += 1e-16 - if maxVal == minVal: - data = rescaleData(data, 1, minVal, dtype=int) - else: - data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) + if minVal != 0 or maxVal != scale: + if minVal == maxVal: + maxVal += 1e-16 + data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) + profile() - ## apply LUT if given + # apply LUT if given if lut is not None: data = applyLookupTable(data, lut) else: @@ -952,16 +1084,18 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): profile() - ## copy data into ARGB ordered array + # this will be the final image array imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) profile() + # decide channel order if useRGBA: - order = [0,1,2,3] ## array comes out RGBA + order = [0,1,2,3] # array comes out RGBA else: - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + order = [2,1,0,3] # for some reason, the colors line up as BGR in the final image. + # copy data into image array if data.ndim == 2: # This is tempting: # imgData[..., :3] = data[..., np.newaxis] @@ -976,7 +1110,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): imgData[..., i] = data[..., order[i]] profile() - + + # add opaque alpha channel if needed if data.ndim == 2 or data.shape[2] == 3: alpha = False imgData[..., 3] = 255 @@ -1106,10 +1241,9 @@ def imageToArray(img, copy=False, transpose=True): # If this works on all platforms, then there is no need to use np.asarray.. arr = np.frombuffer(ptr, np.ubyte, img.byteCount()) + arr = arr.reshape(img.height(), img.width(), 4) if fmt == img.Format_RGB32: - arr = arr.reshape(img.height(), img.width(), 3) - elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: - arr = arr.reshape(img.height(), img.width(), 4) + arr[...,3] = 255 if copy: arr = arr.copy() @@ -1304,22 +1438,17 @@ def arrayToQPath(x, y, connect='all'): arr[1:-1]['y'] = y # decide which points are connected by lines - if connect == 'pairs': - connect = np.empty((n/2,2), dtype=np.int32) - if connect.size != n: - raise Exception("x,y array lengths must be multiple of 2 to use connect='pairs'") - connect[:,0] = 1 - connect[:,1] = 0 - connect = connect.flatten() - if connect == 'finite': - connect = np.isfinite(x) & np.isfinite(y) - arr[1:-1]['c'] = connect - if connect == 'all': + if eq(connect, 'all'): arr[1:-1]['c'] = 1 + elif eq(connect, 'pairs'): + arr[1:-1]['c'][::2] = 1 + arr[1:-1]['c'][1::2] = 0 + elif eq(connect, 'finite'): + arr[1:-1]['c'] = np.isfinite(x) & np.isfinite(y) elif isinstance(connect, np.ndarray): arr[1:-1]['c'] = connect else: - raise Exception('connect argument must be "all", "pairs", or array') + raise Exception('connect argument must be "all", "pairs", "finite", or array') #profiler('fill array') # write last 0 @@ -1510,7 +1639,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme vertIndex = i+2*j #print i,j,k," : ", fields[i,j,k], 2**vertIndex - index += fields[i,j] * 2**vertIndex + np.add(index, fields[i,j] * 2**vertIndex, out=index, casting='unsafe') #print index #print index @@ -1660,7 +1789,7 @@ def isosurface(data, level): See Paul Bourke, "Polygonising a Scalar Field" (http://paulbourke.net/geometry/polygonise/) - *data* 3D numpy array of scalar values + *data* 3D numpy array of scalar values. Must be contiguous. *level* The level at which to generate an isosurface Returns an array of vertex coordinates (Nv, 3) and an array of @@ -2012,7 +2141,10 @@ def isosurface(data, level): else: faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache - + # We use strides below, which means we need contiguous array input. + # Ideally we can fix this just by removing the dependency on strides. + if not data.flags['C_CONTIGUOUS']: + raise TypeError("isosurface input data must be c-contiguous.") ## mark everything below the isosurface level mask = data < level @@ -2026,7 +2158,7 @@ def isosurface(data, level): for k in [0,1]: fields[i,j,k] = mask[slices[i], slices[j], slices[k]] vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme - index += fields[i,j,k] * 2**vertIndex + np.add(index, fields[i,j,k] * 2**vertIndex, out=index, casting='unsafe') ### Generate table of edges that have been cut cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32) @@ -2095,7 +2227,7 @@ def isosurface(data, level): ### expensive: verts = faceShiftTables[i][cellInds] #profiler() - verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges + np.add(verts[...,:3], cells[:,np.newaxis,np.newaxis,:], out=verts[...,:3], casting='unsafe') ## we now have indexes into cutEdges verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) #profiler() diff --git a/pyqtgraph/graphicsItems/CurvePoint.py b/pyqtgraph/graphicsItems/CurvePoint.py index bb6beebc..f7682a43 100644 --- a/pyqtgraph/graphicsItems/CurvePoint.py +++ b/pyqtgraph/graphicsItems/CurvePoint.py @@ -91,6 +91,11 @@ class CurvePoint(GraphicsObject): pass def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): + # In Python 3, a bytes object needs to be used as a property name in + # QPropertyAnimation. PyQt stopped automatically encoding a str when a + # QByteArray was expected in v5.5 (see qbytearray.sip). + if not isinstance(prop, bytes): + prop = prop.encode('latin-1') anim = QtCore.QPropertyAnimation(self, prop) anim.setDuration(duration) anim.setStartValue(start) diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index 15a14f86..0efb11dd 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui +from ..Qt import QtGui, USE_PYQT5, USE_PYQT4, USE_PYSIDE from .. import functions as fn from .PlotDataItem import PlotDataItem from .PlotCurveItem import PlotCurveItem @@ -7,17 +7,24 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): """ GraphicsItem filling the space between two PlotDataItems. """ - def __init__(self, curve1=None, curve2=None, brush=None): + def __init__(self, curve1=None, curve2=None, brush=None, pen=None): QtGui.QGraphicsPathItem.__init__(self) self.curves = None if curve1 is not None and curve2 is not None: self.setCurves(curve1, curve2) elif curve1 is not None or curve2 is not None: raise Exception("Must specify two curves to fill between.") - + if brush is not None: - self.setBrush(fn.mkBrush(brush)) + self.setBrush(brush) + self.setPen(pen) self.updatePath() + + def setBrush(self, *args, **kwds): + QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds)) + + def setPen(self, *args, **kwds): + QtGui.QGraphicsPathItem.setPen(self, fn.mkPen(*args, **kwds)) def setCurves(self, curve1, curve2): """Set the curves to fill between. @@ -26,14 +33,13 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): Added in version 0.9.9 """ - if self.curves is not None: for c in self.curves: try: c.sigPlotChanged.disconnect(self.curveChanged) except (TypeError, RuntimeError): pass - + curves = [curve1, curve2] for c in curves: if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem): @@ -43,7 +49,7 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): curve2.sigPlotChanged.connect(self.curveChanged) self.setZValue(min(curve1.zValue(), curve2.zValue())-1) self.curveChanged() - + def setBrush(self, *args, **kwds): """Change the fill brush. Acceps the same arguments as pg.mkBrush()""" QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds)) @@ -61,13 +67,17 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): paths.append(c.curve.getPath()) elif isinstance(c, PlotCurveItem): paths.append(c.getPath()) - + path = QtGui.QPainterPath() - p1 = paths[0].toSubpathPolygons() - p2 = paths[1].toReversed().toSubpathPolygons() - if len(p1) == 0 or len(p2) == 0: + transform = QtGui.QTransform() + ps1 = paths[0].toSubpathPolygons(transform) + ps2 = paths[1].toReversed().toSubpathPolygons(transform) + ps2.reverse() + if len(ps1) == 0 or len(ps2) == 0: self.setPath(QtGui.QPainterPath()) return + - path.addPolygon(p1[0] + p2[0]) + for p1, p2 in zip(ps1, ps2): + path.addPolygon(p1 + p2) self.setPath(path) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index a151798a..f359ff11 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -1,18 +1,18 @@ +import weakref +import numpy as np from ..Qt import QtGui, QtCore from ..python2_3 import sortList from .. import functions as fn from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget from ..widgets.SpinBox import SpinBox -import weakref from ..pgcollections import OrderedDict from ..colormap import ColorMap +from ..python2_3 import cmp -import numpy as np __all__ = ['TickSliderItem', 'GradientEditorItem'] - Gradients = OrderedDict([ ('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), ('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}), @@ -24,7 +24,13 @@ Gradients = OrderedDict([ ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), ]) - +def addGradientListToDocstring(): + """Decorator to add list of current pre-defined gradients to the end of a function docstring.""" + def dec(fn): + if fn.__doc__ is not None: + fn.__doc__ = fn.__doc__ + str(Gradients.keys()).strip('[').strip(']') + return fn + return dec @@ -117,16 +123,20 @@ class TickSliderItem(GraphicsWidget): self.resetTransform() ort = orientation if ort == 'top': - self.scale(1, -1) - self.translate(0, -self.height()) + transform = QtGui.QTransform.fromScale(1, -1) + transform.translate(0, -self.height()) + self.setTransform(transform) elif ort == 'left': - self.rotate(270) - self.scale(1, -1) - self.translate(-self.height(), -self.maxDim) + transform = QtGui.QTransform() + transform.rotate(270) + transform.scale(1, -1) + transform.translate(-self.height(), -self.maxDim) + self.setTransform(transform) elif ort == 'right': - self.rotate(270) - self.translate(-self.height(), 0) - #self.setPos(0, -self.height()) + transform = QtGui.QTransform() + transform.rotate(270) + transform.translate(-self.height(), 0) + self.setTransform(transform) elif ort != 'bottom': raise Exception("%s is not a valid orientation. Options are 'left', 'right', 'top', and 'bottom'" %str(ort)) @@ -238,7 +248,7 @@ class TickSliderItem(GraphicsWidget): self.addTick(pos.x()/self.length) elif ev.button() == QtCore.Qt.RightButton: self.showMenu(ev) - + #if ev.button() == QtCore.Qt.RightButton: #if self.moving: #ev.accept() @@ -468,11 +478,12 @@ class GradientEditorItem(TickSliderItem): act = self.sender() self.loadPreset(act.name) + @addGradientListToDocstring() def loadPreset(self, name): """ - Load a predefined gradient. - - """ ## TODO: provide image with names of defined gradients + Load a predefined gradient. Currently defined gradients are: + """## TODO: provide image with names of defined gradients + #global Gradients self.restoreState(Gradients[name]) @@ -783,11 +794,15 @@ class GradientEditorItem(TickSliderItem): self.updateGradient() self.sigGradientChangeFinished.emit(self) - -class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsObject instead results in + +class Tick(QtGui.QGraphicsWidget): ## NOTE: Making this a subclass of GraphicsObject instead results in ## activating this bug: https://bugreports.qt-project.org/browse/PYSIDE-86 ## private class - + + # When making Tick a subclass of QtGui.QGraphicsObject as origin, + # ..GraphicsScene.items(self, *args) will get Tick object as a + # class of QtGui.QMultimediaWidgets.QGraphicsVideoItem in python2.7-PyQt5(5.4.0) + sigMoving = QtCore.Signal(object) sigMoved = QtCore.Signal(object) @@ -805,7 +820,7 @@ class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsO self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale)) self.pg.closeSubpath() - QtGui.QGraphicsObject.__init__(self) + QtGui.QGraphicsWidget.__init__(self) self.setPos(pos[0], pos[1]) if self.movable: self.setZValue(1) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 2ca35193..d45818dc 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -37,9 +37,6 @@ class GraphicsItem(object): if register: GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() - - - def getViewWidget(self): """ Return the view widget for this item. @@ -95,7 +92,6 @@ class GraphicsItem(object): def forgetViewBox(self): self._viewBox = None - def deviceTransform(self, viewportTransform=None): """ Return the transform that converts local item coordinates to device coordinates (usually pixels). diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index b8325736..6ec38fb5 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -160,4 +160,12 @@ class GraphicsLayout(GraphicsWidget): for i in list(self.items.keys()): self.removeItem(i) + def setContentsMargins(self, *args): + # Wrap calls to layout. This should happen automatically, but there + # seems to be a Qt bug: + # http://stackoverflow.com/questions/27092164/margins-in-pyqtgraphs-graphicslayout + self.layout.setContentsMargins(*args) + def setSpacing(self, *args): + self.layout.setSpacing(*args) + \ No newline at end of file diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6a915902..31764250 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -49,7 +49,7 @@ class HistogramLUTItem(GraphicsWidget): self.setLayout(self.layout) self.layout.setContentsMargins(1,1,1,1) self.layout.setSpacing(0) - self.vb = ViewBox() + self.vb = ViewBox(parent=self) self.vb.setMaximumWidth(152) self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=False, y=True) @@ -59,7 +59,7 @@ class HistogramLUTItem(GraphicsWidget): self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.region.setZValue(1000) self.vb.addItem(self.region) - self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.gradient, 0, 2) @@ -139,6 +139,9 @@ class HistogramLUTItem(GraphicsWidget): #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): + """Set an ImageItem to have its levels and LUT automatically controlled + by this HistogramLUTItem. + """ self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result @@ -163,6 +166,9 @@ class HistogramLUTItem(GraphicsWidget): self.sigLookupTableChanged.emit(self) def getLookupTable(self, img=None, n=None, alpha=None): + """Return a lookup table from the color gradient defined by this + HistogramLUTItem. + """ if n is None: if img.dtype == np.uint8: n = 256 @@ -173,8 +179,8 @@ class HistogramLUTItem(GraphicsWidget): return self.lut def regionChanged(self): - #if self.imageItem is not None: - #self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelChangeFinished.emit(self) #self.update() @@ -199,7 +205,11 @@ class HistogramLUTItem(GraphicsWidget): profiler('set region') def getLevels(self): + """Return the min and max levels. + """ return self.region.getRegion() def setLevels(self, mn, mx): + """Set the min and max levels. + """ self.region.setRegion([mn, mx]) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 5b041433..3d45ad77 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -7,6 +7,8 @@ from .. import functions as fn from .. import debug as debug from .GraphicsObject import GraphicsObject from ..Point import Point +from .. import getConfigOption + __all__ = ['ImageItem'] @@ -28,7 +30,6 @@ class ImageItem(GraphicsObject): for controlling the levels and lookup table used to display the image. """ - sigImageChanged = QtCore.Signal() sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu @@ -47,6 +48,12 @@ class ImageItem(GraphicsObject): self.lut = None self.autoDownsample = False + self.axisOrder = getConfigOption('imageAxisOrder') + + # In some cases, we use a modified lookup table to handle both rescaling + # and LUT more efficiently + self._effectiveLut = None + self.drawKernel = None self.border = None self.removable = False @@ -74,11 +81,6 @@ class ImageItem(GraphicsObject): """ self.paintMode = mode self.update() - - ## use setOpacity instead. - #def setAlpha(self, alpha): - #self.setOpacity(alpha) - #self.updateImage() def setBorder(self, b): self.border = fn.mkPen(b) @@ -87,28 +89,20 @@ class ImageItem(GraphicsObject): def width(self): if self.image is None: return None - return self.image.shape[0] + axis = 0 if self.axisOrder == 'col-major' else 1 + return self.image.shape[axis] def height(self): if self.image is None: return None - return self.image.shape[1] + axis = 1 if self.axisOrder == 'col-major' else 0 + return self.image.shape[axis] def boundingRect(self): if self.image is None: return QtCore.QRectF(0., 0., 0., 0.) return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) - #def setClipLevel(self, level=None): - #self.clipLevel = level - #self.updateImage() - - #def paint(self, p, opt, widget): - #pass - #if self.pixmap is not None: - #p.drawPixmap(0, 0, self.pixmap) - #print "paint" - def setLevels(self, levels, update=True): """ Set image scaling levels. Can be one of: @@ -119,9 +113,13 @@ class ImageItem(GraphicsObject): Only the first format is compatible with lookup tables. See :func:`makeARGB ` for more details on how levels are applied. """ - self.levels = levels - if update: - self.updateImage() + if levels is not None: + levels = np.asarray(levels) + if not fn.eq(levels, self.levels): + self.levels = levels + self._effectiveLut = None + if update: + self.updateImage() def getLevels(self): return self.levels @@ -137,9 +135,11 @@ class ImageItem(GraphicsObject): Ordinarily, this table is supplied by a :class:`HistogramLUTItem ` or :class:`GradientEditorItem `. """ - self.lut = lut - if update: - self.updateImage() + if lut is not self.lut: + self.lut = lut + self._effectiveLut = None + if update: + self.updateImage() def setAutoDownsample(self, ads): """ @@ -152,7 +152,11 @@ class ImageItem(GraphicsObject): self.update() def setOpts(self, update=True, **kargs): - + if 'axisOrder' in kargs: + val = kargs['axisOrder'] + if val not in ('row-major', 'col-major'): + raise ValueError('axisOrder must be either "row-major" or "col-major"') + self.axisOrder = val if 'lut' in kargs: self.setLookupTable(kargs['lut'], update=update) if 'levels' in kargs: @@ -195,7 +199,7 @@ class ImageItem(GraphicsObject): image (numpy array) Specifies the image data. May be 2D (width, height) or 3D (width, height, RGBa). The array dtype must be integer or floating point of any bit depth. For 3D arrays, the third dimension must - be of length 3 (RGB) or 4 (RGBA). + be of length 3 (RGB) or 4 (RGBA). See *notes* below. autoLevels (bool) If True, this forces the image to automatically select levels based on the maximum and minimum values in the data. By default, this argument is true unless the levels argument is @@ -206,12 +210,26 @@ class ImageItem(GraphicsObject): data. By default, this will be set to the minimum and maximum values in the image. If the image array has dtype uint8, no rescaling is necessary. opacity (float 0.0-1.0) - compositionMode see :func:`setCompositionMode ` + compositionMode See :func:`setCompositionMode ` border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the screen resolution. This improves performance for large images and reduces aliasing. ================= ========================================================================= + + + **Notes:** + + For backward compatibility, image data is assumed to be in column-major order (column, row). + However, most image data is stored in row-major order (row, column) and will need to be + transposed before calling setImage():: + + imageitem.setImage(imagedata.T) + + This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or + by changing the ``imageAxisOrder`` :ref:`global configuration option `. + + """ profile = debug.Profiler() @@ -222,7 +240,10 @@ class ImageItem(GraphicsObject): else: gotNewData = True shapeChanged = (self.image is None or image.shape != self.image.shape) - self.image = image.view(np.ndarray) + image = image.view(np.ndarray) + if self.image is None or image.dtype != self.image.dtype: + self._effectiveLut = None + self.image = image if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1: if 'autoDownsample' not in kargs: kargs['autoDownsample'] = True @@ -261,6 +282,53 @@ class ImageItem(GraphicsObject): if gotNewData: self.sigImageChanged.emit() + def dataTransform(self): + """Return the transform that maps from this image's input array to its + local coordinate system. + + This transform corrects for the transposition that occurs when image data + is interpreted in row-major order. + """ + # Might eventually need to account for downsampling / clipping here + tr = QtGui.QTransform() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + def inverseDataTransform(self): + """Return the transform that maps from this image's local coordinate + system to its input array. + + See dataTransform() for more information. + """ + tr = QtGui.QTransform() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + def mapToData(self, obj): + tr = self.inverseDataTransform() + return tr.map(obj) + + def mapFromData(self, obj): + tr = self.dataTransform() + return tr.map(obj) + + def quickMinMax(self, targetSize=1e6): + """ + Estimate the min/max values of the image data by subsampling. + """ + data = self.image + while data.size > targetSize: + ax = np.argmax(data.shape) + sl = [slice(None)] * data.ndim + sl[ax] = slice(None, None, 2) + data = data[sl] + return nanmin(data), nanmax(data) def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. @@ -291,14 +359,48 @@ class ImageItem(GraphicsObject): y = self.mapToDevice(QtCore.QPointF(0,1)) w = Point(x-o).length() h = Point(y-o).length() - xds = max(1, int(1/w)) - yds = max(1, int(1/h)) - image = fn.downsample(self.image, xds, axis=0) - image = fn.downsample(image, yds, axis=1) + if w == 0 or h == 0: + self.qimage = None + return + xds = max(1, int(1.0 / w)) + yds = max(1, int(1.0 / h)) + axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1] + image = fn.downsample(self.image, xds, axis=axes[0]) + image = fn.downsample(image, yds, axis=axes[1]) + self._lastDownsample = (xds, yds) else: image = self.image + + # if the image data is a small int, then we can combine levels + lut + # into a single lut for better performance + levels = self.levels + if levels is not None and levels.ndim == 1 and image.dtype in (np.ubyte, np.uint16): + if self._effectiveLut is None: + eflsize = 2**(image.itemsize*8) + ind = np.arange(eflsize) + minlev, maxlev = levels + levdiff = maxlev - minlev + levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0 + if lut is None: + efflut = fn.rescaleData(ind, scale=255./levdiff, + offset=minlev, dtype=np.ubyte) + else: + lutdtype = np.min_scalar_type(lut.shape[0]-1) + efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff, + offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1)) + efflut = lut[efflut] + + self._effectiveLut = efflut + lut = self._effectiveLut + levels = None - argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels) + # Assume images are in column-major order for backward compatibility + # (most images are in row-major order) + + if self.axisOrder == 'col-major': + image = image.transpose((1, 0, 2)[:image.ndim]) + + argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False) def paint(self, p, *args): @@ -314,7 +416,8 @@ class ImageItem(GraphicsObject): p.setCompositionMode(self.paintMode) profile('set comp mode') - p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage) + shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1] + p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage) profile('p.drawImage') if self.border is not None: p.setPen(self.border) @@ -347,8 +450,8 @@ class ImageItem(GraphicsObject): if self.image is None: return None,None if step == 'auto': - step = (np.ceil(self.image.shape[0] / targetImageSize), - np.ceil(self.image.shape[1] / targetImageSize)) + step = (int(np.ceil(self.image.shape[0] / targetImageSize)), + int(np.ceil(self.image.shape[1] / targetImageSize))) if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] @@ -365,6 +468,7 @@ class ImageItem(GraphicsObject): bins = 500 kwds['bins'] = bins + stepData = stepData[np.isfinite(stepData)] hist = np.histogram(stepData, **kwds) return hist[1][:-1], hist[0] @@ -400,21 +504,6 @@ class ImageItem(GraphicsObject): self.qimage = None self.update() - #def mousePressEvent(self, ev): - #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: - #self.drawAt(ev.pos(), ev) - #ev.accept() - #else: - #ev.ignore() - - #def mouseMoveEvent(self, ev): - ##print "mouse move", ev.pos() - #if self.drawKernel is not None: - #self.drawAt(ev.pos(), ev) - - #def mouseReleaseEvent(self, ev): - #pass - def mouseDragEvent(self, ev): if ev.button() != QtCore.Qt.LeftButton: ev.ignore() @@ -451,24 +540,18 @@ class ImageItem(GraphicsObject): self.menu.remAct = remAct return self.menu - def hoverEvent(self, ev): if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. ev.acceptClicks(QtCore.Qt.RightButton) - #self.box.setBrush(fn.mkBrush('w')) elif not ev.isExit() and self.removable: ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks - #else: - #self.box.setBrush(self.brush) - #self.update() - - def tabletEvent(self, ev): - print(ev.device()) - print(ev.pointerType()) - print(ev.pressure()) + pass + #print(ev.device()) + #print(ev.pointerType()) + #print(ev.pressure()) def drawAt(self, pos, ev=None): pos = [int(pos.x()), int(pos.y())] diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 8108c3cf..3da82327 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,19 +1,23 @@ from ..Qt import QtGui, QtCore from ..Point import Point from .GraphicsObject import GraphicsObject +from .TextItem import TextItem +from .ViewBox import ViewBox from .. import functions as fn import numpy as np import weakref -__all__ = ['InfiniteLine'] +__all__ = ['InfiniteLine', 'InfLineLabel'] + + class InfiniteLine(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` - + Displays a line of infinite length. This line may be dragged to indicate a position in data coordinates. - + =============================== =================================================== **Signals:** sigDragged(self) @@ -21,12 +25,13 @@ class InfiniteLine(GraphicsObject): sigPositionChanged(self) =============================== =================================================== """ - + sigDragged = QtCore.Signal(object) sigPositionChangeFinished = QtCore.Signal(object) sigPositionChanged = QtCore.Signal(object) - - def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): + + def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, + hoverPen=None, label=None, labelOpts=None, name=None): """ =============== ================================================================== **Arguments:** @@ -37,13 +42,26 @@ class InfiniteLine(GraphicsObject): for :func:`mkPen `. Default pen is transparent yellow. movable If True, the line can be dragged to a new position by the user. + hoverPen Pen to use when drawing line when hovering over it. Can be any + arguments that are valid for :func:`mkPen `. + Default pen is red. bounds Optional [min, max] bounding values. Bounds are only valid if the line is vertical or horizontal. + label Text to be displayed in a label attached to the line, or + None to show no label (default is None). May optionally + include formatting strings to display the line value. + labelOpts A dict of keyword arguments to use when constructing the + text label. See :class:`InfLineLabel`. + name Name of the item =============== ================================================================== """ - + self._boundingRect = None + self._line = None + + self._name = name + GraphicsObject.__init__(self) - + if bounds is None: ## allowed value boundaries for orthogonal lines self.maxRange = [None, None] else: @@ -53,64 +71,70 @@ class InfiniteLine(GraphicsObject): self.mouseHovering = False self.p = [0, 0] self.setAngle(angle) + if pos is None: pos = Point(0,0) self.setPos(pos) if pen is None: pen = (200, 200, 100) - self.setPen(pen) - self.setHoverPen(color=(255,0,0), width=self.pen.width()) + if hoverPen is None: + self.setHoverPen(color=(255,0,0), width=self.pen.width()) + else: + self.setHoverPen(hoverPen) self.currentPen = self.pen - #self.setFlag(self.ItemSendsScenePositionChanges) - + + if label is not None: + labelOpts = {} if labelOpts is None else labelOpts + self.label = InfLineLabel(self, text=label, **labelOpts) + def setMovable(self, m): """Set whether the line is movable by the user.""" self.movable = m self.setAcceptHoverEvents(m) - + def setBounds(self, bounds): """Set the (minimum, maximum) allowable values when dragging.""" self.maxRange = bounds self.setValue(self.value()) - + def setPen(self, *args, **kwargs): - """Set the pen for drawing the line. Allowable arguments are any that are valid + """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen `.""" self.pen = fn.mkPen(*args, **kwargs) if not self.mouseHovering: self.currentPen = self.pen self.update() - + def setHoverPen(self, *args, **kwargs): - """Set the pen for drawing the line while the mouse hovers over it. - Allowable arguments are any that are valid + """Set the pen for drawing the line while the mouse hovers over it. + Allowable arguments are any that are valid for :func:`mkPen `. - + If the line is not movable, then hovering is also disabled. - + Added in version 0.9.9.""" self.hoverPen = fn.mkPen(*args, **kwargs) if self.mouseHovering: self.currentPen = self.hoverPen self.update() - + def setAngle(self, angle): """ Takes angle argument in degrees. 0 is horizontal; 90 is vertical. - - Note that the use of value() and setValue() changes if the line is + + Note that the use of value() and setValue() changes if the line is not vertical or horizontal. """ self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 self.resetTransform() self.rotate(self.angle) self.update() - + def setPos(self, pos): - + if type(pos) in [list, tuple]: newPos = pos elif isinstance(pos, QtCore.QPointF): @@ -122,10 +146,10 @@ class InfiniteLine(GraphicsObject): newPos = [0, pos] else: raise Exception("Must specify 2D coordinate for non-orthogonal lines.") - + ## check bounds (only works for orthogonal lines) if self.angle == 90: - if self.maxRange[0] is not None: + if self.maxRange[0] is not None: newPos[0] = max(newPos[0], self.maxRange[0]) if self.maxRange[1] is not None: newPos[0] = min(newPos[0], self.maxRange[1]) @@ -134,24 +158,24 @@ class InfiniteLine(GraphicsObject): newPos[1] = max(newPos[1], self.maxRange[0]) if self.maxRange[1] is not None: newPos[1] = min(newPos[1], self.maxRange[1]) - + if self.p != newPos: self.p = newPos + self._invalidateCache() GraphicsObject.setPos(self, Point(self.p)) - self.update() self.sigPositionChanged.emit(self) def getXPos(self): return self.p[0] - + def getYPos(self): return self.p[1] - + def getPos(self): return self.p def value(self): - """Return the value of the line. Will be a single number for horizontal and + """Return the value of the line. Will be a single number for horizontal and vertical lines, and a list of [x,y] values for diagonal lines.""" if self.angle%180 == 0: return self.getYPos() @@ -159,10 +183,10 @@ class InfiniteLine(GraphicsObject): return self.getXPos() else: return self.getPos() - + def setValue(self, v): - """Set the position of the line. If line is horizontal or vertical, v can be - a single value. Otherwise, a 2D coordinate must be specified (list, tuple and + """Set the position of the line. If line is horizontal or vertical, v can be + a single value. Otherwise, a 2D coordinate must be specified (list, tuple and QPointF are all acceptable).""" self.setPos(v) @@ -175,26 +199,35 @@ class InfiniteLine(GraphicsObject): #else: #print "ignore", change #return GraphicsObject.itemChange(self, change, val) - + + def _invalidateCache(self): + self._line = None + self._boundingRect = None + def boundingRect(self): - #br = UIGraphicsItem.boundingRect(self) - br = self.viewRect() - ## add a 4-pixel radius around the line for mouse interaction. - - px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line - if px is None: - px = 0 - w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px - br.setBottom(-w) - br.setTop(w) - return br.normalized() - + if self._boundingRect is None: + #br = UIGraphicsItem.boundingRect(self) + br = self.viewRect() + if br is None: + return QtCore.QRectF() + + ## add a 4-pixel radius around the line for mouse interaction. + px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line + if px is None: + px = 0 + w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px + br.setBottom(-w) + br.setTop(w) + + br = br.normalized() + self._boundingRect = br + self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0) + return self._boundingRect + def paint(self, p, *args): - br = self.boundingRect() p.setPen(self.currentPen) - p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) - #p.drawRect(self.boundingRect()) - + p.drawLine(self._line) + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: return None ## x axis should never be auto-scaled @@ -208,20 +241,16 @@ class InfiniteLine(GraphicsObject): self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) self.startPosition = self.pos() ev.accept() - + if not self.moving: return - - #pressDelta = self.mapToParent(ev.buttonDownPos()) - Point(self.p) + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) self.sigDragged.emit(self) if ev.isFinish(): self.moving = False self.sigPositionChangeFinished.emit(self) - #else: - #print ev - def mouseClickEvent(self, ev): if self.moving and ev.button() == QtCore.Qt.RightButton: ev.accept() @@ -246,30 +275,196 @@ class InfiniteLine(GraphicsObject): else: self.currentPen = self.pen self.update() - - #def hoverEnterEvent(self, ev): - #print "line hover enter" - #ev.ignore() - #self.updateHoverPen() - #def hoverMoveEvent(self, ev): - #print "line hover move" - #ev.ignore() - #self.updateHoverPen() - - #def hoverLeaveEvent(self, ev): - #print "line hover leave" - #ev.ignore() - #self.updateHoverPen(False) + def viewTransformChanged(self): + """ + Called whenever the transformation matrix of the view has changed. + (eg, the view range has changed or the view was resized) + """ + self._invalidateCache() - #def updateHoverPen(self, hover=None): - #if hover is None: - #scene = self.scene() - #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) - - #if hover: - #self.currentPen = fn.mkPen(255, 0,0) - #else: - #self.currentPen = self.pen - #self.update() + def setName(self, name): + self._name = name + def name(self): + return self._name + + +class InfLineLabel(TextItem): + """ + A TextItem that attaches itself to an InfiniteLine. + + This class extends TextItem with the following features: + + * Automatically positions adjacent to the line at a fixed position along + the line and within the view box. + * Automatically reformats text when the line value has changed. + * Can optionally be dragged to change its location along the line. + * Optionally aligns to its parent line. + + =============== ================================================================== + **Arguments:** + line The InfiniteLine to which this label will be attached. + text String to display in the label. May contain a {value} formatting + string to display the current value of the line. + movable Bool; if True, then the label can be dragged along the line. + position Relative position (0.0-1.0) within the view to position the label + along the line. + anchors List of (x,y) pairs giving the text anchor positions that should + be used when the line is moved to one side of the view or the + other. This allows text to switch to the opposite side of the line + as it approaches the edge of the view. These are automatically + selected for some common cases, but may be specified if the + default values give unexpected results. + =============== ================================================================== + + All extra keyword arguments are passed to TextItem. A particularly useful + option here is to use `rotateAxis=(1, 0)`, which will cause the text to + be automatically rotated parallel to the line. + """ + def __init__(self, line, text="", movable=False, position=0.5, anchors=None, **kwds): + self.line = line + self.movable = movable + self.moving = False + self.orthoPos = position # text will always be placed on the line at a position relative to view bounds + self.format = text + self.line.sigPositionChanged.connect(self.valueChanged) + self._endpoints = (None, None) + if anchors is None: + # automatically pick sensible anchors + rax = kwds.get('rotateAxis', None) + if rax is not None: + if tuple(rax) == (1,0): + anchors = [(0.5, 0), (0.5, 1)] + else: + anchors = [(0, 0.5), (1, 0.5)] + else: + if line.angle % 180 == 0: + anchors = [(0.5, 0), (0.5, 1)] + else: + anchors = [(0, 0.5), (1, 0.5)] + + self.anchors = anchors + TextItem.__init__(self, **kwds) + self.setParentItem(line) + self.valueChanged() + + def valueChanged(self): + if not self.isVisible(): + return + value = self.line.value() + self.setText(self.format.format(value=value)) + self.updatePosition() + + def getEndpoints(self): + # calculate points where line intersects view box + # (in line coordinates) + if self._endpoints[0] is None: + lr = self.line.boundingRect() + pt1 = Point(lr.left(), 0) + pt2 = Point(lr.right(), 0) + + if self.line.angle % 90 != 0: + # more expensive to find text position for oblique lines. + view = self.getViewBox() + if not self.isVisible() or not isinstance(view, ViewBox): + # not in a viewbox, skip update + return (None, None) + p = QtGui.QPainterPath() + p.moveTo(pt1) + p.lineTo(pt2) + p = self.line.itemTransform(view)[0].map(p) + vr = QtGui.QPainterPath() + vr.addRect(view.boundingRect()) + paths = vr.intersected(p).toSubpathPolygons(QtGui.QTransform()) + if len(paths) > 0: + l = list(paths[0]) + pt1 = self.line.mapFromItem(view, l[0]) + pt2 = self.line.mapFromItem(view, l[1]) + self._endpoints = (pt1, pt2) + return self._endpoints + + def updatePosition(self): + # update text position to relative view location along line + self._endpoints = (None, None) + pt1, pt2 = self.getEndpoints() + if pt1 is None: + return + pt = pt2 * self.orthoPos + pt1 * (1-self.orthoPos) + self.setPos(pt) + + # update anchor to keep text visible as it nears the view box edge + vr = self.line.viewRect() + if vr is not None: + self.setAnchor(self.anchors[0 if vr.center().y() < 0 else 1]) + + def setVisible(self, v): + TextItem.setVisible(self, v) + if v: + self.updateText() + self.updatePosition() + + def setMovable(self, m): + """Set whether this label is movable by dragging along the line. + """ + self.movable = m + self.setAcceptHoverEvents(m) + + def setPosition(self, p): + """Set the relative position (0.0-1.0) of this label within the view box + and along the line. + + For horizontal (angle=0) and vertical (angle=90) lines, a value of 0.0 + places the text at the bottom or left of the view, respectively. + """ + self.orthoPos = p + self.updatePosition() + + def setFormat(self, text): + """Set the text format string for this label. + + May optionally contain "{value}" to include the lines current value + (the text will be reformatted whenever the line is moved). + """ + self.format = text + self.valueChanged() + + def mouseDragEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self._moving = True + self._cursorOffset = self._posToRel(ev.buttonDownPos()) + self._startPosition = self.orthoPos + ev.accept() + + if not self._moving: + return + + rel = self._posToRel(ev.pos()) + self.orthoPos = np.clip(self._startPosition + rel - self._cursorOffset, 0, 1) + self.updatePosition() + if ev.isFinish(): + self._moving = False + + def mouseClickEvent(self, ev): + if self.moving and ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.orthoPos = self._startPosition + self.moving = False + + def hoverEvent(self, ev): + if not ev.isExit() and self.movable: + ev.acceptDrags(QtCore.Qt.LeftButton) + + def viewTransformChanged(self): + self.updatePosition() + TextItem.viewTransformChanged(self) + + def _posToRel(self, pos): + # convert local position to relative position along line between view bounds + pt1, pt2 = self.getEndpoints() + if pt1 is None: + return 0 + view = self.getViewBox() + pos = self.mapToParent(pos) + return (pos.x() - pt1.x()) / (pt2.x()-pt1.x()) diff --git a/pyqtgraph/graphicsItems/IsocurveItem.py b/pyqtgraph/graphicsItems/IsocurveItem.py index 4474e29a..03ebc69f 100644 --- a/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/pyqtgraph/graphicsItems/IsocurveItem.py @@ -1,5 +1,4 @@ - - +from .. import getConfigOption from .GraphicsObject import * from .. import functions as fn from ..Qt import QtGui, QtCore @@ -9,12 +8,10 @@ class IsocurveItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` - Item displaying an isocurve of a 2D array.To align this item correctly with an - ImageItem,call isocurve.setParentItem(image) + Item displaying an isocurve of a 2D array. To align this item correctly with an + ImageItem, call ``isocurve.setParentItem(image)``. """ - - - def __init__(self, data=None, level=0, pen='w'): + def __init__(self, data=None, level=0, pen='w', axisOrder=None): """ Create a new isocurve item. @@ -25,6 +22,9 @@ class IsocurveItem(GraphicsObject): level The cutoff value at which to draw the isocurve. pen The color of the curve item. Can be anything valid for :func:`mkPen ` + axisOrder May be either 'row-major' or 'col-major'. By default this uses + the ``imageAxisOrder`` + :ref:`global configuration option `. ============== =============================================================== """ GraphicsObject.__init__(self) @@ -32,9 +32,9 @@ class IsocurveItem(GraphicsObject): self.level = level self.data = None self.path = None + self.axisOrder = getConfigOption('imageAxisOrder') if axisOrder is None else axisOrder self.setPen(pen) self.setData(data, level) - def setData(self, data, level=None): """ @@ -54,7 +54,6 @@ class IsocurveItem(GraphicsObject): self.path = None self.prepareGeometryChange() self.update() - def setLevel(self, level): """Set the level at which the isocurve is drawn.""" @@ -62,7 +61,6 @@ class IsocurveItem(GraphicsObject): self.path = None self.prepareGeometryChange() self.update() - def setPen(self, *args, **kwargs): """Set the pen used to draw the isocurve. Arguments can be any that are valid @@ -75,18 +73,8 @@ class IsocurveItem(GraphicsObject): for :func:`mkBrush `""" self.brush = fn.mkBrush(*args, **kwargs) self.update() - def updateLines(self, data, level): - ##print "data:", data - ##print "level", level - #lines = fn.isocurve(data, level) - ##print len(lines) - #self.path = QtGui.QPainterPath() - #for line in lines: - #self.path.moveTo(*line[0]) - #self.path.lineTo(*line[1]) - #self.update() self.setData(data, level) def boundingRect(self): @@ -100,7 +88,13 @@ class IsocurveItem(GraphicsObject): if self.data is None: self.path = None return - lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True) + + if self.axisOrder == 'row-major': + data = self.data.T + else: + data = self.data + + lines = fn.isocurve(data, self.level, connected=True, extendToEdge=True) self.path = QtGui.QPainterPath() for line in lines: self.path.moveTo(*line[0]) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 3d3e969d..d66a8a99 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -126,10 +126,18 @@ class PlotCurveItem(GraphicsObject): ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: + # include complete data range + # first try faster nanmin/max function, then cut out infs if needed. b = (np.nanmin(d), np.nanmax(d)) + if any(np.isinf(b)): + mask = np.isfinite(d) + d = d[mask] + b = (d.min(), d.max()) + elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: + # include a percentile of data range mask = np.isfinite(d) d = d[mask] b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 6148989d..37245bec 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,13 +1,14 @@ +import numpy as np from .. import metaarray as metaarray from ..Qt import QtCore from .GraphicsObject import GraphicsObject from .PlotCurveItem import PlotCurveItem from .ScatterPlotItem import ScatterPlotItem -import numpy as np from .. import functions as fn from .. import debug as debug from .. import getConfigOption + class PlotDataItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` @@ -522,6 +523,10 @@ class PlotDataItem(GraphicsObject): #y = y[::ds] if self.opts['fftMode']: x,y = self._fourierTransform(x, y) + # Ignore the first bin for fft data if we have a logx scale + if self.opts['logMode'][0]: + x=x[1:] + y=y[1:] if self.opts['logMode'][0]: x = np.log10(x) if self.opts['logMode'][1]: @@ -569,11 +574,11 @@ class PlotDataItem(GraphicsObject): x = x[::ds] y = y[::ds] elif self.opts['downsampleMethod'] == 'mean': - n = len(x) / ds + n = len(x) // ds x = x[:n*ds:ds] y = y[:n*ds].reshape(n,ds).mean(axis=1) elif self.opts['downsampleMethod'] == 'peak': - n = len(x) / ds + n = len(x) // ds x1 = np.empty((n,2)) x1[:] = x[:n*ds:ds,np.newaxis] x = x1.reshape(n*2) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f8959e22..41011df3 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -16,20 +16,14 @@ This class is very heavily featured: - Control panel with a huge feature set including averaging, decimation, display, power spectrum, svg/png export, plot linking, and more. """ -from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE -from ... import pixmaps import sys - -if USE_PYSIDE: - from .plotConfigTemplate_pyside import * -else: - from .plotConfigTemplate_pyqt import * - -from ... import functions as fn -from ...widgets.FileDialog import FileDialog import weakref import numpy as np import os +from ...Qt import QtGui, QtCore, QT_LIB +from ... import pixmaps +from ... import functions as fn +from ...widgets.FileDialog import FileDialog from .. PlotDataItem import PlotDataItem from .. ViewBox import ViewBox from .. AxisItem import AxisItem @@ -39,6 +33,14 @@ from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem from .. InfiniteLine import InfiniteLine from ...WidgetGroup import WidgetGroup +from ...python2_3 import basestring + +if QT_LIB == 'PyQt4': + from .plotConfigTemplate_pyqt import * +elif QT_LIB == 'PySide': + from .plotConfigTemplate_pyside import * +elif QT_LIB == 'PyQt5': + from .plotConfigTemplate_pyqt5 import * __all__ = ['PlotItem'] @@ -145,7 +147,7 @@ class PlotItem(GraphicsWidget): self.layout.setVerticalSpacing(0) if viewBox is None: - viewBox = ViewBox() + viewBox = ViewBox(parent=self) self.vb = viewBox self.vb.sigStateChanged.connect(self.viewStateChanged) self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus @@ -168,14 +170,17 @@ class PlotItem(GraphicsWidget): axisItems = {} self.axes = {} for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - axis = axisItems.get(k, AxisItem(orientation=k)) + if k in axisItems: + axis = axisItems[k] + else: + axis = AxisItem(orientation=k, parent=self) axis.linkToView(self.vb) self.axes[k] = {'item': axis, 'pos': pos} self.layout.addItem(axis, *pos) axis.setZValue(-1000) axis.setFlag(axis.ItemNegativeZStacksBehindParent) - self.titleLabel = LabelItem('', size='11pt') + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide @@ -469,12 +474,13 @@ class PlotItem(GraphicsWidget): ### Average data together (x, y) = curve.getData() + stepMode = curve.opts['stepMode'] if plot.yData is not None and y.shape == plot.yData.shape: # note that if shapes do not match, then the average resets. newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) - plot.setData(plot.xData, newData) + plot.setData(plot.xData, newData, stepMode=stepMode) else: - plot.setData(x, y) + plot.setData(x, y, stepMode=stepMode) def autoBtnClicked(self): if self.autoBtn.mode == 'auto': @@ -768,14 +774,6 @@ class PlotItem(GraphicsWidget): y = pos.y() * sy fh.write('\n' % (x, y, color, opacity)) - #fh.write('') - - ## get list of curves, scatter plots - fh.write("\n") @@ -787,42 +785,9 @@ class PlotItem(GraphicsWidget): fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) - self.svg = QtSvg.QSvgGenerator() - self.svg.setFileName(fileName) - res = 120. - view = self.scene().views()[0] - bounds = view.viewport().rect() - bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) - - self.svg.setResolution(res) - self.svg.setViewBox(bounds) - - self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) - - painter = QtGui.QPainter(self.svg) - view.render(painter, bounds) - - painter.end() - - ## Workaround to set pen widths correctly - import re - data = open(fileName).readlines() - for i in range(len(data)): - line = data[i] - m = re.match(r'(