pax_global_header00006660000000000000000000000064135500774410014520gustar00rootroot0000000000000052 comment=22976f950e80516251b5815153c65c493bab2581 taurus-pyqtgraph-0.3.1/000077500000000000000000000000001355007744100150615ustar00rootroot00000000000000taurus-pyqtgraph-0.3.1/.editorconfig000066400000000000000000000004441355007744100175400ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab taurus-pyqtgraph-0.3.1/.gitattributes000066400000000000000000000000141355007744100177470ustar00rootroot00000000000000* text=auto taurus-pyqtgraph-0.3.1/.gitignore000066400000000000000000000022351355007744100170530ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # 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/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ taurus-pyqtgraph-0.3.1/.travis.yml000066400000000000000000000025411355007744100171740ustar00rootroot00000000000000# Config file for automatic testing at travis-ci.org language: python python: - 3.7 - 3.6 - 3.5 matrix: include: - python: 3.6 env: TOXENV=flake8 # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -U tox-travis # Command to run tests, e.g. python setup.py test script: tox # Use travis-encrypt to encrypt the PyPI password for Travis: # travis-encrypt taurus-org taurus_pyqtgraph deploy: provider: pypi distributions: sdist bdist_wheel user: taurus_bot password: secure: KGOmOt3B/ARfJCtDy/JmnTvRz51sK5Y8P0WuZOU93aI1SkrOK7nWz7p7eTTXMuDG7BlH6A7WxyvpHLmvVmdX2H4HSTPPYIJ/0pCd1u2YJi5rU4PDozacQNpVwU5p6jw9zFuuD5e7+cIjOXYnuPvHMxoerkmJsEogJRDQpzwwegvH/mJqj5w33tuweKA6PU4KfRoi2ce8e0r7ALzxKHAAklTCeyWz+WfvhP31wlWOWp0suZGyvYc4mKnA19v/K/hpbByT0EaqFA7Sjkbs0p1m5NknFt0/10fTKJc/jnjeR5Y+1fR0fj7WgU+wKFr+S04wp4Cl6pucLr+Y7ujN7wPH9+TK+pmmTSQRJj1eu7U0jont8BqPPHwwCPdSkAtZqbAHhgYsauWaEETjCE/q+FEKrHyrH9c3W5+1rz7ZrzDpcKwposF+Radx1jCySnyNdRmp/LnNeuwCCt418C58FmyIF/4OwY7NB59zPf4V0op+FAzHYXnP0Gb1hpdr29Cjbg7kSjKl3RQr9RL337Rgc7DOWolawublLMVQZQvl24iswho36MzKXc6b6cY5P9tZYxFFvZOIf3EsFF/7Brjlz4kZ29C97He0pLfCn6DZeFW9vZ9OzyKTTbstcEmB2w2hm6gkRIt4WuNn7umjDbinhzchjIGF5ipLejkepC1z3cGgGn8= on: tags: true condition: "$TRAVIS_TAG =~ ^[0-9]+.[0-9]+.[0-9]+$" repo: taurus-org/taurus_pyqtgraph python: 3.6 taurus-pyqtgraph-0.3.1/CHANGELOG.md000066400000000000000000000011331355007744100166700ustar00rootroot00000000000000# Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This file follows the formats and conventions from [keepachangelog.com] Since changes in this pre-1.0.0 stage of the project will be fast, this changelog won't be properly updated for now. For progress, keep track of the checklist in the `README.md` file. [Unreleased] [keepachangelog.com]: http://keepachangelog.com [TEP17]: https://github.com/taurus-org/taurus/pull/452 [Unreleased]: https://github.com/taurus-org/taurus_pyqtgraph/tree/master taurus-pyqtgraph-0.3.1/CONTRIBUTING.md000077500000000000000000000063711355007744100173240ustar00rootroot00000000000000# Guidelines for Contributing to taurus_pyqtgraph Contributions are always welcome. You can contribute by sending feedback (use our [issue tracker][] for that) or by proposing a code change via a [Pull Request][] The taurus_pyqtgraph repository uses the [GithubFlow][] branching model (i.e. features are developed on individual feature branches and they are eventually merged into the master branch, which is considered always "deployable". For the contributions, we use the [Fork & Pull Model][]: 1. the contributor first [forks][] the official repository 2. the contributor commits changes to a branch based on the `master` branch and pushes it to the forked repository. 3. the contributor creates a [Pull Request][] against the `master` branch of the official repository. 4. anybody interested may review and comment on the Pull Request, and suggest changes to it (even doing Pull Requests against the Pull Request branch). At this point more changes can be committed on the requestor's branch until the result is satisfactory. 5. once the proposed code is considered ready by an appointed taurus integrator, the integrator merges the pull request into `master`. ## Important considerations: In general, the contributions to should consider following: - The code must comply with the [Taurus coding conventions][] - The contributor must be clearly identified. The commit author email should be valid and usable for contacting him/her. - Commit messages should follow the [commit message guidelines][]. Contributions may be rejected if their commit messages are poor. - The licensing terms for the contributed code must be compatible with (and preferably the same as) the license chosen for the Taurus project (at the time of writing this TEP, it is the [LGPL][], version 3 *or later*). ## Notes: - This workflow is slightly different to the one used for the main taurus repo, (which uses [gitflow][]), but the contribution guidelines are very similar. Basically, the role of the `develop` branch in the taurus repo is done by the `master` branch in this repo. - If the contributor wants to explicitly bring the attention of some specific person to the review process, [mentions][] can be used - If a pull request (or a specific commit) fixes an open issue, the pull request (or commit) message may contain a `Fixes #N` tag (N being the number of the issue) which will automatically [close the related Issue][tag_issue_closing] [issue tracker]: https://github.com/taurus-org/taurus_pyqtgraph/issues [gitflow]: http://nvie.com/posts/a-successful-git-branching-model/ [Fork & Pull Model]: https://en.wikipedia.org/wiki/Fork_and_pull_model [forks]: https://help.github.com/articles/fork-a-repo/ [Pull Request]: https://help.github.com/articles/creating-a-pull-request/ [commit message guidelines]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [GitHubFlow]: https://guides.github.com/introduction/flow/index.html [mentions]: https://github.com/blog/821-mention-somebody-they-re-notified [tag_issue_closing]: https://help.github.com/articles/closing-issues-via-commit-messages/ [Taurus coding conventions]: http://taurus-scada.org/devel/coding_guide.html [LGPL]: http://www.gnu.org/licenses/lgpl.html taurus-pyqtgraph-0.3.1/LICENSE.txt000066400000000000000000000027131355007744100167070ustar00rootroot00000000000000taurus_pyqtgraph is a plugin for the Taurus project and can be considered as part of the Taurus project. It is Free Software by the CELLS / ALBA Synchrotron, Bellaterra, Spain SECTION 1: GENERAL LICENSE FOR TAURUS_PYQTGRAPH SOURCE CODE =========================================================== The files in taurus_pyqtgraph, except for the cases described in SECTION 2 are distributed under the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See SECTION 2: EXCEPTIONS ===================== Some files (e.g., those authored by 3rd parties or the documentation sources) are distributed under Free Software / documentation licenses that may differ from the the general one defined in SECTION 1. The following is a list of these exceptions: 2.1: Explicit copyright info in header/metadata: ------------------------------------------------ If a file contains an explicit license or other copyright information in its header or metadata which differs from the one defined in SECTION 1, or the following sections, such license/copyright info mentioned in the header/metadata prevails. 2.2: Documentation: ------------------- The .py scripts in the doc directory are treated as per SECTION 1, and the rest of its files are distributed under a Creative Commons Attribution 3.0 License See taurus-pyqtgraph-0.3.1/MANIFEST.in000066400000000000000000000003721355007744100166210ustar00rootroot00000000000000graft taurus_pyqtgraph include LICENSE.txt include CHANGELOG.md include CONTRIBUTING.md recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif taurus-pyqtgraph-0.3.1/Makefile000066400000000000000000000043511355007744100165240ustar00rootroot00000000000000.PHONY: clean clean-test clean-pyc clean-build docs help .DEFAULT_GOAL := help define BROWSER_PYSCRIPT import os, webbrowser, sys try: from urllib import pathname2url except: from urllib.request import pathname2url webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) endef export BROWSER_PYSCRIPT define PRINT_HELP_PYSCRIPT import re, sys for line in sys.stdin: match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) if match: target, help = match.groups() print("%-20s %s" % (target, help)) endef export PRINT_HELP_PYSCRIPT BROWSER := python -c "$$BROWSER_PYSCRIPT" help: @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts clean-build: ## remove build artifacts rm -fr build/ rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -f {} + clean-pyc: ## remove Python file artifacts find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-test: ## remove test and coverage artifacts rm -fr .tox/ rm -f .coverage rm -fr htmlcov/ rm -fr .pytest_cache lint: ## check style with flake8 flake8 taurus_pyqtgraph tests test: ## run tests quickly with the default Python py.test test-all: ## run tests on every Python version with tox tox coverage: ## check code coverage quickly with the default Python coverage run --source taurus_pyqtgraph -m pytest coverage report -m coverage html $(BROWSER) htmlcov/index.html docs: ## generate Sphinx HTML documentation, including API docs rm -f docs/taurus_pyqtgraph.rst rm -f docs/modules.rst sphinx-apidoc -o docs/ taurus_pyqtgraph $(MAKE) -C docs clean $(MAKE) -C docs html $(BROWSER) docs/_build/html/index.html servedocs: docs ## compile the docs watching for changes watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . release: dist ## package and upload a release twine upload dist/* dist: clean ## builds source and wheel package python setup.py sdist python setup.py bdist_wheel ls -l dist install: clean ## install the package to the active Python's site-packages python setup.py install taurus-pyqtgraph-0.3.1/README.md000066400000000000000000000114311355007744100163400ustar00rootroot00000000000000# taurus_pyqtgraph `taurus_pyqtgraph` is an extension package for the [Taurus] package. It adds the `taurus.qt.qtgui.tpg` submodule which provides [pyqtgraph]-based widgets. The rationale behind taurus_pyqtgraph is described in the [TEP17] ## Install Just install this module e.g.: For the latest release in PyPI: `pip install taurus_pyqtgraph` For developing, use a python3 virtual env (or conda, or similar) and: ``` git clone https://github.com/taurus-org/taurus_pyqtgraph.git cd taurus_pyqtgraph pip install -r requirements_dev.txt -r requirements.txt pip install -e . ``` After successful installation, the module will be accessible as `taurus.qt.qtgui.tpg` and the `taurus tpg` CLI will be available ## Features implementation checklist `taurus_pyqtgraph` is still in alpha stage. Its API may be subject to change before the 1.0.0 release. This is a list of planned / done features. The tasks which are checked are those for which there is already an alpha-quality prototype: ### For 1D plots - [x] 1D plot: plot of multiple 1D models with auto-changing color and availability of legend - [x] Date-time support on X axis (display only, see "UI for setting scale limits *in date/time format*" below) - [x] Stand-alone widget - [x] Zooming & panning with "restore original view" option (not the same as zoom stacking, see below) - [x] Possibility to use (at least) 2 Y-scales - [x] UI for adding taurus curves via ModelChooser. See also "Improved Model Chooser" below - [x] Store/retreive configuration (save/load settings) - [x] Support for non-taurus curves in same plot (aka "raw data") - [x] UI for setting scale limits and lin/log options - [x] Export data as ascii: without date-time support - [x] Export plot as image (S0) - [x] UI for moving a curve from one Y-scale to another - [x] UI for choosing line color, thickness symbol, filling... - [x] Arbitrary Label scale (aka FixedLabelsScale) - [ ] configurable properties support (setting permanence) Outside TEP17 scope: - [ ] UI for setting scale limits *in date/time format* (S16) - [x] Point-picking (aka "inspect mode") (S4) - [ ] Date-time support in "export data as ascii" (S24) - [ ] Plot freeze (pause) (S8) - [x] Improved Model Chooser: replacement of the "input data selection" dialog allowing to choose *both* X and Y models (see curve selection dialog in extra_guiqwt's tauruscurve) (C16) - [x] Drop support for taurus attributes (C4) - [ ] Zoom stack: possibility of stacking zoom levels and navigating back one level at a time. (C16) - [ ] Cursor position info (display X-Y position of cursor in active axis coords) (C2) - [ ] 1D ROI selector (C2) - [ ] Curve statistics calculator (mean, stdev...) as in curve stats dialog of TaurusPlot/Trend (C8) - [ ] UI for changing curve names (C8) - [ ] Peak locator: Visual label min/max of curves (C12) - [ ] UI for adding raw data (W8) ### For 1D trends Most of the features mentioned for 1D plots affect the 1D trends as well. Apart from those, here is a list of more specific features of trends: - [x] "1D trends": plot of scalars vs event number or timestamp - [x] Fixed-range scale (aka oscilloscope mode) - [x] UI to switch between fixed and free scale mode - [x] Stand-alone Widget - [x] Support for forced-reading of attributes (aka "-r mode") - [x] UI for forced-reading mode - [ ] configurable properties support (setting permanence) Outside TEP17 scope: - [x] "Trend sets": plot of 1D attribute vs time interpreting it as a set of 1D scalars (M16) - [x] Accessing Archived values (M40). Done via [taurus_tangoarchiving plugin] - [ ] Accessing Tango Polling buffer (W24) - [x] Support for limiting curve buffers (C8) - [ ] UI for curve buffers (C2) ### For 2D plots (images) Outside TEP17 scope: - [x] Plot a single image - [x] UI for Add/remove image - [ ] Stand-alone Widget (M8) - [ ] "calibrated" XYImage (assigning values to X and Y scale, as in guiqwt's XYImageItem) S8 - [ ] Cross sections (slicing) (S4) - [ ] 2D ROI Selector (S4) - [x] LUT/contrast control (S0) - [ ] Drop support for taurus attributes (C4) - [ ] LogZ scale (C?) - [ ] Annotation/measure tools (C16) ### For 2D trends (spectrograms) Most of the features for 2D plots affect also the 2D trends. Apart from those, here is a list of more specific features of 2D trends: Outside TEP17 scope: - [ ] Stand-alone Widget (M8) - [ ] Absolute date-time scale (display, see same feat in TaurusPlot) - [ ] Fixed-range scale (aka oscilloscope mode, same as for 1Dtrends) (M8) - [ ] UI to switch between fixed and free scale mode (S12) ### In general: - [ ] Document all public API - [x] Make all code pep8-clean [Taurus]: http://taurus-scada.org [pyqtgraph]: http://pyqtgraph.org [TEP17]: https://github.com/taurus-org/taurus/pull/452 [taurus_tangoarchiving plugin]: https://github.com/taurus-org/taurus_tangoarchiving taurus-pyqtgraph-0.3.1/docs/000077500000000000000000000000001355007744100160115ustar00rootroot00000000000000taurus-pyqtgraph-0.3.1/docs/Makefile000066400000000000000000000011511355007744100174470ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = python -msphinx SPHINXPROJ = taurus_pyqtgraph SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) taurus-pyqtgraph-0.3.1/docs/conf.py000077500000000000000000000116631355007744100173220ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # taurus_pyqtgraph documentation build configuration file, created by # sphinx-quickstart on Fri Jun 9 13:47:02 2017. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("..")) import taurus_pyqtgraph # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = u"taurus_pyqtgraph" copyright = u"2019, Taurus Community" author = u"Taurus Community" # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The short X.Y version. version = taurus_pyqtgraph.__version__ # The full version, including alpha/beta/rc tags. release = taurus_pyqtgraph.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = "taurus_pyqtgraphdoc" # -- Options for LaTeX output ------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto, manual, or own class]). latex_documents = [ ( master_doc, "taurus_pyqtgraph.tex", u"taurus_pyqtgraph Documentation", u"Taurus Community", "manual", ) ] # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( master_doc, "taurus_pyqtgraph", u"taurus_pyqtgraph Documentation", [author], 1, ) ] # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "taurus_pyqtgraph", u"taurus_pyqtgraph Documentation", author, "taurus_pyqtgraph", "One line description of project.", "Miscellaneous", ) ] taurus-pyqtgraph-0.3.1/docs/history.rst000066400000000000000000000000341355007744100202410ustar00rootroot00000000000000.. include:: ../HISTORY.rst taurus-pyqtgraph-0.3.1/docs/index.rst000066400000000000000000000004231355007744100176510ustar00rootroot00000000000000Welcome to taurus_pyqtgraph's documentation! ====================================== .. toctree:: :maxdepth: 2 :caption: Contents: readme installation usage modules Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` taurus-pyqtgraph-0.3.1/docs/installation.rst000066400000000000000000000022521355007744100212450ustar00rootroot00000000000000.. highlight:: shell ============ Installation ============ Stable release -------------- To install taurus_pyqtgraph, run this command in your terminal: .. code-block:: console $ pip install taurus_pyqtgraph This is the preferred method to install taurus_pyqtgraph, as it will always install the most recent stable release. If you don't have `pip`_ installed, this `Python installation guide`_ can guide you through the process. .. _pip: https://pip.pypa.io .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ From sources ------------ The sources for taurus_pyqtgraph can be downloaded from the `Github repo`_. You can either clone the public repository: .. code-block:: console $ git clone git://github.com/taurus-org/taurus_pyqtgraph Or download the `tarball`_: .. code-block:: console $ curl -OL https://github.com/taurus-org/taurus_pyqtgraph/tarball/master Once you have a copy of the source, you can install it with: .. code-block:: console $ python setup.py install .. _Github repo: https://github.com/taurus-org/taurus_pyqtgraph .. _tarball: https://github.com/taurus-org/taurus_pyqtgraph/tarball/master taurus-pyqtgraph-0.3.1/docs/make.bat000066400000000000000000000014121355007744100174140ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=python -msphinx ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=taurus_pyqtgraph if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The Sphinx module was not found. Make sure you have Sphinx installed, echo.then set the SPHINXBUILD environment variable to point to the full echo.path of the 'sphinx-build' executable. Alternatively you may add the echo.Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd taurus-pyqtgraph-0.3.1/docs/readme.rst000066400000000000000000000000341355007744100177750ustar00rootroot00000000000000.. mdinclude:: ../README.md taurus-pyqtgraph-0.3.1/docs/usage.rst000066400000000000000000000002411355007744100176440ustar00rootroot00000000000000===== Usage ===== To use taurus_pyqtgraph in a project:: import taurus.qt.qtgui.tpg Or, to use it independently of Taurus: import taurus_pyqtgraph taurus-pyqtgraph-0.3.1/requirements.txt000066400000000000000000000001171355007744100203440ustar00rootroot00000000000000Click==7.0 PyQt5==5.13.0 pyqtgraph==0.10.0 ply==3.11 lxml==4.2.5 taurus==4.6.1 taurus-pyqtgraph-0.3.1/requirements_dev.txt000066400000000000000000000002611355007744100212020ustar00rootroot00000000000000pip==18.1 bumpversion==0.5.3 wheel==0.32.1 watchdog==0.9.0 flake8==3.5.0 tox==3.5.2 coverage==4.5.1 Sphinx==1.8.1 twine==1.12.1 pytest==3.8.2 pytest-runner==4.2 travis-encrypt taurus-pyqtgraph-0.3.1/setup.cfg000066400000000000000000000014611355007744100167040ustar00rootroot00000000000000[bumpversion] current_version = 0.3.1 commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} {major}.{minor}.{patch} [bumpversion:part:release] optional_value = gamma values = alpha gamma [bumpversion:file:setup.py] search = version="{current_version}" replace = version="{new_version}" [bumpversion:file:taurus_pyqtgraph/__init__.py] search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bdist_wheel] universal = 1 [flake8] exclude = docs build ignore = E203, W503 per-file-ignores = taurus_pyqtgraph/__init__.py:F401 [aliases] test = pytest [tool:pytest] collect_ignore = ['setup.py'] taurus-pyqtgraph-0.3.1/setup.py000066400000000000000000000065361355007744100166050ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################## ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################## from setuptools import setup, find_packages description = "Taurus extension providing pyqtgraph-based widgets" long_description = """taurus_pyqtgraph is an extension for the Taurus package. It adds the taurus.qt.qtgui.tpg submodule which provides pyqtgraph-based widgets.""" author = "Taurus Community" maintainer = author maintainer_email = "tauruslib-devel@lists.sourceforge.net" url = "https://github.com/taurus-org/taurus_pyqtgraph" download_url = url platforms = ["Linux", "Windows"] keywords = ["Taurus", "pyqtgraph", "plugin", "widgets"] install_requires = ["pyqtgraph", "click", "taurus>=4.5.2", "lxml", "ply"] setup_requirements = ["pytest-runner"] test_requirements = ["pytest"] entry_points = { "taurus.qt.qtgui": ["tpg = taurus_pyqtgraph"], "taurus.cli.subcommands": ["tpg = taurus_pyqtgraph.cli:tpg"], } classifiers = [ "Development Status :: 3 - Alpha", "Environment :: X11 Applications :: Qt", "Environment :: Win32 (MS Windows)", "Intended Audience :: Developers", "Intended Audience :: Science/Research", ( "License :: OSI Approved :: " + "GNU Lesser General Public License v3 or later (LGPLv3+)" ), "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Operating System :: Unix", "Operating System :: OS Independent", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: User Interfaces", "Topic :: Software Development :: Widget Sets", ] setup( name="taurus_pyqtgraph", version="0.3.1", description=description, long_description=long_description, author=author, maintainer=maintainer, maintainer_email=maintainer_email, url=url, download_url=download_url, platforms=platforms, license="LGPLv3+", keywords=keywords, packages=find_packages(), classifiers=classifiers, include_package_data=True, entry_points=entry_points, test_suite="tests", tests_require=test_requirements, install_requires=install_requires, setup_requires=setup_requirements, ) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/000077500000000000000000000000001355007744100205035ustar00rootroot00000000000000taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/__init__.py000066400000000000000000000037161355007744100226230ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """Top-level package for taurus_pyqtgraph.""" from __future__ import absolute_import import taurus.external.qt as _ # avoid API1 errors due to pyqtgraph imports from .y2axis import Y2ViewBox from .curvespropertiestool import CurvesPropertiesTool from .dateaxisitem import DateAxisItem from .autopantool import XAutoPanTool from .plot import TaurusPlot from .trend import TaurusTrend from .legendtool import PlotLegendTool from .forcedreadtool import ForcedReadTool from .taurusimageitem import TaurusImageItem from .taurusplotdataitem import TaurusPlotDataItem from .taurustrendset import TaurusTrendSet from .curvesmodel import TaurusItemConf, TaurusItemConfDlg from .taurusmodelchoosertool import ( TaurusModelChooserTool, TaurusImgModelChooserTool, TaurusXYModelChooserTool, ) from .curveproperties import ( CurveAppearanceProperties, CurvePropAdapter, CurvesAppearanceChooser, serialize_opts, deserialize_opts, ) # Do not modify the __version__ manually. To be modified by bumpversion __version__ = "0.3.1" taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/autopantool.py000066400000000000000000000077031355007744100234310ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["XAutoPanTool"] from taurus.external.qt import QtGui, QtCore class XAutoPanTool(QtGui.QAction): """ A tool that provides the "AutoPan" for the X axis of a plot (aka "oscilloscope mode"). It is implemented as an Action, and provides a method to attach it to a :class:`pyqtgraph.PlotItem` """ def __init__(self, parent=None): QtGui.QAction.__init__(self, "Fixed range scale", parent) self.setCheckable(True) self.toggled.connect(self._onToggled) self._timer = QtCore.QTimer() self._timer.timeout.connect(self.updateRange) self._originalXAutoRange = None self._viewBox = None self._XactionMenu = None self._scrollStep = 0.2 def attachToPlotItem(self, plot_item): """Use this method to add this tool to a plot :param plot_item: (PlotItem) """ self._viewBox = plot_item.getViewBox() self._addToMenu(self._viewBox.menu) self._originalXAutoRange = self._viewBox.autoRangeEnabled()[0] self._viewBox.sigXRangeChanged.connect(self._onXRangeChanged) def _addToMenu(self, menu): for m in menu.axes: if m.title() == "X Axis": x_menu = m self._XactionMenu = x_menu.actions()[0] x_menu.insertAction(self._XactionMenu, self) self.setParent(menu) def _onToggled(self, checked): if checked: self._originalXAutoRange = self._viewBox.autoRangeEnabled()[0] self._viewBox.enableAutoRange(x=False) axisXrange = self._viewBox.state["viewRange"][0] x_range = axisXrange[1] - axisXrange[0] t = int(x_range / 10.0) * 1000 t = min(3000, t) t = max(50, t) self._timer.start(t) else: self._timer.stop() self._viewBox.enableAutoRange(x=self._originalXAutoRange) self._XactionMenu.setEnabled(not checked) def _onXRangeChanged(self): self.setChecked(False) def updateRange(self): """Pans the x axis (change the viewbox range maintaining width but ensuring that the right-most point is shown """ if len(self._viewBox.addedItems) < 1: self._timer.stop() children_bounds = self._viewBox.childrenBounds() _, boundMax = children_bounds[0] axis_X_range, _ = self._viewBox.state["viewRange"] x_range = axis_X_range[1] - axis_X_range[0] if boundMax > axis_X_range[1] or boundMax < axis_X_range[0]: x_min = boundMax - axis_X_range[1] x_max = boundMax - axis_X_range[0] step = min(max(x_range * self._scrollStep, x_min), x_max) self._viewBox.sigXRangeChanged.disconnect(self._onXRangeChanged) self._viewBox.setXRange( axis_X_range[0] + step, axis_X_range[1] + step, padding=0.0, update=False, ) self._viewBox.sigXRangeChanged.connect(self._onXRangeChanged) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/cli.py000066400000000000000000000056441355007744100216350ustar00rootroot00000000000000############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# import pkg_resources import click _tpg_version = pkg_resources.require("taurus_pyqtgraph")[0].version _taurus_version = pkg_resources.require("taurus")[0].version _version = "{0} (with taurus {1})".format(_tpg_version, _taurus_version) @click.group("tpg") @click.version_option(version=_version, prog_name="tpg") def tpg(): """Taurus-pyqtgraph related commands""" pass @tpg.command("plot") @click.argument("models", nargs=-1) @click.option( "--config", "config_file", type=click.File(), help="configuration file for initialization", ) @click.option( "-x", "--x-axis-mode", "x_axis_mode", type=click.Choice(["t", "n"]), default="n", show_default=True, help=( 'X axis mode. "t" implies using a Date axis' + '"n" uses the regular axis' ), ) @click.option("--demo", is_flag=True, help="show a demo of the widget") @click.option( "--window-name", "window_name", default="TaurusPlot (pg)", help="Name of the window", ) def plot_cmd(models, config_file, x_axis_mode, demo, window_name): """Shows a plot for the given models""" from .plot import plot_main return plot_main( models=models, config_file=config_file, x_axis_mode=x_axis_mode, demo=demo, window_name=window_name, ) @tpg.command("trend") @click.argument("models", nargs=-1) @click.option( "--config", "config_file", type=click.File(), help="configuration file for initialization", ) @click.option("--demo", is_flag=True, help="show a demo of the widget") @click.option( "--window-name", "window_name", default="TaurusPlot (pg)", help="Name of the window", ) def trend_cmd(models, config_file, demo, window_name): """Shows a trend for the given models""" from .trend import trend_main return trend_main( models=models, config_file=config_file, demo=demo, window_name=window_name, ) if __name__ == "__main__": import sys sys.exit(tpg()) # pragma: no cover taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/curveproperties.py000066400000000000000000001003351355007744100243200ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """ A Qt dialog for choosing plot appearance (symbols and lines) for a Pyqtgraph.PlotDataItem or taurus-derived class like TaurusPlotDataItem .. warning:: this is Work-in-progress. The API from this module may still change. Please """ from __future__ import absolute_import # TODO: WIP from builtins import str from builtins import object __all__ = [ "CurveAppearanceProperties", "CurvePropAdapter", "CurvesAppearanceChooser", "serialize_opts", "deserialize_opts", ] import copy from taurus.external.qt import Qt from taurus.core.util.containers import CaselessDict from taurus.qt.qtgui.util.ui import UILoadable from taurus_pyqtgraph.y2axis import Y2ViewBox import pyqtgraph class CONFLICT(object): """ This is just a do-nothing class to be used to indicate that there are conflicting values when merging properties from different curves """ pass NamedLineStyles = { CONFLICT: "", Qt.Qt.NoPen: "No line", Qt.Qt.SolidLine: "_____", Qt.Qt.DashLine: "_ _ _", Qt.Qt.DotLine: ".....", Qt.Qt.DashDotLine: "_._._", Qt.Qt.DashDotDotLine: ".._..", } ReverseNamedLineStyles = {} for k, v in NamedLineStyles.items(): ReverseNamedLineStyles[v] = k # TODO:allow the dialog to use this curve styles NamedCurveStyles = { CONFLICT: "", None: "", "No curve": "No curve", "Lines": "Lines", "Sticks": "Sticks", "Steps": "Steps", "Dots": "Dots", } ReverseNamedCurveStyles = {} for k, v in NamedCurveStyles.items(): ReverseNamedCurveStyles[v] = k NamedSymbolStyles = { CONFLICT: "", None: "No symbol", "o": "Circle", "s": "Square", "d": "Diamond", "t": "Down Triangle", "t1": "Up triangle", "t3": "Left Triangle", "t2": "Right Triangle", "+": "Cross", "star": "Star", "p": "Pentagon", "h": "Hexagon", } ReverseNamedSymbolStyles = {} for k, v in NamedSymbolStyles.items(): ReverseNamedSymbolStyles[v] = k NamedColors = [ "Black", "Red", "Blue", "Magenta", "Green", "Cyan", "Yellow", "Gray", "White", ] @UILoadable class CurvesAppearanceChooser(Qt.QWidget): """ A plot_item for choosing plot appearance for one or more curves. The current curves properties are passed using the setCurves() method using a dictionary with the following structure:: curvePropDict={name1:prop1, name2:prop2,...} where propX is an instance of :class:`CurveAppearanceProperties` When applying, a signal is emitted and the chosen properties are made available in a similar dictionary. """ NAME_ROLE = Qt.Qt.UserRole controlChanged = Qt.pyqtSignal() curveAppearanceChanged = Qt.pyqtSignal(object, list) CurveTitleEdited = Qt.pyqtSignal("QString", "QString") def __init__( self, parent=None, curvePropDict={}, showButtons=False, autoApply=False, Y2Axis=None, curvePropAdapter=None, ): # try: super(CurvesAppearanceChooser, self).__init__(parent) self.loadUi() self.autoApply = autoApply self.sStyleCB.insertItems(0, sorted(NamedSymbolStyles.values())) self.lStyleCB.insertItems(0, list(NamedLineStyles.values())) self.cStyleCB.insertItems(0, list(NamedCurveStyles.values())) self.sColorCB.addItem("") self.lColorCB.addItem("") if not showButtons: self.applyBT.hide() self.resetBT.hide() for color in NamedColors: icon = self._colorIcon(color) self.sColorCB.addItem(icon, "", Qt.QColor(color)) self.lColorCB.addItem(icon, "", Qt.QColor(color)) self.__itemsDict = CaselessDict() self.setCurves(curvePropDict) # set the icon for the background button (stupid designer limitations # forces to do it programatically) self.bckgndBT.setIcon(Qt.QIcon(":color-fill.svg")) # connections. self.curvesLW.itemSelectionChanged.connect( self._onSelectedCurveChanged ) self.curvesLW.itemChanged.connect(self._onItemChanged) self.applyBT.clicked.connect(self.onApply) self.resetBT.clicked.connect(self.onReset) self.sStyleCB.currentIndexChanged.connect(self._onSymbolStyleChanged) self.sStyleCB.currentIndexChanged.connect(self._onControlChanged) self.lStyleCB.currentIndexChanged.connect(self._onControlChanged) self.sColorCB.currentIndexChanged.connect(self._onControlChanged) self.lColorCB.currentIndexChanged.connect(self._onControlChanged) self.cStyleCB.currentIndexChanged.connect(self._onControlChanged) self.sSizeSB.valueChanged.connect(self._onControlChanged) self.lWidthSB.valueChanged.connect(self._onControlChanged) self.cAreaDSB.valueChanged.connect(self._onControlChanged) self.sFillCB.stateChanged.connect(self._onControlChanged) self.cFillCB.stateChanged.connect(self._onControlChanged) self.assignToY1BT.toggled[bool].connect(self.__onY1Toggled) self.assignToY2BT.toggled[bool].connect(self.__onY2Toggled) # self.bckgndBT.clicked.connect(self.changeBackgroundColor) # Disabled buttons until future implementations # (set background color and set curve labels) self.changeTitlesBT.setEnabled(False) self.bckgndBT.setEnabled(False) # disable the group box with the options for swap curves between Y axes if Y2Axis is None: self.groupBox.setEnabled(False) # set properties from curves for first launch of config dialog and # keeps a curvePropAdapter object self._onSelectedCurveChanged() self.curvePropAdapter = curvePropAdapter self.axis = None def __onY1Toggled(self, checked): if checked: self.assignToY2BT.setChecked(False) def __onY2Toggled(self, checked): if checked: self.assignToY1BT.setChecked(False) def changeBackgroundColor(self): """Launches a dialog for choosing the parent's canvas background color """ color = Qt.QColorDialog.getColor( self.curvePropAdapter.getBackgroundColor(), self ) if Qt.QColor.isValid(color): self.curvePropAdapter.setBackgroundColor(color) def setCurves(self, curvePropDict): """Populates the list of curves from the properties dictionary. It uses the curve title for display, and stores the curve name as the item data (with role=CurvesAppearanceChooser.NAME_ROLE) :param curvePropDict: (dict) a dictionary whith keys=curvenames and values= :class:`CurveAppearanceProperties` object """ self.curvePropDict = curvePropDict self._curvePropDictOrig = copy.deepcopy(curvePropDict) self.curvesLW.clear() self.__itemsDict = CaselessDict() for name, prop in self.curvePropDict.items(): # create and insert the item item = Qt.QListWidgetItem(prop.title, self.curvesLW) self.__itemsDict[name] = item item.setData(self.NAME_ROLE, name) item.setToolTip("Curve Name: %s" % name) item.setFlags( Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable | Qt.Qt.ItemIsUserCheckable | Qt.Qt.ItemIsDragEnabled | Qt.Qt.ItemIsEditable ) self.curvesLW.setCurrentRow(0) def _onItemChanged(self, item): """slot used when an item data has changed""" name = item.data(self.NAME_ROLE) previousTitle = self.curvePropDict[name].title currentTitle = item.text() if previousTitle != currentTitle: self.curvePropDict[name].title = currentTitle self.CurveTitleEdited.emit(name, currentTitle) def updateTitles(self, newTitlesDict=None): """ Updates the titles of the curves that are displayed in the curves list. :param newTitlesDict: (dict) dictionary with key=curve_name and value=title """ if newTitlesDict is None: return for name, title in newTitlesDict.items(): self.curvePropDict[name].title = title self.__itemsDict[name].setText(title) def getSelectedCurveNames(self): """Returns the curve names for the curves selected at the curves list. *Note*: The names may differ from the displayed text, which corresponds to the curve titles (this method is what you likely need if you want to get keys to use in curves or curveProp dicts). :return: (string_list) the names of the selected curves """ return [ item.data(self.NAME_ROLE) for item in self.curvesLW.selectedItems() ] def showProperties(self, prop=None): """Updates the dialog to show the given properties. :param prop: (CurveAppearanceProperties) the properties object containing what should be shown. If a given property is set to CONFLICT, the corresponding plot_item will show a "neutral" display """ if prop is None: prop = self._shownProp # set the Style comboboxes self.sStyleCB.setCurrentIndex( self.sStyleCB.findText(NamedSymbolStyles[prop.sStyle]) ) self.lStyleCB.setCurrentIndex( self.lStyleCB.findText(NamedLineStyles[prop.lStyle]) ) self.cStyleCB.setCurrentIndex( self.cStyleCB.findText(NamedCurveStyles[prop.cStyle]) ) if prop.y2 is CONFLICT: self.assignToY1BT.setChecked(False) self.assignToY2BT.setChecked(False) elif prop.y2: self.assignToY2BT.setChecked(True) else: self.assignToY1BT.setChecked(True) # set sSize and lWidth spinboxes. if prop.sSize is None, it puts -1 # (which is the special value for these switchhboxes) if prop.sSize is CONFLICT or prop.sStyle is None: self.sSizeSB.setValue(-1) else: self.sSizeSB.setValue(max(prop.sSize, -1)) if prop.lWidth is CONFLICT: self.lWidthSB.setValue(-1) else: self.lWidthSB.setValue(max(prop.lWidth, -1)) # Set the Color combo boxes. The item at index 0 is the empty one in # the comboboxes Manage unknown colors by including them if prop.sColor in (None, CONFLICT) or prop.sStyle is None: index = 0 else: index = self.sColorCB.findData(Qt.QColor(prop.sColor)) if index == -1: # if the color is not supported, add it to combobox index = self.sColorCB.count() self.sColorCB.addItem( self._colorIcon(Qt.QColor(prop.sColor)), "", Qt.QColor(prop.sColor), ) self.sColorCB.setCurrentIndex(index) if prop.lColor is None or prop.lColor is CONFLICT: index = 0 else: index = self.lColorCB.findData(Qt.QColor(prop.lColor)) if index == -1: # if the color is not supported, add it to combobox index = self.lColorCB.count() self.lColorCB.addItem( self._colorIcon(Qt.QColor(prop.lColor)), "", Qt.QColor(prop.lColor), ) self.lColorCB.setCurrentIndex(index) # set the Fill Checkbox. The prop.sFill value can be in 3 states: True, # False and None if prop.sFill is None or prop.sFill is CONFLICT: checkState = Qt.Qt.PartiallyChecked elif prop.sFill: checkState = Qt.Qt.Checked else: checkState = Qt.Qt.Unchecked self.sFillCB.setCheckState(checkState) # set the Area Fill Checkbox. The prop.cFill value can be in 3 states: # True, False and None if prop.cFill is CONFLICT: checkState = Qt.Qt.PartiallyChecked self.cAreaDSB.setValue(0.0) elif prop.cFill is None: checkState = Qt.Qt.Unchecked self.cAreaDSB.setValue(0.0) else: checkState = Qt.Qt.Checked self.cAreaDSB.setValue(prop.cFill) self.cFillCB.setCheckState(checkState) def _onControlChanged(self, *args): """ Slot to be called whenever a control plot_item is changed. It emits a `controlChanged` signal and applies the change if in autoapply mode. It ignores any arguments passed """ self.controlChanged.emit() if self.autoApply: self.onApply() def _onSelectedCurveChanged(self): """Updates the shown properties when the curve selection changes""" plist = [ self.curvePropDict[name] for name in self.getSelectedCurveNames() ] if len(plist) == 0: plist = [CurveAppearanceProperties()] self.lineGB.setEnabled(False) self.symbolGB.setEnabled(False) self.otherGB.setEnabled(False) else: self.lineGB.setEnabled(True) self.symbolGB.setEnabled(True) self.otherGB.setEnabled(True) self._shownProp = CurveAppearanceProperties.merge(plist) self.showProperties(self._shownProp) def _onSymbolStyleChanged(self, text): """Slot called when the Symbol style is changed, to ensure that symbols are visible if you choose them :param text: (str) the new symbol style label """ text = str(text) if self.sSizeSB.value() < 2 and text not in ["", "No symbol"]: # a symbol size of 0 is invisible and 1 means you should use # cStyle=dots self.sSizeSB.setValue(3) def getShownProperties(self): """Returns a copy of the currently shown properties and updates self._shownProp :return: (CurveAppearanceProperties) """ prop = CurveAppearanceProperties() for name in self.getSelectedCurveNames(): prop.title = self.curvePropDict[name].title # get the values from the Style comboboxes. Note that the empty string # ("") translates into CONFLICT prop.sStyle = ReverseNamedSymbolStyles[ str(self.sStyleCB.currentText()) ] prop.lStyle = ReverseNamedLineStyles[str(self.lStyleCB.currentText())] prop.cStyle = ReverseNamedCurveStyles[str(self.cStyleCB.currentText())] # get sSize and lWidth from the spinboxes (-1 means conflict) v = self.sSizeSB.value() if v == -1: prop.sSize = CONFLICT else: prop.sSize = v v = self.lWidthSB.value() if v == -1: prop.lWidth = CONFLICT else: prop.lWidth = v # Get the Color combo boxes. The item at index 0 is the empty one in # the comboboxes index = self.sColorCB.currentIndex() if index == 0: prop.sColor = CONFLICT else: prop.sColor = Qt.QColor(self.sColorCB.itemData(index)) index = self.lColorCB.currentIndex() if index == 0: prop.lColor = CONFLICT else: prop.lColor = Qt.QColor(self.lColorCB.itemData(index)) # get the sFill from the Checkbox. checkState = self.sFillCB.checkState() if checkState == Qt.Qt.PartiallyChecked: prop.sFill = CONFLICT else: prop.sFill = bool(checkState) # get the cFill from the Checkbox. checkState = self.cFillCB.checkState() if checkState == Qt.Qt.PartiallyChecked: prop.cFill = CONFLICT elif checkState == Qt.Qt.Checked: prop.cFill = self.cAreaDSB.value() else: prop.cFill = None # get the y2 state from the buttons y1 = self.assignToY1BT.isChecked() y2 = self.assignToY2BT.isChecked() if not y1 and not y2: prop.y2 = CONFLICT elif y1: prop.y2 = False elif y2: prop.y2 = True else: # both buttons should never be checked simultaneously raise RuntimeError("Inconsistent state of Y-axis buttons") # store the props self._shownProp = copy.deepcopy(prop) return copy.deepcopy(prop) def onApply(self): """Apply does 2 things: - It updates `self.curvePropDict` using the current values chosen in the dialog - It emits a curveAppearanceChanged signal that indicates the names of the curves that changed and the new properties. (TODO) :return: (tuple) a tuple containing the curve properties and a list of the selected curve names (as a list) """ names = self.getSelectedCurveNames() prop = self.getShownProperties() # Update self.curvePropDict for selected properties for n in names: self.curvePropDict[n] = CurveAppearanceProperties.merge( [self.curvePropDict[n], prop], conflict=CurveAppearanceProperties.inConflict_update_a, ) # emit a (PyQt) signal telling what properties (first argument) need to # be applied to which curves (second argument) # self.curveAppearanceChanged.emit(prop, names) # return both values self.curvePropAdapter.setCurveProperties(self.curvePropDict, names) return prop, names def onReset(self): """slot to be called when the reset action is triggered. It reverts to the original situation""" self.setCurves(self._curvePropDictOrig) self.curvesLW.clearSelection() def _colorIcon(self, color, w=10, h=10): # to do: create a border pixmap = Qt.QPixmap(w, h) pixmap.fill(Qt.QColor(color)) return Qt.QIcon(pixmap) class CurvePropAdapter(object): """ This class allows to extract the curve properties (:class:`CurveAppearanceProperties`) from curves (:class:`pyqtgraph.PlotDataItem`) in a given plot (:class:`pyqtgraph.PlotItem`). """ def __init__(self, dataItems=None, plotItem=None, y2axis=None): self.dataItems = dataItems self.plotItem = plotItem self._curve_items = dict() self.y2axis = y2axis def getCurveProperties(self): """ Returns CurveAppearanceProperties objects for all curves in the associated PlotItem :return: A dictionary(key, value), whose keys are curve names and values are the corresponding CurveApearanceProperties object """ curves_prop = {} for item in self.dataItems: y2 = isinstance(item.getViewBox(), Y2ViewBox) opts = item.opts pen = pyqtgraph.mkPen(opts["pen"]) symbol_pen = pyqtgraph.mkPen(opts["symbolPen"]) symbol_brush = pyqtgraph.mkBrush(opts["symbolBrush"]) title = opts.get("name") sStyle = opts["symbol"] sSize = opts["symbolSize"] if sStyle is None: sColor = None sSize = -1 else: sColor = symbol_pen.color() sFill = symbol_brush.color() if sFill is None or sStyle is None: sFill = False else: sFill = True lStyle = pen.style() lWidth = pen.width() lColor = pen.color() cStyle = None cFill = opts["fillLevel"] curve_appearance_properties = CurveAppearanceProperties( sStyle=sStyle, sSize=sSize, sColor=sColor, sFill=sFill, lStyle=lStyle, lWidth=lWidth, lColor=lColor, cStyle=cStyle, cFill=cFill, y2=y2, title=title, ) curves_prop[title] = curve_appearance_properties self._curve_items[title] = item return curves_prop def setCurveProperties(self, properties, names): """ Assign the properties from a CurveAppearanceProperties object to a PlotDataItem :param properties: (dict) dictionary containing :class:`CurveAppearanceProperties` (keys are curve names) :param names: (seq) names of the curves for which to set the curve properties (they must be present in the `properties` dict) """ for name in names: prop = properties[name] sStyle = prop.sStyle sSize = prop.sSize sColor = prop.sColor sFill = prop.sFill lStyle = prop.lStyle lWidth = prop.lWidth lColor = prop.lColor cFill = prop.cFill y2 = prop.y2 # title = properties.title dataItem = self._curve_items[name] dataItem.setPen(dict(style=lStyle, width=lWidth, color=lColor)) if cFill is not None: dataItem.setFillLevel(cFill) try: cFillColor = Qt.QColor(lColor) cFillColor.setAlphaF(0.5) # set to semitransparent except Exception: cFillColor = lColor dataItem.setFillBrush(cFillColor) else: dataItem.setFillLevel(None) dataItem.setSymbol(sStyle) dataItem.setSymbolPen(pyqtgraph.mkPen(color=sColor)) if sStyle is None or sSize < 0: dataItem.setSymbolSize(0) else: dataItem.setSymbolSize(sSize) if sFill is True: dataItem.setSymbolBrush(sColor) else: dataItem.setSymbolBrush(None) # Set the Y1 / Y2 axis if required old_view = dataItem.getViewBox() # current view for the curve if y2 is None: # axis is not to be changed new_view = old_view elif y2: # Y axis must be Y2 new_view = self.y2axis # y2 axis view else: # Y axis must be Y1 new_view = self.plotItem.getViewBox() # main view if new_view is not old_view: if old_view is not None: old_view.removeItem(dataItem) if not y2: # adapt the log mode to the main view logMode # (this is already done automatically when adding to y2) dataItem.setLogMode( self.plotItem.getAxis("bottom").logMode, self.plotItem.getAxis("left").logMode, ) new_view.addItem(dataItem) old_view.autoRange() new_view.autoRange() # change background color of the whole window, not just the plot area # def setBackgroundColor(self, color): # self.plot_item.setBackground(color) def setBackgroundColor(self, color): # background=None for default in plotting (black color) if color.value() == 0: color = None self.plotItem.getViewBox().setBackgroundColor(color) def getBackgroundColor(self): backgroundColor = self.plotItem.getViewBox().state["background"] if backgroundColor is None: return Qt.QColor("black") return backgroundColor class CurveAppearanceProperties(object): """An object describing the appearance of a TaurusCurve""" def __init__( self, sStyle=CONFLICT, sSize=CONFLICT, sColor=CONFLICT, sFill=CONFLICT, lStyle=CONFLICT, lWidth=CONFLICT, lColor=CONFLICT, cStyle=CONFLICT, y2=CONFLICT, cFill=CONFLICT, title=CONFLICT, visible=CONFLICT, ): """ Possible keyword arguments are: - sStyle= symbolstyle - sSize= int - sColor= color - sFill= bool - lStyle= linestyle - lWidth= int - lColor= color - cStyle= curvestyle - cFill= float or None - y2= bool - visible = bool - title= str Where: - color is a color that QColor() understands (i.e. a Qt.Qt.GlobalColor, a color name, or a Qt.Qcolor) - symbolstyle is a key of NamedSymbolStyles - linestyle is a key of Qt.Qt.PenStyle - curvestyle is a key of NamedCurveStyles - cFill can either be None (meaning not to fill) or a float that indicates the baseline from which to fill - y2 is True if the curve is associated to the y2 axis """ self.sStyle = sStyle self.sSize = sSize self.sColor = sColor self.sFill = sFill self.lStyle = lStyle self.lWidth = lWidth self.lColor = lColor self.cStyle = cStyle self.cFill = cFill self.y2 = y2 self.title = title self.visible = visible self.propertyList = [ "sStyle", "sSize", "sColor", "sFill", "lStyle", "lWidth", "lColor", "cStyle", "cFill", "y2", "title", "visible", ] @staticmethod def inConflict_update_a(a, b): """ This function can be passed to CurvesAppearance.merge() if one wants to update prop1 with prop2 except for those attributes of prop2 that are set to CONFLICT """ if b is CONFLICT: return a else: return b @staticmethod def inConflict_CONFLICT(a, b): """In case of conflict, returns CONFLICT""" return CONFLICT def conflictsWith(self, other, strict=True): """ Compares itself with another CurveAppearanceProperties object and returns (a list of then names of) the attributes that are in conflict between the two """ result = [] for aname in self.propertyList: vself = getattr(self, aname) vother = getattr(other, aname) if vself != vother and ( strict or not (CONFLICT in (vself, vother)) ): result.append(aname) return result @staticmethod def merge(plist, attributes=None, conflict=None): """ returns a CurveAppearanceProperties object formed by merging a list of other CurveAppearanceProperties objects :param plist: (sequence) objects to be merged :param attributes: (sequence) the name of the attributes to consider for the merge. If None, all the attributes will be merged :param conflict: (callable) a function that takes 2 objects (having a different attribute) and returns a value that solves the conflict. If None is given, any conflicting attribute will be set to CONFLICT. :return: (CurveAppearanceProperties) merged properties """ n = len(plist) if n < 1: raise ValueError("plist must contain at least 1 member") plist = copy.deepcopy(plist) if n == 1: return plist[0] if attributes is None: attributes = [ "sStyle", "sSize", "sColor", "sFill", "lStyle", "lWidth", "lColor", "cStyle", "cFill", "y2", "title", ] if conflict is None: conflict = CurveAppearanceProperties.inConflict_CONFLICT p = CurveAppearanceProperties() for a in attributes: alist = [p.__getattribute__(a) for p in plist] p.__setattr__(a, alist[0]) for ai in alist[1:]: if alist[0] != ai: # print "MERGING:",a,alist[0],ai,conflict(alist[0],ai) p.__setattr__(a, conflict(alist[0], ai)) break return p def deserialize_opts(opts): """ Deserialize opts dict to pass it to a PlotDataItem :param opts: (dict) serialized properties (as the output of :meth:`deserialize_opts`) :return: (dict) deserialized properties (acceptable by PlotDataItem) """ # pen property if opts["pen"] is not None: opts["pen"] = _unmarshallingQPainter(opts, "pen", "pen") # shadowPen property if opts["shadowPen"] is not None: opts["shadowPen"] = _unmarshallingQPainter(opts, "shadowPen", "pen") # symbolPen property if opts["symbolPen"] is not None: opts["symbolPen"] = _unmarshallingQPainter(opts, "symbolPen", "pen") # fillBrush property if opts["fillBrush"] is not None: opts["fillBrush"] = _unmarshallingQPainter(opts, "fillBrush", "brush") # symbolBrush property if opts["symbolBrush"] is not None: opts["symbolBrush"] = _unmarshallingQPainter( opts, "symbolBrush", "brush" ) return opts def serialize_opts(opts): """ Serialize all properties from PlotDataItem. :param opts: (dict) PlotDataItem opts (may include non-serializable objects) :return: (dict) serialized properties (can be pickled) """ # pen property (QPen object) if opts["pen"] is not None: _marshallingQPainter(opts, "pen", "pen") # shadowPen property (QPen object) if opts["shadowPen"] is not None: _marshallingQPainter(opts, "shadowPen", "pen") # symbolPen property (QPen object) if opts["symbolPen"] is not None: _marshallingQPainter(opts, "symbolPen", "pen") # fillBrush property (QBrush object) if opts["fillBrush"] is not None: _marshallingQPainter(opts, "fillBrush", "brush") # symbolBrush property (QBrush object) if opts["symbolBrush"] is not None: _marshallingQPainter(opts, "symbolBrush", "brush") return opts def _marshallingQPainter(opts, prop_name, qPainter): if qPainter == "pen": painter = pyqtgraph.mkPen(opts[prop_name]) opts[prop_name + "_width"] = painter.width() opts[prop_name + "_dash"] = painter.dashPattern() opts[prop_name + "_cosmetic"] = painter.isCosmetic() elif qPainter == "brush": painter = pyqtgraph.mkBrush(opts[prop_name]) else: return color = pyqtgraph.colorStr(painter.color()) opts[prop_name] = color opts[prop_name + "_style"] = painter.style() def _unmarshallingQPainter(opts, prop_name, qPainter): color = opts[prop_name] style = opts[prop_name + "_style"] del opts[prop_name + "_style"] if qPainter == "pen": width = opts[prop_name + "_width"] dash = opts[prop_name + "_dash"] cosmetic = opts[prop_name + "_cosmetic"] del opts[prop_name + "_width"] del opts[prop_name + "_dash"] del opts[prop_name + "_cosmetic"] painter = pyqtgraph.mkPen( color=color, style=style, width=width, dash=dash, cosmetic=cosmetic ) elif qPainter == "brush": painter = pyqtgraph.mkBrush(color=color) painter.setStyle(style) else: return return painter taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/curvesmodel.py000066400000000000000000000454511355007744100234160ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """ curvesmodel Model and view for new CurveItem configuration .. warning:: this is Work-in-progress. The API may change. Do not rely on current API of this module """ from __future__ import print_function from future.utils import string_types from builtins import bytes __all__ = ["TaurusCurveItemTableModel", "TaurusItemConf", "TaurusItemConfDlg"] import copy from taurus.external.qt import Qt import taurus from taurus.core import TaurusElementType from taurus.qt.qtcore.mimetypes import ( TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE, ) from taurus.qt.qtgui.util.ui import UILoadable from taurus.qt.qtgui.panel import TaurusModelSelector # columns: NUMCOLS = 3 X, Y, TITLE = list(range(NUMCOLS)) SRC_ROLE = Qt.Qt.UserRole + 1 class Component(object): def __init__(self, src): self.display = "" self.icon = Qt.QIcon() self.ok = True self.processSrc(src) def processSrc(self, src): """ processes the src and sets the values of display, icon and ok attributes """ if src is None: self.display, self.icon, self.ok = "", Qt.QIcon(), True return src = str(src).strip() # empty if src == "": self.display, self.icon, self.ok = "", Qt.QIcon(), True return # for taurus attributes if taurus.isValidName(src, etypes=[TaurusElementType.Attribute]): self.display, self.icon, self.ok = ( src, Qt.QIcon("logos:taurus.png"), True, ) return # if not caught before, it is unsupported self.display, self.icon, self.ok = ( src, Qt.QIcon.fromTheme("dialog-warning"), False, ) class TaurusItemConf(object): """An object to hold an item of the TaurusCurveItemTableModel""" def __init__(self, YModel=None, XModel=None, name=None): self.x = Component(XModel) self.y = Component(YModel) self.xModel = XModel self.yModel = YModel self.curveLabel = name def __repr__(self): ret = "TaurusItemConf(xModel='%s', yModel='%s')" % ( self.xModel, self.yModel, ) return ret class TaurusCurveItemTableModel(Qt.QAbstractTableModel): """ A Qt data model for describing curves""" dataChanged = Qt.pyqtSignal("QModelIndex", "QModelIndex") def __init__(self, taurusItems=None): super(TaurusCurveItemTableModel, self).__init__() self.ncolumns = NUMCOLS self.taurusItems = list(taurusItems) def dumpData(self): return copy.copy(self.taurusItems) def rowCount(self, index=Qt.QModelIndex()): return len(self.taurusItems) def columnCount(self, index=Qt.QModelIndex()): return self.ncolumns def swapItems(self, index1, index2): """ swap the items described by index1 and index2 in the list""" r1, r2 = index1.row(), index2.row() items = self.taurusItems self.layoutAboutToBeChanged.emit() items[r1], items[r2] = items[r2], items[r1] self.dataChanged.emit(index1, index2) self.layoutChanged.emit() def data(self, index, role=Qt.Qt.DisplayRole): if not index.isValid() or not (0 <= index.row() < self.rowCount()): return None row = index.row() column = index.column() # Display Role if role == Qt.Qt.DisplayRole: if column == X: return str(self.taurusItems[row].x.display) elif column == Y: return str(self.taurusItems[row].y.display) elif column == TITLE: return str(self.taurusItems[row].curveLabel) else: return None elif role == Qt.Qt.DecorationRole: if column == X: return self.taurusItems[row].x.icon elif column == Y: return self.taurusItems[row].y.icon else: return None elif role == Qt.Qt.TextColorRole: if column == X: Qt.QColor(self.taurusItems[row].x.ok and "green" or "red") elif column == Y: Qt.QColor(self.taurusItems[row].y.ok and "green" or "red") else: return None elif role == SRC_ROLE: if column == X: return str(self.taurusItems[row].xModel) elif column == Y: return str(self.taurusItems[row].yModel) else: return None elif role == Qt.Qt.ToolTipRole: if column == X: return str(self.taurusItems[row].xModel) elif column == Y: return str(self.taurusItems[row].yModel) else: return None if role == Qt.Qt.EditRole: if column == X: return str(self.taurusItems[row].xModel) elif column == Y: return str(self.taurusItems[row].yModel) elif column == TITLE: return str(self.taurusItems[row].curveLabel) else: return None return None def headerData(self, section, orientation, role=Qt.Qt.DisplayRole): if role == Qt.Qt.TextAlignmentRole: if orientation == Qt.Qt.Horizontal: return int(Qt.Qt.AlignLeft | Qt.Qt.AlignVCenter) return int(Qt.Qt.AlignRight | Qt.Qt.AlignVCenter) if role != Qt.Qt.DisplayRole: return None # So this is DisplayRole... if orientation == Qt.Qt.Horizontal: if section == X: return "X source" elif section == Y: return "Y Source" elif section == TITLE: return "Title" return None else: return str(section + 1) def flags(self, index): # use this to set the editable flag when fix is selected if not index.isValid(): return Qt.Qt.ItemIsEnabled column = index.column() if column in (X, Y): return Qt.Qt.ItemFlags( Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsEditable | Qt.Qt.ItemIsDragEnabled | Qt.Qt.ItemIsDropEnabled | Qt.Qt.ItemIsSelectable ) elif column == TITLE: return Qt.Qt.ItemFlags( Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsEditable | Qt.Qt.ItemIsDragEnabled ) return Qt.Qt.ItemFlags( Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsEditable | Qt.Qt.ItemIsDragEnabled ) def setData(self, index, value=None, role=Qt.Qt.EditRole): if index.isValid() and (0 <= index.row() < self.rowCount()): row = index.row() curve = self.taurusItems[row] column = index.column() if column == X: curve.xModel = value curve.x.processSrc(value) elif column == Y: curve.yModel = value curve.y.processSrc(value) elif column == TITLE: curve.curveLabel = value self.dataChanged.emit(index, index) return True return False def insertRows(self, position=None, rows=1, parentindex=None): if position is None: position = self.rowCount() if parentindex is None: parentindex = Qt.QModelIndex() self.beginInsertRows(parentindex, position, position + rows - 1) slice = [TaurusItemConf() for i in range(rows)] self.taurusItems = ( self.taurusItems[:position] + slice + self.taurusItems[position:] ) self.endInsertRows() return True def removeRows(self, position, rows=1, parentindex=None): if parentindex is None: parentindex = Qt.QModelIndex() self.beginResetModel() self.beginRemoveRows(parentindex, position, position + rows - 1) self.taurusItems = ( self.taurusItems[:position] + self.taurusItems[position + rows :] ) self.endRemoveRows() self.endResetModel() return True def clearAll(self): self.removeRows(0, self.rowCount()) def mimeTypes(self): result = list(Qt.QAbstractTableModel.mimeTypes(self)) result += [TAURUS_ATTR_MIME_TYPE, "text/plain"] return result def dropMimeData(self, data, action, row, column, parent): if row == -1: if parent.isValid(): row = parent.row() else: row = parent.rowCount() if column == -1: if parent.isValid(): column = parent.column() else: column = parent.columnCount() if data.hasFormat(TAURUS_ATTR_MIME_TYPE): model = bytes(data.data(TAURUS_ATTR_MIME_TYPE)).decode("utf-8") self.setData(self.index(row, column), value=model) return True elif data.hasFormat(TAURUS_MODEL_LIST_MIME_TYPE): d = bytes(data.data(TAURUS_MODEL_LIST_MIME_TYPE)) models = d.decode("utf-8").split() if len(models) == 1: self.setData(self.index(row, column), value=models[0]) return True else: self.insertRows(row, len(models)) for i, m in enumerate(models): self.setData(self.index(row + i, column), value=m) return True elif data.hasText(): self.setData(self.index(row, column), data.text()) return True return False def mimeData(self, indexes): mimedata = Qt.QAbstractTableModel.mimeData(self, indexes) if len(indexes) == 1: # mimedata.setData(TAURUS_ATTR_MIME_TYPE, data) data = self.data(indexes[0], role=SRC_ROLE) mimedata.setText(data) return mimedata # mimedata.setData() @UILoadable(with_ui="ui") class TaurusItemConfDlg(Qt.QWidget): """ A configuration dialog for creating new CurveItems. Provides a TaurusModelBrowser for Taurus models and an editable table for the sources and title of data """ dataChanged = Qt.pyqtSignal("QModelIndex", "QModelIndex") applied = Qt.pyqtSignal() def __init__(self, parent=None, taurusItemsConf=None, showXcol=True): super(TaurusItemConfDlg, self).__init__(parent) self.loadUi() self._showXcol = showXcol if taurusItemsConf is None: taurusItemsConf = [ TaurusItemConf(YModel=None, XModel=None, name=None) ] # @todo: The action for this button is not yet implemented self.ui.reloadBT.setEnabled(False) self.model = TaurusCurveItemTableModel(taurusItemsConf) self._toolbar = Qt.QToolBar("Selector toolbar") self.ui.horizontalLayout_2.addWidget(self._toolbar) self._toolbar.setIconSize(Qt.QSize(16, 16)) self._toolbar.addAction( Qt.QIcon.fromTheme("list-add"), "Add row", self._onAddAction ) self._removeAction = self._toolbar.addAction( Qt.QIcon.fromTheme("list-remove"), "Remove selected row", self._onRemoveThisAction, ) self._removeAllAction = self._toolbar.addAction( Qt.QIcon.fromTheme("edit-clear"), "Remove all rows", self._onClearAll, ) self._moveUpAction = self._toolbar.addAction( Qt.QIcon.fromTheme("go-up"), "Move up the row", self._onMoveUpAction, ) self._moveDownAction = self._toolbar.addAction( Qt.QIcon.fromTheme("go-down"), "Move down the row", self._onMoveDownAction, ) table = self.ui.curvesTable table.setModel(self.model) table.setColumnHidden(X, not self._showXcol) selectionmodel = table.selectionModel() selectionmodel.selectionChanged.connect(self._onSelectionChanged) # ------------------------------------------------------------------- # I get "UnboundLocalError: local variable 'taurus' referenced before # assignment" if I don't import taurus again here # TODO: check if this workaround is really needed import taurus # noqa # ------------------------------------------------------------------- modelSelector = TaurusModelSelector(parent=self) self.ui.verticalLayout.addWidget(modelSelector) # Connections self.ui.applyBT.clicked.connect(self.onApply) self.ui.reloadBT.clicked.connect(self.onReload) self.ui.cancelBT.clicked.connect(self.close) self.ui.curvesTable.customContextMenuRequested.connect( self.onTableContextMenu ) modelSelector.modelsAdded.connect(self.onModelsAdded) def onTableContextMenu(self, pos): index = self.ui.curvesTable.indexAt(pos) row = index.row() menu = Qt.QMenu(self.ui.curvesTable) if row >= 0: menu.addAction( Qt.QIcon.fromTheme("list-remove"), "Remove this curve", self._onRemoveThisAction, ) menu.addAction( Qt.QIcon.fromTheme("edit-clear"), "Clear all", self.model.clearAll ) menu.addAction( Qt.QIcon.fromTheme("list-add"), "Add new row", self.model.insertRows, ) menu.exec_(Qt.QCursor.pos()) def _onSelectionChanged(self): """ Modify the status of the actions""" selected = self.ui.curvesTable.selectedIndexes() rows = [] for item in selected: if item.row() not in rows: rows.append(item.row()) lrows = len(rows) row = self.ui.curvesTable.currentIndex().row() isLastElem = row == self.model.rowCount() - 1 isFirstElem = row == 0 self._removeAction.setEnabled(lrows == 1) self._moveUpAction.setEnabled(lrows == 1 and not isFirstElem) self._moveDownAction.setEnabled(lrows == 1 and not isLastElem) def _onAddAction(self): """ Add a new row""" self.model.insertRows() self._removeAllAction.setEnabled(True) def _onRemoveThisAction(self): """ Remove the selected row""" row = self.ui.curvesTable.currentIndex().row() self.model.removeRows(row) if self.model.rowCount() == 0: self._removeAllAction.setEnabled(False) def _onClearAll(self): """ Remove all rows""" self.model.clearAll() self._removeAction.setEnabled(False) self._moveUpAction.setEnabled(False) self._moveDownAction.setEnabled(False) self._removeAllAction.setEnabled(False) def _onMoveUpAction(self): """ Move up action swap the selected row with the previous one""" i1 = self.ui.curvesTable.currentIndex() i2 = self.ui.curvesTable.model().index(i1.row() - 1, 0) self.__commitAndCloseEditor(i1) self.model.swapItems(i1, i2) self.ui.curvesTable.setCurrentIndex(i2) def _onMoveDownAction(self): """ Move down action swap the selected row with the next one""" i1 = self.ui.curvesTable.currentIndex() i2 = self.ui.curvesTable.model().index(i1.row() + 1, 0) self.__commitAndCloseEditor(i1) self.model.swapItems(i1, i2) self.ui.curvesTable.setCurrentIndex(i2) def __commitAndCloseEditor(self, idx): """if an editor is open, commit the data and close it before moving :param idx: qmodel index """ w = self.ui.curvesTable.indexWidget(idx) if w is not None: self.ui.curvesTable.commitData(w) self.ui.curvesTable.closePersistentEditor(idx) def onModelsAdded(self, models): nmodels = len(models) rowcount = self.model.rowCount() self.model.insertRows(rowcount, nmodels) for i, m in enumerate(models): if isinstance(m, string_types): modelx, modely = None, m else: modelx, modely = m if modelx is not None: self.model.setData( self.model.index(rowcount + i, X), value=modelx ) self.model.setData(self.model.index(rowcount + i, Y), value=modely) title = self.model.data( self.model.index(rowcount + i, Y) ) # the display data # print type(title), title self.model.setData( self.model.index(rowcount + i, TITLE), value=title ) def getItemConfs(self): return self.model.dumpData() @staticmethod def showDlg(parent=None, taurusItemConf=None, showXCol=True): """ Static method that launches a modal dialog containing a TaurusItemConfDlg. For the parameters, see :class:`TaurusItemConfDlg` :return: (list,bool) Returns a models,ok tuple models is a list of models. ok is True if the dialog was accepted (by clicking on the "update" button) and False otherwise """ dlg = Qt.QDialog(parent) dlg.setWindowTitle("Curves Selection") layout = Qt.QVBoxLayout() w = TaurusItemConfDlg( parent=parent, taurusItemsConf=taurusItemConf, showXcol=showXCol ) layout.addWidget(w) dlg.setLayout(layout) w.applied.connect(dlg.accept) w.ui.cancelBT.clicked.connect(dlg.close) dlg.exec_() return w.getItemConfs(), (dlg.result() == dlg.Accepted) def onApply(self): self.applied.emit() def onReload(self): # TODO print("RELOAD!!! (todo)") if __name__ == "__main__": from taurus.qt.qtgui.application import TaurusApplication import sys app = TaurusApplication(cmd_line_parser=None) TaurusItemConfDlg.showDlg() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/curvespropertiestool.py000066400000000000000000000112341355007744100254000ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["CurvesPropertiesTool"] from taurus.external.qt import QtGui, Qt from taurus.external.qt import QtCore from taurus_pyqtgraph.curveproperties import ( CurvePropAdapter, CurvesAppearanceChooser, ) import pyqtgraph class CurvesPropertiesTool(QtGui.QAction): """ This tool inserts an action in the menu of the :class:`pyqtgraph.PlotItem` to which it is attached to show a dialog for editing curve properties. It is implemented as an Action, and provides a method to attach it to a PlotItem. """ def __init__(self, parent=None): QtGui.QAction.__init__(self, "Plot configuration", parent) self.triggered.connect(self._onTriggered) self.plot_item = None self.Y2Axis = None def attachToPlotItem(self, plot_item, y2=None): """ Use this method to add this tool to a plot :param plot_item: (PlotItem) :param y2: (Y2ViewBox) instance of the Y2Viewbox attached to plot_item if the axis change controls are to be used """ self.plot_item = plot_item menu = plot_item.getViewBox().menu menu.addAction(self) self.Y2Axis = y2 def _onTriggered(self): data_items = self.plot_item.listDataItems() # checks in all ViewBoxes from plot_item, # looking for a data_items (Curves). for item in self.plot_item.scene().items(): if isinstance(item, pyqtgraph.ViewBox): for data in item.addedItems: if data not in data_items: data_items.append(data) # The dialog will ignore curves that define `._UImodifiable=False` modifiable_items = [] for item in data_items: if getattr(item, "_UImodifiable", True): modifiable_items.append(item) # It is necessary a CurvePropAdapter object for 'translate' # the PlotDataItem properties into generic form given for the dialog curvePropAdapter = CurvePropAdapter( modifiable_items, self.plot_item, self.Y2Axis ) curves = curvePropAdapter.getCurveProperties() dlg = Qt.QDialog(parent=self.parent()) dlg.setWindowTitle("Plot Configuration") layout = Qt.QVBoxLayout() w = CurvesAppearanceChooser( parent=dlg, curvePropDict=curves, showButtons=True, Y2Axis=self.Y2Axis, curvePropAdapter=curvePropAdapter, ) layout.addWidget(w) dlg.setLayout(layout) dlg.exec_() if __name__ == "__main__": import sys import numpy import pyqtgraph as pg from taurus.qt.qtgui.tpg import TaurusPlotDataItem from taurus.qt.qtgui.application import TaurusApplication app = TaurusApplication() # a standard pyqtgraph plot_item w = pg.PlotWidget() # add legend to the plot, for that we have to give a name to plot items w.addLegend() # add a Y2 axis from taurus.qt.qtgui.tpg import Y2ViewBox y2ViewBox = Y2ViewBox() y2ViewBox.attachToPlotItem(w.getPlotItem()) # adding a regular data item (non-taurus) c1 = pg.PlotDataItem( name="st plot", pen=dict(color="y", width=3, style=QtCore.Qt.DashLine), fillLevel=0.3, fillBrush="g", ) c1.setData(numpy.arange(300) / 300.0) w.addItem(c1) # adding a taurus data item c2 = TaurusPlotDataItem( name="st2 plot", pen="r", symbol="o", symbolSize=10 ) c2.setModel("sys/tg_test/1/wave") w.addItem(c2) # attach tool to plot item of the PlotWidget tool = CurvesPropertiesTool() tool.attachToPlotItem(w.getPlotItem(), y2=y2ViewBox) w.show() # directly trigger the tool tool.trigger() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/datainspectortool.py000066400000000000000000000207331355007744100246200ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# from datetime import datetime import numpy from taurus.external.qt import Qt from taurus.qt.qtcore.configuration import BaseConfigurableClass from taurus_pyqtgraph import DateAxisItem from pyqtgraph import SignalProxy, InfiniteLine, TextItem, ScatterPlotItem class DataInspectorLine(InfiniteLine): """ DataInspectorLine provides a moveable vertical line item that shows labels containing the coordinates of the points of existing curves it touches. It provides a method to attach it to a PlotItem. .. todo:: for now it only works on the main viewbox. """ # TODO: support more than 1 viewbox (e.g. y2axis). # TODO: modify anchor of labels so that they are plotted on the left if # they do not fit in the view def __init__( self, date_format="%Y-%m-%d %H:%M:%S", y_format="%0.4f", trigger_point_size=10, ): super(DataInspectorLine, self).__init__(angle=90, movable=True) self._labels = [] self._plot_item = None self.y_format = y_format self.trigger_point_size = trigger_point_size self.date_format = date_format self._label_style = "background-color: #35393C;" self.sigPositionChanged.connect(self._inspect) self._highlights = ScatterPlotItem( pos=(), symbol="s", brush="35393C88", pxMode=True, size=trigger_point_size, ) # hack to make the CurvesPropertiesTool ignore the highlight points self._highlights._UImodifiable = False def _inspect(self): """ Slot to re inspector line movemethe mouse move event, and perform the action on the plot. :param evt: mouse event """ xpos = self.pos().x() x_px_size, _ = self.getViewBox().viewPixelSize() self._removeLabels() points = [] # iterate over the existing curves for c in self._plot_item.curves: if c is self._highlights: continue if c.xData is not None: # find the index of the closest point of this curve adiff = numpy.abs(c.xData - xpos) idx = numpy.argmin(adiff) # only add a label if the line touches the symbol tolerance = 0.5 * max(1, c.opts["symbolSize"]) * x_px_size if adiff[idx] < tolerance: points.append((c.xData[idx], c.yData[idx])) self._createLabels(points) def _createLabels(self, points): for x, y in points: # create label at x,y _x = self._getXValue(x) _y = self._getYValue(y) text_item = TextItem() text_item.setPos(x, y) text_item.setHtml( ( "
" + "x={} " + "y={} " + "
" ).format(self._label_style, _x, _y) ) self._labels.append(text_item) self._plot_item.addItem(text_item, ignoreBounds=True) # Add "highlight" marker at each point self._highlights.setData(pos=points) def _getXValue(self, x): """ Helper method converting x value to time if necessary :param x: current x value :return: time or normal x value (depends of the x axis type) """ x_axis = self._plot_item.getAxis("bottom") if isinstance(x_axis, DateAxisItem): return self._timestampToDateTime(x) else: return x def _getYValue(self, y): return str(self.y_format % y) def _timestampToDateTime(self, timestamp): """ Method used to caste the timestamp from the curve to date in proper format (%Y-%m-%d %H:%M:%S) :param timestamp: selected timestamp from curve """ return datetime.utcfromtimestamp(timestamp).strftime(self.date_format) def _removeLabels(self): # remove existing texts for item in self._labels: self.getViewBox().removeItem(item) self._labels = [] # remove existing highlights self._highlights.setData(pos=()) def attachToPlotItem(self, plot_item): """ Method to attach :class:`DataInspectorLine` to the plot :param plot: to attach """ self._plot_item = plot_item self._plot_item.addItem(self, ignoreBounds=True) self._plot_item.addItem(self._highlights, ignoreBounds=True) def dettach(self): """ Method use to detach the class:`DataInspectorLine` from the plot """ self._removeLabels() self._plot_item.removeItem(self._highlights) self._plot_item.removeItem(self) self._plot_item = None class DataInspectorTool(Qt.QWidgetAction, BaseConfigurableClass): """ This tool inserts an action in the menu of the :class:`pyqtgraph.PlotItem` to which it is attached. When activated, the data inspection mode is entered (a :class:`DataInspectorLine` is added and it follows the mouse, allowing the user to inspect the coordinates of existing curves). It is implemented as an Action, and provides a method to attach it to a PlotItem. """ def __init__(self, parent=None): BaseConfigurableClass.__init__(self) Qt.QWidgetAction.__init__(self, parent) self._cb = Qt.QCheckBox() self._cb.setText("Data Inspector") self._cb.toggled.connect(self._onToggled) self.setDefaultWidget(self._cb) self.plot_item = None self.enable = False self.data_inspector = DataInspectorLine() self.registerConfigProperty(self.isChecked, self.setChecked, "checked") def attachToPlotItem(self, plot_item): """ Use this method to add this tool to a plot :param plot_item: (PlotItem) :param y2: (Y2ViewBox) instance of the Y2Viewbox attached to plot_item if the axis change controls are to be used """ self.plot_item = plot_item menu = plot_item.getViewBox().menu menu.addAction(self) def _onToggled(self): if not self.enable: self.data_inspector.attachToPlotItem(self.plot_item) # Signal Proxy which connect the movement of the mouse with # the onMoved method in the data inspector object self.proxy = SignalProxy( self.plot_item.scene().sigMouseMoved, rateLimit=60, slot=self._followMouse, ) self.enable = True # auto-close the menu so that the user can start inspecting self.plot_item.getViewBox().menu.close() else: self.proxy.disconnect() self.data_inspector.dettach() self.enable = False def _followMouse(self, evt): mouse_pos = evt[0] inspector_x = self.plot_item.vb.mapSceneToView(mouse_pos).x() self.data_inspector.setPos(inspector_x) if __name__ == "__main__": from taurus.qt.qtgui.application import TaurusApplication import pyqtgraph as pg import sys app = TaurusApplication() w = pg.PlotWidget(title="[hint: enable inspector mode from context menu]") w.plot(y=numpy.arange(4), pen="r", symbol="o") w.plot(y=numpy.random.rand(4), pen="c") w.plot( x=numpy.linspace(0, 3, 40), y=1 + numpy.random.rand(40), pen="w", symbol="+", ) t = DataInspectorTool(w) t.attachToPlotItem(w.getPlotItem()) w.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/dateaxisitem.py000066400000000000000000000175411355007744100235460ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """ This module provides date-time aware axis """ __all__ = ["DateAxisItem"] # ------------------------------------------------------------------------- # There is a conflict problem with PyQt versions. Pyqtgraph imports his own # library of PyQt, and Taurus too. So we have to import Qt from own version # first as a workaround for forcing our own (as a workaround) from taurus.external.qt import Qt # noqa # ------------------------------------------------------------------------- import numpy from pyqtgraph import AxisItem from datetime import datetime, timedelta from time import mktime class DateAxisItem(AxisItem): """ A tool that provides a date-time aware axis. It is implemented as an AxisItem that interpretes positions as unix timestamps (i.e. seconds since 1970). The labels and the tick positions are dynamically adjusted depending on the range. It provides a :meth:`attachToPlotItem` method to add it to a given PlotItem """ # TODO: Document this class and methods # Max width in pixels reserved for each label in axis _pxLabelWidth = 80 def __init__(self, *args, **kwargs): AxisItem.__init__(self, *args, **kwargs) self._oldAxis = None def tickValues(self, minVal, maxVal, size): """ Reimplemented from PlotItem to adjust to the range and to force the ticks at "round" positions in the context of time units instead of rounding in a decimal base """ maxMajSteps = int(size // self._pxLabelWidth) dt1 = datetime.fromtimestamp(minVal) dt2 = datetime.fromtimestamp(maxVal) dx = maxVal - minVal majticks = [] if dx > 63072001: # 3600s*24*(365+366) = 2 years (count leap year) d = timedelta(days=366) for y in range(dt1.year + 1, dt2.year): dt = datetime(year=y, month=1, day=1) majticks.append(mktime(dt.timetuple())) elif dx > 5270400: # 3600s*24*61 = 61 days d = timedelta(days=31) dt = ( dt1.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + d ) while dt < dt2: # make sure that we are on day 1 (even if always sum 31 days) dt = dt.replace(day=1) majticks.append(mktime(dt.timetuple())) dt += d elif dx > 172800: # 3600s24*2 = 2 days d = timedelta(days=1) dt = dt1.replace(hour=0, minute=0, second=0, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 7200: # 3600s*2 = 2hours d = timedelta(hours=1) dt = dt1.replace(minute=0, second=0, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 1200: # 60s*20 = 20 minutes d = timedelta(minutes=10) dt = ( dt1.replace( minute=(dt1.minute // 10) * 10, second=0, microsecond=0 ) + d ) while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 120: # 60s*2 = 2 minutes d = timedelta(minutes=1) dt = dt1.replace(second=0, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 20: # 20s d = timedelta(seconds=10) dt = dt1.replace(second=(dt1.second // 10) * 10, microsecond=0) + d while dt < dt2: majticks.append(mktime(dt.timetuple())) dt += d elif dx > 2: # 2s d = timedelta(seconds=1) majticks = list(range(int(minVal), int(maxVal))) else: # <2s , use standard implementation from parent return AxisItem.tickValues(self, minVal, maxVal, size) # print("majticks >: ", majticks) L = len(majticks) if L > maxMajSteps: if maxMajSteps == 0: majticks = [] else: majticks = majticks[:: int(numpy.ceil(float(L) / maxMajSteps))] # print("majticks <: ", majticks) # print "----------------------------" return [(d.total_seconds(), majticks)] def tickStrings(self, values, scale, spacing): """Reimplemented from PlotItem to adjust to the range""" ret = [] if not values: return [] # rng = max(values)-min(values) # print('values: ', values) # print('scale: ', scale) # print('spacing: ', spacing) if spacing >= 31622400: # = timedelta(days=366).total_seconds fmt = "%Y" elif spacing >= 2678400: # = timedelta(days=31).total_seconds fmt = "%Y %b" elif spacing >= 86400: # = timedelta(days = 1).total_seconds fmt = "%b/%d" elif spacing >= 3600: # = timedelta(hours=1).total_seconds fmt = "%b/%d-%Hh" elif spacing >= 600: # = timedelta(minutes=10).total_seconds fmt = "%H:%M" elif spacing >= 60: # = timedelta(minutes=1).total_seconds fmt = "%H:%M" elif spacing >= 10: # 10 s fmt = "%H:%M:%S" elif spacing >= 1: # 1s fmt = "%H:%M:%S" else: # less than 2s (show microseconds) # fmt = '%S.%f"' fmt = "[+%fms]" # explicitly relative to last second for x in values: try: t = datetime.fromtimestamp(x) ret.append(t.strftime(fmt)) except ValueError: # Windows can't handle dates before 1970 ret.append("") return ret def attachToPlotItem(self, plotItem): """Add this axis to the given PlotItem :param plotItem: (PlotItem) """ self.setParentItem(plotItem) viewBox = plotItem.getViewBox() self.linkToView(viewBox) self._oldAxis = plotItem.axes[self.orientation]["item"] self._oldAxis.hide() plotItem.axes[self.orientation]["item"] = self pos = plotItem.axes[self.orientation]["pos"] plotItem.layout.addItem(self, *pos) self.setZValue(-1000) def detachFromPlotItem(self): """Remove this axis from its attached PlotItem (not yet implemented) """ pass # TODO if __name__ == "__main__": import sys import pyqtgraph as pg from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.tpg import TaurusPlotDataItem app = TaurusApplication() # a standard pyqtgraph plot_item w = pg.PlotWidget() axis = DateAxisItem(orientation="bottom") axis.attachToPlotItem(w.getPlotItem()) # adding a taurus data item c2 = TaurusPlotDataItem() w.addItem(c2) w.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/000077500000000000000000000000001355007744100223215ustar00rootroot00000000000000taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/__init__.py000066400000000000000000000017631355007744100244410ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """Examples / recipes for usage of taurus with pyqtgraph""" taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/axislabels.py000066400000000000000000000027301355007744100250240ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """ This is an example of how to assign specific labels to arbitrary positions of a given axis using AxisItem.setTicks(). (Pure Qt) """ import sys from pyqtgraph.Qt import QtGui import pyqtgraph as pg import numpy as np if __name__ == "__main__": app = QtGui.QApplication([]) w = pg.PlotWidget() # define a list of position,label tuples ticks = [list(zip((1, 2, 3, 7, 8), ("a", "b", "c", "d", "e")))] xax = w.getAxis("bottom") xax.setTicks(ticks) w.plot(np.arange(9)) w.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/legendExample.py000066400000000000000000000042661355007744100254550ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """ Example on how to use a separate widget (LegendItem) for the legend of a plot. (Pure Qt) """ import pyqtgraph as pg from pyqtgraph.Qt import QtGui import sys if __name__ == "__main__": app = QtGui.QApplication([]) # instantiate the main plot plt = pg.PlotWidget() plt.setWindowTitle("pyqtgraph example: PLOT") # instantiate a graphics view to contain the legend gv = QtGui.QGraphicsView(QtGui.QGraphicsScene()) gv.setWindowTitle("pyqtgraph example: Legend") gv.setBackgroundBrush(QtGui.QBrush(QtGui.QColor("black"))) legend = pg.LegendItem(size=(100, 60), offset=(70, 30)) gv.scene().addItem(legend) # create 3 curves c1 = plt.plot( [1, 3, 2, 4], pen="r", symbol="o", symbolPen="r", symbolBrush=0.5, name="red plot", ) c2 = plt.plot( [2, 1, 4, 3], pen="g", fillLevel=0, fillBrush=(255, 255, 255, 30), name="green plot", ) c3 = plt.plot(list(range(7)), pen="c", fillLevel=0) # add the **named** curves to the legend for dataitem in plt.getPlotItem().listDataItems(): if dataitem.name(): legend.addItem(dataitem, dataitem.name()) plt.show() gv.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/taurusplotdataitem.py000066400000000000000000000034101355007744100266240ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """Example on using a tpg.TaurusPlotDataItem on a pure pyqtgraph plot""" if __name__ == "__main__": import sys import numpy from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.tpg import TaurusPlotDataItem import pyqtgraph as pg app = TaurusApplication() # a standard pyqtgraph plot_item w = pg.PlotWidget() # add legend to the plot, for that we have to give a name to plot items w.addLegend() # add a regular data item (non-taurus) c1 = pg.PlotDataItem(name="pg item", pen="b", fillLevel=0, brush="c") c1.setData(numpy.linspace(0, 2, 250)) w.addItem(c1) # add a taurus data item c2 = TaurusPlotDataItem(name="taurus item", pen="r", symbol="o") c2.setModel('eval:Quantity(rand(256),"m")') w.addItem(c2) w.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/taurustrendset.py000066400000000000000000000040331355007744100257670ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """Example on using a tpg.TaurusTrendSet and some related tools on a pure pyqtgraph plot""" if __name__ == "__main__": import sys from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.tpg import ( TaurusTrendSet, DateAxisItem, XAutoPanTool, ForcedReadTool, ) import pyqtgraph as pg from taurus.core.taurusmanager import TaurusManager taurusM = TaurusManager() taurusM.changeDefaultPollingPeriod(1000) # ms app = TaurusApplication() # Add a date-time X axis axis = DateAxisItem(orientation="bottom") w = pg.PlotWidget() axis.attachToPlotItem(w.getPlotItem()) # Add the auto-pan ("oscilloscope mode") tool autopan = XAutoPanTool() autopan.attachToPlotItem(w.getPlotItem()) # Add Forced-read tool fr = ForcedReadTool(w, period=1000) fr.attachToPlotItem(w.getPlotItem()) # add legend the legend tool w.addLegend() # adding a taurus data item... c2 = TaurusTrendSet(name="foo") c2.setModel("eval:rand(2)") w.addItem(c2) w.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/examples/y2axis.py000066400000000000000000000036521355007744100241200ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# """Example on using a tpg.Y2ViewBox to provide a secondary Y axis""" from PyQt5 import Qt import pyqtgraph as pg import numpy from taurus.qt.qtgui.tpg import Y2ViewBox, CurvesPropertiesTool if __name__ == "__main__": import sys app = Qt.QApplication([]) w = pg.PlotWidget() # add Y2 viewbox (provides a ViewBox associated to bottom & right axes) y2 = Y2ViewBox() y2.attachToPlotItem(w.getPlotItem()) # add a data item to Y1 (just as you would normally) c1 = pg.PlotDataItem(name="c1", pen="c") c1.setData(y=numpy.linspace(0, 20, 250)) w.addItem(c1) # add a data item to Y2 (similar, but adding it to the secondary ViewBox!) c2 = pg.PlotDataItem(name="c2", pen="y") c2.setData(y=numpy.random.rand(250)) y2.addItem(c2) # <- note that it is y2, not w ! # (optional) add CurvesPropertiesTool to switch curves between Y1 and Y2 t = CurvesPropertiesTool() t.attachToPlotItem(w.getPlotItem(), y2=y2) w.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/forcedreadtool.py000066400000000000000000000141401355007744100240510ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["ForcedReadTool"] from taurus.external.qt import QtGui, QtCore from taurus.qt.qtcore.configuration.configuration import BaseConfigurableClass class ForcedReadTool(QtGui.QWidgetAction, BaseConfigurableClass): """ This tool provides a menu option to control the "Forced Read" period of Plot data items that implement a `setForcedReadPeriod` method (see, e.g. :meth:`TaurusTrendSet.setForcedReadPeriod`). The force-read feature consists on forcing periodic attribute reads for those attributes being plotted with a :class:`TaurusTrendSet` object. This allows to force plotting periodical updates even for attributes for which the taurus polling is not enabled. Note that this is done at the widget level and therefore does not affect the rate of arrival of events for other widgets connected to the same attributes This tool inserts an action with a spinbox and emits a `valueChanged` signal whenever the value is changed. The connection between the data items and this tool can be done manually (by connecting to the `valueChanged` signal or automatically, if :meth:`autoconnect()` is `True` (default). The autoconnection feature works by discovering the compliant data items that share associated to the plot_item. This tool is implemented as an Action, and provides a method to attach it to a :class:`pyqtgraph.PlotItem` """ valueChanged = QtCore.pyqtSignal(int) def __init__( self, parent=None, period=0, text="Forced read", autoconnect=True ): BaseConfigurableClass.__init__(self) QtGui.QWidgetAction.__init__(self, parent) # defining the widget self._w = QtGui.QWidget() self._w.setLayout(QtGui.QHBoxLayout()) tt = "Period between forced readings.\nSet to 0 to disable" self._w.setToolTip(tt) self._label = QtGui.QLabel(text) self._w.layout().addWidget(self._label) self._sb = QtGui.QSpinBox() self._w.layout().addWidget(self._sb) self._sb.setRange(0, 604800000) self._sb.setValue(period) self._sb.setSingleStep(500) self._sb.setSuffix(" ms") self._sb.setSpecialValueText("disabled") self._autoconnect = autoconnect self.setDefaultWidget(self._w) # register config properties self.registerConfigProperty(self.period, self.setPeriod, "period") self.registerConfigProperty( self.autoconnect, self.setAutoconnect, "autoconnect" ) # internal conections self._sb.valueChanged[int].connect(self._onValueChanged) def _onValueChanged(self, period): """emit valueChanged and update all associated trendsets (if self.autoconnect=True """ self.valueChanged.emit(period) if self.autoconnect() and self.plot_item is not None: for item in self.plot_item.listDataItems(): if hasattr(item, "setForcedReadPeriod"): item.setForcedReadPeriod(period) def attachToPlotItem(self, plot_item): """Use this method to add this tool to a plot :param plot_item: (PlotItem) """ menu = plot_item.getViewBox().menu menu.addAction(self) self.plot_item = plot_item # force an update of period for connected trendsets self._onValueChanged(self.period()) def autoconnect(self): """Returns autoconnect state :return: (bool) """ return self._autoconnect def setAutoconnect(self, autoconnect): """Set autoconnect state. If True, the tool will autodetect trendsets associated to the plot item and will call setForcedReadPeriod on each of them for each change. If False, it will only emit a valueChanged signal and only those connected to it will be notified of changes :param autoconnect: (bool) """ self._autoconnect = autoconnect def period(self): """Returns the current period value (in ms) :return: (int) """ return self._sb.value() def setPeriod(self, value): """Change the period value. Use 0 for disabling :param period: (int) period in ms """ self._sb.setValue(value) if __name__ == "__main__": import taurus taurus.setLogLevel(taurus.Debug) import sys from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.tpg import TaurusTrendSet, DateAxisItem import pyqtgraph as pg # from taurus.qt.qtgui.tpg import ForcedReadTool app = TaurusApplication() w = pg.PlotWidget() axis = DateAxisItem(orientation="bottom") w = pg.PlotWidget() axis.attachToPlotItem(w.getPlotItem()) # test adding the curve before the tool ts1 = TaurusTrendSet(name="before", symbol="o") ts1.setModel("eval:rand()+1") w.addItem(ts1) fr = ForcedReadTool(w, period=1000) fr.attachToPlotItem(w.getPlotItem()) # test adding the curve after the tool ts2 = TaurusTrendSet(name="after", symbol="+") ts2.setModel("eval:rand()") w.addItem(ts2) w.show() ret = app.exec_() import pprint pprint.pprint(fr.createConfig()) sys.exit(ret) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/legendtool.py000066400000000000000000000045531355007744100232200ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["PlotLegendTool"] from taurus.external.qt import QtGui from taurus.qt.qtcore.configuration.configuration import BaseConfigurableClass class PlotLegendTool(QtGui.QWidgetAction, BaseConfigurableClass): """ This tool adds a legend to the PlotItem to which it is attached, and it inserts a checkable menu action for showing/hiding the legend. Implementation note: this is implemented as a QWidgetAction+QCheckBox instead of a checkable QAction to avoid closing the menu when toggling it """ def __init__(self, parent=None): BaseConfigurableClass.__init__(self) QtGui.QWidgetAction.__init__(self, parent) self._cb = QtGui.QCheckBox() self._cb.setText("Show legend") self.setDefaultWidget(self._cb) self.registerConfigProperty( self._cb.isChecked, self._cb.setChecked, "checked" ) # TODO: register config prop for legend position self._cb.toggled.connect(self._onToggled) self._legend = None def attachToPlotItem(self, plotItem): """ Use this method to add this tool to a plot :param plot_item: (PlotItem) """ self._legend = plotItem.addLegend() self._cb.setChecked(True) menu = plotItem.getViewBox().menu menu.addAction(self) def _onToggled(self, checked): if checked: self._legend.show() else: self._legend.hide() taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/plot.py000066400000000000000000000241631355007744100220410ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# from __future__ import absolute_import __all__ = ["TaurusPlot"] from future.utils import string_types import copy from taurus.external.qt import QtGui, Qt from taurus.core.util.containers import LoopList from taurus.qt.qtcore.configuration import BaseConfigurableClass from pyqtgraph import PlotWidget from .curvespropertiestool import CurvesPropertiesTool from .taurusmodelchoosertool import TaurusXYModelChooserTool from .legendtool import PlotLegendTool from .datainspectortool import DataInspectorTool from .taurusplotdataitem import TaurusPlotDataItem from .y2axis import Y2ViewBox CURVE_COLORS = [ Qt.QPen(Qt.Qt.red), Qt.QPen(Qt.Qt.blue), Qt.QPen(Qt.Qt.green), Qt.QPen(Qt.Qt.magenta), Qt.QPen(Qt.Qt.cyan), Qt.QPen(Qt.Qt.yellow), Qt.QPen(Qt.Qt.white), ] class TaurusPlot(PlotWidget, BaseConfigurableClass): """ TaurusPlot is a general widget for plotting 1D data sets. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusPlot incorporates the following tools/features: - Secondary Y axis (right axis) - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): if Qt.QT_VERSION < 0x050000: # Workaround for issue when using super with pyqt<5 BaseConfigurableClass.__init__(self) PlotWidget.__init__(self, parent=parent, **kwargs) else: super(TaurusPlot, self).__init__(parent=None, **kwargs) # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) # add save & retrieve configuration actions menu = self.getPlotItem().getViewBox().menu saveConfigAction = QtGui.QAction("Save configuration", menu) saveConfigAction.triggered.connect(self.saveConfigFile) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction("Retrieve saved configuration", menu) loadConfigAction.triggered.connect(self.loadConfigFile) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, "state") # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(self.getPlotItem()) # add model chooser self._model_chooser_tool = TaurusXYModelChooserTool(self) self._model_chooser_tool.attachToPlotItem( self.getPlotItem(), self, self._curveColors ) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(self.getPlotItem()) # add plot configuration dialog cprop_tool = CurvesPropertiesTool(self) cprop_tool.attachToPlotItem(self.getPlotItem(), y2=self._y2) # add a data inspector inspector_tool = DataInspectorTool(self) inspector_tool.attachToPlotItem(self.getPlotItem()) # Register config properties self.registerConfigDelegate(self._y2, "Y2Axis") self.registerConfigDelegate(legend_tool, "legend") self.registerConfigDelegate(inspector_tool, "inspector") # -------------------------------------------------------------------- # workaround for bug in pyqtgraph v<=0.10.0, already fixed in # https://github.com/pyqtgraph/pyqtgraph/commit/52754d4859 # TODO: remove this once pyqtgraph v>0.10 is released def __getattr__(self, item): try: return PlotWidget.__getattr__(self, item) except NameError: raise AttributeError( "{} has no attribute {}".format(self.__class__.__name__, item) ) # -------------------------------------------------------------------- def setModel(self, names): """Reimplemented to delegate to the """ # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.updateModels(names) def addModels(self, names): """Reimplemented to delegate to the """ # support passing a string in names if isinstance(names, string_types): names = [names] self._model_chooser_tool.addModels(names) def createConfig(self, allowUnpickable=False): """ Reimplemented from BaseConfigurableClass to manage the config properties of the curves attached to this plot """ try: # Temporarily register curves as delegates tmpreg = [] curve_list = self.getPlotItem().listDataItems() for idx, curve in enumerate(curve_list): if isinstance(curve, TaurusPlotDataItem): name = "__TaurusPlotDataItem_%d__" % idx tmpreg.append(name) self.registerConfigDelegate(curve, name) configdict = copy.deepcopy( BaseConfigurableClass.createConfig( self, allowUnpickable=allowUnpickable ) ) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) return configdict def applyConfig(self, configdict, depth=None): """ Reimplemented from BaseConfigurableClass to manage the config properties of the curves attached to this plot """ try: # Temporarily register curves as delegates tmpreg = [] curves = [] for name in configdict["__orderedConfigNames__"]: if name.startswith("__TaurusPlotDataItem_"): # Instantiate empty TaurusPlotDataItem curve = TaurusPlotDataItem() curves.append(curve) self.registerConfigDelegate(curve, name) tmpreg.append(name) # remove the curves from the second axis (Y2) for avoid dups self._y2.clearItems() BaseConfigurableClass.applyConfig( self, configdict=configdict, depth=depth ) # keep a dict of existing curves (to use it for avoiding dups) currentCurves = dict() for curve in self.getPlotItem().listDataItems(): if isinstance(curve, TaurusPlotDataItem): currentCurves[curve.getFullModelNames()] = curve # remove curves that exists in currentCurves, also remove from # the legend (avoid duplicates) for curve in curves: c = currentCurves.get(curve.getFullModelNames(), None) if c is not None: self.getPlotItem().legend.removeItem(c.name()) self.getPlotItem().removeItem(c) # Add to plot **after** their configuration has been applied for curve in curves: # First we add all the curves in self. This way the plotItem # can keeps a list of dataItems (plotItem.listDataItems()) self.addItem(curve) # Add curves to Y2 axis, when the curve configurations # have been applied. # Ideally, the Y2ViewBox class must handle the action of adding # curves to itself, but we want add the curves when they are # restored with all their properties. if curve.getFullModelNames() in self._y2.getCurves(): self.getPlotItem().getViewBox().removeItem(curve) self._y2.addItem(curve) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state["view"]["viewRange"] return state def plot_main( models=(), config_file=None, x_axis_mode="n", demo=False, window_name="TaurusPlot (pg)", ): """Launch a TaurusPlot""" import sys from taurus.qt.qtgui.application import TaurusApplication app = TaurusApplication(cmd_line_parser=None, app_name="taurusplot(pg)") w = TaurusPlot() # w.loadConfigFile('tmp/TaurusPlot.pck') w.setWindowTitle(window_name) if demo: models = list(models) models.extend(["eval:rand(100)", "eval:0.5*sqrt(arange(100))"]) if x_axis_mode.lower() == "t": from taurus.qt.qtgui.tpg import DateAxisItem axis = DateAxisItem(orientation="bottom") axis.attachToPlotItem(w.getPlotItem()) if config_file is not None: w.loadConfigFile(config_file) if models: w.setModel(models) w.show() ret = app.exec_() # import pprint # pprint.pprint(w.createConfig()) sys.exit(ret) if __name__ == "__main__": plot_main() taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/taurusimageitem.py000066400000000000000000000042321355007744100242630ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["TaurusImageItem"] import sys from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.base import TaurusBaseComponent from pyqtgraph import ImageItem class TaurusImageItem(ImageItem, TaurusBaseComponent): """ Displays 2D and 3D image data """ # TODO: clear image if .setModel(None) def __init__(self, *args, **kwargs): ImageItem.__init__(self, *args, **kwargs) TaurusBaseComponent.__init__(self, "TaurusImageItem") def handleEvent(self, evt_src, evt_type, evt_val): try: data = evt_val.rvalue self.setImage(data) except Exception as e: self.warning("Exception in handleEvent: %s", e) if __name__ == "__main__": import pyqtgraph as pg app = TaurusApplication() plot_widget = pg.PlotWidget() plot_item = plot_widget.getPlotItem() image_item = TaurusImageItem() # Add taurus 2D image data image_item.setModel("eval:randint(0,256,(16,16))") # add TarusImageItem to a PlotItem plot_item.addItem(image_item) # show or hide axis from the plot plot_item.showAxis("left", show=True) plot_item.showAxis("bottom", show=True) plot_widget.show() sys.exit(app.exec_()) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/taurusmodelchoosertool.py000066400000000000000000000433671355007744100257170ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["TaurusModelChooserTool", "TaurusImgModelChooserTool"] from builtins import bytes from future.utils import string_types from taurus.external.qt import Qt from taurus.core import TaurusElementType from taurus.qt.qtgui.panel import TaurusModelChooser from taurus_pyqtgraph.taurusimageitem import TaurusImageItem from taurus_pyqtgraph.taurusplotdataitem import TaurusPlotDataItem from taurus_pyqtgraph.curvesmodel import TaurusItemConf, TaurusItemConfDlg import taurus from collections import OrderedDict from taurus.qt.qtcore.mimetypes import ( TAURUS_MODEL_LIST_MIME_TYPE, TAURUS_ATTR_MIME_TYPE, ) class TaurusModelChooserTool(Qt.QAction): """ This tool inserts an action in the menu of the :class:`pyqtgraph.PlotItem` to which it is attached to show choosing taurus models to be shown. It is implemented as an Action, and provides a method to attach it to a PlotItem. """ def __init__(self, parent=None, itemClass=None): Qt.QAction.__init__(self, "Model selection", parent) self.triggered.connect(self._onTriggered) self.plot_item = None self.legend = None if itemClass is None: itemClass = TaurusPlotDataItem self.itemClass = itemClass def attachToPlotItem(self, plot_item, parentWidget=None): """ Use this method to add this tool to a plot :param plot_item: (PlotItem) """ self.plot_item = plot_item if self.plot_item.legend is not None: self.legend = self.plot_item.legend menu = self.plot_item.getViewBox().menu menu.addAction(self) self.setParent(parentWidget or menu) def _onTriggered(self): currentModelNames = [] for item in self.plot_item.items: if isinstance(item, self.itemClass): currentModelNames.append(item.getFullModelName()) names, ok = TaurusModelChooser.modelChooserDlg( selectables=[TaurusElementType.Attribute], listedModels=currentModelNames, ) if ok: self.updateModels(names) def updateModels(self, names): """Accepts a list of model names and updates the data items of class `itemClass` (provided in the constructor) attached to the plot. It creates and removes items if needed, and enforces the z-order according to that given in the `models` list """ # from names, construct an ordered dict with k=fullname, v=modelObj models = OrderedDict() for n in names: m = taurus.Attribute(n) models[m.getFullName()] = m # construct a dict and a list for current models and names currentModelItems = dict() currentModelNames = [] for item in self.plot_item.items: if isinstance(item, self.itemClass): fullname = item.getFullModelName() currentModelNames.append(fullname) currentModelItems[fullname] = item # remove existing curves from plot (but not discarding the object) # so that they can be re-added later in the correct z-order for k, v in currentModelItems.items(): # v.getViewBox().removeItem(v) # TODO: maybe needed for Y2 self.plot_item.removeItem(v) # ------------------------------------------------- # Workaround for bug in pyqtgraph 0.10.0 # (which is fixed in pyqtgraph's commit ee0ea5669) # TODO: remove this lines when pyqtgraph > 0.10.0 is released if self.legend is not None: self.legend.removeItem(v.name()) # ------------------------------------------------- # Add all curves (creating those that did not exist previously) # respecting the z-order for modelName, model in models.items(): if modelName in currentModelNames: item = currentModelItems[modelName] self.plot_item.addItem(item) # item.getViewBox().addItem(item) # TODO: maybe needed for Y2 else: # TODO support labels item = self.itemClass(name=model.getSimpleName()) item.setModel(modelName) self.plot_item.addItem(item) # self.plot_item.enableAutoRange() # TODO: Why? remove? def setParent(self, parent): """Reimplement setParent to add an event filter""" Qt.QAction.setParent(self, parent) if parent is not None: parent.installEventFilter(self) def _dropMimeData(self, data): """Method to process the dropped MimeData""" ymodels = [] if data.hasFormat(TAURUS_ATTR_MIME_TYPE): m = bytes(data.data(TAURUS_ATTR_MIME_TYPE)).decode("utf-8") ymodels.append(m) elif data.hasFormat(TAURUS_MODEL_LIST_MIME_TYPE): ymodels = ( bytes(data.data(TAURUS_MODEL_LIST_MIME_TYPE)) .decode("utf-8") .split() ) elif data.hasText(): ymodels.append(data.text()) # Add models current = [] for item in self.plot_item.items: if isinstance(item, self.itemClass): current.append(item.getFullModelName()) self.updateModels(current + ymodels) return True def eventFilter(self, source, event): """ Reimplementation of eventFilter to delegate parent's drag and drop events to TaurusModelChooserTool """ if source is self.parent(): if event.type() == Qt.QEvent.DragEnter: event.acceptProposedAction() return True if event.type() == Qt.QEvent.Drop: event.acceptProposedAction() return self._dropMimeData(event.mimeData()) return self.parent().eventFilter(source, event) class TaurusImgModelChooserTool(Qt.QAction): """ This tool inserts an action in the menu of the :class:`pyqtgraph.PlotItem` to which it is attached for choosing a 2D taurus model to be shown. It is implemented as an Action, and provides a method to attach it to a PlotItem. """ # TODO: merge this with TaurusModelChooserTool (or use a common base) def __init__(self, parent=None): Qt.QAction.__init__(self, parent) self._plot_item = None def attachToPlotItem(self, plot_item): """ Use this method to add this tool to a plot :param plot_item: (PlotItem) """ self._plot_item = plot_item view = plot_item.getViewBox() menu = view.menu model_chooser = Qt.QAction("Model selection", menu) model_chooser.triggered.connect(self._onTriggered) menu.addAction(model_chooser) def _onTriggered(self): imageItem = None for item in self._plot_item.items: if isinstance(item, TaurusImageItem): imageItem = item break if imageItem is None: imageItem = TaurusImageItem() modelName = imageItem.getFullModelName() if modelName is None: listedModels = [] else: listedModels = [modelName] res, ok = TaurusModelChooser.modelChooserDlg( selectables=[TaurusElementType.Attribute], singleModel=True, listedModels=listedModels, ) if ok: if res: model = res[0] else: model = None imageItem.setModel(model) class TaurusXYModelChooserTool(Qt.QAction): """ (Work-in-Progress) This tool inserts an action in the menu of the :class:`pyqtgraph.PlotItem` to which it is attached for choosing X and Y 1D taurus models of the curves to be shown. It is implemented as an Action, and provides a method to attach it to a PlotItem. It only deals with the subgroup of plot data items of the type defined by `self.ItemClass` (which defaults to :class:`TaurusPlotDataItem`) """ # TODO: This class is WIP. def __init__(self, parent=None, itemClass=None): Qt.QAction.__init__(self, "Model selection", parent) self.triggered.connect(self._onTriggered) self.plot_item = None self.legend = None self._curveColors = None if itemClass is None: itemClass = TaurusPlotDataItem self.itemClass = itemClass def setParent(self, parent): """Reimplement setParent to add an event filter""" Qt.QAction.setParent(self, parent) if parent is not None: parent.installEventFilter(self) def attachToPlotItem( self, plot_item, parentWidget=None, curve_colors=None ): """ Use this method to add this tool to a plot :param plot_item: (PlotItem) .. warning:: this is Work-in-progress. The API may change. Do not rely on current signature of this method """ # TODO: Check if we can simplify the signature (remove keyword args) self.plot_item = plot_item self._curveColors = curve_colors if self.plot_item.legend is not None: self.legend = self.plot_item.legend menu = self.plot_item.getViewBox().menu menu.addAction(self) self.setParent(parentWidget or menu) def _onTriggered(self): oldconfs = self._getTaurusPlotDataItemConfigs().values() newconfs, ok = TaurusItemConfDlg.showDlg( parent=self.parent(), taurusItemConf=oldconfs ) if ok: xy_names = [(c.xModel, c.yModel) for c in newconfs] self.updateModels(xy_names) # TODO: apply configurations too def _getTaurusPlotDataItemConfigs(self): """Get all the TaurusItemConf of the existing TaurusPlotDataItems Returns an ordered dict whose keys are (xfullname, yfullname) and whose values are the corresponding item config class """ itemconfigs = OrderedDict() for curve in self.plot_item.listDataItems(): if isinstance(curve, self.itemClass): xmodel, ymodel = curve.getFullModelNames() c = TaurusItemConf( YModel=ymodel, XModel=xmodel, name=curve.name() ) itemconfigs[(xmodel, ymodel)] = c return itemconfigs def _dropMimeData(self, data): """Method to process the dropped MimeData""" ymodels = [] if data.hasFormat(TAURUS_ATTR_MIME_TYPE): m = bytes(data.data(TAURUS_ATTR_MIME_TYPE)).decode("utf-8") ymodels.append(m) elif data.hasFormat(TAURUS_MODEL_LIST_MIME_TYPE): ymodels = ( bytes(data.data(TAURUS_MODEL_LIST_MIME_TYPE)) .decode("utf-8") .split() ) elif data.hasText(): ymodels.append(data.text()) xmodels = [None] * len(ymodels) self.addModels(list(zip(xmodels, ymodels))) return True def eventFilter(self, source, event): """ Reimplementation of eventFilter to delegate parent's drag and drop events to TaurusXYModelChooserTool """ if source is self.parent(): if event.type() == Qt.QEvent.DragEnter: event.acceptProposedAction() return True if event.type() == Qt.QEvent.Drop: event.acceptProposedAction() return self._dropMimeData(event.mimeData()) return self.parent().eventFilter(source, event) def getModelNames(self): """ Get the x and y model names for the data items of type defined by `self.itemClass` present in the plot item to which this tool is attached """ return list(self._getTaurusPlotDataItemConfigs().keys()) def addModels(self, xy_names): """Add new items with the given x and y model pairs. Those given model pairs that are already present will not be altered or duplicated (e.g. the z-order of the corresponding curve will not be modified for in case of adding duplicates) """ current = self.getModelNames() self.updateModels(current + xy_names) def updateModels(self, xy_names): """ Update the current plot item list with the given configuration items """ mainViewBox = self.plot_item.getViewBox() # Remove existing taurus curves from the plot (but keep the item object # and a reference to their viewbox so that they can be readded # later on if needed. currentModelItems = OrderedDict() _currentCurves = list(self.plot_item.listDataItems()) for curve in _currentCurves: if isinstance(curve, self.itemClass): xname, yname = curve.getFullModelNames() viewbox = curve.getViewBox() # store curve and current viewbox for later use currentModelItems[(xname, yname)] = curve, viewbox # remove the curve self.plot_item.removeItem(curve) # if viewbox is not mainViewBox: # TODO: do we need this? # viewbox.removeItem(curve) if self.legend is not None: self.legend.removeItem(curve.name()) # Add only the curves defined in xy_names (reusing existing ones and # creating those that did not exist) in the desired z-order _already_added = [] for xy_name in xy_names: # each member of xy_names can be yname or a (xname, yname) tuple if isinstance(xy_name, string_types): xname, yname = None, xy_name else: xname, yname = xy_name # make sure that fullnames are used try: if xname is not None: xmodel = taurus.Attribute(xname) xname = xmodel.getFullName() ymodel = taurus.Attribute(yname) yname = ymodel.getFullName() except Exception as e: from taurus import warning warning("Problem adding %r: %r", (xname, yname), e) continue # do not add it again if we already added it (avoid duplications) if (xname, yname) in _already_added: continue _already_added.append((xname, yname)) # if the item already existed, re-use it if (xname, yname) in currentModelItems: item, viewbox = currentModelItems[(xname, yname)] self.plot_item.addItem(item) if viewbox is not mainViewBox: # if the curve was originally associated to a viewbox # other than the main one, we should move it there after # re-adding it to the plot_item mainViewBox.removeItem(item) viewbox.addItem(item) # if it is a new curve, create it and add it to the main plot_item else: item = self.itemClass( xModel=xname, yModel=yname, name=ymodel.getSimpleName() ) if self._curveColors is not None: item.setPen(self._curveColors.next().color()) self.plot_item.addItem(item) def _demo_ModelChooser(): import sys import numpy import pyqtgraph as pg from taurus.qt.qtgui.tpg import TaurusModelChooserTool from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.tpg import TaurusPlotDataItem app = TaurusApplication() # a standard pyqtgraph plot_item w = pg.PlotWidget() # add legend to the plot, for that we have to give a name to plot items w.addLegend() # adding a regular data item (non-taurus) c1 = pg.PlotDataItem(name="st plot", pen="b", fillLevel=0, brush="c") c1.setData(numpy.arange(300) / 300.0) w.addItem(c1) # adding a taurus data item c2 = TaurusPlotDataItem(name="st2 plot", pen="r", symbol="o") c2.setModel("eval:rand(222)") w.addItem(c2) # attach to plot item tool = TaurusModelChooserTool(itemClass=TaurusPlotDataItem) tool.attachToPlotItem(w.getPlotItem()) w.show() tool.trigger() sys.exit(app.exec_()) def _demo_ModelChooserImage(): import sys from taurus.qt.qtgui.tpg import TaurusImgModelChooserTool, TaurusImageItem from taurus.qt.qtgui.application import TaurusApplication import pyqtgraph as pg app = TaurusApplication() w = pg.PlotWidget() img = TaurusImageItem() # Add taurus 2D image data img.setModel("eval:rand(256,256)") w.addItem(img) w.showAxis("left", show=False) w.showAxis("bottom", show=False) tool = TaurusImgModelChooserTool() tool.attachToPlotItem(w.getPlotItem()) w.show() tool.trigger() sys.exit(app.exec_()) if __name__ == "__main__": _demo_ModelChooser() # _demo_ModelChooserImage() taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/taurusplotdataitem.py000066400000000000000000000132031355007744100250070ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["TaurusPlotDataItem"] import copy from taurus import Attribute from taurus.core import TaurusEventType from taurus.qt.qtgui.base import TaurusBaseComponent from pyqtgraph import PlotDataItem class TaurusPlotDataItem(PlotDataItem, TaurusBaseComponent): """A taurus-ified PlotDataItem""" def __init__(self, *args, **kwargs): """ Accepts same args and kwargs as PlotDataItem, plus: :param xModel: (str) Taurus model name for abscissas values. Default=None :param yModel: (str) Taurus model name for ordinate values. Default=None """ xModel = kwargs.pop("xModel", None) yModel = kwargs.pop("yModel", None) PlotDataItem.__init__(self, *args, **kwargs) TaurusBaseComponent.__init__(self, "TaurusBaseComponent") self._x = None self._y = None self.xModel = None if xModel is not None: self.setXModel(xModel) if yModel is not None: self.setModel(yModel) self.registerConfigProperty(self.getOpts, self.setOpts, "opts") self.setModelInConfig(True) self.registerConfigProperty( self.getXModelName, self.setXModel, "XModel" ) def setXModel(self, xModel): if self.xModel is not None: self.xModel.removeListener(self) if not xModel: self.xModel = None return self.xModel = Attribute(xModel) self.xModel.addListener(self) def getXModelName(self): if self.xModel is None: return None else: return self.xModel.getFullName() def handleEvent(self, evt_src, evt_type, evt_value): if evt_type not in (TaurusEventType.Change, TaurusEventType.Periodic): return yModel = self.getModelObj() if yModel == evt_src: if yModel is not None: self._y = evt_value.rvalue else: self._y = None if self.xModel == evt_src: if self.xModel is not None: self._x = evt_value.rvalue else: self._x = None try: self.setData(x=self._x, y=self._y) except Exception as e: self.debug("Could not set data. Reason: %r", e) def getOpts(self): from taurus.qt.qtgui.tpg import serialize_opts return serialize_opts(copy.copy(self.opts)) def setOpts(self, opts): # creates QPainters (QPen or QBrush) from a pickle loaded file # for adapt the serialized objects into PlotDataItem properties from taurus.qt.qtgui.tpg import deserialize_opts self.opts = deserialize_opts(opts) # This is a workaround for the following pyqtgraph's bug: # https://github.com/pyqtgraph/pyqtgraph/issues/531 if opts["connect"] == "all": self.opts["connect"] = "all" elif opts["connect"] == "pairs": self.opts["connect"] = "pairs" elif opts["connect"] == "finite": self.opts["connect"] = "finite" def getFullModelNames(self): return (self.getXModelName(), self.getFullModelName()) if __name__ == "__main__": import sys import numpy import pyqtgraph as pg from taurus.qt.qtgui.application import TaurusApplication # from taurus.qt.qtgui.tpg import TaurusPlotDataItem from taurus.external.qt import Qt from taurus.qt.qtgui.tpg import TaurusModelChooserTool app = TaurusApplication() # a standard pyqtgraph plot_item w = pg.PlotWidget() # add legend to the plot, for that we have to give a name to plot items w.addLegend() # adding a regular data item (non-taurus) c1 = pg.PlotDataItem(name="st plot", pen="b", fillLevel=0, brush="c") c1.setData(numpy.arange(300) / 300.0) w.addItem(c1) pen = pg.mkPen(color="r", style=4) brush = pg.mkBrush(color="b") brush.setStyle(3) # adding a taurus data item # c2 = TaurusPlotDataItem(name='st2 plot', pen='r', symbol='o') c2 = TaurusPlotDataItem(pen=pen, name="foo") # c2 = TaurusPlotDataItem() # c2.loadConfigFile('tmp/conf.cfg') c2.setModel('eval:Quantity(rand(256),"m")') # c2.setModel('sys/tg_test/1/wave') # c2.setModel(None) # c2.setXModel(None) # c2.setXModel('eval:Quantity(rand(256),"m")') w.addItem(c2) # ...and remove it after a while def rem(): w.removeItem(c2) Qt.QTimer.singleShot(2000, rem) modelchooser = TaurusModelChooserTool(itemClass=TaurusPlotDataItem) modelchooser.attachToPlotItem(w.getPlotItem()) w.show() res = app.exec_() # config = c2.createConfig() # print config # c2.saveConfigFile('tmp/conf.cfg') sys.exit(res) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/taurustrendset.py000066400000000000000000000353601355007744100241600ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["TaurusTrendSet"] """This provides the pyqtgraph implementation of :class:`TaurusTrendSet`""" import copy import numpy from taurus.core import TaurusEventType, TaurusTimeVal from taurus.qt.qtgui.base import TaurusBaseComponent from taurus.core.util.containers import ArrayBuffer, LoopList from taurus.external.qt import Qt from pyqtgraph import PlotDataItem from taurus_pyqtgraph.forcedreadtool import ForcedReadTool import taurus CURVE_COLORS = [ Qt.QPen(Qt.Qt.red), Qt.QPen(Qt.Qt.blue), Qt.QPen(Qt.Qt.green), Qt.QPen(Qt.Qt.magenta), Qt.QPen(Qt.Qt.cyan), Qt.QPen(Qt.Qt.yellow), Qt.QPen(Qt.Qt.white), ] class TaurusTrendSet(PlotDataItem, TaurusBaseComponent): """ A PlotDataItem for displaying trend curve(s) associated to a TaurusAttribute. The TaurusTrendSet itself does not contain any data, but acts as a manager that dynamically adds/removes curve(s) (other PlotDataItems) to its associated plot. If the attribute is a scalar, the Trend Set generates only one curve representing the evolution of the value of the attribute. If the attribute is an array, as many curves as the attribute size are created, each representing the evolution of the value of a component of the array. When an event is received, all curves belonging to a TaurusTrendSet are updated. TaurusTrendSet can be considered used as a container of (sorted) curves. As such, the curves contained by it can be accessed by index:: ts = TaurusTrendSet('eval:rand(3)') # (...) wait for a Taurus Event arriving so that the curves are created ncurves = len(ts) # ncurves will be 3 (assuming the event arrived) curve0 = ts[0] # you can access the curve by index Note that internally each curve is a :class:`pyqtgraph.PlotDataItem` (i.e., it is not aware of events by itself, but it relies on the TaurusTrendSet object to update its values) """ def __init__(self, *args, **kwargs): PlotDataItem.__init__(self, *args, **kwargs) TaurusBaseComponent.__init__(self, "TaurusBaseComponent") self._UImodifiable = False self._maxBufferSize = 65536 # (=2**16, i.e., 64K events)) self._xBuffer = None self._yBuffer = None self._curveColors = LoopList(CURVE_COLORS) self._args = args self._kwargs = kwargs self._curves = [] self._timer = Qt.QTimer() self._timer.timeout.connect(self._forceRead) self._legend = None # register config properties self.setModelInConfig(True) self.registerConfigProperty( self._getCurvesOpts, self._setCurvesOpts, "opts" ) # TODO: store forceReadPeriod config # TODO: store _maxBufferSize config def name(self): """Reimplemented from PlotDataItem to avoid having the ts itself added to legends. .. seealso:: :meth:`basename` """ return None def base_name(self): """Returns the name of the trendset, which is used as a prefix for constructing the associated curves names .. seealso:: :meth:`name` """ return PlotDataItem.name(self) def __getitem__(self, k): return self._curves[k] def __len__(self): return len(self._curves) def __contains__(self, k): return k in self._curves def setModel(self, name): """Reimplemented from :meth:`TaurusBaseComponent.setModel`""" TaurusBaseComponent.setModel(self, name) # force a read to ensure that the curves are created self._forceRead() def _initBuffers(self, ntrends): """initializes new x and y buffers""" self._yBuffer = ArrayBuffer( numpy.zeros((min(128, self._maxBufferSize), ntrends), dtype="d"), maxSize=self._maxBufferSize, ) self._xBuffer = ArrayBuffer( (numpy.zeros(min(128, self._maxBufferSize), dtype="d")), maxSize=self._maxBufferSize, ) def _initCurves(self, ntrends): """ Initializes new curves """ # self._removeFromLegend(self._legend) self._curves = [] self._curveColors.setCurrentIndex(-1) a = self._args kw = self._kwargs.copy() base_name = ( self.base_name() or taurus.Attribute(self.getModel()).getSimpleName() ) for i in range(ntrends): subname = "%s[%i]" % (base_name, i) kw["name"] = subname curve = PlotDataItem(*a, **kw) if "pen" not in kw: curve.setPen(self._curveColors.next().color()) self._curves.append(curve) self._updateViewBox() def _addToLegend(self, legend): # ------------------------------------------------------------------ # In theory, TaurusTrendSet only uses viewBox.addItem to add its # sub-curves to the plot. In theory this should not add the curves # to the legend, and therefore we should do it here. # But somewhere the curves are already being added to the legend, and # if we re-add them here we get duplicated legend entries # TODO: Find where are the curves being added to the legend pass # if legend is None: # return # for c in self._curves: # legend.addItem(c, c.name()) # ------------------------------------------------------------------- def _removeFromLegend(self, legend): if legend is None: return for c in self._curves: legend.removeItem(c.name()) def _updateViewBox(self): """Add/remove the "extra" curves from the viewbox if needed""" if self._curves: viewBox = self.getViewBox() self.forgetViewBox() for curve in self._curves: curve.forgetViewBox() curve_viewBox = curve.getViewBox() if curve_viewBox is not None: curve_viewBox.removeItem(curve) if viewBox is not None: viewBox.addItem(curve) def _updateBuffers(self, evt_value): """Update the x and y buffers with the new data. If the new data is not compatible with the existing buffers, the buffers are reset """ # TODO: we use .magnitude below to avoid issue #509 in pint # https://github.com/hgrecco/pint/issues/509 ntrends = numpy.size(evt_value.rvalue.magnitude) if not self._isDataCompatible(evt_value, ntrends): self._initBuffers(ntrends) self._yUnits = evt_value.rvalue.units self._initCurves(ntrends) try: self._yBuffer.append(evt_value.rvalue.to(self._yUnits).magnitude) except Exception as e: self.warning( "Problem updating buffer Y (%s):%s", evt_value.rvalue, e ) evt_value = None try: self._xBuffer.append(evt_value.time.totime()) except Exception as e: self.warning("Problem updating buffer X (%s):%s", evt_value, e) return self._xBuffer.contents(), self._yBuffer.contents() def _isDataCompatible(self, evt_value, ntrends): """ Check that the new evt_value is compatible with the current data in the buffers. Check shape and unit compatibility. """ if self._xBuffer is None or self._yBuffer is None: return False rvalue = evt_value.rvalue if rvalue.dimensionality != self._yUnits.dimensionality: return False current_trends = numpy.prod(self._yBuffer.contents().shape[1:]) if ntrends != current_trends: return False return True def _addData(self, x, y): for i, curve in enumerate(self._curves): curve.setData(x=x, y=y[:, i]) def handleEvent(self, evt_src, evt_type, evt_value): """Reimplementation of :meth:`TaurusBaseComponent.handleEvent`""" # model = evt_src if evt_src is not None else self.getModelObj() # TODO: support boolean values from evt_value.rvalue if evt_value is None or evt_value.rvalue is None: self.info("Invalid value. Ignoring.") return else: try: xValues, yValues = self._updateBuffers(evt_value) except Exception: # TODO: handle dropped events see: TaurusTrend._onDroppedEvent raise self._addData(xValues, yValues) def parentChanged(self): """Reimplementation of :meth:`PlotDataItem.parentChanged` to handle the change of the containing viewbox """ PlotDataItem.parentChanged(self) self._updateViewBox() # update legend if needed try: legend = self.getViewWidget().getPlotItem().legend except Exception: legend = None if legend is not self._legend: self._removeFromLegend(self._legend) self._addToLegend(legend) self._legend = legend # Set period from ForcedReadTool (if found) try: for a in self.getViewBox().menu.actions(): if isinstance(a, ForcedReadTool) and a.autoconnect(): self.setForcedReadPeriod(a.period()) break except Exception as e: self.debug("cannot set period from ForcedReadTool: %r", e) @property def forcedReadPeriod(self): """Returns the forced reading period (in ms). A value <= 0 indicates that the forced reading is disabled """ return self._timer.interval() def setForcedReadPeriod(self, period): """ Forces periodic reading of the subscribed attribute in order to show new points even if no events are received. It will create fake events as needed with the read value. It will also block the plotting of regular events when period > 0. :param period: (int) period in milliseconds. Use period<=0 to stop the forced periodic reading """ # stop the timer and remove the __ONLY_OWN_EVENTS filter self._timer.stop() filters = self.getEventFilters() if self.__ONLY_OWN_EVENTS in filters: filters.remove(self.__ONLY_OWN_EVENTS) self.setEventFilters(filters) # if period is positive, set the filter and start if period > 0: self.insertEventFilter(self.__ONLY_OWN_EVENTS) self._timer.start(period) def _forceRead(self, cache=False): """Forces a read of the associated attribute. :param cache: (bool) If True, the reading will be done with cache=True but the timestamp of the resulting event will be replaced by the current time. If False, no cache will be used at all. """ value = self.getModelValueObj(cache=cache) if cache and value is not None: value = copy.copy(value) value.time = TaurusTimeVal.now() self.fireEvent(self, TaurusEventType.Periodic, value) def __ONLY_OWN_EVENTS(self, s, t, v): """An event filter that rejects all events except those that originate from this object """ if s is self: return s, t, v else: return None def _getCurvesOpts(self): """returns a list of serialized opts (one for each curve)""" from taurus.qt.qtgui.tpg import serialize_opts return [serialize_opts(copy.copy(c.opts)) for c in self._curves] def _setCurvesOpts(self, all_opts): """restore options to curves""" # If no curves are yet created, force a read to create them if not self._curves: self._forceRead(cache=True) # Check consistency in the number of curves if len(self._curves) != len(all_opts): self.warning( "Cannot apply curve options (mismatch in curves number)" ) return from taurus.qt.qtgui.tpg import deserialize_opts for c, opts in zip(self._curves, all_opts): c.opts = deserialize_opts(opts) # This is a workaround for the following pyqtgraph's bug: # https://github.com/pyqtgraph/pyqtgraph/issues/531 if opts["connect"] == "all": c.opts["connect"] = "all" elif opts["connect"] == "pairs": c.opts["connect"] = "pairs" elif opts["connect"] == "finite": c.opts["connect"] = "finite" if __name__ == "__main__": import sys import pyqtgraph as pg from taurus.qt.qtgui.application import TaurusApplication from taurus.qt.qtgui.tpg import ( # TaurusTrendSet, DateAxisItem, XAutoPanTool, # TaurusModelChooserTool, CurvesPropertiesTool, ) from taurus.core.taurusmanager import TaurusManager taurusM = TaurusManager() taurusM.changeDefaultPollingPeriod(1000) # ms app = TaurusApplication() # a standard pyqtgraph plot_item axis = DateAxisItem(orientation="bottom") w = pg.PlotWidget() axis.attachToPlotItem(w.getPlotItem()) cp = CurvesPropertiesTool() cp.attachToPlotItem(w.getPlotItem()) autopan = XAutoPanTool() autopan.attachToPlotItem(w.getPlotItem()) # add legend to the plot, for that we have to give a name to plot items w.addLegend() # adding a taurus data item... c2 = TaurusTrendSet(name="foo") c2.setModel("eval:rand(2)") # c2.setForcedReadPeriod(500) w.addItem(c2) # ...and remove it after a while def rem(): w.removeItem(c2) Qt.QTimer.singleShot(2000, rem) # modelchooser = TaurusModelChooserTool(itemClass=TaurusTrendSet) # modelchooser.attachToPlotItem(w.getPlotItem()) w.show() ret = app.exec_() # import pprint # pprint.pprint(c2.createConfig()) sys.exit(ret) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/trend.py000066400000000000000000000246451355007744100222040ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# from __future__ import absolute_import __all__ = ["TaurusTrend"] from future.utils import string_types import copy from taurus.external.qt import QtGui, Qt from taurus.core.util.containers import LoopList from taurus.qt.qtcore.configuration import BaseConfigurableClass from pyqtgraph import PlotWidget from .curvespropertiestool import CurvesPropertiesTool from .dateaxisitem import DateAxisItem from .legendtool import PlotLegendTool from .forcedreadtool import ForcedReadTool from .datainspectortool import DataInspectorTool from .taurusmodelchoosertool import TaurusModelChooserTool from .taurustrendset import TaurusTrendSet from .y2axis import Y2ViewBox from .autopantool import XAutoPanTool CURVE_COLORS = [ Qt.QPen(Qt.Qt.red), Qt.QPen(Qt.Qt.blue), Qt.QPen(Qt.Qt.green), Qt.QPen(Qt.Qt.magenta), Qt.QPen(Qt.Qt.cyan), Qt.QPen(Qt.Qt.yellow), Qt.QPen(Qt.Qt.white), ] class TaurusTrend(PlotWidget, BaseConfigurableClass): """ TaurusTrend is a general widget for plotting the evolution of a value over time. It is an extended taurus-aware version of :class:`pyqtgraph.PlotWidget`. Apart from all the features already available in a regulat PlotWidget, TaurusTrend incorporates the following tools/features: - Secondary Y axis (right axis) - Time X axis - A plot configuration dialog, and save/restore configuration facilities - A menu option for adding/removing taurus models - A menu option for showing/hiding the legend - Automatic color change of curves for newly added models """ def __init__(self, parent=None, **kwargs): if Qt.QT_VERSION < 0x050000: # Workaround for issue when using super with pyqt<5 BaseConfigurableClass.__init__(self) PlotWidget.__init__(self, parent=parent, **kwargs) else: super(TaurusTrend, self).__init__(parent=parent, **kwargs) # set up cyclic color generator self._curveColors = LoopList(CURVE_COLORS) self._curveColors.setCurrentIndex(-1) plot_item = self.getPlotItem() menu = plot_item.getViewBox().menu # add save & retrieve configuration actions saveConfigAction = QtGui.QAction("Save configuration", menu) saveConfigAction.triggered.connect(self.saveConfigFile) menu.addAction(saveConfigAction) loadConfigAction = QtGui.QAction("Retrieve saved configuration", menu) loadConfigAction.triggered.connect(self.loadConfigFile) menu.addAction(loadConfigAction) self.registerConfigProperty(self._getState, self.restoreState, "state") # add legend tool legend_tool = PlotLegendTool(self) legend_tool.attachToPlotItem(plot_item) # add model chooser self._model_chooser_tool = TaurusModelChooserTool( self, itemClass=TaurusTrendSet ) self._model_chooser_tool.attachToPlotItem(plot_item, self) # add Y2 axis self._y2 = Y2ViewBox() self._y2.attachToPlotItem(plot_item) # Add time X axis axis = DateAxisItem(orientation="bottom") axis.attachToPlotItem(plot_item) # add plot configuration dialog cprop_tool = CurvesPropertiesTool(self) cprop_tool.attachToPlotItem(plot_item, y2=self._y2) # add data inspector widget inspector_tool = DataInspectorTool(self) inspector_tool.attachToPlotItem(self.getPlotItem()) # add force read tool fr_tool = ForcedReadTool(self) fr_tool.attachToPlotItem(self.getPlotItem()) # Add the auto-pan ("oscilloscope mode") tool autopan = XAutoPanTool() autopan.attachToPlotItem(self.getPlotItem()) # Register config properties self.registerConfigDelegate(self._y2, "Y2Axis") self.registerConfigDelegate(legend_tool, "legend") self.registerConfigDelegate(fr_tool, "forceread") self.registerConfigDelegate(inspector_tool, "inspector") # -------------------------------------------------------------------- # workaround for bug in pyqtgraph v<=0.10.0, already fixed in # https://github.com/pyqtgraph/pyqtgraph/commit/52754d4859 # TODO: remove this once pyqtgraph v>0.10 is released def __getattr__(self, item): try: return PlotWidget.__getattr__(self, item) except NameError: raise AttributeError( "{} has no attribute {}".format(self.__class__.__name__, item) ) # -------------------------------------------------------------------- def setModel(self, names): """Set a list of models""" # support passing a string in names instead of a sequence if isinstance(names, string_types): names = [names] self._model_chooser_tool.updateModels(names or []) def createConfig(self, allowUnpickable=False): """ Reimplemented from BaseConfigurableClass to manage the config properties of the trendsets attached to this plot """ try: # Temporarily register trendsets as delegates tmpreg = [] data_items = self.getPlotItem().listDataItems() for idx, item in enumerate(data_items): if isinstance(item, TaurusTrendSet): name = "__TaurusTrendSet_%d__" % idx tmpreg.append(name) self.registerConfigDelegate(item, name) configdict = copy.deepcopy( BaseConfigurableClass.createConfig( self, allowUnpickable=allowUnpickable ) ) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) return configdict def applyConfig(self, configdict, depth=None): """ Reimplemented from BaseConfigurableClass to manage the config properties of the trendsets attached to this plot """ try: # Temporarily register trendsets as delegates tmpreg = [] tsets = [] for name in configdict["__orderedConfigNames__"]: if name.startswith("__TaurusTrendSet_"): # Instantiate empty TaurusTrendSet tset = TaurusTrendSet() tsets.append(tset) self.registerConfigDelegate(tset, name) tmpreg.append(name) # remove the trendsets from the second axis (Y2) to avoid dups self._y2.clearItems() BaseConfigurableClass.applyConfig( self, configdict=configdict, depth=depth ) plot_item = self.getPlotItem() # keep a dict of existing trendsets (to use it for avoiding dups) currentTrendSets = dict() curveNames = [] for tset in plot_item.listDataItems(): if isinstance(tset, TaurusTrendSet): currentTrendSets[tset.getFullModelName()] = tset curveNames.extend([c.name for c in tset]) # remove trendsets that exists in currentTrendSets from plot # (to avoid duplicates). Also remove curves from the legend for tset in tsets: ts = currentTrendSets.get(tset.getFullModelName(), None) if ts is not None: plot_item.removeItem(ts) # Add to plot **after** their configuration has been applied for tset in tsets: # First we add all the trendsets to self. This way the plotItem # can keep a list of dataItems (PlotItem.listDataItems()) self.addItem(tset) # Add trendsets to Y2 axis, when the trendset configurations # have been applied. # Ideally, the Y2ViewBox class must handle the action of adding # trendsets to itself, but we want add the trendsets when they # are restored with all their properties. if tset.getFullModelName() in self._y2.getCurves(): plot_item.getViewBox().removeItem(tset) self._y2.addItem(tset) finally: # Ensure that temporary delegates are unregistered for n in tmpreg: self.unregisterConfigurableItem(n, raiseOnError=False) def _getState(self): """Same as PlotWidget.saveState but removing viewRange conf to force a refresh with targetRange when loading """ state = copy.deepcopy(self.saveState()) # remove viewRange conf del state["view"]["viewRange"] return state def trend_main( models=(), config_file=None, demo=False, window_name="TaurusTrend (pg)" ): """Launch a TaurusTrend""" import sys from taurus.qt.qtgui.application import TaurusApplication app = TaurusApplication(cmd_line_parser=None, app_name="taurustrend(pg)") w = TaurusTrend() w.setWindowTitle(window_name) # config_file = 'tmp/TaurusTrend.pck' if demo: models = list(models) models.extend(["eval:rand()", "eval:1+rand(2)"]) if config_file is not None: w.loadConfigFile(config_file) if models: w.setModel(models) w.show() ret = app.exec_() # w.saveConfigFile('tmp/TaurusTrend.pck') # import pprint # pprint.pprint(w.createConfig()) sys.exit(ret) if __name__ == "__main__": trend_main(models=("eval:rand()", "sys/tg_test/1/ampli")) taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/ui/000077500000000000000000000000001355007744100211205ustar00rootroot00000000000000taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/ui/CurvesAppearanceChooser.ui000066400000000000000000000262641355007744100262430ustar00rootroot00000000000000 curvesAppearanceChooserDlg 0 0 808 205 0 0 16777215 205 Form 0 0 QAbstractItemView::ExtendedSelection false &Reset &Apply Line 0 0 &Area Fill S&tyle lStyleCB 0 0 -- -1 10 1 0 0 -99.989999999999995 <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Style of the pen used to connect the points.</p></body></html> Mode C&olor lColorCB &Width lWidthSB <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:10pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Connector mode: how the data points are connected (steps, straight lines,...)</p></body></html> Symbols &Style sStyleCB Si&ze sSizeSB -- -1 10 1 3 &Color sColorCB Qt::Vertical 111 16 &Fill false Other Curve Title(s)... Assign to axis <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Assign selected curves to Y1 (left axis)</p></body></html> Y1 false <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Assign selected curves to Y2 (right axis)</p></body></html> Y2 false 0 0 Background fill... taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/ui/TaurusItemConfDlg.ui000066400000000000000000000063401355007744100250210ustar00rootroot00000000000000 TaurusItemConfDlg 0 0 850 495 Form Qt::Horizontal Sources of data Contents & appearance 400 0 Qt::CustomContextMenu QAbstractItemView::DragDrop Qt::Horizontal 40 20 Qt::Horizontal 40 20 Reload Cancel Apply taurus-pyqtgraph-0.3.1/taurus_pyqtgraph/y2axis.py000066400000000000000000000175271355007744100223100ustar00rootroot00000000000000#!/usr/bin/env python ############################################################################# ## # This file is part of Taurus ## # http://taurus-scada.org ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## # Taurus is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. ## # Taurus is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. ## # You should have received a copy of the GNU Lesser General Public License # along with Taurus. If not, see . ## ############################################################################# __all__ = ["Y2ViewBox"] from pyqtgraph import ViewBox, PlotItem from taurus.qt.qtcore.configuration.configuration import BaseConfigurableClass def _PlotItem_addItem(self, item, *args, **kwargs): """replacement for `PlotItem.addItem` that Y2Axis will use to monkey-patch the original one """ PlotItem.addItem(self, item, *args, **kwargs) if hasattr(item, "setLogMode"): item.setLogMode( self.getAxis("bottom").logMode, self.getAxis("left").logMode ) class Y2ViewBox(ViewBox, BaseConfigurableClass): """ A tool that inserts a secondary Y axis to a plot item (see :meth:`attachToPlotItem`). It is implemented as a :class:`pyqtgraph.ViewBox` and provides methods to add and remove :class:`pyqtgraph.PlotDataItem` objects to it. """ def __init__(self, *args, **kwargs): BaseConfigurableClass.__init__(self) ViewBox.__init__(self, *args, **kwargs) self.registerConfigProperty(self.getCurves, self.setCurves, "Y2Curves") self.registerConfigProperty(self._getState, self.setState, "viewState") self._isAttached = False self.plotItem = None self._curvesModelNames = [] def attachToPlotItem(self, plot_item): """Use this method to add this axis to a plot :param plot_item: (PlotItem) """ if self._isAttached: return # TODO: log a message it's already attached self._isAttached = True mainViewBox = plot_item.getViewBox() mainViewBox.sigResized.connect(self._updateViews) self.plotItem = plot_item # add axis-independent actions for logarithmic scale self._addLogAxisActions() # disable the standard (custom view-unfriendly) log actions self.plotItem.ctrl.logXCheck.setEnabled(False) self.plotItem.ctrl.logYCheck.setEnabled(False) # monkey-patch the addItem method of the PlotItem from types import MethodType self.plotItem.addItem = MethodType(_PlotItem_addItem, self.plotItem) def _updateViews(self, viewBox): self.setGeometry(viewBox.sceneBoundingRect()) self.linkedViewChanged(viewBox, self.XAxis) def removeItem(self, item): """Reimplemented from :class:`pyqtgraph.ViewBox`""" ViewBox.removeItem(self, item) # when last curve is removed from self (axis Y2), we must remove the # axis from scene and hide the axis. if len(self.addedItems) < 1: self.plotItem.scene().removeItem(self) self.plotItem.hideAxis("right") if hasattr(item, "getFullModelNames"): self._curvesModelNames.remove(item.getFullModelNames()) def addItem(self, item, ignoreBounds=False): """Reimplemented from :class:`pyqtgraph.ViewBox`""" ViewBox.addItem(self, item, ignoreBounds=ignoreBounds) if len(self.addedItems) == 1: # when the first curve is added to self (axis Y2), we must # add Y2 to main scene(), show the axis and link X axis to self. self.plotItem.showAxis("right") self.plotItem.scene().addItem(self) self.plotItem.getAxis("right").linkToView(self) self.setXLink(self.plotItem) # set the item log mode to match this view: if hasattr(item, "setLogMode"): item.setLogMode( self.plotItem.getAxis("bottom").logMode, self.plotItem.getAxis("right").logMode, ) if hasattr(item, "getFullModelNames") and ( len(self.addedItems) > 0 and item.getFullModelNames() not in self._curvesModelNames ): self._curvesModelNames.append(item.getFullModelNames()) def getCurves(self): """Returns the curve model names of curves associated to the Y2 axis. :return: (list) List of tuples of model names (xModelName, yModelName) from each curve in this view """ return self._curvesModelNames def setCurves(self, curves): """Sets the curve names associated to the Y2 axis (but does not create/remove any curve. """ self._curvesModelNames = curves def _getState(self): """Same as ViewBox.getState but removing viewRange conf to force a refresh with targetRange when loading """ state = self.getState(copy=True) del state["viewRange"] return state def clearItems(self): """Remove the added items""" for c in self.addedItems: self.removeItem(c) def _addLogAxisActions(self): # insert & connect actions Log Scale Actions # X (bottom) menu = self.plotItem.getViewBox().menu.axes[0] action = menu.addAction("Log scale") action.setCheckable(True) action.setChecked(self.plotItem.getAxis("bottom").logMode) action.setParent(menu) action.toggled.connect(self._onXLogToggled) self.menu.axes[0].addAction(action) # Add same action to X2 menu too # Y1 (left) menu = self.plotItem.getViewBox().menu.axes[1] action = menu.addAction("Log scale") action.setCheckable(True) action.setChecked(self.plotItem.getAxis("left").logMode) action.setParent(menu) action.toggled.connect(self._onY1LogToggled) # Y2 (right) menu = self.menu.axes[1] action = menu.addAction("Log scale") action.setCheckable(True) action.setChecked(self.plotItem.getAxis("right").logMode) action.setParent(menu) action.toggled.connect(self._onY2LogToggled) def _onXLogToggled(self, checked): logx = checked # set log mode for items of main viewbox logy = self.plotItem.getAxis("left").logMode for i in self.plotItem.getViewBox().addedItems: if hasattr(i, "setLogMode"): i.setLogMode(logx, logy) # set log mode for items of Y2 viewbox logy = self.plotItem.getAxis("right").logMode for i in self.addedItems: if hasattr(i, "setLogMode"): i.setLogMode(logx, logy) # set log mode for the bottom axis self.plotItem.getAxis("bottom").setLogMode(checked) def _onY1LogToggled(self, checked): # set log mode for items of main viewbox logx = self.plotItem.getAxis("bottom").logMode logy = checked for i in self.plotItem.getViewBox().addedItems: if hasattr(i, "setLogMode"): i.setLogMode(logx, logy) # set log mode for the left axis self.plotItem.getAxis("left").setLogMode(checked) def _onY2LogToggled(self, checked): # set log mode for items of Y2 viewbox logx = self.plotItem.getAxis("bottom").logMode logy = checked for i in self.addedItems: if hasattr(i, "setLogMode"): i.setLogMode(logx, logy) # set log mode for the right axis self.plotItem.getAxis("right").setLogMode(checked) taurus-pyqtgraph-0.3.1/tests/000077500000000000000000000000001355007744100162235ustar00rootroot00000000000000taurus-pyqtgraph-0.3.1/tests/test_taurus_pyqtgraph.py000066400000000000000000000011241355007744100232540ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """Tests for `taurus_pyqtgraph` package.""" from click.testing import CliRunner def test_smoke(): import taurus_pyqtgraph # noqa def test_command_line_interface(): """Test the CLI.""" from taurus_pyqtgraph import cli runner = CliRunner() result = runner.invoke(cli.tpg) assert result.exit_code == 0 assert "Taurus-pyqtgraph related commands" in result.output help_result = runner.invoke(cli.tpg, ["--help"]) assert help_result.exit_code == 0 assert "Show this message and exit." in help_result.output taurus-pyqtgraph-0.3.1/tox.ini000066400000000000000000000006061355007744100163760ustar00rootroot00000000000000[tox] envlist = py35, py36, py37, flake8 [travis] python = 3.7: py37 3.6: py36 3.5: py35 [testenv:flake8] basepython = python deps = flake8 commands = flake8 taurus_pyqtgraph [testenv] setenv = PYTHONPATH = {toxinidir} deps = -r{toxinidir}/requirements_dev.txt -r{toxinidir}/requirements.txt commands = pip install -U pip py.test --basetemp={envtmpdir}