├── .cruft.json ├── .cruft.json.license ├── .flake8 ├── .gitattributes ├── .gitignore ├── .gitlab-ci.yml ├── .pre-commit-config.yaml ├── .reuse ├── add_license.py └── shortcuts.yaml ├── .vscode ├── settings.json └── settings.json.license ├── CHANGELOG.rst ├── CITATION.cff ├── LICENSES ├── CC-BY-4.0.txt ├── CC0-1.0.txt └── LGPL-3.0-only.txt ├── MANIFEST.in ├── Makefile ├── README.rst ├── binder ├── apt.txt ├── apt.txt.license ├── environment.yml ├── postBuild └── postBuild.license ├── ci └── matrix │ └── default │ └── Pipfile ├── docs ├── Makefile ├── _static │ ├── license_logo.png │ ├── license_logo.png.license │ ├── ncview.png │ ├── ncview.png.license │ ├── resize-demo.gif │ ├── resize-demo.gif.license │ ├── screenshot.png │ ├── screenshot.png.license │ └── theme_overrides.css ├── _templates │ └── footer.html ├── api.rst ├── command_line.rst ├── conf.py ├── contributing.rst ├── demo.nc ├── demo.nc.license ├── getting-started.rst ├── index.rst ├── installing.rst ├── make.bat ├── ncview.rst ├── requirements.txt ├── todo.rst └── user-guide.rst ├── icon ├── CreateICNS.sh ├── CreateICO.sh ├── icon.py ├── icon1024.png ├── icon1024.png.license ├── icon1024.svg ├── icon1024.svg.license ├── psy-view.svg ├── psy-view.svg.license ├── psyplot.svg └── psyplot.svg.license ├── psy_view ├── __init__.py ├── __main__.py ├── _version.py ├── dialogs.py ├── ds_widget.py ├── icons │ ├── color_settings.png │ ├── color_settings.png.license │ ├── color_settings.svg │ ├── color_settings.svg.license │ ├── proj_settings.png │ ├── proj_settings.png.license │ ├── proj_settings.svg │ └── proj_settings.svg.license ├── plotmethods.py ├── py.typed ├── rcsetup.py └── utils.py ├── pyproject.toml ├── setup.py ├── tests ├── conftest.py ├── icon-test.nc ├── icon-test.nc.license ├── pytest.ini ├── regional-icon-test.nc ├── regional-icon-test.nc.license ├── regular-test.nc ├── regular-test.nc.license ├── rotated-pole-test.nc ├── rotated-pole-test.nc.license ├── test_dialogs.py └── test_ds_widget.py └── tox.ini /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git", 3 | "commit": "8b481a9c63f07a7d709a1dc383dacea7f076101e", 4 | "checkout": null, 5 | "context": { 6 | "cookiecutter": { 7 | "project_authors": "Philipp S. Sommer", 8 | "project_author_emails": "philipp.sommer@hereon.de", 9 | "project_maintainers": "Philipp S. Sommer", 10 | "project_maintainer_emails": "philipp.sommer@hereon.de", 11 | "gitlab_host": "codebase.helmholtz.cloud", 12 | "gitlab_username": "psyplot", 13 | "git_remote_protocoll": "ssh", 14 | "institution": "Helmholtz-Zentrum Hereon", 15 | "institution_url": "https://www.hereon.de", 16 | "copyright_holder": "Helmholtz-Zentrum hereon GmbH", 17 | "copyright_year": "2021-2024", 18 | "use_reuse": "yes", 19 | "code_license": "LGPL-3.0-only", 20 | "documentation_license": "CC-BY-4.0", 21 | "supplementary_files_license": "CC0-1.0", 22 | "project_title": "psy-view", 23 | "project_slug": "psy-view", 24 | "package_folder": "psy_view", 25 | "project_short_description": "ncview-like interface to psyplot", 26 | "keywords": "visualization,psyplot,netcdf,raster,cartopy,earth-sciences,pyqt,qt,ipython,jupyter,qtconsole,ncview", 27 | "documentation_url": "https://psyplot.github.io/psy-view", 28 | "use_markdown_for_documentation": "no", 29 | "ci_matrix": "pipenv", 30 | "requires_gui": "yes", 31 | "deploy_package_in_ci": "yes", 32 | "deploy_pages_in_ci": "git-push", 33 | "_extensions": [ 34 | "local_extensions.UnderlinedExtension" 35 | ], 36 | "_template": "https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git" 37 | } 38 | }, 39 | "directory": null, 40 | "skip": [ 41 | ".git", 42 | ".mypy_cache" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.cruft.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | [flake8] 6 | extend-ignore = 7 | E203 8 | E402 9 | E501 10 | W503 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | psy_view/_version.py export-subst 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/api/ 77 | docs/_build/ 78 | docs/index.doctree 79 | docs/docs-*.png 80 | docs/_static/docs-*.png 81 | 82 | 83 | # PyBuilder 84 | .pybuilder/ 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # IPython 91 | profile_default/ 92 | ipython_config.py 93 | 94 | # pyenv 95 | # For a library or package, you might want to ignore these files since the code is 96 | # intended to run in multiple environments; otherwise, check them in: 97 | # .python-version 98 | 99 | # pipenv 100 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 101 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 102 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 103 | # install all needed dependencies. 104 | #Pipfile.lock 105 | 106 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 107 | __pypackages__/ 108 | 109 | # Celery stuff 110 | celerybeat-schedule 111 | celerybeat.pid 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # pytype static type analyzer 144 | .pytype/ 145 | 146 | # Cython debug symbols 147 | cython_debug/ 148 | 149 | static/ 150 | 151 | docs/api 152 | psy_view/migrations/00*.py 153 | docs/_static/orcid.* 154 | 155 | # ignore Pipfile.lock files in ci 156 | # if a lock-file needs to be added, add it with `git add -f` 157 | ci/matrix/*/Pipfile.lock 158 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | image: python:3.9 6 | 7 | variables: 8 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" 9 | 10 | cache: 11 | paths: 12 | - .cache/pip 13 | 14 | before_script: 15 | # replace git internal paths in order to use the CI_JOB_TOKEN 16 | - apt-get update -y && apt-get install -y pandoc graphviz 17 | - python -m pip install -U pip 18 | 19 | test-package: 20 | stage: test 21 | script: 22 | - pip install build twine 23 | - make dist 24 | - twine check dist/* 25 | artifacts: 26 | name: python-artifacts 27 | paths: 28 | - "dist/*" 29 | expire_in: 7 days 30 | 31 | test: 32 | stage: test 33 | variables: 34 | PIPENV_PIPFILE: "ci/matrix/${SCENARIO}/Pipfile" 35 | # disable sandboxing, otherwise chrome reports errors when the 36 | # container runs as root 37 | # https://doc.qt.io/qt-5/qtwebengine-platform-notes.html#sandboxing-support 38 | QTWEBENGINE_DISABLE_SANDBOX: "true" 39 | script: 40 | # install necessary libraries for pyqt 41 | - apt-get install -y xvfb python3-pyqt5.qtwebengine 42 | - pip install pipenv 43 | - pipenv install 44 | - xvfb-run make pipenv-test 45 | parallel: 46 | matrix: 47 | - SCENARIO: 48 | - default 49 | artifacts: 50 | name: pipfile 51 | paths: 52 | - "ci/matrix/${SCENARIO}/*" 53 | expire_in: 30 days 54 | coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' 55 | 56 | test-docs: 57 | stage: test 58 | variables: 59 | MPLBACKEND: "module://psyplot_gui.backend" 60 | # disable sandboxing, otherwise chrome reports errors when the 61 | # container runs as root 62 | # https://doc.qt.io/qt-5/qtwebengine-platform-notes.html#sandboxing-support 63 | QTWEBENGINE_DISABLE_SANDBOX: "true" 64 | script: 65 | # install necessary libraries for pyqt 66 | - apt-get install -y xvfb python3-pyqt5.qtwebengine 67 | - make dev-install 68 | # install PyQt5 (not part of requirements.txt because this is complicated 69 | # to install on different platforms) 70 | - pip install PyQt5 PyQtWebEngine 71 | - xvfb-run make -C docs html 72 | - xvfb-run make -C docs linkcheck 73 | artifacts: 74 | paths: 75 | - docs/_build 76 | 77 | 78 | deploy-package: 79 | stage: deploy 80 | needs: 81 | - test-package 82 | - test-docs 83 | - test 84 | only: 85 | - master 86 | script: 87 | - pip install twine 88 | - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* 89 | 90 | 91 | deploy-docs: 92 | stage: deploy 93 | only: 94 | - master 95 | needs: 96 | - test-docs 97 | image: node:21 98 | before_script: 99 | - npm install -g gh-pages@6.1.1 100 | - mkdir .gh-pages-cache 101 | script: 102 | # make sure, the DEPLOY_TOKEN is defined 103 | - >- 104 | [ ${CI_DEPLOY_TOKEN} ] || 105 | echo "The CI_DEPLOY_TOKEN variable is not set. Please create an access 106 | token with scope 'read_repository' and 'write_repository'" && 107 | [ ${CI_DEPLOY_TOKEN} ] 108 | - >- 109 | CACHE_DIR=$(realpath .gh-pages-cache) 110 | gh-pages 111 | --dotfiles 112 | --nojekyll 113 | --branch gh-pages 114 | --repo https://ci-user:${CI_DEPLOY_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git 115 | --user "${CI_COMMIT_AUTHOR}" 116 | --message "CI Pipeline ${CI_PIPELINE_ID}, commit ${CI_COMMIT_SHORT_SHA}" 117 | --dist docs/_build/html 118 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # https://pre-commit.com/ 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.4.0 9 | hooks: 10 | - id: trailing-whitespace 11 | - id: end-of-file-fixer 12 | - id: check-yaml 13 | # isort should run before black as black sometimes tweaks the isort output 14 | - repo: https://github.com/PyCQA/isort 15 | rev: 5.12.0 16 | hooks: 17 | - id: isort 18 | args: 19 | - --profile 20 | - black 21 | - --line-length 22 | - "79" 23 | - --filter-files 24 | - -skip-gitignore 25 | - --float-to-top 26 | # https://github.com/python/black#version-control-integration 27 | - repo: https://github.com/psf/black 28 | rev: 23.1.0 29 | hooks: 30 | - id: black 31 | args: 32 | - --line-length 33 | - "79" 34 | - --exclude 35 | - venv 36 | - repo: https://github.com/keewis/blackdoc 37 | rev: v0.3.8 38 | hooks: 39 | - id: blackdoc 40 | - repo: https://github.com/pycqa/flake8 41 | rev: 6.0.0 42 | hooks: 43 | - id: flake8 44 | - repo: https://github.com/pre-commit/mirrors-mypy 45 | rev: v1.0.1 46 | hooks: 47 | - id: mypy 48 | additional_dependencies: 49 | - types-PyYAML 50 | - types-docutils 51 | args: 52 | - --ignore-missing-imports 53 | 54 | - repo: https://github.com/fsfe/reuse-tool 55 | rev: v1.1.2 56 | hooks: 57 | - id: reuse 58 | 59 | - repo: https://github.com/citation-file-format/cff-converter-python 60 | # there is no release with this hook yet 61 | rev: "44e8fc9" 62 | hooks: 63 | - id: validate-cff 64 | -------------------------------------------------------------------------------- /.reuse/add_license.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | """Helper script to add licenses to files. 6 | 7 | This script can be used to apply the licenses and default copyright holders 8 | to files in the repository. 9 | 10 | It uses the short cuts from the ``.reuse/shortcuts.yaml`` file and 11 | adds them to the call of ``reuse annotate``. Any command line option however 12 | overwrites the config in ``shortcuts.yaml`` 13 | 14 | Usage:: 15 | 16 | python .reuse/add_license.py [OPTIONS] 17 | """ 18 | 19 | import os.path as osp 20 | from argparse import ArgumentParser 21 | from textwrap import dedent 22 | from typing import Dict, Optional, TypedDict 23 | 24 | import yaml 25 | from reuse.project import Project 26 | from reuse.vcs import find_root 27 | 28 | try: 29 | from reuse._annotate import add_arguments as _orig_add_arguments 30 | from reuse._annotate import run 31 | except ImportError: 32 | # reuse < 3.0 33 | from reuse.header import add_arguments as _orig_add_arguments 34 | from reuse.header import run 35 | 36 | 37 | class LicenseShortCut(TypedDict): 38 | """Shortcut to add a copyright statement""" 39 | 40 | #: The copyright statement 41 | copyright: str 42 | 43 | #: year of copyright statement 44 | year: str 45 | 46 | #: SPDX Identifier of the license 47 | license: Optional[str] 48 | 49 | 50 | def load_shortcuts() -> Dict[str, LicenseShortCut]: 51 | """Load the ``shortcuts.yaml`` file.""" 52 | 53 | with open(osp.join(osp.dirname(__file__), "shortcuts.yaml")) as f: 54 | return yaml.safe_load(f) 55 | 56 | 57 | def add_arguments( 58 | parser: ArgumentParser, shortcuts: Dict[str, LicenseShortCut] 59 | ): 60 | parser.add_argument( 61 | "shortcut", 62 | choices=[key for key in shortcuts if not key.startswith(".")], 63 | help=( 64 | "What license should be applied? Shortcuts are loaded from " 65 | ".reuse/shortcuts.yaml. Possible shortcuts are %(choices)s" 66 | ), 67 | ) 68 | 69 | _orig_add_arguments(parser) 70 | 71 | parser.set_defaults(func=run) 72 | parser.set_defaults(parser=parser) 73 | 74 | 75 | def main(argv=None): 76 | shortcuts = load_shortcuts() 77 | 78 | parser = ArgumentParser( 79 | prog=".reuse/add_license.py", 80 | description=dedent( 81 | """ 82 | Add copyright and licensing into the header of files with shortcuts 83 | 84 | This script uses the ``reuse annotate`` command to add copyright 85 | and licensing information into the header the specified files. 86 | 87 | It accepts the same arguments as ``reuse annotate``, plus an 88 | additional required `shortcuts` argument. The given `shortcut` 89 | comes from the file at ``.reuse/shortcuts.yaml`` to fill in 90 | copyright, year and license identifier. 91 | 92 | For further information, please type ``reuse annotate --help``""" 93 | ), 94 | ) 95 | add_arguments(parser, shortcuts) 96 | 97 | args = parser.parse_args(argv) 98 | 99 | shortcut = shortcuts[args.shortcut] 100 | 101 | if args.year is None: 102 | args.year = [] 103 | if args.copyright is None: 104 | args.copyright = [] 105 | 106 | if args.license is None and shortcut.get("license"): 107 | args.license = [shortcut["license"]] 108 | elif args.license and shortcut.get("license"): 109 | args.license.append(shortcut["license"]) 110 | args.year.append(shortcut["year"]) 111 | args.copyright.append(shortcut["copyright"]) 112 | 113 | project = Project(find_root()) 114 | args.func(args, project) 115 | 116 | 117 | if __name__ == "__main__": 118 | main() 119 | -------------------------------------------------------------------------------- /.reuse/shortcuts.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | .defaults: &defaults 6 | year: "2021-2024" 7 | copyright: "Helmholtz-Zentrum hereon GmbH" 8 | 9 | # The following dictionaries items map to dictionaries with three possible 10 | # keys: 11 | # 12 | # copyright: The copyright statement 13 | # year: year of copyright statement 14 | # license: SPDX Identifier 15 | docs: 16 | <<: *defaults 17 | license: "CC-BY-4.0" 18 | code: 19 | <<: *defaults 20 | license: "LGPL-3.0-only" 21 | supp: 22 | <<: *defaults 23 | license: "CC0-1.0" 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "ms-python.python", 4 | "gitlab.featureFlags": {"securityScansFlag": false}, 5 | "python.formatting.provider": "black", 6 | "python.formatting.blackArgs": [ 7 | "--line-length", 8 | "79" 9 | ], 10 | "python.linting.mypyCategorySeverity.note": "Hint", 11 | "python.linting.mypyEnabled": true, 12 | 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | v0.3.0 6 | ====== 7 | Formatting fixes, Background images and viewport control buttons 8 | 9 | Changed 10 | ------- 11 | - migrate to psyplot-plugin-template, see `!64 `__ 38 | - Documentation is now hosted with Github Pages at https://psyplot.github.io/psy-view. 39 | Redirects from the old documentation at `https://psy-view.readthedocs.io` have 40 | been configured. 41 | - We use CicleCI now for a standardized CI/CD pipeline to build and test 42 | the code and docs all at one place, see `#57 `__ 43 | 44 | 45 | v0.1.0 46 | ====== 47 | 48 | Changed 49 | ------- 50 | - The plotmethod tabs have now a more intuitive gridlayout (see 51 | `#46 `__) 52 | - When closing the mainwindow of psy-view now, one closes all open windows (i.e. 53 | also the open figures, see 54 | `#47 `__) 55 | 56 | 57 | Added 58 | ----- 59 | - A widget to control the plot type for mapplot and plot2d (see 60 | `#46 `__) 61 | - A button to reload all plots. This is useful, for instance, if the data on 62 | your disk changed and you just want to update the plot 63 | `#48 `__) 64 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # YAML 1.2 6 | --- 7 | cff-version: "1.2.0" 8 | message: "If you use this software, please cite both the article from preferred-citation and the software itself." 9 | title: "psy-view: An ncview-like interface to psyplot" 10 | authors: 11 | - family-names: Sommer 12 | given-names: "Philipp S." 13 | affiliation: "Helmholtz-Zentrum Hereon" 14 | orcid: "https://orcid.org/0000-0001-6171-7716" 15 | website: "https://www.philipp-s-sommer.de" 16 | post-code: 21502 17 | city: Geesthacht 18 | country: DE 19 | email: philipp.sommer@hereon.de 20 | doi: "10.5281/zenodo.4077933" 21 | contact: 22 | - email: psyplot@hereon.de 23 | name: "Psyplot developers at hereon" 24 | license: "LGPL-3.0-only" 25 | repository-code: https://github.com/psyplot/psyplot 26 | type: software 27 | keywords: 28 | - psyplot 29 | - python 30 | - visualization 31 | - xarray 32 | - matplotlib 33 | - netcdf4 34 | - interactive 35 | - climate models 36 | - unstructured 37 | - earth-sciences 38 | - pyqt 39 | - qt 40 | - ipython 41 | - jupyter 42 | - qtconsole 43 | - raster 44 | - cartopy 45 | - ncview 46 | preferred-citation: 47 | title: "The psyplot interactive visualization framework" 48 | authors: 49 | - family-names: Sommer 50 | given-names: "Philipp S." 51 | affiliation: "Helmholtz-Zentrum Hereon" 52 | orcid: "https://orcid.org/0000-0001-6171-7716" 53 | year: 2017 54 | type: article 55 | doi: "10.21105/joss.00363" 56 | date-published: 2017-08-22 57 | journal: Journal of Open Source Software 58 | volume: 2 59 | number: 16 60 | pages: 363 61 | publisher: 62 | name: The Open Journal 63 | license: CC-BY-4.0 64 | ... 65 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. 10 | 11 | Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. 12 | 13 | Creative Commons Attribution 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | Section 1 – Definitions. 18 | 19 | a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 24 | 25 | d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 26 | 27 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 28 | 29 | f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 30 | 31 | g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 32 | 33 | h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. 34 | 35 | i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 36 | 37 | j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 38 | 39 | k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 40 | 41 | Section 2 – Scope. 42 | 43 | a. License grant. 44 | 45 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 46 | 47 | A. reproduce and Share the Licensed Material, in whole or in part; and 48 | 49 | B. produce, reproduce, and Share Adapted Material. 50 | 51 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 52 | 53 | 3. Term. The term of this Public License is specified in Section 6(a). 54 | 55 | 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 56 | 57 | 5. Downstream recipients. 58 | 59 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 60 | 61 | B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 62 | 63 | 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 64 | 65 | b. Other rights. 66 | 67 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 68 | 69 | 2. Patent and trademark rights are not licensed under this Public License. 70 | 71 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 72 | 73 | Section 3 – License Conditions. 74 | 75 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 76 | 77 | a. Attribution. 78 | 79 | 1. If You Share the Licensed Material (including in modified form), You must: 80 | 81 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 82 | 83 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 84 | 85 | ii. a copyright notice; 86 | 87 | iii. a notice that refers to this Public License; 88 | 89 | iv. a notice that refers to the disclaimer of warranties; 90 | 91 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 92 | 93 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 94 | 95 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 96 | 97 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 98 | 99 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 100 | 101 | 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. 102 | 103 | Section 4 – Sui Generis Database Rights. 104 | 105 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 106 | 107 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 108 | 109 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 110 | 111 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 112 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 113 | 114 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 115 | 116 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 117 | 118 | b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 119 | 120 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 121 | 122 | Section 6 – Term and Termination. 123 | 124 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 125 | 126 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 127 | 128 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 129 | 130 | 2. upon express reinstatement by the Licensor. 131 | 132 | c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 133 | 134 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 135 | 136 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 137 | 138 | Section 7 – Other Terms and Conditions. 139 | 140 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 141 | 142 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 143 | 144 | Section 8 – Interpretation. 145 | 146 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 147 | 148 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 149 | 150 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 151 | 152 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 153 | 154 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 155 | 156 | Creative Commons may be contacted at creativecommons.org. 157 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | include README.rst 6 | include LICENSES/* 7 | include psy_view/icons/*.png 8 | include psy_view/icons/*.png.license 9 | include psy_view/_version.py 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | .PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 lint/black 6 | .DEFAULT_GOAL := help 7 | 8 | define BROWSER_PYSCRIPT 9 | import os, webbrowser, sys 10 | 11 | from urllib.request import pathname2url 12 | 13 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 14 | endef 15 | export BROWSER_PYSCRIPT 16 | 17 | define PRINT_HELP_PYSCRIPT 18 | import re, sys 19 | 20 | for line in sys.stdin: 21 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 22 | if match: 23 | target, help = match.groups() 24 | print("%-20s %s" % (target, help)) 25 | endef 26 | export PRINT_HELP_PYSCRIPT 27 | 28 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 29 | 30 | help: 31 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 32 | 33 | clean: clean-build clean-pyc clean-test clean-venv ## remove all build, virtual environments, test, coverage and Python artifacts 34 | 35 | clean-build: ## remove build artifacts 36 | rm -fr build/ 37 | rm -fr dist/ 38 | rm -fr .eggs/ 39 | find . -name '*.egg-info' -exec rm -fr {} + 40 | find . -name '*.egg' -exec rm -f {} + 41 | 42 | clean-pyc: ## remove Python file artifacts 43 | find . -name '*.pyc' -exec rm -f {} + 44 | find . -name '*.pyo' -exec rm -f {} + 45 | find . -name '*~' -exec rm -f {} + 46 | find . -name '__pycache__' -exec rm -fr {} + 47 | 48 | clean-test: ## remove test and coverage artifacts 49 | rm -fr .tox/ 50 | rm -f .coverage 51 | rm -fr htmlcov/ 52 | rm -fr .pytest_cache 53 | 54 | clean-venv: # remove the virtual environment 55 | rm -rf venv 56 | 57 | lint/isort: ## check style with flake8 58 | isort --check psy_view tests 59 | lint/flake8: ## check style with flake8 60 | flake8 psy_view tests 61 | lint/black: ## check style with black 62 | black --check psy_view tests 63 | blackdoc --check psy_view tests 64 | lint/reuse: ## check licenses 65 | reuse lint 66 | 67 | lint: lint/isort lint/black lint/flake8 lint/reuse ## check style 68 | 69 | formatting: 70 | isort psy_view tests 71 | black psy_view tests 72 | blackdoc psy_view tests 73 | 74 | quick-test: ## run tests quickly with the default Python 75 | python -m pytest 76 | 77 | pipenv-test: ## run tox 78 | pipenv run isort --check psy_view 79 | pipenv run black --line-length 79 --check psy_view 80 | pipenv run flake8 psy_view 81 | pipenv run pytest -v --cov=psy_view -x 82 | pipenv run reuse lint 83 | pipenv run cffconvert --validate 84 | 85 | test: ## run tox 86 | tox 87 | 88 | test-all: test test-docs ## run tests and test the docs 89 | 90 | coverage: ## check code coverage quickly with the default Python 91 | python -m pytest --cov psy_view --cov-report=html 92 | $(BROWSER) htmlcov/index.html 93 | 94 | docs: ## generate Sphinx HTML documentation, including API docs 95 | $(MAKE) -C docs clean 96 | $(MAKE) -C docs html 97 | $(BROWSER) docs/_build/html/index.html 98 | 99 | test-docs: ## generate Sphinx HTML documentation, including API docs 100 | $(MAKE) -C docs clean 101 | $(MAKE) -C docs linkcheck 102 | 103 | servedocs: docs ## compile the docs watching for changes 104 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 105 | 106 | release: dist ## package and upload a release 107 | twine upload dist/* 108 | 109 | dist: clean ## builds source and wheel package 110 | python -m build 111 | ls -l dist 112 | 113 | install: clean ## install the package to the active Python's site-packages 114 | python -m pip install . 115 | 116 | dev-install: clean 117 | python -m pip install -r docs/requirements.txt 118 | python -m pip install -e .[dev] 119 | pre-commit install 120 | 121 | venv-install: clean 122 | python -m venv venv 123 | venv/bin/python -m pip install -r docs/requirements.txt 124 | venv/bin/python -m pip install -e .[dev] 125 | venv/bin/pre-commit install 126 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | ========================================= 6 | psy-view: An ncview-like GUI with psyplot 7 | ========================================= 8 | 9 | .. start-badges 10 | 11 | |CI| 12 | |Code coverage| 13 | |Latest Release| 14 | |PyPI version| 15 | |Code style: black| 16 | |Imports: isort| 17 | |PEP8| 18 | |Checked with mypy| 19 | |REUSE status| 20 | 21 | .. end-badges 22 | 23 | This package defines a viewer application for netCDF files, that is highly 24 | motivated by the ncview_ package but entirely built upon the psyplot framework. 25 | It supports strucutured and unstructured grids and provides an intuitive 26 | graphical user interface to quickly dive into the data inside a netCDF file. 27 | 28 | .. _ncview: http://meteora.ucsd.edu/~pierce/ncview_home_page.html 29 | 30 | **This package is currently under development and we highly appreciate your 31 | feedback! Please try it out yourself and, if you would like to see more features, 32 | find bugs or want to say anything else, please leave your comments and 33 | experiences at https://github.com/psyplot/psy-view/issues or send a mail to 34 | psyplot@hzg.de.** 35 | 36 | .. image:: docs/_static/screenshot.png 37 | :alt: Screenshot 38 | :target: https://github.com/psyplot/psy-view 39 | 40 | Features 41 | -------- 42 | Some of the most important features offered by psy-view are: 43 | 44 | - intuitive GUI to select variables, dimensions, slices, etc. and change the 45 | plot 46 | - automatically decodes CF-conventions and supports unstructured grid, such as 47 | ICON_ or UGRID_ 48 | - animation interface 49 | - different projections 50 | - implemented in psyplot-gui_ for full flexibility (if desired) 51 | 52 | .. _ICON: https://code.mpimet.mpg.de/projects/iconpublic 53 | .. _UGRID: http://ugrid-conventions.github.io/ugrid-conventions/ 54 | .. _psyplot-gui: https://psyplot.github.io/psyplot-gui 55 | 56 | 57 | Test it without installation 58 | ---------------------------- 59 | You can try the functionalities with some selected example files headless in 60 | your browser by clicking on |mybinder|. Note that it might take a bit to load 61 | and that the speed depends on your WiFi-connection. 62 | 63 | 64 | Installation 65 | ------------ 66 | To install the current work-in-progress, please 67 | 68 | 1. download Miniconda_ 69 | 2. open the terminal (or `Anaconda Prompt` on Windows) and type:: 70 | 71 | conda create -n psyplot -c conda-forge psy-view 72 | 73 | On Linux and OS X, you may instead want to type:: 74 | 75 | $ conda create -n psyplot -c conda-forge --override-channels psy-view 76 | 77 | in order to not mix the anaconda defaults and and conda-forge channel, because 78 | mixing them can sometimes cause incompatibilities. 79 | 3. The commands above will installed psy-view and all it's necessary 80 | dependencies into a separate environment. You can activate it via:: 81 | 82 | 83 | conda activate psyplot 84 | 85 | 4. Now launch the GUI via typing:: 86 | 87 | psy-view 88 | 89 | or:: 90 | 91 | psy-view 92 | 93 | See ``psy-view --help`` for more options 94 | 95 | .. _Miniconda: https://conda.io/en/latest/miniconda.html 96 | 97 | 98 | As an alternativ to a local installation, you can also run it 99 | headless in you browser by clicking |mybinder| 100 | 101 | 102 | For alternative installation instructions, update information or deinstallation 103 | instructions, please have a look into the `installation docs`_. 104 | 105 | .. _installation docs: https://psyplot.github.io/psy-view/installing.html 106 | 107 | 108 | Get in touch 109 | ------------ 110 | Any quesions? Do not hessitate to get in touch with the psyplot developers. 111 | 112 | - Create an issue at the `bug tracker`_ 113 | - Chat with the developers in out `channel on gitter`_ 114 | - Subscribe to the `mailing list`_ and ask for support 115 | - Sent a mail to psyplot@hzg.de 116 | 117 | See also the `code of conduct`_, and our `contribution guide`_ for more 118 | information and a guide about good bug reports. 119 | 120 | .. _bug tracker: https://github.com/psyplot/psy-view 121 | .. _channel on gitter: https://gitter.im/psyplot/community 122 | .. _mailing list: https://www.listserv.dfn.de/sympa/subscribe/psyplot 123 | .. _code of conduct: https://github.com/psyplot/psyplot/blob/master/CODE_OF_CONDUCT.md 124 | .. _contribution guide: https://github.com/psyplot/psyplot/blob/master/CONTRIBUTING.md 125 | 126 | 127 | 128 | Copyright disclaimer 129 | -------------------- 130 | Copyright (C) 2020 Philipp S. Sommer 131 | 132 | This program is free software: you can redistribute it and/or modify 133 | it under the terms of the GNU General Public License as published by 134 | the Free Software Foundation, either version 3 of the License, or 135 | (at your option) any later version. 136 | 137 | This program is distributed in the hope that it will be useful, 138 | but WITHOUT ANY WARRANTY; without even the implied warranty of 139 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 140 | GNU General Public License for more details. 141 | 142 | You should have received a copy of the GNU General Public License 143 | along with this program. If not, see https://www.gnu.org/licenses/. 144 | 145 | 146 | .. |CI| image:: https://codebase.helmholtz.cloud/psyplot/psy-view/badges/master/pipeline.svg 147 | :target: https://codebase.helmholtz.cloud/psyplot/psy-view/-/pipelines?page=1&scope=all&ref=master 148 | .. |Code coverage| image:: https://codebase.helmholtz.cloud/psyplot/psy-view/badges/master/coverage.svg 149 | :target: https://codebase.helmholtz.cloud/psyplot/psy-view/-/graphs/master/charts 150 | .. |Latest Release| image:: https://codebase.helmholtz.cloud/psyplot/psy-view/-/badges/release.svg 151 | :target: https://codebase.helmholtz.cloud/psyplot/psy-view 152 | .. |PyPI version| image:: https://img.shields.io/pypi/v/psy-view.svg 153 | :target: https://pypi.python.org/pypi/psy-view/ 154 | .. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 155 | :target: https://github.com/psf/black 156 | .. |Imports: isort| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 157 | :target: https://pycqa.github.io/isort/ 158 | .. |PEP8| image:: https://img.shields.io/badge/code%20style-pep8-orange.svg 159 | :target: https://www.python.org/dev/peps/pep-0008/ 160 | .. |Checked with mypy| image:: http://www.mypy-lang.org/static/mypy_badge.svg 161 | :target: http://mypy-lang.org/ 162 | .. |REUSE status| image:: https://api.reuse.software/badge/codebase.helmholtz.cloud/psyplot/psy-view 163 | :target: https://api.reuse.software/info/codebase.helmholtz.cloud/psyplot/psy-view 164 | -------------------------------------------------------------------------------- /binder/apt.txt: -------------------------------------------------------------------------------- 1 | dbus-x11 2 | xfce4 3 | xfce4-panel 4 | xfce4-session 5 | xfce4-settings 6 | xorg 7 | xubuntu-icon-theme 8 | libxss1 9 | libpci3 10 | libasound2 11 | -------------------------------------------------------------------------------- /binder/apt.txt.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | channels: 6 | - psyplot/label/master 7 | - psyplot 8 | - manics # Used by jupyter-desktop-server 9 | - conda-forge 10 | dependencies: 11 | - psy-maps 12 | - psyplot-gui 13 | - netcdf4 14 | - pip 15 | - ncview 16 | - matplotlib>=3.3 17 | - cartopy>=0.18 18 | 19 | # Required for jupyter-desktop-server 20 | - websockify 21 | - pip: 22 | - jupyter-desktop-server 23 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | mkdir -p $HOME/Desktop 6 | 7 | cp -r tests/*.nc $HOME/Desktop 8 | 9 | python -m pip install ./ 10 | 11 | cat > $HOME/Desktop/Psy-View.desktop << EOF 12 | [Desktop Entry] 13 | Version=1.0 14 | Type=Application 15 | Name=Psy-View 16 | Exec=$(which psy-view) %f 17 | Icon=$HOME/icon/psy-view.svg 18 | Path=$HOME/Desktop 19 | MimeType=application/x-netcdf; 20 | EOF 21 | 22 | cat > $HOME/Desktop/Psyplot.desktop << EOF 23 | [Desktop Entry] 24 | Version=1.0 25 | Type=Application 26 | Name=Psyplot 27 | Exec=$(which psyplot) %f 28 | Icon=$HOME/icon/psyplot.svg 29 | Path=$HOME/Desktop 30 | MimeType=application/x-netcdf; 31 | EOF 32 | 33 | chmod u+x $HOME/Desktop/Psyplot.desktop $HOME/Desktop/Psy-View.desktop 34 | 35 | # associate default applications 36 | mkdir -p $HOME/.local/share/applications/ 37 | ln -s $HOME/Desktop/Psyplot.desktop $HOME/.local/share/applications/ 38 | ln -s $HOME/Desktop/Psy-View.desktop $HOME/.local/share/applications/ 39 | 40 | cat > $HOME/.config/mimeapps.list << EOF 41 | [Default Applications] 42 | application/x-netcdf=Psy-View.Desktop 43 | 44 | [Added Associations] 45 | application/x-netcdf=Psy-View.desktop;application/x-netcdf=Psyplot.desktop; 46 | EOF 47 | 48 | # create a demo preset 49 | mkdir -p $HOME/.config/psyplot/presets 50 | cat > $HOME/.config/psyplot/presets/EUR-temperature.yml << EOF 51 | clabel: '%(long_name)s %(units)s' 52 | cmap: YlOrRd 53 | datagrid: k- 54 | lonlatbox: Europe 55 | lsm: 56 | coast: 57 | - 0.0 58 | - 0.0 59 | - 0.0 60 | - 1.0 61 | land: 62 | - 0.6666666666666666 63 | - 0.3333333333333333 64 | - 0.0 65 | - 1.0 66 | ocean: 67 | - 0.592156862745098 68 | - 0.7137254901960784 69 | - 0.8823529411764706 70 | - 1.0 71 | res: 50m 72 | title: '%(long_name)s over Europe' 73 | xgrid: false 74 | ygrid: false 75 | EOF 76 | -------------------------------------------------------------------------------- /binder/postBuild.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /ci/matrix/default/Pipfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | [[source]] 6 | url = "https://pypi.org/simple" 7 | verify_ssl = true 8 | name = "pypi" 9 | 10 | [packages] 11 | psy-view = {extras = ["testsite"], file = "../../.."} 12 | psyplot = {ref = "develop", git = "git+https://codebase.helmholtz.cloud/psyplot/psyplot.git"} 13 | matplotlib = "3.7.*" 14 | psyplot-gui = {ref = "develop", git = "git+https://codebase.helmholtz.cloud/psyplot/psyplot-gui.git"} 15 | PyQt5 = {version="*"} 16 | PyQtWebEngine = {version="*"} 17 | psy-simple = {ref = "develop", git = "git+https://codebase.helmholtz.cloud/psyplot/psy-simple.git"} 18 | psy-maps = {ref = "develop", git = "git+https://codebase.helmholtz.cloud/psyplot/psy-maps.git"} 19 | 20 | 21 | [dev-packages] 22 | 23 | [pipenv] 24 | allow_prereleases = true 25 | 26 | [requires] 27 | python_version = "3.9" 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | # Minimal makefile for Sphinx documentation 6 | # 7 | 8 | # You can set these variables from the command line, and also 9 | # from the environment for the first two. 10 | SPHINXOPTS ?= 11 | SPHINXBUILD ?= sphinx-build 12 | SOURCEDIR = . 13 | BUILDDIR = _build 14 | 15 | # Put it first so that "make" without argument is like "make help". 16 | help: 17 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 18 | 19 | .PHONY: help Makefile 20 | 21 | # Catch-all target: route all unknown targets to Sphinx using the new 22 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 23 | %: Makefile 24 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | -------------------------------------------------------------------------------- /docs/_static/license_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/docs/_static/license_logo.png -------------------------------------------------------------------------------- /docs/_static/license_logo.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Creative Commons 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/_static/ncview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/docs/_static/ncview.png -------------------------------------------------------------------------------- /docs/_static/ncview.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/_static/resize-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/docs/_static/resize-demo.gif -------------------------------------------------------------------------------- /docs/_static/resize-demo.gif.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/_static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/docs/_static/screenshot.png -------------------------------------------------------------------------------- /docs/_static/screenshot.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/_static/theme_overrides.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 3 | * 4 | * SPDX-License-Identifier: CC-BY-4.0 5 | */ 6 | 7 | /* override table width restrictions */ 8 | .wy-table-responsive table td, .wy-table-responsive table th { 9 | white-space: normal; 10 | } 11 | 12 | .wy-table-responsive { 13 | margin-bottom: 24px; 14 | max-width: 100%; 15 | overflow: visible; 16 | } 17 | -------------------------------------------------------------------------------- /docs/_templates/footer.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | {% extends "!footer.html" %} 8 | {% block extrafooter %} 9 | 10 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _api: 6 | 7 | API Reference 8 | ============= 9 | 10 | 11 | .. toctree:: 12 | 13 | api/psy_view 14 | -------------------------------------------------------------------------------- /docs/command_line.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. highlight:: bash 6 | 7 | .. _command-line: 8 | 9 | Command line usage 10 | ================== 11 | The :mod:`psy_view.__main__` module defines the command line options for 12 | psy-view. It can be run from the command line via:: 13 | 14 | python -m psy-view [options] [arguments] 15 | 16 | or simply:: 17 | 18 | psy-view [options] [arguments] 19 | 20 | .. argparse:: 21 | :module: psy_view 22 | :func: get_parser 23 | :prog: psy-view 24 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | # Configuration file for the Sphinx documentation builder. 6 | # 7 | # This file only contains a selection of the most common options. For a full 8 | # list see the documentation: 9 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 10 | 11 | # -- Path setup -------------------------------------------------------------- 12 | 13 | # If extensions (or modules to document with autodoc) are in another directory, 14 | # add these directories to sys.path here. If the directory is relative to the 15 | # documentation root, use os.path.abspath to make it absolute, like shown here. 16 | # 17 | import os 18 | import os.path as osp 19 | import sys 20 | import warnings 21 | from pathlib import Path 22 | from typing import Optional 23 | 24 | # note: we need to import pyplot here, because otherwise it might fail to load 25 | # the ipython extension 26 | import matplotlib.pyplot as plt # noqa: F401 27 | from docutils import nodes 28 | from docutils.parsers.rst import directives 29 | from docutils.parsers.rst.directives import images 30 | from docutils.statemachine import StringList 31 | from sphinx.ext import apidoc 32 | from sphinx.util.docutils import SphinxDirective 33 | 34 | sys.path.insert(0, os.path.abspath("..")) 35 | 36 | warnings.filterwarnings("ignore", message=r"\s*Downloading:") 37 | 38 | if not os.path.exists("_static"): 39 | os.makedirs("_static") 40 | 41 | # isort: off 42 | 43 | import psy_view 44 | 45 | # isort: on 46 | 47 | 48 | def generate_apidoc(app): 49 | appdir = Path(app.__file__).parent 50 | apidoc.main( 51 | ["-fMEeTo", str(api), str(appdir), str(appdir / "migrations" / "*")] 52 | ) 53 | 54 | 55 | api = Path("api") 56 | 57 | if not api.exists(): 58 | generate_apidoc(psy_view) 59 | 60 | # -- Project information ----------------------------------------------------- 61 | 62 | project = "psy-view" 63 | copyright = "2021-2024 Helmholtz-Zentrum hereon GmbH" 64 | author = "Philipp S. Sommer" 65 | 66 | 67 | linkcheck_ignore = [ 68 | # we do not check link of the psy-view as the 69 | # badges might not yet work everywhere. Once psy-view 70 | # is settled, the following link should be removed 71 | r"https://.*psy-view" 72 | ] 73 | 74 | 75 | # -- General configuration --------------------------------------------------- 76 | 77 | # Add any Sphinx extension module names here, as strings. They can be 78 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 79 | # ones. 80 | extensions = [ 81 | "hereon_nc_sphinxext", 82 | "sphinx.ext.intersphinx", 83 | "sphinx_design", 84 | "sphinx.ext.todo", 85 | "autodocsumm", 86 | "sphinx.ext.autodoc", 87 | "sphinx.ext.autosummary", 88 | "sphinx.ext.viewcode", 89 | "IPython.sphinxext.ipython_console_highlighting", 90 | "IPython.sphinxext.ipython_directive", 91 | "sphinxarg.ext", 92 | "psyplot.sphinxext.extended_napoleon", 93 | ] 94 | 95 | rebuild_screenshots = False 96 | 97 | confdir = osp.dirname(__file__) 98 | 99 | 100 | # Add any paths that contain templates here, relative to this directory. 101 | templates_path = ["_templates"] 102 | 103 | # List of patterns, relative to source directory, that match files and 104 | # directories to ignore when looking for source files. 105 | # This pattern also affects html_static_path and html_extra_path. 106 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 107 | 108 | 109 | autodoc_default_options = { 110 | "show_inheritance": True, 111 | "members": True, 112 | "autosummary": True, 113 | } 114 | 115 | 116 | # -- Options for HTML output ------------------------------------------------- 117 | 118 | # The theme to use for HTML and HTML Help pages. See the documentation for 119 | # a list of builtin themes. 120 | html_theme = "sphinx_rtd_theme" 121 | 122 | html_theme_options = { 123 | "collapse_navigation": False, 124 | "includehidden": False, 125 | } 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ["_static"] 131 | 132 | 133 | intersphinx_mapping = { 134 | "python": ("https://docs.python.org/3/", None), 135 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), 136 | "numpy": ("https://numpy.org/doc/stable/", None), 137 | "matplotlib": ("https://matplotlib.org/stable/", None), 138 | "sphinx": ("https://www.sphinx-doc.org/en/master/", None), 139 | "xarray": ("https://xarray.pydata.org/en/stable/", None), 140 | "cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None), 141 | "psyplot": ("https://psyplot.github.io/psyplot/", None), 142 | "psy_simple": ("https://psyplot.github.io/psy-simple/", None), 143 | "psy_maps": ("https://psyplot.github.io/psy-maps/", None), 144 | "psyplot_gui": ("https://psyplot.github.io/psyplot-gui/", None), 145 | } 146 | 147 | 148 | def create_screenshot( 149 | code: str, 150 | output: str, 151 | make_plot: bool = False, 152 | enable: Optional[bool] = None, 153 | plotmethod: str = "mapplot", 154 | minwidth=None, 155 | generate=rebuild_screenshots, 156 | ) -> str: 157 | """Generate a screenshot of the GUI.""" 158 | from psyplot.data import open_dataset 159 | from PyQt5.QtWidgets import ( # pylint: disable=no-name-in-module 160 | QApplication, 161 | QSizePolicy, 162 | ) 163 | 164 | from psy_view.ds_widget import DatasetWidget 165 | 166 | output = osp.join("_static", output) 167 | 168 | app = QApplication.instance() 169 | if app is None: 170 | app = QApplication([]) 171 | 172 | if not generate and osp.exists(output): 173 | return output 174 | 175 | ds_widget = DatasetWidget(open_dataset(osp.join(confdir, "demo.nc"))) 176 | ds_widget.plotmethod = plotmethod 177 | 178 | if make_plot: 179 | ds_widget.variable_buttons["t2m"].click() 180 | 181 | if minwidth: 182 | ds_widget.setMinimumWidth(minwidth) 183 | 184 | options = {"ds_widget": ds_widget} 185 | exec("w = " + code, options) 186 | w = options["w"] 187 | 188 | if enable is not None: 189 | w.setEnabled(enable) 190 | 191 | w.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) 192 | 193 | ds_widget.show() # to make sure we can see everything 194 | 195 | w.grab().save(osp.join(confdir, output)) 196 | ds_widget.close_sp() 197 | ds_widget.close() 198 | return output 199 | 200 | 201 | def plotmethod(argument): 202 | return directives.choice(argument, ("mapplot", "lineplot", "plot2d")) 203 | 204 | 205 | class ScreenshotDirective(SphinxDirective): 206 | """A directive to generate screenshots of the GUI. 207 | 208 | Usage:: 209 | 210 | .. screenshot:: 211 | :width: 20px 212 | ... other image options ... 213 | """ 214 | 215 | has_content = False 216 | 217 | option_spec = images.Image.option_spec.copy() 218 | 219 | option_spec["plot"] = directives.flag 220 | option_spec["enable"] = directives.flag 221 | option_spec["plotmethod"] = plotmethod 222 | option_spec["minwidth"] = directives.positive_int 223 | option_spec["generate"] = directives.flag 224 | 225 | target_directive = "image" 226 | 227 | required_arguments = 2 228 | optional_arguments = 0 229 | 230 | def add_line(self, line: str) -> None: 231 | """Append one line of generated reST to the output.""" 232 | source = self.get_source_info() 233 | if line.strip(): # not a blank line 234 | self.result.append(line, *source) 235 | else: 236 | self.result.append("", *source) 237 | 238 | def generate(self) -> None: 239 | """Generate the content.""" 240 | self.add_line(f".. {self.target_directive}:: {self.img_name}") 241 | 242 | for option, val in self.options.items(): 243 | self.add_line(f" :{option}: {val}") 244 | 245 | def run(self): 246 | """Run the directive.""" 247 | self.result = StringList() 248 | 249 | make_plot = self.options.pop("plot", False) is None 250 | enable = True if self.options.pop("enable", False) is None else None 251 | 252 | rebuild_screenshot = ( 253 | self.options.pop("generate", False) 254 | or self.env.app.config.rebuild_screenshots 255 | ) 256 | 257 | self.img_name = create_screenshot( 258 | *self.arguments, 259 | make_plot=make_plot, 260 | enable=enable, 261 | plotmethod=self.options.pop("plotmethod", None) or "mapplot", 262 | minwidth=self.options.pop("minwidth", None), 263 | generate=rebuild_screenshot, 264 | ) 265 | 266 | self.generate() 267 | 268 | node = nodes.paragraph() 269 | node.document = self.state.document 270 | self.state.nested_parse(self.result, 0, node) 271 | 272 | return node.children 273 | 274 | 275 | class ScreenshotFigureDirective(ScreenshotDirective): 276 | """A directive to generate screenshots of the GUI. 277 | 278 | Usage:: 279 | 280 | .. screenshot-figure:: 281 | :width: 20px 282 | ... other image options ... 283 | 284 | some caption 285 | """ 286 | 287 | target_directive = "figure" 288 | 289 | has_content = True 290 | 291 | def generate(self): 292 | super().generate() 293 | 294 | if self.content: 295 | self.add_line("") 296 | indent = " " 297 | for line in self.content: 298 | self.add_line(indent + line) 299 | 300 | 301 | def setup(app): 302 | app.add_directive("screenshot", ScreenshotDirective) 303 | app.add_directive("screenshot-figure", ScreenshotFigureDirective) 304 | app.add_config_value("rebuild_screenshots", rebuild_screenshots, "env") 305 | app.add_css_file("theme_overrides.css") 306 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _contributing: 6 | 7 | Contribution and development hints 8 | ================================== 9 | 10 | See :ref:`psyplots contribution guidelines `. 11 | -------------------------------------------------------------------------------- /docs/demo.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/docs/demo.nc -------------------------------------------------------------------------------- /docs/demo.nc.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _getting-started: 6 | 7 | Getting started 8 | =============== 9 | 10 | .. highlight:: bash 11 | 12 | Binder examples 13 | --------------- 14 | You can run a demo of psy-view in your webbrowser by clicking |mybinder|. This 15 | will show you a remote desktop (after some time for launching the server) where 16 | you can test psy-view (and the psyplot-gui) for different test files. 17 | 18 | .. _binder-upload: 19 | 20 | Uploading your own data 21 | ^^^^^^^^^^^^^^^^^^^^^^^ 22 | You can also upload your own data to visualize it on the binder instance. Once 23 | you see the remote desktop, your URL will be something like 24 | ``https://hub.gke2.mybinder.org/user/psyplot-psy-view-.../desktop/?token=...``. 25 | 26 | 1. open a new tab 27 | 2. take the URL from the previous tab and replace the last part from the url 28 | (``desktop/?token=...``) with ``tree/Desktop``). 29 | 3. Now click the :guilabel:`Upload` button and select the file you want to upload 30 | 4. Upload the file 31 | 5. The file you uploaded will now appear on the desktop in the previous tab 32 | 33 | 34 | .. |mybinder| image:: https://static.mybinder.org/badge_logo.svg 35 | :target: https://mybinder.org/v2/gh/psyplot/psy-view/master?urlpath=%2Fdesktop 36 | :alt: mybinder-demo 37 | 38 | 39 | Run it locally 40 | -------------- 41 | 42 | Congratulations! You successfully installed psy-view on your system (if not, 43 | head over to :ref:`install`). 44 | 45 | This small example shows you, how to make a simple georeferenced plot. You can 46 | use the :download:`demo.nc` file for this demo. 47 | 48 | Start the GUI from the command line via:: 49 | 50 | psy-view demo.nc 51 | 52 | A widget will open that looks like 53 | 54 | .. screenshot:: ds_widget docs-getting-started-ds_widget.png 55 | 56 | You see a button for the `t2m` variable: |t2m|. Click it, and it opens a plot 57 | like this: 58 | 59 | .. ipython:: 60 | :suppress: 61 | 62 | In [1]: import psyplot.project as psy 63 | ...: 64 | ...: with psy.plot.mapplot( 65 | ...: "demo.nc", 66 | ...: name="t2m", 67 | ...: cmap="viridis", 68 | ...: ) as sp: 69 | ...: sp.export("docs-getting-started-example.png") 70 | 71 | .. image:: docs-getting-started-example.png 72 | 73 | Now use the |btn_cmap| button to select a different colormap, edit the 74 | projection via the |btn_proj| button, or update the dimensions via the 75 | navigation buttons: |btn_prev|, |btn_next|, |btn_animate_backward| and 76 | |btn_animate_forward|. 77 | 78 | More documentation about the GUI elements is provided in our 79 | :ref:`User guide `. And if you are interested in the python code, 80 | checkout the :ref:`API reference `. 81 | 82 | 83 | .. |t2m| screenshot:: ds_widget.variable_buttons['t2m'] docs-getting-started-t2m.png 84 | :height: 1.3em 85 | 86 | .. |btn_cmap| screenshot:: ds_widget.plotmethod_widget.btn_cmap docs-getting-started-btn_cmap.png 87 | :plot: 88 | :height: 1.3em 89 | 90 | .. |btn_proj| screenshot:: ds_widget.plotmethod_widget.btn_proj docs-getting-started-btn_proj.png 91 | :plot: 92 | :height: 1.3em 93 | 94 | .. |btn_prev| screenshot:: ds_widget.btn_prev docs-btn_prev.png 95 | :height: 1.3em 96 | :enable: 97 | 98 | .. |btn_next| screenshot:: ds_widget.btn_next docs-btn_next.png 99 | :height: 1.3em 100 | :enable: 101 | 102 | .. |btn_animate_backward| screenshot:: ds_widget.btn_animate_backward docs-btn_animate_backward.png 103 | :height: 1.3em 104 | :enable: 105 | 106 | .. |btn_animate_forward| screenshot:: ds_widget.btn_animate_forward docs-btn_animate_forward.png 107 | :height: 1.3em 108 | :enable: 109 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. psy-view documentation master file 6 | You can adapt this file completely to your liking, but it should at least 7 | contain the root `toctree` directive. 8 | 9 | Welcome to psy-view's documentation! 10 | ==================================== 11 | 12 | |CI| 13 | |Code coverage| 14 | |Latest Release| 15 | |PyPI version| 16 | |Code style: black| 17 | |Imports: isort| 18 | |PEP8| 19 | |Checked with mypy| 20 | |REUSE status| 21 | 22 | .. rubric:: ncview-like interface to psyplot 23 | 24 | .. warning:: 25 | 26 | This page has been automatically generated as has not yet been reviewed by 27 | the authors of psy-view! 28 | Stay tuned for updates and discuss with us at 29 | https://codebase.helmholtz.cloud/psyplot/psy-view 30 | 31 | Features 32 | -------- 33 | Some of the most important features offered by psy-view are: 34 | 35 | - intuitive GUI to select variables, dimensions, slices, etc. and change the 36 | plot 37 | - automatically decodes CF-conventions and supports unstructured grid, such as 38 | ICON_ or UGRID_ 39 | - animation interface 40 | - different projections 41 | - implemented in psyplot-gui_ for full flexibility (if desired) 42 | 43 | Interested? Read more in the section :ref:`psy-view-vs-ncview`. 44 | 45 | .. _ICON: https://code.mpimet.mpg.de/projects/iconpublic 46 | .. _UGRID: http://ugrid-conventions.github.io/ugrid-conventions/ 47 | .. _psyplot-gui: https://psyplot.github.io/psyplot-gui 48 | 49 | 50 | 51 | 52 | .. toctree:: 53 | :maxdepth: 2 54 | :caption: Contents: 55 | 56 | installing 57 | getting-started 58 | user-guide 59 | ncview 60 | command_line 61 | api 62 | contributing 63 | todo 64 | 65 | 66 | Get in touch 67 | ------------ 68 | Any quesions? Do not hessitate to get in touch with the psyplot developers. 69 | 70 | - Create an issue at the `bug tracker`_ 71 | - Chat with the developers in out `channel on gitter`_ 72 | - Subscribe to the `mailing list`_ and ask for support 73 | - Sent a mail to psyplot@hzg.de 74 | 75 | See also the `code of conduct`_, and our `contribution guide`_ for more 76 | information and a guide about good bug reports. 77 | 78 | .. _bug tracker: https://github.com/psyplot/psy-view 79 | .. _channel on gitter: https://gitter.im/psyplot/community 80 | .. _mailing list: https://www.listserv.dfn.de/sympa/subscribe/psyplot 81 | .. _code of conduct: https://github.com/psyplot/psyplot/blob/master/CODE_OF_CONDUCT.md 82 | .. _contribution guide: https://github.com/psyplot/psyplot/blob/master/CONTRIBUTING.md 83 | 84 | 85 | How to cite this software 86 | ------------------------- 87 | 88 | .. card:: Please do cite this software! 89 | 90 | .. tab-set:: 91 | 92 | .. tab-item:: APA 93 | 94 | .. citation-info:: 95 | :format: apalike 96 | 97 | .. tab-item:: BibTex 98 | 99 | .. citation-info:: 100 | :format: bibtex 101 | 102 | .. tab-item:: RIS 103 | 104 | .. citation-info:: 105 | :format: ris 106 | 107 | .. tab-item:: Endnote 108 | 109 | .. citation-info:: 110 | :format: endnote 111 | 112 | .. tab-item:: CFF 113 | 114 | .. citation-info:: 115 | :format: cff 116 | 117 | 118 | License information 119 | ------------------- 120 | Copyright © 2021-2024 Helmholtz-Zentrum hereon GmbH 121 | 122 | The source code of psy-view is licensed under 123 | LGPL-3.0-only. 124 | 125 | If not stated otherwise, the contents of this documentation is licensed under 126 | CC-BY-4.0. 127 | 128 | 129 | Indices and tables 130 | ================== 131 | 132 | * :ref:`genindex` 133 | * :ref:`modindex` 134 | * :ref:`search` 135 | 136 | 137 | .. |CI| image:: https://codebase.helmholtz.cloud/psyplot/psy-view/badges/master/pipeline.svg 138 | :target: https://codebase.helmholtz.cloud/psyplot/psy-view/-/pipelines?page=1&scope=all&ref=master 139 | .. |Code coverage| image:: https://codebase.helmholtz.cloud/psyplot/psy-view/badges/master/coverage.svg 140 | :target: https://codebase.helmholtz.cloud/psyplot/psy-view/-/graphs/master/charts 141 | .. |Latest Release| image:: https://codebase.helmholtz.cloud/psyplot/psy-view/-/badges/release.svg 142 | :target: https://codebase.helmholtz.cloud/psyplot/psy-view 143 | .. |PyPI version| image:: https://img.shields.io/pypi/v/psy-view.svg 144 | :target: https://pypi.python.org/pypi/psy-view/ 145 | .. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 146 | :target: https://github.com/psf/black 147 | .. |Imports: isort| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 148 | :target: https://pycqa.github.io/isort/ 149 | .. |PEP8| image:: https://img.shields.io/badge/code%20style-pep8-orange.svg 150 | :target: https://www.python.org/dev/peps/pep-0008/ 151 | .. |Checked with mypy| image:: http://www.mypy-lang.org/static/mypy_badge.svg 152 | :target: http://mypy-lang.org/ 153 | .. |REUSE status| image:: https://api.reuse.software/badge/codebase.helmholtz.cloud/psyplot/psy-view 154 | :target: https://api.reuse.software/info/codebase.helmholtz.cloud/psyplot/psy-view 155 | -------------------------------------------------------------------------------- /docs/installing.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _install: 6 | 7 | Installation 8 | ============ 9 | 10 | .. highlight:: bash 11 | 12 | How to install 13 | -------------- 14 | 15 | .. _install-conda: 16 | 17 | Installation using conda 18 | ^^^^^^^^^^^^^^^^^^^^^^^^ 19 | 20 | We strongly recommend to install psy-view via the anaconda package 21 | manager. Either by downloading anaconda_, or miniconda_ for you operating 22 | system. If you installed `conda` for your operating system, open the 23 | terminal (or `Anaconda Prompt` on Windows) and type:: 24 | 25 | $ conda create -n psyplot -c conda-forge psy-view 26 | 27 | to install it. On Linux and OS X, you may instead want to type:: 28 | 29 | $ conda create -n psyplot -c conda-forge --override-channels psy-view 30 | 31 | in order to not mix the anaconda defaults and and conda-forge channel, because 32 | mixing them can sometimes cause incompatibilities. 33 | 34 | The commands above installed psy-view and all it's necessary 35 | dependencies into a separate environment that you can activate via:: 36 | 37 | $ conda activate psyplot 38 | 39 | Now launch the GUI via typing:: 40 | 41 | $ psy-view 42 | 43 | in the terminal (Anaconda Prompt). On Windows, you will also have a 44 | corresponding entry in the start menu. 45 | 46 | Note that you will always have to activate the conda environment 47 | (`conda activate psyplot`) in order to start `psy-view`. The advantage, however, 48 | is that other packages installed via conda are not affected by the dependencies 49 | of psy-view. 50 | 51 | .. note:: 52 | 53 | Alternatively, you can also install psy-view directly in an existing conda 54 | environment by using:: 55 | 56 | $ conda install -c conda-forge psy-view 57 | 58 | 59 | .. _install-pip: 60 | 61 | Installation using pip 62 | ^^^^^^^^^^^^^^^^^^^^^^ 63 | If you do not want to use conda for managing your python packages, you can also 64 | use the python package manager ``pip`` and install via:: 65 | 66 | $ pip install psy-view 67 | 68 | But we strongly recommend that you make sure you have the :ref:`dependencies` 69 | installed before. 70 | 71 | .. _install-source: 72 | 73 | Installation from source 74 | ^^^^^^^^^^^^^^^^^^^^^^^^ 75 | To install it from source, make sure you have the :ref:`dependencies` 76 | installed, clone the github_ repository via:: 77 | 78 | git clone https://github.com/psyplot/psy-view.git 79 | 80 | and install it via:: 81 | 82 | python -m pip install ./psy-view 83 | 84 | 85 | .. _dependencies: 86 | 87 | Dependencies 88 | ------------ 89 | 90 | Required dependencies 91 | ^^^^^^^^^^^^^^^^^^^^^ 92 | Psy-view supports all python versions greater than 3.7. Other dependencies are 93 | 94 | - psyplot_ and `the corresponding dependencies`_ 95 | - the psyplot plugin psy-maps_ 96 | - the general GUI for psyplot, psyplot-gui_ 97 | - netCDF4_ 98 | 99 | 100 | .. _conda: https://conda.io/docs/ 101 | .. _anaconda: https://www.anaconda.com/products/individual 102 | .. _miniconda: https://docs.conda.io/en/latest/miniconda.html 103 | .. _psyplot: https://psyplot.github.io/psyplot/installing.html 104 | .. _the corresponding dependencies: https://psyplot.github.io/psyplot/installing.html#dependencies 105 | .. _psy-maps: https://psyplot.github.io/psy-maps/installing.html 106 | .. _psyplot-gui: https://psyplot.github.io/psyplot-gui/installing.html 107 | .. _netCDF4: https://github.com/Unidata/netcdf4-python 108 | 109 | 110 | Running the tests 111 | ----------------- 112 | We us pytest_ to run our tests. So install pytest and pytest-qt via:: 113 | 114 | $ conda install -c conda-forge pytest pytest-qt 115 | 116 | clone the github repository via:: 117 | 118 | $ git clone https://github.com/psyplot/psy-view.git 119 | 120 | And from within the cloned repository, run 121 | 122 | $ pytest -xv 123 | 124 | Alternatively, you can build the conda recipe at ``ci/conda-recipe`` which 125 | will also run the test suite. Just install `conda-build` via:: 126 | 127 | $ conda install -n base conda-build 128 | 129 | and build the recipe via:: 130 | 131 | $ conda build ci/conda-recipe 132 | 133 | 134 | .. _install-docs: 135 | 136 | Building the docs 137 | ----------------- 138 | To build the docs, check out the github_ repository and install the 139 | requirements in ``'docs/environment.yml'``. The easiest way to do this is, 140 | again, via conda:: 141 | 142 | $ conda env create -f docs/environment.yml 143 | $ conda activate psy-view-docs 144 | 145 | You also need to install the sphinx_rtd_theme via:: 146 | 147 | $ pip install sphinx_rtd_theme 148 | 149 | Then build the docs via:: 150 | 151 | $ cd docs 152 | $ make html 153 | 154 | 155 | .. _github: https://github.com/psyplot/psy-view 156 | .. _pytest: https://pytest.org/en/latest/contents.html 157 | 158 | 159 | .. _uninstall: 160 | 161 | Uninstallation 162 | -------------- 163 | The uninstallation depends on the system you used to install psyplot. Either 164 | you did it via :ref:`conda ` (see 165 | :ref:`uninstall-conda`), via :ref:`pip ` or from the 166 | :ref:`source files ` (see :ref:`uninstall-pip`). 167 | 168 | Anyway, if you may want to remove the psyplot configuration files. If you did 169 | not specify anything else (see :func:`psyplot.config.rcsetup.psyplot_fname`), 170 | the configuration files for psyplot are located in the user home directory. 171 | Under linux and OSX, this is ``$HOME/.config/psyplot``. On other platforms it 172 | is in the ``.psyplot`` directory in the user home. 173 | 174 | .. _uninstall-conda: 175 | 176 | Uninstallation via conda 177 | ^^^^^^^^^^^^^^^^^^^^^^^^ 178 | If you installed psy-view via :ref:`conda ` into a separate 179 | environment, simply run:: 180 | 181 | conda env remove -n psyplot # assuming you named the environment psyplot 182 | 183 | If you want to uninstall psy-view, only, type:: 184 | 185 | conda uninstall psy-view 186 | 187 | .. _uninstall-pip: 188 | 189 | Uninstallation via pip 190 | ^^^^^^^^^^^^^^^^^^^^^^ 191 | Uninstalling via pip simply goes via:: 192 | 193 | pip uninstall psy-view 194 | 195 | Note, however, that you should use :ref:`conda ` if you 196 | installed it via conda. 197 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | REM SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | REM 3 | REM SPDX-License-Identifier: CC0-1.0 4 | 5 | @ECHO OFF 6 | 7 | pushd %~dp0 8 | 9 | REM Command file for Sphinx documentation 10 | 11 | if "%SPHINXBUILD%" == "" ( 12 | set SPHINXBUILD=sphinx-build 13 | ) 14 | set SOURCEDIR=. 15 | set BUILDDIR=_build 16 | 17 | if "%1" == "" goto help 18 | 19 | %SPHINXBUILD% >NUL 2>NUL 20 | if errorlevel 9009 ( 21 | echo. 22 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 23 | echo.installed, then set the SPHINXBUILD environment variable to point 24 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 25 | echo.may add the Sphinx directory to PATH. 26 | echo. 27 | echo.If you don't have Sphinx installed, grab it from 28 | echo.http://sphinx-doc.org/ 29 | exit /b 1 30 | ) 31 | 32 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | goto end 34 | 35 | :help 36 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 37 | 38 | :end 39 | popd 40 | -------------------------------------------------------------------------------- /docs/ncview.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _psy-view-vs-ncview: 6 | 7 | psy-view vs. ncview 8 | =================== 9 | When developping *psy-view*, we had the intuitiveness of ncview_ in mind, a 10 | light-weight graphical user interface to visualize the contents of netCDF files. 11 | 12 | In general, `psy-view` can do everything that `ncview` does, and more. 13 | 14 | .. image:: _static/ncview.png 15 | :alt: ncview screenshot 16 | :target: http://meteora.ucsd.edu/~pierce/ncview_home_page.html 17 | 18 | .. _ncview: http://meteora.ucsd.edu/~pierce/ncview_home_page.html 19 | 20 | The following table tries to summarize the differences of the features for both 21 | softwares. If you feel like anything is missing or wrong, please tell us by 22 | creating a new issue at https://github.com/psyplot/psy-view/issues/ 23 | 24 | .. list-table:: psy-view vs. ncview 25 | :stub-columns: 1 26 | :header-rows: 1 27 | :widths: 2 4 2 28 | 29 | * - Feature 30 | - psy-view 31 | - ncview 32 | * - supported grids 33 | - 34 | * rectilinear (i.e. standard :math:`nx\times ny` grid) 35 | * ICON_ (triangular, hexagonal, etc.) 36 | * UGRID_ (triangular, hexagonal, etc.) 37 | - rectilinear 38 | * - supported plots 39 | - 40 | * georeferenced plots 41 | * standard 2D-plots 42 | * line plots 43 | - 44 | * georeferenced plots 45 | * standard 2D-plots 46 | * line plots 47 | * - mouse features 48 | - 49 | * plot a time series when clicking on a plot 50 | * show coordinates and data when hovering the plot 51 | - 52 | * plot a time series when clicking on a plot 53 | * show coordinates and data when hovering the plot 54 | * - View the data 55 | - not yet implemented 56 | - comes with a simple and basic editor 57 | * - image export 58 | - all common formats (e.g. 59 | :abbr:`PDF (Portable Document Format)`, 60 | :abbr:`PNG (Portable Network Graphics)`, 61 | :abbr:`GIF (Graphics Interchange Format)`, etc.) with high resolution 62 | - :abbr:`PS (PostScript)` 63 | * - animation export 64 | - GIF, MP4 (using ffmpeg or imagemagick) 65 | 66 | .. note:: 67 | 68 | This is a beta feature 69 | 70 | - not implemented 71 | * - :abbr:`GUI (Graphical User Interface`) startup time 72 | - fast locally, slow via X11 73 | - fast 74 | * - projection support 75 | - 76 | * decodes CF-conformal grid_mapping_ attributes 77 | * flexibly choose the `projection of the plot via cartopy`_ 78 | - 79 | * decodes CF-conformal grid_mapping_ attributes 80 | * plots on standard lat-lon projection only 81 | * - supported files 82 | - anything that is supported by xarray (netCDF, GRIB, GeoTIFF, etc.) 83 | 84 | .. todo:: 85 | 86 | add more documentation for supported file types 87 | - netCDF files only 88 | * - Language 89 | - Entirely written in Python with the use of 90 | 91 | * xarray_ 92 | * matplotlib_ 93 | * PyQt5_ 94 | * cartopy_ 95 | - Entirely written in C 96 | * - Extensibility 97 | - psy-view is built upon psyplot, so you can 98 | 99 | * export the plot settings 100 | * use it in python scripts 101 | * use the more general `psyplot GUI`_ 102 | - cannot be extended 103 | 104 | 105 | .. _grid_mapping: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#appendix-grid-mappings 106 | .. _projection of the plot via cartopy: https://scitools.org.uk/cartopy/docs/latest/reference/crs.html#list-of-projections 107 | .. _xarray: http://xarray.pydata.org/en/stable/ 108 | .. _matplotlib: https://matplotlib.org/ 109 | .. _PyQt5: https://riverbankcomputing.com/software/pyqt 110 | .. _cartopy: https://scitools.org.uk/cartopy/docs/latest 111 | .. _psyplot GUI: https://psyplot.github.io/psyplot-gui/ 112 | .. _ICON: https://code.mpimet.mpg.de/projects/iconpublic 113 | .. _UGRID: http://ugrid-conventions.github.io/ugrid-conventions/ 114 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | sphinx-design 6 | git+https://codebase.helmholtz.cloud/hcdc/hereon-netcdf/sphinxext.git 7 | git+https://codebase.helmholtz.cloud/psyplot/psyplot.git@develop 8 | git+https://codebase.helmholtz.cloud/psyplot/psy-simple.git@develop 9 | git+https://codebase.helmholtz.cloud/psyplot/psy-maps.git@develop 10 | git+https://codebase.helmholtz.cloud/psyplot/psyplot-gui.git@develop 11 | -------------------------------------------------------------------------------- /docs/todo.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _todo: 6 | 7 | ToDos 8 | ===== 9 | 10 | .. todolist:: 11 | -------------------------------------------------------------------------------- /docs/user-guide.rst: -------------------------------------------------------------------------------- 1 | .. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | .. 3 | .. SPDX-License-Identifier: CC-BY-4.0 4 | 5 | .. _user-guide: 6 | 7 | User guide 8 | =============== 9 | 10 | .. highlight:: bash 11 | 12 | Starting the GUI 13 | ---------------- 14 | 15 | Assuming that you :ref:`installed ` psy-view, you can start it by 16 | typing:: 17 | 18 | $ psy-view 19 | 20 | in the terminal (or Anaconda Prompt on Windows). On windows you additionally 21 | have the opportunity to start it from the start menu (just search for 22 | psy-view), assuming that you have :ref:`installed it via conda `. 23 | 24 | You can also directly pass a path to a netCDF file, e.g.:: 25 | 26 | $ psy-view demo.nc 27 | 28 | to open it. You can also directly select a variable for plotting via the 29 | ``-n`` option, e.g.:: 30 | 31 | $ psy-view demo.nc -n t2m 32 | 33 | Please see the section :ref:`command-line` for more information, or type:: 34 | 35 | $ psy-view --help 36 | 37 | in the terminal. 38 | 39 | .. note:: 40 | 41 | The psy-view widget is also available from the `psyplot GUI`_. Just type:: 42 | 43 | $ psyplot 44 | 45 | in the terminal to start it from there. See also :ref:`psyplot-gui-embed`. 46 | 47 | 48 | .. _psyplot GUI: https://psyplot.github.io/psyplot-gui 49 | 50 | 51 | .. _user-guide-gui: 52 | 53 | The GUI 54 | ------- 55 | 56 | The usage of psy-view should be quite intuitive and this small guide gives you 57 | a quick intro into the central elements. Please let us know if you 58 | encounter any problems. 59 | 60 | .. screenshot-figure:: ds_widget docs-ds_widget.png 61 | :plot: 62 | 63 | psy-views central element: the dataset widget. 64 | 65 | The dataset widget is the central element of psy-view. It runs as a standalone 66 | application, or within the psyplot gui (see :ref:`psyplot-gui-embed`). 67 | 68 | Resizing the GUI elements 69 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 70 | The widget is made flexible such that you can adapt the heights of the 71 | individual elements. Just move your cursor between the elements to change their 72 | size. 73 | 74 | .. only:: html 75 | 76 | The following screencast illustrates this functionality: 77 | 78 | .. image:: _static/resize-demo.gif 79 | 80 | The central elements (from top to bottom) are presented in the next sections. 81 | 82 | .. _user-guide-open: 83 | 84 | Open a netCDF file 85 | ^^^^^^^^^^^^^^^^^^ 86 | 87 | .. screenshot:: ds_widget.open_widget docs-open_widget.png 88 | 89 | Click the |btn_open| button to select a netCDF file from the disk, or directly 90 | enter the path in the line widget. You can open multiple datasets at the 91 | same time within the widget. The selection of the current dataset can be 92 | done through the dataset tree (see below) 93 | 94 | 95 | .. |btn_open| screenshot:: ds_widget.btn_open docs-btn_open.png 96 | :width: 1.3em 97 | 98 | 99 | .. _user-guide-ds_tree: 100 | 101 | View the dataset 102 | ^^^^^^^^^^^^^^^^ 103 | 104 | .. screenshot:: ds_widget.ds_tree docs-ds_tree.png 105 | 106 | Here you can see all open datasets and select the one you want to 107 | visualize. Expand the items to get more information about variables and 108 | their attributes. 109 | 110 | .. _user-guide-navigation: 111 | 112 | Navigate and export 113 | ^^^^^^^^^^^^^^^^^^^ 114 | 115 | .. screenshot:: ds_widget.navigation_box.parentWidget() docs-navigation.png 116 | :plot: 117 | 118 | In the top row, you can increase or decrease the dimension of the plotted variable. 119 | Clicking |btn_prev| (or |btn_next|) decreases (or increases) the selected 120 | dimension, whereas |btn_animate_backward| and |btn_animate_forward| makes an 121 | animation. You can control the speed (i.e. frames per second) of the 122 | animation via the slider next to the control |sl_interval| |lbl_interval| 123 | 124 | The |btn_export| menu allows you to export your plots as images files, 125 | animations or to export the plot settings for later usage. The |btn_preset| 126 | button lets you select custom presets for your plots (see the 127 | :ref:`psyplot docs `). 128 | 129 | The |btn_reload| button finally let's you close all open figures and datasets 130 | and automatically recreates the figures with the same settings. This can be 131 | very useful when the file on your disk changed, and you just want to see the 132 | latest version. 133 | 134 | .. |btn_prev| screenshot:: ds_widget.btn_prev docs-btn_prev.png 135 | :height: 1.3em 136 | :enable: 137 | 138 | .. |btn_next| screenshot:: ds_widget.btn_next docs-btn_next.png 139 | :height: 1.3em 140 | :enable: 141 | 142 | .. |btn_animate_backward| screenshot:: ds_widget.btn_animate_backward docs-btn_animate_backward.png 143 | :height: 1.3em 144 | :enable: 145 | 146 | .. |btn_animate_forward| screenshot:: ds_widget.btn_animate_forward docs-btn_animate_forward.png 147 | :height: 1.3em 148 | :enable: 149 | 150 | .. |sl_interval| screenshot:: ds_widget.sl_interval docs-sl_interval.png 151 | :height: 1.3em 152 | :enable: 153 | 154 | .. |lbl_interval| screenshot:: ds_widget.lbl_interval docs-lbl_interval.png 155 | :height: 1.3em 156 | :enable: 157 | 158 | .. |btn_preset| screenshot:: ds_widget.btn_preset docs-btn_preset.png 159 | :height: 1.3em 160 | :enable: 161 | 162 | .. |btn_export| screenshot:: ds_widget.btn_export docs-btn_export.png 163 | :height: 1.3em 164 | :enable: 165 | 166 | .. |btn_reload| screenshot:: ds_widget.btn_reload docs-btn_reload.png 167 | :height: 1.3em 168 | :enable: 169 | 170 | .. _user-guide-select-plot: 171 | 172 | Select the active plot 173 | ^^^^^^^^^^^^^^^^^^^^^^ 174 | 175 | .. screenshot:: ds_widget.array_frame docs-array_frame.png 176 | :plot: 177 | 178 | The next section let's you switch between the different open plots. Once you 179 | have created a new plot with one of the variable buttons (see 180 | :ref:`below `), you can 181 | 182 | - create additional plots by clicking the |btn_add| button. This will open a 183 | dialog to select a variable which is then plotted with the current plotmethod 184 | - close existing plots by clicking the |btn_del| button. 185 | - switch between the plots using the combo box |combo_array| which allows you 186 | to change the appearence of a different plot. 187 | 188 | .. |btn_add| screenshot:: ds_widget.btn_add docs-btn_add.png 189 | :width: 1.3em 190 | :enable: 191 | 192 | .. |btn_del| screenshot:: ds_widget.btn_del docs-btn_del.png 193 | :width: 1.3em 194 | :enable: 195 | 196 | .. |combo_array| screenshot:: ds_widget.combo_array docs-combo_array.png 197 | :height: 1.3em 198 | :plot: 199 | 200 | 201 | .. _user-guide-plotmethod: 202 | 203 | Select the plot method 204 | ^^^^^^^^^^^^^^^^^^^^^^ 205 | 206 | .. screenshot:: ds_widget.plot_tabs docs-plot_tabs.png 207 | :plot: 208 | 209 | psy-view (currently) supports three of the psyplot plot methods. 210 | 211 | - :attr:`~psy_simple:psyplot.project.plot.plot2d` for 2D scalar fields 212 | (rectilinear or unstructured, see the section :ref:`user-guide-plot2d`) 213 | - :attr:`~psy_maps:psyplot.project.plot.mapplot` for **georeferenced** 2D scalar 214 | fields (rectilinear or unstructured, see the section :ref:`user-guide-mapplot`) 215 | - :attr:`~psy_simple:psyplot.project.plot.lineplot` for 1D lines (see the 216 | section , see the section :ref:`user-guide-lineplot`) 217 | 218 | .. _user-guide-mapplot: 219 | 220 | mapplot 221 | ~~~~~~~ 222 | 223 | .. ipython:: 224 | :suppress: 225 | 226 | In [1]: import psyplot.project as psy 227 | ...: 228 | ...: with psy.plot.mapplot( 229 | ...: "demo.nc", 230 | ...: name="t2m", 231 | ...: cmap="viridis", 232 | ...: xgrid=False, 233 | ...: ygrid=False, 234 | ...: ) as sp: 235 | ...: sp.export("docs-mapplot-example.png") 236 | 237 | .. image:: docs-mapplot-example.png 238 | 239 | .. screenshot:: ds_widget.plotmethod_widget docs-mapplot.png 240 | :plot: 241 | :plotmethod: mapplot 242 | 243 | 244 | For georeferenced 2D-scalar fields (or more than 2D), you have the following 245 | options: 246 | 247 | - clicking on a grid cell in the plot generates a line plot of the variable at 248 | that location (as you know it from ncview). The x-axis is determined by the 249 | dimension you chose in the navigation (see :ref:`user-guide-navigation`). 250 | - the colormap button |btn_cmap| changes the colormap to another preset 251 | - the |btn_cmap_settings| button opens a dialog for more advanced color settings 252 | - the |btn_proj| button switches to other projections for the basemap 253 | - the |btn_proj_settings| button opens a dialog for formatting the background 254 | (meridionals, parallels, land color, ocean color, coastlines, etc.) 255 | - the :guilabel:`Plot type` menu |combo_plot| let's you select the type of 256 | plotting. You can choose one of the following options 257 | 258 | Default 259 | This mode uses an efficient algorithm for regular lat-lon meshes (using 260 | matplotlibs :func:`~matplotlib.pyplot.pcolormesh` function), or an 261 | explicit drawing of the individual grid cell polygons for unstructured 262 | grids (see `Gridcell polygons` below). These two methods draw each grid 263 | cells explicitly. Gridcell boundaries are thereby extracted following the 264 | CF (or UGRID)-Conventions. If this is not possible, they are interpolated 265 | from the gridcell coordinates. 266 | Filled contours 267 | Different from the `Default` method this is not visualizing each cell 268 | individually, but instead plots the contours using matplotlibs 269 | :func:`~matplotlib.pyplot.contourf` function. 270 | Contours 271 | Similar to `Filled contours`, but we only draw the outlines of the contour 272 | areas using matplotlibs :func:`~matplotlib.pyplot.contour` function. 273 | Gridcell polygons 274 | This mode (which is the default for unstructured grids (not curvilinear 275 | grids) draws each grid cell individually using a variant of matplotlibs 276 | :func:`~matplotlib.pyplot.pcolor` function 277 | Disable 278 | Make no plotting at all. This can be useful if you want to display the 279 | datagrid only (see next point) 280 | 281 | More information on the plot options can be found in the docs of the 282 | :attr:`~psy_maps.plotters.FieldPlotter.plot` formatoption. 283 | 284 | - the |btn_datagrid| toggles the visibility of grid cell boundaries 285 | - the |btn_labels| button opens a dialog to edit colorbar labels, titles, etc. 286 | 287 | Furthermore you have a couple of dropdowns: 288 | 289 | x- and y-dimension 290 | This is the dimension in the netCDF variable that represents the longitudinal 291 | (latitudinal) dimension. 292 | x- and y-coordinate 293 | This is the coordinate in the netCDF file that is used for the finally to 294 | visualize the data (equivalent to the `CF-conventions coordinates attribute`_ 295 | of a netCDF variable.) 296 | 297 | psyplot automatically decodes the variable and sets x- and y-dimension, as well 298 | as the appropriate coordinate. These dropdowns, however, let you modify the 299 | automatic choice of psyplot. 300 | 301 | .. _CF-conventions coordinates attribute: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#coordinate-types 302 | 303 | .. |btn_cmap| screenshot:: ds_widget.plotmethod_widget.btn_cmap docs-mapplot-btn_cmap.png 304 | :height: 1.3em 305 | :plot: 306 | 307 | .. |btn_cmap_settings| screenshot:: ds_widget.plotmethod_widget.btn_cmap_settings docs-mapplot-btn_cmap_settings.png 308 | :width: 1.3em 309 | :plot: 310 | 311 | .. |btn_proj| screenshot:: ds_widget.plotmethod_widget.btn_proj docs-mapplot-btn_proj.png 312 | :height: 1.3em 313 | :plot: 314 | 315 | .. |btn_proj_settings| screenshot:: ds_widget.plotmethod_widget.btn_proj_settings docs-mapplot-btn_proj_settings.png 316 | :width: 1.3em 317 | :plot: 318 | 319 | .. |combo_plot| screenshot:: ds_widget.plotmethod_widget.combo_plot docs-mapplot-combo_plot.png 320 | :height: 1.3em 321 | :plot: 322 | 323 | .. |btn_datagrid| screenshot:: ds_widget.plotmethod_widget.btn_datagrid docs-mapplot-btn_datagrid.png 324 | :height: 1.3em 325 | :plot: 326 | 327 | .. |btn_labels| screenshot:: ds_widget.plotmethod_widget.btn_labels docs-mapplot-btn_labels.png 328 | :height: 1.3em 329 | :plot: 330 | 331 | 332 | .. _user-guide-plot2d: 333 | 334 | plot2d 335 | ~~~~~~ 336 | 337 | .. ipython:: 338 | :suppress: 339 | 340 | In [1]: import psyplot.project as psy 341 | ...: 342 | ...: with psy.plot.plot2d( 343 | ...: "demo.nc", 344 | ...: name="t2m", 345 | ...: cmap="viridis", 346 | ...: ) as sp: 347 | ...: sp.export("docs-plot2d-example.png") 348 | 349 | .. image:: docs-plot2d-example.png 350 | 351 | .. screenshot:: ds_widget.plotmethod_widget docs-plot2d.png 352 | :plot: 353 | :plotmethod: plot2d 354 | 355 | Simple 2D plots are also possible for variables with 2 dimensions and more (or 356 | scalar fields on an unstructured grid). The options are the same as for 357 | :ref:`mapplot `, but for obvious reasons there are no 358 | projection and basemap settings. 359 | 360 | 361 | .. _user-guide-lineplot: 362 | 363 | lineplot 364 | ~~~~~~~~ 365 | 366 | .. ipython:: 367 | :suppress: 368 | :okwarning: 369 | 370 | In [1]: import psyplot.project as psy 371 | ...: 372 | ...: with psy.plot.lineplot( 373 | ...: "demo.nc", 374 | ...: name="t2m", 375 | ...: x=0, 376 | ...: y=[0, 15], 377 | ...: z=0, 378 | ...: xticklabels="%B", 379 | ...: xticks="data", 380 | ...: legendlabels="%(y)1.0f°N", 381 | ...: legend="lower right", 382 | ...: ylabel="{desc}", 383 | ...: ) as sp: 384 | ...: sp.export("docs-lineplot-example.png") 385 | 386 | .. image:: docs-lineplot-example.png 387 | 388 | .. screenshot:: ds_widget.plotmethod_widget docs-lineplot.png 389 | :plot: 390 | :plotmethod: lineplot 391 | 392 | 393 | The lineplot visualizes your variables as a 1D line. This widget provides the 394 | following functionalities: 395 | 396 | 397 | - choose the x-dimension using the dimension dropdown |combo_dims| 398 | - add new lines to the plot using the |lineplot.btn_add| button 399 | - remove lines from the plot using the |lineplot.btn_del| button 400 | - switch the current line using the dropdown |combo_lines| 401 | 402 | .. note:: 403 | 404 | Changing the variable (see :ref:`user-guide-variables`) or the 405 | dimensions (see :ref:`user-guide-dimensions`) only affects the current 406 | line that you can select with the |combo_lines| dropdown. 407 | 408 | 409 | 410 | .. |combo_dims| screenshot:: ds_widget.plotmethod_widget.combo_dims docs-lineplot-combo_dims.png 411 | :height: 1.3em 412 | :plot: 413 | :plotmethod: lineplot 414 | 415 | .. |lineplot.btn_add| screenshot:: ds_widget.plotmethod_widget.btn_add docs-lineplot-btn_add.png 416 | :width: 1.3em 417 | :enable: 418 | :plotmethod: lineplot 419 | 420 | .. |lineplot.btn_del| screenshot:: ds_widget.plotmethod_widget.btn_del docs-lineplot-btn_del.png 421 | :width: 1.3em 422 | :enable: 423 | :plotmethod: lineplot 424 | 425 | .. |combo_lines| screenshot:: ds_widget.plotmethod_widget.combo_lines docs-lineplot-combo_lines.png 426 | :height: 1.3em 427 | :plot: 428 | :plotmethod: lineplot 429 | 430 | 431 | .. _user-guide-variables: 432 | 433 | Select the variables 434 | ^^^^^^^^^^^^^^^^^^^^ 435 | 436 | .. screenshot:: ds_widget.variable_frame docs-variable_frame.png 437 | 438 | The next section in the GUI shows the variables in the active dataset (note that 439 | you can switch to another dataset using the dataset tree, see 440 | :ref:`above `). 441 | 442 | Click on a variable to make a plot. If there is already a plot of a variable in 443 | the dataset, it will be updated to show the new data. 444 | 445 | .. note:: 446 | 447 | The variable buttons will make new plots, if there is None already, or 448 | update the variable in the current plot. If you want to visualize two plots 449 | at the same time, use the |btn_add| button (see the 450 | :ref:`plot selection above `). 451 | 452 | 453 | .. _user-guide-dimensions: 454 | 455 | Select the dimensions 456 | ^^^^^^^^^^^^^^^^^^^^^ 457 | 458 | .. screenshot:: ds_widget.dimension_table docs-dimension_table.png 459 | :plot: 460 | :minwidth: 1200 461 | 462 | The last table is the dimension table. When a variable is plotted, this widget 463 | displays the ranges, of the netCDF dimensions and lets you update the scalar 464 | dimensions (in the screenshot above, `time` |btn_time| and `lev` |btn_lev|). 465 | 466 | Left-click on such a button increases the dimension of the plot by one step, 467 | right-click decreases the dimension. 468 | 469 | .. |btn_time| screenshot:: ds_widget.dimension_table.cellWidget(0,2) docs-time-button.png 470 | :height: 1.3em 471 | :plot: 472 | 473 | .. |btn_lev| screenshot:: ds_widget.dimension_table.cellWidget(1,2) docs-lev-button.png 474 | :height: 1.3em 475 | :plot: 476 | 477 | .. _user-guide-navigate-plot: 478 | 479 | Navigation inside the plot 480 | -------------------------- 481 | 482 | psy-view uses matplotlib for the visualization which comes with an interactive 483 | backend to navigate inside the plot. 484 | 485 | .. screenshot:: ds_widget.plotter.ax.figure.canvas.manager.toolbar docs-mpl-toolbar.png 486 | :plot: 487 | 488 | Especially the Pan/Zoom button |btn_mpl_pan| and the zoom-to-rectangle button 489 | |btn_mpl_zoom| are of interest for you. You can enable and disable them by 490 | clicking on the corresponding button in the toolbar. 491 | 492 | .. warning:: 493 | 494 | In principal you can also edit the colormap using the |btn_mpl_settings| 495 | button from the toolbar. But this is known to cause errors for the mapplot 496 | method (see `#25`_), so you should use the corresponding widgets from the gui (see 497 | :ref:`user-guide-mapplot`). 498 | 499 | .. screenshot:: ds_widget.plotter.ax.figure.canvas.manager.toolbar.actions()[4].associatedWidgets()[1] docs-btn_mpl_pan.png 500 | :width: 3em 501 | :plot: 502 | 503 | The ``Pan/Zoom`` button 504 | This button has two modes: pan and zoom. Click the toolbar button 505 | to activate panning and zooming, then put your mouse somewhere 506 | over an axes. Press the left mouse button and hold it to pan the 507 | figure, dragging it to a new position. When you release it, the 508 | data under the point where you pressed will be moved to the point 509 | where you released. If you press 'x' or 'y' while panning the 510 | motion will be constrained to the x or y axis, respectively. Press 511 | the right mouse button to zoom, dragging it to a new position. 512 | The x axis will be zoomed in proportionately to the rightward 513 | movement and zoomed out proportionately to the leftward movement. 514 | The same is true for the y axis and up/down motions. The point under your 515 | mouse when you begin the zoom remains stationary, allowing you to 516 | zoom in or out around that point as much as you wish. You can use the 517 | modifier keys 'x', 'y' or 'CONTROL' to constrain the zoom to the x 518 | axis, the y axis, or aspect ratio preserve, respectively. 519 | 520 | With polar plots, the pan and zoom functionality behaves 521 | differently. The radius axis labels can be dragged using the left 522 | mouse button. The radius scale can be zoomed in and out using the 523 | right mouse button. 524 | 525 | .. screenshot:: ds_widget.plotter.ax.figure.canvas.manager.toolbar.actions()[5].associatedWidgets()[1] docs-btn_mpl_zoom.png 526 | :width: 3em 527 | :plot: 528 | 529 | The ``Zoom-to-rectangle`` button 530 | Click this toolbar button to activate this mode. Put your mouse somewhere 531 | over an axes and press a mouse button. Define a rectangular region by 532 | dragging the mouse while holding the button to a new location. When using 533 | the left mouse button, the axes view limits will be zoomed to the defined 534 | region. When using the right mouse button, the axes view limits will be 535 | zoomed out, placing the original axes in the defined region. 536 | 537 | More information can be found in the `matplotlib documentation`_. 538 | 539 | .. |btn_mpl_pan| image:: _static/docs-btn_mpl_pan.png 540 | :width: 1.3em 541 | 542 | .. |btn_mpl_zoom| image:: _static/docs-btn_mpl_zoom.png 543 | :width: 1.3em 544 | 545 | .. |btn_mpl_settings| screenshot:: ds_widget.plotter.ax.figure.canvas.manager.toolbar.actions()[6].associatedWidgets()[1] docs-btn_mpl_settings.png 546 | :width: 1.3em 547 | :plot: 548 | 549 | .. _#25: https://github.com/psyplot/psy-view/issues/25 550 | .. _matplotlib documentation: https://matplotlib.org/users/navigation_toolbar.html 551 | 552 | .. _psyplot-gui-embed: 553 | 554 | Using psy-view within the psyplot GUI 555 | ------------------------------------- 556 | psy-view is also available from the psyplot GUI. Just type ``psyplot`` in the 557 | terminal to start it. The only difference is that the available plots (see 558 | :ref:`user-guide-select-plot`) are managed through the current main project 559 | (:func:`psyplot.project.gcp`, see also the psyplot GUIs 560 | :ref:`project content `), also accessible through 561 | the ``mp`` variable in the 562 | :ref:`integrated IPython console `. This gives you extra 563 | power as you now cannot only change your plots through the intuitive psy-view 564 | interface, but also from the command line or through the more flexible 565 | :ref:`formatoptions widget `. 566 | -------------------------------------------------------------------------------- /icon/CreateICNS.sh: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | mkdir main.iconset 6 | sips -z 16 16 icon1024.png --out main.iconset/icon_16x16.png 7 | sips -z 32 32 icon1024.png --out main.iconset/icon_16x16@2x.png 8 | sips -z 32 32 icon1024.png --out main.iconset/icon_32x32.png 9 | sips -z 64 64 icon1024.png --out main.iconset/icon_32x32@2x.png 10 | sips -z 128 128 icon1024.png --out main.iconset/icon_128x128.png 11 | sips -z 256 256 icon1024.png --out main.iconset/icon_128x128@2x.png 12 | sips -z 256 256 icon1024.png --out main.iconset/icon_256x256.png 13 | sips -z 512 512 icon1024.png --out main.iconset/icon_256x256@2x.png 14 | sips -z 512 512 icon1024.png --out main.iconset/icon_512x512.png 15 | cp icon1024.png main.iconset/icon_512x512@2x.png 16 | iconutil -c icns main.iconset 17 | rm -R main.iconset 18 | -------------------------------------------------------------------------------- /icon/CreateICO.sh: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | convert icon1024.png -define icon:auto-resize=64,48,32,16 psyplot.ico 6 | -------------------------------------------------------------------------------- /icon/icon.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | """Create the psyplot icon 6 | 7 | This script creates the psyplot icon with a dpi of 128 and a width and height 8 | of 8 inches. The file is saved it to ``'icon1024.pkl'``""" 9 | 10 | import cartopy.crs as ccrs 11 | import cartopy.feature as cf 12 | import matplotlib.pyplot as plt 13 | from matplotlib.text import FontProperties 14 | 15 | # The path to the font 16 | fontpath = "/usr/share/fonts/truetype/freefont/FreeSansBoldOblique.ttf" 17 | 18 | fig = plt.figure(figsize=(8, 8), dpi=128) 19 | 20 | ax = fig.add_axes( 21 | [0.0, 0.0, 1.0, 1.0], projection=ccrs.Orthographic(central_latitude=5) 22 | ) 23 | 24 | land = ax.add_feature(cf.LAND, facecolor="0.975") 25 | ocean = ax.add_feature(cf.OCEAN, facecolor=plt.get_cmap("Blues")(0.5)) 26 | 27 | text = ax.text( 28 | 0.47, 29 | 0.5, 30 | "Psy", 31 | transform=fig.transFigure, 32 | name="FreeSans", 33 | fontproperties=FontProperties(fname=fontpath), 34 | size=256, 35 | ha="center", 36 | va="center", 37 | weight=400, 38 | ) 39 | 40 | ax.outline_patch.set_edgecolor("none") 41 | 42 | plt.savefig("icon1024.png", transparent=True) 43 | plt.savefig("icon1024.svg", transparent=True) 44 | -------------------------------------------------------------------------------- /icon/icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/icon/icon1024.png -------------------------------------------------------------------------------- /icon/icon1024.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /icon/icon1024.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /icon/psy-view.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /icon/psyplot.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /psy_view/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | """psy-view 6 | 7 | ncview-like interface to psyplot 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import argparse 13 | import sys 14 | from typing import Optional 15 | 16 | # importing xarray here for some reason speeds up starting the GUI... 17 | import xarray as xr 18 | 19 | from . import _version 20 | 21 | __version__ = _version.get_versions()["version"] 22 | 23 | __author__ = "Philipp S. Sommer" 24 | 25 | __copyright__ = """ 26 | Copyright (C) 2021 Helmholtz-Zentrum Hereon 27 | Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht 28 | """ 29 | 30 | __credits__ = ["Philipp S. Sommer"] 31 | __license__ = "LGPL-3.0-only" 32 | 33 | __maintainer__ = "Philipp S. Sommer" 34 | __email__ = "philipp.sommer@hereon.de" 35 | 36 | __status__ = "Production" 37 | 38 | 39 | def start_app( 40 | ds: Optional[xr.Dataset], 41 | name: Optional[str] = None, 42 | plotmethod: str = "mapplot", 43 | preset: Optional[str] = None, 44 | ) -> None: 45 | """Start the standalone GUI application. 46 | 47 | This function creates a `QApplication` instance, an instance of the 48 | :class:`psy_view.ds_widget.DatasetWidget` and enters the main event loop. 49 | 50 | Parameters 51 | ---------- 52 | ds: xarray.Dataset 53 | The dataset to display. If None, the user can select it afterwards 54 | name: str 55 | The variable name in `ds` to display. If None, the user can select it 56 | afterwards 57 | plotmethod: {'mapplot' | 'lineplot' | 'plot2d' } 58 | The plotmethod to use 59 | preset: str 60 | The preset to apply 61 | """ 62 | from psyplot_gui import rcParams 63 | from PyQt5 import QtWidgets 64 | from PyQt5.QtGui import QIcon # pylint: disable=no-name-in-module 65 | 66 | rcParams["help_explorer.use_webengineview"] = False 67 | 68 | from psyplot_gui.common import get_icon 69 | 70 | from psy_view.ds_widget import DatasetWidgetStandAlone 71 | 72 | app = QtWidgets.QApplication(sys.argv) 73 | ds_widget = DatasetWidgetStandAlone(ds) 74 | ds_widget.setWindowIcon(QIcon(get_icon("logo.svg"))) 75 | if preset is not None: 76 | ds_widget.load_preset(preset) 77 | if name is not None: 78 | if ds is None: 79 | raise ValueError("Variable specified but without dataset") 80 | elif name not in ds_widget.variable_buttons: 81 | valid = list(ds_widget.variable_buttons) 82 | raise ValueError( 83 | f"{name} is not part of the dataset. " 84 | f"Possible variables are {valid}." 85 | ) 86 | ds_widget.plotmethod = plotmethod 87 | ds_widget.variable = name 88 | ds_widget.make_plot() 89 | ds_widget.refresh() 90 | ds_widget.show() 91 | ds_widget.show_current_figure() 92 | sys.excepthook = ds_widget.excepthook 93 | sys.exit(app.exec_()) 94 | 95 | 96 | def get_parser() -> argparse.ArgumentParser: 97 | """Get the command line parser for psy-view.""" 98 | from textwrap import dedent 99 | 100 | parser = argparse.ArgumentParser("psy-view") 101 | 102 | parser.add_argument( 103 | "input_file", help="The file to visualize", nargs="?", default=None 104 | ) 105 | 106 | parser.add_argument( 107 | "-n", 108 | "--name", 109 | help=( 110 | "Variable name to display. Don't provide a variable to display " 111 | "the first variable found in the dataset." 112 | ), 113 | const=object, 114 | nargs="?", 115 | ) 116 | 117 | parser.add_argument( 118 | "-pm", 119 | "--plotmethod", 120 | help="The plotmethod to use", 121 | default="mapplot", 122 | choices=["mapplot", "plot2d", "lineplot"], 123 | ) 124 | 125 | parser.add_argument("--preset", help="Apply a preset to the plot") 126 | 127 | parser.add_argument( 128 | "-V", "--version", action="version", version=__version__ 129 | ) 130 | 131 | parser.epilog = dedent( 132 | """ 133 | psy-view Copyright (C) 2020 Philipp S. Sommer 134 | 135 | This program comes with ABSOLUTELY NO WARRANTY. 136 | This is free software, and you are welcome to redistribute it 137 | under the conditions of the GNU GENERAL PUBLIC LICENSE, Version 3.""" 138 | ) 139 | 140 | return parser 141 | 142 | 143 | def main() -> None: 144 | """Start the app with the provided command-line options.""" 145 | import psyplot.project as psy 146 | 147 | parser = get_parser() 148 | args = parser.parse_known_args()[0] 149 | 150 | if args.input_file is not None: 151 | try: 152 | ds = psy.open_dataset(args.input_file) 153 | except Exception: 154 | ds = psy.open_dataset(args.input_file, decode_times=False) 155 | else: 156 | ds = None 157 | 158 | if args.name is object and ds is not None: 159 | args.name = list(ds)[0] 160 | 161 | start_app(ds, args.name, args.plotmethod, args.preset) 162 | 163 | 164 | if __name__ == "__main__": 165 | main() 166 | -------------------------------------------------------------------------------- /psy_view/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """main module of straditize 3 | 4 | **Disclaimer** 5 | 6 | Copyright (C) 2020 Philipp S. Sommer 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see https://www.gnu.org/licenses/. 20 | """ 21 | 22 | # SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht 23 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 24 | # 25 | # SPDX-License-Identifier: LGPL-3.0-only 26 | 27 | import psy_view 28 | 29 | if __name__ == "__main__": 30 | psy_view.main() 31 | -------------------------------------------------------------------------------- /psy_view/_version.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: LGPL-3.0-only 4 | 5 | # This file helps to compute a version number in source trees obtained from 6 | # git-archive tarball (such as those provided by githubs download-from-tag 7 | # feature). Distribution tarballs (built by setup.py sdist) and build 8 | # directories (produced by setup.py build) will contain a much shorter file 9 | # that just contains the computed version number. 10 | 11 | # This file is released into the public domain. Generated by 12 | # versioneer-0.21 (https://github.com/python-versioneer/python-versioneer) 13 | 14 | """Git implementation of _version.py.""" 15 | 16 | import errno 17 | import os 18 | import re 19 | import subprocess 20 | import sys 21 | from typing import Callable, Dict 22 | 23 | 24 | def get_keywords(): 25 | """Get the keywords needed to look up the version information.""" 26 | # these strings will be replaced by git during git-archive. 27 | # setup.py/versioneer.py will grep for the variable names, so they must 28 | # each be defined on a line of their own. _version.py will just call 29 | # get_keywords(). 30 | git_refnames = " (HEAD -> master, tag: v0.3.0)" 31 | git_full = "85bf032bdd5b66a659e7484249c1bc38b77dfecc" 32 | git_date = "2024-04-03 20:22:08 +0200" 33 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 34 | return keywords 35 | 36 | 37 | class VersioneerConfig: 38 | """Container for Versioneer configuration parameters.""" 39 | 40 | 41 | def get_config(): 42 | """Create, populate and return the VersioneerConfig() object.""" 43 | # these strings are filled in when 'setup.py versioneer' creates 44 | # _version.py 45 | cfg = VersioneerConfig() 46 | cfg.VCS = "git" 47 | cfg.style = "pep440" 48 | cfg.tag_prefix = "v" 49 | cfg.parentdir_prefix = "psy-view-" 50 | cfg.versionfile_source = "psy_view/_version.py" 51 | cfg.verbose = False 52 | return cfg 53 | 54 | 55 | class NotThisMethod(Exception): 56 | """Exception raised if a method is not valid for the current scenario.""" 57 | 58 | 59 | LONG_VERSION_PY: Dict[str, str] = {} 60 | HANDLERS: Dict[str, Dict[str, Callable]] = {} 61 | 62 | 63 | def register_vcs_handler(vcs, method): # decorator 64 | """Create decorator to mark a method as the handler of a VCS.""" 65 | 66 | def decorate(f): 67 | """Store f in HANDLERS[vcs][method].""" 68 | if vcs not in HANDLERS: 69 | HANDLERS[vcs] = {} 70 | HANDLERS[vcs][method] = f 71 | return f 72 | 73 | return decorate 74 | 75 | 76 | def run_command( 77 | commands, args, cwd=None, verbose=False, hide_stderr=False, env=None 78 | ): 79 | """Call the given command(s).""" 80 | assert isinstance(commands, list) 81 | process = None 82 | for command in commands: 83 | try: 84 | dispcmd = str([command] + args) 85 | # remember shell=False, so use git.cmd on windows, not just git 86 | process = subprocess.Popen( 87 | [command] + args, 88 | cwd=cwd, 89 | env=env, 90 | stdout=subprocess.PIPE, 91 | stderr=(subprocess.PIPE if hide_stderr else None), 92 | ) 93 | break 94 | except OSError: 95 | e = sys.exc_info()[1] 96 | if e.errno == errno.ENOENT: 97 | continue 98 | if verbose: 99 | print("unable to run %s" % dispcmd) 100 | print(e) 101 | return None, None 102 | else: 103 | if verbose: 104 | print("unable to find command, tried %s" % (commands,)) 105 | return None, None 106 | stdout = process.communicate()[0].strip().decode() 107 | if process.returncode != 0: 108 | if verbose: 109 | print("unable to run %s (error)" % dispcmd) 110 | print("stdout was %s" % stdout) 111 | return None, process.returncode 112 | return stdout, process.returncode 113 | 114 | 115 | def versions_from_parentdir(parentdir_prefix, root, verbose): 116 | """Try to determine the version from the parent directory name. 117 | 118 | Source tarballs conventionally unpack into a directory that includes both 119 | the project name and a version string. We will also support searching up 120 | two directory levels for an appropriately named parent directory 121 | """ 122 | rootdirs = [] 123 | 124 | for _ in range(3): 125 | dirname = os.path.basename(root) 126 | if dirname.startswith(parentdir_prefix): 127 | return { 128 | "version": dirname[len(parentdir_prefix) :], 129 | "full-revisionid": None, 130 | "dirty": False, 131 | "error": None, 132 | "date": None, 133 | } 134 | rootdirs.append(root) 135 | root = os.path.dirname(root) # up a level 136 | 137 | if verbose: 138 | print( 139 | "Tried directories %s but none started with prefix %s" 140 | % (str(rootdirs), parentdir_prefix) 141 | ) 142 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 143 | 144 | 145 | @register_vcs_handler("git", "get_keywords") 146 | def git_get_keywords(versionfile_abs): 147 | """Extract version information from the given file.""" 148 | # the code embedded in _version.py can just fetch the value of these 149 | # keywords. When used from setup.py, we don't want to import _version.py, 150 | # so we do it with a regexp instead. This function is not used from 151 | # _version.py. 152 | keywords = {} 153 | try: 154 | with open(versionfile_abs, "r") as fobj: 155 | for line in fobj: 156 | if line.strip().startswith("git_refnames ="): 157 | mo = re.search(r'=\s*"(.*)"', line) 158 | if mo: 159 | keywords["refnames"] = mo.group(1) 160 | if line.strip().startswith("git_full ="): 161 | mo = re.search(r'=\s*"(.*)"', line) 162 | if mo: 163 | keywords["full"] = mo.group(1) 164 | if line.strip().startswith("git_date ="): 165 | mo = re.search(r'=\s*"(.*)"', line) 166 | if mo: 167 | keywords["date"] = mo.group(1) 168 | except OSError: 169 | pass 170 | return keywords 171 | 172 | 173 | @register_vcs_handler("git", "keywords") 174 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 175 | """Get version information from git keywords.""" 176 | if "refnames" not in keywords: 177 | raise NotThisMethod("Short version file found") 178 | date = keywords.get("date") 179 | if date is not None: 180 | # Use only the last line. Previous lines may contain GPG signature 181 | # information. 182 | date = date.splitlines()[-1] 183 | 184 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 185 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 186 | # -like" string, which we must then edit to make compliant), because 187 | # it's been around since git-1.5.3, and it's too difficult to 188 | # discover which version we're using, or to work around using an 189 | # older one. 190 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 191 | refnames = keywords["refnames"].strip() 192 | if refnames.startswith("$Format"): 193 | if verbose: 194 | print("keywords are unexpanded, not using") 195 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 196 | refs = {r.strip() for r in refnames.strip("()").split(",")} 197 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 198 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 199 | TAG = "tag: " 200 | tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} 201 | if not tags: 202 | # Either we're using git < 1.8.3, or there really are no tags. We use 203 | # a heuristic: assume all version tags have a digit. The old git %d 204 | # expansion behaves like git log --decorate=short and strips out the 205 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 206 | # between branches and tags. By ignoring refnames without digits, we 207 | # filter out many common branch names like "release" and 208 | # "stabilization", as well as "HEAD" and "master". 209 | tags = {r for r in refs if re.search(r"\d", r)} 210 | if verbose: 211 | print("discarding '%s', no digits" % ",".join(refs - tags)) 212 | if verbose: 213 | print("likely tags: %s" % ",".join(sorted(tags))) 214 | for ref in sorted(tags): 215 | # sorting will prefer e.g. "2.0" over "2.0rc1" 216 | if ref.startswith(tag_prefix): 217 | r = ref[len(tag_prefix) :] 218 | # Filter out refs that exactly match prefix or that don't start 219 | # with a number once the prefix is stripped (mostly a concern 220 | # when prefix is '') 221 | if not re.match(r"\d", r): 222 | continue 223 | if verbose: 224 | print("picking %s" % r) 225 | return { 226 | "version": r, 227 | "full-revisionid": keywords["full"].strip(), 228 | "dirty": False, 229 | "error": None, 230 | "date": date, 231 | } 232 | # no suitable tags, so version is "0+unknown", but full hex is still there 233 | if verbose: 234 | print("no suitable tags, using unknown + full revision id") 235 | return { 236 | "version": "0+unknown", 237 | "full-revisionid": keywords["full"].strip(), 238 | "dirty": False, 239 | "error": "no suitable tags", 240 | "date": None, 241 | } 242 | 243 | 244 | @register_vcs_handler("git", "pieces_from_vcs") 245 | def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 246 | """Get version from 'git describe' in the root of the source tree. 247 | 248 | This only gets called if the git-archive 'subst' keywords were *not* 249 | expanded, and _version.py hasn't already been rewritten with a short 250 | version string, meaning we're inside a checked out source tree. 251 | """ 252 | GITS = ["git"] 253 | TAG_PREFIX_REGEX = "*" 254 | if sys.platform == "win32": 255 | GITS = ["git.cmd", "git.exe"] 256 | TAG_PREFIX_REGEX = r"\*" 257 | 258 | _, rc = runner( 259 | GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True 260 | ) 261 | if rc != 0: 262 | if verbose: 263 | print("Directory %s not under git control" % root) 264 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 265 | 266 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 267 | # if there isn't one, this yields HEX[-dirty] (no NUM) 268 | describe_out, rc = runner( 269 | GITS, 270 | [ 271 | "describe", 272 | "--tags", 273 | "--dirty", 274 | "--always", 275 | "--long", 276 | "--match", 277 | "%s%s" % (tag_prefix, TAG_PREFIX_REGEX), 278 | ], 279 | cwd=root, 280 | ) 281 | # --long was added in git-1.5.5 282 | if describe_out is None: 283 | raise NotThisMethod("'git describe' failed") 284 | describe_out = describe_out.strip() 285 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 286 | if full_out is None: 287 | raise NotThisMethod("'git rev-parse' failed") 288 | full_out = full_out.strip() 289 | 290 | pieces = {} 291 | pieces["long"] = full_out 292 | pieces["short"] = full_out[:7] # maybe improved later 293 | pieces["error"] = None 294 | 295 | branch_name, rc = runner( 296 | GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root 297 | ) 298 | # --abbrev-ref was added in git-1.6.3 299 | if rc != 0 or branch_name is None: 300 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 301 | branch_name = branch_name.strip() 302 | 303 | if branch_name == "HEAD": 304 | # If we aren't exactly on a branch, pick a branch which represents 305 | # the current commit. If all else fails, we are on a branchless 306 | # commit. 307 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 308 | # --contains was added in git-1.5.4 309 | if rc != 0 or branches is None: 310 | raise NotThisMethod("'git branch --contains' returned error") 311 | branches = branches.split("\n") 312 | 313 | # Remove the first line if we're running detached 314 | if "(" in branches[0]: 315 | branches.pop(0) 316 | 317 | # Strip off the leading "* " from the list of branches. 318 | branches = [branch[2:] for branch in branches] 319 | if "master" in branches: 320 | branch_name = "master" 321 | elif not branches: 322 | branch_name = None 323 | else: 324 | # Pick the first branch that is returned. Good or bad. 325 | branch_name = branches[0] 326 | 327 | pieces["branch"] = branch_name 328 | 329 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 330 | # TAG might have hyphens. 331 | git_describe = describe_out 332 | 333 | # look for -dirty suffix 334 | dirty = git_describe.endswith("-dirty") 335 | pieces["dirty"] = dirty 336 | if dirty: 337 | git_describe = git_describe[: git_describe.rindex("-dirty")] 338 | 339 | # now we have TAG-NUM-gHEX or HEX 340 | 341 | if "-" in git_describe: 342 | # TAG-NUM-gHEX 343 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 344 | if not mo: 345 | # unparsable. Maybe git-describe is misbehaving? 346 | pieces["error"] = ( 347 | "unable to parse git-describe output: '%s'" % describe_out 348 | ) 349 | return pieces 350 | 351 | # tag 352 | full_tag = mo.group(1) 353 | if not full_tag.startswith(tag_prefix): 354 | if verbose: 355 | fmt = "tag '%s' doesn't start with prefix '%s'" 356 | print(fmt % (full_tag, tag_prefix)) 357 | pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( 358 | full_tag, 359 | tag_prefix, 360 | ) 361 | return pieces 362 | pieces["closest-tag"] = full_tag[len(tag_prefix) :] 363 | 364 | # distance: number of commits since tag 365 | pieces["distance"] = int(mo.group(2)) 366 | 367 | # commit: short hex revision ID 368 | pieces["short"] = mo.group(3) 369 | 370 | else: 371 | # HEX: no tags 372 | pieces["closest-tag"] = None 373 | count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root) 374 | pieces["distance"] = int(count_out) # total number of commits 375 | 376 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 377 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ 378 | 0 379 | ].strip() 380 | # Use only the last line. Previous lines may contain GPG signature 381 | # information. 382 | date = date.splitlines()[-1] 383 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 384 | 385 | return pieces 386 | 387 | 388 | def plus_or_dot(pieces): 389 | """Return a + if we don't already have one, else return a .""" 390 | if "+" in pieces.get("closest-tag", ""): 391 | return "." 392 | return "+" 393 | 394 | 395 | def render_pep440(pieces): 396 | """Build up version string, with post-release "local version identifier". 397 | 398 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 399 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 400 | 401 | Exceptions: 402 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 403 | """ 404 | if pieces["closest-tag"]: 405 | rendered = pieces["closest-tag"] 406 | if pieces["distance"] or pieces["dirty"]: 407 | rendered += plus_or_dot(pieces) 408 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 409 | if pieces["dirty"]: 410 | rendered += ".dirty" 411 | else: 412 | # exception #1 413 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 414 | if pieces["dirty"]: 415 | rendered += ".dirty" 416 | return rendered 417 | 418 | 419 | def render_pep440_branch(pieces): 420 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 421 | 422 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 423 | (a feature branch will appear "older" than the master branch). 424 | 425 | Exceptions: 426 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 427 | """ 428 | if pieces["closest-tag"]: 429 | rendered = pieces["closest-tag"] 430 | if pieces["distance"] or pieces["dirty"]: 431 | if pieces["branch"] != "master": 432 | rendered += ".dev0" 433 | rendered += plus_or_dot(pieces) 434 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 435 | if pieces["dirty"]: 436 | rendered += ".dirty" 437 | else: 438 | # exception #1 439 | rendered = "0" 440 | if pieces["branch"] != "master": 441 | rendered += ".dev0" 442 | rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 443 | if pieces["dirty"]: 444 | rendered += ".dirty" 445 | return rendered 446 | 447 | 448 | def pep440_split_post(ver): 449 | """Split pep440 version string at the post-release segment. 450 | 451 | Returns the release segments before the post-release and the 452 | post-release version number (or -1 if no post-release segment is present). 453 | """ 454 | vc = str.split(ver, ".post") 455 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 456 | 457 | 458 | def render_pep440_pre(pieces): 459 | """TAG[.postN.devDISTANCE] -- No -dirty. 460 | 461 | Exceptions: 462 | 1: no tags. 0.post0.devDISTANCE 463 | """ 464 | if pieces["closest-tag"]: 465 | if pieces["distance"]: 466 | # update the post release segment 467 | tag_version, post_version = pep440_split_post( 468 | pieces["closest-tag"] 469 | ) 470 | rendered = tag_version 471 | if post_version is not None: 472 | rendered += ".post%d.dev%d" % ( 473 | post_version + 1, 474 | pieces["distance"], 475 | ) 476 | else: 477 | rendered += ".post0.dev%d" % (pieces["distance"]) 478 | else: 479 | # no commits, use the tag as the version 480 | rendered = pieces["closest-tag"] 481 | else: 482 | # exception #1 483 | rendered = "0.post0.dev%d" % pieces["distance"] 484 | return rendered 485 | 486 | 487 | def render_pep440_post(pieces): 488 | """TAG[.postDISTANCE[.dev0]+gHEX] . 489 | 490 | The ".dev0" means dirty. Note that .dev0 sorts backwards 491 | (a dirty tree will appear "older" than the corresponding clean one), 492 | but you shouldn't be releasing software with -dirty anyways. 493 | 494 | Exceptions: 495 | 1: no tags. 0.postDISTANCE[.dev0] 496 | """ 497 | if pieces["closest-tag"]: 498 | rendered = pieces["closest-tag"] 499 | if pieces["distance"] or pieces["dirty"]: 500 | rendered += ".post%d" % pieces["distance"] 501 | if pieces["dirty"]: 502 | rendered += ".dev0" 503 | rendered += plus_or_dot(pieces) 504 | rendered += "g%s" % pieces["short"] 505 | else: 506 | # exception #1 507 | rendered = "0.post%d" % pieces["distance"] 508 | if pieces["dirty"]: 509 | rendered += ".dev0" 510 | rendered += "+g%s" % pieces["short"] 511 | return rendered 512 | 513 | 514 | def render_pep440_post_branch(pieces): 515 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 516 | 517 | The ".dev0" means not master branch. 518 | 519 | Exceptions: 520 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 521 | """ 522 | if pieces["closest-tag"]: 523 | rendered = pieces["closest-tag"] 524 | if pieces["distance"] or pieces["dirty"]: 525 | rendered += ".post%d" % pieces["distance"] 526 | if pieces["branch"] != "master": 527 | rendered += ".dev0" 528 | rendered += plus_or_dot(pieces) 529 | rendered += "g%s" % pieces["short"] 530 | if pieces["dirty"]: 531 | rendered += ".dirty" 532 | else: 533 | # exception #1 534 | rendered = "0.post%d" % pieces["distance"] 535 | if pieces["branch"] != "master": 536 | rendered += ".dev0" 537 | rendered += "+g%s" % pieces["short"] 538 | if pieces["dirty"]: 539 | rendered += ".dirty" 540 | return rendered 541 | 542 | 543 | def render_pep440_old(pieces): 544 | """TAG[.postDISTANCE[.dev0]] . 545 | 546 | The ".dev0" means dirty. 547 | 548 | Exceptions: 549 | 1: no tags. 0.postDISTANCE[.dev0] 550 | """ 551 | if pieces["closest-tag"]: 552 | rendered = pieces["closest-tag"] 553 | if pieces["distance"] or pieces["dirty"]: 554 | rendered += ".post%d" % pieces["distance"] 555 | if pieces["dirty"]: 556 | rendered += ".dev0" 557 | else: 558 | # exception #1 559 | rendered = "0.post%d" % pieces["distance"] 560 | if pieces["dirty"]: 561 | rendered += ".dev0" 562 | return rendered 563 | 564 | 565 | def render_git_describe(pieces): 566 | """TAG[-DISTANCE-gHEX][-dirty]. 567 | 568 | Like 'git describe --tags --dirty --always'. 569 | 570 | Exceptions: 571 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 572 | """ 573 | if pieces["closest-tag"]: 574 | rendered = pieces["closest-tag"] 575 | if pieces["distance"]: 576 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 577 | else: 578 | # exception #1 579 | rendered = pieces["short"] 580 | if pieces["dirty"]: 581 | rendered += "-dirty" 582 | return rendered 583 | 584 | 585 | def render_git_describe_long(pieces): 586 | """TAG-DISTANCE-gHEX[-dirty]. 587 | 588 | Like 'git describe --tags --dirty --always -long'. 589 | The distance/hash is unconditional. 590 | 591 | Exceptions: 592 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 593 | """ 594 | if pieces["closest-tag"]: 595 | rendered = pieces["closest-tag"] 596 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 597 | else: 598 | # exception #1 599 | rendered = pieces["short"] 600 | if pieces["dirty"]: 601 | rendered += "-dirty" 602 | return rendered 603 | 604 | 605 | def render(pieces, style): 606 | """Render the given version pieces into the requested style.""" 607 | if pieces["error"]: 608 | return { 609 | "version": "unknown", 610 | "full-revisionid": pieces.get("long"), 611 | "dirty": None, 612 | "error": pieces["error"], 613 | "date": None, 614 | } 615 | 616 | if not style or style == "default": 617 | style = "pep440" # the default 618 | 619 | if style == "pep440": 620 | rendered = render_pep440(pieces) 621 | elif style == "pep440-branch": 622 | rendered = render_pep440_branch(pieces) 623 | elif style == "pep440-pre": 624 | rendered = render_pep440_pre(pieces) 625 | elif style == "pep440-post": 626 | rendered = render_pep440_post(pieces) 627 | elif style == "pep440-post-branch": 628 | rendered = render_pep440_post_branch(pieces) 629 | elif style == "pep440-old": 630 | rendered = render_pep440_old(pieces) 631 | elif style == "git-describe": 632 | rendered = render_git_describe(pieces) 633 | elif style == "git-describe-long": 634 | rendered = render_git_describe_long(pieces) 635 | else: 636 | raise ValueError("unknown style '%s'" % style) 637 | 638 | return { 639 | "version": rendered, 640 | "full-revisionid": pieces["long"], 641 | "dirty": pieces["dirty"], 642 | "error": None, 643 | "date": pieces.get("date"), 644 | } 645 | 646 | 647 | def get_versions(): 648 | """Get version information or return default if unable to do so.""" 649 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 650 | # __file__, we can work backwards from there to the root. Some 651 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 652 | # case we can only use expanded keywords. 653 | 654 | cfg = get_config() 655 | verbose = cfg.verbose 656 | 657 | try: 658 | return git_versions_from_keywords( 659 | get_keywords(), cfg.tag_prefix, verbose 660 | ) 661 | except NotThisMethod: 662 | pass 663 | 664 | try: 665 | root = os.path.realpath(__file__) 666 | # versionfile_source is the relative path from the top of the source 667 | # tree (where the .git directory might live) to this file. Invert 668 | # this to find the root from __file__. 669 | for _ in cfg.versionfile_source.split("/"): 670 | root = os.path.dirname(root) 671 | except NameError: 672 | return { 673 | "version": "0+unknown", 674 | "full-revisionid": None, 675 | "dirty": None, 676 | "error": "unable to find root of source tree", 677 | "date": None, 678 | } 679 | 680 | try: 681 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 682 | return render(pieces, cfg.style) 683 | except NotThisMethod: 684 | pass 685 | 686 | try: 687 | if cfg.parentdir_prefix: 688 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 689 | except NotThisMethod: 690 | pass 691 | 692 | return { 693 | "version": "0+unknown", 694 | "full-revisionid": None, 695 | "dirty": None, 696 | "error": "unable to compute version", 697 | "date": None, 698 | } 699 | -------------------------------------------------------------------------------- /psy_view/icons/color_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/psy_view/icons/color_settings.png -------------------------------------------------------------------------------- /psy_view/icons/color_settings.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH 2 | SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) 3 | 4 | SPDX-License-Identifier: CC-BY-4.0 5 | -------------------------------------------------------------------------------- /psy_view/icons/color_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /psy_view/icons/color_settings.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /psy_view/icons/proj_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/psy_view/icons/proj_settings.png -------------------------------------------------------------------------------- /psy_view/icons/proj_settings.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH 2 | SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) 3 | 4 | SPDX-License-Identifier: CC-BY-4.0 5 | -------------------------------------------------------------------------------- /psy_view/icons/proj_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /psy_view/icons/proj_settings.svg.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /psy_view/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/psy_view/py.typed -------------------------------------------------------------------------------- /psy_view/rcsetup.py: -------------------------------------------------------------------------------- 1 | """Configuration parameters for psy-view.""" 2 | 3 | # SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht 4 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 5 | # 6 | # SPDX-License-Identifier: LGPL-3.0-only 7 | 8 | from __future__ import annotations 9 | 10 | from typing import Any, Dict, List, Optional 11 | 12 | from psyplot.config.rcsetup import validate_dict 13 | from psyplot_gui.config.rcsetup import ( 14 | RcParams, 15 | psyplot_fname, 16 | validate_stringlist, 17 | ) 18 | 19 | defaultParams: Dict[str, List[Any]] = { 20 | "projections": [ 21 | ["cf", "cyl", "robin", "ortho", "moll", "northpole", "southpole"], 22 | validate_stringlist, 23 | "The names of available projections", 24 | ], 25 | "savefig_kws": [ 26 | dict(dpi=250), 27 | validate_dict, 28 | "Options that are passed to plt.savefig when exporting images", 29 | ], 30 | "animations.export_kws": [ 31 | dict(writer="ffmpeg"), 32 | validate_dict, 33 | "Options that are passed to FuncAnimation.save", 34 | ], 35 | } 36 | 37 | 38 | class PsyViewRcParams(RcParams): 39 | """RcParams for the psyplot-gui package.""" 40 | 41 | HEADER: str = RcParams.HEADER.replace( 42 | "psyplotrc.yml", "psyviewrc.yml" 43 | ).replace("PSYVIEWRC", "psyviewrc.yml") 44 | 45 | def load_from_file(self, fname: Optional[str] = None): 46 | """ 47 | Update rcParams from user-defined settings 48 | 49 | This function updates the instance with what is found in `fname` 50 | 51 | Parameters 52 | ---------- 53 | fname: str 54 | Path to the yaml configuration file. Possible keys of the 55 | dictionary are defined by :data:`config.rcsetup.defaultParams`. 56 | If None, the :func:`config.rcsetup.psyplot_fname` function is used. 57 | 58 | See Also 59 | -------- 60 | dump_to_file, psyplot_fname""" 61 | fname = fname or psyplot_fname( 62 | env_key="PSYVIEWRC", fname="psyviewrc.yml" 63 | ) 64 | if fname: 65 | super().load_from_file(fname) 66 | 67 | 68 | rcParams = PsyViewRcParams(defaultParams=defaultParams) 69 | rcParams.update_from_defaultParams() 70 | rcParams.load_from_file() 71 | -------------------------------------------------------------------------------- /psy_view/utils.py: -------------------------------------------------------------------------------- 1 | """Utility functions for psy-view.""" 2 | 3 | # SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht 4 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 5 | # 6 | # SPDX-License-Identifier: LGPL-3.0-only 7 | 8 | from __future__ import annotations 9 | 10 | import os.path as osp 11 | from typing import TYPE_CHECKING, Callable, List, Optional, Union, cast 12 | 13 | from PyQt5 import QtCore, QtGui, QtWidgets 14 | 15 | if TYPE_CHECKING: 16 | from PyQt5.QtCore import QEvent # pylint: disable=no-name-in-module 17 | 18 | 19 | def get_icon(name: str, ending: str = ".png") -> str: 20 | return osp.join(osp.dirname(__file__), "icons", name + ending) 21 | 22 | 23 | def add_pushbutton( 24 | label: str, 25 | connections: Optional[Union[List[Callable], Callable]] = None, 26 | tooltip: Optional[str] = None, 27 | layout: Optional[QtWidgets.QLayout] = None, 28 | icon: bool = False, 29 | toolbutton: bool = False, 30 | *args, 31 | **kwargs, 32 | ) -> Union[QtWidgets.QPushButton, QtWidgets.QToolButton]: 33 | if icon or toolbutton: 34 | btn = QtWidgets.QToolButton(*args, **kwargs) 35 | if icon: 36 | btn.setIcon(QtGui.QIcon(label)) 37 | else: 38 | btn.setText(label) 39 | else: 40 | btn = QtWidgets.QPushButton(label, *args, **kwargs) 41 | if tooltip is not None: 42 | btn.setToolTip(tooltip) 43 | if connections is not None: 44 | try: 45 | iter(connections) # type: ignore 46 | except TypeError: 47 | connections = [connections] # type: ignore 48 | connections = cast(List[Callable], connections) 49 | for con in connections: 50 | btn.clicked.connect(con) 51 | if layout is not None: 52 | layout.addWidget(btn) 53 | return btn 54 | 55 | 56 | class QRightPushButton(QtWidgets.QPushButton): 57 | """A push button that acts differently when right-clicked""" 58 | 59 | rightclicked = QtCore.pyqtSignal() 60 | 61 | def mousePressEvent(self, event: QEvent): 62 | if event.button() == QtCore.Qt.RightButton: 63 | self.rightclicked.emit() 64 | event.accept() 65 | else: 66 | return super().mousePressEvent(event) 67 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | [build-system] 6 | build-backend = 'setuptools.build_meta' 7 | requires = ['setuptools >= 61.0', 'versioneer[toml]'] 8 | 9 | [project] 10 | name = "psy-view" 11 | dynamic = ["version"] 12 | description = "ncview-like interface to psyplot" 13 | 14 | readme = "README.md" 15 | keywords = [ 16 | "visualization", 17 | 18 | "psyplot", 19 | 20 | "netcdf", 21 | 22 | "raster", 23 | 24 | "cartopy", 25 | 26 | "earth-sciences", 27 | 28 | "pyqt", 29 | 30 | "qt", 31 | 32 | "ipython", 33 | 34 | "jupyter", 35 | 36 | "qtconsole", 37 | 38 | "ncview", 39 | ] 40 | 41 | authors = [ 42 | { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' }, 43 | ] 44 | maintainers = [ 45 | { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' }, 46 | ] 47 | license = { text = 'LGPL-3.0-only' } 48 | 49 | classifiers = [ 50 | "Development Status :: 5 - Production/Stable", 51 | "Intended Audience :: Science/Research", 52 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 53 | "Topic :: Scientific/Engineering :: Visualization", 54 | "Topic :: Scientific/Engineering :: GIS", 55 | "Topic :: Scientific/Engineering", 56 | "Operating System :: OS Independent", 57 | "Programming Language :: Python", 58 | "Programming Language :: Python :: 3", 59 | "Programming Language :: Python :: 3 :: Only", 60 | "Programming Language :: Python :: 3.9", 61 | "Programming Language :: Python :: 3.10", 62 | "Typing :: Typed", 63 | ] 64 | 65 | requires-python = '>= 3.9' 66 | dependencies = [ 67 | "psyplot", 68 | # add your dependencies here 69 | "netCDF4", 70 | "psyplot-gui>=1.5.0", 71 | "psy-maps>=1.5.0", 72 | ] 73 | 74 | [project.urls] 75 | Homepage = 'https://codebase.helmholtz.cloud/psyplot/psy-view' 76 | Documentation = "https://psyplot.github.io/psy-view" 77 | Source = "https://codebase.helmholtz.cloud/psyplot/psy-view" 78 | Tracker = "https://codebase.helmholtz.cloud/psyplot/psy-view/issues/" 79 | 80 | 81 | 82 | [project.scripts] 83 | psy-view = "psy_view:main" 84 | 85 | 86 | [project.entry-points."psyplot_gui"] 87 | psy-view = "psy_view.ds_widget:DatasetWidgetPlugin" 88 | 89 | [project.optional-dependencies] 90 | testsite = [ 91 | "tox", 92 | "isort==5.12.0", 93 | "black==23.1.0", 94 | "blackdoc==0.3.8", 95 | "flake8==6.0.0", 96 | "pre-commit", 97 | "mypy", 98 | "pytest-cov", 99 | "reuse", 100 | "cffconvert", 101 | "types-PyYAML", 102 | "types-docutils", 103 | "dask", 104 | "pytest-qt", 105 | ] 106 | docs = [ 107 | "autodocsumm", 108 | "sphinx-rtd-theme", 109 | "hereon-netcdf-sphinxext", 110 | "sphinx-design", 111 | "dask", 112 | "sphinx-argparse", 113 | ] 114 | dev = [ 115 | "psy-view[testsite]", 116 | "psy-view[docs]", 117 | "PyYAML", 118 | ] 119 | 120 | 121 | [tool.mypy] 122 | ignore_missing_imports = true 123 | 124 | [tool.setuptools] 125 | zip-safe = false 126 | license-files = ["LICENSES/*"] 127 | 128 | [tool.setuptools.package-data] 129 | psy_view = [ 130 | "py.typed", 131 | "psy_view/icons/*.png", 132 | "psy_view/icons/*.png.license", 133 | ] 134 | 135 | [tool.setuptools.packages.find] 136 | namespaces = false 137 | exclude = [ 138 | 'docs', 139 | 'tests*', 140 | 'examples' 141 | ] 142 | 143 | [tool.pytest.ini_options] 144 | addopts = '-v' 145 | 146 | [tool.versioneer] 147 | VCS = 'git' 148 | style = 'pep440' 149 | versionfile_source = 'psy_view/_version.py' 150 | versionfile_build = 'psy_view/_version.py' 151 | tag_prefix = 'v' 152 | parentdir_prefix = 'psy-view-' 153 | 154 | [tool.isort] 155 | profile = "black" 156 | line_length = 79 157 | src_paths = ["psy_view"] 158 | float_to_top = true 159 | known_first_party = "psy_view" 160 | 161 | [tool.black] 162 | line-length = 79 163 | target-version = ['py39'] 164 | 165 | [tool.coverage.run] 166 | omit = ["psy_view/_version.py"] 167 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | # 3 | # SPDX-License-Identifier: CC0-1.0 4 | 5 | """Setup script for the psy-view package.""" 6 | import versioneer 7 | from setuptools import setup 8 | 9 | setup( 10 | version=versioneer.get_version(), 11 | cmdclass=versioneer.get_cmdclass(), 12 | ) 13 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """pytest configuration file for psy-view.""" 2 | 3 | # SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht 4 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 5 | # 6 | # SPDX-License-Identifier: LGPL-3.0-only 7 | 8 | import os.path as osp 9 | from pathlib import Path 10 | 11 | # import psyplot_gui.compat to make sure, qt settings are set 12 | import psyplot_gui.compat.qtcompat # noqa: F401 13 | import pytest 14 | 15 | _test_dir = Path(__file__).parent 16 | 17 | 18 | @pytest.fixture 19 | def test_dir() -> Path: 20 | return _test_dir 21 | 22 | 23 | @pytest.fixture( 24 | params=[ 25 | "regular-test.nc", 26 | "regional-icon-test.nc", 27 | "rotated-pole-test.nc", 28 | "icon-test.nc", 29 | ] 30 | ) 31 | def test_file(test_dir, request) -> Path: 32 | return test_dir / request.param 33 | 34 | 35 | @pytest.fixture 36 | def test_ds(test_file): 37 | import psyplot.data as psyd 38 | 39 | with psyd.open_dataset(test_file) as ds: 40 | yield ds 41 | 42 | 43 | @pytest.fixture 44 | def ds_widget(qtbot, test_ds): 45 | import matplotlib.pyplot as plt 46 | import psyplot.project as psy 47 | 48 | from psy_view.ds_widget import DatasetWidget 49 | 50 | w = DatasetWidget(test_ds) 51 | qtbot.addWidget(w) 52 | yield w 53 | w._sp = None 54 | psy.close("all") 55 | plt.close("all") 56 | -------------------------------------------------------------------------------- /tests/icon-test.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/tests/icon-test.nc -------------------------------------------------------------------------------- /tests/icon-test.nc.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | ; SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | ; 3 | ; SPDX-License-Identifier: CC-BY-4.0 4 | 5 | [pytest] 6 | qt_api=pyqt5 7 | -------------------------------------------------------------------------------- /tests/regional-icon-test.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/tests/regional-icon-test.nc -------------------------------------------------------------------------------- /tests/regional-icon-test.nc.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /tests/regular-test.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/tests/regular-test.nc -------------------------------------------------------------------------------- /tests/regular-test.nc.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /tests/rotated-pole-test.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psyplot/psy-view/85bf032bdd5b66a659e7484249c1bc38b77dfecc/tests/rotated-pole-test.nc -------------------------------------------------------------------------------- /tests/rotated-pole-test.nc.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | 3 | SPDX-License-Identifier: CC0-1.0 4 | -------------------------------------------------------------------------------- /tests/test_dialogs.py: -------------------------------------------------------------------------------- 1 | """Test the formatoption dialogs.""" 2 | 3 | # SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht 4 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 5 | # 6 | # SPDX-License-Identifier: LGPL-3.0-only 7 | from __future__ import annotations 8 | 9 | from typing import TYPE_CHECKING 10 | 11 | import pytest 12 | 13 | if TYPE_CHECKING: 14 | from psy_view.dialogs import BasemapDialog 15 | 16 | 17 | @pytest.fixture 18 | def test_project(test_ds): 19 | sp = test_ds.psy.plot.mapplot(name="t2m") 20 | yield sp 21 | sp.close() 22 | 23 | 24 | @pytest.fixture 25 | def cmap_dialog(qtbot, test_project): 26 | from psy_view.dialogs import CmapDialog 27 | 28 | dialog = CmapDialog(test_project) 29 | qtbot.addWidget(dialog) 30 | return dialog 31 | 32 | 33 | @pytest.fixture 34 | def basemap_dialog(qtbot, test_project): 35 | from psy_view.dialogs import BasemapDialog 36 | 37 | dialog = BasemapDialog(test_project.plotters[0]) 38 | qtbot.addWidget(dialog) 39 | return dialog 40 | 41 | 42 | def test_colorbar_preview_valid_bounds(cmap_dialog): 43 | """Test whether the update to a new bounds setting works""" 44 | bounds = [240, 270, 310] 45 | cmap_dialog.bounds_widget.editor.set_obj(bounds) 46 | 47 | assert list(cmap_dialog.cbar_preview.cbar.norm.boundaries) == bounds 48 | 49 | 50 | def test_colorbar_preview_valid_cmap(cmap_dialog): 51 | """Test whether the update to a new cmap setting works""" 52 | cmap = "Blues" 53 | cmap_dialog.cmap_widget.editor.set_obj(cmap) 54 | 55 | assert cmap_dialog.cbar_preview.cbar.cmap.name == cmap 56 | 57 | 58 | def test_colorbar_preview_valid_ticks(cmap_dialog): 59 | """Test whether the update to a new cticks setting works""" 60 | ticks = [285, 290] 61 | cmap_dialog.cticks_widget.editor.set_obj(ticks) 62 | 63 | assert list(cmap_dialog.cbar_preview.cbar.get_ticks()) == ticks 64 | 65 | 66 | def test_colorbar_preview_invalid_bounds(cmap_dialog): 67 | """Test whether the update to a invalid bounds setting works""" 68 | bounds = list(cmap_dialog.cbar_preview.cbar.norm.boundaries) 69 | 70 | # set invalid bounds 71 | cmap_dialog.bounds_widget.editor.text = "[1, 2, 3" 72 | 73 | assert list(cmap_dialog.cbar_preview.cbar.norm.boundaries) == bounds 74 | 75 | 76 | def test_colorbar_preview_invalid_cmap(cmap_dialog): 77 | """Test whether the update to a invalued cmap setting works""" 78 | cmap = cmap_dialog.cbar_preview.cbar.cmap.name 79 | 80 | # set invalid cmap 81 | cmap_dialog.cmap_widget.editor.text = "Blue" 82 | 83 | assert cmap_dialog.cbar_preview.cbar.cmap.name == cmap 84 | 85 | 86 | def test_colorbar_preview_invalid_ticks(cmap_dialog): 87 | """Test whether the update to a new color setting works""" 88 | ticks = list(cmap_dialog.cbar_preview.cbar.get_ticks()) 89 | 90 | # set invalid ticks 91 | cmap_dialog.cticks_widget.editor.text = "[1, 2, 3" 92 | 93 | assert list(cmap_dialog.cbar_preview.cbar.get_ticks()) == ticks 94 | 95 | 96 | def test_cmap_dialog_fmts(cmap_dialog): 97 | """Test the updating of formatoptions""" 98 | assert not cmap_dialog.fmts 99 | 100 | cmap_dialog.bounds_widget.editor.set_obj("minmax") 101 | 102 | assert cmap_dialog.fmts == {"bounds": "minmax"} 103 | 104 | 105 | def test_basemap_dialog_background_image_default( 106 | basemap_dialog: BasemapDialog, 107 | ): 108 | """Test the updating of the basemap stock image""" 109 | assert not basemap_dialog.background_img_box.isChecked() 110 | fmts = basemap_dialog.value 111 | 112 | assert "stock_img" in fmts 113 | assert not fmts["stock_img"] 114 | 115 | assert "google_map_detail" in fmts 116 | assert fmts["google_map_detail"] is None 117 | 118 | 119 | def test_basemap_dialog_background_image_stock_img( 120 | basemap_dialog: BasemapDialog, 121 | ): 122 | # test checking the stock img 123 | basemap_dialog.background_img_box.setChecked(True) 124 | basemap_dialog.opt_stock_img.setChecked(True) 125 | 126 | fmts = basemap_dialog.value 127 | 128 | assert "stock_img" in fmts 129 | assert fmts["stock_img"] 130 | 131 | assert "google_map_detail" in fmts 132 | assert fmts["google_map_detail"] is None 133 | 134 | 135 | def test_basemap_dialog_background_image_google_image( 136 | basemap_dialog: BasemapDialog, 137 | ): 138 | # test checking the stock img 139 | basemap_dialog.background_img_box.setChecked(True) 140 | basemap_dialog.opt_google_image.setChecked(True) 141 | 142 | fmts = basemap_dialog.value 143 | 144 | assert "stock_img" in fmts 145 | assert not fmts["stock_img"] 146 | 147 | assert "google_map_detail" in fmts 148 | assert fmts["google_map_detail"] == basemap_dialog.sb_google_image.value() 149 | -------------------------------------------------------------------------------- /tests/test_ds_widget.py: -------------------------------------------------------------------------------- 1 | """Test the main functionality of the psy-view package, namely the widget.""" 2 | 3 | # SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht 4 | # SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 5 | # 6 | # SPDX-License-Identifier: LGPL-3.0-only 7 | 8 | import os.path as osp 9 | import shutil 10 | import sys 11 | 12 | import pytest 13 | from PyQt5 import QtWidgets 14 | from PyQt5.QtCore import Qt 15 | 16 | 17 | def test_variables(ds_widget, test_ds): 18 | """Test existence of variables in netCDF file""" 19 | for v in test_ds: 20 | assert v in ds_widget.variable_buttons 21 | assert ds_widget.variable_buttons[v].text() == v 22 | 23 | 24 | def test_mapplot(qtbot, ds_widget): 25 | """Test plotting and closing with mapplot""" 26 | ds_widget.plotmethod = "mapplot" 27 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 28 | assert ds_widget.sp 29 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 30 | assert not ds_widget.sp 31 | 32 | 33 | def test_mapplot_fix_extent(qtbot, ds_widget, test_file): 34 | """Test restricting the extent of the data.""" 35 | import cartopy.crs as ccrs 36 | 37 | if test_file.name == "rotated-pole-test.nc": 38 | return pytest.skip("Does not work for the rotated pole CRS") 39 | 40 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 41 | 42 | plotter = ds_widget.sp.plotters[0] 43 | x0, x1, y0, y1 = map(round, plotter.ax.get_extent(ccrs.PlateCarree())) 44 | extent = [x0 + 2, x1 - 2, y0 + 2, y1 - 2] 45 | plotter.ax.set_extent(extent, ccrs.PlateCarree()) 46 | 47 | qtbot.mouseClick(ds_widget.plotmethod_widget.btn_fix_extent, Qt.LeftButton) 48 | 49 | rounded = list(map(round, plotter.map_extent.value)) 50 | assert rounded == list(extent) 51 | 52 | 53 | def test_mapplot_fix_lonlatbox(qtbot, ds_widget, test_file): 54 | """Test restricting the extent of the data.""" 55 | import cartopy.crs as ccrs 56 | 57 | if test_file.name == "rotated-pole-test.nc": 58 | return pytest.skip("Does not work for the rotated pole CRS") 59 | 60 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 61 | 62 | plotter = ds_widget.sp.plotters[0] 63 | x0, x1, y0, y1 = map(round, plotter.ax.get_extent(ccrs.PlateCarree())) 64 | extent = [x0 + 2, x1 - 2, y0 + 2, y1 - 2] 65 | plotter.ax.set_extent(extent, ccrs.PlateCarree()) 66 | 67 | qtbot.mouseClick( 68 | ds_widget.plotmethod_widget.btn_fix_lonlatbox, Qt.LeftButton 69 | ) 70 | 71 | rounded = list(map(round, plotter.lonlatbox.value)) 72 | assert rounded == list(extent) 73 | 74 | 75 | def test_mapplot_map_extent_reset(qtbot, ds_widget, test_file): 76 | """Test resetting the view.""" 77 | test_mapplot_fix_extent(qtbot, ds_widget, test_file) 78 | 79 | qtbot.mouseClick( 80 | ds_widget.plotmethod_widget.btn_reset_extent, Qt.LeftButton 81 | ) 82 | 83 | plotter = ds_widget.sp.plotters[0] 84 | 85 | assert plotter.map_extent.value is None 86 | 87 | 88 | def test_mapplot_lonlatbox_reset(qtbot, ds_widget, test_file): 89 | """Test resetting the view.""" 90 | test_mapplot_fix_lonlatbox(qtbot, ds_widget, test_file) 91 | 92 | qtbot.mouseClick( 93 | ds_widget.plotmethod_widget.btn_reset_extent, Qt.LeftButton 94 | ) 95 | 96 | plotter = ds_widget.sp.plotters[0] 97 | 98 | assert plotter.lonlatbox.value is None 99 | 100 | 101 | def test_mapplot_view_reset(qtbot, ds_widget, test_file): 102 | """Test resetting the view.""" 103 | test_mapplot_fix_extent(qtbot, ds_widget, test_file) 104 | 105 | plotter = ds_widget.sp.plotters[0] 106 | 107 | qtbot.mouseClick( 108 | ds_widget.plotmethod_widget.btn_fix_lonlatbox, Qt.LeftButton 109 | ) 110 | assert plotter.lonlatbox.value is not None 111 | 112 | qtbot.mouseClick( 113 | ds_widget.plotmethod_widget.btn_reset_extent, Qt.LeftButton 114 | ) 115 | 116 | assert plotter.map_extent.value is None 117 | assert plotter.lonlatbox.value is None 118 | 119 | 120 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"]) 121 | @pytest.mark.parametrize("i", list(range(5))) 122 | def test_change_plot_type(qtbot, ds_widget, plotmethod, i): 123 | """Test plotting and closing with mapplot""" 124 | ds_widget.plotmethod = plotmethod 125 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 126 | assert ds_widget.sp 127 | pm_widget = ds_widget.plotmethod_widget 128 | pm_widget.combo_plot.setCurrentIndex(i) 129 | plot_type = pm_widget.plot_types[i] 130 | 131 | assert ds_widget.sp.plotters[0].plot.value == plot_type 132 | 133 | 134 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"]) 135 | def test_variable_switch(qtbot, ds_widget, plotmethod): 136 | """Test switching of variables""" 137 | ds_widget.plotmethod = plotmethod 138 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 139 | assert len(ds_widget.sp) == 1 140 | assert ds_widget.data.name == "t2m" 141 | qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton) 142 | assert len(ds_widget.sp) == 1 143 | assert ds_widget.data.name == "v" 144 | qtbot.mouseClick(ds_widget.variable_buttons["v_2d"], Qt.LeftButton) 145 | assert len(ds_widget.sp) == 1 146 | assert ds_widget.data.name == "v_2d" 147 | qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton) 148 | assert len(ds_widget.sp) == 1 149 | assert ds_widget.data.name == "v" 150 | qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton) 151 | assert not ds_widget.sp 152 | 153 | 154 | def test_plot2d(qtbot, ds_widget): 155 | """Test plotting and closing with plot2d""" 156 | ds_widget.plotmethod = "plot2d" 157 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 158 | assert ds_widget.sp 159 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 160 | assert not ds_widget.sp 161 | 162 | 163 | def test_plot2d_dim_switch(qtbot, ds_widget, test_ds): 164 | arr = test_ds["t2m"] 165 | 166 | ds_widget.plotmethod = "plot2d" 167 | 168 | pm_widget = ds_widget.plotmethod_widget 169 | 170 | pm_widget.combo_xdim.setCurrentText(arr.dims[0]) 171 | pm_widget.combo_ydim.setCurrentText(arr.dims[1]) 172 | 173 | assert pm_widget.combo_xcoord.currentText() == arr.dims[0] 174 | assert pm_widget.combo_ycoord.currentText() == arr.dims[1] 175 | 176 | fmts = pm_widget.init_dims(arr) 177 | 178 | assert fmts["transpose"] 179 | 180 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 181 | 182 | assert not pm_widget.combo_xdim.isEnabled() 183 | 184 | assert ds_widget.sp 185 | assert ds_widget.plotter.plot_data.dims == arr.dims[:2] 186 | 187 | 188 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"]) 189 | def test_plot2d_coord(qtbot, ds_widget, test_ds, test_file, plotmethod): 190 | arr = test_ds.psy["t2m"] 191 | 192 | if test_file.name != "rotated-pole-test.nc": 193 | return pytest.skip("Testing rotated coords only") 194 | 195 | ydim, xdim = arr.dims[-2:] 196 | 197 | test_ds[xdim].attrs.pop("axis", None) 198 | test_ds[ydim].attrs.pop("axis", None) 199 | 200 | assert "coordinates" in arr.encoding 201 | 202 | ds_widget.plotmethod = plotmethod 203 | 204 | pm_widget = ds_widget.plotmethod_widget 205 | 206 | assert pm_widget.combo_xcoord.isEnabled() 207 | 208 | # make the plot with default setting 209 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 210 | 211 | assert not pm_widget.combo_xcoord.isEnabled() 212 | 213 | assert pm_widget.data.psy.get_coord("x").name != xdim 214 | assert pm_widget.data.psy.get_coord("y").name != ydim 215 | 216 | # remove the plot 217 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 218 | 219 | assert pm_widget.combo_xcoord.isEnabled() 220 | 221 | # tell to use the dimensions 222 | pm_widget.combo_xcoord.setCurrentText(xdim) 223 | pm_widget.combo_ycoord.setCurrentText(ydim) 224 | 225 | # make the plot with the changed settings 226 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 227 | 228 | assert not pm_widget.combo_xcoord.isEnabled() 229 | 230 | assert pm_widget.data.psy.get_coord("x").name == xdim 231 | assert pm_widget.data.psy.get_coord("y").name == ydim 232 | 233 | 234 | def test_lineplot(qtbot, ds_widget): 235 | """Test plotting and closing with lineplot""" 236 | ds_widget.plotmethod = "lineplot" 237 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 238 | assert ds_widget.sp 239 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 240 | assert not ds_widget.sp 241 | 242 | 243 | def test_lineplot_switch(qtbot, ds_widget): 244 | """Test switching of variables""" 245 | ds_widget.plotmethod = "lineplot" 246 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 247 | assert len(ds_widget.sp) == 1 248 | assert ds_widget.data.name == "t2m" 249 | qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton) 250 | assert len(ds_widget.sp) == 1 251 | assert ds_widget.data.name == "v" 252 | qtbot.mouseClick(ds_widget.variable_buttons["v"], Qt.LeftButton) 253 | assert not ds_widget.sp 254 | 255 | 256 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d"]) 257 | def test_cmap(qtbot, ds_widget, plotmethod): 258 | ds_widget.plotmethod = plotmethod 259 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 260 | cmap = ds_widget.plotter.cmap.value 261 | assert ds_widget.plotter.plot.mappable.get_cmap().name == cmap 262 | ds_widget.plotmethod_widget.btn_cmap.menu().actions()[5].trigger() 263 | assert ds_widget.plotter.cmap.value != cmap 264 | assert ds_widget.plotter.plot.mappable.get_cmap().name != cmap 265 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 266 | 267 | 268 | def test_add_and_remove_line(qtbot, ds_widget, monkeypatch): 269 | "Test adding and removing lines" 270 | ds_widget.plotmethod = "lineplot" 271 | 272 | monkeypatch.setattr( 273 | QtWidgets.QInputDialog, "getItem", lambda *args: ("t2m", True) 274 | ) 275 | 276 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 277 | assert ds_widget.sp 278 | assert len(ds_widget.sp[0]) == 1 279 | qtbot.mouseClick(ds_widget.plotmethod_widget.btn_add, Qt.LeftButton) 280 | assert len(ds_widget.sp[0]) == 2 281 | qtbot.mouseClick(ds_widget.plotmethod_widget.btn_del, Qt.LeftButton) 282 | assert len(ds_widget.sp[0]) == 1 283 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 284 | assert not ds_widget.sp 285 | 286 | 287 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"]) 288 | def test_btn_step(qtbot, ds_widget, plotmethod): 289 | """Test clicking the next time button""" 290 | ds_widget.plotmethod = plotmethod 291 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 292 | dim = ds_widget.combo_dims.currentText() 293 | assert dim 294 | assert ds_widget.data.psy.idims[dim] == 0 295 | 296 | # increase time 297 | qtbot.mouseClick(ds_widget.btn_next, Qt.LeftButton) 298 | assert ds_widget.data.psy.idims[dim] == 1 299 | 300 | # decrease time 301 | qtbot.mouseClick(ds_widget.btn_prev, Qt.LeftButton) 302 | assert ds_widget.data.psy.idims[dim] == 0 303 | 304 | 305 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"]) 306 | def test_dimension_button(qtbot, ds_widget, plotmethod): 307 | """Test clicking on a button in the dimension table""" 308 | ds_widget.plotmethod = plotmethod 309 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 310 | 311 | btn = ds_widget.dimension_table.cellWidget(1, 2) 312 | 313 | dim = ds_widget.dimension_table.verticalHeaderItem(1).text() 314 | 315 | assert ds_widget.data.psy.idims[dim] == 0 316 | 317 | qtbot.mouseClick(btn, Qt.LeftButton) 318 | 319 | assert ds_widget.data.psy.idims[dim] == 1 320 | 321 | qtbot.mouseClick(btn, Qt.RightButton) 322 | 323 | assert ds_widget.data.psy.idims[dim] == 0 324 | 325 | 326 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"]) 327 | @pytest.mark.parametrize("direction", ["forward", "backward"]) 328 | def test_animate(qtbot, ds_widget, plotmethod, direction): 329 | """Test clicking the next time button""" 330 | 331 | def animation_finished(): 332 | current = ds_widget.data.psy.idims[dim] 333 | if steps and current in steps: 334 | steps.remove(current) 335 | return False 336 | elif steps: 337 | return False 338 | else: 339 | return True 340 | 341 | ds_widget.plotmethod = plotmethod 342 | ds_widget.sl_interval.setValue(10) 343 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 344 | dim = ds_widget.combo_dims.currentText() 345 | 346 | assert dim 347 | 348 | steps = set(range(ds_widget.ds.dims[dim])) 349 | 350 | btn = getattr(ds_widget, "btn_animate_" + direction) 351 | 352 | assert not ds_widget._animating 353 | 354 | # start animation 355 | qtbot.mouseClick(btn, Qt.LeftButton) 356 | assert ds_widget._animating 357 | qtbot.waitUntil(animation_finished, timeout=30000) 358 | 359 | # stop animation 360 | qtbot.mouseClick(btn, Qt.LeftButton) 361 | assert not ds_widget._animating 362 | 363 | # restart animation 364 | steps = set(range(ds_widget.ds.dims[dim])) 365 | qtbot.mouseClick(btn, Qt.LeftButton) 366 | assert ds_widget._animating 367 | qtbot.waitUntil(animation_finished, timeout=30000) 368 | 369 | # stop animation 370 | qtbot.mouseClick(btn, Qt.LeftButton) 371 | assert not ds_widget._animating 372 | 373 | 374 | def test_enable_disable_variables(test_ds, qtbot): 375 | import numpy as np 376 | 377 | from psy_view.ds_widget import DatasetWidget 378 | 379 | test_ds["line"] = ("xtest", np.zeros(7)) 380 | test_ds["xtest"] = ("xtest", np.arange(7)) 381 | 382 | ds_widget = DatasetWidget(test_ds) 383 | qtbot.addWidget(ds_widget) 384 | 385 | assert ds_widget.variable_buttons["t2m"].isEnabled() 386 | assert not ds_widget.variable_buttons["line"].isEnabled() 387 | 388 | ds_widget.plotmethod = "lineplot" 389 | 390 | assert ds_widget.variable_buttons["t2m"].isEnabled() 391 | assert ds_widget.variable_buttons["line"].isEnabled() 392 | 393 | ds_widget.plotmethod = "plot2d" 394 | 395 | assert ds_widget.variable_buttons["t2m"].isEnabled() 396 | assert not ds_widget.variable_buttons["line"].isEnabled() 397 | 398 | 399 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"]) 400 | def test_open_and_close_plots(ds_widget, qtbot, monkeypatch, plotmethod): 401 | """Create multiple plots and export them all""" 402 | ds_widget.plotmethod = plotmethod 403 | 404 | monkeypatch.setattr( 405 | QtWidgets.QInputDialog, "getItem", lambda *args: ("t2m", True) 406 | ) 407 | 408 | qtbot.mouseClick(ds_widget.btn_add, Qt.LeftButton) 409 | assert ds_widget.sp 410 | assert len(ds_widget.sp) == 1 411 | assert ds_widget.variable_buttons["t2m"].isChecked() 412 | 413 | monkeypatch.setattr( 414 | QtWidgets.QInputDialog, "getItem", lambda *args: ("u", True) 415 | ) 416 | 417 | # create a second plot 418 | qtbot.mouseClick(ds_widget.btn_add, Qt.LeftButton) 419 | 420 | assert ds_widget.sp 421 | assert len(ds_widget.sp) == 1 422 | assert len(ds_widget._sp) == 2 423 | assert ds_widget.combo_array.count() == 2 424 | assert ds_widget.combo_array.currentIndex() == 1 425 | assert ds_widget.variable_buttons["u"].isChecked() 426 | 427 | # switch to the first variable 428 | ds_widget.combo_array.setCurrentIndex(0) 429 | assert len(ds_widget.sp) == 1 430 | assert len(ds_widget._sp) == 2 431 | assert ds_widget.data.name == "t2m" 432 | assert ds_widget.variable_buttons["t2m"].isChecked() 433 | 434 | # close the plot 435 | qtbot.mouseClick(ds_widget.btn_del, Qt.LeftButton) 436 | assert len(ds_widget.sp) == 1 437 | assert len(ds_widget._sp) == 1 438 | assert ds_widget.data.name == "u" 439 | assert ds_widget.variable_buttons["u"].isChecked() 440 | 441 | # close the second plot 442 | qtbot.mouseClick(ds_widget.btn_del, Qt.LeftButton) 443 | assert not bool(ds_widget.sp) 444 | assert not bool(ds_widget._sp) 445 | assert not any( 446 | btn.isChecked() for name, btn in ds_widget.variable_buttons.items() 447 | ) 448 | 449 | 450 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"]) 451 | def test_multi_export(ds_widget, qtbot, monkeypatch, tmpdir, plotmethod): 452 | """Create multiple plots and export them all""" 453 | ds_widget.plotmethod = plotmethod 454 | 455 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 456 | assert ds_widget.sp 457 | assert len(ds_widget.sp) == 1 458 | 459 | monkeypatch.setattr( 460 | QtWidgets.QInputDialog, "getItem", lambda *args: ("u", True) 461 | ) 462 | 463 | # create a second plot 464 | qtbot.mouseClick(ds_widget.btn_add, Qt.LeftButton) 465 | 466 | assert ds_widget.sp 467 | assert len(ds_widget.sp) == 1 468 | assert len(ds_widget._sp) == 2 469 | assert ds_widget.combo_array.count() == 2 470 | assert ds_widget.combo_array.currentIndex() == 1 471 | 472 | # export the plots 473 | 474 | monkeypatch.setattr( 475 | QtWidgets.QFileDialog, 476 | "getSaveFileName", 477 | lambda *args: (osp.join(tmpdir, "test.pdf"), True), 478 | ) 479 | 480 | ds_widget.export_all_images() 481 | 482 | # Test if warning is triggered when exporting only one image 483 | 484 | monkeypatch.setattr( 485 | QtWidgets.QFileDialog, 486 | "getSaveFileName", 487 | lambda *args: (osp.join(tmpdir, "test.png"), True), 488 | ) 489 | 490 | question_asked = [] 491 | 492 | def dont_save(*args): 493 | question_asked.append(True) 494 | return QtWidgets.QMessageBox.No 495 | 496 | monkeypatch.setattr(QtWidgets.QMessageBox, "question", dont_save) 497 | 498 | ds_widget.export_all_images() 499 | 500 | assert question_asked == [True] 501 | 502 | assert not osp.exists(osp.join(tmpdir, "test.png")) 503 | 504 | 505 | @pytest.mark.parametrize("plotmethod", ["mapplot", "plot2d", "lineplot"]) 506 | def test_export_animation(qtbot, ds_widget, plotmethod, tmpdir, monkeypatch): 507 | """Test clicking the next time button""" 508 | from psy_view.rcsetup import rcParams 509 | 510 | ds_widget.plotmethod = plotmethod 511 | ds_widget.sl_interval.setValue(10) 512 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 513 | dim = ds_widget.combo_dims.currentText() 514 | 515 | assert dim 516 | 517 | assert not ds_widget._animating 518 | 519 | monkeypatch.setattr( 520 | QtWidgets.QFileDialog, 521 | "getSaveFileName", 522 | lambda *args: (osp.join(tmpdir, "test.gif"), True), 523 | ) 524 | 525 | with rcParams.catch(): 526 | rcParams["animations.export_kws"] = {"writer": "pillow"} 527 | 528 | ds_widget.export_animation() 529 | 530 | assert not ds_widget._animating 531 | 532 | assert osp.exists(osp.join(tmpdir, "test.gif")) 533 | 534 | 535 | @pytest.mark.skipif(sys.platform == "win32", reason="Troubles with tmp_path") 536 | def test_reload(qtbot, test_dir, tmp_path) -> None: 537 | """Test the reload button.""" 538 | import psyplot.project as psy 539 | 540 | from psy_view.ds_widget import DatasetWidget 541 | 542 | f1, f2 = "regular-test.nc", "regional-icon-test.nc" 543 | shutil.copy(str(test_dir / f1), str(tmp_path / f1)) 544 | 545 | ds_widget = DatasetWidget(psy.open_dataset(str(tmp_path / f1))) 546 | qtbot.addWidget(ds_widget) 547 | qtbot.mouseClick(ds_widget.variable_buttons["t2m"], Qt.LeftButton) 548 | 549 | assert ds_widget.ds_tree.topLevelItemCount() == 1 550 | assert ds_widget.ds is not None 551 | assert ds_widget.ds["t2m"].ndim == 4 552 | 553 | # now copy the icon file to the same destination and reload everything 554 | shutil.copy(str(test_dir / f2), str(tmp_path / f1)) 555 | ds_widget.reload() 556 | 557 | assert ds_widget.ds_tree.topLevelItemCount() == 1 558 | assert ds_widget.ds is not None 559 | assert ds_widget.ds["t2m"].ndim == 3 560 | assert len(psy.gcp(True)) == 1 561 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH 2 | ; 3 | ; SPDX-License-Identifier: CC0-1.0 4 | 5 | [tox] 6 | 7 | [testenv] 8 | extras = 9 | testsite 10 | 11 | commands = 12 | ; mypy psy_view 13 | isort --check psy_view 14 | black --line-length 79 --check psy_view 15 | blackdoc --check psy_view 16 | flake8 psy_view 17 | pytest -v --cov=psy_view -x 18 | reuse lint 19 | cffconvert --validate 20 | 21 | [pytest] 22 | DJANGO_SETTINGS_MODULE = testproject.settings 23 | python_files = tests.py test_*.py *_tests.py 24 | norecursedirs = .* build dist *.egg venv docs 25 | --------------------------------------------------------------------------------