├── .circleci └── config.yml ├── .github ├── release.yml └── workflows │ ├── ci_workflows.yml │ └── update-changelog.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── .ruff.toml ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── RELEASE.rst ├── codecov.yml ├── doc ├── Makefile ├── _static │ └── logo.png ├── api.rst ├── conf.py └── index.rst ├── glue ├── __init__.py ├── _mpl_backend.py ├── _plugin_helpers.py ├── _settings_helpers.py ├── app │ ├── __init__.py │ └── tests │ │ └── __init__.py ├── backends.py ├── config.py ├── config_gen.py ├── conftest.py ├── core │ ├── __init__.py │ ├── aggregate.py │ ├── application_base.py │ ├── autolinking.py │ ├── callback_property.py │ ├── command.py │ ├── component.py │ ├── component_id.py │ ├── component_link.py │ ├── contracts.py │ ├── coordinate_helpers.py │ ├── coordinates.py │ ├── data.py │ ├── data_collection.py │ ├── data_combo_helper.py │ ├── data_derived.py │ ├── data_exporters │ │ ├── __init__.py │ │ ├── astropy_table.py │ │ ├── gridded_fits.py │ │ ├── hdf5.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── test_astropy_table.py │ │ │ ├── test_gridded_fits.py │ │ │ └── test_hdf5.py │ ├── data_factories │ │ ├── __init__.py │ │ ├── astropy_table.py │ │ ├── dendrogram.py │ │ ├── deprecated.py │ │ ├── excel.py │ │ ├── fits.py │ │ ├── hdf5.py │ │ ├── helpers.py │ │ ├── image.py │ │ ├── numpy.py │ │ ├── pandas.py │ │ ├── tables.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── data │ │ │ ├── __init__.py │ │ │ ├── bunit.fits │ │ │ ├── casalike.fits │ │ │ ├── comment.fits │ │ │ ├── compressed_image.fits │ │ │ ├── data.hdf5 │ │ │ ├── datetime.xlsx │ │ │ ├── events.fits │ │ │ ├── generic.fits │ │ │ ├── simple_data.xlsx │ │ │ ├── ssc2006-16a1_Ti.jpg │ │ │ └── w5_subset.vot │ │ │ ├── test_data_factories.py │ │ │ ├── test_excel.py │ │ │ ├── test_fits.py │ │ │ ├── test_hdf5.py │ │ │ ├── test_image.py │ │ │ ├── test_misc.py │ │ │ ├── test_numpy.py │ │ │ └── test_pandas.py │ ├── data_region.py │ ├── decorators.py │ ├── edit_subset_mode.py │ ├── exceptions.py │ ├── fitters.py │ ├── fixed_resolution_buffer.py │ ├── glue_pickle.py │ ├── hub.py │ ├── hub_callback_container.py │ ├── joins.py │ ├── layer_artist.py │ ├── layout.py │ ├── link_helpers.py │ ├── link_manager.py │ ├── message.py │ ├── parse.py │ ├── parsers │ │ ├── __init__.py │ │ └── parsers.py │ ├── regions.py │ ├── registry.py │ ├── roi.py │ ├── roi_pretransforms.py │ ├── session.py │ ├── simpleforms.py │ ├── state.py │ ├── state_objects.py │ ├── state_path_patches.txt │ ├── subset.py │ ├── subset_group.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_aggregate.py │ │ ├── test_application_base.py │ │ ├── test_base_cartesian_data.py │ │ ├── test_command.py │ │ ├── test_component.py │ │ ├── test_component_link.py │ │ ├── test_components_changed.py │ │ ├── test_coordinate_links.py │ │ ├── test_coordinates.py │ │ ├── test_data.py │ │ ├── test_data_collection.py │ │ ├── test_data_combo_helper.py │ │ ├── test_data_derived.py │ │ ├── test_data_region.py │ │ ├── test_data_retrieval.py │ │ ├── test_data_translation.py │ │ ├── test_decorators.py │ │ ├── test_edit_subset_mode.py │ │ ├── test_fitters.py │ │ ├── test_fixed_resolution_buffer.py │ │ ├── test_hub.py │ │ ├── test_join_on_key.py │ │ ├── test_joins.py │ │ ├── test_layout.py │ │ ├── test_link_helpers.py │ │ ├── test_link_manager.py │ │ ├── test_links.py │ │ ├── test_message.py │ │ ├── test_pandas.py │ │ ├── test_parse.py │ │ ├── test_registry.py │ │ ├── test_roi.py │ │ ├── test_roi_transforms.py │ │ ├── test_simpleforms.py │ │ ├── test_state.py │ │ ├── test_state_objects.py │ │ ├── test_subset.py │ │ ├── test_subset_group.py │ │ ├── test_units.py │ │ ├── test_util.py │ │ ├── test_visual.py │ │ └── util.py │ ├── units.py │ ├── util.py │ └── visual.py ├── default_config.py ├── dialogs │ ├── README.md │ ├── __init__.py │ ├── autolinker │ │ └── __init__.py │ ├── common │ │ ├── __init__.py │ │ └── tests │ │ │ └── __init__.py │ ├── component_arithmetic │ │ └── __init__.py │ ├── component_manager │ │ └── __init__.py │ ├── data_wizard │ │ ├── __init__.py │ │ └── tests │ │ │ └── __init__.py │ ├── link_editor │ │ ├── __init__.py │ │ ├── state.py │ │ └── tests │ │ │ └── __init__.py │ └── subset_facet │ │ ├── __init__.py │ │ └── tests │ │ └── __init__.py ├── external │ ├── __init__.py │ ├── axescache.py │ ├── echo │ │ ├── __init__.py │ │ ├── callback_container.py │ │ ├── core.py │ │ ├── list.py │ │ └── selection.py │ ├── modest_image.py │ └── tests │ │ └── __init__.py ├── icons │ ├── IPythonConsole.png │ ├── __init__.py │ ├── app_icon.png │ ├── arithmetic.png │ ├── arithmetic.svg │ ├── convert.sh │ ├── glue_and.png │ ├── glue_and.svg │ ├── glue_andnot.png │ ├── glue_andnot.svg │ ├── glue_back.png │ ├── glue_back.svg │ ├── glue_box_point.png │ ├── glue_box_point.svg │ ├── glue_circle.png │ ├── glue_circle.svg │ ├── glue_circle_point.png │ ├── glue_circle_point.svg │ ├── glue_contour.png │ ├── glue_contour.svg │ ├── glue_contrast.png │ ├── glue_contrast.svg │ ├── glue_cross.png │ ├── glue_cross.svg │ ├── glue_crosshair.png │ ├── glue_crosshair.svg │ ├── glue_delete.png │ ├── glue_delete.svg │ ├── glue_down_arrow.png │ ├── glue_down_arrow.svg │ ├── glue_filesave.png │ ├── glue_filesave.svg │ ├── glue_forward.png │ ├── glue_forward.svg │ ├── glue_home.png │ ├── glue_home.svg │ ├── glue_image.png │ ├── glue_image.svg │ ├── glue_lasso.png │ ├── glue_lasso.svg │ ├── glue_link.png │ ├── glue_link.svg │ ├── glue_move.png │ ├── glue_move.svg │ ├── glue_move_x.png │ ├── glue_move_x.svg │ ├── glue_move_y.png │ ├── glue_move_y.svg │ ├── glue_not.png │ ├── glue_not.svg │ ├── glue_open.png │ ├── glue_open.svg │ ├── glue_or.png │ ├── glue_or.svg │ ├── glue_patch.png │ ├── glue_patch.svg │ ├── glue_point.png │ ├── glue_point.svg │ ├── glue_pythonsave.png │ ├── glue_pythonsave.svg │ ├── glue_rainbow.png │ ├── glue_rainbow.svg │ ├── glue_replace.png │ ├── glue_replace.svg │ ├── glue_row_select.png │ ├── glue_settings.png │ ├── glue_settings.svg │ ├── glue_slice.png │ ├── glue_slice.svg │ ├── glue_spawn.png │ ├── glue_spawn.svg │ ├── glue_spectrum.png │ ├── glue_spectrum.svg │ ├── glue_square.png │ ├── glue_square.svg │ ├── glue_star.png │ ├── glue_star.svg │ ├── glue_subset.png │ ├── glue_subset.svg │ ├── glue_tree.png │ ├── glue_tree.svg │ ├── glue_triangle_up.png │ ├── glue_triangle_up.svg │ ├── glue_unlink.png │ ├── glue_unlink.svg │ ├── glue_welcome.png │ ├── glue_xor.png │ ├── glue_xor.svg │ ├── glue_xrange_select.png │ ├── glue_xrange_select.svg │ ├── glue_yrange_select.png │ ├── glue_yrange_select.svg │ ├── glue_zoom_to_rect.png │ ├── glue_zoom_to_rect.svg │ ├── icon_preview.html │ ├── pencil.png │ ├── pencil.svg │ ├── playback_back.png │ ├── playback_first.png │ ├── playback_forw.png │ ├── playback_forw.svg │ ├── playback_last.png │ ├── playback_last.svg │ ├── playback_next.png │ ├── playback_next.svg │ ├── playback_prev.png │ ├── playback_stop.png │ ├── playback_stop.svg │ ├── tests │ │ ├── __init__.py │ │ └── test_main.py │ ├── window_tab.png │ ├── window_tab.svg │ ├── window_title.png │ ├── window_title.svg │ └── windows.png ├── io │ ├── __init__.py │ ├── formats │ │ ├── __init__.py │ │ ├── fits │ │ │ ├── __init__.py │ │ │ ├── subset_mask.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ └── test_subset_mask.py │ │ └── tests │ │ │ └── __init__.py │ ├── subset_mask.py │ └── tests │ │ ├── __init__.py │ │ └── test_subset_mask.py ├── logger.py ├── logo.png ├── main.py ├── plugins │ ├── __init__.py │ ├── coordinate_helpers │ │ ├── __init__.py │ │ ├── deprecated.py │ │ ├── link_helpers.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_link_helpers.py │ ├── data_factories │ │ └── __init__.py │ ├── dendro_viewer │ │ ├── __init__.py │ │ ├── compat.py │ │ ├── data_factory.py │ │ ├── dendro_helpers.py │ │ ├── layer_artist.py │ │ ├── layer_style_editor.py │ │ ├── state.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── data │ │ │ ├── __init__.py │ │ │ ├── dendro.fits │ │ │ ├── dendro.hdf5 │ │ │ └── dendro_old.fits │ │ │ └── test_data_factory.py │ ├── exporters │ │ └── __init__.py │ ├── tests │ │ └── __init__.py │ ├── tools │ │ ├── __init__.py │ │ ├── pv_slicer │ │ │ └── __init__.py │ │ ├── python_export.py │ │ └── tests │ │ │ └── __init__.py │ └── wcs_autolinking │ │ ├── __init__.py │ │ ├── tests │ │ ├── __init__.py │ │ └── test_wcs_autolinking.py │ │ └── wcs_autolinking.py ├── tests │ ├── __init__.py │ ├── example_data.py │ ├── helpers.py │ ├── test_config.py │ ├── test_main.py │ ├── test_settings_helpers.py │ └── visual │ │ ├── __init__.py │ │ ├── helpers.py │ │ └── py311-test-visual.json ├── utils │ ├── __init__.py │ ├── array.py │ ├── colors.py │ ├── data.py │ ├── decorators.py │ ├── error.py │ ├── geometry.py │ ├── matplotlib.py │ ├── misc.py │ ├── noconflict.py │ └── tests │ │ ├── __init__.py │ │ ├── test_array.py │ │ ├── test_decorator.py │ │ ├── test_geometry.py │ │ ├── test_matplotlib.py │ │ └── test_misc.py └── viewers │ ├── __init__.py │ ├── common │ ├── __init__.py │ ├── layer_artist.py │ ├── python_export.py │ ├── state.py │ ├── stretch_state_mixin.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_stretch_state_mixin.py │ │ ├── test_utils.py │ │ └── test_viewer.py │ ├── tool.py │ ├── utils.py │ └── viewer.py │ ├── custom │ ├── __init__.py │ ├── helper.py │ └── tests │ │ └── __init__.py │ ├── histogram │ ├── __init__.py │ ├── compat.py │ ├── layer_artist.py │ ├── python_export.py │ ├── state.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_layer_artist.py │ │ ├── test_python_export.py │ │ └── test_viewer.py │ └── viewer.py │ ├── image │ ├── __init__.py │ ├── compat.py │ ├── composite_array.py │ ├── frb_artist.py │ ├── layer_artist.py │ ├── pixel_selection_subset_state.py │ ├── python_export.py │ ├── state.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_composite_array.py │ │ ├── test_pixel_selection_subset_state.py │ │ ├── test_python_export.py │ │ ├── test_state.py │ │ └── test_viewer.py │ └── viewer.py │ ├── matplotlib │ ├── __init__.py │ ├── layer_artist.py │ ├── mouse_mode.py │ ├── mpl_axes.py │ ├── state.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_mouse_mode.py │ │ ├── test_python_export.py │ │ ├── test_state.py │ │ └── test_viewer.py │ ├── toolbar_mode.py │ └── viewer.py │ ├── profile │ ├── __init__.py │ ├── layer_artist.py │ ├── python_export.py │ ├── state.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_python_export.py │ │ ├── test_state.py │ │ └── test_viewer.py │ └── viewer.py │ ├── scatter │ ├── __init__.py │ ├── compat.py │ ├── layer_artist.py │ ├── plot_polygons.py │ ├── python_export.py │ ├── state.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_python_export.py │ │ └── test_viewer.py │ └── viewer.py │ └── table │ ├── __init__.py │ ├── compat.py │ ├── state.py │ └── tests │ └── __init__.py ├── pyproject.toml ├── setup.cfg ├── setup.py └── tox.ini /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - pre-commit-ci 5 | labels: 6 | - no-changelog-entry-needed 7 | - skip-changelog 8 | 9 | categories: 10 | - title: New Features 11 | labels: 12 | - enhancement 13 | - title: Bug Fixes 14 | labels: 15 | - bug 16 | - title: Documentation 17 | labels: 18 | - documentation 19 | - title: Other Changes 20 | labels: 21 | - "*" 22 | -------------------------------------------------------------------------------- /.github/workflows/ci_workflows.yml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | initial_checks: 13 | # Mandatory checks before CI tests 14 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 15 | with: 16 | coverage: false 17 | envs: | 18 | # Code style 19 | - linux: codestyle 20 | 21 | tests: 22 | needs: initial_checks 23 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 24 | with: 25 | coverage: codecov 26 | 27 | envs: | 28 | # Linux build 29 | - linux: py310-test-all 30 | - linux: py311-test 31 | - linux: py312-test-all 32 | 33 | # Documentation build 34 | - linux: py310-docs 35 | coverage: false 36 | - macos: py311-docs 37 | coverage: false 38 | 39 | # Test a few configurations on macOS 40 | - macos: py310-test-all 41 | - macos: py311-test 42 | - macos: py312-test-all 43 | 44 | # Test some configurations on Windows 45 | - windows: py310-test 46 | - windows: py312-test-all 47 | 48 | # Test against latest developer versions of some packages 49 | - linux: py311-test-dev-all 50 | - linux: py312-test-dev 51 | - linux: py313-test-dev-all 52 | 53 | - macos: py311-test-dev 54 | - macos: py312-test-dev-all 55 | - macos: py313-test-dev 56 | 57 | - windows: py311-test-dev-all 58 | - windows: py313-test-dev 59 | 60 | publish: 61 | needs: tests 62 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1 63 | with: 64 | test_extras: 'test' 65 | test_command: pytest --pyargs glue 66 | secrets: 67 | pypi_token: ${{ secrets.pypi_token }} 68 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yaml: -------------------------------------------------------------------------------- 1 | # This workflow takes the GitHub release notes and updates the changelog on the 2 | # main branch with the body of the release notes, thereby keeping a log in 3 | # the git repo of the changes. 4 | 5 | name: "Update Changelog" 6 | 7 | on: 8 | release: 9 | types: [released] 10 | 11 | jobs: 12 | update: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | with: 19 | ref: main 20 | 21 | - name: Update Changelog 22 | uses: stefanzweifel/changelog-updater-action@v1 23 | with: 24 | release-notes: ${{ github.event.release.body }} 25 | latest-version: ${{ github.event.release.name }} 26 | path-to-changelog: CHANGES.md 27 | 28 | - name: Commit updated Changelog 29 | uses: stefanzweifel/git-auto-commit-action@v4 30 | with: 31 | branch: main 32 | commit_message: Update CHANGELOG 33 | file_pattern: CHANGES.md 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sphinx & coverage 2 | build 3 | doc/_build 4 | doc/api 5 | glue/tests/htmlcov 6 | *.coverage 7 | *htmlcov* 8 | 9 | # Packages/installer info 10 | doc/.eggs 11 | *.egg-info 12 | dist 13 | 14 | # Compiled files 15 | *.pyc 16 | 17 | # Other generated files 18 | glue/_githash.py 19 | 20 | # Other 21 | .pylintrc 22 | *.ropeproject 23 | *.__junk* 24 | *.orig 25 | *~ 26 | .cache 27 | 28 | # Mac OSX 29 | .DS_Store 30 | 31 | # PyCharm 32 | .idea 33 | 34 | # Eclipse editor project files 35 | .project 36 | .pydevproject 37 | .settings 38 | 39 | .eggs 40 | .hypothesis 41 | .pytest_cache 42 | .tox 43 | .vscode 44 | # vscode plugin 45 | .history 46 | 47 | results 48 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | autoupdate_schedule: 'monthly' 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v5.0.0 8 | hooks: 9 | - id: check-added-large-files 10 | args: ["--enforce-all", "--maxkb=300"] 11 | exclude: "^(\ 12 | CHANGES.md|\ 13 | glue/external/echoes|\ 14 | )$" 15 | # Prevent giant files from being committed. 16 | - id: check-case-conflict 17 | # Check for files with names that would conflict on a case-insensitive 18 | # filesystem like MacOS HFS+ or Windows FAT. 19 | - id: check-json 20 | # Attempts to load all json files to verify syntax. 21 | - id: check-merge-conflict 22 | # Check for files that contain merge conflict strings. 23 | - id: check-symlinks 24 | # Checks for symlinks which do not point to anything. 25 | - id: check-toml 26 | # Attempts to load all TOML files to verify syntax. 27 | - id: check-xml 28 | # Attempts to load all xml files to verify syntax. 29 | - id: check-yaml 30 | # Attempts to load all yaml files to verify syntax. 31 | exclude: ".*(.github.*)$" 32 | - id: detect-private-key 33 | # Checks for the existence of private keys. 34 | - id: end-of-file-fixer 35 | # Makes sure files end in a newline and only a newline. 36 | exclude: ".*(glue/dialogs/README.md|data.*|extern.*|icons.*|licenses.*|_static.*|_parsetab.py)$" 37 | # - id: fix-encoding-pragma # covered by pyupgrade 38 | - id: trailing-whitespace 39 | # Trims trailing whitespace. 40 | exclude_types: [python] # Covered by Ruff W291. 41 | exclude: ".*(CHANGES.md|data.*|extern.*|licenses.*|_static.*)$" 42 | 43 | - repo: https://github.com/pre-commit/pygrep-hooks 44 | rev: v1.10.0 45 | hooks: 46 | - id: rst-directive-colons 47 | # Detect mistake of rst directive not ending with double colon. 48 | - id: rst-inline-touching-normal 49 | # Detect mistake of inline code touching normal text in rst. 50 | - id: text-unicode-replacement-char 51 | # Forbid files which have a UTF-8 Unicode replacement character. 52 | 53 | - repo: https://github.com/astral-sh/ruff-pre-commit 54 | rev: "v0.11.12" 55 | hooks: 56 | - id: ruff 57 | args: ["--fix", "--show-fixes"] 58 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3" 7 | 8 | sphinx: 9 | builder: html 10 | configuration: doc/conf.py 11 | fail_on_warning: true 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - docs 19 | - all 20 | 21 | formats: [] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Glue - multidimensional data exploration 2 | 3 | Copyright (c) 2013-2019, Glue developers 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of the Glue project nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include glue *.ui *.png *.glu *.hdf5 *.fits *.xlsx 2 | include LICENSE 3 | include README.md 4 | include glueviz.desktop 5 | include CHANGES.md 6 | include conftest.py 7 | recursive-include doc * 8 | -------------------------------------------------------------------------------- /RELEASE.rst: -------------------------------------------------------------------------------- 1 | How to release a new version of Glue 2 | ==================================== 3 | 4 | #. Follow the instructions in the `Glue documentation 5 | `_ 6 | to create a release using the `GitHub menu 7 | `_. 8 | 9 | #. Have a beverage of your choosing while you can check the build progress 10 | `here `_. 11 | (Note that the wheels may take some time to build). 12 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | target: auto 7 | # adjust accordingly based on how flaky your tests are 8 | # this allows a 0.001% drop from the previous base commit coverage 9 | # basically just to prevent counting zero changes as negative 10 | threshold: 0.001% 11 | -------------------------------------------------------------------------------- /doc/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/doc/_static/logo.png -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # -- Project information ----------------------------------------------------- 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 5 | 6 | project = "Glue" 7 | copyright = "2012-2023, Chris Beaumont, Thomas Robitaille, Michelle Borkin" 8 | author = "Chris Beaumont, Thomas Robitaille, Michelle Borkin" 9 | 10 | # -- General configuration --------------------------------------------------- 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 12 | 13 | extensions = [ 14 | "sphinx.ext.autodoc", 15 | "sphinx.ext.todo", 16 | "sphinx.ext.coverage", 17 | "sphinx.ext.mathjax", 18 | "sphinx.ext.viewcode", 19 | "sphinx.ext.intersphinx", 20 | "numpydoc", 21 | "sphinx_automodapi.automodapi", 22 | "sphinx_automodapi.smart_resolver", 23 | ] 24 | 25 | templates_path = ["_templates"] 26 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 27 | 28 | autoclass_content = "both" 29 | 30 | nitpick_ignore = [ 31 | ("py:class", "glue.viewers.histogram.layer_artist.HistogramLayerBase"), 32 | ("py:class", "glue.viewers.scatter.layer_artist.ScatterLayerBase"), 33 | ("py:class", "glue.viewers.image.layer_artist.ImageLayerBase"), 34 | ("py:class", "glue.viewers.image.layer_artist.RGBImageLayerBase"), 35 | ("py:class", "glue.viewers.image.state.BaseImageLayerState"), 36 | ("py:class", "glue.viewers.common.stretch_state_mixin.StretchStateMixin") 37 | ] 38 | 39 | viewcode_follow_imported_members = False 40 | 41 | numpydoc_show_class_members = False 42 | autosummary_generate = True 43 | automodapi_toctreedirnm = "api" 44 | 45 | linkcheck_ignore = [r"https://s3.amazonaws.com"] 46 | linkcheck_retries = 5 47 | linkcheck_timeout = 10 48 | 49 | 50 | intersphinx_mapping = { 51 | "python": ("https://docs.python.org/3.7", None), 52 | "matplotlib": ("https://matplotlib.org", None), 53 | "numpy": ("https://numpy.org/doc/stable/", None), 54 | "astropy": ("https://docs.astropy.org/en/stable/", None), 55 | "echo": ("https://echo.readthedocs.io/en/latest/", None), 56 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), 57 | "shapely": ("https://shapely.readthedocs.io/en/stable/", None), 58 | } 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 62 | 63 | html_theme = "sphinx_book_theme" 64 | html_static_path = ["_static"] 65 | html_logo = "_static/logo.png" 66 | html_theme_options = {'navigation_with_keys': False} 67 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | glue-core documentation 2 | ======================== 3 | 4 | Glue is a Python library to explore relationships within and among related datasets. 5 | 6 | This is the documentation for the glue-core frontend-independent library for the 7 | glue project, and is primary intended for advanced users and developers. For the 8 | main documentation about the desktop glue application, see http://docs.glueviz.org. 9 | 10 | 11 | API 12 | --- 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | 17 | api.rst 18 | -------------------------------------------------------------------------------- /glue/__init__.py: -------------------------------------------------------------------------------- 1 | # Set up configuration variables 2 | 3 | __all__ = ['custom_viewer', 'test'] 4 | 5 | import os 6 | 7 | import sys 8 | 9 | import importlib.metadata 10 | 11 | __version__ = importlib.metadata.version('glue-core') 12 | 13 | from ._mpl_backend import MatplotlibBackendSetter 14 | sys.meta_path.append(MatplotlibBackendSetter()) 15 | 16 | from glue.viewers.custom.helper import custom_viewer 17 | 18 | # Load user's configuration file 19 | from .config import load_configuration 20 | env = load_configuration() 21 | 22 | from .main import load_plugins, list_loaded_plugins, list_available_plugins # noqa 23 | 24 | 25 | def test(no_optional_skip=False): 26 | from pytest import main 27 | root = os.path.abspath(os.path.dirname(__file__)) 28 | args = [root, '-x'] 29 | if no_optional_skip: 30 | args.append('--no-optional-skip') 31 | return main(args=args) 32 | 33 | 34 | from glue._settings_helpers import load_settings 35 | load_settings() 36 | -------------------------------------------------------------------------------- /glue/_mpl_backend.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | 5 | class MatplotlibBackendSetter(object): 6 | """ 7 | Import hook to make sure the proper backend is set when importing 8 | Matplotlib. 9 | """ 10 | 11 | enabled = True 12 | 13 | def find_module(self, mod_name, pth=None): 14 | if self.enabled and 'matplotlib' in mod_name: 15 | self.enabled = False 16 | set_mpl_backend() 17 | 18 | def find_spec(self, name, import_path, target_module=None): 19 | if self.enabled and name.startswith('matplotlib'): 20 | self.enabled = False 21 | set_mpl_backend() 22 | 23 | 24 | def set_mpl_backend(): 25 | 26 | from matplotlib import rcParams, rcdefaults 27 | 28 | # Standardize mpl setup 29 | rcdefaults() 30 | 31 | # Set default backend to Agg. The Qt and Jupyter glue applications don't 32 | # use the default backend, so this is just to make sure that importing 33 | # matplotlib doesn't cause errors related to the MacOSX or Qt backend. 34 | rcParams['backend'] = 'Agg' 35 | 36 | # Disable key bindings in matplotlib 37 | for setting in list(rcParams.keys()): 38 | if setting.startswith('keymap'): 39 | rcParams[setting] = '' 40 | 41 | # Set the MPLBACKEND variable explicitly, because ipykernel uses the lack of 42 | # MPLBACKEND variable to indicate that it should use its own backend, and 43 | # this in turn causes some rcParams to be changed, causing test failures 44 | # etc. 45 | os.environ['MPLBACKEND'] = 'Agg' 46 | 47 | # Explicitly switch backend 48 | from matplotlib.pyplot import switch_backend 49 | switch_backend('agg') 50 | 51 | # We override the datetime64 units.registry entry to use our class. We do 52 | # this here to make sure that rcdefaults hasn't reset this. 53 | import matplotlib.units as units 54 | from glue.utils.matplotlib import Datetime64Converter 55 | units.registry[np.datetime64] = Datetime64Converter() 56 | -------------------------------------------------------------------------------- /glue/_settings_helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | import configparser 5 | from glue.logger import logger 6 | 7 | 8 | def save_settings(): 9 | 10 | from glue.config import settings, CFG_DIR 11 | 12 | if not getattr(settings, '_save_to_disk', True): 13 | return 14 | 15 | settings_cfg = os.path.join(CFG_DIR, 'settings.cfg') 16 | 17 | config = configparser.ConfigParser() 18 | config.add_section('main') 19 | 20 | for name, value, _ in sorted(settings): 21 | config.set('main', name, value=json.dumps(value, sort_keys=True)) 22 | 23 | if not os.path.exists(CFG_DIR): 24 | os.mkdir(CFG_DIR) 25 | 26 | with open(settings_cfg, 'w') as fout: 27 | config.write(fout) 28 | 29 | 30 | def load_settings(force=False): 31 | """ 32 | Load the settings from disk. 33 | 34 | By default, only settings not already defined in memory are read in, but 35 | by setting ``force=True``, all settings will be read in. 36 | """ 37 | 38 | from glue.config import settings, CFG_DIR 39 | settings_cfg = os.path.join(CFG_DIR, 'settings.cfg') 40 | 41 | logger.info("Loading settings from {0}".format(settings_cfg)) 42 | 43 | config = configparser.ConfigParser() 44 | read = config.read(settings_cfg) 45 | 46 | if len(read) == 0 or not config.has_section('main'): 47 | return 48 | 49 | for name, value in config.items('main'): 50 | name = name.upper() 51 | if name in settings: 52 | if settings.is_default(name) or force: 53 | setattr(settings, name, json.loads(value)) 54 | elif not settings.is_default(name): 55 | logger.info("Setting {0} already initialized - skipping".format(name)) 56 | else: 57 | logger.info("Unknown setting {0} - skipping".format(name)) 58 | -------------------------------------------------------------------------------- /glue/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/app/__init__.py -------------------------------------------------------------------------------- /glue/app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/app/tests/__init__.py -------------------------------------------------------------------------------- /glue/backends.py: -------------------------------------------------------------------------------- 1 | """ 2 | A common interface for accessing backend UI functionality. 3 | 4 | At the moment, the only backend is Qt 5 | """ 6 | from abc import abstractmethod 7 | _backend = None 8 | 9 | 10 | class TimerBase(object): 11 | 12 | @abstractmethod 13 | def __init__(self, interval, callback): 14 | pass 15 | 16 | @abstractmethod 17 | def stop(self): 18 | pass 19 | 20 | @abstractmethod 21 | def start(self): 22 | pass 23 | 24 | 25 | class SimpleTimer(TimerBase): 26 | 27 | def __init__(self, interval, callback): 28 | from threading import Timer 29 | self._timer = Timer(interval / 1000., callback) 30 | 31 | def start(self): 32 | self._timer.start() 33 | 34 | def stop(self): 35 | self._timer.cancel() 36 | 37 | 38 | class QtTimer(TimerBase): 39 | 40 | def __init__(self, interval, callback): 41 | from qtpy import QtCore 42 | self._timer = QtCore.QTimer() 43 | self._timer.setInterval(interval) 44 | self._timer.timeout.connect(callback) 45 | 46 | def start(self): 47 | self._timer.start() 48 | 49 | def stop(self): 50 | self._timer.stop() 51 | 52 | 53 | def get_timer(): 54 | try: 55 | from qtpy import QtCore # noqa 56 | except ImportError: 57 | return SimpleTimer 58 | else: 59 | return QtTimer 60 | 61 | 62 | def get_backend(backend='qt'): 63 | global _backend 64 | 65 | if _backend is not None: 66 | return _backend 67 | 68 | if backend != 'qt': 69 | raise ValueError("Only QT Backend supported") 70 | 71 | from glue.qt import qt_backend 72 | 73 | _backend = qt_backend 74 | return _backend 75 | -------------------------------------------------------------------------------- /glue/config_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Script used to create template config.py files for Glue 4 | """ 5 | 6 | import os 7 | import sys 8 | from shutil import copyfile 9 | 10 | import glue 11 | 12 | 13 | def get_clobber(): 14 | result = None 15 | result = input("\nDestination file exists. Overwrite? [y/n] ") 16 | while result not in ['y', 'n']: 17 | print("\tPlease choose one of [y/n]") 18 | result = input("\nDestination file exists. Overwrite? [y/n] ") 19 | 20 | return result == 'y' 21 | 22 | 23 | def main(): 24 | 25 | # Import at runtime because some tests change this value. We also don't 26 | # just import the function directly otherwise it is cached. 27 | from glue import config 28 | dest = config.CFG_DIR 29 | 30 | if not os.path.exists(dest): 31 | print("Creating directory %s" % dest) 32 | os.makedirs(dest) 33 | 34 | infile = os.path.join(glue.__path__[0], 'default_config.py') 35 | outfile = os.path.join(dest, 'config.py') 36 | 37 | print("Creating file %s" % outfile) 38 | 39 | if os.path.exists(outfile): 40 | clobber = get_clobber() 41 | if not clobber: 42 | print("Exiting") 43 | sys.exit(1) 44 | 45 | copyfile(infile, outfile) 46 | 47 | 48 | if __name__ == "__main__": 49 | main() 50 | -------------------------------------------------------------------------------- /glue/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from glue.config import CFG_DIR as CFG_DIR_ORIG 5 | 6 | STDERR_ORIGINAL = sys.stderr 7 | 8 | ON_APPVEYOR = os.environ.get('APPVEYOR', 'False') == 'True' 9 | 10 | 11 | def pytest_runtest_teardown(item, nextitem): 12 | sys.stderr = STDERR_ORIGINAL 13 | global start_dir 14 | os.chdir(start_dir) 15 | 16 | 17 | def pytest_addoption(parser): 18 | parser.addoption("--no-optional-skip", action="store_true", default=False, 19 | help="don't skip any tests with optional dependencies") 20 | 21 | 22 | start_dir = None 23 | 24 | 25 | def pytest_configure(config): 26 | 27 | global start_dir 28 | start_dir = os.path.abspath('.') 29 | 30 | os.environ['GLUE_TESTING'] = 'True' 31 | 32 | from glue._mpl_backend import set_mpl_backend 33 | set_mpl_backend() 34 | 35 | if config.getoption('no_optional_skip'): 36 | from glue.tests import helpers 37 | for attr in helpers.__dict__: 38 | if attr.startswith('requires_'): 39 | # The following line replaces the decorators with a function 40 | # that does noting, effectively disabling it. 41 | setattr(helpers, attr, lambda f: f) 42 | 43 | # Make sure we don't affect the real glue config dir 44 | import tempfile 45 | from glue import config 46 | config.CFG_DIR = tempfile.mkdtemp() 47 | 48 | # Force loading of plugins 49 | from glue.main import load_plugins 50 | load_plugins() 51 | 52 | 53 | def pytest_unconfigure(config): 54 | 55 | os.environ.pop('GLUE_TESTING') 56 | 57 | # Reset configuration directory to original one 58 | from glue import config 59 | config.CFG_DIR = CFG_DIR_ORIG 60 | -------------------------------------------------------------------------------- /glue/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .command import Command, CommandStack # noqa 2 | from .component import Component # noqa 3 | from .component_id import ComponentID # noqa 4 | from .component_link import ComponentLink # noqa 5 | from .coordinates import Coordinates # noqa # noqa 6 | from .data import BaseData, BaseCartesianData, Data # noqa 7 | from .data_collection import DataCollection # noqa 8 | from .hub import Hub, HubListener # noqa 9 | from .link_manager import LinkManager # noqa 10 | from .session import Session # noqa 11 | from .subset import Subset # noqa 12 | from .subset_group import SubsetGroup # noqa 13 | from .visual import VisualAttributes # noqa 14 | 15 | # We import this last to avoid circular imports 16 | from . import parsers # noqa 17 | from .application_base import Application # noqa 18 | -------------------------------------------------------------------------------- /glue/core/aggregate.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes to perform aggregations over cubes 3 | """ 4 | 5 | import numpy as np 6 | 7 | 8 | def mom1(data, axis=0): 9 | """ 10 | Intensity-weighted coordinate (function version). Pixel units. 11 | """ 12 | 13 | shp = list(data.shape) 14 | n = shp.pop(axis) 15 | 16 | result = np.zeros(shp) 17 | w = np.zeros(shp) 18 | 19 | # build up slice-by-slice, to avoid big temporary cubes 20 | for loc in range(n): 21 | slc = tuple(loc if j == axis else slice(None) for j in range(data.ndim)) 22 | val = data[slc] 23 | val = np.maximum(val, 0) 24 | result += val * loc 25 | w += val 26 | return result / w 27 | 28 | 29 | def mom2(data, axis=0): 30 | """ 31 | Intensity-weighted coordinate dispersion (function version). Pixel units. 32 | """ 33 | 34 | shp = list(data.shape) 35 | n = shp.pop(axis) 36 | 37 | x = np.zeros(shp) 38 | x2 = np.zeros(shp) 39 | w = np.zeros(shp) 40 | 41 | # build up slice-by-slice, to avoid big temporary cubes 42 | for loc in range(n): 43 | slc = tuple(loc if j == axis else slice(None) for j in range(data.ndim)) 44 | val = data[slc] 45 | val = np.maximum(val, 0) 46 | x += val * loc 47 | x2 += val * loc * loc 48 | w += val 49 | return np.sqrt(x2 / w - (x / w) ** 2) 50 | -------------------------------------------------------------------------------- /glue/core/autolinking.py: -------------------------------------------------------------------------------- 1 | from glue.config import autolinker 2 | 3 | __all__ = ['find_possible_links'] 4 | 5 | 6 | def expand_links(links): 7 | new_links = [] 8 | if isinstance(links, list): 9 | for link in links: 10 | new_links.extend(expand_links(link)) 11 | else: 12 | new_links.append(links) 13 | return new_links 14 | 15 | 16 | def find_possible_links(data_collection): 17 | """ 18 | Given a `~glue.core.data_collection.DataCollection` object, return a 19 | dictionary containing possible link suggestions, where the keys are the 20 | name of the auto-linking plugin, and the values are lists of links. 21 | """ 22 | 23 | suggestions = {} 24 | 25 | for label, function in autolinker: 26 | links = function(data_collection) 27 | links = expand_links(links) 28 | if len(links) > 0: 29 | suggestions[label] = links 30 | 31 | return suggestions 32 | -------------------------------------------------------------------------------- /glue/core/callback_property.py: -------------------------------------------------------------------------------- 1 | from echo import (CallbackProperty, add_callback, # noqa 2 | delay_callback, ignore_callback, # noqa 3 | remove_callback, callback_property) # noqa 4 | -------------------------------------------------------------------------------- /glue/core/data_exporters/__init__.py: -------------------------------------------------------------------------------- 1 | def setup(): 2 | from . import gridded_fits # noqa 3 | from . import astropy_table # noqa 4 | from . import hdf5 # noqa 5 | -------------------------------------------------------------------------------- /glue/core/data_exporters/astropy_table.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from glue.core import Subset 4 | from glue.config import data_exporter 5 | 6 | __all__ = [] 7 | 8 | 9 | def data_to_astropy_table(data, components=None): 10 | 11 | if isinstance(data, Subset): 12 | mask = data.to_mask() 13 | data = data.data 14 | else: 15 | mask = None 16 | 17 | from astropy.table import Table 18 | 19 | table = Table() 20 | for cid in data.main_components + data.derived_components: 21 | 22 | if components is not None and cid not in components: 23 | continue 24 | 25 | values = data[cid] 26 | 27 | if mask is not None: 28 | values = values[mask] 29 | 30 | table[cid.label] = values 31 | 32 | return table 33 | 34 | 35 | def table_exporter(fmt, label, extension): 36 | 37 | @data_exporter(label=label, extension=extension) 38 | def factory(filename, data, components=None): 39 | if os.path.exists(filename): 40 | os.remove(filename) 41 | return data_to_astropy_table(data, components=components).write(filename, format=fmt) 42 | 43 | # rename function to its variable reference below 44 | # allows pickling to work 45 | factory.__name__ = '%s_factory' % fmt.replace('.', '_') 46 | 47 | return factory 48 | 49 | 50 | csv_exporter = table_exporter('ascii.csv', 'Comma-separated table', ['csv']) 51 | ipac_exporter = table_exporter('ascii.ipac', 'IPAC Catalog', ['tbl']) 52 | latex_exporter = table_exporter('ascii.latex', 'LaTeX Table', ['tex']) 53 | votable_exporter = table_exporter('votable', 'VO Table', ['xml', 'vot']) 54 | fits_exporter = table_exporter('fits', 'FITS Table', ['fits', 'fit']) 55 | -------------------------------------------------------------------------------- /glue/core/data_exporters/hdf5.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import numpy as np 4 | 5 | from glue.core import Subset 6 | from glue.config import data_exporter 7 | 8 | 9 | __all__ = [] 10 | 11 | 12 | @data_exporter(label='HDF5', extension=['hdf5']) 13 | def hdf5_writer(filename, data, components=None): 14 | """ 15 | Write a dataset or a subset to a FITS file. 16 | 17 | Parameters 18 | ---------- 19 | data : `~glue.core.data.Data` or `~glue.core.subset.Subset` 20 | The data or subset to export 21 | components : `list` or `None` 22 | The components to export. Set this to `None` to export all components. 23 | """ 24 | 25 | if isinstance(data, Subset): 26 | mask = data.to_mask() 27 | data = data.data 28 | else: 29 | mask = None 30 | 31 | from h5py import File 32 | 33 | f = File(filename, 'w') 34 | 35 | for cid in data.main_components + data.derived_components: 36 | 37 | if components is not None and cid not in components: 38 | continue 39 | 40 | if data.get_kind(cid) == 'categorical': 41 | values = data[cid] 42 | if values.dtype.kind == 'U': 43 | values = np.char.encode(values, encoding='ascii', errors='replace') 44 | else: 45 | values = values.copy() 46 | else: 47 | values = data[cid].copy() 48 | 49 | if mask is not None: 50 | if values.ndim == 1: 51 | values = values[mask] 52 | else: 53 | if values.dtype.kind == 'f': 54 | values[~mask] = np.nan 55 | elif values.dtype.kind == 'i': 56 | values[~mask] = 0 57 | elif values.dtype.kind == 'S': 58 | values[~mask] = '' 59 | else: 60 | warnings.warn("Unknown data type in HDF5 export: {0}".format(values.dtype)) 61 | continue 62 | 63 | f.create_dataset(cid.label, data=values) 64 | 65 | f.close() 66 | -------------------------------------------------------------------------------- /glue/core/data_exporters/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_exporters/tests/__init__.py -------------------------------------------------------------------------------- /glue/core/data_exporters/tests/test_astropy_table.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import numpy as np 4 | from astropy.table import Table 5 | 6 | from glue.core import Data 7 | from ..astropy_table import (ipac_exporter, latex_exporter, 8 | votable_exporter, fits_exporter) 9 | 10 | EXPORTERS = {} 11 | EXPORTERS['ascii.ipac'] = ipac_exporter 12 | EXPORTERS['ascii.latex'] = latex_exporter 13 | EXPORTERS['votable'] = votable_exporter 14 | EXPORTERS['fits'] = fits_exporter 15 | 16 | 17 | @pytest.mark.parametrize('fmt', EXPORTERS) 18 | def test_astropy_table(tmpdir, fmt): 19 | 20 | filename = tmpdir.join('test1').strpath 21 | data = Data(x=[1, 2, 3], y=[b'a', b'b', b'c']) 22 | EXPORTERS[fmt](filename, data) 23 | t = Table.read(filename, format=fmt) 24 | assert t.colnames == ['x', 'y'] 25 | np.testing.assert_equal(t['x'], [1, 2, 3]) 26 | np.testing.assert_equal(t['y'].astype(bytes), [b'a', b'b', b'c']) 27 | 28 | filename = tmpdir.join('test2').strpath 29 | EXPORTERS[fmt](filename, data, components=[data.id['x']]) 30 | t = Table.read(filename, format=fmt) 31 | assert t.colnames == ['x'] 32 | np.testing.assert_equal(t['x'], [1, 2, 3]) 33 | -------------------------------------------------------------------------------- /glue/core/data_exporters/tests/test_hdf5.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | from glue.core import Data 5 | from glue.tests.helpers import requires_h5py 6 | 7 | from ..hdf5 import hdf5_writer 8 | 9 | DTYPES = [np.int16, np.int32, np.int64, np.float32, np.float64] 10 | 11 | 12 | @requires_h5py 13 | @pytest.mark.parametrize('dtype', DTYPES) 14 | def test_hdf5_writer_data(tmpdir, dtype): 15 | 16 | filename = tmpdir.join('test1.hdf5').strpath 17 | 18 | data = Data(x=np.arange(6).reshape(2, 3).astype(dtype), 19 | y=(np.arange(6) * 2).reshape(2, 3).astype(dtype)) 20 | 21 | hdf5_writer(filename, data) 22 | 23 | from h5py import File 24 | 25 | f = File(filename) 26 | assert len(f) == 2 27 | np.testing.assert_equal(f['x'][()], data['x']) 28 | np.testing.assert_equal(f['y'][()], data['y']) 29 | assert f['x'][()].dtype == dtype 30 | assert f['y'][()].dtype == dtype 31 | f.close() 32 | 33 | # Only write out some components 34 | 35 | filename = tmpdir.join('test2.hdf5').strpath 36 | 37 | hdf5_writer(filename, data, components=[data.id['x']]) 38 | 39 | f = File(filename) 40 | assert len(f) == 1 41 | np.testing.assert_equal(f['x'][()], data['x']) 42 | f.close() 43 | 44 | 45 | @requires_h5py 46 | @pytest.mark.parametrize('dtype', DTYPES) 47 | def test_hdf5_writer_subset(tmpdir, dtype): 48 | 49 | filename = tmpdir.join('test').strpath 50 | 51 | data = Data(x=np.arange(6).reshape(2, 3).astype(dtype), 52 | y=(np.arange(6) * 2).reshape(2, 3).astype(dtype)) 53 | 54 | subset = data.new_subset() 55 | subset.subset_state = data.id['x'] > 2 56 | 57 | hdf5_writer(filename, subset) 58 | 59 | from h5py import File 60 | 61 | f = File(filename) 62 | 63 | if np.dtype(dtype).kind == 'f': 64 | assert np.all(np.isnan(f['x'][0])) 65 | assert np.all(np.isnan(f['y'][0])) 66 | else: 67 | np.testing.assert_equal(f['x'][0], 0) 68 | np.testing.assert_equal(f['y'][0], 0) 69 | 70 | np.testing.assert_equal(f['x'][1], data['x'][1]) 71 | np.testing.assert_equal(f['y'][1], data['y'][1]) 72 | assert f['x'][()].dtype == dtype 73 | assert f['y'][()].dtype == dtype 74 | f.close() 75 | -------------------------------------------------------------------------------- /glue/core/data_factories/__init__.py: -------------------------------------------------------------------------------- 1 | from .astropy_table import * # noqa 2 | from .dendrogram import * # noqa 3 | from .excel import * # noqa 4 | from .fits import * # noqa 5 | from .hdf5 import * # noqa 6 | from .helpers import * # noqa 7 | from .image import * # noqa 8 | from .numpy import * # noqa 9 | from .pandas import * # noqa 10 | from .tables import * # noqa 11 | -------------------------------------------------------------------------------- /glue/core/data_factories/dendrogram.py: -------------------------------------------------------------------------------- 1 | from glue.core.data_factories.helpers import has_extension # noqa 2 | 3 | 4 | __all__ = [] 5 | 6 | try: 7 | from glue.core.data_factories.dendro_loader import load_dendro, is_dendro # noqa 8 | except ImportError: 9 | pass 10 | -------------------------------------------------------------------------------- /glue/core/data_factories/excel.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from glue.core.data_factories.helpers import has_extension 4 | from glue.core.data_factories.pandas import panda_process 5 | from glue.config import data_factory 6 | 7 | 8 | __all__ = [] 9 | 10 | 11 | @data_factory(label="Excel", identifier=has_extension('xls xlsx')) 12 | def panda_read_excel(path, sheet=None, **kwargs): 13 | """ 14 | A factory for reading excel data using pandas. 15 | 16 | Parameters 17 | ---------- 18 | path : str 19 | Path to the file. 20 | 21 | sheet : str, optional 22 | The sheet to read. If `None`, all sheets are read. 23 | 24 | **kwargs 25 | All other kwargs are passed to :func:`pandas.read_excel`. 26 | """ 27 | 28 | try: 29 | import pandas as pd 30 | except ImportError: 31 | raise ImportError('Pandas is required for Excel input.') 32 | 33 | name = os.path.basename(path) 34 | if '.xls' in name: 35 | name = name.rsplit('.xls', 1)[0] 36 | 37 | if sheet is None: 38 | 39 | if path.endswith('xlsx'): 40 | 41 | try: 42 | from openpyxl import load_workbook 43 | except ImportError: 44 | raise ImportError('openpyxl is required for xlsx input.') 45 | 46 | xl_workbook = load_workbook(filename=path) 47 | 48 | sheet_names = xl_workbook.sheetnames 49 | 50 | else: 51 | 52 | try: 53 | import xlrd 54 | except ImportError: 55 | raise ImportError('xlrd is required for xls input.') 56 | 57 | xl_workbook = xlrd.open_workbook(path) 58 | 59 | sheet_names = xl_workbook.sheet_names() 60 | 61 | else: 62 | 63 | sheet_names = [sheet] 64 | 65 | all_data = [] 66 | for sheet in sheet_names: 67 | indf = pd.read_excel(path, sheet, **kwargs) 68 | data = panda_process(indf) 69 | data.label = "{0}:{1}".format(name, sheet) 70 | all_data.append(data) 71 | 72 | return all_data 73 | -------------------------------------------------------------------------------- /glue/core/data_factories/image.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from glue.core.coordinates import coordinates_from_wcs 4 | from glue.core.data_factories.helpers import has_extension 5 | from glue.core.data import Data 6 | from glue.config import data_factory 7 | 8 | 9 | IMG_FMT = ['jpg', 'jpeg', 'bmp', 'png', 'tiff', 'tif'] 10 | 11 | __all__ = ['img_data'] 12 | 13 | 14 | def img_loader(file_name): 15 | """Load an image to a numpy array, using either PIL or skimage 16 | 17 | Parameters 18 | ---------- 19 | file_name : str 20 | Path of the file to load. 21 | 22 | Returns 23 | ------- 24 | :class:`~numpy.ndarray` 25 | """ 26 | try: 27 | from skimage import img_as_ubyte 28 | from skimage.io import imread 29 | return np.asarray(img_as_ubyte(imread(file_name))) 30 | except ImportError: 31 | pass 32 | 33 | try: 34 | from PIL import Image 35 | return np.asarray(Image.open(file_name)) 36 | except ImportError: 37 | raise ImportError("Reading %s requires PIL or scikit-image" % 38 | file_name) 39 | 40 | 41 | @data_factory(label='Image', identifier=has_extension(' '.join(IMG_FMT))) 42 | def img_data(file_name): 43 | """Load common image files into a Glue data object""" 44 | result = Data() 45 | 46 | data = img_loader(file_name) 47 | data = np.flipud(data) 48 | shp = data.shape 49 | 50 | comps = [] 51 | labels = [] 52 | 53 | # split 3 color images into each color plane 54 | if len(shp) == 3 and shp[2] in [3, 4]: 55 | comps.extend([data[:, :, 0], data[:, :, 1], data[:, :, 2]]) 56 | labels.extend(['red', 'green', 'blue']) 57 | if shp[2] == 4: 58 | comps.append(data[:, :, 3]) 59 | labels.append('alpha') 60 | else: 61 | comps = [data] 62 | labels = ['PRIMARY'] 63 | 64 | # look for AVM coordinate metadata 65 | try: 66 | from pyavm import AVM 67 | avm = AVM.from_image(str(file_name)) # avoid unicode 68 | wcs = avm.to_wcs() 69 | except Exception: 70 | pass 71 | else: 72 | result.coords = coordinates_from_wcs(wcs) 73 | 74 | for c, l in zip(comps, labels): 75 | result.add_component(c, l) 76 | 77 | return result 78 | -------------------------------------------------------------------------------- /glue/core/data_factories/numpy.py: -------------------------------------------------------------------------------- 1 | from glue.core.data import Data, Component 2 | from glue.config import data_factory 3 | from glue.core.data_factories.helpers import has_extension 4 | 5 | __all__ = ['is_npy_npz', 'npy_npz_reader'] 6 | 7 | 8 | def is_npy_npz(filename): 9 | """ 10 | The first bytes are x93NUMPY (for npy) or PKx03x04 (for npz) 11 | """ 12 | # See: https://github.com/numpy/numpy/blob/master/doc/neps/npy-format.rst 13 | from numpy.lib.format import MAGIC_PREFIX 14 | MAGIC_PREFIX_NPZ = b'PK\x03\x04' # first 4 bytes for a zipfile 15 | tester = has_extension('npz .npz') 16 | with open(filename, 'rb') as infile: 17 | prefix = infile.read(6) 18 | return prefix == MAGIC_PREFIX or (tester(filename) and prefix[:4] == MAGIC_PREFIX_NPZ) 19 | 20 | 21 | @data_factory(label="Numpy save file", identifier=is_npy_npz, priority=100) 22 | def npy_npz_reader(filename, format='auto', auto_merge=False, **kwargs): 23 | """ 24 | Read in a Numpy structured array saved to a .npy or .npz file. 25 | 26 | Parameters 27 | ---------- 28 | source: str 29 | The pathname to the Numpy save file. 30 | """ 31 | 32 | import numpy as np 33 | data = np.load(filename) 34 | 35 | if isinstance(data, np.ndarray): 36 | data = {None: data} 37 | 38 | groups = [] 39 | for groupname in sorted(data): 40 | 41 | d = Data(label=groupname) 42 | arr = data[groupname] 43 | 44 | if arr.dtype.names is None: 45 | comp = Component.autotyped(arr) 46 | d.add_component(comp, label='array') 47 | else: 48 | for name in arr.dtype.names: 49 | comp = Component.autotyped(arr[name]) 50 | d.add_component(comp, label=name) 51 | 52 | groups.append(d) 53 | 54 | return groups 55 | -------------------------------------------------------------------------------- /glue/core/data_factories/tables.py: -------------------------------------------------------------------------------- 1 | from glue.core.data_factories.helpers import has_extension 2 | from glue.config import data_factory 3 | 4 | 5 | __all__ = ['tabular_data'] 6 | 7 | 8 | @data_factory(label="ASCII Table", 9 | identifier=has_extension('csv txt tsv tbl dat ' 10 | 'csv.gz txt.gz tbl.bz ' 11 | 'dat.gz'), 12 | priority=1) 13 | def tabular_data(path, **kwargs): 14 | """ 15 | A factory for reading ASCII table data using 16 | :func:`glue.core.data_factories.astropy_tabular_data` or 17 | :func:`pandas.read_table`, tried in sequence. 18 | 19 | Parameters 20 | ---------- 21 | path : str 22 | Path to the file. 23 | 24 | **kwargs 25 | All other kwargs are passed to the reader backend. 26 | """ 27 | 28 | from glue.core.data_factories.astropy_table import astropy_tabular_data 29 | from glue.core.data_factories.pandas import pandas_read_table 30 | for fac in [astropy_tabular_data, pandas_read_table]: 31 | try: 32 | return fac(path, **kwargs) 33 | except Exception: 34 | pass 35 | else: 36 | raise IOError("Could not parse file: %s" % path) 37 | -------------------------------------------------------------------------------- /glue/core/data_factories/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/__init__.py -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/__init__.py -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/bunit.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/bunit.fits -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/compressed_image.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/compressed_image.fits -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/data.hdf5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/data.hdf5 -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/datetime.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/datetime.xlsx -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/events.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/events.fits -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/generic.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/generic.fits -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/simple_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/simple_data.xlsx -------------------------------------------------------------------------------- /glue/core/data_factories/tests/data/ssc2006-16a1_Ti.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/data_factories/tests/data/ssc2006-16a1_Ti.jpg -------------------------------------------------------------------------------- /glue/core/data_factories/tests/test_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from numpy.testing import assert_array_equal 4 | 5 | from glue.core.coordinates import WCSCoordinates 6 | from glue.core import data_factories as df 7 | from glue.tests.helpers import requires_pyavm, requires_pil_or_skimage, make_file 8 | 9 | DATA = os.path.join(os.path.dirname(__file__), 'data') 10 | 11 | 12 | @requires_pil_or_skimage 13 | def test_grey_png_loader(): 14 | # Greyscale PNG 15 | data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x00\x00\x00\x00W\xddR\xf8\x00\x00\x00\x0eIDATx\x9ccdddab\x04\x00\x00&\x00\x0b\x8e`\xe7A\x00\x00\x00\x00IEND\xaeB`\x82' 16 | with make_file(data, '.png') as fname: 17 | d = df.load_data(fname) 18 | assert df.find_factory(fname) is df.img_data 19 | assert_array_equal(d['PRIMARY'], [[3, 4], [1, 2]]) 20 | 21 | 22 | @requires_pil_or_skimage 23 | def test_color_png_loader(): 24 | # Colorscale PNG 25 | data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08\x02\x00\x00\x00\xfd\xd4\x9as\x00\x00\x00\x15IDAT\x08\xd7\x05\xc1\x01\x01\x00\x00\x00\x80\x10\xffO\x17B\x14\x1a!\xec\x04\xfc\xf2!Q\\\x00\x00\x00\x00IEND\xaeB`\x82' 26 | with make_file(data, '.png') as fname: 27 | d = df.load_data(fname) 28 | assert df.find_factory(fname) is df.img_data 29 | assert_array_equal(d['red'], [[255, 0], [255, 0]]) 30 | assert_array_equal(d['green'], [[255, 0], [0, 255]]) 31 | assert_array_equal(d['blue'], [[0, 255], [0, 0]]) 32 | 33 | 34 | @requires_pyavm 35 | @requires_pil_or_skimage 36 | def test_avm(): 37 | data = df.load_data(os.path.join(DATA, 'ssc2006-16a1_Ti.jpg')) 38 | assert isinstance(data.coords, WCSCoordinates) 39 | -------------------------------------------------------------------------------- /glue/core/data_factories/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from glue.core import data_factories as df 4 | from glue.tests.helpers import requires_astropy 5 | 6 | 7 | DATA = os.path.join(os.path.dirname(__file__), 'data') 8 | 9 | 10 | @requires_astropy 11 | def test_load_vot(): 12 | # This checks that we can load a VO table which incidentally is a subset of 13 | # the one included in the tutorial. 14 | d_set = df.load_data(os.path.join(DATA, 'w5_subset.vot')) 15 | assert len(d_set.components) == 15 16 | -------------------------------------------------------------------------------- /glue/core/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | __all__ = ['memoize', 'singleton', 'memoize_attr_check'] 5 | 6 | 7 | def _make_key(args, kwargs): 8 | return args, frozenset(kwargs.items()) 9 | 10 | 11 | def memoize(func): 12 | """Save results of function calls to avoid repeated calculation""" 13 | memo = {} 14 | 15 | @wraps(func) 16 | def wrapper(*args, **kwargs): 17 | 18 | # Note that here we have two separate try...except statements, because 19 | # we want to make sure that we catch only TypeError on the first 20 | # statement, and both TypeError and KeyError on the second. 21 | 22 | try: 23 | key = _make_key(args, kwargs) 24 | except TypeError: # unhashable input 25 | return func(*args, **kwargs) 26 | 27 | try: 28 | return memo[key] 29 | except KeyError: 30 | result = func(*args, **kwargs) 31 | memo[key] = result 32 | return result 33 | except TypeError: # unhashable input 34 | return func(*args, **kwargs) 35 | 36 | wrapper.__memoize_cache = memo 37 | return wrapper 38 | 39 | 40 | def clear_cache(func): 41 | """ 42 | Clear the cache of a function that has potentially been 43 | decorated by memoize. Safely ignores non-decorated functions 44 | """ 45 | try: 46 | func.__memoize_cache.clear() 47 | except AttributeError: 48 | pass 49 | 50 | 51 | def memoize_attr_check(attr): 52 | """ Memoize a method call, cached both on arguments and given attribute 53 | of first argument (which is presumably self) 54 | 55 | Has the effect of re-calculating results if a specific attribute changes 56 | """ 57 | 58 | def decorator(func): 59 | # must return a decorator function 60 | 61 | @wraps(func) 62 | def result(*args, **kwargs): 63 | first_arg = getattr(args[0], attr) 64 | return memo(first_arg, *args, **kwargs) 65 | 66 | @memoize 67 | def memo(*args, **kwargs): 68 | return func(*args[1:], **kwargs) 69 | 70 | return result 71 | 72 | return decorator 73 | 74 | 75 | def singleton(cls): 76 | """Turn a class into a singleton, such that new objects 77 | in this class share the same instance""" 78 | instances = {} 79 | 80 | @wraps(cls) 81 | def getinstance(): 82 | if cls not in instances: 83 | instances[cls] = cls() 84 | return instances[cls] 85 | return getinstance 86 | -------------------------------------------------------------------------------- /glue/core/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class IncompatibleAttribute(Exception): 3 | pass 4 | 5 | 6 | class IncompatibleDataException(Exception): 7 | pass 8 | 9 | 10 | class UndefinedROI(Exception): 11 | pass 12 | 13 | 14 | class InvalidSubscriber(Exception): 15 | pass 16 | 17 | 18 | class InvalidMessage(Exception): 19 | pass 20 | -------------------------------------------------------------------------------- /glue/core/glue_pickle.py: -------------------------------------------------------------------------------- 1 | from glue.logger import logger 2 | 3 | try: 4 | from dill import dumps, loads # noqa 5 | except ImportError: 6 | logger.info("Dill library not installed. Falling back to cPickle") 7 | from pickle import dumps, loads # noqa 8 | -------------------------------------------------------------------------------- /glue/core/layout.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides some routines for performing layout 3 | calculations to organize rectangular windows in a larger canvas 4 | """ 5 | 6 | from collections import Counter 7 | 8 | 9 | class Rectangle(object): 10 | 11 | def __init__(self, x, y, w, h): 12 | """ A rectangle (obviously). 13 | 14 | :param x: Left edge 15 | :param y: Bottom edge 16 | :param w: Width 17 | :param h: Height 18 | """ 19 | self.x = x 20 | self.y = y 21 | self.w = w 22 | self.h = h 23 | 24 | def __eq__(self, other): 25 | return (self.x == other.x and 26 | self.y == other.y and 27 | self.w == other.w and 28 | self.h == other.h) 29 | 30 | # If __eq__ is defined, then __hash__ has to be re-defined 31 | __hash__ = object.__hash__ 32 | 33 | def __str__(self): 34 | return repr(self) 35 | 36 | def __repr__(self): 37 | return "Rectangle(%f, %f, %f, %f)" % (self.x, self.y, self.w, self.h) 38 | 39 | def snap(self, xstep, ystep=None, padding=0.0): 40 | """ 41 | Snap the rectangle onto a grid, with optional padding. 42 | 43 | :param xstep: The number of intervals to split the x=[0, 1] range into. 44 | :param ystep: The number of intervals to split the y=[0, 1] range into. 45 | :param padding: Uniform padding to add around the result. This shrinks 46 | the result so that the edges + padding line up with the 47 | grid. 48 | 49 | :returns: A new Rectangle, obtained by snapping self onto the grid, 50 | and applying padding 51 | """ 52 | 53 | if ystep is None: 54 | ystep = xstep 55 | 56 | return Rectangle(round(self.x * xstep) / xstep + padding, 57 | round(self.y * ystep) / ystep + padding, 58 | round(self.w * xstep) / xstep - 2 * padding, 59 | round(self.h * ystep) / ystep - 2 * padding) 60 | 61 | 62 | def _snap_size(rectangles): 63 | x = Counter([round(1 / r.w) for r in rectangles]) 64 | y = Counter([round(1 / r.h) for r in rectangles]) 65 | return x.most_common()[0][0], y.most_common()[0][0] 66 | 67 | 68 | def snap_to_grid(rectangles, padding=0.0): 69 | """ 70 | Snap a collection of rectangles onto a grid, in a sensible fashion 71 | 72 | :param rectangles: List of Rectangle instances 73 | :returns: A dictionary mapping each input rectangle to a snapped position 74 | """ 75 | result = {} 76 | xs, ys = _snap_size(rectangles) 77 | for r in rectangles: 78 | result[r] = r.snap(xs, ys, padding=padding) 79 | return result 80 | -------------------------------------------------------------------------------- /glue/core/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .parsers import parse_data, parse_links # noqa 2 | -------------------------------------------------------------------------------- /glue/core/regions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions to support data that defines regions 3 | """ 4 | import numpy as np 5 | 6 | from glue.core.roi import PolygonalROI 7 | from glue.core.data_region import RegionData 8 | 9 | from glue.config import layer_action 10 | from glue.core.subset import RoiSubsetState, MultiOrState 11 | 12 | 13 | def reg_to_roi(reg): 14 | if reg.geom_type == "Polygon": 15 | ext_coords = np.array(reg.exterior.coords.xy) 16 | roi = PolygonalROI(vx=ext_coords[0], vy=ext_coords[1]) # Need to account for interior rings 17 | return roi 18 | 19 | 20 | @layer_action(label='Subset of regions -> Subset over region extent', single=True, subset=True) 21 | def layer_to_subset(layer, data_collection): 22 | """ 23 | This should be limited to the case where subset.Data is RegionData 24 | and/or return a warning when applied to some other kind of data. 25 | """ 26 | if isinstance(layer.data, RegionData): 27 | 28 | extended_comp = layer.data._extended_component_ids[0] 29 | regions = layer[extended_comp] 30 | list_of_rois = [reg_to_roi(region) for region in regions] 31 | 32 | roisubstates = [RoiSubsetState(layer.data.ext_x, 33 | layer.data.ext_y, 34 | roi=roi 35 | ) 36 | for roi in list_of_rois] 37 | if len(list_of_rois) > 1: 38 | composite_substate = MultiOrState(roisubstates) 39 | else: 40 | composite_substate = roisubstates[0] 41 | _ = data_collection.new_subset_group(subset_state=composite_substate) 42 | -------------------------------------------------------------------------------- /glue/core/session.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | from glue.core.command import CommandStack 4 | from glue.core.data_collection import DataCollection 5 | from glue.core.edit_subset_mode import EditSubsetMode 6 | 7 | __all__ = ['Session'] 8 | 9 | 10 | class Session(object): 11 | 12 | def __init__(self, application=None, data_collection=None, 13 | command_stack=None, hub=None): 14 | 15 | self.application = application 16 | self.data_collection = data_collection or DataCollection() 17 | self.hub = self.data_collection.hub 18 | self.command_stack = command_stack or CommandStack() 19 | self.command_stack.session = self 20 | 21 | self.edit_subset_mode = EditSubsetMode() 22 | self.edit_subset_mode.data_collection = self.data_collection 23 | 24 | @property 25 | def application(self): 26 | if self._application is None: 27 | return None 28 | else: 29 | return self._application() 30 | 31 | @application.setter 32 | def application(self, value): 33 | if value is None: 34 | self._application = None 35 | else: 36 | self._application = weakref.ref(value) 37 | -------------------------------------------------------------------------------- /glue/core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/tests/__init__.py -------------------------------------------------------------------------------- /glue/core/tests/test_aggregate.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/core/tests/test_aggregate.py -------------------------------------------------------------------------------- /glue/core/tests/test_components_changed.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test that data.update_components() sends a NumericalDataChangedMessage 3 | that conveys which components have been changed. 4 | """ 5 | from glue.core.data import Data 6 | from glue.core.hub import HubListener 7 | from glue.core.data_collection import DataCollection 8 | from glue.core.message import NumericalDataChangedMessage 9 | 10 | import numpy as np 11 | from numpy.testing import assert_array_equal 12 | 13 | 14 | def test_message_carries_components(): 15 | 16 | test_data = Data(x=np.array([1, 2, 3, 4, 5]), y=np.array([1, 2, 3, 4, 5]), label='test_data') 17 | data_collection = DataCollection([test_data]) 18 | 19 | class CustomListener(HubListener): 20 | 21 | def __init__(self, hub): 22 | self.received = 0 23 | self.components_changed = None 24 | hub.subscribe(self, NumericalDataChangedMessage, 25 | handler=self.receive_message) 26 | 27 | def receive_message(self, message): 28 | self.received += 1 29 | try: 30 | self.components_changed = message.components_changed 31 | except AttributeError: 32 | self.components_changed = None 33 | 34 | listener = CustomListener(data_collection.hub) 35 | assert listener.received == 0 36 | assert listener.components_changed is None 37 | 38 | cid_to_change = test_data.id['x'] 39 | new_data = [5, 2, 6, 7, 10] 40 | test_data.update_components({cid_to_change: new_data}) 41 | 42 | assert listener.received == 1 43 | assert cid_to_change in listener.components_changed 44 | 45 | assert_array_equal(test_data['x'], new_data) 46 | -------------------------------------------------------------------------------- /glue/core/tests/test_data_retrieval.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=I0011,W0613,W0201,W0212,E1101,E1103 2 | 3 | import numpy as np 4 | 5 | from ..data import Data, Component 6 | 7 | 8 | class TestDataRetrieval(object): 9 | 10 | def setup_method(self, method): 11 | 12 | data1 = Data() 13 | comp1 = Component(np.arange(5)) 14 | id1 = data1.add_component(comp1, 'comp_1') 15 | comp2 = Component(np.arange(5) * 2) 16 | id2 = data1.add_component(comp2, 'comp_2') 17 | 18 | data2 = Data() 19 | comp3 = Component(np.arange(5) * 3) 20 | id3 = data2.add_component(comp3, 'comp_3') 21 | comp4 = Component(np.arange(5) * 4) 22 | id4 = data2.add_component(comp4, 'comp_4') 23 | 24 | self.data = [data1, data2] 25 | self.components = [comp1, comp2, comp3, comp4] 26 | self.component_ids = [id1, id2, id3, id4] 27 | 28 | def test_direct_get(self): 29 | assert self.data[0][self.component_ids[0]] is self.components[0].data 30 | assert self.data[0][self.component_ids[1]] is self.components[1].data 31 | assert self.data[1][self.component_ids[2]] is self.components[2].data 32 | assert self.data[1][self.component_ids[3]] is self.components[3].data 33 | -------------------------------------------------------------------------------- /glue/core/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=I0011,W0613,W0201,W0212,E1101,E1103, R0903 2 | 3 | from ..decorators import singleton, memoize, memoize_attr_check 4 | 5 | 6 | @singleton 7 | class SingletonOne(object): 8 | """test docstring""" 9 | pass 10 | 11 | 12 | @singleton 13 | class SingletonTwo(object): 14 | pass 15 | 16 | 17 | class MemoAtt(object): 18 | def __init__(self): 19 | self.target = 1 20 | self.trigger = 0 21 | 22 | @memoize_attr_check('trigger') 23 | def test(self): 24 | return self.target 25 | 26 | @memoize_attr_check('trigger') 27 | def test_kwarg(self, x=0): 28 | return self.target + x 29 | 30 | 31 | def test_singleton(): 32 | f = SingletonOne() 33 | g = SingletonOne() 34 | h = SingletonTwo() 35 | k = SingletonTwo() 36 | assert f is g 37 | assert h is k 38 | assert f is not h 39 | 40 | 41 | def test_memoize(): 42 | class Bar(object): 43 | pass 44 | 45 | @memoize 46 | def func(x): 47 | return x.att 48 | 49 | b = Bar() 50 | b.att = 5 51 | 52 | assert func(b) == 5 53 | b.att = 7 54 | assert func(b) == 5 # should return memoized func 55 | 56 | 57 | def test_memoize_unhashable(): 58 | 59 | @memoize 60 | def func(x, view=None): 61 | return 2 * x 62 | 63 | assert func(1, view=slice(1, 2, 3)) == 2 64 | assert func(1, view=slice(1, 2, 3)) == 2 65 | 66 | 67 | def test_memoize_attribute(): 68 | f = MemoAtt() 69 | assert f.test() == 1 70 | f.target = 2 71 | assert f.test() == 1 72 | f.trigger = 1 73 | assert f.test() == 2 74 | 75 | 76 | def test_decorators_maintain_docstrings(): 77 | assert SingletonOne.__doc__ == "test docstring" 78 | 79 | @memoize 80 | def test(): 81 | """test docstring""" 82 | 83 | assert test.__doc__ == "test docstring" 84 | 85 | class MemoClass(object): 86 | @memoize_attr_check('test') 87 | def test(self): 88 | """123""" 89 | pass 90 | 91 | assert MemoClass.test.__doc__ == "123" 92 | 93 | 94 | def test_memoize_kwargs(): 95 | 96 | @memoize 97 | def memoadd(x, y=0): 98 | return x + y 99 | 100 | assert memoadd(3) == 3 101 | assert memoadd(3, 2) == 5 102 | assert memoadd(3, y=3) == 6 103 | 104 | 105 | def test_memoize_attribute_kwargs(): 106 | 107 | f = MemoAtt() 108 | assert f.test_kwarg() == 1 109 | assert f.test_kwarg(x=5) == 6 110 | f.target = 2 111 | assert f.test_kwarg() == 1 112 | f.trigger = 1 113 | assert f.test_kwarg() == 2 114 | assert f.test_kwarg(x=6) == 8 115 | -------------------------------------------------------------------------------- /glue/core/tests/test_layout.py: -------------------------------------------------------------------------------- 1 | from ..layout import Rectangle, snap_to_grid 2 | 3 | 4 | class TestSnap(object): 5 | 6 | @staticmethod 7 | def check(input, expected, **kwargs): 8 | result = snap_to_grid(input, **kwargs) 9 | for i, e in zip(input, expected): 10 | assert result[i] == e 11 | 12 | def test_2x2(self): 13 | 14 | rs = [Rectangle(-.2, -.1, .45, .52), 15 | Rectangle(.52, -.23, .49, .49), 16 | Rectangle(0, .45, .51, .53), 17 | Rectangle(.50, .45, .51, .53)] 18 | 19 | ex = [Rectangle(0, 0, .5, .5), 20 | Rectangle(.5, 0, .5, .5), 21 | Rectangle(0, .5, .5, .5), 22 | Rectangle(.5, .5, .5, .5)] 23 | 24 | self.check(rs, ex) 25 | 26 | def test_1x2(self): 27 | 28 | rs = [Rectangle(-.2, -.2, .95, .48), 29 | Rectangle(0, .45, .51, .53), 30 | Rectangle(.50, .45, .51, .53)] 31 | 32 | ex = [Rectangle(0, 0, 1, .5), 33 | Rectangle(0, .5, .5, .5), 34 | Rectangle(.5, .5, .5, .5)] 35 | 36 | self.check(rs, ex) 37 | 38 | def test_1x3(self): 39 | 40 | rs = [Rectangle(-.02, -.2, .95, .48), 41 | Rectangle(0.1, .51, 0.32, .53), 42 | Rectangle(0.32, .49, .30, .53), 43 | Rectangle(0.7, .52, .40, .53)] 44 | 45 | ex = [Rectangle(0, 0, 1, .5), 46 | Rectangle(0, .5, 1 / 3., .5), 47 | Rectangle(1 / 3., .5, 1 / 3., .5), 48 | Rectangle(2 / 3., .5, 1 / 3., .5)] 49 | 50 | self.check(rs, ex) 51 | 52 | def test_padding_1x2(self): 53 | 54 | rs = [Rectangle(0, 0, 1, .5), 55 | Rectangle(0, .5, .5, .5), 56 | Rectangle(.5, .5, .5, .5)] 57 | ex = [Rectangle(.1, .1, .8, .3), 58 | Rectangle(.1, .6, .3, .3), 59 | Rectangle(.6, .6, .3, .3)] 60 | 61 | self.check(rs, ex, padding=0.1) 62 | -------------------------------------------------------------------------------- /glue/core/tests/test_links.py: -------------------------------------------------------------------------------- 1 | """This file contains tests concerning linking data and accessing 2 | linked components""" 3 | 4 | import numpy as np 5 | from numpy.random import random as r 6 | 7 | from glue.core.coordinates import IdentityCoordinates 8 | 9 | from .. import Data, DataCollection 10 | from ..link_helpers import LinkSame 11 | 12 | 13 | def test_1d_world_link(): 14 | x, y = r(10), r(10) 15 | d1 = Data(label='d1', x=x) 16 | d2 = Data(label='d2', y=y, coords=IdentityCoordinates(n_dim=1)) 17 | dc = DataCollection([d1, d2]) 18 | 19 | dc.add_link(LinkSame(d2.world_component_ids[0], d1.id['x'])) 20 | 21 | assert d2.world_component_ids[0] in d1.externally_derivable_components 22 | 23 | np.testing.assert_array_equal(d1[d2.world_component_ids[0]], x) 24 | np.testing.assert_array_equal(d1[d2.pixel_component_ids[0]], x) 25 | 26 | 27 | def test_3d_world_link(): 28 | """Should be able to grab pixel coords after linking world""" 29 | x, y, z = r(10), r(10), r(10) 30 | cat = Data(label='cat', x=x, y=y, z=z) 31 | im = Data(label='im', inten=r((3, 3, 3)), coords=IdentityCoordinates(n_dim=3)) 32 | 33 | dc = DataCollection([cat, im]) 34 | 35 | dc.add_link(LinkSame(im.world_component_ids[2], cat.id['x'])) 36 | dc.add_link(LinkSame(im.world_component_ids[1], cat.id['y'])) 37 | dc.add_link(LinkSame(im.world_component_ids[0], cat.id['z'])) 38 | 39 | np.testing.assert_array_equal(cat[im.pixel_component_ids[2]], x) 40 | np.testing.assert_array_equal(cat[im.pixel_component_ids[1]], y) 41 | np.testing.assert_array_equal(cat[im.pixel_component_ids[0]], z) 42 | 43 | 44 | def test_2d_world_link(): 45 | """Should be able to grab pixel coords after linking world""" 46 | 47 | x, y = r(10), r(10) 48 | cat = Data(label='cat', x=x, y=y) 49 | im = Data(label='im', inten=r((3, 3)), coords=IdentityCoordinates(n_dim=2)) 50 | 51 | dc = DataCollection([cat, im]) 52 | 53 | dc.add_link(LinkSame(im.world_component_ids[0], cat.id['x'])) 54 | dc.add_link(LinkSame(im.world_component_ids[1], cat.id['y'])) 55 | 56 | np.testing.assert_array_equal(cat[im.pixel_component_ids[0]], x) 57 | np.testing.assert_array_equal(cat[im.pixel_component_ids[1]], y) 58 | -------------------------------------------------------------------------------- /glue/core/tests/test_message.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .. import message as msg 4 | 5 | 6 | def test_invalid_subset_msg(): 7 | with pytest.raises(TypeError) as exc: 8 | msg.SubsetMessage(None) 9 | assert exc.value.args[0].startswith('Sender must be a subset') 10 | 11 | 12 | def test_invalid_data_msg(): 13 | with pytest.raises(TypeError) as exc: 14 | msg.DataMessage(None) 15 | assert exc.value.args[0].startswith('Sender must be a data') 16 | 17 | 18 | def test_invalid_data_collection_msg(): 19 | with pytest.raises(TypeError) as exc: 20 | msg.DataCollectionMessage(None) 21 | assert exc.value.args[0].startswith('Sender must be a DataCollection') 22 | -------------------------------------------------------------------------------- /glue/core/tests/test_pandas.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from unittest.mock import MagicMock 4 | from pandas.testing import (assert_series_equal, 5 | assert_frame_equal) 6 | 7 | from ..component import Component, DerivedComponent, CategoricalComponent 8 | from ..data import Data 9 | 10 | 11 | class TestPandasConversion(object): 12 | 13 | def test_Component_conversion(self): 14 | 15 | comp = Component(np.arange(5)) 16 | series = pd.Series(np.arange(5)) 17 | 18 | assert_series_equal(series, comp.to_series()) 19 | 20 | def test_DerivedComponent_conversion(self): 21 | 22 | data = MagicMock() 23 | link = MagicMock() 24 | link.compute.return_value = np.arange(5) 25 | comp = DerivedComponent(data, link) 26 | 27 | series = pd.Series(np.arange(5)) 28 | assert_series_equal(series, comp.to_series()) 29 | 30 | def test_CategoricalComponent_conversion(self): 31 | 32 | comp = CategoricalComponent(np.array(['a', 'b', 'c', 'd'])) 33 | series = pd.Series(['a', 'b', 'c', 'd']) 34 | 35 | assert_series_equal(series, comp.to_series()) 36 | 37 | def test_CoordinateComponent_conversion(self): 38 | 39 | d = Data(x=[1, 2, 3]) 40 | series = pd.Series(np.array([0, 1, 2])) 41 | comp = d.get_component(d.pixel_component_ids[0]) 42 | assert_series_equal(series, comp.to_series()) 43 | 44 | def test_Data_conversion(self): 45 | 46 | d = Data(n=np.array([4, 5, 6, 7])) 47 | cat_comp = CategoricalComponent(np.array(['a', 'b', 'c', 'd'])) 48 | d.add_component(cat_comp, 'c') 49 | link = MagicMock() 50 | link.compute.return_value = np.arange(4) 51 | deriv_comp = DerivedComponent(d, link) 52 | d.add_component(deriv_comp, 'd') 53 | order = [comp.label for comp in d.components] 54 | 55 | frame = pd.DataFrame() 56 | frame['Pixel Axis 0 [x]'] = np.ogrid[0:4] 57 | frame['n'] = np.array([4, 5, 6, 7]) 58 | frame['c'] = ['a', 'b', 'c', 'd'] 59 | frame['d'] = np.arange(4) 60 | out_frame = d.to_dataframe() 61 | 62 | assert_frame_equal(out_frame, frame) 63 | assert list(out_frame.columns) == order 64 | 65 | def test_multi_dimensional(self): 66 | 67 | a = np.array([[2, 3], [5, 4], [6, 7]]) 68 | comp = Component(a) 69 | series = pd.Series(a.ravel()) 70 | 71 | assert_series_equal(series, comp.to_series()) 72 | -------------------------------------------------------------------------------- /glue/core/tests/test_registry.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=I0011,W0613,W0201,W0212,E1101,E1103 2 | 3 | from ..registry import Registry 4 | 5 | 6 | def setup_function(function): 7 | Registry().clear() 8 | 9 | 10 | def test_singleton(): 11 | assert Registry() is Registry() 12 | 13 | 14 | def test_unique(): 15 | r = Registry() 16 | assert r.register(3, "test") == "test" 17 | assert r.register(4, "test2") == "test2" 18 | 19 | 20 | def test_disambiguate(): 21 | r = Registry() 22 | assert r.register(3, "test") == "test" 23 | assert r.register(4, "test") == "test_01" 24 | 25 | 26 | def test_rename(): 27 | r = Registry() 28 | assert r.register(3, "test") == "test" 29 | assert r.register(4, "test2") == "test2" 30 | assert r.register(3, "test") == "test" 31 | 32 | 33 | def test_rename_then_new(): 34 | r = Registry() 35 | assert r.register(3, "test") == "test" 36 | assert r.register(3, "test2") == "test2" 37 | assert r.register(4, "test") == "test" 38 | 39 | 40 | def test_cross_class(): 41 | r = Registry() 42 | assert r.register(3, "test") == "test" 43 | assert r.register(3.5, "test") == "test" 44 | assert r.register(4.5, "test") == "test_01" 45 | 46 | 47 | def test_group_override(): 48 | r = Registry() 49 | assert r.register(3, "test") == "test" 50 | assert r.register(3.5, "test", group=int) == "test_01" 51 | assert r.register(4, "test", group=float) == "test" 52 | 53 | 54 | def test_unregister(): 55 | r = Registry() 56 | assert r.register(3, "test") == "test" 57 | r.unregister(3) 58 | assert r.register(4, "test") == "test" 59 | 60 | 61 | def test_relabel_to_self(): 62 | r = Registry() 63 | assert r.register(3, "test") == "test" 64 | assert r.register(3, "test") == "test" 65 | 66 | 67 | def test_lowest_disambiguation(): 68 | r = Registry() 69 | assert r.register(3, "test") == "test" 70 | assert r.register(4, "test") == "test_01" 71 | assert r.register(4, "test") == "test_01" 72 | -------------------------------------------------------------------------------- /glue/core/tests/test_simpleforms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ..simpleforms import IntOption, FloatOption, BoolOption 4 | 5 | 6 | class Stub(object): 7 | int_opt = IntOption(min=0, max=10, default=3) 8 | float_opt = FloatOption(min=1, max=2, default=1.5) 9 | bool_opt = BoolOption() 10 | 11 | 12 | class TestSimpleForms(object): 13 | 14 | def test_get_set_int(self): 15 | assert Stub.int_opt.min == 0 16 | assert Stub.int_opt.max == 10 17 | assert Stub().int_opt == 3 18 | 19 | def test_get_set_bool(self): 20 | s = Stub() 21 | assert s.bool_opt is False 22 | s.bool_opt = True 23 | assert s.bool_opt 24 | 25 | def test_get_set_float(self): 26 | 27 | s = Stub() 28 | assert s.float_opt == 1.5 29 | 30 | s.float_opt = 1 31 | assert s.float_opt == 1.0 32 | assert isinstance(s.float_opt, float) 33 | 34 | def test_invalid_int(self): 35 | 36 | s = Stub() 37 | s.int_opt = 4 38 | 39 | with pytest.raises(ValueError): 40 | s.int_opt = -1 41 | 42 | with pytest.raises(ValueError): 43 | s.int_opt = 11 44 | 45 | with pytest.raises(ValueError): 46 | s.int_opt = 2.5 47 | 48 | def test_invalid_float(self): 49 | s = Stub() 50 | 51 | with pytest.raises(ValueError): 52 | s.float_opt = -0.1 53 | 54 | with pytest.raises(ValueError): 55 | s.float_opt = 10.1 56 | 57 | def test_invalid(self): 58 | s = Stub() 59 | 60 | with pytest.raises(ValueError): 61 | s.bool_opt = 3 62 | -------------------------------------------------------------------------------- /glue/core/tests/test_visual.py: -------------------------------------------------------------------------------- 1 | from glue.core.visual import VisualAttributes 2 | from glue.utils.matplotlib import MATPLOTLIB_GE_36 3 | 4 | if MATPLOTLIB_GE_36: 5 | from matplotlib import colormaps 6 | else: 7 | from matplotlib.cm import get_cmap 8 | 9 | import pytest 10 | 11 | 12 | def test_VA_preferred_cmap(): 13 | # Not a real CMAP array - errors 14 | with pytest.raises(TypeError, match="`preferred_cmap` must be a string or an instance of " 15 | "a matplotlib.colors.Colormap"): 16 | VisualAttributes(preferred_cmap=1) 17 | 18 | # Not a valid string / known key [mpl 3.6+] for a CMAP - errors 19 | with pytest.raises(ValueError, match="not_a_cmap is not a valid colormap name."): 20 | VisualAttributes(preferred_cmap="not_a_cmap") 21 | 22 | viridis_cmap = colormaps["viridis"] if MATPLOTLIB_GE_36 else get_cmap("viridis") 23 | 24 | # get_cmap cmap name 25 | va = VisualAttributes(preferred_cmap="viridis") 26 | assert va.preferred_cmap == viridis_cmap 27 | # formal cmap name 28 | va = VisualAttributes(preferred_cmap="Viridis") 29 | assert va.preferred_cmap == viridis_cmap 30 | 31 | # Valid Colormap 32 | va = VisualAttributes(preferred_cmap=viridis_cmap) 33 | assert va.preferred_cmap == viridis_cmap 34 | 35 | # None is allowed - it is the default 36 | va = VisualAttributes(preferred_cmap=None) 37 | assert va.preferred_cmap is None 38 | -------------------------------------------------------------------------------- /glue/core/tests/util.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | from unittest.mock import MagicMock 4 | 5 | from glue import core 6 | from glue.core.application_base import Application 7 | from glue.tests.helpers import make_file 8 | 9 | 10 | @contextmanager 11 | def simple_catalog(): 12 | """Context manager to create a temporary data file 13 | 14 | :param suffix: File suffix. string 15 | """ 16 | with make_file(b'#a, b\n1, 2\n3, 4', '.csv') as result: 17 | yield result 18 | 19 | 20 | def simple_session(): 21 | collect = core.data_collection.DataCollection() 22 | hub = core.hub.Hub() 23 | result = core.Session(data_collection=collect, hub=hub, 24 | application=MagicMock(Application), 25 | command_stack=core.CommandStack()) 26 | result.command_stack.session = result 27 | return result 28 | -------------------------------------------------------------------------------- /glue/default_config.py: -------------------------------------------------------------------------------- 1 | 2 | """Declare any extra link functions like this""" 3 | # @link_function(info='translates A to B', output_labels=['b']) 4 | # def a_to_b(a): 5 | # return a * 3 6 | 7 | 8 | """Data factories take a filename as input and return a Data object""" 9 | # @data_factory('JPEG Image') 10 | # def jpeg_reader(file_name): 11 | # ... 12 | # return data 13 | -------------------------------------------------------------------------------- /glue/dialogs/README.md: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | These are GUI dialogs that could be re-used in other glue applications. These 5 | will often be dialogs that allow the user to change the underlying glue state, 6 | for example creating links, editing the data, creating new components. 7 | 8 | Each dialog should be included in a directory, and should contain a 9 | sub-directory for each specific GUI framework (e.g. ``qt/``). -------------------------------------------------------------------------------- /glue/dialogs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/autolinker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/autolinker/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/common/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/common/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/common/tests/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/component_arithmetic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/component_arithmetic/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/component_manager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/component_manager/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/data_wizard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/data_wizard/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/data_wizard/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/data_wizard/tests/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/link_editor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/link_editor/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/link_editor/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/link_editor/tests/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/subset_facet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/subset_facet/__init__.py -------------------------------------------------------------------------------- /glue/dialogs/subset_facet/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/dialogs/subset_facet/tests/__init__.py -------------------------------------------------------------------------------- /glue/external/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Modules in this directory smooth over importing functionality that may be 3 | present in different libraries, depending on the users' system. 4 | """ 5 | -------------------------------------------------------------------------------- /glue/external/echo/__init__.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from echo import * 3 | warnings.warn('glue.external.echo is deprecated, import from echo directly instead') 4 | -------------------------------------------------------------------------------- /glue/external/echo/callback_container.py: -------------------------------------------------------------------------------- 1 | from echo.callback_container import * 2 | -------------------------------------------------------------------------------- /glue/external/echo/core.py: -------------------------------------------------------------------------------- 1 | from echo.core import * 2 | -------------------------------------------------------------------------------- /glue/external/echo/list.py: -------------------------------------------------------------------------------- 1 | from echo.containers import * 2 | -------------------------------------------------------------------------------- /glue/external/echo/selection.py: -------------------------------------------------------------------------------- 1 | from echo.selection import * 2 | -------------------------------------------------------------------------------- /glue/external/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/external/tests/__init__.py -------------------------------------------------------------------------------- /glue/icons/IPythonConsole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/IPythonConsole.png -------------------------------------------------------------------------------- /glue/icons/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 9): 4 | import importlib.resources as importlib_resources 5 | else: 6 | import importlib_resources 7 | 8 | 9 | __all__ = ['icon_path'] 10 | 11 | 12 | def icon_path(icon_name, icon_format='png'): 13 | """ 14 | Return the absolute path to an icon 15 | 16 | Parameters 17 | ---------- 18 | icon_name : str 19 | Name of icon, without extension or directory prefix 20 | icon_format : str, optional 21 | Can be either 'png' or 'svg' 22 | 23 | Returns 24 | ------- 25 | path : str 26 | Full path to icon 27 | """ 28 | 29 | icon_name += '.{0}'.format(icon_format) 30 | 31 | icon_file = importlib_resources.files("glue") / "icons" / icon_name 32 | # when running on pyinstaller, the path might include .. 33 | # so we need to convert it to an absolute path 34 | icon_file = icon_file.resolve() 35 | if icon_file.is_file(): 36 | return str(icon_file) 37 | else: 38 | raise RuntimeError("Icon does not exist: %s" % icon_name) 39 | -------------------------------------------------------------------------------- /glue/icons/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/app_icon.png -------------------------------------------------------------------------------- /glue/icons/arithmetic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/arithmetic.png -------------------------------------------------------------------------------- /glue/icons/convert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for input in *.svg 4 | do 5 | output=${input/svg/png} 6 | inkscape --without-gui --export-png=$PWD/$output --export-width=125 --export-height=125 $PWD/$input 7 | done 8 | 9 | convert -flop playback_forw.png playback_back.png 10 | convert -flop playback_next.png playback_prev.png 11 | convert -flop playback_last.png playback_first.png 12 | -------------------------------------------------------------------------------- /glue/icons/glue_and.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_and.png -------------------------------------------------------------------------------- /glue/icons/glue_andnot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_andnot.png -------------------------------------------------------------------------------- /glue/icons/glue_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_back.png -------------------------------------------------------------------------------- /glue/icons/glue_box_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_box_point.png -------------------------------------------------------------------------------- /glue/icons/glue_box_point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_circle.png -------------------------------------------------------------------------------- /glue/icons/glue_circle_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_circle_point.png -------------------------------------------------------------------------------- /glue/icons/glue_circle_point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_contour.png -------------------------------------------------------------------------------- /glue/icons/glue_contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_contrast.png -------------------------------------------------------------------------------- /glue/icons/glue_contrast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_cross.png -------------------------------------------------------------------------------- /glue/icons/glue_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_crosshair.png -------------------------------------------------------------------------------- /glue/icons/glue_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_delete.png -------------------------------------------------------------------------------- /glue/icons/glue_delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_down_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_down_arrow.png -------------------------------------------------------------------------------- /glue/icons/glue_filesave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_filesave.png -------------------------------------------------------------------------------- /glue/icons/glue_forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_forward.png -------------------------------------------------------------------------------- /glue/icons/glue_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_home.png -------------------------------------------------------------------------------- /glue/icons/glue_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_image.png -------------------------------------------------------------------------------- /glue/icons/glue_lasso.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_lasso.png -------------------------------------------------------------------------------- /glue/icons/glue_link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_link.png -------------------------------------------------------------------------------- /glue/icons/glue_move.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_move.png -------------------------------------------------------------------------------- /glue/icons/glue_move_x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_move_x.png -------------------------------------------------------------------------------- /glue/icons/glue_move_y.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_move_y.png -------------------------------------------------------------------------------- /glue/icons/glue_not.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_not.png -------------------------------------------------------------------------------- /glue/icons/glue_not.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_open.png -------------------------------------------------------------------------------- /glue/icons/glue_or.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_or.png -------------------------------------------------------------------------------- /glue/icons/glue_patch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_patch.png -------------------------------------------------------------------------------- /glue/icons/glue_patch.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_point.png -------------------------------------------------------------------------------- /glue/icons/glue_pythonsave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_pythonsave.png -------------------------------------------------------------------------------- /glue/icons/glue_rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_rainbow.png -------------------------------------------------------------------------------- /glue/icons/glue_replace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_replace.png -------------------------------------------------------------------------------- /glue/icons/glue_row_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_row_select.png -------------------------------------------------------------------------------- /glue/icons/glue_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_settings.png -------------------------------------------------------------------------------- /glue/icons/glue_settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /glue/icons/glue_slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_slice.png -------------------------------------------------------------------------------- /glue/icons/glue_spawn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_spawn.png -------------------------------------------------------------------------------- /glue/icons/glue_spectrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_spectrum.png -------------------------------------------------------------------------------- /glue/icons/glue_spectrum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_square.png -------------------------------------------------------------------------------- /glue/icons/glue_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_star.png -------------------------------------------------------------------------------- /glue/icons/glue_star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_subset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_subset.png -------------------------------------------------------------------------------- /glue/icons/glue_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_tree.png -------------------------------------------------------------------------------- /glue/icons/glue_tree.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_triangle_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_triangle_up.png -------------------------------------------------------------------------------- /glue/icons/glue_triangle_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /glue/icons/glue_unlink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_unlink.png -------------------------------------------------------------------------------- /glue/icons/glue_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_welcome.png -------------------------------------------------------------------------------- /glue/icons/glue_xor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_xor.png -------------------------------------------------------------------------------- /glue/icons/glue_xrange_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_xrange_select.png -------------------------------------------------------------------------------- /glue/icons/glue_yrange_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_yrange_select.png -------------------------------------------------------------------------------- /glue/icons/glue_zoom_to_rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/glue_zoom_to_rect.png -------------------------------------------------------------------------------- /glue/icons/pencil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/pencil.png -------------------------------------------------------------------------------- /glue/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /glue/icons/playback_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_back.png -------------------------------------------------------------------------------- /glue/icons/playback_first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_first.png -------------------------------------------------------------------------------- /glue/icons/playback_forw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_forw.png -------------------------------------------------------------------------------- /glue/icons/playback_forw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 31 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /glue/icons/playback_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_last.png -------------------------------------------------------------------------------- /glue/icons/playback_last.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 32 | 37 | 45 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /glue/icons/playback_next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_next.png -------------------------------------------------------------------------------- /glue/icons/playback_next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 31 | 36 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /glue/icons/playback_prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_prev.png -------------------------------------------------------------------------------- /glue/icons/playback_stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/playback_stop.png -------------------------------------------------------------------------------- /glue/icons/playback_stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 31 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /glue/icons/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/tests/__init__.py -------------------------------------------------------------------------------- /glue/icons/tests/test_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .. import icon_path 3 | 4 | 5 | def test_icon_path(): 6 | 7 | path = icon_path('glue_replace') 8 | assert os.path.exists(path) 9 | 10 | path = icon_path('glue_replace', icon_format='png') 11 | assert os.path.exists(path) 12 | 13 | path = icon_path('glue_replace', icon_format='svg') 14 | assert os.path.exists(path) 15 | -------------------------------------------------------------------------------- /glue/icons/window_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/window_tab.png -------------------------------------------------------------------------------- /glue/icons/window_tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /glue/icons/window_title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/window_title.png -------------------------------------------------------------------------------- /glue/icons/window_title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /glue/icons/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/icons/windows.png -------------------------------------------------------------------------------- /glue/io/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/io/__init__.py -------------------------------------------------------------------------------- /glue/io/formats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/io/formats/__init__.py -------------------------------------------------------------------------------- /glue/io/formats/fits/__init__.py: -------------------------------------------------------------------------------- 1 | # Data factories are already defined in glue.core.data_factories, but they will 2 | # be moved here in future. 3 | 4 | 5 | def setup(): 6 | from . import subset_mask # noqa 7 | -------------------------------------------------------------------------------- /glue/io/formats/fits/subset_mask.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import OrderedDict 3 | 4 | import numpy as np 5 | from astropy.io import fits 6 | from glue.config import subset_mask_importer, subset_mask_exporter 7 | from glue.core.data_factories.fits import is_fits 8 | 9 | 10 | @subset_mask_importer(label='FITS', extension=['fits', 'fit', 11 | 'fits.gz', 'fit.gz']) 12 | def fits_subset_mask_importer(filename): 13 | 14 | if not is_fits(filename): 15 | raise IOError("File {0} is not a valid FITS file".format(filename)) 16 | 17 | masks = OrderedDict() 18 | 19 | label = os.path.basename(filename).rpartition('.')[0] 20 | 21 | with fits.open(filename) as hdulist: 22 | 23 | for ihdu, hdu in enumerate(hdulist): 24 | if hdu.data is not None and hdu.data.dtype.kind == 'i': 25 | if not hdu.name: 26 | name = '{0}[{1}]'.format(label, ihdu) 27 | elif ihdu == 0: 28 | name = label 29 | else: 30 | name = hdu.name 31 | masks[name] = hdu.data > 0 32 | 33 | if len(masks) == 0: 34 | raise ValueError('No HDUs with integer values (which would normally indicate a mask) were found in file') 35 | 36 | return masks 37 | 38 | 39 | @subset_mask_exporter(label='FITS', extension=['fits', 'fit', 40 | 'fits.gz', 'fit.gz']) 41 | def fits_subset_mask_exporter(filename, masks): 42 | 43 | hdulist = fits.HDUList() 44 | hdulist.append(fits.PrimaryHDU()) 45 | 46 | # We store the subset masks in the extensions to make sure we can give 47 | # then a name. 48 | for label, mask in masks.items(): 49 | hdulist.append(fits.ImageHDU(np.asarray(mask, int), name=label)) 50 | 51 | hdulist.writeto(filename, overwrite=True) 52 | -------------------------------------------------------------------------------- /glue/io/formats/fits/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/io/formats/fits/tests/__init__.py -------------------------------------------------------------------------------- /glue/io/formats/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/io/formats/tests/__init__.py -------------------------------------------------------------------------------- /glue/io/subset_mask.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from glue.core import Subset 4 | from glue.core.subset import MaskSubsetState 5 | 6 | __all__ = ['SubsetMaskImporter', 'SubsetMaskExporter'] 7 | 8 | 9 | class SubsetMaskImporter(object): 10 | 11 | def get_filename_and_reader(self): 12 | raise NotImplementedError 13 | 14 | def run(self, data_or_subset, data_collection): 15 | 16 | filename, reader = self.get_filename_and_reader() 17 | 18 | if filename is None: 19 | return 20 | 21 | # Read in the masks 22 | masks = reader(filename) 23 | 24 | # Make sure shape is unique 25 | shapes = set(mask.shape for mask in masks.values()) 26 | 27 | if len(shapes) == 0: 28 | raise ValueError("No subset masks were returned") 29 | 30 | elif len(shapes) > 1: 31 | raise ValueError("Not all subsets have the same shape") 32 | 33 | if list(shapes)[0] != data_or_subset.shape: 34 | raise ValueError("Mask shape {0} does not match data shape {1}".format(list(shapes)[0], data_or_subset.shape)) 35 | 36 | if isinstance(data_or_subset, Subset): 37 | 38 | subset = data_or_subset 39 | 40 | if len(masks) != 1: 41 | raise ValueError("Can only read in a single subset when importing into a subset") 42 | 43 | mask = list(masks.values())[0] 44 | 45 | subset_state = MaskSubsetState(mask, subset.pixel_component_ids) 46 | subset.subset_state = subset_state 47 | 48 | else: 49 | 50 | data = data_or_subset 51 | 52 | for label, mask in masks.items(): 53 | 54 | subset_state = MaskSubsetState(mask, data.pixel_component_ids) 55 | data_collection.new_subset_group(label=label, subset_state=subset_state) 56 | 57 | 58 | class SubsetMaskExporter(object): 59 | 60 | def get_filename_and_writer(self): 61 | raise NotImplementedError 62 | 63 | def run(self, data_or_subset): 64 | 65 | filename, writer = self.get_filename_and_writer() 66 | 67 | if filename is None: 68 | return 69 | 70 | # Prepare dictionary of masks 71 | masks = OrderedDict() 72 | 73 | if isinstance(data_or_subset, Subset): 74 | 75 | subset = data_or_subset 76 | masks[subset.label] = subset.to_mask() 77 | 78 | else: 79 | 80 | data = data_or_subset 81 | 82 | if len(data.subsets) == 0: 83 | raise ValueError("Data has no subsets") 84 | 85 | for subset in data.subsets: 86 | masks[subset.label] = subset.to_mask() 87 | 88 | writer(filename, masks) 89 | -------------------------------------------------------------------------------- /glue/io/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/io/tests/__init__.py -------------------------------------------------------------------------------- /glue/logger.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger, StreamHandler 2 | logger = getLogger("glue") 3 | logger.addHandler(StreamHandler()) 4 | -------------------------------------------------------------------------------- /glue/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/logo.png -------------------------------------------------------------------------------- /glue/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | def load_plugin(plugin): 3 | """ 4 | Load plugin referred to by name 'plugin' 5 | """ 6 | import importlib 7 | module = importlib.import_module(plugin) 8 | if hasattr(module, 'setup'): 9 | module.setup() 10 | else: 11 | raise AttributeError("Plugin {0} should define 'setup' function".format(plugin)) 12 | -------------------------------------------------------------------------------- /glue/plugins/coordinate_helpers/__init__.py: -------------------------------------------------------------------------------- 1 | def setup(): 2 | from . import link_helpers # noqa 3 | -------------------------------------------------------------------------------- /glue/plugins/coordinate_helpers/deprecated.py: -------------------------------------------------------------------------------- 1 | from astropy import units as u 2 | from astropy.coordinates import FK5, Galactic 3 | 4 | 5 | def fk52gal(ra, dec): 6 | c = FK5(ra * u.deg, dec * u.deg) 7 | out = c.transform_to(Galactic()) 8 | return out.l.degree, out.b.degree 9 | 10 | 11 | def gal2fk5(l, b): 12 | c = Galactic(l * u.deg, b * u.deg) 13 | out = c.transform_to(FK5()) 14 | return out.ra.degree, out.dec.degree 15 | 16 | 17 | def radec2glon(ra, dec): 18 | """ 19 | Compute galactic longitude from right ascension and declination. 20 | """ 21 | return fk52gal(ra, dec)[0] 22 | 23 | 24 | def radec2glat(ra, dec): 25 | """ 26 | Compute galactic latitude from right ascension and declination. 27 | """ 28 | return fk52gal(ra, dec)[1] 29 | 30 | 31 | def lb2ra(lon, lat): 32 | """ 33 | Compute right ascension from galactic longitude and latitude. 34 | """ 35 | return gal2fk5(lon, lat)[0] 36 | 37 | 38 | def lb2dec(lon, lat): 39 | """ 40 | Compute declination from galactic longitude and latitude. 41 | """ 42 | return gal2fk5(lon, lat)[1] 43 | -------------------------------------------------------------------------------- /glue/plugins/coordinate_helpers/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/coordinate_helpers/tests/__init__.py -------------------------------------------------------------------------------- /glue/plugins/data_factories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/data_factories/__init__.py -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | def setup(): 2 | from .data_factory import load_dendro # noqa 3 | -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/compat.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from .state import DendrogramLayerState 4 | 5 | STATE_CLASS = {} 6 | STATE_CLASS['DendroLayerArtist'] = DendrogramLayerState 7 | 8 | 9 | def update_dendrogram_viewer_state(rec, context): 10 | """ 11 | Given viewer session information, make sure the session information is 12 | compatible with the current version of the viewers, and if not, update 13 | the session information in-place. 14 | """ 15 | 16 | if '_protocol' not in rec: 17 | 18 | # Note that files saved with protocol < 1 have bin settings saved per 19 | # layer but they were always restricted to be the same, so we can just 20 | # use the settings from the first layer 21 | 22 | rec['state'] = {} 23 | rec['state']['values'] = {} 24 | 25 | # TODO: could generalize this into a mapping 26 | properties = rec.pop('properties') 27 | viewer_state = rec['state']['values'] 28 | viewer_state['parent_att'] = properties['parent'] 29 | viewer_state['height_att'] = properties['height'] 30 | viewer_state['order_att'] = properties['order'] 31 | viewer_state['y_log'] = properties['ylog'] 32 | 33 | layer_states = [] 34 | 35 | for layer in rec['layers']: 36 | state_id = str(uuid.uuid4()) 37 | state_cls = STATE_CLASS[layer['_type'].split('.')[-1]] 38 | state = state_cls(layer=context.object(layer.pop('layer'))) 39 | for prop in ('visible', 'zorder'): 40 | value = layer.pop(prop) 41 | value = context.object(value) 42 | setattr(state, prop, value) 43 | context.register_object(state_id, state) 44 | layer['state'] = state_id 45 | layer_states.append(state) 46 | 47 | list_id = str(uuid.uuid4()) 48 | context.register_object(list_id, layer_states) 49 | rec['state']['values']['layers'] = list_id 50 | -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/layer_style_editor.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from qtpy import QtWidgets 4 | 5 | from echo.qt import autoconnect_callbacks_to_qt 6 | from glue.utils.qt import load_ui 7 | 8 | 9 | class DendorgramLayerStyleEditor(QtWidgets.QWidget): 10 | 11 | def __init__(self, layer, parent=None): 12 | 13 | super(DendorgramLayerStyleEditor, self).__init__(parent=parent) 14 | 15 | self.ui = load_ui('layer_style_editor.ui', self, 16 | directory=os.path.dirname(__file__)) 17 | 18 | connect_kwargs = {'alpha': dict(value_range=(0, 1))} 19 | 20 | self._connections = autoconnect_callbacks_to_qt(layer.state, self.ui, connect_kwargs) 21 | -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/dendro_viewer/tests/__init__.py -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/tests/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/dendro_viewer/tests/data/__init__.py -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/tests/data/dendro.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/dendro_viewer/tests/data/dendro.fits -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/tests/data/dendro.hdf5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/dendro_viewer/tests/data/dendro.hdf5 -------------------------------------------------------------------------------- /glue/plugins/dendro_viewer/tests/data/dendro_old.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/dendro_viewer/tests/data/dendro_old.fits -------------------------------------------------------------------------------- /glue/plugins/exporters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/exporters/__init__.py -------------------------------------------------------------------------------- /glue/plugins/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/tests/__init__.py -------------------------------------------------------------------------------- /glue/plugins/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | 4 | def setup(): 5 | 6 | from glue.plugins.tools import python_export # noqa 7 | 8 | from glue_qt.viewers.histogram.data_viewer import HistogramViewer 9 | HistogramViewer.subtools = deepcopy(HistogramViewer.subtools) 10 | HistogramViewer.subtools['save'].append('save:python') 11 | 12 | from glue_qt.viewers.image.data_viewer import ImageViewer 13 | ImageViewer.subtools = deepcopy(ImageViewer.subtools) 14 | ImageViewer.subtools['save'].append('save:python') 15 | 16 | from glue_qt.viewers.scatter.data_viewer import ScatterViewer 17 | ScatterViewer.subtools = deepcopy(ScatterViewer.subtools) 18 | ScatterViewer.subtools['save'].append('save:python') 19 | 20 | from glue_qt.viewers.profile.data_viewer import ProfileViewer 21 | ProfileViewer.subtools = deepcopy(ProfileViewer.subtools) 22 | ProfileViewer.subtools['save'].append('save:python') 23 | -------------------------------------------------------------------------------- /glue/plugins/tools/pv_slicer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/tools/pv_slicer/__init__.py -------------------------------------------------------------------------------- /glue/plugins/tools/python_export.py: -------------------------------------------------------------------------------- 1 | from qtpy import compat 2 | from glue.config import viewer_tool 3 | from glue.viewers.common.tool import Tool 4 | 5 | 6 | @viewer_tool 7 | class PythonExportTool(Tool): 8 | 9 | icon = 'glue_pythonsave' 10 | tool_id = 'save:python' 11 | action_text = 'Save Python script to reproduce plot' 12 | tool_tip = 'Save Python script to reproduce plot' 13 | 14 | def activate(self): 15 | 16 | filename, _ = compat.getsavefilename(parent=self.viewer, basedir="make_plot.py") 17 | 18 | if not filename: 19 | return 20 | 21 | if not filename.endswith('.py'): 22 | filename += '.py' 23 | 24 | self.viewer.export_as_script(filename) 25 | -------------------------------------------------------------------------------- /glue/plugins/tools/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/tools/tests/__init__.py -------------------------------------------------------------------------------- /glue/plugins/wcs_autolinking/__init__.py: -------------------------------------------------------------------------------- 1 | def setup(): 2 | from . import wcs_autolinking # noqa 3 | -------------------------------------------------------------------------------- /glue/plugins/wcs_autolinking/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/plugins/wcs_autolinking/tests/__init__.py -------------------------------------------------------------------------------- /glue/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/tests/__init__.py -------------------------------------------------------------------------------- /glue/tests/example_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from glue.core.component import Component, CategoricalComponent 4 | from glue.core.data import Data 5 | 6 | 7 | def test_histogram_data(): 8 | data = Data(label="Test Data") 9 | comp_a = Component(np.random.uniform(size=500)) 10 | comp_b = Component(np.random.normal(size=500)) 11 | data.add_component(comp_a, 'uniform') 12 | data.add_component(comp_b, 'normal') 13 | return data 14 | 15 | 16 | def test_data(): 17 | data = Data(label="Test Data 1") 18 | data2 = Data(label="Teset Data 2") 19 | 20 | comp_a = Component(np.array([1, 2, 3])) 21 | comp_b = Component(np.array([1, 2, 3])) 22 | comp_c = Component(np.array([2, 4, 6])) 23 | comp_d = Component(np.array([1, 3, 5])) 24 | data.add_component(comp_a, 'a') 25 | data.add_component(comp_b, 'b') 26 | data2.add_component(comp_c, 'c') 27 | data2.add_component(comp_d, 'd') 28 | return data, data2 29 | 30 | 31 | def test_categorical_data(): 32 | 33 | data = Data(label="Test Cat Data 1") 34 | data2 = Data(label="Teset Cat Data 2") 35 | 36 | comp_x1 = CategoricalComponent(np.array(['a', 'a', 'b'])) 37 | comp_y1 = Component(np.array([1, 2, 3])) 38 | comp_x2 = CategoricalComponent(np.array(['c', 'a', 'b'])) 39 | comp_y2 = Component(np.array([1, 3, 5])) 40 | data.add_component(comp_x1, 'x1') 41 | data.add_component(comp_y1, 'y1') 42 | data2.add_component(comp_x2, 'x2') 43 | data2.add_component(comp_y2, 'y2') 44 | return data, data2 45 | 46 | 47 | def test_image(): 48 | data = Data(label="Test Image") 49 | comp_a = Component(np.ones((25, 25))) 50 | data.add_component(comp_a, 'test_1') 51 | comp_b = Component(np.zeros((25, 25))) 52 | data.add_component(comp_b, 'test_2') 53 | return data 54 | 55 | 56 | def test_cube(): 57 | data = Data(label="Test Cube") 58 | comp_a = Component(np.ones((16, 16, 16))) 59 | data.add_component(comp_a, 'test_3') 60 | return data 61 | -------------------------------------------------------------------------------- /glue/tests/test_config.py: -------------------------------------------------------------------------------- 1 | from ..config import link_function, data_factory 2 | 3 | 4 | def test_add_link_default(): 5 | @link_function(info='maps x to y', output_labels=['y']) 6 | def foo(x): 7 | return 3 8 | val = (foo, 'maps x to y', ['y'], 'General') 9 | assert val in link_function 10 | 11 | 12 | def test_add_data_factory(): 13 | @data_factory('XYZ file', "*txt") 14 | def foo(x): 15 | pass 16 | assert (foo, 'XYZ file', '*txt', 0, False) in data_factory 17 | -------------------------------------------------------------------------------- /glue/tests/test_main.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from glue.main import load_plugins, list_loaded_plugins, list_available_plugins 4 | from glue._plugin_helpers import REQUIRED_PLUGINS 5 | 6 | 7 | def test_load_plugins(capsys): 8 | """ 9 | Test customisable list of plugins load 10 | """ 11 | from glue.logger import logger 12 | 13 | with patch.object(logger, 'info') as info: 14 | load_plugins() 15 | 16 | # plugins = [call[0][0] for call in info.call_args_list if ('succeeded' or 'loaded') in call[0][0]] 17 | plugins = [] 18 | for acall in info.call_args_list: 19 | if ('loaded' or 'succeeded') in acall[0][0]: 20 | plugins.append(acall[0][0].split(' ')[1]) 21 | 22 | assert 'coordinate_helpers' in plugins 23 | 24 | 25 | def test_no_duplicate_loading(capsys): 26 | """ 27 | Regression test for duplicated loading of plugins 28 | on subsequent calls of `load_plugins()` after initial 29 | glue-qt startup. 30 | 31 | """ 32 | from glue.logger import logger 33 | 34 | with patch.object(logger, 'info') as info: 35 | load_plugins() 36 | 37 | plugins = [] 38 | for acall in info.call_args_list: 39 | plugins.append(acall[0][0]) 40 | if 'Loading plugin' in acall[0][0]: 41 | assert 'failed' in acall[0][0] 42 | 43 | loaded_plugins = list_loaded_plugins() 44 | assert 'glue.plugins.wcs_autolinking' in loaded_plugins 45 | assert 'glue.core.data_exporters' in loaded_plugins 46 | assert 'glue.plugins.coordinate_helpers' in loaded_plugins 47 | 48 | 49 | def test_list_loaded_plugins(): 50 | """ 51 | Unit test for retrieving the list of currently loaded plugins 52 | """ 53 | load_plugins(require_qt_plugins=False) 54 | plugins = list_loaded_plugins() 55 | assert isinstance(plugins, list) 56 | for test_plugin in REQUIRED_PLUGINS: 57 | assert test_plugin in plugins 58 | 59 | 60 | def test_list_available_plugins(): 61 | """ 62 | Unit test for retrieving the list of currently available plugins 63 | """ 64 | available_plugins = list_available_plugins() 65 | assert isinstance(available_plugins, list) 66 | assert 'glue.plugins.wcs_autolinking' in available_plugins 67 | assert 'glue.core.data_exporters' in available_plugins 68 | assert 'glue.plugins.coordinate_helpers' in available_plugins 69 | -------------------------------------------------------------------------------- /glue/tests/test_settings_helpers.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import os 4 | 5 | from glue.config import SettingRegistry 6 | from glue._settings_helpers import load_settings, save_settings 7 | 8 | 9 | def test_roundtrip(tmpdir): 10 | 11 | settings = SettingRegistry() 12 | 13 | settings.add('STRING', 'green', str) 14 | settings.add('INT', 3, int) 15 | settings.add('FLOAT', 5.5, float) 16 | settings.add('LIST', [1, 2, 3], list) 17 | 18 | with patch('glue.config.settings', settings): 19 | with patch('glue.config.CFG_DIR', tmpdir.strpath): 20 | 21 | settings.STRING = 'blue' 22 | settings.INT = 4 23 | settings.FLOAT = 3.5 24 | settings.LIST = ['A', 'BB', 'CCC'] 25 | 26 | settings.reset_defaults() 27 | 28 | assert settings.STRING == 'green' 29 | assert settings.INT == 3 30 | assert settings.FLOAT == 5.5 31 | assert settings.LIST == [1, 2, 3] 32 | 33 | settings.STRING = 'blue' 34 | settings.INT = 4 35 | settings.FLOAT = 3.5 36 | settings.LIST = ['A', 'BB', 'CCC'] 37 | 38 | save_settings() 39 | 40 | assert os.path.exists(os.path.join(tmpdir.strpath, 'settings.cfg')) 41 | 42 | settings.reset_defaults() 43 | 44 | settings.STRING = 'red' 45 | settings.INT = 5 46 | 47 | # Loading settings will only change settings that have not been 48 | # changed from the defaults... 49 | load_settings() 50 | 51 | assert settings.STRING == 'red' 52 | assert settings.INT == 5 53 | assert settings.FLOAT == 3.5 54 | assert settings.LIST == ['A', 'BB', 'CCC'] 55 | 56 | # ... unless the ``force=True`` option is passed 57 | load_settings(force=True) 58 | 59 | assert settings.STRING == 'blue' 60 | assert settings.INT == 4 61 | assert settings.FLOAT == 3.5 62 | assert settings.LIST == ['A', 'BB', 'CCC'] 63 | -------------------------------------------------------------------------------- /glue/tests/visual/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/tests/visual/__init__.py -------------------------------------------------------------------------------- /glue/tests/visual/helpers.py: -------------------------------------------------------------------------------- 1 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 2 | 3 | from functools import wraps 4 | 5 | import pytest 6 | 7 | try: 8 | import pytest_mpl # noqa 9 | except ImportError: 10 | HAS_PYTEST_MPL = False 11 | else: 12 | HAS_PYTEST_MPL = True 13 | 14 | 15 | def visual_test(*args, **kwargs): 16 | """ 17 | A decorator that defines a visual test. 18 | 19 | This automatically decorates tests with mpl_image_compare with common 20 | options used by all figure tests in glue-core. 21 | """ 22 | 23 | tolerance = kwargs.pop("tolerance", 0) 24 | style = kwargs.pop("style", {}) 25 | savefig_kwargs = kwargs.pop("savefig_kwargs", {}) 26 | savefig_kwargs["metadata"] = {"Software": None} 27 | 28 | def decorator(test_function): 29 | @pytest.mark.mpl_image_compare( 30 | tolerance=tolerance, style=style, savefig_kwargs=savefig_kwargs, **kwargs 31 | ) 32 | @pytest.mark.skipif( 33 | not HAS_PYTEST_MPL, reason="pytest-mpl is required for the figure tests" 34 | ) 35 | @wraps(test_function) 36 | def test_wrapper(*args, **kwargs): 37 | return test_function(*args, **kwargs) 38 | 39 | return test_wrapper 40 | 41 | # If the decorator was used without any arguments, the only positional 42 | # argument will be the test to decorate so we do the following: 43 | if len(args) == 1: 44 | return decorator(*args) 45 | 46 | return decorator 47 | -------------------------------------------------------------------------------- /glue/tests/visual/py311-test-visual.json: -------------------------------------------------------------------------------- 1 | { 2 | "glue.viewers.histogram.tests.test_viewer.test_simple_histogram_viewer": "cb08123fbad135ab614bb7ec13475fcc83321057d884fe80c3a32970b2d14762", 3 | "glue.viewers.image.tests.test_viewer.test_simple_image_viewer": "82016f9dfe0a813c23a1facea51e2ac6c2da3b84066606340112c5879667a33b", 4 | "glue.viewers.image.tests.test_viewer.test_image_region_layer": "0114922ab0a3980f56252656c69545927841aea0e6950250cdc2b1bafcd19d50", 5 | "glue.viewers.image.tests.test_viewer.test_image_region_layer_flip": "a142142f34961aba7e98188ad43abafe0e6e5b82e13e8cdab5131d297ed5832c", 6 | "glue.viewers.image.tests.test_viewer.TestWCSRegionDisplay.test_image_wcs_viewer": "eeb40b0a9ac574d1819bd0adab7bce2905b25eebfb23d304b3edb5b37f78588f", 7 | "glue.viewers.image.tests.test_viewer.TestWCSRegionDisplay.test_image_flipped_wcs_viewer": "d44822eb30b64558b06b307b1ca979e4133b3ca21491036eac9638d8b6900c49", 8 | "glue.viewers.profile.tests.test_viewer.test_simple_profile_viewer": "f68a21be5080fec513388b2d2b220512e7b0df5498e2489da54e58708de435b3", 9 | "glue.viewers.scatter.tests.test_viewer.test_simple_scatter_viewer": "1020a7bd3abe40510b9e03047c3b423b75c3c64ac18e6dcd6257173cec1ed53f", 10 | "glue.viewers.scatter.tests.test_viewer.test_scatter_density_map": "2748a46bf960f39be97d6195c661b1344c6cbfdd321523fc8085bfa8baf27e28" 11 | } 12 | -------------------------------------------------------------------------------- /glue/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | General utilities not specifically related to data linking (e.g. WCS or 3 | matplotlib helper functions). 4 | 5 | Utilities here cannot import from anywhere else in glue, with the exception of 6 | glue.external, and can only import standard library or external dependencies. 7 | """ 8 | 9 | from .array import * # noqa 10 | from .matplotlib import * # noqa 11 | from .misc import * # noqa 12 | from .geometry import * # noqa 13 | from .colors import * # noqa 14 | from .decorators import * # noqa 15 | from .data import * # noqa 16 | -------------------------------------------------------------------------------- /glue/utils/colors.py: -------------------------------------------------------------------------------- 1 | from matplotlib.colors import ColorConverter 2 | 3 | __all__ = ['alpha_blend_colors'] 4 | 5 | COLOR_CONVERTER = ColorConverter() 6 | 7 | 8 | def alpha_blend_colors(colors, additional_alpha=1.0): 9 | """ 10 | Given a sequence of colors, return the alpha blended color. 11 | 12 | This assumes the last color is the one in front. 13 | """ 14 | 15 | srcr, srcg, srcb, srca = COLOR_CONVERTER.to_rgba(colors[0]) 16 | srca *= additional_alpha 17 | 18 | for color in colors[1:]: 19 | dstr, dstg, dstb, dsta = COLOR_CONVERTER.to_rgba(color) 20 | dsta *= additional_alpha 21 | outa = srca + dsta * (1 - srca) 22 | outr = (srcr * srca + dstr * dsta * (1 - srca)) / outa 23 | outg = (srcg * srca + dstg * dsta * (1 - srca)) / outa 24 | outb = (srcb * srca + dstb * dsta * (1 - srca)) / outa 25 | srca, srcr, srcg, srcb = outa, outr, outg, outb 26 | 27 | return srcr, srcg, srcb, srca 28 | -------------------------------------------------------------------------------- /glue/utils/data.py: -------------------------------------------------------------------------------- 1 | from urllib.request import urlopen 2 | 3 | __all__ = ['require_data'] 4 | 5 | DATA_REPO = "https://raw.githubusercontent.com/glue-viz/glue-example-data/master/" 6 | 7 | 8 | def require_data(file_path): 9 | """ 10 | Download the specified file to the current folder, preserving the directory 11 | structure. 12 | 13 | Note that this should include forward slashes for paths even on Windows. 14 | """ 15 | 16 | # We use urlopen instead of urlretrieve to have control over the timeout 17 | 18 | local_path = file_path.split('/')[-1] 19 | 20 | request = urlopen(DATA_REPO + file_path, timeout=60) 21 | with open(local_path, 'wb') as f: 22 | f.write(request.read()) 23 | 24 | print("Successfully downloaded data file to {0}".format(local_path)) 25 | -------------------------------------------------------------------------------- /glue/utils/decorators.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import traceback 3 | 4 | __all__ = ['die_on_error', 'avoid_circular', 'decorate_all_methods'] 5 | 6 | 7 | def die_on_error(msg): 8 | """ 9 | Non-GUI version of the decorator in glue.utils.qt.decorators. 10 | 11 | In this case we just let the Python exception terminate the execution. 12 | """ 13 | def decorator(func): 14 | def wrapper(*args, **kwargs): 15 | try: 16 | return func(*args, **kwargs) 17 | except Exception as e: 18 | print('=' * 72) 19 | print(msg + ' (traceback below)') 20 | print('-' * 72) 21 | traceback.print_exc() 22 | print('=' * 72) 23 | return wrapper 24 | return decorator 25 | 26 | 27 | def avoid_circular(meth): 28 | def wrapper(self, *args, **kwargs): 29 | if not hasattr(self, '_in_avoid_circular') or not self._in_avoid_circular: 30 | self._in_avoid_circular = True 31 | try: 32 | return meth(self, *args, **kwargs) 33 | finally: 34 | self._in_avoid_circular = False 35 | return wrapper 36 | 37 | 38 | def decorate_all_methods(decorator): 39 | 40 | def decorate(cls): 41 | for name, value in inspect.getmembers(cls, inspect.isfunction): 42 | setattr(cls, name, decorator(value)) 43 | return cls 44 | 45 | return decorate 46 | -------------------------------------------------------------------------------- /glue/utils/error.py: -------------------------------------------------------------------------------- 1 | 2 | class GlueDeprecationWarning(UserWarning): 3 | """ 4 | Deprecation warnings for glue - this inherits from UserWarning not 5 | DeprecationWarning, to make sure it is shown by default. 6 | """ 7 | -------------------------------------------------------------------------------- /glue/utils/noconflict.py: -------------------------------------------------------------------------------- 1 | # Code adapted from: 2 | # 3 | # http://code.activestate.com/recipes/204197-solving-the-metaclass-conflict/ 4 | # 5 | # The code at the above URL was released under the PSF license. 6 | 7 | import inspect 8 | import builtins # noqa 9 | 10 | CLASS_TYPE = type 11 | 12 | __all__ = ['classmaker'] 13 | 14 | 15 | def skip_redundant(iterable, skipset=None): 16 | """ 17 | Redundant items are repeated items or items in the original skipset. 18 | """ 19 | if skipset is None: 20 | skipset = set() 21 | for item in iterable: 22 | if item not in skipset: 23 | skipset.add(item) 24 | yield item 25 | 26 | 27 | def remove_redundant(metaclasses): 28 | skipset = set([CLASS_TYPE]) 29 | for meta in metaclasses: # determines the metaclasses to be skipped 30 | skipset.update(inspect.getmro(meta)[1:]) 31 | return tuple(skip_redundant(metaclasses, skipset)) 32 | 33 | 34 | memoized_metaclasses_map = {} 35 | 36 | 37 | def get_noconflict_metaclass(bases, left_metas, right_metas): 38 | """ 39 | Not intended to be used outside of this module, unless you know 40 | what you are doing. 41 | """ 42 | # make tuple of needed metaclasses in specified priority order 43 | metas = left_metas + tuple(map(type, bases)) + right_metas 44 | needed_metas = remove_redundant(metas) 45 | 46 | # return existing confict-solving meta, if any 47 | if needed_metas in memoized_metaclasses_map: 48 | return memoized_metaclasses_map[needed_metas] 49 | # nope: compute, memoize and return needed conflict-solving meta 50 | elif not needed_metas: # wee, a trivial case, happy us 51 | meta = type 52 | elif len(needed_metas) == 1: # another trivial case 53 | meta = needed_metas[0] 54 | # check for recursion, can happen i.e. for Zope ExtensionClasses 55 | elif needed_metas == bases: 56 | raise TypeError("Incompatible root metatypes", needed_metas) 57 | else: # gotta work ... 58 | metaname = '_' + ''.join([m.__name__ for m in needed_metas]) 59 | meta = classmaker()(metaname, needed_metas, {}) 60 | memoized_metaclasses_map[needed_metas] = meta 61 | return meta 62 | 63 | 64 | def classmaker(left_metas=(), right_metas=()): 65 | def make_class(name, bases, adict): 66 | metaclass = get_noconflict_metaclass( 67 | bases, left_metas, right_metas) 68 | return metaclass(name, bases, adict) 69 | return make_class 70 | -------------------------------------------------------------------------------- /glue/utils/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/utils/tests/__init__.py -------------------------------------------------------------------------------- /glue/utils/tests/test_decorator.py: -------------------------------------------------------------------------------- 1 | from ..decorators import avoid_circular 2 | 3 | 4 | def test_avoid_circular(): 5 | 6 | class CircularCall(object): 7 | 8 | @avoid_circular 9 | def a(self): 10 | self.b() 11 | 12 | @avoid_circular 13 | def b(self): 14 | self.a() 15 | 16 | c = CircularCall() 17 | 18 | # Without avoid_circular, the following causes a recursion error 19 | c.a() 20 | -------------------------------------------------------------------------------- /glue/viewers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/__init__.py -------------------------------------------------------------------------------- /glue/viewers/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/common/__init__.py -------------------------------------------------------------------------------- /glue/viewers/common/python_export.py: -------------------------------------------------------------------------------- 1 | __all__ = ['code', 'serialize_options'] 2 | 3 | 4 | class code(str): 5 | pass 6 | 7 | 8 | def serialize_options(options): 9 | result = [] 10 | for key, value in options.items(): 11 | if isinstance(value, code): 12 | result.append(key + '=' + value) 13 | else: 14 | result.append(key + '=' + repr(value)) 15 | return ', '.join(result) 16 | -------------------------------------------------------------------------------- /glue/viewers/common/state.py: -------------------------------------------------------------------------------- 1 | from echo import CallbackProperty, ListCallbackProperty 2 | 3 | from glue.core.state_objects import State 4 | 5 | __all__ = ['ViewerState', 'LayerState'] 6 | 7 | 8 | class ViewerState(State): 9 | """ 10 | A base class for all viewer states. 11 | """ 12 | 13 | layers = ListCallbackProperty(docstring='A collection of all layers in the viewer') 14 | title = CallbackProperty(docstring='The title of the viewer') 15 | 16 | @property 17 | def layers_data(self): 18 | return [layer_state.layer for layer_state in self.layers] 19 | 20 | 21 | class LayerState(State): 22 | """ 23 | A base class for all layer states. 24 | """ 25 | 26 | layer = CallbackProperty(docstring='The :class:`~glue.core.data.Data` or ' 27 | ':class:`~glue.core.subset.Subset` ' 28 | 'represented by the layer') 29 | zorder = CallbackProperty(0, docstring='A value used to indicate which ' 30 | 'layers are shown in front of which ' 31 | '(larger zorder values are on top of ' 32 | 'other layers)') 33 | visible = CallbackProperty(True, docstring='Whether the layer is currently visible') 34 | 35 | def __init__(self, viewer_state=None, **kwargs): 36 | super(LayerState, self).__init__(**kwargs) 37 | self.viewer_state = viewer_state 38 | 39 | def __repr__(self): 40 | if self.layer is None: 41 | return "%s with layer unset" % (self.__class__.__name__) 42 | else: 43 | return "%s for %s" % (self.__class__.__name__, self.layer.label) 44 | -------------------------------------------------------------------------------- /glue/viewers/common/stretch_state_mixin.py: -------------------------------------------------------------------------------- 1 | from glue.config import stretches 2 | from glue.viewers.matplotlib.state import ( 3 | DeferredDrawDictCallbackProperty as DDDCProperty, 4 | DeferredDrawSelectionCallbackProperty as DDSCProperty, 5 | ) 6 | 7 | __all__ = ["StretchStateMixin"] 8 | 9 | 10 | class StretchStateMixin: 11 | stretch = DDSCProperty( 12 | docstring="The stretch used to render the layer, " 13 | "which should be one of ``linear``, " 14 | "``sqrt``, ``log``, or ``arcsinh``" 15 | ) 16 | stretch_parameters = DDDCProperty( 17 | docstring="Keyword arguments to pass to the stretch" 18 | ) 19 | 20 | _stretch_set_up = False 21 | 22 | def setup_stretch_callback(self): 23 | type(self).stretch.set_choices(self, list(stretches.members)) 24 | type(self).stretch.set_display_func(self, stretches.display_func) 25 | self._reset_stretch() 26 | self.add_callback("stretch", self._reset_stretch) 27 | self.add_callback("stretch_parameters", self._sync_stretch_parameters) 28 | self._stretch_set_up = True 29 | 30 | @property 31 | def stretch_object(self): 32 | if not self._stretch_set_up: 33 | raise Exception("setup_stretch_callback has not been called") 34 | return self._stretch_object 35 | 36 | def _sync_stretch_parameters(self, *args): 37 | for key, value in self.stretch_parameters.items(): 38 | if hasattr(self._stretch_object, key): 39 | setattr(self._stretch_object, key, value) 40 | else: 41 | raise ValueError( 42 | f"Stretch object {self._stretch_object.__class__.__name__} has no attribute {key}" 43 | ) 44 | 45 | def _reset_stretch(self, *args): 46 | self._stretch_object = stretches.members[self.stretch]() 47 | self.stretch_parameters.clear() 48 | -------------------------------------------------------------------------------- /glue/viewers/common/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/common/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/common/tests/test_stretch_state_mixin.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from astropy.visualization import LinearStretch, LogStretch 4 | 5 | from glue.core.state_objects import State 6 | from glue.viewers.common.stretch_state_mixin import StretchStateMixin 7 | 8 | 9 | class ExampleStateWithStretch(State, StretchStateMixin): 10 | pass 11 | 12 | 13 | def test_not_set_up(): 14 | state = ExampleStateWithStretch() 15 | with pytest.raises(Exception, match="setup_stretch_callback has not been called"): 16 | state.stretch_object 17 | 18 | 19 | class TestStretchStateMixin: 20 | def setup_method(self, method): 21 | self.state = ExampleStateWithStretch() 22 | self.state.setup_stretch_callback() 23 | 24 | def test_defaults(self): 25 | assert self.state.stretch == "linear" 26 | assert len(self.state.stretch_parameters) == 0 27 | assert isinstance(self.state.stretch_object, LinearStretch) 28 | 29 | def test_change_stretch(self): 30 | self.state.stretch = "log" 31 | assert self.state.stretch == "log" 32 | assert len(self.state.stretch_parameters) == 0 33 | assert isinstance(self.state.stretch_object, LogStretch) 34 | 35 | def test_invalid_parameter(self): 36 | with pytest.raises( 37 | ValueError, match="Stretch object LinearStretch has no attribute foo" 38 | ): 39 | self.state.stretch_parameters["foo"] = 1 40 | 41 | def test_set_parameter(self): 42 | 43 | pytest.importorskip('astropy', minversion='6.0') 44 | 45 | self.state.stretch = "log" 46 | 47 | assert self.state.stretch_object.a == 1000 48 | 49 | # Setting the stretch parameter 'exp' is synced with the stretch object attribute 50 | self.state.stretch_parameters["a"] = 200 51 | assert self.state.stretch_object.a == 200 52 | 53 | # Changing stretch resets the stretch parameter dictionary 54 | self.state.stretch = "linear" 55 | assert len(self.state.stretch_parameters) == 0 56 | 57 | # And there is no memory of previous parameters 58 | self.state.stretch = "log" 59 | assert self.state.stretch_object.a == 1000 60 | -------------------------------------------------------------------------------- /glue/viewers/common/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from glue.viewers.common.viewer import Viewer 2 | from glue.viewers.common.utils import get_viewer_tools 3 | 4 | 5 | class ViewerWithTools(Viewer): 6 | inherit_tools = True 7 | tools = ['save'] 8 | subtools = {'save': []} 9 | 10 | 11 | def test_get_viewer_tools(): 12 | 13 | class CustomViewer1(ViewerWithTools): 14 | pass 15 | 16 | tools, subtools = get_viewer_tools(CustomViewer1) 17 | 18 | assert tools == ['save'] 19 | assert subtools == {'save': []} 20 | 21 | class CustomViewer2(ViewerWithTools): 22 | tools = ['banana'] 23 | subtools = {'save': ['apple', 'pear']} 24 | 25 | tools, subtools = get_viewer_tools(CustomViewer2) 26 | 27 | assert tools == ['save', 'banana'] 28 | assert subtools == {'save': ['apple', 'pear']} 29 | 30 | CustomViewer2.inherit_tools = False 31 | 32 | tools, subtools = get_viewer_tools(CustomViewer2) 33 | 34 | assert tools == ['banana'] 35 | assert subtools == {'save': ['apple', 'pear']} 36 | 37 | class Mixin(object): 38 | pass 39 | 40 | class CustomViewer3(CustomViewer2, Mixin): 41 | tools = ['orange'] 42 | subtools = {'banana': ['one', 'two']} 43 | inherit_tools = True 44 | 45 | tools, subtools = get_viewer_tools(CustomViewer3) 46 | 47 | assert tools == ['banana', 'orange'] 48 | assert subtools == {'save': ['apple', 'pear'], 'banana': ['one', 'two']} 49 | -------------------------------------------------------------------------------- /glue/viewers/common/utils.py: -------------------------------------------------------------------------------- 1 | __all__ = ['get_viewer_tools'] 2 | 3 | 4 | def get_viewer_tools(cls, tools=None, subtools=None): 5 | """ 6 | Given a viewer class, find all the tools and subtools to include in the 7 | viewer. 8 | 9 | Parameters 10 | ---------- 11 | cls : type 12 | The viewer class for which to look for tools. 13 | tools : list 14 | The list to add the tools to - this is modified in-place. 15 | subtools : dict 16 | The dictionary to add the subtools to - this is modified in-place. 17 | """ 18 | if not hasattr(cls, 'inherit_tools'): 19 | return 20 | if tools is None: 21 | tools = [] 22 | if subtools is None: 23 | subtools = {} 24 | if cls.inherit_tools: 25 | for parent_cls in cls.__bases__: 26 | get_viewer_tools(parent_cls, tools, subtools) 27 | for tool_id in cls.tools: 28 | if tool_id not in tools: 29 | tools.append(tool_id) 30 | for tool_id in cls.subtools: 31 | if tool_id not in subtools: 32 | subtools[tool_id] = [] 33 | for subtool_id in cls.subtools[tool_id]: 34 | if subtool_id not in subtools[tool_id]: 35 | subtools[tool_id].append(subtool_id) 36 | return tools, subtools 37 | -------------------------------------------------------------------------------- /glue/viewers/custom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/custom/__init__.py -------------------------------------------------------------------------------- /glue/viewers/custom/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/custom/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/histogram/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/histogram/__init__.py -------------------------------------------------------------------------------- /glue/viewers/histogram/compat.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from .state import HistogramLayerState 4 | 5 | STATE_CLASS = {} 6 | STATE_CLASS['HistogramLayerArtist'] = HistogramLayerState 7 | 8 | 9 | def update_histogram_viewer_state(rec, context): 10 | """ 11 | Given viewer session information, make sure the session information is 12 | compatible with the current version of the viewers, and if not, update 13 | the session information in-place. 14 | """ 15 | 16 | if '_protocol' not in rec: 17 | 18 | # Note that files saved with protocol < 1 have bin settings saved per 19 | # layer but they were always restricted to be the same, so we can just 20 | # use the settings from the first layer 21 | 22 | rec['state'] = {} 23 | rec['state']['values'] = {} 24 | 25 | # TODO: could generalize this into a mapping 26 | properties = rec.pop('properties') 27 | viewer_state = rec['state']['values'] 28 | viewer_state['x_min'] = properties['xmin'] 29 | viewer_state['x_max'] = properties['xmax'] 30 | viewer_state['hist_n_bin'] = int(properties['nbins']) 31 | viewer_state['hist_x_min'] = properties['xmin'] 32 | viewer_state['hist_x_max'] = properties['xmax'] 33 | viewer_state['x_log'] = properties['xlog'] 34 | viewer_state['y_log'] = properties['ylog'] 35 | viewer_state['normalize'] = properties['normed'] 36 | viewer_state['cumulative'] = properties['cumulative'] 37 | viewer_state['x_att'] = properties['component'] 38 | 39 | layer_states = [] 40 | 41 | for layer in rec['layers']: 42 | state_id = str(uuid.uuid4()) 43 | state_cls = STATE_CLASS[layer['_type'].split('.')[-1]] 44 | state = state_cls(layer=context.object(layer.pop('layer'))) 45 | for prop in ('visible', 'zorder'): 46 | value = layer.pop(prop) 47 | value = context.object(value) 48 | setattr(state, prop, value) 49 | context.register_object(state_id, state) 50 | layer['state'] = state_id 51 | layer_states.append(state) 52 | layer.pop('lo', None) 53 | layer.pop('hi', None) 54 | layer.pop('nbins', None) 55 | layer.pop('xlog', None) 56 | 57 | list_id = str(uuid.uuid4()) 58 | context.register_object(list_id, layer_states) 59 | rec['state']['values']['layers'] = list_id 60 | -------------------------------------------------------------------------------- /glue/viewers/histogram/python_export.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from glue.viewers.common.python_export import code, serialize_options 4 | from glue.utils.matplotlib import MATPLOTLIB_GE_30 5 | 6 | 7 | def python_export_histogram_layer(layer, *args): 8 | 9 | if len(layer.mpl_artists) == 0 or not layer.enabled or not layer.visible: 10 | return [], None 11 | 12 | script = "" 13 | imports = ["import numpy as np"] 14 | 15 | x = layer.layer[layer._viewer_state.x_att] 16 | x_min = np.nanmin(x) 17 | x_max = np.nanmax(x) 18 | 19 | hist_x_min = layer._viewer_state.hist_x_min 20 | hist_x_max = layer._viewer_state.hist_x_max 21 | 22 | script += "# Get main data values\n" 23 | script += "x = layer_data['{0}']\n\n".format(layer._viewer_state.x_att.label) 24 | 25 | script += "# Set up histogram bins\n" 26 | script += "hist_n_bin = {0}\n".format(layer._viewer_state.hist_n_bin) 27 | 28 | if abs((x_min - hist_x_min) / (hist_x_max - hist_x_min)) < 0.001: 29 | script += "hist_x_min = np.nanmin(x)\n" 30 | else: 31 | script += "hist_x_min = {0}\n".format(hist_x_min) 32 | 33 | if abs((x_max - hist_x_max) / (hist_x_max - hist_x_min)) < 0.001: 34 | script += "hist_x_max = np.nanmax(x)\n" 35 | else: 36 | script += "hist_x_max = {0}\n".format(hist_x_max) 37 | 38 | options = dict(alpha=layer.state.alpha, 39 | color=layer.state.color, 40 | zorder=layer.state.zorder, 41 | edgecolor='none') 42 | 43 | if layer._viewer_state.x_log: 44 | script += "bins = np.logspace(np.log10(hist_x_min), np.log10(hist_x_max), hist_n_bin)\n" 45 | options['bins'] = code('bins') 46 | else: 47 | options['range'] = code('[hist_x_min, hist_x_max]') 48 | options['bins'] = code('hist_n_bin') 49 | 50 | if layer._viewer_state.normalize: 51 | if MATPLOTLIB_GE_30: 52 | options['density'] = True 53 | else: 54 | options['normed'] = True 55 | 56 | if layer._viewer_state.cumulative: 57 | options['cumulative'] = True 58 | 59 | script += "\nx = x[(x >= hist_x_min) & (x <= hist_x_max)]\n\n" 60 | 61 | script += "ax.hist(x, {0})\n\n".format(serialize_options(options)) 62 | options = dict( 63 | facecolor=layer.state.color, 64 | edgecolor='none', 65 | alpha=layer.state.alpha) 66 | 67 | imports += ["from matplotlib.patches import Patch"] 68 | script += "handle = Patch({0}) # for legend\n".format(serialize_options(options)) 69 | script += "legend_handles.append(handle)\n" 70 | script += "legend_labels.append(layer_data.label)\n" 71 | 72 | return imports, script.strip() 73 | -------------------------------------------------------------------------------- /glue/viewers/histogram/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/histogram/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/histogram/tests/test_python_export.py: -------------------------------------------------------------------------------- 1 | from astropy.utils import NumpyRNGContext 2 | 3 | from glue.core import Data, DataCollection 4 | from glue.core.application_base import Application 5 | from glue.viewers.histogram.viewer import SimpleHistogramViewer 6 | from glue.viewers.matplotlib.tests.test_python_export import BaseTestExportPython, random_with_nan 7 | 8 | 9 | class TestExportPython(BaseTestExportPython): 10 | 11 | def setup_method(self, method): 12 | 13 | with NumpyRNGContext(12345): 14 | self.data = Data(**dict((name, random_with_nan(100, nan_index=idx + 1)) for idx, name in enumerate('abcdefgh'))) 15 | self.data_collection = DataCollection([self.data]) 16 | self.app = Application(self.data_collection) 17 | self.viewer = self.app.new_data_viewer(SimpleHistogramViewer) 18 | self.viewer.add_data(self.data) 19 | self.viewer.state.x_att = self.data.id['a'] 20 | 21 | def teardown_method(self, method): 22 | self.viewer = None 23 | self.app = None 24 | 25 | def test_simple(self, tmpdir): 26 | self.assert_same(tmpdir) 27 | 28 | def test_simple_visual(self, tmpdir): 29 | self.viewer.state.layers[0].color = 'blue' 30 | self.viewer.state.layers[0].alpha = 0.5 31 | self.assert_same(tmpdir) 32 | 33 | def test_simple_visual_legend(self, tmpdir): 34 | self.viewer.state.legend.visible = True 35 | self.viewer.state.layers[0].color = 'blue' 36 | self.viewer.state.layers[0].alpha = 0.5 37 | self.assert_same(tmpdir) 38 | 39 | def test_cumulative(self, tmpdir): 40 | self.viewer.state.cumulative = True 41 | self.assert_same(tmpdir) 42 | 43 | def test_normalize(self, tmpdir): 44 | self.viewer.state.normalize = True 45 | self.assert_same(tmpdir) 46 | 47 | def test_subset(self, tmpdir): 48 | self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) 49 | self.assert_same(tmpdir) 50 | 51 | def test_subset_legend(self, tmpdir): 52 | self.viewer.state.legend.visible = True 53 | self.data_collection.new_subset_group('mysubset', self.data.id['a'] > 0.5) 54 | self.assert_same(tmpdir) 55 | 56 | def test_empty(self, tmpdir): 57 | self.viewer.state.x_min = 10 58 | self.viewer.state.x_max = 11 59 | self.viewer.state.hist_x_min = 10 60 | self.viewer.state.hist_x_max = 11 61 | self.assert_same(tmpdir) 62 | -------------------------------------------------------------------------------- /glue/viewers/image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/image/__init__.py -------------------------------------------------------------------------------- /glue/viewers/image/frb_artist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Matplotlib artist for fixed resolution buffers. 3 | """ 4 | 5 | from mpl_scatter_density.base_image_artist import BaseImageArtist 6 | 7 | import numpy as np 8 | 9 | from glue.utils import color2rgb 10 | 11 | 12 | class FRBArtist(BaseImageArtist): 13 | 14 | def __init__(self, ax, **kwargs): 15 | self._array_maker = None 16 | super(FRBArtist, self).__init__(ax, update_while_panning=False, 17 | array_func=self.array_func_wrapper, 18 | **kwargs) 19 | 20 | def array_func_wrapper(self, bins=None, range=None): 21 | if self._array_maker is None: 22 | return np.array([[np.nan]]) 23 | else: 24 | ny, nx = bins 25 | (ymin, ymax), (xmin, xmax) = range 26 | bounds = [(ymin, ymax, ny), (xmin, xmax, nx)] 27 | array = self._array_maker(bounds) 28 | if array is None: 29 | return np.array([[np.nan]]) 30 | else: 31 | return array 32 | 33 | def set_array_maker(self, array_maker): 34 | self._array_maker = array_maker 35 | 36 | def invalidate_cache(self): 37 | self.stale = True 38 | self.set_visible(True) 39 | 40 | 41 | def imshow(axes, array_maker, aspect=None, vmin=None, vmax=None, color=None, **kwargs): 42 | """ 43 | Similar to matplotlib's imshow command, but produces a FRBArtist 44 | """ 45 | 46 | axes.set_aspect(aspect) 47 | 48 | im = FRBArtist(axes, **kwargs) 49 | 50 | if color: 51 | 52 | def wrapper(bounds=None): 53 | 54 | # Get original array 55 | mask = array_maker(bounds=bounds) 56 | 57 | # Convert to RGBA array" 58 | r, g, b = color2rgb(color) 59 | mask = np.dstack((r * mask, g * mask, b * mask, mask * .5)) 60 | mask = (255 * mask).astype(np.uint8) 61 | 62 | return mask 63 | 64 | im.set_array_maker(wrapper) 65 | 66 | else: 67 | 68 | im.set_array_maker(array_maker) 69 | 70 | axes._set_artist_props(im) 71 | 72 | if im.get_clip_path() is None: 73 | # image does not already have clipping set, clip to axes patch 74 | im.set_clip_path(axes.patch) 75 | 76 | if vmin is not None or vmax is not None: 77 | im.set_clim(vmin, vmax) 78 | 79 | axes.add_image(im) 80 | 81 | return im 82 | -------------------------------------------------------------------------------- /glue/viewers/image/pixel_selection_subset_state.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from glue.core.subset import SliceSubsetState 4 | from glue.core.exceptions import IncompatibleAttribute 5 | from glue.core.link_manager import pixel_cid_to_pixel_cid_matrix 6 | 7 | __all__ = ['PixelSubsetState'] 8 | 9 | 10 | class PixelSubsetState(SliceSubsetState): 11 | 12 | def copy(self): 13 | return PixelSubsetState(self.reference_data, self.slices) 14 | 15 | def to_array(self, data, att): 16 | 17 | try: 18 | 19 | return super(PixelSubsetState, self).to_array(data, att) 20 | 21 | except IncompatibleAttribute: 22 | 23 | if data is not self.reference_data: 24 | pix_coord_out = self._to_linked_pixel_coords(data) 25 | pix_coord_out = tuple([slice(None) if p is None else slice(p, p + 1) for p in pix_coord_out]) 26 | return data[att, pix_coord_out] 27 | 28 | raise IncompatibleAttribute() 29 | 30 | def _to_linked_pixel_coords(self, data): 31 | 32 | # Determine which pixel dimensions are being sliced over 33 | dimensions = [idim for idim, slc in enumerate(self.slices) if slc.start is not None] 34 | 35 | # Determine pixel to pixel correlation matrix 36 | matrix = pixel_cid_to_pixel_cid_matrix(self.reference_data, data) 37 | 38 | # Find pixel dimensions in 'data' that are correlated 39 | correlated_dims = np.nonzero(np.any(matrix[dimensions], axis=0))[0] 40 | 41 | # Check that if we do the operation backwards we just get the 42 | # original dimensions back 43 | check_dimensions = np.nonzero(np.any(matrix[:, correlated_dims], axis=1))[0] 44 | 45 | if np.array_equal(dimensions, check_dimensions): 46 | pix_coord_in = tuple([slice(0, 1) if slc.start is None else slc for slc in self.slices]) 47 | pix_coord_out = [] 48 | for idim, pix_cid in enumerate(data.pixel_component_ids): 49 | if idim in correlated_dims: 50 | coord = int(np.round(self.reference_data[pix_cid, pix_coord_in].ravel()[0])) 51 | else: 52 | coord = None 53 | pix_coord_out.append(coord) 54 | 55 | return pix_coord_out 56 | 57 | raise IncompatibleAttribute() 58 | 59 | def get_xy(self, data, dim1, dim2): 60 | pix_coord_out = self._to_linked_pixel_coords(data) 61 | if pix_coord_out[dim1] is None or pix_coord_out[dim2] is None: 62 | raise IncompatibleAttribute 63 | else: 64 | return pix_coord_out[dim1], pix_coord_out[dim2] 65 | -------------------------------------------------------------------------------- /glue/viewers/image/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/image/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/matplotlib/__init__.py: -------------------------------------------------------------------------------- 1 | from . import toolbar_mode # noqa 2 | -------------------------------------------------------------------------------- /glue/viewers/matplotlib/mpl_axes.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | 3 | from glue.utils.matplotlib import freeze_margins 4 | 5 | 6 | __all__ = ['update_appearance_from_settings', 'init_mpl'] 7 | 8 | 9 | def set_background_color(axes, color): 10 | axes.figure.set_facecolor(color) 11 | axes.patch.set_facecolor(color) 12 | 13 | 14 | def set_foreground_color(axes, color): 15 | if hasattr(axes, 'coords'): 16 | axes.coords.frame.set_color(color) 17 | axes.coords.frame.set_linewidth(1) 18 | for coord in axes.coords: 19 | coord.set_ticks(color=color) 20 | coord.set_ticklabel(color=color) 21 | coord.axislabels.set_color(color) 22 | else: 23 | for spine in axes.spines.values(): 24 | spine.set_color(color) 25 | axes.tick_params(which="both", 26 | color=color, 27 | labelcolor=color) 28 | axes.xaxis.label.set_color(color) 29 | axes.yaxis.label.set_color(color) 30 | 31 | 32 | def set_figure_colors(axes, background, foreground): 33 | set_background_color(axes, background) 34 | set_foreground_color(axes, foreground) 35 | 36 | 37 | def update_appearance_from_settings(axes): 38 | from glue.config import settings 39 | set_figure_colors(axes, settings.BACKGROUND_COLOR, settings.FOREGROUND_COLOR) 40 | 41 | 42 | def init_mpl(figure=None, axes=None, wcs=False, axes_factory=None, projection=None): 43 | 44 | if (axes is not None and figure is not None and 45 | axes.figure is not figure): 46 | raise ValueError("Axes and figure are incompatible") 47 | 48 | try: 49 | from astropy.visualization.wcsaxes import WCSAxesSubplot 50 | except ImportError: 51 | WCSAxesSubplot = None 52 | 53 | if axes is not None: 54 | _axes = axes 55 | _figure = axes.figure 56 | else: 57 | _figure = figure or plt.figure() 58 | if wcs and WCSAxesSubplot is not None: 59 | _axes = WCSAxesSubplot(_figure, 111) 60 | _figure.add_axes(_axes) 61 | else: 62 | if axes_factory is None: 63 | _axes = _figure.add_subplot(1, 1, 1, projection=projection) 64 | else: 65 | _axes = axes_factory(_figure) 66 | 67 | freeze_margins(_axes, margins=[1, 0.25, 0.50, 0.25]) 68 | 69 | update_appearance_from_settings(_axes) 70 | 71 | return _figure, _axes 72 | -------------------------------------------------------------------------------- /glue/viewers/matplotlib/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/matplotlib/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/matplotlib/tests/test_python_export.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pytest 4 | import subprocess 5 | 6 | from glue.config import settings 7 | 8 | import numpy as np 9 | 10 | from matplotlib.testing.compare import compare_images 11 | 12 | __all__ = ['random_with_nan', 'BaseTestExportPython'] 13 | 14 | 15 | def random_with_nan(nsamples, nan_index): 16 | x = np.random.random(nsamples) 17 | x[nan_index] = np.nan 18 | return x 19 | 20 | 21 | class BaseTestExportPython: 22 | 23 | def assert_same(self, tmpdir, tol=0.1): 24 | 25 | os.chdir(tmpdir.strpath) 26 | 27 | expected = tmpdir.join('expected.png').strpath 28 | script = tmpdir.join('actual.py').strpath 29 | actual = tmpdir.join('glue_plot.png').strpath 30 | 31 | self.viewer.axes.figure.savefig(expected) 32 | 33 | self.viewer.export_as_script(script) 34 | subprocess.call([sys.executable, script]) 35 | 36 | msg = compare_images(expected, actual, tol=tol) 37 | 38 | if msg: 39 | 40 | from base64 import b64encode 41 | 42 | print("SCRIPT:") 43 | with open(script, 'r') as f: 44 | print(f.read()) 45 | 46 | print("EXPECTED:") 47 | with open(expected, 'rb') as f: 48 | print(b64encode(f.read()).decode()) 49 | 50 | print("ACTUAL:") 51 | with open(actual, 'rb') as f: 52 | print(b64encode(f.read()).decode()) 53 | 54 | pytest.fail(msg, pytrace=False) 55 | 56 | def test_color_settings(self, tmpdir): 57 | settings.FOREGROUND_COLOR = '#a51d2d' 58 | settings.BACKGROUND_COLOR = '#99c1f1' 59 | self.viewer._update_appearance_from_settings() 60 | self.assert_same(tmpdir) 61 | settings.reset_defaults() 62 | self.viewer._update_appearance_from_settings() 63 | -------------------------------------------------------------------------------- /glue/viewers/matplotlib/tests/test_state.py: -------------------------------------------------------------------------------- 1 | from ..state import MatplotlibLegendState, MatplotlibDataViewerState 2 | from glue.config import settings 3 | from glue.core.tests.test_state import clone 4 | 5 | from matplotlib.colors import to_rgba 6 | 7 | 8 | class TestMatplotlibDataViewerState: 9 | def setup_method(self, method): 10 | self.state = MatplotlibDataViewerState() 11 | 12 | def test_legend_serialization(self): 13 | legend_state = self.state.legend 14 | legend_state.visible = True 15 | legend_state.location = "best" 16 | legend_state.title = "Legend" 17 | legend_state.fontsize = 13 18 | legend_state.alpha = 0.7 19 | legend_state.frame_color = "#1e00f1" 20 | legend_state.show_edge = False 21 | legend_state.text_color = "#fad8f1" 22 | 23 | new_state = clone(self.state) 24 | new_legend_state = new_state.legend 25 | assert new_legend_state.visible 26 | assert new_legend_state.location == "best" 27 | assert new_legend_state.title == "Legend" 28 | assert new_legend_state.fontsize == 13 29 | assert new_legend_state.alpha == 0.7 30 | assert new_legend_state.frame_color == "#1e00f1" 31 | assert not new_legend_state.show_edge 32 | assert new_legend_state.text_color == "#fad8f1" 33 | 34 | 35 | class TestMatplotlibLegendState: 36 | def setup_method(self, method): 37 | self.state = MatplotlibLegendState() 38 | 39 | def test_draggable(self): 40 | self.state.location = 'draggable' 41 | assert self.state.draggable 42 | assert self.state.mpl_location == 'best' 43 | 44 | def test_no_draggable(self): 45 | self.state.location = 'lower left' 46 | assert not self.state.draggable 47 | assert self.state.mpl_location == 'lower left' 48 | 49 | def test_no_edge(self): 50 | self.state.show_edge = False 51 | assert self.state.edge_color is None 52 | 53 | def test_default_color(self): 54 | assert self.state.frame_color == settings.BACKGROUND_COLOR 55 | assert self.state.edge_color == to_rgba(settings.FOREGROUND_COLOR, self.state.alpha) 56 | -------------------------------------------------------------------------------- /glue/viewers/matplotlib/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | from numpy.testing import assert_allclose 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | from glue.core.application_base import Application 6 | from glue.viewers.common.viewer import Viewer 7 | from glue.viewers.matplotlib.viewer import MatplotlibViewerMixin 8 | from glue.viewers.matplotlib.state import MatplotlibDataViewerState 9 | 10 | 11 | def assert_limits(viewer, x_min, x_max, y_min, y_max): 12 | # Convenience to check both state and matplotlib 13 | assert_allclose(viewer.state.x_min, x_min) 14 | assert_allclose(viewer.state.x_max, x_max) 15 | assert_allclose(viewer.state.y_min, y_min) 16 | assert_allclose(viewer.state.y_max, y_max) 17 | assert_allclose(viewer.axes.get_xlim(), (x_min, x_max)) 18 | assert_allclose(viewer.axes.get_ylim(), (y_min, y_max)) 19 | 20 | 21 | def test_aspect_ratio(): 22 | 23 | # Test of the aspect ratio infrastructure 24 | 25 | class CustomViewer(MatplotlibViewerMixin, Viewer): 26 | 27 | _state_cls = MatplotlibDataViewerState 28 | 29 | def __init__(self, *args, **kwargs): 30 | Viewer.__init__(self, *args, **kwargs) 31 | self.figure = plt.figure(figsize=(12, 6)) 32 | self.axes = self.figure.add_axes([0, 0, 1, 1]) 33 | MatplotlibViewerMixin.setup_callbacks(self) 34 | 35 | def show(self): 36 | pass 37 | 38 | class CustomApplication(Application): 39 | def add_widget(self, *args, **kwargs): 40 | pass 41 | 42 | app = CustomApplication() 43 | 44 | viewer = app.new_data_viewer(CustomViewer) 45 | viewer.state.aspect = 'equal' 46 | 47 | assert_limits(viewer, -0.5, 1.5, 0., 1.) 48 | 49 | # Test changing x limits in state, which should just change the y limits 50 | 51 | viewer.state.x_min = -2.5 52 | assert_limits(viewer, -2.5, 1.5, -0.5, 1.5) 53 | 54 | viewer.state.x_max = -1.5 55 | assert_limits(viewer, -2.5, -1.5, 0.25, 0.75) 56 | 57 | # Test changing y limits in state, which should just change the x limits 58 | 59 | viewer.state.y_max = 1.25 60 | assert_limits(viewer, -3.0, -1.0, 0.25, 1.25) 61 | 62 | viewer.state.y_min = 0.75 63 | assert_limits(viewer, -2.5, -1.5, 0.75, 1.25) 64 | 65 | # Test changing x limits in Matplotlib, which should just change the x limits 66 | 67 | viewer.axes.set_xlim(1., 3.) 68 | assert_limits(viewer, 1.0, 3.0, 0.5, 1.5) 69 | 70 | # Test changing x limits in Matplotlib, which should just change the x limits 71 | 72 | viewer.axes.set_ylim(0., 2.) 73 | assert_limits(viewer, 0.0, 4.0, 0.0, 2.0) 74 | 75 | # We include tests for resizing inside the Qt folder (since this doesn't 76 | # work correctly with the Agg backend) 77 | -------------------------------------------------------------------------------- /glue/viewers/profile/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/profile/__init__.py -------------------------------------------------------------------------------- /glue/viewers/profile/python_export.py: -------------------------------------------------------------------------------- 1 | from glue.viewers.common.python_export import serialize_options 2 | from glue.core import Subset 3 | 4 | 5 | def python_export_profile_layer(layer, *args): 6 | 7 | if len(layer.mpl_artists) == 0 or not layer.enabled or not layer.visible: 8 | return [], None 9 | 10 | script = "" 11 | imports = ["import numpy as np"] 12 | 13 | script += "# Calculate the profile of the data\n" 14 | script += "profile_axis = {0}\n".format(layer._viewer_state.x_att_pixel.axis) 15 | script += "collapsed_axes = tuple(i for i in range(layer_data.ndim) if i != profile_axis)\n" 16 | if isinstance(layer.state.layer, Subset): 17 | script += "base_data = layer_data.data\n" 18 | script += "cid = base_data.find_component_id('{0}')\n".format(layer.state.attribute.label) 19 | script += "profile_values = base_data.compute_statistic('{0}', cid, axis=collapsed_axes, subset_state=layer_data.subset_state)\n\n".format(layer._viewer_state.function) 20 | else: 21 | script += "cid = layer_data.find_component_id('{0}')\n".format(layer.state.attribute.label) 22 | script += "profile_values = layer_data.compute_statistic('{0}', cid, axis=collapsed_axes)\n\n".format(layer._viewer_state.function) 23 | 24 | script += "# Extract the values for the x-axis\n" 25 | script += "axis_view = [0] * layer_data.ndim\n" 26 | script += "axis_view[profile_axis] = slice(None)\n" 27 | script += "profile_x_values = layer_data['{0}', tuple(axis_view)]\n".format(layer._viewer_state.x_att) 28 | script += "keep = ~np.isnan(profile_values) & ~np.isnan(profile_x_values)\n\n" 29 | 30 | if layer._viewer_state.normalize: 31 | script += "# Normalize the profile data\n" 32 | script += "vmax = np.nanmax(profile_values)\n" 33 | script += "vmin = np.nanmin(profile_values)\n" 34 | script += "profile_values = (profile_values - vmin)/(vmax - vmin)\n\n" 35 | 36 | script += "# Plot the profile\n" 37 | plot_options = dict(color=layer.state.color, 38 | linewidth=layer.state.linewidth, 39 | alpha=layer.state.alpha, 40 | zorder=layer.state.zorder, 41 | drawstyle='steps-mid') 42 | 43 | script += "handle, = ax.plot(profile_x_values[keep], profile_values[keep], '-', {0})\n".format(serialize_options(plot_options)) 44 | script += "legend_handles.append(handle)\n" 45 | script += "legend_labels.append(layer_data.label)\n\n" 46 | 47 | return imports, script.strip() 48 | -------------------------------------------------------------------------------- /glue/viewers/profile/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/profile/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/scatter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/scatter/__init__.py -------------------------------------------------------------------------------- /glue/viewers/scatter/compat.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from .state import ScatterLayerState 4 | 5 | STATE_CLASS = {} 6 | STATE_CLASS['ScatterLayerArtist'] = ScatterLayerState 7 | 8 | 9 | def update_scatter_viewer_state(rec, context): 10 | """ 11 | Given viewer session information, make sure the session information is 12 | compatible with the current version of the viewers, and if not, update 13 | the session information in-place. 14 | """ 15 | 16 | if '_protocol' not in rec: 17 | 18 | # Note that files saved with protocol < 1 have bin settings saved per 19 | # layer but they were always restricted to be the same, so we can just 20 | # use the settings from the first layer 21 | 22 | rec['state'] = {} 23 | rec['state']['values'] = {} 24 | 25 | # TODO: could generalize this into a mapping 26 | properties = rec.pop('properties') 27 | viewer_state = rec['state']['values'] 28 | viewer_state['x_min'] = properties['xmin'] 29 | viewer_state['x_max'] = properties['xmax'] 30 | viewer_state['y_min'] = properties['ymin'] 31 | viewer_state['y_max'] = properties['ymax'] 32 | viewer_state['x_log'] = properties['xlog'] 33 | viewer_state['y_log'] = properties['ylog'] 34 | viewer_state['x_att'] = properties['xatt'] 35 | viewer_state['y_att'] = properties['yatt'] 36 | 37 | layer_states = [] 38 | 39 | for layer in rec['layers']: 40 | state_id = str(uuid.uuid4()) 41 | state_cls = STATE_CLASS[layer['_type'].split('.')[-1]] 42 | state = state_cls(layer=context.object(layer.pop('layer'))) 43 | for prop in ('visible', 'zorder'): 44 | value = layer.pop(prop) 45 | value = context.object(value) 46 | setattr(state, prop, value) 47 | context.register_object(state_id, state) 48 | layer['state'] = state_id 49 | layer_states.append(state) 50 | layer.pop('lo', None) 51 | layer.pop('hi', None) 52 | layer.pop('nbins', None) 53 | layer.pop('xlog', None) 54 | 55 | list_id = str(uuid.uuid4()) 56 | context.register_object(list_id, layer_states) 57 | rec['state']['values']['layers'] = list_id 58 | -------------------------------------------------------------------------------- /glue/viewers/scatter/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/scatter/tests/__init__.py -------------------------------------------------------------------------------- /glue/viewers/table/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/table/__init__.py -------------------------------------------------------------------------------- /glue/viewers/table/compat.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from glue.viewers.common.state import LayerState 4 | 5 | STATE_CLASS = {} 6 | STATE_CLASS['TableLayerArtist'] = LayerState 7 | 8 | 9 | def update_table_viewer_state(rec, context): 10 | """ 11 | Given viewer session information, make sure the session information is 12 | compatible with the current version of the viewers, and if not, update 13 | the session information in-place. 14 | """ 15 | 16 | if '_protocol' not in rec: 17 | 18 | # Note that files saved with protocol < 1 have bin settings saved per 19 | # layer but they were always restricted to be the same, so we can just 20 | # use the settings from the first layer 21 | 22 | rec['state'] = {} 23 | rec['state']['values'] = {} 24 | 25 | _ = rec.pop('properties') 26 | 27 | layer_states = [] 28 | 29 | for layer in rec['layers']: 30 | state_id = str(uuid.uuid4()) 31 | state_cls = STATE_CLASS[layer['_type'].split('.')[-1]] 32 | state = state_cls(layer=context.object(layer.pop('layer'))) 33 | for prop in ('visible', 'zorder'): 34 | value = layer.pop(prop) 35 | value = context.object(value) 36 | setattr(state, prop, value) 37 | context.register_object(state_id, state) 38 | layer['state'] = state_id 39 | layer_states.append(state) 40 | 41 | list_id = str(uuid.uuid4()) 42 | context.register_object(list_id, layer_states) 43 | rec['state']['values']['layers'] = list_id 44 | -------------------------------------------------------------------------------- /glue/viewers/table/state.py: -------------------------------------------------------------------------------- 1 | from echo import CallbackProperty, SelectionCallbackProperty 2 | from glue.core.data_combo_helper import ComponentIDComboHelper 3 | 4 | from glue.viewers.common.state import ViewerState 5 | 6 | 7 | __all__ = ['TableViewerState'] 8 | 9 | 10 | class TableViewerState(ViewerState): 11 | """ 12 | A state class that includes all the attributes for a table viewer. 13 | """ 14 | 15 | filter_att = SelectionCallbackProperty(docstring='The component/column to filter/search on', default_index=0) 16 | filter = CallbackProperty(docstring='The text string to filter/search on') 17 | regex = CallbackProperty(docstring='Whether to apply regex to filter/search', default=False) 18 | 19 | def __init__(self, **kwargs): 20 | 21 | super(TableViewerState, self).__init__() 22 | self.filter_att_helper = ComponentIDComboHelper(self, 'filter_att', categorical=True, numeric=False) 23 | self.add_callback('layers', self._layers_changed) 24 | 25 | self.update_from_dict(kwargs) 26 | 27 | def _layers_changed(self, *args): 28 | 29 | layers_data = self.layers_data 30 | 31 | layers_data_cache = getattr(self, '_layers_data_cache', []) 32 | 33 | if layers_data == layers_data_cache: 34 | return 35 | 36 | self.filter_att_helper.set_multiple_data(self.layers_data) 37 | 38 | self._layers_data_cache = layers_data 39 | 40 | def _update_priority(self, name): 41 | if name == 'layers': 42 | return 2 43 | else: 44 | return 1 45 | -------------------------------------------------------------------------------- /glue/viewers/table/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue/0c612670816cbfcd29460d482e7d637f19a277ef/glue/viewers/table/tests/__init__.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from distutils.version import LooseVersion 5 | 6 | try: 7 | import setuptools 8 | assert LooseVersion(setuptools.__version__) >= LooseVersion('30.3') 9 | except (ImportError, AssertionError): 10 | sys.stderr.write("ERROR: setuptools 30.3 or later is required\n") 11 | sys.exit(1) 12 | 13 | from setuptools import setup 14 | 15 | setup(use_scm_version=True) 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{38,39,310,311,312,313}-{codestyle,test,docs}-all-{dev,legacy}{,-visual} 4 | requires = pip >= 18.0 5 | setuptools >= 30.3.0 6 | 7 | [testenv] 8 | # Pass through the following environment variables which are needed for the CI 9 | passenv = 10 | DISPLAY 11 | HOME 12 | setenv = 13 | dev: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple 14 | visual: MPLFLAGS = -m "mpl_image_compare" --mpl --mpl-generate-summary=html --mpl-results-path={toxinidir}/results --mpl-hash-library={toxinidir}/glue/tests/visual/{envname}.json --mpl-baseline-path=https://raw.githubusercontent.com/glue-viz/glue-core-visual-tests/main/images/{envname}/ 15 | whitelist_externals = 16 | find 17 | rm 18 | sed 19 | make 20 | changedir = 21 | test: .tmp/{envname} 22 | docs: doc 23 | deps = 24 | dev: numpy>=0.0.dev0 25 | dev: scipy>=0.0.dev0 26 | dev: astropy>=0.0.dev0 27 | # LTS 28 | lts: astropy==5.0.* 29 | lts: matplotlib==3.5.* 30 | # Pin numpy-lts until permanent solution for #2353/#2428 31 | lts: numpy==1.24.* 32 | legacy: numpy==1.17.* 33 | legacy: matplotlib==3.2.* 34 | legacy: scipy==1.1.* 35 | legacy: pandas==1.2.* 36 | legacy: echo==0.5.* 37 | legacy: astropy==4.0.* 38 | legacy: ipython==7.16.* 39 | legacy: ipykernel==5.3.* 40 | legacy: dill==0.2.* 41 | legacy: xlrd==1.2.* 42 | legacy: h5py==2.10.* 43 | legacy: mpl-scatter-density==0.8.* 44 | legacy: openpyxl==3.0.* 45 | extras = 46 | test 47 | all: all 48 | docs: docs 49 | visual: visualtest 50 | # Need `--pre` for packages like pandas having no released version supporting numpy>=2.0 yet, 51 | # + `--no-deps` for casa-formats-io having no branch for numpy>=2.0 - 52 | # for as long all test deps need to be manually pulled in above as well! 53 | install_command = 54 | !dev: python -I -m pip install 55 | dev: python -I -m pip install -v 56 | commands = 57 | test: pip freeze 58 | test: pytest --pyargs glue --cov glue --cov-config={toxinidir}/setup.cfg {env:MPLFLAGS} {posargs} 59 | docs: sphinx-build -W -n -b html -d _build/doctrees . _build/html 60 | 61 | [testenv:codestyle] 62 | skipsdist = true 63 | skip_install = true 64 | description = Run all style and file checks with pre-commit 65 | deps = 66 | pre-commit 67 | commands = 68 | pre-commit install-hooks 69 | pre-commit run --color always --all-files --show-diff-on-failure 70 | --------------------------------------------------------------------------------