diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 000000000..ec08fb760
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,92 @@
+version: 2.1
+
+jobs:
+
+ # The following job is to run any visual comparison test, and runs on any branch
+ # or in any pull request. It will generate a summary page for each tox environment
+ # being run which is accessible through the CircleCI artifacts.
+
+ visual:
+ parameters:
+ jobname:
+ type: string
+ docker:
+ - image: cimg/python:3.11
+ environment:
+ TOXENV: << parameters.jobname >>
+ steps:
+ - checkout
+ - run:
+ name: Install dependencies
+ command: |
+ sudo apt update
+ pip install pip tox --upgrade
+ - run:
+ name: Run tests
+ command: tox -v
+ - store_artifacts:
+ path: results
+ - run:
+ name: "Image comparison page is available at: "
+ command: echo "${CIRCLE_BUILD_URL}/artifacts/${CIRCLE_NODE_INDEX}/results/fig_comparison.html"
+
+ # The following job runs only on main - and its main purpose is to update the
+ # reference images in the glue-core-visual-tests repository. This job needs
+ # a deploy key. To produce this, go to the glue-core-visual-tests
+ # repository settings and go to SSH keys, then add your public SSH key.
+ deploy-reference-images:
+ parameters:
+ jobname:
+ type: string
+ docker:
+ - image: cimg/python:3.11
+ environment:
+ TOXENV: << parameters.jobname >>
+ steps:
+ - checkout
+ - run:
+ name: Install dependencies
+ command: |
+ sudo apt update
+ pip install pip tox --upgrade
+ - run: ssh-add -D
+ - add_ssh_keys:
+ fingerprints: "44:09:69:d7:c6:77:25:e9:46:da:f1:22:7d:d4:38:29"
+ - run: ssh-keyscan github.com >> ~/.ssh/known_hosts
+ - run: git config --global user.email "glue@circleci" && git config --global user.name "Glue Circle CI"
+ - run: git clone git@github.com:glue-viz/glue-core-visual-tests.git --depth 1 ~/glue-core-visual-tests/
+ - run:
+ name: Generate reference images
+ command: tox -v -- --mpl-generate-path=/home/circleci/glue-core-visual-tests/images/$TOXENV
+ - run: |
+ cd ~/glue-core-visual-tests/
+ git pull
+ git status
+ git add .
+ git commit -m "Update reference images from ${CIRCLE_BRANCH}" || echo "No changes to reference images to deploy"
+ git push
+
+workflows:
+ version: 2
+
+ visual-tests:
+ jobs:
+ - visual:
+ name: << matrix.jobname >>
+ matrix:
+ parameters:
+ jobname:
+ - "py311-test-visual"
+
+ - deploy-reference-images:
+ name: baseline-<< matrix.jobname >>
+ matrix:
+ parameters:
+ jobname:
+ - "py311-test-visual"
+ requires:
+ - << matrix.jobname >>
+ filters:
+ branches:
+ only:
+ - main
diff --git a/.gitignore b/.gitignore
index 37c6fda52..bb776a810 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,5 @@ glue/_githash.py
.vscode
# vscode plugin
.history
+
+results
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 309f6d1bf..2d9052f0f 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,7 +1,9 @@
version: 2
build:
- image: latest
+ os: "ubuntu-22.04"
+ tools:
+ python: "3"
sphinx:
builder: html
@@ -9,7 +11,6 @@ sphinx:
fail_on_warning: true
python:
- version: 3.8
install:
- method: pip
path: .
diff --git a/CHANGES.md b/CHANGES.md
index 1c8b12b22..d7b798043 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,115 @@
# Full changelog
+## v1.18.0 - 2024-03-26
+
+
+### What's Changed
+
+#### New Features
+
+* Preserve alpha while compositing with cmap.set_bad by @bmorris3 in https://github.com/glue-viz/glue/pull/2468
+
+#### Other Changes
+
+* Remove deprecated Qt-related code (and `glue.qglue`) by @astrofrog in https://github.com/glue-viz/glue/pull/2477
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.17.1...v1.18.0
+
+## v1.17.1 - 2023-12-07
+
+
+### What's Changed
+
+#### Bug Fixes
+
+* Fix default stretch density by @astrofrog in https://github.com/glue-viz/glue/pull/2467
+* Provide stronger checking for region display. Fix x/y flip. by @jfoster17 in https://github.com/glue-viz/glue/pull/2465
+
+#### Other Changes
+
+* Fix documentation build by @astrofrog in https://github.com/glue-viz/glue/pull/2466
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.17.0...v1.17.1
+
+## v1.17.0 - 2023-12-05
+
+
+### What's Changed
+
+#### New Features
+
+* Make stretches be customizable on a layer by layer basis by @astrofrog in https://github.com/glue-viz/glue/pull/2453
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.16.0...v1.17.0
+
+## v1.16.0 - 2023-11-16
+
+
+### What's Changed
+
+#### New Features
+
+- Customizable limits percentile and behavior on updating bins when resetting limits by @astrofrog in https://github.com/glue-viz/glue/pull/2455
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.15.0...v1.16.0
+
+## v1.15.0 - 2023-11-14
+
+
+### What's Changed
+
+#### New Features
+
+- Support regions in image viewer by @jfoster17 in https://github.com/glue-viz/glue/pull/2456
+- Added 'simple' viewers which can be used for testing or simple use cases by @astrofrog in https://github.com/glue-viz/glue/pull/2458
+
+#### Other Changes
+
+- Added giles configuration by @astrofrog in https://github.com/glue-viz/glue/pull/2460
+- Add visual test for ScatterRegionLayerArtist by @jfoster17 in https://github.com/glue-viz/glue/pull/2461
+- Ignore all test files in coverage calculation by @astrofrog in https://github.com/glue-viz/glue/pull/2462
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.14.1...v1.15.0
+
+## v1.14.1 - 2023-10-26
+
+
+### What's Changed
+
+#### Bug Fixes
+
+- Fix bug when changing the number of bins on a histogram with modified/deleted data by @astrofrog in https://github.com/glue-viz/glue/pull/2451
+
+#### Documentation
+
+- Fix docs failure related to theme warning by @astrofrog in https://github.com/glue-viz/glue/pull/2452
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.14.0...v1.14.1
+
+## v1.14.0 - 2023-10-23
+
+
+### What's Changed
+
+#### New Features
+
+- Support dynamic components by @jfoster17 in https://github.com/glue-viz/glue/pull/2446
+- Add regiondata by @jfoster17 in https://github.com/glue-viz/glue/pull/2442
+
+#### Bug Fixes
+
+- Render nans as 'bad' in mpl cmaps when mode='colormap' by @bmorris3 in https://github.com/glue-viz/glue/pull/2427
+- Update deprecated call to resize_event in test by @jfoster17 in https://github.com/glue-viz/glue/pull/2443
+- Do not use fancy index for subsets over Dask data by @jfoster17 in https://github.com/glue-viz/glue/pull/2444
+- Fix bug that caused incompatible subsets to be rendered in some contexts rather than be hidden by @jfoster17 in https://github.com/glue-viz/glue/pull/2425
+
+#### Other Changes
+
+- Switch to Sphinx book theme and tidied up Sphinx configuration by @astrofrog in https://github.com/glue-viz/glue/pull/2436
+- Make tests robust to plugins adding new translators by @jfoster17 in https://github.com/glue-viz/glue/pull/2445
+
+**Full Changelog**: https://github.com/glue-viz/glue/compare/v1.13.1...v1.14.0
+
## v1.13.1 - 2023-08-17
@@ -683,86 +793,86 @@
### What's Changed
-- Improved how we handle equal aspect ratio to not depend on
+- Improved how we handle equal aspect ratio to not depend on
-- Matplotlib. https://github.com/glue-viz/glue/pull/1894
+- Matplotlib. https://github.com/glue-viz/glue/pull/1894
-
-- Avoid showing a warning when closing an empty tab. https://github.com/glue-viz/glue/pull/1890
+- Avoid showing a warning when closing an empty tab. https://github.com/glue-viz/glue/pull/1890
-
-- Fix bug that caused component arithmetic to not work if
+- Fix bug that caused component arithmetic to not work if
-- Numpy was imported in user's config.py file. https://github.com/glue-viz/glue/pull/1887
+- Numpy was imported in user's config.py file. https://github.com/glue-viz/glue/pull/1887
-
-- Added the ability to define custom layer artist makers to
+- Added the ability to define custom layer artist makers to
-- override default layer artists in viewers. https://github.com/glue-viz/glue/pull/1850
+- override default layer artists in viewers. https://github.com/glue-viz/glue/pull/1850
-
-- Fix Plot.ly exporter for categorical components and histogram
+- Fix Plot.ly exporter for categorical components and histogram
-- viewer. https://github.com/glue-viz/glue/pull/1886
+- viewer. https://github.com/glue-viz/glue/pull/1886
-
-- Fix issues with reading very large FITS files on some systems. https://github.com/glue-viz/glue/pull/1884
+- Fix issues with reading very large FITS files on some systems. https://github.com/glue-viz/glue/pull/1884
-
-- Added documentation about plugins. https://github.com/glue-viz/glue/pull/1837
+- Added documentation about plugins. https://github.com/glue-viz/glue/pull/1837
-
-- Better isolate code related to pixel selection tool in image
+- Better isolate code related to pixel selection tool in image
-- viewer that depended on Qt. https://github.com/glue-viz/glue/pull/1763
+- viewer that depended on Qt. https://github.com/glue-viz/glue/pull/1763
-
-- Improve handling of units in FITS files. https://github.com/glue-viz/glue/pull/1723
+- Improve handling of units in FITS files. https://github.com/glue-viz/glue/pull/1723
-
-- Added documentation about creating viewers for glue using the
+- Added documentation about creating viewers for glue using the
-- new state-based infrastructure. https://github.com/glue-viz/glue/pull/1740
+- new state-based infrastructure. https://github.com/glue-viz/glue/pull/1740
-
-- Make it possible to pass the initial state of a viewer to an
+- Make it possible to pass the initial state of a viewer to an
-- application's `new_data_viewer` method. https://github.com/glue-viz/glue/pull/1877
+- application's `new_data_viewer` method. https://github.com/glue-viz/glue/pull/1877
-
-- Ensure that glue can be imported if QtPy is installed but PyQt
+- Ensure that glue can be imported if QtPy is installed but PyQt
-- and PySide aren't. [#1865, #1836]
+- and PySide aren't. [#1865, #1836]
-
-- Fix unit display for coordinates from WCS headers that don't have
+- Fix unit display for coordinates from WCS headers that don't have
-- CTYPE but have CUNIT. https://github.com/glue-viz/glue/pull/1856
+- CTYPE but have CUNIT. https://github.com/glue-viz/glue/pull/1856
-
-- Enable tab completion on Data objects. https://github.com/glue-viz/glue/pull/1874
+- Enable tab completion on Data objects. https://github.com/glue-viz/glue/pull/1874
-
-- Automatically select datasets in link editor if there are only two. https://github.com/glue-viz/glue/pull/1837
+- Automatically select datasets in link editor if there are only two. https://github.com/glue-viz/glue/pull/1837
-
-- Change 'Export Session' dialog to offer to save with relative paths to data
+- Change 'Export Session' dialog to offer to save with relative paths to data
-- by default instead of absolute paths. https://github.com/glue-viz/glue/pull/1803
+- by default instead of absolute paths. https://github.com/glue-viz/glue/pull/1803
-
-- Added a new method `screenshot` on `GlueApplication` to save a
+- Added a new method `screenshot` on `GlueApplication` to save a
-- screenshot of the current view. https://github.com/glue-viz/glue/pull/1808
+- screenshot of the current view. https://github.com/glue-viz/glue/pull/1808
-
-- Show the active subset in the toolbar. https://github.com/glue-viz/glue/pull/1797
+- Show the active subset in the toolbar. https://github.com/glue-viz/glue/pull/1797
-
-- Refactored the viewer class base classes https://github.com/glue-viz/glue/pull/1746:
+- Refactored the viewer class base classes https://github.com/glue-viz/glue/pull/1746:
-
-- - `glue.core.application_base.ViewerBase` has been removed in favor of
+- - `glue.core.application_base.ViewerBase` has been removed in favor of
-
-
@@ -774,7 +884,7 @@
-
-
-
-- - `glue.viewers.common.viewer.BaseViewer` and
+- - `glue.viewers.common.viewer.BaseViewer` and
-
-
@@ -786,7 +896,7 @@
-
-
-
-- - `glue.viewers.common.viewer.Viewer`.
+- - `glue.viewers.common.viewer.Viewer`.
-
-
@@ -798,7 +908,7 @@
-
-
-
-- -
+- -
-
-
@@ -810,7 +920,7 @@
-
-
-
-- - `glue.viewers.common.viewer.Viewer` is now where the base logic is defined
+- - `glue.viewers.common.viewer.Viewer` is now where the base logic is defined
-
-
@@ -822,7 +932,7 @@
-
-
-
-- - for using state classes in viewers (instead of
+- - for using state classes in viewers (instead of
-
-
@@ -834,7 +944,7 @@
-
-
-
-- - `glue.viewers.common.qt.DataViewerWithState`).
+- - `glue.viewers.common.qt.DataViewerWithState`).
-
-
@@ -846,7 +956,7 @@
-
-
-
-- -
+- -
-
-
@@ -858,7 +968,7 @@
-
-
-
-- - `glue.viewers.common.qt.DataViewerWithState` is now deprecated.
+- - `glue.viewers.common.qt.DataViewerWithState` is now deprecated.
-
-
@@ -870,7 +980,7 @@
-
-
-
-- -
+- -
-
-
@@ -883,106 +993,106 @@
-
-
-
-- Make it so that the modest image only resamples the data when the
+- Make it so that the modest image only resamples the data when the
-- mouse is no longer pressed - this avoids too many refreshes when
+- mouse is no longer pressed - this avoids too many refreshes when
-- panning/zooming. https://github.com/glue-viz/glue/pull/1866
+- panning/zooming. https://github.com/glue-viz/glue/pull/1866
-
-- Make it possible to unglue multiple links in one go. https://github.com/glue-viz/glue/pull/1809
+- Make it possible to unglue multiple links in one go. https://github.com/glue-viz/glue/pull/1809
-
-- Make it so that adding a subset to a viewer no longer adds the
+- Make it so that adding a subset to a viewer no longer adds the
-- associated data, since in some cases the viewer can handle the
+- associated data, since in some cases the viewer can handle the
-- subset size, but not the full data. https://github.com/glue-viz/glue/pull/1807
+- subset size, but not the full data. https://github.com/glue-viz/glue/pull/1807
-
-- Defined a new abstract base class for all datasets, `BaseData`,
+- Defined a new abstract base class for all datasets, `BaseData`,
-- and a base class `BaseCartesianData`,
+- and a base class `BaseCartesianData`,
-- which can be used to implement interfaces to datasets that may be
+- which can be used to implement interfaces to datasets that may be
-- remote or may not be stored as regular cartesian data. https://github.com/glue-viz/glue/pull/1768
+- remote or may not be stored as regular cartesian data. https://github.com/glue-viz/glue/pull/1768
-
-- Add a new method `Data.compute_statistic` which can be used
+- Add a new method `Data.compute_statistic` which can be used
-- to find scalar and array statistics on the data, and use for
+- to find scalar and array statistics on the data, and use for
-- the profile viewer and the state limits helpers. https://github.com/glue-viz/glue/pull/1737
+- the profile viewer and the state limits helpers. https://github.com/glue-viz/glue/pull/1737
-
-- Add a new method `Data.compute_histogram` which can be used
+- Add a new method `Data.compute_histogram` which can be used
-- to find histograms of specific components, with or without
+- to find histograms of specific components, with or without
-- subsets applied. https://github.com/glue-viz/glue/pull/1739
+- subsets applied. https://github.com/glue-viz/glue/pull/1739
-
-- Removed `Data.get_pixel_component_ids` and `Data.get_world_component_ids`
+- Removed `Data.get_pixel_component_ids` and `Data.get_world_component_ids`
-- in favor of `Data.pixel_component_ids` and `Data.world_component_ids`.
+- in favor of `Data.pixel_component_ids` and `Data.world_component_ids`.
-- https://github.com/glue-viz/glue/pull/1784
+- https://github.com/glue-viz/glue/pull/1784
-
-- Deprecated `Data.visible_components` and `Data.primary_components`. https://github.com/glue-viz/glue/pull/1788
+- Deprecated `Data.visible_components` and `Data.primary_components`. https://github.com/glue-viz/glue/pull/1788
-
-- Speed up histogram calculations by using the fast-histogram package instead of
+- Speed up histogram calculations by using the fast-histogram package instead of
-- np.histogram. https://github.com/glue-viz/glue/pull/1806
+- np.histogram. https://github.com/glue-viz/glue/pull/1806
-
-- In the case of categorical attributes, `Data[name]` now returns a
+- In the case of categorical attributes, `Data[name]` now returns a
-- `categorical_ndarray` object rather than the indices of the categories. You
+- `categorical_ndarray` object rather than the indices of the categories. You
-- can access the indices with `Data[name].codes` and the unique categories
+- can access the indices with `Data[name].codes` and the unique categories
-- with `Data[name].categories`. https://github.com/glue-viz/glue/pull/1784
+- with `Data[name].categories`. https://github.com/glue-viz/glue/pull/1784
-
-- Compute profiles and histograms asynchronously when dataset is large
+- Compute profiles and histograms asynchronously when dataset is large
-- to avoid holding up the UI, and compute profiles in chunks to avoid
+- to avoid holding up the UI, and compute profiles in chunks to avoid
-- excessive memory usage. [#1736, #1764]
+- excessive memory usage. [#1736, #1764]
-
-- Improved naming of components when merging datasets. https://github.com/glue-viz/glue/pull/1249
+- Improved naming of components when merging datasets. https://github.com/glue-viz/glue/pull/1249
-
-- Fixed an issue that caused residual references to viewers
+- Fixed an issue that caused residual references to viewers
-- after they were closed if they were accessed through the
+- after they were closed if they were accessed through the
-- IPython console. https://github.com/glue-viz/glue/pull/1770
+- IPython console. https://github.com/glue-viz/glue/pull/1770
-
-- Don't show layer edit options if layer is not visible. https://github.com/glue-viz/glue/pull/1805
+- Don't show layer edit options if layer is not visible. https://github.com/glue-viz/glue/pull/1805
-
-- Make the Matplotlib viewer code that doesn't depend on Qt accessible
+- Make the Matplotlib viewer code that doesn't depend on Qt accessible
-- to non-Qt frontends. https://github.com/glue-viz/glue/pull/1841
+- to non-Qt frontends. https://github.com/glue-viz/glue/pull/1841
-
-- Avoid repeated coordinate components in merged datasets. https://github.com/glue-viz/glue/pull/1792
+- Avoid repeated coordinate components in merged datasets. https://github.com/glue-viz/glue/pull/1792
-
-- Fix bug that caused new subset to be created when dragging an existing
+- Fix bug that caused new subset to be created when dragging an existing
-- subset in an image viewer. https://github.com/glue-viz/glue/pull/1793
+- subset in an image viewer. https://github.com/glue-viz/glue/pull/1793
-
-- Better preserve data types when exporting data/subsets to FITS
+- Better preserve data types when exporting data/subsets to FITS
-- and HDF5 formats. https://github.com/glue-viz/glue/pull/1800
+- and HDF5 formats. https://github.com/glue-viz/glue/pull/1800
-
diff --git a/README.rst b/README.rst
index a0a9d020d..421b92126 100644
--- a/README.rst
+++ b/README.rst
@@ -12,8 +12,8 @@ This repository contains the **glue-core** package which includes much of the co
functionality of glue that is used for the different front-ends, including the Qt-based
application and the Jupyter-based application. Other key repositories include:
-* `glue-qt `_: the original Qt/desktop application for glue
-* `glue-jupyter `_: a Jupyter front-end for glue
+* `glue-qt `_: the original Qt/desktop application for glue
+* `glue-jupyter `_: a Jupyter front-end for glue
In addition to these, there are a number of plugin packages available. For a full list of repositories,
see https://github.com/glue-viz/.
diff --git a/doc/conf.py b/doc/conf.py
index bfac95830..e75410542 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -33,6 +33,7 @@
("py:class", "glue.viewers.image.layer_artist.ImageLayerBase"),
("py:class", "glue.viewers.image.layer_artist.RGBImageLayerBase"),
("py:class", "glue.viewers.image.state.BaseImageLayerState"),
+ ("py:class", "glue.viewers.common.stretch_state_mixin.StretchStateMixin")
]
viewcode_follow_imported_members = False
@@ -53,6 +54,7 @@
"astropy": ("https://docs.astropy.org/en/stable/", None),
"echo": ("https://echo.readthedocs.io/en/latest/", None),
"pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
+ "shapely": ("https://shapely.readthedocs.io/en/stable/", None),
}
# -- Options for HTML output -------------------------------------------------
@@ -61,3 +63,4 @@
html_theme = "sphinx_book_theme"
html_static_path = ["_static"]
html_logo = "_static/logo.png"
+html_theme_options = {'navigation_with_keys': False}
diff --git a/glue/__init__.py b/glue/__init__.py
index 8e0a8839e..91883381d 100644
--- a/glue/__init__.py
+++ b/glue/__init__.py
@@ -1,11 +1,10 @@
# Set up configuration variables
-__all__ = ['custom_viewer', 'qglue', 'test']
+__all__ = ['custom_viewer', 'test']
import os
import sys
-import warnings
import importlib.metadata
@@ -15,19 +14,11 @@
sys.meta_path.append(MatplotlibBackendSetter())
from glue.viewers.custom.helper import custom_viewer
-from glue.utils.error import GlueDeprecationWarning
# Load user's configuration file
from .config import load_configuration
env = load_configuration()
-
-def qglue(*args, **kwargs):
- warnings.warn('glue.qglue is deprecated, import qglue from the glue_qt module instead', GlueDeprecationWarning)
- from glue_qt import qglue
- return qglue(*args, **kwargs)
-
-
from .main import load_plugins # noqa
diff --git a/glue/_mpl_backend.py b/glue/_mpl_backend.py
index 0069c3fff..1b464a732 100644
--- a/glue/_mpl_backend.py
+++ b/glue/_mpl_backend.py
@@ -4,7 +4,7 @@
class MatplotlibBackendSetter(object):
"""
- Import hook to make sure the proper Qt backend is set when importing
+ Import hook to make sure the proper backend is set when importing
Matplotlib.
"""
diff --git a/glue/app/qt/__init__.py b/glue/app/qt/__init__.py
deleted file mode 100644
index c8a89500b..000000000
--- a/glue/app/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt is deprecated, use glue_qt.app instead', GlueDeprecationWarning)
-from glue_qt.app import * # noqa
diff --git a/glue/app/qt/actions.py b/glue/app/qt/actions.py
deleted file mode 100644
index 070bb1d7b..000000000
--- a/glue/app/qt/actions.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.actions is deprecated, use glue_qt.app.actions instead', GlueDeprecationWarning)
-from glue_qt.app.actions import * # noqa
diff --git a/glue/app/qt/application.py b/glue/app/qt/application.py
deleted file mode 100644
index 4f59db281..000000000
--- a/glue/app/qt/application.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.application is deprecated, use glue_qt.app.application instead', GlueDeprecationWarning)
-from glue_qt.app.application import * # noqa
diff --git a/glue/app/qt/edit_subset_mode_toolbar.py b/glue/app/qt/edit_subset_mode_toolbar.py
deleted file mode 100644
index 417a14ea3..000000000
--- a/glue/app/qt/edit_subset_mode_toolbar.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.edit_subset_mode_toolbar is deprecated, use glue_qt.app.edit_subset_mode_toolbar instead', GlueDeprecationWarning)
-from glue_qt.app.edit_subset_mode_toolbar import * # noqa
diff --git a/glue/app/qt/feedback.py b/glue/app/qt/feedback.py
deleted file mode 100644
index 0750eb2bb..000000000
--- a/glue/app/qt/feedback.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.feedback is deprecated, use glue_qt.app.feedback instead', GlueDeprecationWarning)
-from glue_qt.app.feedback import * # noqa
diff --git a/glue/app/qt/keyboard_shortcuts.py b/glue/app/qt/keyboard_shortcuts.py
deleted file mode 100644
index 5c3917eee..000000000
--- a/glue/app/qt/keyboard_shortcuts.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.keyboard_shortcuts is deprecated, use glue_qt.app.keyboard_shortcuts instead', GlueDeprecationWarning)
-from glue_qt.app.keyboard_shortcuts import * # noqa
diff --git a/glue/app/qt/layer_tree_widget.py b/glue/app/qt/layer_tree_widget.py
deleted file mode 100644
index c1d0b0910..000000000
--- a/glue/app/qt/layer_tree_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.layer_tree_widget is deprecated, use glue_qt.app.layer_tree_widget instead', GlueDeprecationWarning)
-from glue_qt.app.layer_tree_widget import * # noqa
diff --git a/glue/app/qt/mdi_area.py b/glue/app/qt/mdi_area.py
deleted file mode 100644
index 54092e4ab..000000000
--- a/glue/app/qt/mdi_area.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.mdi_area is deprecated, use glue_qt.app.mdi_area instead', GlueDeprecationWarning)
-from glue_qt.app.mdi_area import * # noqa
diff --git a/glue/app/qt/metadata.py b/glue/app/qt/metadata.py
deleted file mode 100644
index 44ace5293..000000000
--- a/glue/app/qt/metadata.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.metadata is deprecated, use glue_qt.app.metadata instead', GlueDeprecationWarning)
-from glue_qt.app.metadata import * # noqa
diff --git a/glue/app/qt/plugin_manager.py b/glue/app/qt/plugin_manager.py
deleted file mode 100644
index 7ba92f677..000000000
--- a/glue/app/qt/plugin_manager.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.plugin_manager is deprecated, use glue_qt.app.plugin_manager instead', GlueDeprecationWarning)
-from glue_qt.app.plugin_manager import * # noqa
diff --git a/glue/app/qt/preferences.py b/glue/app/qt/preferences.py
deleted file mode 100644
index 30383ddba..000000000
--- a/glue/app/qt/preferences.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.preferences is deprecated, use glue_qt.app.preferences instead', GlueDeprecationWarning)
-from glue_qt.app.preferences import * # noqa
diff --git a/glue/app/qt/save_data.py b/glue/app/qt/save_data.py
deleted file mode 100644
index b755a9430..000000000
--- a/glue/app/qt/save_data.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.save_data is deprecated, use glue_qt.app.save_data instead', GlueDeprecationWarning)
-from glue_qt.app.save_data import * # noqa
diff --git a/glue/app/qt/splash_screen.py b/glue/app/qt/splash_screen.py
deleted file mode 100644
index 6cf0f7315..000000000
--- a/glue/app/qt/splash_screen.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.splash_screen is deprecated, use glue_qt.app.splash_screen instead', GlueDeprecationWarning)
-from glue_qt.app.splash_screen import * # noqa
diff --git a/glue/app/qt/terminal.py b/glue/app/qt/terminal.py
deleted file mode 100644
index eae551147..000000000
--- a/glue/app/qt/terminal.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.terminal is deprecated, use glue_qt.app.terminal instead', GlueDeprecationWarning)
-from glue_qt.app.terminal import * # noqa
diff --git a/glue/app/qt/versions.py b/glue/app/qt/versions.py
deleted file mode 100644
index cabf25c3d..000000000
--- a/glue/app/qt/versions.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.app.qt.versions is deprecated, use glue_qt.app.versions instead', GlueDeprecationWarning)
-from glue_qt.app.versions import * # noqa
diff --git a/glue/config.py b/glue/config.py
index 24a1129b8..0c970e642 100644
--- a/glue/config.py
+++ b/glue/config.py
@@ -836,10 +836,10 @@ def __iter__(self):
from astropy.visualization import (LinearStretch, SqrtStretch, AsinhStretch,
LogStretch)
stretches = StretchRegistry()
-stretches.add('linear', LinearStretch(), display='Linear')
-stretches.add('sqrt', SqrtStretch(), display='Square Root')
-stretches.add('arcsinh', AsinhStretch(), display='Arcsinh')
-stretches.add('log', LogStretch(), display='Logarithmic')
+stretches.add('linear', LinearStretch, display='Linear')
+stretches.add('sqrt', SqrtStretch, display='Square Root')
+stretches.add('arcsinh', AsinhStretch, display='Arcsinh')
+stretches.add('log', LogStretch, display='Logarithmic')
# Backward-compatibility
qglue_parser = cli_parser
diff --git a/glue/conftest.py b/glue/conftest.py
index 73a745cf3..e0e0251b1 100644
--- a/glue/conftest.py
+++ b/glue/conftest.py
@@ -1,24 +1,8 @@
import os
import sys
-import warnings
-
-import pytest
-
-try:
- from qtpy import PYSIDE2
-except Exception:
- PYSIDE2 = False
from glue.config import CFG_DIR as CFG_DIR_ORIG
-try:
- import objgraph
-except ImportError:
- OBJGRAPH_INSTALLED = False
-else:
- OBJGRAPH_INSTALLED = True
-
-
STDERR_ORIGINAL = sys.stderr
ON_APPVEYOR = os.environ.get('APPVEYOR', 'False') == 'True'
@@ -73,61 +57,3 @@ def pytest_unconfigure(config):
# Reset configuration directory to original one
from glue import config
config.CFG_DIR = CFG_DIR_ORIG
-
- # Remove reference to QApplication to prevent segmentation fault on PySide
- try:
- from glue.utils.qt import app
- app.qapp = None
- except Exception: # for when we run the tests without the qt directories
- # Note that we catch any exception, not just ImportError, because
- # QtPy can raise a PythonQtError.
- pass
-
- if OBJGRAPH_INSTALLED and not ON_APPVEYOR:
-
- # Make sure there are no lingering references to GlueApplication
- obj = objgraph.by_type('GlueApplication')
- if len(obj) > 0:
- objgraph.show_backrefs(objgraph.by_type('GlueApplication'))
- warnings.warn('There are {0} remaining references to GlueApplication'.format(len(obj)))
-
- # Uncomment when checking for memory leaks
- # objgraph.show_most_common_types(limit=100)
-
-
-# With PySide2, tests can fail in a non-deterministic way on a teardown error
-# or with the following error:
-#
-# AttributeError: 'PySide2.QtGui.QStandardItem' object has no attribute '...'
-#
-# Until this can be properly debugged and fixed, we xfail any test that fails
-# with one of these exceptions.
-
-if PYSIDE2:
- QTSTANDARD_EXC = "QtGui.QStandardItem' object has no attribute "
- QTSTANDARD_ATTRS = ["'connect'", "'item'", "'triggered'"]
-
- @pytest.hookimpl(hookwrapper=True)
- def pytest_runtest_setup():
- try:
- outcome = yield
- return outcome.get_result()
- except AttributeError:
- exc = str(outcome.excinfo[1])
- for attr in QTSTANDARD_ATTRS:
- if QTSTANDARD_EXC + attr in exc:
- pytest.xfail(f'Known issue {exc}')
-
- @pytest.hookimpl(hookwrapper=True)
- def pytest_runtest_call():
- try:
- outcome = yield
- return outcome.get_result()
- # excinfo seems only to be preserved through a single hook
- except (AttributeError, ValueError):
- exc = str(outcome.excinfo[1])
- if "No net viewers should be created in tests" in exc:
- pytest.xfail(f'Known issue {exc}')
- for attr in QTSTANDARD_ATTRS:
- if QTSTANDARD_EXC + attr in exc:
- pytest.xfail(f'Known issue {exc}')
diff --git a/glue/core/application_base.py b/glue/core/application_base.py
index 8c755814c..1f17683de 100644
--- a/glue/core/application_base.py
+++ b/glue/core/application_base.py
@@ -93,6 +93,9 @@ def new_data_viewer(self, viewer_class, data=None, state=None):
self.add_widget(c)
return c
+ def add_widget(self, viewer):
+ pass
+
@catch_error("Failed to save session")
def save_session(self, path, include_data=False, absolute_paths=True):
"""
diff --git a/glue/core/component.py b/glue/core/component.py
index 9857b5e37..fe7202b46 100644
--- a/glue/core/component.py
+++ b/glue/core/component.py
@@ -2,6 +2,7 @@
import numpy as np
import pandas as pd
+import shapely
from glue.core.coordinate_helpers import dependent_axes, pixel2world_single_axis
from glue.utils import shape_to_string, coerce_numeric, categorical_ndarray
@@ -13,7 +14,7 @@
DASK_INSTALLED = False
__all__ = ['Component', 'DerivedComponent', 'CategoricalComponent',
- 'CoordinateComponent', 'DateTimeComponent']
+ 'CoordinateComponent', 'DateTimeComponent', 'ExtendedComponent']
class Component(object):
@@ -107,6 +108,13 @@ def datetime(self):
"""
return False
+ @property
+ def extended(self):
+ """
+ Whether or not or not the datatype represents an extended region
+ """
+ return False
+
def __str__(self):
return "%s with shape %s" % (self.__class__.__name__, shape_to_string(self.shape))
@@ -549,3 +557,98 @@ def categorical(self):
@property
def datetime(self):
return False
+
+
+class ExtendedComponent(Component):
+ """
+ A data component representing an extent or a region.
+
+ This component can be used when a dataset describes regions or ranges
+ and is typically used with a `RegionData` object, since that object
+ provides helper functions to display regions on viewers. For example,
+ a `RegionData` object might provide properties of geographic
+ regions, and the boundaries of these regions would be an ExtendedComponent.
+
+ Data loaders are required to know how to convert regions to a list
+ of Shapely objects which can be used to initialize an ExtendedComponent.
+
+ A circular region can be represented as:
+
+ >>> circle = shapely.Point(x, y).buffer(rad)
+
+ A range in one dimension can be represented as:
+
+ >>> range = shapely.LineString([[x0,0],[x1,0]])
+
+ (This is a bit of an odd representation, since we are forced to specify a y
+ coordinate for this line. We adopt a convention of y == 0.)
+
+ ExtendedComponents are NOT used directly in linking. Instead, ExtendedComponents
+ always have corresponding ComponentIDs that represent the x (and y) coordinates
+ over which the regions are defined. If not specified otherwise, a
+ `RegionData` object will create 'representative points'
+ for each region, representing a point near the center of the reigon that is
+ guaranteed to be inside the region.
+
+ NOTE: that this implementation does not support regions in more than
+ two dimensions. (Shapely has limited support for 3D shapes, but not more).
+
+ Parameters
+ ----------
+ data : list of `shapely.Geometry`` objects
+ The data to store.
+ center_comp_ids : list of :class:`glue.core.component_id.ComponentID` objects
+ The ComponentIDs of the `center` of the extended region. These do not
+ have to be the literal center of the region, but they must be in the x (and y)
+ coordinates of the regions. These ComponentIDs are used in the linking
+ framework to allow an ExtendedComponent to be linked to other components.
+ units : `str`, optional
+ Unit description.
+
+ Attributes
+ ----------
+ x : ComponentID
+ The ComponentID of the x coordinate at the center of the extended region.
+ y : ComponentID
+ The ComponentID of the y coordinate at the center of the extended region.
+
+ Raises
+ ------
+ TypeError
+ If data is not a list of ``shapely.Geometry`` objects
+ ValueError
+ If center_comp_ids is not a list of length 1 or 2
+ """
+ def __init__(self, data, center_comp_ids, units=None):
+ if not all(isinstance(s, shapely.Geometry) for s in data):
+ raise TypeError(
+ "Input data for a ExtendedComponent should be a list of shapely.Geometry objects"
+ )
+ if len(center_comp_ids) == 2:
+ self.x = center_comp_ids[0]
+ self.y = center_comp_ids[1]
+ elif len(center_comp_ids) == 1:
+ self.x = center_comp_ids[0]
+ self.y = None
+ else:
+ raise ValueError(
+ "ExtendedComponent must be initialized with one or two ComponentIDs"
+ )
+ self.units = units
+ self._data = data
+
+ @property
+ def extended(self):
+ return True
+
+ @property
+ def numeric(self):
+ return False
+
+ @property
+ def datetime(self):
+ return False
+
+ @property
+ def categorical(self):
+ return False
diff --git a/glue/core/data.py b/glue/core/data.py
index 5293a488b..6313eace5 100644
--- a/glue/core/data.py
+++ b/glue/core/data.py
@@ -1427,6 +1427,8 @@ def get_kind(self, cid):
return 'numerical'
elif comp.categorical:
return 'categorical'
+ elif comp.extended:
+ return 'extended'
else:
raise TypeError("Unknown data kind")
@@ -1543,7 +1545,7 @@ def update_components(self, mapping):
# alert hub of the change
if self.hub is not None:
- msg = NumericalDataChangedMessage(self)
+ msg = NumericalDataChangedMessage(self, components_changed=list(mapping.keys()))
self.hub.broadcast(msg)
for subset in self.subsets:
diff --git a/glue/core/data_exporters/qt/__init__.py b/glue/core/data_exporters/qt/__init__.py
deleted file mode 100644
index 87cca1e92..000000000
--- a/glue/core/data_exporters/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.data_exporters.qt is deprecated, use glue_qt.core.data_exporters instead', GlueDeprecationWarning)
-from glue_qt.core.data_exporters import * # noqa
diff --git a/glue/core/data_exporters/qt/dialog.py b/glue/core/data_exporters/qt/dialog.py
deleted file mode 100644
index 88e4fb38f..000000000
--- a/glue/core/data_exporters/qt/dialog.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.data_exporters.qt.dialog is deprecated, use glue_qt.core.data_exporters.dialog instead', GlueDeprecationWarning)
-from glue_qt.core.data_exporters.dialog import * # noqa
diff --git a/glue/core/data_region.py b/glue/core/data_region.py
new file mode 100644
index 000000000..64d6abd8a
--- /dev/null
+++ b/glue/core/data_region.py
@@ -0,0 +1,326 @@
+import numpy as np
+import shapely
+
+from glue.core.data import Data
+from glue.core.contracts import contract
+
+from glue.core.component import Component, ExtendedComponent
+
+
+__all__ = ['RegionData']
+
+
+class RegionData(Data):
+ """
+ A glue Data object for storing data that is associated with a region.
+
+ This object can be used when a dataset describes 2D regions or 1D ranges. It
+ contains exactly one :class:`~glue.core.component.ExtendedComponent` object
+ which contains the boundaries of the regions, and must also contain
+ one or two components that give the center of the regions in whatever data
+ coordinates the regions are described in. Links in glue are not made
+ directly on the :class:`~glue.core.component.ExtendedComponent`, but instead
+ on the center components.
+
+ Thus, a subset that includes the center of a region will include that region,
+ but a subset that includes just a little part of the region will not include
+ that region. These center components are not the same pixel components. For
+ example, a dataset that is a table of 2D regions will have a single
+ :class:`~glue.core.component.CoordinateComponent`, but must have two of these center
+ components.
+
+ A typical use case for this object is to store the properties of geographic
+ regions, where the boundaries of the regions are stored in an
+ :class:`~glue.core.component.ExtendedComponent` and the centers of the
+ regions are stored in two components, one for the longitude and one for the
+ latitude. Additional components may describe arbitrary properties of these
+ geographic regions (e.g. population, area, etc).
+
+ This class is mostly a convenience class. By using this class, Data
+ Loaders can create RegionData directly from an iterable of geometries,
+ since this class deals with creating representative points. Viewers
+ can assume that when adding a RegionData object they are probably
+ being asked to visualize the ExtendedComponent, and this class provides
+ convenience methods for ascertaining whether the components currently
+ visualized in a Viewer are the correct ones to enable display of the
+ ExtendedComponent and to transform the regions through the glue
+ linking architecture into the correct coordinates for display.
+
+ Parameters
+ ----------
+ label : `str`, optional
+ The label of the data.
+ coords : :class:`~glue.core.coordinates.Coordinates`, optional
+ The coordinates associated with the data.
+ **kwargs
+ All other keyword arguments are passed to the :class:`~glue.core.data.Data`
+ constructor.
+
+ Attributes
+ ----------
+ extended_component_id : :class:`~glue.core.component_id.ComponentID`
+ The ID of the :class:`~glue.core.component.ExtendedComponent` object
+ that contains the boundaries of the regions.
+ center_x_id : :class:`~glue.core.component_id.ComponentID`
+ The ID of the Component object that contains the x-coordinate of the
+ center of the regions. This is actually stored in the component
+ with the extended_component_id, but it is convenient to have it here.
+ center_y_id : :class:`~glue.core.component_id.ComponentID`
+ The ID of the Component object that contains the y-coordinate of the
+ center of the regions. This is actually stored in the component
+ with the extended_component_id, but it is convenient to have it here.
+
+ Examples
+ --------
+
+ There are two main options for initializing a :class:`~glue.core.data_region.RegionData`
+ object. The first is to simply pass in a list of ``Shapely.Geometry`` objects
+ with dimesionality N, from which we will create N+1 components: one
+ :class:`~glue.core.component.ExtendedComponent` with the boundaries, and N
+ regular Component(s) with the center coordinates computed from the Shapley
+ method :meth:`~shapely.GeometryCollection.representative_point`:
+
+ >>> geometries = [shapely.geometry.Point(0, 0).buffer(1), shapely.geometry.Point(1, 1).buffer(1)]
+ >>> my_region_data = RegionData(label='My Regions', boundary=geometries)
+
+ This will create a :class:`~glue.core.data_region.RegionData` object with three
+ components: one :class:`~glue.core.component.ExtendedComponent` with label
+ "geo" and two regular Components with labels "Center [x] for boundary"
+ and "Center [y] for boundary".
+
+ The second is to explicitly create an :class:`~glue.core.component.ExtendedComponent`
+ (which requires passing in the ComponentIDs for the center coordinates) and
+ then use `add_component` to add this component to a :class:`~glue.core.data_region.RegionData`
+ object. You might use this approach if your dataset already contains points that
+ represent the centers of your regions and you want to avoid re-calculating them. For example:
+
+ >>> center_x = [0, 1]
+ >>> center_y = [0, 1]
+ >>> geometries = [shapely.geometry.Point(0, 0).buffer(1), shapely.geometry.Point(1, 1).buffer(1)]
+
+ >>> my_region_data = RegionData(label='My Regions')
+ >>> # Region IDs are created and returned when we add a Component to a Data object
+ >>> cen_x_id = my_region_data.add_component(center_x, label='Center [x]')
+ >>> cen_y_id = my_region_data.add_component(center_y, label='Center [y]')
+ >>> extended_comp = ExtendedComponent(geometries, center_comp_ids=[cen_x_id, cen_y_id])
+ >>> my_region_data.add_component(extended_comp, label='boundaries')
+
+ """
+
+ def __init__(self, label="", coords=None, **kwargs):
+ self._extended_component_id = None
+ # __init__ calls add_component which deals with ExtendedComponent logic
+ super().__init__(label=label, coords=coords, **kwargs)
+
+ def __repr__(self):
+ return f'RegionData (label: {self.label} | extended_component: {self.extended_component_id})'
+
+ @property
+ def center_x_id(self):
+ return self.get_component(self.extended_component_id).x
+
+ @property
+ def center_y_id(self):
+ return self.get_component(self.extended_component_id).y
+
+ @property
+ def extended_component_id(self):
+ return self._extended_component_id
+
+ @contract(component='component_like', label='cid_like')
+ def add_component(self, component, label):
+ """ Add a new component to this data set, allowing only one :class:`~glue.core.component.ExtendedComponent`
+
+ If component is an array of Shapely objects then we use
+ :meth:`~shapely.GeometryCollection.representative_point`: to
+ create two new components for the center coordinates of the regions and
+ add them to the :class:`~glue.core.data_region.RegionData` object as well.
+
+ If component is an :class:`~glue.core.component.ExtendedComponent`,
+ then we simply add it to the :class:`~glue.core.data_region.RegionData` object.
+
+ We do this here instead of extending ``Component.autotyped`` because
+ we only want to use :class:`~glue.core.component.ExtendedComponent` objects
+ in the context of a :class:`~glue.core.data_region.RegionData` object.
+
+ Parameters
+ ----------
+ component : :class:`~glue.core.component.Component` or array-like
+ Object to add. If this is an array of Shapely objects, then we
+ create two new components for the center coordinates of the regions
+ as well.
+ label : `str` or :class:`~glue.core.component_id.ComponentID`
+ The label. If this is a string, a new
+ :class:`glue.core.component_id.ComponentID`
+ with this label will be created and associated with the Component.
+
+ Raises
+ ------
+ `ValueError`, if the :class:`~glue.core.data_region.RegionData` already has an extended component
+ """
+
+ if not isinstance(component, Component):
+ if all(isinstance(s, shapely.Geometry) for s in component):
+ center_x = []
+ center_y = []
+ for s in component:
+ rep = s.representative_point()
+ center_x.append(rep.x)
+ center_y.append(rep.y)
+ cen_x_id = super().add_component(np.asarray(center_x), f"Center [x] for {label}")
+ cen_y_id = super().add_component(np.asarray(center_y), f"Center [y] for {label}")
+ ext_component = ExtendedComponent(np.asarray(component), center_comp_ids=[cen_x_id, cen_y_id])
+ self._extended_component_id = super().add_component(ext_component, label)
+ return self._extended_component_id
+
+ if isinstance(component, ExtendedComponent):
+ if self.extended_component_id is not None:
+ raise ValueError(f"Cannot add another ExtendedComponent; existing extended component: {self.extended_component_id}")
+ else:
+ self._extended_component_id = super().add_component(component, label)
+ return self._extended_component_id
+ else:
+ return super().add_component(component, label)
+
+ def _get_trans_to_cids(self, cen_cids, other_cids):
+ """
+ Use recursion to traverse links and build up a list of functions
+ to convert cen_ids to other_cids.
+
+ Parameters
+ ----------
+ cen_cids : list of :class:`~glue.core.component_id.ComponentID`
+ The ComponentIDs that are the inputs to the transformation
+ function.
+ other_cids : list of :class:`~glue.core.component_id.ComponentID`
+ The ComponentIDs that are the outputs of the transformation
+ function.
+
+ Raises
+ ------
+ ValueError
+ If the links imply a transformation that cannot be done by Shapely.
+ """
+ if len(other_cids) != 2:
+ raise ValueError("Can only deal with 2D transformations")
+ linkx = self._get_external_link(other_cids[0])
+ linky = self._get_external_link(other_cids[1])
+
+ funcx = linkx.get_using()
+ funcy = linky.get_using()
+
+ if len(linkx.get_from_ids()) > 2 or len(linky.get_from_ids()) > 2:
+ raise ValueError("Can only display regions if links depend on 2 or fewer other components.")
+
+ def conv_function(x, y=None):
+ if len(linkx.get_from_ids()) == 1 and len(linky.get_from_ids()) == 1:
+ return [funcx(x), funcy(y)]
+ else:
+ return [funcx(x, y), funcy(x, y)]
+
+ self.list_of_functions.append(conv_function)
+ if len(linkx.get_from_ids()) == 2:
+ other_cids = linkx.get_from_ids()
+ else:
+ other_cids = linkx.get_from_ids() + linky.get_from_ids()
+ if cen_cids[0] in other_cids or cen_cids[1] in other_cids:
+ if set([cen_cids[0]]+[cen_cids[1]]) == set(other_cids):
+ return
+ else:
+ raise ValueError("Cannot display regions if links depend on other components.")
+ else:
+ self._get_trans_to_cids(cen_cids, other_cids)
+
+ def get_transform_to_cids(self, other_cids):
+ """
+ Return the function that converts the center components to other_cids.
+
+ We can use this to get the transformation from the x,y coordinates
+ that the ExtendedComponent are in to x and y attributes that are
+ visualized in a Viewer so that we can translate the geometries
+ in the ExtendedComponent to the new coordinates before displaying them.
+
+ Can be called in viewers as:
+
+ >>> tfunc = region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+
+ And the function can be used to transform the geometries as:
+
+ >>> from shapely.ops import transform
+ >>> new_geoms = [transform(tfunc, g) for g in old_geoms]
+
+ TODO: This is currently hard-coded to work with 2D transformations,
+ but could be extended to work with 1D viewers as well. Our region
+ geometries are limited to be 2D (or 1D ranges), so links
+ that require more than 2 input components do not admit to
+ a valid transformation.
+
+ Parameters
+ ----------
+ other_cid : list of :class:`~glue.core.component.ComponentID`
+ The other ComponentIDs (typically the ones that are
+ visualized in a Viewer).
+
+ Returns
+ -------
+ func : `callable`
+ The function that converts center_x_id and center_y_id to
+ other_cids which can then be used to transform the
+ geometries before display. Returns None if there is no
+ such valid transformation.
+ """
+
+ self.list_of_functions = []
+ self._get_trans_to_cids([self.center_x_id, self.center_y_id], other_cids)
+ if not self.list_of_functions:
+ return None
+ elif len(self.list_of_functions) == 1:
+ return self.list_of_functions[0]
+ else:
+ def conv_function(*args):
+ # Our list of functions is built up in reverse order
+ for f in self.list_of_functions[::-1]:
+ args = f(*args)
+ return args
+ return conv_function
+
+ def linked_to_center_comp(self, target_cid):
+ """
+ Check if target_cid can be mapped to one of the center components.
+
+ This is used to see if we can display the ExtendedComponent in a Viewer.
+
+ It is not sufficient to simply see if we can retrieve data from
+ target_cid like is commonly done in Viewers:
+
+ >>> _ = self[target_cid]
+
+ Because if target_cid is mapped to a Component that is not one of the
+ center components, then we cannot display the regions.
+
+ Parameters
+ ----------
+ target_cid : :class:`~glue.core.component.ComponentID`
+ The ComponentID (typically displayed in a Viewer) which we
+ want to check if it is one of the special center components.
+
+ Returns
+ -------
+ bool
+ True if target_cid can be mapped to one of the center components, False otherwise.
+ """
+ from glue.core.link_manager import is_equivalent_cid # avoid circular import
+
+ center_cids = [self.center_x_id, self.center_y_id]
+ for center_cid in center_cids:
+ if is_equivalent_cid(self, center_cid, target_cid):
+ return True
+
+ link = self._get_external_link(target_cid)
+ if not link:
+ return False
+ for center_cid in center_cids:
+ if center_cid in link:
+ return True
+ else:
+ return any([self.linked_to_center_comp(x) for x in link.get_from_ids()])
diff --git a/glue/core/layer_artist.py b/glue/core/layer_artist.py
index b7b5b8756..aa24e956b 100644
--- a/glue/core/layer_artist.py
+++ b/glue/core/layer_artist.py
@@ -228,6 +228,12 @@ def _check_subset_state_changed(self):
self._changed = True
self._state = state
+ def _on_components_changed(self, components_changed):
+ """
+ React to a change to one or more of the components in this layer.
+ """
+ pass
+
def __str__(self):
return "%s for %s" % (self.__class__.__name__, self.layer.label)
diff --git a/glue/core/message.py b/glue/core/message.py
index 94302dd7c..68615ddd1 100644
--- a/glue/core/message.py
+++ b/glue/core/message.py
@@ -182,7 +182,9 @@ def __init__(self, sender, attribute, tag=None):
class NumericalDataChangedMessage(DataMessage):
- pass
+ def __init__(self, sender, components_changed=None, tag=None):
+ super(NumericalDataChangedMessage, self).__init__(sender, tag=tag)
+ self.components_changed = components_changed
class DataCollectionMessage(Message):
diff --git a/glue/core/qt/__init__.py b/glue/core/qt/__init__.py
deleted file mode 100644
index 52b7c076a..000000000
--- a/glue/core/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt is deprecated, use glue_qt.core instead', GlueDeprecationWarning)
-from glue_qt.core import * # noqa
diff --git a/glue/core/qt/data_collection_model.py b/glue/core/qt/data_collection_model.py
deleted file mode 100644
index 78300433e..000000000
--- a/glue/core/qt/data_collection_model.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.data_collection_model is deprecated, use glue_qt.core.data_collection_model instead', GlueDeprecationWarning)
-from glue_qt.core.data_collection_model import * # noqa
diff --git a/glue/core/qt/dialogs.py b/glue/core/qt/dialogs.py
deleted file mode 100644
index 117ecd691..000000000
--- a/glue/core/qt/dialogs.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.dialogs is deprecated, use glue_qt.core.dialogs instead', GlueDeprecationWarning)
-from glue_qt.core.dialogs import * # noqa
diff --git a/glue/core/qt/fitters.py b/glue/core/qt/fitters.py
deleted file mode 100644
index 54bb772cc..000000000
--- a/glue/core/qt/fitters.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.fitters is deprecated, use glue_qt.core.fitters instead', GlueDeprecationWarning)
-from glue_qt.core.fitters import * # noqa
diff --git a/glue/core/qt/layer_artist_model.py b/glue/core/qt/layer_artist_model.py
deleted file mode 100644
index af0a56455..000000000
--- a/glue/core/qt/layer_artist_model.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.layer_artist_model is deprecated, use glue_qt.core.layer_artist_model instead', GlueDeprecationWarning)
-from glue_qt.core.layer_artist_model import * # noqa
diff --git a/glue/core/qt/message_widget.py b/glue/core/qt/message_widget.py
deleted file mode 100644
index 2fe5fc3dd..000000000
--- a/glue/core/qt/message_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.message_widget is deprecated, use glue_qt.core.message_widget instead', GlueDeprecationWarning)
-from glue_qt.core.message_widget import * # noqa
diff --git a/glue/core/qt/mime.py b/glue/core/qt/mime.py
deleted file mode 100644
index 4909c48f4..000000000
--- a/glue/core/qt/mime.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.mime is deprecated, use glue_qt.core.mime instead', GlueDeprecationWarning)
-from glue_qt.core.mime import * # noqa
diff --git a/glue/core/qt/simpleforms.py b/glue/core/qt/simpleforms.py
deleted file mode 100644
index e4be6effa..000000000
--- a/glue/core/qt/simpleforms.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.simpleforms is deprecated, use glue_qt.core.simpleforms instead', GlueDeprecationWarning)
-from glue_qt.core.simpleforms import * # noqa
diff --git a/glue/core/qt/style_dialog.py b/glue/core/qt/style_dialog.py
deleted file mode 100644
index 71e6c438a..000000000
--- a/glue/core/qt/style_dialog.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.core.qt.style_dialog is deprecated, use glue_qt.core.style_dialog instead', GlueDeprecationWarning)
-from glue_qt.core.style_dialog import * # noqa
diff --git a/glue/core/regions.py b/glue/core/regions.py
new file mode 100644
index 000000000..9c28b0dfa
--- /dev/null
+++ b/glue/core/regions.py
@@ -0,0 +1,41 @@
+"""
+Functions to support data that defines regions
+"""
+import numpy as np
+
+from glue.core.roi import PolygonalROI
+from glue.core.data_region import RegionData
+
+from glue.config import layer_action
+from glue.core.subset import RoiSubsetState, MultiOrState
+
+
+def reg_to_roi(reg):
+ if reg.geom_type == "Polygon":
+ ext_coords = np.array(reg.exterior.coords.xy)
+ roi = PolygonalROI(vx=ext_coords[0], vy=ext_coords[1]) # Need to account for interior rings
+ return roi
+
+
+@layer_action(label='Subset of regions -> Subset over region extent', single=True, subset=True)
+def layer_to_subset(layer, data_collection):
+ """
+ This should be limited to the case where subset.Data is RegionData
+ and/or return a warning when applied to some other kind of data.
+ """
+ if isinstance(layer.data, RegionData):
+
+ extended_comp = layer.data._extended_component_ids[0]
+ regions = layer[extended_comp]
+ list_of_rois = [reg_to_roi(region) for region in regions]
+
+ roisubstates = [RoiSubsetState(layer.data.ext_x,
+ layer.data.ext_y,
+ roi=roi
+ )
+ for roi in list_of_rois]
+ if len(list_of_rois) > 1:
+ composite_substate = MultiOrState(roisubstates)
+ else:
+ composite_substate = roisubstates[0]
+ _ = data_collection.new_subset_group(subset_state=composite_substate)
diff --git a/glue/core/state.py b/glue/core/state.py
index 05558b641..449005c6a 100644
--- a/glue/core/state.py
+++ b/glue/core/state.py
@@ -66,12 +66,15 @@ def load(rec, context)
from matplotlib.colors import Colormap
from matplotlib import cm
from astropy.wcs import WCS
+import shapely
from glue import core
from glue.core.data import Data
+from glue.core.data_region import RegionData
from glue.core.component_id import ComponentID, PixelComponentID
from glue.core.component import (Component, CategoricalComponent,
- DerivedComponent, CoordinateComponent)
+ DerivedComponent, CoordinateComponent,
+ ExtendedComponent)
from glue.core.subset import (OPSYM, SYMOP, CompositeSubsetState,
SubsetState, Subset, RoiSubsetState,
InequalitySubsetState, RangeSubsetState)
@@ -1028,12 +1031,18 @@ def load_cid_tuple(cids):
@saver(ComponentID)
def _save_component_id(cid, context):
- return dict(label=cid.label)
+ return dict(label=cid.label,
+ uuid=cid.uuid)
@loader(ComponentID)
def _load_component_id(rec, context):
- return ComponentID(rec['label'])
+ if 'uuid' in rec and rec['uuid'] is not None:
+ result = ComponentID(rec['label'])
+ result._uuid = rec['uuid']
+ return result
+ else:
+ return ComponentID(rec['label'])
@saver(PixelComponentID)
@@ -1057,7 +1066,6 @@ def _save_component(component, context):
log = component._load_log
return dict(log=context.id(log),
log_item=log.id(component))
-
return dict(data=context.do(component.data),
units=component.units)
@@ -1228,6 +1236,16 @@ def _load_datetime64(rec, context):
return np.datetime64(rec['datetime64'])
+@saver(shapely.Geometry)
+def _save_shapely_geometry(shape, context):
+ return {'shapely_geometry': str(shapely.to_wkt(shape))}
+
+
+@loader(shapely.Geometry)
+def _load_shapely_geometry(rec, context):
+ return shapely.from_wkt(rec['shapely_geometry'])
+
+
def apply_inplace_patches(rec):
"""
Apply in-place patches to a loaded session file. Ideally this should be
@@ -1263,3 +1281,169 @@ def apply_inplace_patches(rec):
contents = state['contents']
if 'st__coords' not in contents:
contents['st__coords'] = ['x']
+
+
+@saver(RegionData, version=1)
+def _save_regiondata(data, context):
+ result = dict(
+ components=[
+ (context.id(c), context.id(data.get_component(c))) for c in data._components
+ ],
+ subsets=[context.id(s) for s in data.subsets],
+ label=data.label,
+ )
+
+ if data.coords is not None:
+ result["coords"] = context.id(data.coords)
+
+ if data._extended_component_id is not None:
+ result["_extended_component_id"] = context.id(data._extended_component_id)
+
+ result["style"] = context.do(data.style)
+
+ def save_cid_tuple(cids):
+ return tuple(context.id(cid) for cid in cids)
+
+ result["_key_joins"] = [
+ [context.id(k), save_cid_tuple(v0), save_cid_tuple(v1)]
+ for k, (v0, v1) in data._key_joins.items()
+ ]
+ result["uuid"] = data.uuid
+ result["xuuid"] = data.center_x_id.uuid
+ result["yuuid"] = data.center_y_id.uuid
+ result["primary_owner"] = [
+ context.id(cid) for cid in data.components if cid.parent is data
+ ]
+ # Filter out keys/values that can't be serialized
+ meta_filtered = OrderedDict()
+ for key, value in data.meta.items():
+ try:
+ context.do(key)
+ context.do(value)
+ except GlueSerializeError:
+ continue
+ else:
+ meta_filtered[key] = value
+ result["meta"] = context.do(meta_filtered)
+
+ return result
+
+
+@loader(RegionData, version=1)
+def _load_regiondata(rec, context):
+ """
+ Custom load function for RegionData.
+ This is the same as the chain of logic in
+ _save_data_5 for Data, but result is an RegionData object
+ instead.
+ """
+
+ label = rec["label"]
+ result = RegionData(label=label)
+
+ # we manually rebuild pixel/world components, so
+ # we override this function. This is pretty ugly
+ result._create_pixel_and_world_components = lambda ndim: None
+
+ comps = [list(map(context.object, [cid, comp])) for cid, comp in rec["components"]]
+
+ for icomp, (cid, comp) in enumerate(comps):
+ if isinstance(comp, CoordinateComponent):
+ comp._data = result
+
+ # For backward compatibility, we need to check for cases where
+ # the component ID for the pixel components was not a PixelComponentID
+ # and upgrade it to one. This can be removed once we no longer
+ # support pre-v0.8 session files.
+ if not comp.world and not isinstance(cid, PixelComponentID):
+ cid = PixelComponentID(comp.axis, cid.label, parent=cid.parent)
+ comps[icomp] = (cid, comp)
+
+ result.add_component(comp, cid)
+
+ assert result._world_component_ids == []
+
+ coord = [c for c in comps if isinstance(c[1], CoordinateComponent)]
+ coord = [x[0] for x in sorted(coord, key=lambda x: x[1])]
+
+ if getattr(result, "coords") is not None:
+ assert len(coord) == result.ndim * 2
+ # Might black formatting break this?
+ result._world_component_ids = coord[: len(coord) // 2]
+ result._pixel_component_ids = coord[len(coord) // 2 :] # noqa E203
+ else:
+ assert len(coord) == result.ndim
+ result._pixel_component_ids = coord
+
+ # We can now re-generate the coordinate links
+ result._set_up_coordinate_component_links(result.ndim)
+
+ for s in rec["subsets"]:
+ result.add_subset(context.object(s))
+
+ result.style = context.object(rec["style"])
+
+ if "primary_owner" in rec:
+ for cid in rec["primary_owner"]:
+ cid = context.object(cid)
+ cid.parent = result
+ yield result
+
+ def load_cid_tuple(cids):
+ return tuple(context.object(cid) for cid in cids)
+
+ result._key_joins = dict(
+ (context.object(k), (load_cid_tuple(v0), load_cid_tuple(v1)))
+ for k, v0, v1 in rec["_key_joins"]
+ )
+ if "uuid" in rec and rec["uuid"] is not None:
+ result.uuid = rec["uuid"]
+ else:
+ result.uuid = str(uuid.uuid4())
+ if "meta" in rec:
+ result.meta.update(context.object(rec["meta"]))
+
+ result._extended_component_id = context.object(rec["_extended_component_id"])
+
+ def fix_special_component_ids(ext_data):
+ """
+ We need to update the .x and .y attributes on the extended component
+ to be the actual component IDs in the data object. This is a fragile
+ way to do it because it assumes that the component labels are
+ unique.
+ """
+ ext_comp = ext_data.get_component(ext_data.extended_component_id)
+
+ for comp_id in ext_data.component_ids():
+ if comp_id.uuid == rec['xuuid']:
+ new_x = comp_id
+ elif comp_id.uuid == rec['yuuid']:
+ new_y = comp_id
+ ext_comp.x = new_x
+ ext_comp.y = new_y
+ yield fix_special_component_ids(result)
+
+
+@saver(ExtendedComponent)
+def _save_extended_component(component, context):
+ if not context.include_data and hasattr(component, "_load_log"):
+ log = component._load_log
+ return dict(log=context.id(log), log_item=log.id(component))
+
+ data_to_save = [x for x in component.data]
+
+ return dict(data=context.do(data_to_save),
+ x=context.do(component.x),
+ y=context.do(component.y),
+ units=component.units)
+
+
+@loader(ExtendedComponent)
+def _load_extended_component(rec, context):
+ if "log" in rec:
+ return context.object(rec["log"]).component(rec["log_item"])
+
+ data_to_load = np.asarray([x for x in context.object(rec["data"])])
+ return ExtendedComponent(data=data_to_load,
+ center_comp_ids=[context.object(rec['x']), context.object(rec['y'])],
+ units=rec["units"])
diff --git a/glue/core/state_objects.py b/glue/core/state_objects.py
index e5e31a36b..149ff43f9 100644
--- a/glue/core/state_objects.py
+++ b/glue/core/state_objects.py
@@ -9,9 +9,10 @@
HasCallbackProperties, CallbackList)
from glue.core.state import saver, loader
from glue.core.component_id import PixelComponentID
+from glue.core.exceptions import IncompatibleAttribute
__all__ = ['State', 'StateAttributeCacheHelper',
- 'StateAttributeLimitsHelper', 'StateAttributeSingleValueHelper']
+ 'StateAttributeLimitsHelper', 'StateAttributeSingleValueHelper', 'StateAttributeHistogramHelper']
@saver(CallbackList)
@@ -428,9 +429,16 @@ def __init__(self, state, attribute, random_subset=10000, max_n_bin=30,
self._common_n_bin = None
def _apply_common_n_bin(self):
- for att in self._cache:
- if not self.data.get_kind(att) == 'categorical':
- self._cache[att]['n_bin'] = self._common_n_bin
+ for att in list(self._cache):
+ try:
+ if not self.data.get_kind(att) == 'categorical':
+ self._cache[att]['n_bin'] = self._common_n_bin
+ except IncompatibleAttribute:
+ # This can indicate that a dataset has been removed from the
+ # data collection or that the attribute has changed to a
+ # new dataset that is not compatible with the previous one.
+ # In this case we should remove the entry
+ self._cache.pop(att)
def _update_common_n_bin(self, common_n_bin):
if common_n_bin:
diff --git a/glue/core/tests/test_component.py b/glue/core/tests/test_component.py
index 7448e6597..a1924eece 100644
--- a/glue/core/tests/test_component.py
+++ b/glue/core/tests/test_component.py
@@ -7,13 +7,14 @@
from unittest.mock import MagicMock
from astropy.wcs import WCS
+from shapely.geometry import MultiPolygon, Polygon, Point, LineString
from glue import core
from glue.tests.helpers import requires_astropy
from ..coordinates import Coordinates
from ..component import (Component, DerivedComponent, CoordinateComponent,
- CategoricalComponent)
+ CategoricalComponent, ExtendedComponent)
from ..component_id import ComponentID
from ..data import Data
from ..parse import ParsedCommand, ParsedComponentLink
@@ -403,3 +404,52 @@ def test_coordinate_component_1d_coord():
data = Data(flux=np.random.random(5), coords=wcs, label='data')
np.testing.assert_equal(data['Frequency'], [1, 2, 3, 4, 5])
+
+
+class TestExtendedComponent(object):
+
+ def setup_method(self):
+
+ self.cen_x_id = ComponentID('x')
+ self.cen_y_id = ComponentID('y')
+
+ poly_1 = Polygon([(20, 20), (60, 20), (60, 40), (20, 40)])
+ poly_2 = Polygon([(60, 50), (60, 70), (80, 70), (80, 50)])
+ poly_3 = Polygon([(10, 10), (15, 10), (15, 15), (10, 15)])
+ poly_4 = Polygon([(10, 20), (15, 20), (15, 30), (10, 30), (12, 25)])
+
+ polygons = MultiPolygon([poly_3, poly_4])
+ self.polys = np.array([poly_1, poly_2, polygons])
+ self.poly2d = ExtendedComponent(self.polys, center_comp_ids=[self.cen_x_id, self.cen_y_id])
+
+ circle_1 = Point(1.0, 0.0).buffer(1)
+ circle_2 = Point(2.0, 3.0).buffer(2)
+ circles = np.array([circle_1, circle_2])
+ self.circles = ExtendedComponent(circles, center_comp_ids=[self.cen_x_id, self.cen_y_id])
+
+ ranges = np.array([LineString([(0, 0), (1, 0)]), LineString([(0, 0), (4, 0)])])
+ self.ranges = ExtendedComponent(ranges, center_comp_ids=[self.cen_x_id])
+
+ def test_basic_proprties(self):
+ assert self.poly2d.ndim == 1
+ assert isinstance(self.poly2d, ExtendedComponent)
+ assert self.poly2d.shape == (3,)
+ assert self.poly2d.x == self.cen_x_id
+ assert self.poly2d.y == self.cen_y_id
+
+ assert self.ranges.ndim == 1
+ assert isinstance(self.ranges, ExtendedComponent)
+ assert self.ranges.shape == (2,)
+ assert self.ranges.x == self.cen_x_id
+ assert self.ranges.y is None
+
+ def test_incorrect_inputs(self):
+ with pytest.raises(TypeError, match='Input data for a ExtendedComponent should be a list of shapely.Geometry objects'):
+ bad_data = np.array([1, 2, 3])
+ bad_data_comp = ExtendedComponent(bad_data, center_comp_ids=[self.cen_x_id, self.cen_y_id])
+
+ with pytest.raises(ValueError, match='ExtendedComponent must be initialized with one or two ComponentIDs'):
+ no_center_ids_comp = ExtendedComponent(self.polys, center_comp_ids=[])
+
+ with pytest.raises(ValueError, match='ExtendedComponent must be initialized with one or two ComponentIDs'):
+ no_center_ids_comp = ExtendedComponent(self.polys, center_comp_ids=[self.cen_x_id, self.cen_y_id, self.cen_x_id])
diff --git a/glue/core/tests/test_components_changed.py b/glue/core/tests/test_components_changed.py
new file mode 100644
index 000000000..d729c97d7
--- /dev/null
+++ b/glue/core/tests/test_components_changed.py
@@ -0,0 +1,45 @@
+"""
+Test that data.update_components() sends a NumericalDataChangedMessage
+that conveys which components have been changed.
+"""
+from glue.core.data import Data
+from glue.core.hub import HubListener
+from glue.core.data_collection import DataCollection
+from glue.core.message import NumericalDataChangedMessage
+
+import numpy as np
+from numpy.testing import assert_array_equal
+
+
+def test_message_carries_components():
+
+ test_data = Data(x=np.array([1, 2, 3, 4, 5]), y=np.array([1, 2, 3, 4, 5]), label='test_data')
+ data_collection = DataCollection([test_data])
+
+ class CustomListener(HubListener):
+
+ def __init__(self, hub):
+ self.received = 0
+ self.components_changed = None
+ hub.subscribe(self, NumericalDataChangedMessage,
+ handler=self.receive_message)
+
+ def receive_message(self, message):
+ self.received += 1
+ try:
+ self.components_changed = message.components_changed
+ except AttributeError:
+ self.components_changed = None
+
+ listener = CustomListener(data_collection.hub)
+ assert listener.received == 0
+ assert listener.components_changed is None
+
+ cid_to_change = test_data.id['x']
+ new_data = [5, 2, 6, 7, 10]
+ test_data.update_components({cid_to_change: new_data})
+
+ assert listener.received == 1
+ assert cid_to_change in listener.components_changed
+
+ assert_array_equal(test_data['x'], new_data)
diff --git a/glue/core/tests/test_data_region.py b/glue/core/tests/test_data_region.py
new file mode 100644
index 000000000..9d527cf14
--- /dev/null
+++ b/glue/core/tests/test_data_region.py
@@ -0,0 +1,397 @@
+import pytest
+from numpy.testing import assert_array_equal
+
+import numpy as np
+import shapely
+from shapely.geometry import MultiPolygon, Polygon, Point
+from shapely.affinity import affine_transform
+from shapely.ops import transform
+
+from glue.core.data import Data
+from glue.core.data_collection import DataCollection
+from glue.core.data_region import RegionData
+from glue.core.component import ExtendedComponent
+from glue.core.state import GlueUnSerializer
+from glue.core.tests.test_application_base import MockApplication
+from glue.core.link_helpers import LinkSame, LinkTwoWay
+from glue.core.exceptions import IncompatibleAttribute
+
+from glue.plugins.wcs_autolinking.wcs_autolinking import AffineLink
+
+poly_1 = Polygon([(20, 20), (60, 20), (60, 40), (20, 40)])
+poly_2 = Polygon([(60, 50), (60, 70), (80, 70), (80, 50)])
+poly_3 = Polygon([(10, 10), (15, 10), (15, 15), (10, 15)])
+poly_4 = Polygon([(10, 20), (15, 20), (15, 30), (10, 30), (12, 25)])
+
+polygons = MultiPolygon([poly_3, poly_4])
+SHAPELY_POLYGON_ARRAY = np.array([poly_1, poly_2, polygons])
+
+SHAPELY_CIRCLE_ARRAY = np.array([Point(0, 0).buffer(1), Point(1, 1).buffer(1)])
+CENTER_X = [0, 1]
+CENTER_Y = [0, 1]
+
+
+def shift(x):
+ return x + 1
+
+
+def unshift(x):
+ return x - 1
+
+
+def forwards(x):
+ return x * 2
+
+
+def backwards(x):
+ return x / 2
+
+
+class TestRegionDataLinks(object):
+ def setup_method(self):
+ self.region_data = RegionData(label='My Regions',
+ color=np.array(['red', 'blue', 'green']),
+ area=shapely.area(SHAPELY_POLYGON_ARRAY),
+ boundary=SHAPELY_POLYGON_ARRAY)
+ self.other_data = Data(label='Other Data',
+ x=np.array([1, 2, 3]),
+ y=np.array([5, 6, 7]),
+ color=np.array(['yellow', 'pink', 'orange']),
+ area=np.array([10, 20, 30]))
+
+ def test_linked_properly(self):
+ dc = DataCollection([self.region_data, self.other_data])
+
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.region_data.center_x_id, self.other_data.id['x']))
+ dc.add_link(LinkSame(self.region_data.center_y_id, self.other_data.id['y']))
+
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ def test_linked_incorrectly(self):
+ dc = DataCollection([self.region_data, self.other_data])
+
+ viewer_x_att = self.other_data.id['color']
+ viewer_y_att = self.other_data.id['area']
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.region_data.id['color'], self.other_data.id['color']))
+ dc.add_link(LinkSame(self.region_data.id['area'], self.other_data.id['area']))
+
+ # This is how a Viewer typically checks to see if is being asked
+ # to plot an incompatible dataset. RegionData is a special case
+ # because we need to know if the viewer is showing the center components
+ # (in which case we can show the regions) or some other components
+ # (in which case we can't). In this case we need to know that we
+ # CANNOT show the center component.
+ try:
+ x = self.region_data[viewer_x_att]
+ except IncompatibleAttribute:
+ assert False
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+
+class TestRegionData(object):
+
+ def setup_method(self):
+ self.region_data = RegionData(label='My Regions', boundary=SHAPELY_POLYGON_ARRAY)
+
+ self.manual_region_data = RegionData(label='My Manual Regions')
+
+ self.cen_x_id = self.manual_region_data.add_component(CENTER_X, label='Center [x]')
+ self.cen_y_id = self.manual_region_data.add_component(CENTER_Y, label='Center [y]')
+ self.extended_comp = ExtendedComponent(SHAPELY_CIRCLE_ARRAY, center_comp_ids=[self.cen_x_id, self.cen_y_id])
+ self.manual_region_data.add_component(self.extended_comp, label='circles')
+
+ self.other_data = Data(x=np.array([1, 2, 3]), y=np.array([5, 6, 7]), label='Other Data')
+ self.mid_data = Data(x=np.array([4, 5, 6]), y=np.array([2, 3, 4]), label='Middle Data')
+
+ def test_basic_properties_simple(self):
+ assert self.region_data.label == 'My Regions'
+ assert self.region_data.shape == SHAPELY_POLYGON_ARRAY.shape
+ assert self.region_data.ndim == 1
+ assert self.region_data.size == 3
+ assert len(self.region_data.components) == 4
+ assert_array_equal(self.region_data['boundary'], SHAPELY_POLYGON_ARRAY)
+ assert len(self.region_data.main_components) == 3
+ component_labels = [cid.label for cid in self.region_data.main_components]
+ assert 'boundary' in component_labels
+ assert 'Center [x] for boundary' in component_labels
+ assert 'Center [y] for boundary' in component_labels
+
+ def test_basic_properties_manual(self):
+ assert self.manual_region_data.label == 'My Manual Regions'
+ assert self.manual_region_data.shape == np.asarray(SHAPELY_CIRCLE_ARRAY).shape
+ assert self.manual_region_data.ndim == 1
+ assert self.manual_region_data.size == 2
+ assert len(self.manual_region_data.components) == 4
+ assert_array_equal(self.manual_region_data['circles'], SHAPELY_CIRCLE_ARRAY)
+ assert len(self.region_data.main_components) == 3
+ component_labels = [cid.label for cid in self.manual_region_data.main_components]
+ assert 'circles' in component_labels
+ assert 'Center [x]' in component_labels
+ assert 'Center [y]' in component_labels
+
+ def test_get_kind(self):
+ assert self.region_data.get_kind('Center [x] for boundary') == 'numerical'
+ assert self.region_data.get_kind('Center [y] for boundary') == 'numerical'
+ assert self.region_data.get_kind('boundary') == 'extended'
+
+ assert self.manual_region_data.get_kind('Center [x]') == 'numerical'
+ assert self.manual_region_data.get_kind('Center [y]') == 'numerical'
+ assert self.manual_region_data.get_kind('circles') == 'extended'
+
+ def test_check_if_can_display(self):
+ dc = DataCollection([self.region_data, self.other_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.region_data.center_x_id, self.other_data.id['x']))
+ dc.add_link(LinkSame(self.region_data.center_y_id, self.other_data.id['y']))
+
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ def test_check_if_can_display_through_intermediate(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.region_data.center_x_id, self.mid_data.id['x']))
+ dc.add_link(LinkSame(self.region_data.center_y_id, self.mid_data.id['y']))
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.other_data.id['x'], self.mid_data.id['x']))
+
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.other_data.id['y'], self.mid_data.id['y']))
+
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ def test_check_if_can_display_through_complicated_intermediate(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkSame(self.region_data.center_x_id, self.mid_data.id['x']))
+ dc.add_link(LinkSame(self.region_data.center_y_id, self.mid_data.id['y']))
+
+ assert not self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkTwoWay(self.other_data.id['x'], self.mid_data.id['x'], forwards, backwards))
+
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert not self.region_data.linked_to_center_comp(viewer_y_att)
+
+ dc.add_link(LinkTwoWay(self.other_data.id['y'], self.mid_data.id['y'], forwards, backwards))
+
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ def test_get_transformation_to_cid(self):
+ dc = DataCollection([self.region_data, self.other_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ dc.add_link(LinkTwoWay(self.region_data.center_x_id, viewer_x_att, forwards, backwards))
+ dc.add_link(LinkTwoWay(self.region_data.center_y_id, viewer_y_att, backwards, forwards))
+
+ translation_func = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+ x_data = self.region_data[self.region_data.center_x_id]
+ y_data = self.region_data[self.region_data.center_y_id]
+
+ assert_array_equal(translation_func(x_data, y_data),
+ [forwards(self.region_data[self.region_data.center_x_id]),
+ backwards(self.region_data[self.region_data.center_y_id])])
+
+ def test_get_multilink_transformation(self):
+ dc = DataCollection([self.region_data, self.other_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ matrix = np.array([[2, 0, 0], [0, 2, 0], [0, 0, 1]])
+ shap_matrix = [2, 0, 0, 2, 0, 0] # This is how shapely defines this affine matrix
+ dc.add_link(AffineLink(self.region_data, self.other_data,
+ [self.region_data.center_x_id, self.region_data.center_y_id],
+ [viewer_x_att, viewer_y_att],
+ matrix=matrix))
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ translation_func = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+ x_data = self.region_data[self.region_data.center_x_id]
+ y_data = self.region_data[self.region_data.center_y_id]
+ assert_array_equal(translation_func(x_data, y_data),
+ [self.region_data[viewer_x_att], self.region_data[viewer_y_att]])
+
+ new_regions = [transform(translation_func, g) for g in self.region_data['boundary']]
+ shapley_trans = [affine_transform(g, shap_matrix) for g in SHAPELY_POLYGON_ARRAY]
+ assert_array_equal(new_regions, shapley_trans)
+
+ def test_get_multilink_transformation_through_int(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ dc.add_link(LinkTwoWay(self.region_data.center_x_id, self.mid_data.id['x'], shift, unshift))
+ dc.add_link(LinkTwoWay(self.region_data.center_y_id, self.mid_data.id['y'], unshift, shift))
+
+ matrix = np.array([[2, 0, 0], [0, 3, 0], [0, 0, 1]])
+
+ dc.add_link(AffineLink(self.mid_data, self.other_data,
+ [self.mid_data.id['x'], self.mid_data.id['y']],
+ [viewer_x_att, viewer_y_att],
+ matrix=matrix))
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ translation_func = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+ x_data = self.region_data[self.region_data.center_x_id]
+ y_data = self.region_data[self.region_data.center_y_id]
+ assert_array_equal(translation_func(x_data, y_data),
+ [self.region_data[viewer_x_att], self.region_data[viewer_y_att]])
+
+ def test_get_multilink_transformation_through_int_mixed(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+
+ dc.add_link(LinkTwoWay(self.region_data.center_x_id, self.mid_data.id['x'], shift, unshift))
+ dc.add_link(LinkTwoWay(self.region_data.center_y_id, self.mid_data.id['y'], unshift, shift))
+
+ matrix = np.array([[2, 2, 0], [1, 3, 3], [0, 0, 1]])
+
+ dc.add_link(AffineLink(self.mid_data, self.other_data,
+ [self.mid_data.id['x'], self.mid_data.id['y']],
+ [viewer_x_att, viewer_y_att],
+ matrix=matrix))
+ assert self.region_data.linked_to_center_comp(viewer_x_att)
+ assert self.region_data.linked_to_center_comp(viewer_y_att)
+
+ translation_func = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+ x_data = self.region_data[self.region_data.center_x_id]
+ y_data = self.region_data[self.region_data.center_y_id]
+ assert_array_equal(translation_func(x_data, y_data),
+ [self.region_data[viewer_x_att], self.region_data[viewer_y_att]])
+
+ def test_errors_too_many_viewer_comps(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ self.other_data.add_component(np.array([4, 5, 6]), label='z')
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+ viewer_z_att = self.other_data.id['z']
+
+ dc.add_link(LinkTwoWay(self.region_data.center_x_id, self.mid_data.id['x'], shift, unshift))
+ dc.add_link(LinkTwoWay(self.region_data.center_y_id, self.mid_data.id['y'], unshift, shift))
+
+ with pytest.raises(ValueError, match="Can only deal with 2D transformations"):
+ _ = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att, viewer_z_att])
+
+ def test_errors_too_many_comps_in_transform(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ self.other_data.add_component(np.array([4, 5, 6]), label='z')
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+ viewer_z_att = self.other_data.id['z']
+ self.region_data.add_component(np.array([90, 80, 70]), label='zz')
+
+ matrix = np.array([[2, 0, 0, 0], [0, 2, 0, 0], [0, 0, 2, 0], [0, 0, 0, 1]])
+ dc.add_link(AffineLink(self.region_data, self.other_data,
+ [self.region_data.center_x_id,
+ self.region_data.center_y_id,
+ self.region_data.id['zz']],
+ [viewer_x_att, viewer_y_att, viewer_z_att],
+ matrix=matrix))
+
+ with pytest.raises(ValueError, match="Can only display regions if links depend on 2 or fewer other components."):
+ _ = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+
+ def test_errors_bad_link(self):
+ dc = DataCollection([self.region_data, self.other_data, self.mid_data])
+ self.other_data.add_component(np.array([4, 5, 6]), label='z')
+ viewer_x_att = self.other_data.id['x']
+ viewer_y_att = self.other_data.id['y']
+ viewer_z_att = self.other_data.id['z']
+ self.region_data.add_component(np.array([90, 80, 70]), label='zz')
+ self.region_data.add_component(np.array([90, 80, 70]), label='fakey')
+
+ matrix = np.array([[2, 0, 0], [0, 2, 0], [0, 0, 1]])
+ dc.add_link(AffineLink(self.region_data, self.other_data,
+ [self.region_data.center_x_id,
+ self.region_data.id['fakey']],
+ [viewer_x_att, viewer_y_att],
+ matrix=matrix))
+
+ with pytest.raises(ValueError, match="Cannot display regions if links depend on other components."):
+ _ = self.region_data.get_transform_to_cids([viewer_x_att, viewer_y_att])
+
+
+class TestRegionDataSaveRestore(object):
+
+ @pytest.fixture(autouse=True)
+ def setup_method(self, tmpdir):
+ app = MockApplication()
+ geodata = RegionData(label='My Regions', regions=SHAPELY_POLYGON_ARRAY)
+ catdata = Data(label='catdata', x=np.array([1, 2, 3, 4]), y=np.array([10, 20, 30, 40]))
+ app.data_collection.append(geodata)
+ app.data_collection.append(catdata)
+
+ app.data_collection.add_link(LinkSame(geodata.id['Center [x] for regions'], catdata.id['x']))
+ app.data_collection.add_link(LinkSame(geodata.id['Center [y] for regions'], catdata.id['y']))
+
+ session_file = tmpdir.mkdir("session").join('test.glu')
+ app.save_session(session_file)
+
+ with open(session_file, "r") as f:
+ session = f.read()
+
+ state = GlueUnSerializer.loads(session)
+ ga = state.object("__main__")
+ dc = ga.session.data_collection
+
+ self.reg_before = app.data_collection[0]
+ self.cat_before = app.data_collection[1]
+
+ self.reg_after = dc[0]
+ self.cat_after = dc[1]
+
+ def test_data_roundtrip(self):
+ assert_array_equal(self.reg_before['regions'], self.reg_after['regions'])
+ assert_array_equal(self.cat_before['x'], self.cat_after['x'])
+ assert_array_equal(self.cat_before['y'], self.cat_after['y'])
+
+ def test_component_ids_are_restored_correctly(self):
+ for data in [self.reg_before, self.reg_after]:
+ assert data.extended_component_id == data.id['regions']
+ assert data.extended_component_id == data.id['regions']
+
+ assert data.components[1] == data.get_component(data.components[3]).x
+ assert data.components[2] == data.get_component(data.components[3]).y
+
+ def test_links_still_work(self):
+ for data in [(self.reg_before, self.cat_before), (self.reg_after, self.cat_after)]:
+ reg_data, cat_data = data
+ assert_array_equal(reg_data[reg_data.get_component(reg_data.extended_component_id).x], cat_data.id['x'])
+ assert_array_equal(reg_data[reg_data.get_component(reg_data.extended_component_id).y], cat_data.id['y'])
diff --git a/glue/dialogs/autolinker/qt/__init__.py b/glue/dialogs/autolinker/qt/__init__.py
deleted file mode 100644
index 7c5364e0d..000000000
--- a/glue/dialogs/autolinker/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.autolinker.qt is deprecated, use glue_qt.dialogs.autolinker instead', GlueDeprecationWarning)
-from glue_qt.dialogs.autolinker import * # noqa
diff --git a/glue/dialogs/autolinker/qt/autolinker.py b/glue/dialogs/autolinker/qt/autolinker.py
deleted file mode 100644
index bbe79aeb2..000000000
--- a/glue/dialogs/autolinker/qt/autolinker.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.autolinker.qt.autolinker is deprecated, use glue_qt.dialogs.autolinker.autolinker instead', GlueDeprecationWarning)
-from glue_qt.dialogs.autolinker.autolinker import * # noqa
diff --git a/glue/dialogs/common/qt/__init__.py b/glue/dialogs/common/qt/__init__.py
deleted file mode 100644
index 1326b992f..000000000
--- a/glue/dialogs/common/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.common.qt is deprecated, use glue_qt.dialogs.common instead', GlueDeprecationWarning)
-from glue_qt.dialogs.common import * # noqa
diff --git a/glue/dialogs/common/qt/component_tree_widget.py b/glue/dialogs/common/qt/component_tree_widget.py
deleted file mode 100644
index 29e288f19..000000000
--- a/glue/dialogs/common/qt/component_tree_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.common.qt.component_tree_widget is deprecated, use glue_qt.dialogs.common.component_tree_widget instead', GlueDeprecationWarning)
-from glue_qt.dialogs.common.component_tree_widget import * # noqa
diff --git a/glue/dialogs/component_arithmetic/qt/__init__.py b/glue/dialogs/component_arithmetic/qt/__init__.py
deleted file mode 100644
index 8a23b3bc1..000000000
--- a/glue/dialogs/component_arithmetic/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.component_arithmetic.qt is deprecated, use glue_qt.dialogs.component_arithmetic instead', GlueDeprecationWarning)
-from glue_qt.dialogs.component_arithmetic import * # noqa
diff --git a/glue/dialogs/component_arithmetic/qt/component_arithmetic.py b/glue/dialogs/component_arithmetic/qt/component_arithmetic.py
deleted file mode 100644
index 7b9b34340..000000000
--- a/glue/dialogs/component_arithmetic/qt/component_arithmetic.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.component_arithmetic.qt.component_arithmetic is deprecated, use glue_qt.dialogs.component_arithmetic.component_arithmetic instead', GlueDeprecationWarning)
-from glue_qt.dialogs.component_arithmetic.component_arithmetic import * # noqa
diff --git a/glue/dialogs/component_arithmetic/qt/equation_editor.py b/glue/dialogs/component_arithmetic/qt/equation_editor.py
deleted file mode 100644
index 7138cd6ef..000000000
--- a/glue/dialogs/component_arithmetic/qt/equation_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.component_arithmetic.qt.equation_editor is deprecated, use glue_qt.dialogs.component_arithmetic.equation_editor instead', GlueDeprecationWarning)
-from glue_qt.dialogs.component_arithmetic.equation_editor import * # noqa
diff --git a/glue/dialogs/component_manager/qt/__init__.py b/glue/dialogs/component_manager/qt/__init__.py
deleted file mode 100644
index 50c6717e4..000000000
--- a/glue/dialogs/component_manager/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.component_manager.qt is deprecated, use glue_qt.dialogs.component_manager instead', GlueDeprecationWarning)
-from glue_qt.dialogs.component_manager import * # noqa
diff --git a/glue/dialogs/component_manager/qt/component_manager.py b/glue/dialogs/component_manager/qt/component_manager.py
deleted file mode 100644
index 68e398a90..000000000
--- a/glue/dialogs/component_manager/qt/component_manager.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.component_manager.qt.component_manager is deprecated, use glue_qt.dialogs.component_manager.component_manager instead', GlueDeprecationWarning)
-from glue_qt.dialogs.component_manager.component_manager import * # noqa
diff --git a/glue/dialogs/data_wizard/qt/__init__.py b/glue/dialogs/data_wizard/qt/__init__.py
deleted file mode 100644
index 108d86cab..000000000
--- a/glue/dialogs/data_wizard/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.data_wizard.qt is deprecated, use glue_qt.dialogs.data_wizard instead', GlueDeprecationWarning)
-from glue_qt.dialogs.data_wizard import * # noqa
diff --git a/glue/dialogs/data_wizard/qt/data_wizard_dialog.py b/glue/dialogs/data_wizard/qt/data_wizard_dialog.py
deleted file mode 100644
index 9b1789158..000000000
--- a/glue/dialogs/data_wizard/qt/data_wizard_dialog.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.data_wizard.qt.data_wizard_dialog is deprecated, use glue_qt.dialogs.data_wizard.data_wizard_dialog instead', GlueDeprecationWarning)
-from glue_qt.dialogs.data_wizard.data_wizard_dialog import * # noqa
diff --git a/glue/dialogs/link_editor/qt/__init__.py b/glue/dialogs/link_editor/qt/__init__.py
deleted file mode 100644
index 558162ed5..000000000
--- a/glue/dialogs/link_editor/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.link_editor.qt is deprecated, use glue_qt.dialogs.link_editor instead', GlueDeprecationWarning)
-from glue_qt.dialogs.link_editor import * # noqa
diff --git a/glue/dialogs/link_editor/qt/data_graph.py b/glue/dialogs/link_editor/qt/data_graph.py
deleted file mode 100644
index e4d3e0378..000000000
--- a/glue/dialogs/link_editor/qt/data_graph.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.link_editor.qt.data_graph is deprecated, use glue_qt.dialogs.link_editor.data_graph instead', GlueDeprecationWarning)
-from glue_qt.dialogs.link_editor.data_graph import * # noqa
diff --git a/glue/dialogs/link_editor/qt/link_editor.py b/glue/dialogs/link_editor/qt/link_editor.py
deleted file mode 100644
index 39f5690e2..000000000
--- a/glue/dialogs/link_editor/qt/link_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.link_editor.qt.link_editor is deprecated, use glue_qt.dialogs.link_editor.link_editor instead', GlueDeprecationWarning)
-from glue_qt.dialogs.link_editor.link_editor import * # noqa
diff --git a/glue/dialogs/subset_facet/qt/__init__.py b/glue/dialogs/subset_facet/qt/__init__.py
deleted file mode 100644
index 92878f2e1..000000000
--- a/glue/dialogs/subset_facet/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.subset_facet.qt is deprecated, use glue_qt.dialogs.subset_facet instead', GlueDeprecationWarning)
-from glue_qt.dialogs.subset_facet import * # noqa
diff --git a/glue/dialogs/subset_facet/qt/subset_facet.py b/glue/dialogs/subset_facet/qt/subset_facet.py
deleted file mode 100644
index b5dd57673..000000000
--- a/glue/dialogs/subset_facet/qt/subset_facet.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.dialogs.subset_facet.qt.subset_facet is deprecated, use glue_qt.dialogs.subset_facet.subset_facet instead', GlueDeprecationWarning)
-from glue_qt.dialogs.subset_facet.subset_facet import * # noqa
diff --git a/glue/icons/qt/__init__.py b/glue/icons/qt/__init__.py
deleted file mode 100644
index 95a08bec4..000000000
--- a/glue/icons/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.icons.qt is deprecated, use glue_qt.icons instead', GlueDeprecationWarning)
-from glue_qt.icons import * # noqa
diff --git a/glue/icons/qt/helpers.py b/glue/icons/qt/helpers.py
deleted file mode 100644
index 46996b1d5..000000000
--- a/glue/icons/qt/helpers.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.icons.qt.helpers is deprecated, use glue_qt.icons.helpers instead', GlueDeprecationWarning)
-from glue_qt.icons.helpers import * # noqa
diff --git a/glue/io/qt/__init__.py b/glue/io/qt/__init__.py
deleted file mode 100644
index a40e02085..000000000
--- a/glue/io/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.io.qt is deprecated, use glue_qt.io instead', GlueDeprecationWarning)
-from glue_qt.io import * # noqa
diff --git a/glue/io/qt/directory_importer/__init__.py b/glue/io/qt/directory_importer/__init__.py
deleted file mode 100644
index ce7679906..000000000
--- a/glue/io/qt/directory_importer/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.io.qt.directory_importer is deprecated, use glue_qt.io.directory_importer instead', GlueDeprecationWarning)
-from glue_qt.io.directory_importer import * # noqa
diff --git a/glue/io/qt/directory_importer/directory_importer.py b/glue/io/qt/directory_importer/directory_importer.py
deleted file mode 100644
index 440576b4f..000000000
--- a/glue/io/qt/directory_importer/directory_importer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.io.qt.directory_importer.directory_importer is deprecated, use glue_qt.io.directory_importer.directory_importer instead', GlueDeprecationWarning)
-from glue_qt.io.directory_importer.directory_importer import * # noqa
diff --git a/glue/io/qt/subset_mask.py b/glue/io/qt/subset_mask.py
deleted file mode 100644
index 98327dfda..000000000
--- a/glue/io/qt/subset_mask.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.io.qt.subset_mask is deprecated, use glue_qt.io.subset_mask instead', GlueDeprecationWarning)
-from glue_qt.io.subset_mask import * # noqa
diff --git a/glue/main.py b/glue/main.py
index 3a03a240b..b47d15a2c 100755
--- a/glue/main.py
+++ b/glue/main.py
@@ -57,7 +57,7 @@ def load_plugins(splash=None, require_qt_plugins=False, plugins_to_use=None):
else:
n_plugins = len(plugins_to_use)
- for i_plugins, item in enumerate(list(iter_plugin_entry_points())):
+ for i_plugin, item in enumerate(list(iter_plugin_entry_points())):
if item.value.replace(':setup', '') in plugins_to_use:
if item.module not in _installed_plugins:
_installed_plugins.add(item.name)
@@ -103,7 +103,7 @@ def load_plugins(splash=None, require_qt_plugins=False, plugins_to_use=None):
_loaded_plugins.add(item.module)
if splash is not None:
- splash.set_progress(100. * iplugin / float(n_plugins))
+ splash.set_progress(100. * i_plugin / float(n_plugins))
try:
config.save()
diff --git a/glue/plugins/coordinate_helpers/link_helpers.py b/glue/plugins/coordinate_helpers/link_helpers.py
index 1ac9bea9d..217a798f6 100644
--- a/glue/plugins/coordinate_helpers/link_helpers.py
+++ b/glue/plugins/coordinate_helpers/link_helpers.py
@@ -21,12 +21,12 @@ class BaseCelestialMultiLink(BaseMultiLink):
def forwards(self, in_lon, in_lat):
cin = self.frame_in(in_lon * u.deg, in_lat * u.deg)
- cout = cin.transform_to(self.frame_out)
+ cout = cin.transform_to(self.frame_out())
return cout.spherical.lon.degree, cout.spherical.lat.degree
def backwards(self, out_lon, out_lat):
cout = self.frame_out(out_lon * u.deg, out_lat * u.deg)
- cin = cout.transform_to(self.frame_in)
+ cin = cout.transform_to(self.frame_in())
return cin.spherical.lon.degree, cin.spherical.lat.degree
# Backward-compatibility with glue-core <0.15
diff --git a/glue/plugins/dendro_viewer/qt/__init__.py b/glue/plugins/dendro_viewer/qt/__init__.py
deleted file mode 100644
index ba953ec70..000000000
--- a/glue/plugins/dendro_viewer/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.plugins.dendro_viewer.qt is deprecated, use glue_qt.plugins.dendro_viewer instead', GlueDeprecationWarning)
-from glue_qt.plugins.dendro_viewer import * # noqa
diff --git a/glue/plugins/dendro_viewer/qt/data_viewer.py b/glue/plugins/dendro_viewer/qt/data_viewer.py
deleted file mode 100644
index 52478e644..000000000
--- a/glue/plugins/dendro_viewer/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.plugins.dendro_viewer.qt.data_viewer is deprecated, use glue_qt.plugins.dendro_viewer.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.plugins.dendro_viewer.data_viewer import * # noqa
diff --git a/glue/plugins/dendro_viewer/qt/layer_style_editor.py b/glue/plugins/dendro_viewer/qt/layer_style_editor.py
deleted file mode 100644
index c72401393..000000000
--- a/glue/plugins/dendro_viewer/qt/layer_style_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.plugins.dendro_viewer.qt.layer_style_editor is deprecated, use glue_qt.plugins.dendro_viewer.layer_style_editor instead', GlueDeprecationWarning)
-from glue_qt.plugins.dendro_viewer.layer_style_editor import * # noqa
diff --git a/glue/plugins/dendro_viewer/qt/options_widget.py b/glue/plugins/dendro_viewer/qt/options_widget.py
deleted file mode 100644
index 1d76ee37a..000000000
--- a/glue/plugins/dendro_viewer/qt/options_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.plugins.dendro_viewer.qt.options_widget is deprecated, use glue_qt.plugins.dendro_viewer.options_widget instead', GlueDeprecationWarning)
-from glue_qt.plugins.dendro_viewer.options_widget import * # noqa
diff --git a/glue/plugins/tools/pv_slicer/qt/__init__.py b/glue/plugins/tools/pv_slicer/qt/__init__.py
deleted file mode 100644
index 031c55332..000000000
--- a/glue/plugins/tools/pv_slicer/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.plugins.tools.pv_slicer.qt is deprecated, use glue_qt.plugins.tools.pv_slicer instead', GlueDeprecationWarning)
-from glue_qt.plugins.tools.pv_slicer import * # noqa
diff --git a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py b/glue/plugins/tools/pv_slicer/qt/pv_slicer.py
deleted file mode 100644
index 12c531e6c..000000000
--- a/glue/plugins/tools/pv_slicer/qt/pv_slicer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.plugins.tools.pv_slicer.qt.pv_slicer is deprecated, use glue_qt.plugins.tools.pv_slicer.pv_slicer instead', GlueDeprecationWarning)
-from glue_qt.plugins.tools.pv_slicer.pv_slicer import * # noqa
diff --git a/glue/qglue.py b/glue/qglue.py
deleted file mode 100644
index 66dc0e175..000000000
--- a/glue/qglue.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-
-warnings.warn('Importing from glue.qglue is deprecated, use glue_qt.qglue instead', GlueDeprecationWarning)
-
-from glue_qt.qglue import * # noqa
-
-
-def parse_data(*args, **kwargs):
- warnings.warn('glue.qglue.parse_data is deprecated, use glue.core.parsers.parse_data instead', GlueDeprecationWarning)
- from glue.core.parsers import parse_data
- return parse_data(*args, **kwargs)
-
-
-def parse_links(*args, **kwargs):
- warnings.warn('glue.qglue.parse_links is deprecated, use glue.core.parsers.parse_links instead', GlueDeprecationWarning)
- from glue.core.parsers import parse_links
- return parse_links(*args, **kwargs)
diff --git a/glue/tests/visual/__init__.py b/glue/tests/visual/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/glue/tests/visual/helpers.py b/glue/tests/visual/helpers.py
new file mode 100644
index 000000000..e6601995d
--- /dev/null
+++ b/glue/tests/visual/helpers.py
@@ -0,0 +1,46 @@
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+
+from functools import wraps
+
+import pytest
+
+try:
+ import pytest_mpl # noqa
+except ImportError:
+ HAS_PYTEST_MPL = False
+else:
+ HAS_PYTEST_MPL = True
+
+
+def visual_test(*args, **kwargs):
+ """
+ A decorator that defines a visual test.
+
+ This automatically decorates tests with mpl_image_compare with common
+ options used by all figure tests in glue-core.
+ """
+
+ tolerance = kwargs.pop("tolerance", 0)
+ style = kwargs.pop("style", {})
+ savefig_kwargs = kwargs.pop("savefig_kwargs", {})
+ savefig_kwargs["metadata"] = {"Software": None}
+
+ def decorator(test_function):
+ @pytest.mark.mpl_image_compare(
+ tolerance=tolerance, style=style, savefig_kwargs=savefig_kwargs, **kwargs
+ )
+ @pytest.mark.skipif(
+ not HAS_PYTEST_MPL, reason="pytest-mpl is required for the figure tests"
+ )
+ @wraps(test_function)
+ def test_wrapper(*args, **kwargs):
+ return test_function(*args, **kwargs)
+
+ return test_wrapper
+
+ # If the decorator was used without any arguments, the only positional
+ # argument will be the test to decorate so we do the following:
+ if len(args) == 1:
+ return decorator(*args)
+
+ return decorator
diff --git a/glue/tests/visual/py311-test-visual.json b/glue/tests/visual/py311-test-visual.json
new file mode 100644
index 000000000..f0cff7ef1
--- /dev/null
+++ b/glue/tests/visual/py311-test-visual.json
@@ -0,0 +1,9 @@
+{
+ "glue.viewers.histogram.tests.test_viewer.test_simple_viewer": "cb08123fbad135ab614bb7ec13475fcc83321057d884fe80c3a32970b2d14762",
+ "glue.viewers.image.tests.test_viewer.test_simple_viewer": "72abd60b484d14f721254f027bb0ab9b36245d5db77eb87693f4dd9998fd28be",
+ "glue.viewers.image.tests.test_viewer.test_region_layer": "0114922ab0a3980f56252656c69545927841aea0e6950250cdc2b1bafcd19d50",
+ "glue.viewers.image.tests.test_viewer.test_region_layer_flip": "a142142f34961aba7e98188ad43abafe0e6e5b82e13e8cdab5131d297ed5832c",
+ "glue.viewers.profile.tests.test_viewer.test_simple_viewer": "f68a21be5080fec513388b2d2b220512e7b0df5498e2489da54e58708de435b3",
+ "glue.viewers.scatter.tests.test_viewer.test_simple_viewer": "1020a7bd3abe40510b9e03047c3b423b75c3c64ac18e6dcd6257173cec1ed53f",
+ "glue.viewers.scatter.tests.test_viewer.test_scatter_density_map": "3379d655262769a6ccbdbaf1970bffa9237adbec23a93d3ab75da51b9a3e7f8b"
+}
diff --git a/glue/utils/qt/__init__.py b/glue/utils/qt/__init__.py
deleted file mode 100644
index 95cc710cd..000000000
--- a/glue/utils/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt is deprecated, use glue_qt.utils instead', GlueDeprecationWarning)
-from glue_qt.utils import * # noqa
diff --git a/glue/utils/qt/app.py b/glue/utils/qt/app.py
deleted file mode 100644
index 1ade6b52a..000000000
--- a/glue/utils/qt/app.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.app is deprecated, use glue_qt.utils.app instead', GlueDeprecationWarning)
-from glue_qt.utils.app import * # noqa
diff --git a/glue/utils/qt/autocomplete_widget.py b/glue/utils/qt/autocomplete_widget.py
deleted file mode 100644
index 07c699e3a..000000000
--- a/glue/utils/qt/autocomplete_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.autocomplete_widget is deprecated, use glue_qt.utils.autocomplete_widget instead', GlueDeprecationWarning)
-from glue_qt.utils.autocomplete_widget import * # noqa
diff --git a/glue/utils/qt/colors.py b/glue/utils/qt/colors.py
deleted file mode 100644
index acd5d2aad..000000000
--- a/glue/utils/qt/colors.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.colors is deprecated, use glue_qt.utils.colors instead', GlueDeprecationWarning)
-from glue_qt.utils.colors import * # noqa
diff --git a/glue/utils/qt/decorators.py b/glue/utils/qt/decorators.py
deleted file mode 100644
index 36f64a5d1..000000000
--- a/glue/utils/qt/decorators.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.decorators is deprecated, use glue_qt.utils.decorators instead', GlueDeprecationWarning)
-from glue_qt.utils.decorators import * # noqa
diff --git a/glue/utils/qt/delegates.py b/glue/utils/qt/delegates.py
deleted file mode 100644
index 8906740dc..000000000
--- a/glue/utils/qt/delegates.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.delegates is deprecated, use glue_qt.utils.delegates instead', GlueDeprecationWarning)
-from glue_qt.utils.delegates import * # noqa
diff --git a/glue/utils/qt/dialogs.py b/glue/utils/qt/dialogs.py
deleted file mode 100644
index dcd1d13b9..000000000
--- a/glue/utils/qt/dialogs.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.dialogs is deprecated, use glue_qt.utils.dialogs instead', GlueDeprecationWarning)
-from glue_qt.utils.dialogs import * # noqa
diff --git a/glue/utils/qt/helpers.py b/glue/utils/qt/helpers.py
deleted file mode 100644
index 07138b9d9..000000000
--- a/glue/utils/qt/helpers.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.helpers is deprecated, use glue_qt.utils.helpers instead', GlueDeprecationWarning)
-from glue_qt.utils.helpers import * # noqa
diff --git a/glue/utils/qt/mime.py b/glue/utils/qt/mime.py
deleted file mode 100644
index 10a67dd2b..000000000
--- a/glue/utils/qt/mime.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.mime is deprecated, use glue_qt.utils.mime instead', GlueDeprecationWarning)
-from glue_qt.utils.mime import * # noqa
diff --git a/glue/utils/qt/mixins.py b/glue/utils/qt/mixins.py
deleted file mode 100644
index 0037a89ae..000000000
--- a/glue/utils/qt/mixins.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.mixins is deprecated, use glue_qt.utils.mixins instead', GlueDeprecationWarning)
-from glue_qt.utils.mixins import * # noqa
diff --git a/glue/utils/qt/python_list_model.py b/glue/utils/qt/python_list_model.py
deleted file mode 100644
index a3ad8699b..000000000
--- a/glue/utils/qt/python_list_model.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.python_list_model is deprecated, use glue_qt.utils.python_list_model instead', GlueDeprecationWarning)
-from glue_qt.utils.python_list_model import * # noqa
diff --git a/glue/utils/qt/threading.py b/glue/utils/qt/threading.py
deleted file mode 100644
index 99e05300c..000000000
--- a/glue/utils/qt/threading.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.threading is deprecated, use glue_qt.utils.threading instead', GlueDeprecationWarning)
-from glue_qt.utils.threading import * # noqa
diff --git a/glue/utils/qt/widget_properties.py b/glue/utils/qt/widget_properties.py
deleted file mode 100644
index 6bb8f6b32..000000000
--- a/glue/utils/qt/widget_properties.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.utils.qt.widget_properties is deprecated, use glue_qt.utils.widget_properties instead', GlueDeprecationWarning)
-from glue_qt.utils.widget_properties import * # noqa
diff --git a/glue/viewers/common/layer_artist.py b/glue/viewers/common/layer_artist.py
index 3a281c02c..96215424e 100644
--- a/glue/viewers/common/layer_artist.py
+++ b/glue/viewers/common/layer_artist.py
@@ -1,4 +1,4 @@
-from echo import keep_in_sync, CallbackProperty
+from echo import keep_in_sync, CallbackProperty, CallbackDict
from glue.core.layer_artist import LayerArtistBase
from glue.viewers.common.state import LayerState
from glue.core.message import LayerArtistVisibilityMessage
@@ -78,4 +78,15 @@ def pop_changed_properties(self):
self._last_viewer_state.update(self._viewer_state.as_dict())
self._last_layer_state.update(self.state.as_dict())
+ # If any of the items are CallbackDict, we make a copy otherwise both
+ # the 'last' and new values will remain the same.
+
+ for key, value in self._last_viewer_state.items():
+ if isinstance(value, CallbackDict):
+ self._last_viewer_state[key] = dict(value)
+
+ for key, value in self._last_layer_state.items():
+ if isinstance(value, CallbackDict):
+ self._last_layer_state[key] = dict(value)
+
return changed
diff --git a/glue/viewers/common/qt/__init__.py b/glue/viewers/common/qt/__init__.py
deleted file mode 100644
index 63c3cdd0c..000000000
--- a/glue/viewers/common/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt is deprecated, use glue_qt.viewers.common instead', GlueDeprecationWarning)
-from glue_qt.viewers.common import * # noqa
diff --git a/glue/viewers/common/qt/base_widget.py b/glue/viewers/common/qt/base_widget.py
deleted file mode 100644
index a863189d7..000000000
--- a/glue/viewers/common/qt/base_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.base_widget is deprecated, use glue_qt.viewers.common.base_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.base_widget import * # noqa
diff --git a/glue/viewers/common/qt/data_slice_widget.py b/glue/viewers/common/qt/data_slice_widget.py
deleted file mode 100644
index edad718b0..000000000
--- a/glue/viewers/common/qt/data_slice_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.data_slice_widget is deprecated, use glue_qt.viewers.common.data_slice_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.data_slice_widget import * # noqa
diff --git a/glue/viewers/common/qt/data_viewer.py b/glue/viewers/common/qt/data_viewer.py
deleted file mode 100644
index 8e9076d65..000000000
--- a/glue/viewers/common/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.data_viewer is deprecated, use glue_qt.viewers.common.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.data_viewer import * # noqa
diff --git a/glue/viewers/common/qt/data_viewer_with_state.py b/glue/viewers/common/qt/data_viewer_with_state.py
deleted file mode 100644
index 1e56224d8..000000000
--- a/glue/viewers/common/qt/data_viewer_with_state.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.data_viewer_with_state is deprecated, use glue_qt.viewers.common.data_viewer_with_state instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.data_viewer_with_state import * # noqa
diff --git a/glue/viewers/common/qt/tool.py b/glue/viewers/common/qt/tool.py
deleted file mode 100644
index 00e222683..000000000
--- a/glue/viewers/common/qt/tool.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.tool is deprecated, use glue_qt.viewers.common.tool instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.tool import * # noqa
diff --git a/glue/viewers/common/qt/toolbar.py b/glue/viewers/common/qt/toolbar.py
deleted file mode 100644
index a8024f693..000000000
--- a/glue/viewers/common/qt/toolbar.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.toolbar is deprecated, use glue_qt.viewers.common.toolbar instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.toolbar import * # noqa
diff --git a/glue/viewers/common/qt/tools.py b/glue/viewers/common/qt/tools.py
deleted file mode 100644
index fcfdba44e..000000000
--- a/glue/viewers/common/qt/tools.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.common.qt.tools is deprecated, use glue_qt.viewers.common.tools instead', GlueDeprecationWarning)
-from glue_qt.viewers.common.tools import * # noqa
diff --git a/glue/viewers/common/stretch_state_mixin.py b/glue/viewers/common/stretch_state_mixin.py
new file mode 100644
index 000000000..9fe45b3ff
--- /dev/null
+++ b/glue/viewers/common/stretch_state_mixin.py
@@ -0,0 +1,47 @@
+from glue.config import stretches
+from glue.viewers.matplotlib.state import (
+ DeferredDrawDictCallbackProperty as DDDCProperty,
+ DeferredDrawSelectionCallbackProperty as DDSCProperty,
+)
+
+__all__ = ["StretchStateMixin"]
+
+
+class StretchStateMixin:
+ stretch = DDSCProperty(
+ docstring="The stretch used to render the layer, "
+ "which should be one of ``linear``, "
+ "``sqrt``, ``log``, or ``arcsinh``"
+ )
+ stretch_parameters = DDDCProperty(
+ docstring="Keyword arguments to pass to the stretch"
+ )
+
+ _stretch_set_up = False
+
+ def setup_stretch_callback(self):
+ type(self).stretch.set_choices(self, list(stretches.members))
+ type(self).stretch.set_display_func(self, stretches.display_func)
+ self._reset_stretch()
+ self.add_callback("stretch", self._reset_stretch)
+ self.add_callback("stretch_parameters", self._sync_stretch_parameters)
+ self._stretch_set_up = True
+
+ @property
+ def stretch_object(self):
+ if not self._stretch_set_up:
+ raise Exception("setup_stretch_callback has not been called")
+ return self._stretch_object
+
+ def _sync_stretch_parameters(self, *args):
+ for key, value in self.stretch_parameters.items():
+ if hasattr(self._stretch_object, key):
+ setattr(self._stretch_object, key, value)
+ else:
+ raise ValueError(
+ f"Stretch object {self._stretch_object.__class__.__name__} has no attribute {key}"
+ )
+
+ def _reset_stretch(self, *args):
+ self._stretch_object = stretches.members[self.stretch]()
+ self.stretch_parameters.clear()
diff --git a/glue/viewers/common/tests/test_stretch_state_mixin.py b/glue/viewers/common/tests/test_stretch_state_mixin.py
new file mode 100644
index 000000000..4325342ba
--- /dev/null
+++ b/glue/viewers/common/tests/test_stretch_state_mixin.py
@@ -0,0 +1,56 @@
+import pytest
+
+from astropy.visualization import LinearStretch, LogStretch
+
+from glue.core.state_objects import State
+from glue.viewers.common.stretch_state_mixin import StretchStateMixin
+
+
+class ExampleStateWithStretch(State, StretchStateMixin):
+ pass
+
+
+def test_not_set_up():
+ state = ExampleStateWithStretch()
+ with pytest.raises(Exception, match="setup_stretch_callback has not been called"):
+ state.stretch_object
+
+
+class TestStretchStateMixin:
+ def setup_method(self, method):
+ self.state = ExampleStateWithStretch()
+ self.state.setup_stretch_callback()
+
+ def test_defaults(self):
+ assert self.state.stretch == "linear"
+ assert len(self.state.stretch_parameters) == 0
+ assert isinstance(self.state.stretch_object, LinearStretch)
+
+ def test_change_stretch(self):
+ self.state.stretch = "log"
+ assert self.state.stretch == "log"
+ assert len(self.state.stretch_parameters) == 0
+ assert isinstance(self.state.stretch_object, LogStretch)
+
+ def test_invalid_parameter(self):
+ with pytest.raises(
+ ValueError, match="Stretch object LinearStretch has no attribute foo"
+ ):
+ self.state.stretch_parameters["foo"] = 1
+
+ def test_set_parameter(self):
+ self.state.stretch = "log"
+
+ assert self.state.stretch_object.exp == 1000
+
+ # Setting the stretch parameter 'exp' is synced with the stretch object attribute
+ self.state.stretch_parameters["exp"] = 200
+ assert self.state.stretch_object.exp == 200
+
+ # Changing stretch resets the stretch parameter dictionary
+ self.state.stretch = "linear"
+ assert len(self.state.stretch_parameters) == 0
+
+ # And there is no memory of previous parameters
+ self.state.stretch = "log"
+ assert self.state.stretch_object.exp == 1000
diff --git a/glue/viewers/common/tests/test_viewer.py b/glue/viewers/common/tests/test_viewer.py
index a7d6ad412..61f18de69 100644
--- a/glue/viewers/common/tests/test_viewer.py
+++ b/glue/viewers/common/tests/test_viewer.py
@@ -3,6 +3,7 @@
from glue.core import Data
from glue.viewers.common.viewer import Viewer
from glue.viewers.common.layer_artist import LayerArtist
+from glue.core.message import NumericalDataChangedMessage
def test_custom_layer_artist_maker():
@@ -45,3 +46,60 @@ def custom_maker(viewer, data):
viewer.add_data(data2)
assert len(viewer.layers) == 2
assert type(viewer.layers[1]) is CustomLayerArtist
+
+
+def test_viewer_update_data():
+ """
+ Test that we can have a LayerArtist that can respond
+ to a NumericalDataChangedMessage.
+ """
+
+ class CustomUpdateLayerArtist(LayerArtist):
+
+ def _on_components_changed(self, components_changed):
+ self.called_component_limits = True
+ self.num_components_changed = len(components_changed)
+
+ class CustomApplication(Application):
+ def add_widget(self, *args, **kwargs):
+ pass
+
+ @layer_artist_maker('custom_maker_for_update')
+ def custom_maker_for_update(viewer, data):
+ if hasattr(data, 'custom_for_update'):
+ return CustomUpdateLayerArtist(viewer.state, layer=data)
+ if hasattr(data.data, 'custom_for_update'):
+ return CustomUpdateLayerArtist(viewer.state, layer=data)
+
+ app = CustomApplication()
+
+ data1 = Data(x=[1, 2, 3], label='test1')
+ data1.custom_for_update = True
+
+ app.data_collection.append(data1)
+
+ viewer = app.new_data_viewer(Viewer)
+
+ assert len(viewer.layers) == 0
+
+ # NOTE: Check exact type, not using isinstance
+ viewer.add_data(data1)
+ assert len(viewer.layers) == 1
+ assert type(viewer.layers[0]) is CustomUpdateLayerArtist
+
+ msg = NumericalDataChangedMessage(data1, components_changed=[data1.id['x']])
+
+ viewer._update_data(msg)
+ assert viewer.layers[0].called_component_limits
+ assert viewer.layers[0].num_components_changed == 1
+
+ subset = data1.new_subset()
+ subset.subset_state = data1.id['x'] > 2
+
+ assert len(viewer.layers) == 2
+ assert type(viewer.layers[1]) is CustomUpdateLayerArtist
+
+ msg = NumericalDataChangedMessage(data1, components_changed=[data1.id['x']])
+ viewer._update_data(msg)
+ assert viewer.layers[1].called_component_limits
+ assert viewer.layers[1].num_components_changed == 1
diff --git a/glue/viewers/common/viewer.py b/glue/viewers/common/viewer.py
index cc2aa4b57..71703c8d5 100644
--- a/glue/viewers/common/viewer.py
+++ b/glue/viewers/common/viewer.py
@@ -288,9 +288,20 @@ def _update_data(self, message):
if isinstance(layer_artist.layer, Subset):
if layer_artist.layer.data is message.data:
layer_artist.update()
+ try:
+ components_changed = message.components_changed
+ layer_artist._on_components_changed(components_changed)
+ except AttributeError:
+ pass
+
else:
if layer_artist.layer is message.data:
layer_artist.update()
+ try:
+ components_changed = message.components_changed
+ layer_artist._on_components_changed(components_changed)
+ except AttributeError:
+ pass
def _update_subset(self, message):
if message.attribute == 'style':
diff --git a/glue/viewers/custom/qt/__init__.py b/glue/viewers/custom/qt/__init__.py
deleted file mode 100644
index b3bf0b8ed..000000000
--- a/glue/viewers/custom/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.custom.qt is deprecated, use glue_qt.viewers.custom instead', GlueDeprecationWarning)
-from glue_qt.viewers.custom import * # noqa
diff --git a/glue/viewers/custom/qt/custom_viewer.py b/glue/viewers/custom/qt/custom_viewer.py
deleted file mode 100644
index 07313bde2..000000000
--- a/glue/viewers/custom/qt/custom_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.custom.qt.custom_viewer is deprecated, use glue_qt.viewers.custom.custom_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.custom.custom_viewer import * # noqa
diff --git a/glue/viewers/custom/qt/elements.py b/glue/viewers/custom/qt/elements.py
deleted file mode 100644
index 1c2d688c9..000000000
--- a/glue/viewers/custom/qt/elements.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.custom.qt.elements is deprecated, use glue_qt.viewers.custom.elements instead', GlueDeprecationWarning)
-from glue_qt.viewers.custom.elements import * # noqa
diff --git a/glue/viewers/histogram/layer_artist.py b/glue/viewers/histogram/layer_artist.py
index 2496f6de6..69ef0a22c 100644
--- a/glue/viewers/histogram/layer_artist.py
+++ b/glue/viewers/histogram/layer_artist.py
@@ -12,6 +12,8 @@
from glue.viewers.matplotlib.layer_artist import MatplotlibLayerArtist
from glue.core.exceptions import IncompatibleAttribute, IncompatibleDataException
+__all__ = ["HistogramLayerArtist"]
+
class HistogramLayerArtist(MatplotlibLayerArtist):
diff --git a/glue/viewers/histogram/qt/__init__.py b/glue/viewers/histogram/qt/__init__.py
deleted file mode 100644
index 96226f2e9..000000000
--- a/glue/viewers/histogram/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.histogram.qt is deprecated, use glue_qt.viewers.histogram instead', GlueDeprecationWarning)
-from glue_qt.viewers.histogram import * # noqa
diff --git a/glue/viewers/histogram/qt/data_viewer.py b/glue/viewers/histogram/qt/data_viewer.py
deleted file mode 100644
index c294f9bfb..000000000
--- a/glue/viewers/histogram/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.histogram.qt.data_viewer is deprecated, use glue_qt.viewers.histogram.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.histogram.data_viewer import * # noqa
diff --git a/glue/viewers/histogram/qt/layer_artist.py b/glue/viewers/histogram/qt/layer_artist.py
deleted file mode 100644
index 693af623e..000000000
--- a/glue/viewers/histogram/qt/layer_artist.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.histogram.qt.layer_artist is deprecated, use glue_qt.viewers.histogram.layer_artist instead', GlueDeprecationWarning)
-from glue_qt.viewers.histogram.layer_artist import * # noqa
diff --git a/glue/viewers/histogram/qt/layer_style_editor.py b/glue/viewers/histogram/qt/layer_style_editor.py
deleted file mode 100644
index 96f7f1f29..000000000
--- a/glue/viewers/histogram/qt/layer_style_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.histogram.qt.layer_style_editor is deprecated, use glue_qt.viewers.histogram.layer_style_editor instead', GlueDeprecationWarning)
-from glue_qt.viewers.histogram.layer_style_editor import * # noqa
diff --git a/glue/viewers/histogram/qt/options_widget.py b/glue/viewers/histogram/qt/options_widget.py
deleted file mode 100644
index 3a7c880eb..000000000
--- a/glue/viewers/histogram/qt/options_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.histogram.qt.options_widget is deprecated, use glue_qt.viewers.histogram.options_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.histogram.options_widget import * # noqa
diff --git a/glue/viewers/histogram/state.py b/glue/viewers/histogram/state.py
index 730a8cfbd..5e2b38754 100644
--- a/glue/viewers/histogram/state.py
+++ b/glue/viewers/histogram/state.py
@@ -38,6 +38,10 @@ class HistogramViewerState(MatplotlibDataViewerState):
common_n_bin = DDCProperty(True, docstring='The number of bins to use for '
'all numerical components')
+ x_limits_percentile = DDCProperty(100, docstring="Percentile to use when automatically determining x limits")
+
+ update_bins_on_reset_limits = DDCProperty(True, docstring="Whether to update the bins to match the view when resetting limits")
+
def __init__(self, **kwargs):
super(HistogramViewerState, self).__init__()
@@ -67,9 +71,10 @@ def _reset_x_limits(self, *args):
if self.x_att is None:
return
with delay_callback(self, 'hist_x_min', 'hist_x_max', 'x_min', 'x_max', 'x_log'):
- self.x_lim_helper.percentile = 100
+ self.x_lim_helper.percentile = self.x_limits_percentile
self.x_lim_helper.update_values(force=True)
- self.update_bins_to_view()
+ if self.update_bins_on_reset_limits:
+ self.update_bins_to_view()
def reset_limits(self):
self._reset_x_limits()
diff --git a/glue/viewers/histogram/tests/test_viewer.py b/glue/viewers/histogram/tests/test_viewer.py
new file mode 100644
index 000000000..b3fedb578
--- /dev/null
+++ b/glue/viewers/histogram/tests/test_viewer.py
@@ -0,0 +1,121 @@
+import numpy as np
+from numpy.testing import assert_allclose
+
+from astropy.utils import NumpyRNGContext
+
+from glue.tests.visual.helpers import visual_test
+from glue.viewers.histogram.viewer import SimpleHistogramViewer
+from glue.core.application_base import Application
+from glue.core.data import Data
+
+
+@visual_test
+def test_simple_viewer():
+
+ # Make sure the simple viewer can be instantiated
+
+ with NumpyRNGContext(12345):
+
+ data1 = Data(x=np.random.normal(1, 2, 1000), label='data1')
+ data2 = Data(y=np.random.uniform(-1, 5, 1000), label='data2')
+
+ app = Application()
+ app.data_collection.append(data1)
+ app.data_collection.append(data2)
+
+ viewer = app.new_data_viewer(SimpleHistogramViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ app.data_collection.new_subset_group(label='subset1', subset_state=data1.id['x'] > 2)
+
+ return viewer.figure
+
+
+def test_remove_data_collection():
+
+ # Regression test for a bug that caused an IncompatibleAttribute
+ # error when updating the number of bins in a histogram after
+ # removing a dataset from the DataCollection (this was due to
+ # a caching issue)
+
+ data1 = Data(x=[1, 2, 3], label='data1')
+ data2 = Data(y=[1, 2, 3], label='data2')
+
+ app = Application()
+ app.data_collection.append(data1)
+ app.data_collection.append(data2)
+
+ viewer = app.new_data_viewer(SimpleHistogramViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ viewer.state.hist_n_bin = 30
+
+ app.data_collection.remove(data1)
+
+ viewer.state.hist_n_bin = 20
+
+
+def test_incompatible_datasets():
+
+ # Regression test for a bug that caused an IncompatibleAttribute
+ # error when changing the dataset used in the histogram viewer to one that
+ # is not linked to the first dataset.
+
+ data1 = Data(x=[1, 2, 3], label='data1')
+ data2 = Data(y=[1, 2, 3], label='data2')
+
+ app = Application()
+ app.data_collection.append(data1)
+ app.data_collection.append(data2)
+
+ viewer = app.new_data_viewer(SimpleHistogramViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ viewer.state.x_att = data1.id['x']
+
+ viewer.state.hist_n_bin = 30
+
+ viewer.state.x_att = data2.id['y']
+
+ viewer.state.hist_n_bin = 20
+
+
+def test_reset_limits():
+
+ data1 = Data(x=np.arange(1000), label='data')
+
+ app = Application()
+ app.data_collection.append(data1)
+
+ viewer = app.new_data_viewer(SimpleHistogramViewer)
+ viewer.add_data(data1)
+
+ viewer.state.reset_limits()
+
+ assert_allclose(viewer.state.x_min, 0)
+ assert_allclose(viewer.state.x_max, 999)
+
+ viewer.state.x_limits_percentile = 90
+
+ viewer.state.reset_limits()
+
+ assert_allclose(viewer.state.x_min, 49.95)
+ assert_allclose(viewer.state.x_max, 949.05)
+
+ assert_allclose(viewer.state.hist_x_min, 49.95)
+ assert_allclose(viewer.state.hist_x_max, 949.05)
+
+ viewer.state.update_bins_on_reset_limits = False
+
+ viewer.state.x_limits_percentile = 80
+
+ viewer.state.reset_limits()
+
+ assert_allclose(viewer.state.x_min, 99.9)
+ assert_allclose(viewer.state.x_max, 899.1)
+
+ assert_allclose(viewer.state.hist_x_min, 49.95)
+ assert_allclose(viewer.state.hist_x_max, 949.05)
diff --git a/glue/viewers/histogram/viewer.py b/glue/viewers/histogram/viewer.py
index 2b9b10478..3f15fb884 100644
--- a/glue/viewers/histogram/viewer.py
+++ b/glue/viewers/histogram/viewer.py
@@ -3,9 +3,12 @@
from glue.core.subset import roi_to_subset_state
from glue.utils import mpl_to_datetime64
+from glue.viewers.matplotlib.viewer import SimpleMatplotlibViewer
from glue.viewers.histogram.compat import update_histogram_viewer_state
+from glue.viewers.histogram.layer_artist import HistogramLayerArtist
+from glue.viewers.histogram.state import HistogramViewerState
-__all__ = ['MatplotlibHistogramMixin']
+__all__ = ['MatplotlibHistogramMixin', 'SimpleHistogramViewer']
class MatplotlibHistogramMixin(object):
@@ -69,3 +72,14 @@ def apply_roi(self, roi, override_mode=None):
@staticmethod
def update_viewer_state(rec, context):
return update_histogram_viewer_state(rec, context)
+
+
+class SimpleHistogramViewer(MatplotlibHistogramMixin, SimpleMatplotlibViewer):
+
+ _state_cls = HistogramViewerState
+ _data_artist_cls = HistogramLayerArtist
+ _subset_artist_cls = HistogramLayerArtist
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ MatplotlibHistogramMixin.setup_callbacks(self)
diff --git a/glue/viewers/image/composite_array.py b/glue/viewers/image/composite_array.py
index 323bc3348..4ef60c7e0 100644
--- a/glue/viewers/image/composite_array.py
+++ b/glue/viewers/image/composite_array.py
@@ -29,6 +29,7 @@ def __init__(self, **kwargs):
self._first = True
self._mode = 'color'
+ self._allow_bad_alpha = False
@property
def mode(self):
@@ -118,7 +119,10 @@ def __call__(self, bounds=None):
interval = ManualInterval(*layer['clim'])
contrast_bias = ContrastBiasStretch(layer['contrast'], layer['bias'])
- stretch = stretches.members[layer['stretch']]
+ if isinstance(layer['stretch'], str):
+ stretch = stretches.members[layer['stretch']]()
+ else:
+ stretch = layer['stretch']
if callable(layer['array']):
array = layer['array'](bounds=bounds)
@@ -143,10 +147,16 @@ def __call__(self, bounds=None):
# ensure "bad" values have the same alpha as the
# rest of the layer:
if hasattr(layer['cmap'], 'get_bad'):
- bad_color = layer['cmap'].get_bad().tolist()[:3]
- layer_cmap = layer['cmap'].with_extremes(
- bad=bad_color + [layer['alpha']]
- )
+ bad_rgba = layer['cmap'].get_bad().tolist()
+ bad_color = bad_rgba[:3]
+ bad_alpha = bad_rgba[3:]
+
+ if self._allow_bad_alpha:
+ bad_rgba = bad_color + bad_alpha
+ else:
+ bad_rgba = bad_color + [layer['alpha']]
+
+ layer_cmap = layer['cmap'].with_extremes(bad=bad_rgba)
else:
layer_cmap = layer['cmap']
@@ -159,8 +169,7 @@ def __call__(self, bounds=None):
# Check what the smallest colormap alpha value for this layer is
# - if it is 1 then this colormap does not change transparency,
# and this allows us to speed things up a little.
- if layer_cmap(CMAP_SAMPLING)[:, 3].min() == 1:
-
+ if layer_cmap(CMAP_SAMPLING)[:, 3].min() == 1 and (not self._allow_bad_alpha or bad_alpha == 1):
if layer['alpha'] == 1:
img[...] = 0
else:
@@ -172,6 +181,10 @@ def __call__(self, bounds=None):
# Use traditional alpha compositing
alpha_plane = layer['alpha'] * plane[:, :, 3]
+ if self._allow_bad_alpha:
+ # ensure "bad" alpha is preserved:
+ alpha_plane[~np.isfinite(data)] *= bad_alpha
+
plane[:, :, 0] = plane[:, :, 0] * alpha_plane
plane[:, :, 1] = plane[:, :, 1] * alpha_plane
plane[:, :, 2] = plane[:, :, 2] * alpha_plane
diff --git a/glue/viewers/image/layer_artist.py b/glue/viewers/image/layer_artist.py
index ea2c7c452..abee0cad2 100644
--- a/glue/viewers/image/layer_artist.py
+++ b/glue/viewers/image/layer_artist.py
@@ -169,7 +169,7 @@ def _update_visual_attributes(self):
contrast=self.state.contrast,
bias=self.state.bias,
alpha=self.state.alpha,
- stretch=self.state.stretch)
+ stretch=self.state.stretch_object)
self.composite_image.invalidate_cache()
@@ -193,7 +193,7 @@ def _update_image(self, force=False, **kwargs):
if force or any(prop in changed for prop in ('v_min', 'v_max', 'contrast',
'bias', 'alpha', 'color_mode',
'cmap', 'color', 'zorder',
- 'visible', 'stretch')):
+ 'visible', 'stretch', 'stretch_parameters')):
self._update_visual_attributes()
@defer_draw
@@ -238,17 +238,19 @@ def __call__(self, bounds):
if (self.layer_artist is None or
self.layer_state is None or
self.viewer_state is None):
- return np.broadcast_to(np.nan, self.shape)
+ return None
# We should compute the mask even if the layer is not visible as we need
# the layer to show up properly when it is made visible (which doesn't
- # trigger __getitem__)
+ # trigger __getitem__). However, if the layer is disabled, then we will
+ # call this method when it is enabled, so in this case we just return
+ # an empty mask.
try:
mask = self.layer_state.get_sliced_data(bounds=bounds)
except IncompatibleAttribute:
self.layer_artist.disable_incompatible_subset()
- return np.broadcast_to(np.nan, self.shape)
+ return None
else:
self.layer_artist.enable(redraw=False)
diff --git a/glue/viewers/image/qt/__init__.py b/glue/viewers/image/qt/__init__.py
deleted file mode 100644
index 5444f7485..000000000
--- a/glue/viewers/image/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt is deprecated, use glue_qt.viewers.image instead', GlueDeprecationWarning)
-from glue_qt.viewers.image import * # noqa
diff --git a/glue/viewers/image/qt/contrast_mouse_mode.py b/glue/viewers/image/qt/contrast_mouse_mode.py
deleted file mode 100644
index 37d452705..000000000
--- a/glue/viewers/image/qt/contrast_mouse_mode.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.contrast_mouse_mode is deprecated, use glue_qt.viewers.image.contrast_mouse_mode instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.contrast_mouse_mode import * # noqa
diff --git a/glue/viewers/image/qt/data_viewer.py b/glue/viewers/image/qt/data_viewer.py
deleted file mode 100644
index 7499d72a3..000000000
--- a/glue/viewers/image/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.data_viewer is deprecated, use glue_qt.viewers.image.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.data_viewer import * # noqa
diff --git a/glue/viewers/image/qt/layer_style_editor.py b/glue/viewers/image/qt/layer_style_editor.py
deleted file mode 100644
index 253591c36..000000000
--- a/glue/viewers/image/qt/layer_style_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.layer_style_editor is deprecated, use glue_qt.viewers.image.layer_style_editor instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.layer_style_editor import * # noqa
diff --git a/glue/viewers/image/qt/layer_style_editor_subset.py b/glue/viewers/image/qt/layer_style_editor_subset.py
deleted file mode 100644
index 8713eb546..000000000
--- a/glue/viewers/image/qt/layer_style_editor_subset.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.layer_style_editor_subset is deprecated, use glue_qt.viewers.image.layer_style_editor_subset instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.layer_style_editor_subset import * # noqa
diff --git a/glue/viewers/image/qt/mouse_mode.py b/glue/viewers/image/qt/mouse_mode.py
deleted file mode 100644
index bf4789933..000000000
--- a/glue/viewers/image/qt/mouse_mode.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.mouse_mode is deprecated, use glue_qt.viewers.image.mouse_mode instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.mouse_mode import * # noqa
diff --git a/glue/viewers/image/qt/options_widget.py b/glue/viewers/image/qt/options_widget.py
deleted file mode 100644
index b157c97de..000000000
--- a/glue/viewers/image/qt/options_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.options_widget is deprecated, use glue_qt.viewers.image.options_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.options_widget import * # noqa
diff --git a/glue/viewers/image/qt/pixel_selection_mode.py b/glue/viewers/image/qt/pixel_selection_mode.py
deleted file mode 100644
index 4bad22fc4..000000000
--- a/glue/viewers/image/qt/pixel_selection_mode.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.pixel_selection_mode is deprecated, use glue_qt.viewers.image.pixel_selection_mode instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.pixel_selection_mode import * # noqa
diff --git a/glue/viewers/image/qt/profile_viewer_tool.py b/glue/viewers/image/qt/profile_viewer_tool.py
deleted file mode 100644
index c89021d39..000000000
--- a/glue/viewers/image/qt/profile_viewer_tool.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.profile_viewer_tool is deprecated, use glue_qt.viewers.image.profile_viewer_tool instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.profile_viewer_tool import * # noqa
diff --git a/glue/viewers/image/qt/slice_widget.py b/glue/viewers/image/qt/slice_widget.py
deleted file mode 100644
index 692f4ea6c..000000000
--- a/glue/viewers/image/qt/slice_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.slice_widget is deprecated, use glue_qt.viewers.image.slice_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.slice_widget import * # noqa
diff --git a/glue/viewers/image/qt/standalone_image_viewer.py b/glue/viewers/image/qt/standalone_image_viewer.py
deleted file mode 100644
index 437466bba..000000000
--- a/glue/viewers/image/qt/standalone_image_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.image.qt.standalone_image_viewer is deprecated, use glue_qt.viewers.image.standalone_image_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.image.standalone_image_viewer import * # noqa
diff --git a/glue/viewers/image/state.py b/glue/viewers/image/state.py
index ecb1db944..45f29dba0 100644
--- a/glue/viewers/image/state.py
+++ b/glue/viewers/image/state.py
@@ -2,7 +2,7 @@
from collections import defaultdict
from glue.core import BaseData
-from glue.config import colormaps, stretches
+from glue.config import colormaps
from glue.viewers.matplotlib.state import (MatplotlibDataViewerState,
MatplotlibLayerState,
DeferredDrawCallbackProperty as DDCProperty,
@@ -12,6 +12,7 @@
from echo import delay_callback
from glue.core.data_combo_helper import ManualDataComboHelper, ComponentIDComboHelper
from glue.core.exceptions import IncompatibleDataException
+from glue.viewers.common.stretch_state_mixin import StretchStateMixin
__all__ = ['ImageViewerState', 'ImageLayerState', 'ImageSubsetLayerState', 'AggregateSlice']
@@ -481,7 +482,7 @@ def slice_to_bound(slc, size):
return image
-class ImageLayerState(BaseImageLayerState):
+class ImageLayerState(BaseImageLayerState, StretchStateMixin):
"""
A state class that includes all the attributes for data layers in an image plot.
"""
@@ -495,9 +496,6 @@ class ImageLayerState(BaseImageLayerState):
bias = DDCProperty(0.5, docstring='A constant value that is added to the '
'layer before rendering')
cmap = DDCProperty(docstring='The colormap used to render the layer')
- stretch = DDSCProperty(docstring='The stretch used to render the layer, '
- 'which should be one of ``linear``, '
- '``sqrt``, ``log``, or ``arcsinh``')
global_sync = DDCProperty(False, docstring='Whether the color and transparency '
'should be synced with the global '
'color and transparency for the data')
@@ -525,8 +523,7 @@ def __init__(self, layer=None, viewer_state=None, **kwargs):
ImageLayerState.percentile.set_choices(self, [100, 99.5, 99, 95, 90, 'Custom'])
ImageLayerState.percentile.set_display_func(self, percentile_display.get)
- ImageLayerState.stretch.set_choices(self, list(stretches.members))
- ImageLayerState.stretch.set_display_func(self, stretches.display_func)
+ self.setup_stretch_callback()
self.add_callback('global_sync', self._update_syncing)
self.add_callback('layer', self._update_attribute)
diff --git a/glue/viewers/image/tests/test_viewer.py b/glue/viewers/image/tests/test_viewer.py
new file mode 100644
index 000000000..ec8b38133
--- /dev/null
+++ b/glue/viewers/image/tests/test_viewer.py
@@ -0,0 +1,99 @@
+import numpy as np
+from echo import delay_callback
+from glue.tests.visual.helpers import visual_test
+from glue.viewers.image.viewer import SimpleImageViewer
+from glue.core.application_base import Application
+from glue.core.data import Data
+from glue.core.link_helpers import LinkSame
+from glue.core.data_region import RegionData
+
+from shapely.geometry import Polygon, MultiPolygon
+
+
+@visual_test
+def test_simple_viewer():
+
+ # Make sure the simple viewer can be instantiated
+
+ data1 = Data(x=np.arange(6).reshape((2, 3)), label='data1')
+ data2 = Data(y=2 * np.arange(6).reshape((2, 3)), label='data2')
+
+ app = Application()
+ app.data_collection.append(data1)
+ app.data_collection.append(data2)
+
+ viewer = app.new_data_viewer(SimpleImageViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ app.data_collection.new_subset_group(label='subset1', subset_state=data1.pixel_component_ids[1] > 1.2)
+
+ return viewer.figure
+
+
+@visual_test
+def test_region_layer():
+ poly_1 = Polygon([(20, 20), (60, 20), (60, 40), (20, 40)])
+ poly_2 = Polygon([(60, 50), (60, 70), (80, 70), (80, 50)])
+ poly_3 = Polygon([(10, 10), (15, 10), (15, 15), (10, 15)])
+ poly_4 = Polygon([(10, 20), (15, 20), (15, 30), (10, 30), (12, 25)])
+
+ polygons = MultiPolygon([poly_3, poly_4])
+
+ geoms = np.array([poly_1, poly_2, polygons])
+ values = np.array([1, 2, 3])
+ region_data = RegionData(regions=geoms, values=values)
+
+ image_data = Data(x=np.arange(10000).reshape((100, 100)), label='data1')
+ app = Application()
+ app.data_collection.append(image_data)
+ app.data_collection.append(region_data)
+
+ link1 = LinkSame(region_data.center_x_id, image_data.pixel_component_ids[0])
+ link2 = LinkSame(region_data.center_y_id, image_data.pixel_component_ids[1])
+ app.data_collection.add_link(link1)
+ app.data_collection.add_link(link2)
+
+ viewer = app.new_data_viewer(SimpleImageViewer)
+ viewer.add_data(image_data)
+ viewer.add_data(region_data)
+
+ return viewer.figure
+
+
+@visual_test
+def test_region_layer_flip():
+ poly_1 = Polygon([(20, 20), (60, 20), (60, 40), (20, 40)])
+ poly_2 = Polygon([(60, 50), (60, 70), (80, 70), (80, 50)])
+ poly_3 = Polygon([(10, 10), (15, 10), (15, 15), (10, 15)])
+ poly_4 = Polygon([(10, 20), (15, 20), (15, 30), (10, 30), (12, 25)])
+
+ polygons = MultiPolygon([poly_3, poly_4])
+
+ geoms = np.array([poly_1, poly_2, polygons])
+ values = np.array([1, 2, 3])
+ region_data = RegionData(regions=geoms, values=values)
+
+ image_data = Data(x=np.arange(10000).reshape((100, 100)), label='data1')
+ app = Application()
+ app.data_collection.append(image_data)
+ app.data_collection.append(region_data)
+
+ link1 = LinkSame(region_data.center_x_id, image_data.pixel_component_ids[0])
+ link2 = LinkSame(region_data.center_y_id, image_data.pixel_component_ids[1])
+ app.data_collection.add_link(link1)
+ app.data_collection.add_link(link2)
+
+ viewer = app.new_data_viewer(SimpleImageViewer)
+ viewer.add_data(image_data)
+ viewer.add_data(region_data)
+
+ # We need this delay callback here because, while this works in the QT GUI,
+ # we need to make sure not to try and redraw the regions while we are flipping
+ # the coordinates displayed.
+
+ with delay_callback(viewer.state, 'x_att', 'y_att'):
+ viewer.state.x_att = image_data.pixel_component_ids[0]
+ viewer.state.y_att = image_data.pixel_component_ids[1]
+
+ return viewer.figure
diff --git a/glue/viewers/image/viewer.py b/glue/viewers/image/viewer.py
index 02f23e243..aec96cc9d 100644
--- a/glue/viewers/image/viewer.py
+++ b/glue/viewers/image/viewer.py
@@ -5,15 +5,18 @@
from glue.core.subset import roi_to_subset_state
from glue.core.coordinates import Coordinates, LegacyCoordinates
from glue.core.coordinate_helpers import dependent_axes
+from glue.core.data_region import RegionData
-from glue.viewers.scatter.layer_artist import ScatterLayerArtist
+from glue.viewers.matplotlib.viewer import SimpleMatplotlibViewer
+from glue.viewers.scatter.layer_artist import ScatterLayerArtist, ScatterRegionLayerArtist
from glue.viewers.image.layer_artist import ImageLayerArtist, ImageSubsetLayerArtist
from glue.viewers.image.compat import update_image_viewer_state
+from glue.viewers.image.state import ImageViewerState
from glue.viewers.image.frb_artist import imshow
from glue.viewers.image.composite_array import CompositeArray
-__all__ = ['MatplotlibImageMixin']
+__all__ = ['MatplotlibImageMixin', 'SimpleImageViewer']
def get_identity_wcs(naxis):
@@ -172,15 +175,24 @@ def _scatter_artist(self, axes, state, layer=None, layer_state=None):
raise Exception("Can only add a scatter plot overlay once an image is present")
return ScatterLayerArtist(axes, state, layer=layer, layer_state=None)
+ def _region_artist(self, axes, state, layer=None, layer_state=None):
+ if len(self._layer_artist_container) == 0:
+ raise Exception("Can only add a region plot overlay once an image is present")
+ return ScatterRegionLayerArtist(axes, state, layer=layer, layer_state=None)
+
def get_data_layer_artist(self, layer=None, layer_state=None):
- if layer.ndim == 1:
+ if isinstance(layer, RegionData):
+ cls = self._region_artist
+ elif layer.ndim == 1:
cls = self._scatter_artist
else:
cls = ImageLayerArtist
return self.get_layer_artist(cls, layer=layer, layer_state=layer_state)
def get_subset_layer_artist(self, layer=None, layer_state=None):
- if layer.ndim == 1:
+ if isinstance(layer.data, RegionData):
+ cls = self._region_artist
+ elif layer.ndim == 1:
cls = self._scatter_artist
else:
cls = ImageSubsetLayerArtist
@@ -248,3 +260,13 @@ def _script_footer(self):
x_ticklabel_size=self.state.x_ticklabel_size,
y_ticklabel_size=self.state.y_ticklabel_size)
return [], EXTRA_FOOTER.format(**options) + os.linesep * 2 + script
+
+
+class SimpleImageViewer(MatplotlibImageMixin, SimpleMatplotlibViewer):
+
+ _state_cls = ImageViewerState
+
+ def __init__(self, *args, **kwargs):
+ kwargs['wcs'] = True
+ super().__init__(*args, **kwargs)
+ MatplotlibImageMixin.setup_callbacks(self)
diff --git a/glue/viewers/matplotlib/qt/__init__.py b/glue/viewers/matplotlib/qt/__init__.py
deleted file mode 100644
index 2d725e620..000000000
--- a/glue/viewers/matplotlib/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt is deprecated, use glue_qt.viewers.matplotlib instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib import * # noqa
diff --git a/glue/viewers/matplotlib/qt/axes_editor.py b/glue/viewers/matplotlib/qt/axes_editor.py
deleted file mode 100644
index dbcf092e9..000000000
--- a/glue/viewers/matplotlib/qt/axes_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.axes_editor is deprecated, use glue_qt.viewers.matplotlib.axes_editor instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.axes_editor import * # noqa
diff --git a/glue/viewers/matplotlib/qt/compute_worker.py b/glue/viewers/matplotlib/qt/compute_worker.py
deleted file mode 100644
index 34e2f794b..000000000
--- a/glue/viewers/matplotlib/qt/compute_worker.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.compute_worker is deprecated, use glue_qt.viewers.matplotlib.compute_worker instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.compute_worker import * # noqa
diff --git a/glue/viewers/matplotlib/qt/data_viewer.py b/glue/viewers/matplotlib/qt/data_viewer.py
deleted file mode 100644
index aa334f43c..000000000
--- a/glue/viewers/matplotlib/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.data_viewer is deprecated, use glue_qt.viewers.matplotlib.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.data_viewer import * # noqa
diff --git a/glue/viewers/matplotlib/qt/legend_editor.py b/glue/viewers/matplotlib/qt/legend_editor.py
deleted file mode 100644
index 762f23046..000000000
--- a/glue/viewers/matplotlib/qt/legend_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.legend_editor is deprecated, use glue_qt.viewers.matplotlib.legend_editor instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.legend_editor import * # noqa
diff --git a/glue/viewers/matplotlib/qt/toolbar.py b/glue/viewers/matplotlib/qt/toolbar.py
deleted file mode 100644
index c2ec8c2be..000000000
--- a/glue/viewers/matplotlib/qt/toolbar.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.toolbar is deprecated, use glue_qt.viewers.matplotlib.toolbar instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.toolbar import * # noqa
diff --git a/glue/viewers/matplotlib/qt/toolbar_mode.py b/glue/viewers/matplotlib/qt/toolbar_mode.py
deleted file mode 100644
index 69af6a89e..000000000
--- a/glue/viewers/matplotlib/qt/toolbar_mode.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.toolbar_mode is deprecated, use glue_qt.viewers.matplotlib.toolbar_mode instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.toolbar_mode import * # noqa
diff --git a/glue/viewers/matplotlib/qt/widget.py b/glue/viewers/matplotlib/qt/widget.py
deleted file mode 100644
index d9b1a89ac..000000000
--- a/glue/viewers/matplotlib/qt/widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.matplotlib.qt.widget is deprecated, use glue_qt.viewers.matplotlib.widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.matplotlib.widget import * # noqa
diff --git a/glue/viewers/matplotlib/state.py b/glue/viewers/matplotlib/state.py
index 01a392c28..038cf3d73 100644
--- a/glue/viewers/matplotlib/state.py
+++ b/glue/viewers/matplotlib/state.py
@@ -1,4 +1,7 @@
-from echo import CallbackProperty, SelectionCallbackProperty, keep_in_sync, delay_callback
+from echo import (CallbackProperty,
+ SelectionCallbackProperty,
+ DictCallbackProperty,
+ keep_in_sync, delay_callback)
from matplotlib.colors import to_rgba
@@ -35,6 +38,17 @@ def notify(self, *args, **kwargs):
super(DeferredDrawSelectionCallbackProperty, self).notify(*args, **kwargs)
+class DeferredDrawDictCallbackProperty(DictCallbackProperty):
+ """
+ A callback property where drawing is deferred until
+ after notify has called all callback functions.
+ """
+
+ @defer_draw
+ def notify(self, *args, **kwargs):
+ super(DeferredDrawDictCallbackProperty, self).notify(*args, **kwargs)
+
+
VALID_WEIGHTS = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black']
diff --git a/glue/viewers/matplotlib/viewer.py b/glue/viewers/matplotlib/viewer.py
index 7b174d8eb..bab8e5442 100644
--- a/glue/viewers/matplotlib/viewer.py
+++ b/glue/viewers/matplotlib/viewer.py
@@ -6,11 +6,12 @@
from matplotlib.artist import setp as msetp
from glue.config import settings
-from glue.viewers.matplotlib.mpl_axes import update_appearance_from_settings
+from glue.viewers.common.viewer import Viewer
+from glue.viewers.matplotlib.mpl_axes import update_appearance_from_settings, init_mpl
from echo import delay_callback
from glue.utils import mpl_to_datetime64
-__all__ = ['MatplotlibViewerMixin']
+__all__ = ['MatplotlibViewerMixin', 'SimpleMatplotlibViewer']
SCRIPT_HEADER = """
# Initialize figure
@@ -352,3 +353,11 @@ def _script_legend(self):
if not self.state.legend.visible:
legend_str = indent(legend_str, "# ")
return [], legend_str
+
+
+class SimpleMatplotlibViewer(MatplotlibViewerMixin, Viewer):
+
+ def __init__(self, session, parent=None, wcs=None, state=None, projection=None):
+ super().__init__(session, state=state)
+ self.figure, self.axes = init_mpl(wcs=wcs, projection=projection)
+ MatplotlibViewerMixin.setup_callbacks(self)
diff --git a/glue/viewers/profile/qt/__init__.py b/glue/viewers/profile/qt/__init__.py
deleted file mode 100644
index 2b9e5fa54..000000000
--- a/glue/viewers/profile/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt is deprecated, use glue_qt.viewers.profile instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile import * # noqa
diff --git a/glue/viewers/profile/qt/data_viewer.py b/glue/viewers/profile/qt/data_viewer.py
deleted file mode 100644
index 77ed871f7..000000000
--- a/glue/viewers/profile/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt.data_viewer is deprecated, use glue_qt.viewers.profile.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile.data_viewer import * # noqa
diff --git a/glue/viewers/profile/qt/layer_artist.py b/glue/viewers/profile/qt/layer_artist.py
deleted file mode 100644
index f584c22fd..000000000
--- a/glue/viewers/profile/qt/layer_artist.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt.layer_artist is deprecated, use glue_qt.viewers.profile.layer_artist instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile.layer_artist import * # noqa
diff --git a/glue/viewers/profile/qt/layer_style_editor.py b/glue/viewers/profile/qt/layer_style_editor.py
deleted file mode 100644
index 8ff0884bb..000000000
--- a/glue/viewers/profile/qt/layer_style_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt.layer_style_editor is deprecated, use glue_qt.viewers.profile.layer_style_editor instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile.layer_style_editor import * # noqa
diff --git a/glue/viewers/profile/qt/mouse_mode.py b/glue/viewers/profile/qt/mouse_mode.py
deleted file mode 100644
index 7c14a0143..000000000
--- a/glue/viewers/profile/qt/mouse_mode.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt.mouse_mode is deprecated, use glue_qt.viewers.profile.mouse_mode instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile.mouse_mode import * # noqa
diff --git a/glue/viewers/profile/qt/options_widget.py b/glue/viewers/profile/qt/options_widget.py
deleted file mode 100644
index 93ff9ed25..000000000
--- a/glue/viewers/profile/qt/options_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt.options_widget is deprecated, use glue_qt.viewers.profile.options_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile.options_widget import * # noqa
diff --git a/glue/viewers/profile/qt/profile_tools.py b/glue/viewers/profile/qt/profile_tools.py
deleted file mode 100644
index 41eb0cf45..000000000
--- a/glue/viewers/profile/qt/profile_tools.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.profile.qt.profile_tools is deprecated, use glue_qt.viewers.profile.profile_tools instead', GlueDeprecationWarning)
-from glue_qt.viewers.profile.profile_tools import * # noqa
diff --git a/glue/viewers/profile/tests/test_viewer.py b/glue/viewers/profile/tests/test_viewer.py
new file mode 100644
index 000000000..9c384ba1b
--- /dev/null
+++ b/glue/viewers/profile/tests/test_viewer.py
@@ -0,0 +1,27 @@
+from glue.tests.visual.helpers import visual_test
+from glue.viewers.profile.viewer import SimpleProfileViewer
+from glue.core.application_base import Application
+from glue.core.data import Data
+
+
+@visual_test
+def test_simple_viewer():
+
+ # Make sure the simple viewer can be instantiated
+
+ data1 = Data(x=[1, 2, 3], label='data1')
+ data2 = Data(y=[1, 2, 3], label='data2')
+
+ app = Application()
+ app.data_collection.append(data1)
+ app.data_collection.append(data2)
+
+ viewer = app.new_data_viewer(SimpleProfileViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ app.data_collection.new_subset_group(label='subset1', subset_state=data1.pixel_component_ids[0] > 0.8)
+
+ viewer.state.layers[2].linewidth = 5
+
+ return viewer.figure
diff --git a/glue/viewers/profile/viewer.py b/glue/viewers/profile/viewer.py
index fad4a05e3..03edadf2e 100644
--- a/glue/viewers/profile/viewer.py
+++ b/glue/viewers/profile/viewer.py
@@ -3,7 +3,11 @@
from glue.core.units import UnitConverter
from glue.core.subset import roi_to_subset_state
-__all__ = ['MatplotlibProfileMixin']
+from glue.viewers.matplotlib.viewer import SimpleMatplotlibViewer
+from glue.viewers.profile.state import ProfileViewerState
+from glue.viewers.profile.layer_artist import ProfileLayerArtist
+
+__all__ = ['MatplotlibProfileMixin', 'SimpleProfileViewer']
class MatplotlibProfileMixin(object):
@@ -55,3 +59,14 @@ def apply_roi(self, roi, override_mode=None):
subset_state = roi_to_subset_state(roi, x_att=self.state.x_att)
self.apply_subset_state(subset_state, override_mode=override_mode)
+
+
+class SimpleProfileViewer(MatplotlibProfileMixin, SimpleMatplotlibViewer):
+
+ _state_cls = ProfileViewerState
+ _data_artist_cls = ProfileLayerArtist
+ _subset_artist_cls = ProfileLayerArtist
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ MatplotlibProfileMixin.setup_callbacks(self)
diff --git a/glue/viewers/scatter/layer_artist.py b/glue/viewers/scatter/layer_artist.py
index bb8ac5c6e..c6277ad46 100644
--- a/glue/viewers/scatter/layer_artist.py
+++ b/glue/viewers/scatter/layer_artist.py
@@ -10,21 +10,28 @@
from glue.config import stretches
from glue.utils import defer_draw, ensure_numerical, datetime64_to_mpl
-from glue.viewers.scatter.state import ScatterLayerState
+from glue.viewers.scatter.state import ScatterLayerState, ScatterRegionLayerState
from glue.viewers.scatter.python_export import python_export_scatter_layer
+from glue.viewers.scatter.plot_polygons import (UpdateableRegionCollection,
+ get_geometry_type,
+ _sanitize_geoms,
+ _PolygonPatch,
+ transform_shapely)
from glue.viewers.matplotlib.layer_artist import MatplotlibLayerArtist
from glue.core.exceptions import IncompatibleAttribute
+from glue.core.data import BaseData
from matplotlib.lines import Line2D
+from shapely.ops import transform
# We keep the following so that scripts exported with previous versions of glue
# continue to work, as they imported STRETCHES from here.
-STRETCHES = stretches.members
+STRETCHES = {key: value() for key, value in stretches.members.items()}
CMAP_PROPERTIES = set(['cmap_mode', 'cmap_att', 'cmap_vmin', 'cmap_vmax', 'cmap'])
MARKER_PROPERTIES = set(['size_mode', 'size_att', 'size_vmin', 'size_vmax', 'size_scaling', 'size', 'fill'])
LINE_PROPERTIES = set(['linewidth', 'linestyle'])
-DENSITY_PROPERTIES = set(['dpi', 'stretch', 'density_contrast'])
+DENSITY_PROPERTIES = set(['dpi', 'stretch', 'stretch_parameters', 'density_contrast'])
VISUAL_PROPERTIES = (CMAP_PROPERTIES | MARKER_PROPERTIES | DENSITY_PROPERTIES |
LINE_PROPERTIES | set(['color', 'alpha', 'zorder', 'visible']))
@@ -364,8 +371,8 @@ def _update_visual_attributes(self, changed, force=False):
c = ensure_numerical(self.layer[self.state.cmap_att].ravel())
set_mpl_artist_cmap(self.density_artist, c, self.state)
- if force or 'stretch' in changed:
- self.density_artist.set_norm(ImageNormalize(stretch=stretches.members[self.state.stretch]))
+ if force or 'stretch' in changed or 'stretch_parameters' in changed:
+ self.density_artist.set_norm(ImageNormalize(stretch=self.state.stretch_object))
if force or 'dpi' in changed:
self.density_artist.set_dpi(self._viewer_state.dpi)
@@ -594,3 +601,214 @@ def _use_plot_artist(self):
res = self.state.cmap_mode == 'Fixed' and self.state.size_mode == 'Fixed'
return res and (not hasattr(self._viewer_state, 'plot_mode') or
not self._viewer_state.plot_mode == 'polar')
+
+
+class ScatterRegionLayerArtist(MatplotlibLayerArtist):
+
+ _layer_state_cls = ScatterRegionLayerState
+ # _python_exporter = python_export_scatter_layer # TODO: Update this to work with regions
+
+ def __init__(self, axes, viewer_state, layer_state=None, layer=None):
+
+ super().__init__(axes, viewer_state,
+ layer_state=layer_state, layer=layer)
+ self._viewer_state.add_global_callback(self._update_scatter_region)
+ self.state.add_global_callback(self._update_scatter_region)
+ self._set_axes(axes)
+
+ def _set_axes(self, axes):
+ self.axes = axes
+ self.region_collection = UpdateableRegionCollection([])
+ self.axes.add_collection(self.region_collection)
+ # This is a little unnecessary, but keeps code more parallel
+ self.mpl_artists = [self.region_collection]
+
+ @defer_draw
+ def _update_data(self):
+ # Layer artist has been cleared already
+ if len(self.mpl_artists) == 0:
+ return
+
+ if self.layer is not None:
+ if isinstance(self.layer, BaseData):
+ data = self.layer
+ else:
+ data = self.layer.data
+ region_att = data.extended_component_id
+
+ try:
+ # These must be special attributes that are linked to a region_att
+ if ((not data.linked_to_center_comp(self._viewer_state.x_att)) and
+ (not data.linked_to_center_comp(self._viewer_state.x_att_world))):
+ raise IncompatibleAttribute
+ x = ensure_numerical(self.layer[self._viewer_state.x_att].ravel())
+ xx = ensure_numerical(data[data.center_x_id].ravel())
+ except (IncompatibleAttribute, IndexError):
+ # The following includes a call to self.clear()
+ self.disable_invalid_attributes(self._viewer_state.x_att)
+ return
+ else:
+ self.enable()
+
+ try:
+ # These must be special attributes that are linked to a region_att
+ if ((not data.linked_to_center_comp(self._viewer_state.y_att)) and
+ (not data.linked_to_center_comp(self._viewer_state.y_att_world))):
+ raise IncompatibleAttribute
+ y = ensure_numerical(self.layer[self._viewer_state.y_att].ravel())
+ yy = ensure_numerical(data[data.center_y_id].ravel())
+ except (IncompatibleAttribute, IndexError):
+ # The following includes a call to self.clear()
+ self.disable_invalid_attributes(self._viewer_state.y_att)
+ return
+ else:
+ self.enable()
+
+ # We need to make sure that x and y viewer attributes are
+ # really the center_x and center_y attributes of the underlying
+ # data, so we compare the values on the centroids using the
+ # glue data access machinery.
+
+ regions = self.layer[region_att]
+
+ def flip_xy(g):
+ return transform(lambda x, y: (y, x), g)
+
+ x_no_match = False
+ if np.array_equal(y, yy):
+ if np.array_equal(x, xx):
+ self.enable()
+ else:
+ x_no_match = True
+ else:
+ if np.array_equal(y, xx) and np.array_equal(x, yy): # This means x and y have been swapped
+ regions = [flip_xy(g) for g in regions]
+ self.enable()
+ else:
+ self.disable_invalid_attributes(self._viewer_state.y_att)
+ if x_no_match:
+ self.disable_invalid_attributes(self._viewer_state.x_att)
+ return
+
+ # If we are using world coordinates (i.e. the regions are specified in world coordinates)
+ # we need to transform the geometries of the regions into pixel coordinates for display
+ # Note that this calls a custom version of the transform function from shapely
+ # to accomodate glue WCS objects
+ if self._viewer_state._display_world:
+ # First, convert to world coordinates
+ try:
+ tfunc = data.get_transform_to_cids([self._viewer_state.x_att_world, self._viewer_state.y_att_world])
+ regions = np.array([transform(tfunc, g) for g in regions])
+
+ # Then convert to pixels for display
+ world2pix = self._viewer_state.reference_data.coords.world_to_pixel_values
+ regions = np.array([transform_shapely(world2pix, g) for g in regions])
+ except ValueError:
+ self.disable_invalid_attributes([self._viewer_state.x_att_world, self._viewer_state.y_att_world])
+ return
+ else:
+ try:
+ tfunc = data.get_transform_to_cids([self._viewer_state.x_att, self._viewer_state.y_att])
+ regions = np.array([transform(tfunc, g) for g in regions])
+ except ValueError:
+ self.disable_invalid_attributes([self._viewer_state.x_att, self._viewer_state.y_att])
+ return
+
+ # decompose GeometryCollections
+ geoms, multiindex = _sanitize_geoms(regions, prefix="Geom")
+ self.multiindex_geometry = multiindex
+
+ geom_types = get_geometry_type(geoms)
+ poly_idx = np.asarray((geom_types == "Polygon") | (geom_types == "MultiPolygon"))
+ polys = geoms[poly_idx]
+
+ # decompose MultiPolygons
+ geoms, multiindex = _sanitize_geoms(polys, prefix="Multi")
+ self.region_collection.patches = [_PolygonPatch(poly) for poly in geoms]
+
+ self.geoms = geoms
+ self.multiindex = multiindex
+
+ @defer_draw
+ def _update_visual_attributes(self, changed, force=False):
+
+ if not self.enabled:
+ return
+
+ if self.state.cmap_mode == 'Fixed':
+ if force or 'color' in changed or 'cmap_mode' in changed or 'fill' in changed:
+ self.region_collection.set_array(None)
+ if self.state.fill:
+ self.region_collection.set_facecolors(self.state.color)
+ self.region_collection.set_edgecolors('none')
+ else:
+ self.region_collection.set_facecolors('none')
+ self.region_collection.set_edgecolors(self.state.color)
+ elif force or any(prop in changed for prop in CMAP_PROPERTIES) or 'fill' in changed:
+ self.region_collection.set_edgecolors(None)
+ self.region_collection.set_facecolors(None)
+ c = ensure_numerical(self.layer[self.state.cmap_att].ravel())
+ c_values = np.take(c, self.multiindex_geometry, axis=0) # Decompose Geoms
+ c_values = np.take(c_values, self.multiindex, axis=0) # Decompose MultiPolys
+ set_mpl_artist_cmap(self.region_collection, c_values, self.state)
+ if self.state.fill:
+ self.region_collection.set_edgecolors('none')
+ else:
+ self.region_collection.set_facecolors('none')
+
+ for artist in [self.region_collection]:
+
+ if artist is None:
+ continue
+
+ if force or 'alpha' in changed:
+ artist.set_alpha(self.state.alpha)
+
+ if force or 'zorder' in changed:
+ artist.set_zorder(self.state.zorder)
+
+ if force or 'visible' in changed:
+ artist.set_visible(self.state.visible)
+
+ self.redraw()
+
+ @defer_draw
+ def _update_scatter_region(self, force=False, **kwargs):
+
+ if (self._viewer_state.x_att is None or
+ self._viewer_state.y_att is None or
+ self.state.layer is None):
+ return
+
+ # NOTE: we need to evaluate this even if force=True so that the cache
+ # of updated properties is up to date after this method has been called.
+ changed = self.pop_changed_properties()
+
+ full_sphere = getattr(self._viewer_state, 'using_full_sphere', False)
+ change_from_limits = full_sphere and len(changed & LIMIT_PROPERTIES) > 0
+ if force or change_from_limits or len(changed & DATA_PROPERTIES) > 0:
+ # This is the signature of flipping the x and y axis of an image in the UI
+ # We must *not* run update_data right away, or it will return an error
+ # The delay_callback wrapper on state._on_xatt_world_change() and
+ # state._on_yatt_world_change() is not properly deferring the callback
+ # until after x and y have been fully swapped, so we need to do it manually.
+ if changed == {'x_att_world', 'x_att', 'y_att_world'} or changed == {'y_att_world', 'y_att', 'x_att_world'}:
+ pass
+ else:
+ self._update_data()
+ force = True
+
+ if force or len(changed & VISUAL_PROPERTIES) > 0:
+ self._update_visual_attributes(changed, force=force)
+
+ @defer_draw
+ def update(self):
+ self._update_scatter_region(force=True)
+ self.redraw()
+
+ @defer_draw
+ def _on_components_changed(self, components_changed):
+ for limit_helper in [self.state.cmap_lim_helper]:
+ if limit_helper.attribute in components_changed:
+ limit_helper.update_values('attribute')
+ self.redraw()
diff --git a/glue/viewers/scatter/plot_polygons.py b/glue/viewers/scatter/plot_polygons.py
new file mode 100644
index 000000000..bc197fb99
--- /dev/null
+++ b/glue/viewers/scatter/plot_polygons.py
@@ -0,0 +1,157 @@
+"""
+Some lightly edited code from geopandas.plotting.py for efficiently
+plotting multiple polygons in matplotlib.
+"""
+# Copyright (c) 2013-2022, GeoPandas developers.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of GeoPandas nor the names of its contributors may
+# be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# 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 OWNER 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.
+
+import numpy as np
+import shapely
+from matplotlib.collections import PatchCollection
+from shapely.errors import GeometryTypeError
+
+
+class UpdateableRegionCollection(PatchCollection):
+ """
+ Allow paths in PatchCollection to be modified after creation.
+ """
+
+ def __init__(self, patches, *args, **kwargs):
+ self.patches = patches
+ PatchCollection.__init__(self, patches, *args, **kwargs)
+
+ def get_paths(self):
+ self.set_paths(self.patches)
+ return self._paths
+
+
+def _sanitize_geoms(geoms, prefix="Multi"):
+ """
+ Returns Series like geoms and index, except that any Multi geometries
+ are split into their components and indices are repeated for all component
+ in the same Multi geometry. At the same time, empty or missing geometries are
+ filtered out. Maintains 1:1 matching of geometry to value.
+ Prefix specifies type of geometry to be flatten. 'Multi' for MultiPoint and similar,
+ "Geom" for GeometryCollection.
+ Returns
+ -------
+ components : list of geometry
+ component_index : index array
+ indices are repeated for all components in the same Multi geometry
+ """
+ # TODO(shapely) look into simplifying this with
+ # shapely.get_parts(geoms, return_index=True) from shapely 2.0
+ components, component_index = [], []
+
+ geom_types = get_geometry_type(geoms).astype("str")
+
+ if (
+ not np.char.startswith(geom_types, prefix).any()
+ # and not geoms.is_empty.any()
+ # and not geoms.isna().any()
+ ):
+ return geoms, np.arange(len(geoms))
+
+ for ix, (geom, geom_type) in enumerate(zip(geoms, geom_types)):
+ if geom is not None and geom_type.startswith(prefix):
+ for poly in geom.geoms:
+ components.append(poly)
+ component_index.append(ix)
+ elif geom is None:
+ continue
+ else:
+ components.append(geom)
+ component_index.append(ix)
+
+ return components, np.array(component_index)
+
+
+def get_geometry_type(data):
+ _names = {
+ "MISSING": None,
+ "NAG": None,
+ "POINT": "Point",
+ "LINESTRING": "LineString",
+ "LINEARRING": "LinearRing",
+ "POLYGON": "Polygon",
+ "MULTIPOINT": "MultiPoint",
+ "MULTILINESTRING": "MultiLineString",
+ "MULTIPOLYGON": "MultiPolygon",
+ "GEOMETRYCOLLECTION": "GeometryCollection",
+ }
+
+ type_mapping = {p.value: _names[p.name] for p in shapely.GeometryType}
+ geometry_type_ids = list(type_mapping.keys())
+ geometry_type_values = np.array(list(type_mapping.values()), dtype=object)
+ res = shapely.get_type_id(data)
+ return geometry_type_values[np.searchsorted(geometry_type_ids, res)]
+
+
+def transform_shapely(func, geom):
+ """
+ A simplified/modified version of shapely.ops.transform where the func
+ call signature is tuned for the coordinate transform functions
+ coming from glue.
+ """
+ if geom.is_empty:
+ return geom
+ if geom.geom_type in ("Point", "LineString", "LinearRing", "Polygon"):
+ if geom.geom_type in ("Point", "LineString", "LinearRing"):
+ return type(geom)(func(geom.coords))
+ elif geom.geom_type == "Polygon":
+ shell = type(geom.exterior)(func(geom.exterior.coords))
+ holes = list(
+ type(ring)(func(ring.coords))
+ for ring in geom.interiors
+ )
+ return type(geom)(shell, holes)
+
+ elif geom.geom_type.startswith("Multi") or geom.geom_type == "GeometryCollection":
+ return type(geom)([transform_shapely(func, part) for part in geom.geoms])
+ else:
+ raise GeometryTypeError(f"Type {geom.geom_type!r} not recognized")
+
+
+def _PolygonPatch(polygon, **kwargs):
+ """Constructs a matplotlib patch from a Polygon geometry
+ The `kwargs` are those supported by the matplotlib.patches.PathPatch class
+ constructor. Returns an instance of matplotlib.patches.PathPatch.
+ Example (using Shapely Point and a matplotlib axes)::
+ b = shapely.geometry.Point(0, 0).buffer(1.0)
+ patch = _PolygonPatch(b, fc='blue', ec='blue', alpha=0.5)
+ ax.add_patch(patch)
+ GeoPandas originally relied on the descartes package by Sean Gillies
+ (BSD license, https://pypi.org/project/descartes) for PolygonPatch, but
+ this dependency was removed in favor of the below matplotlib code.
+ """
+ from matplotlib.patches import PathPatch
+ from matplotlib.path import Path
+
+ path = Path.make_compound_path(
+ Path(np.asarray(polygon.exterior.coords)[:, :2]),
+ *[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors],
+ )
+ return PathPatch(path, **kwargs)
diff --git a/glue/viewers/scatter/python_export.py b/glue/viewers/scatter/python_export.py
index 717805078..d31d53866 100644
--- a/glue/viewers/scatter/python_export.py
+++ b/glue/viewers/scatter/python_export.py
@@ -77,8 +77,7 @@ def python_export_scatter_layer(layer, *args):
if layer.state.density_map:
imports += ["from mpl_scatter_density import ScatterDensityArtist"]
- imports += ["from glue.config import stretches"]
- imports += ["from glue.viewers.scatter.layer_artist import DensityMapLimits"]
+ imports += ["from glue.viewers.scatter.layer_artist import DensityMapLimits, STRETCHES"]
imports += ["from astropy.visualization import ImageNormalize"]
script += "density_limits = DensityMapLimits()\n"
@@ -92,7 +91,7 @@ def python_export_scatter_layer(layer, *args):
options['color'] = layer.state.color
options['vmin'] = code('density_limits.min')
options['vmax'] = code('density_limits.max')
- options['norm'] = code("ImageNormalize(stretch=stretches.members['{0}'])".format(layer.state.stretch))
+ options['norm'] = code("ImageNormalize(stretch=STRETCHES['{0}'])".format(layer.state.stretch))
else:
options['c'] = code("layer_data['{0}']".format(layer.state.cmap_att.label))
options['cmap'] = code("plt.cm.{0}".format(layer.state.cmap.name))
diff --git a/glue/viewers/scatter/qt/__init__.py b/glue/viewers/scatter/qt/__init__.py
deleted file mode 100644
index 5c9d59659..000000000
--- a/glue/viewers/scatter/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.scatter.qt is deprecated, use glue_qt.viewers.scatter instead', GlueDeprecationWarning)
-from glue_qt.viewers.scatter import * # noqa
diff --git a/glue/viewers/scatter/qt/data_viewer.py b/glue/viewers/scatter/qt/data_viewer.py
deleted file mode 100644
index b9f1f1865..000000000
--- a/glue/viewers/scatter/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.scatter.qt.data_viewer is deprecated, use glue_qt.viewers.scatter.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.scatter.data_viewer import * # noqa
diff --git a/glue/viewers/scatter/qt/layer_style_editor.py b/glue/viewers/scatter/qt/layer_style_editor.py
deleted file mode 100644
index 39c7a490c..000000000
--- a/glue/viewers/scatter/qt/layer_style_editor.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.scatter.qt.layer_style_editor is deprecated, use glue_qt.viewers.scatter.layer_style_editor instead', GlueDeprecationWarning)
-from glue_qt.viewers.scatter.layer_style_editor import * # noqa
diff --git a/glue/viewers/scatter/qt/options_widget.py b/glue/viewers/scatter/qt/options_widget.py
deleted file mode 100644
index fe1e5fc51..000000000
--- a/glue/viewers/scatter/qt/options_widget.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.scatter.qt.options_widget is deprecated, use glue_qt.viewers.scatter.options_widget instead', GlueDeprecationWarning)
-from glue_qt.viewers.scatter.options_widget import * # noqa
diff --git a/glue/viewers/scatter/state.py b/glue/viewers/scatter/state.py
index 0ad007bb9..9fb0095f4 100644
--- a/glue/viewers/scatter/state.py
+++ b/glue/viewers/scatter/state.py
@@ -4,7 +4,7 @@
from glue.core import BaseData, Subset
-from glue.config import colormaps, stretches
+from glue.config import colormaps
from glue.viewers.matplotlib.state import (MatplotlibDataViewerState,
MatplotlibLayerState,
DeferredDrawCallbackProperty as DDCProperty,
@@ -13,10 +13,11 @@
from echo import keep_in_sync, delay_callback
from glue.core.data_combo_helper import ComponentIDComboHelper, ComboHelper
from glue.core.exceptions import IncompatibleAttribute
+from glue.viewers.common.stretch_state_mixin import StretchStateMixin
from matplotlib.projections import get_projection_names
-__all__ = ['ScatterViewerState', 'ScatterLayerState']
+__all__ = ['ScatterViewerState', 'ScatterLayerState', 'ScatterRegionLayerState']
class ScatterViewerState(MatplotlibDataViewerState):
@@ -30,6 +31,9 @@ class ScatterViewerState(MatplotlibDataViewerState):
plot_mode = DDSCProperty(docstring="Whether to plot the data in cartesian, polar or another projection")
angle_unit = DDSCProperty(docstring="Whether to use radians or degrees for any angular coordinates")
+ x_limits_percentile = DDCProperty(100, docstring="Percentile to use when automatically determining x limits")
+ y_limits_percentile = DDCProperty(100, docstring="Percentile to use when automatically determining y limits")
+
def __init__(self, **kwargs):
super(ScatterViewerState, self).__init__()
@@ -70,13 +74,13 @@ def __init__(self, **kwargs):
def _reset_x_limits(self, *args):
if self.x_att is None:
return
- self.x_lim_helper.percentile = 100
+ self.x_lim_helper.percentile = self.x_limits_percentile
self.x_lim_helper.update_values(force=True)
def _reset_y_limits(self, *args):
if self.y_att is None:
return
- self.y_lim_helper.percentile = 100
+ self.y_lim_helper.percentile = self.y_limits_percentile
self.y_lim_helper.update_values(force=True)
def reset_limits(self):
@@ -201,7 +205,7 @@ def display_func_slow(x):
return x
-class ScatterLayerState(MatplotlibLayerState):
+class ScatterLayerState(MatplotlibLayerState, StretchStateMixin):
"""
A state class that includes all the attributes for layers in a scatter plot.
"""
@@ -232,9 +236,6 @@ class ScatterLayerState(MatplotlibLayerState):
# Density map
density_map = DDCProperty(False, docstring="Whether to show the points as a density map")
- stretch = DDSCProperty(default='log', docstring='The stretch used to render the layer, '
- 'which should be one of ``linear``, '
- '``sqrt``, ``log``, or ``arcsinh``')
density_contrast = DDCProperty(1, docstring="The dynamic range of the density map")
# Note that we keep the dpi in the viewer state since we want it to always
@@ -327,8 +328,8 @@ def __init__(self, viewer_state=None, layer=None, **kwargs):
ScatterLayerState.vector_origin.set_choices(self, ['tail', 'middle', 'tip'])
ScatterLayerState.vector_origin.set_display_func(self, vector_origin_display.get)
- ScatterLayerState.stretch.set_choices(self, ['linear', 'sqrt', 'arcsinh', 'log'])
- ScatterLayerState.stretch.set_display_func(self, stretches.display_func)
+ self.setup_stretch_callback()
+ self.stretch = 'log'
if self.viewer_state is not None:
self.viewer_state.add_callback('x_att', self._on_xy_change, priority=10000)
@@ -343,6 +344,7 @@ def __init__(self, viewer_state=None, layer=None, **kwargs):
self._on_layer_change()
self.cmap = colormaps.members[0][1]
+ self.add_callback('cmap_att', self._check_for_preferred_cmap)
self.size = self.layer.style.markersize
@@ -350,6 +352,15 @@ def __init__(self, viewer_state=None, layer=None, **kwargs):
self.update_from_dict(kwargs)
+ def _check_for_preferred_cmap(self, *args):
+ if isinstance(self.layer, BaseData):
+ layer = self.layer
+ else:
+ layer = self.layer.data
+ actual_component = layer.get_component(self.cmap_att)
+ if getattr(actual_component, 'preferred_cmap', False):
+ self.cmap = actual_component.preferred_cmap
+
def _update_points_mode(self, *args):
if getattr(self.viewer_state, 'using_polar', False) or getattr(self.viewer_state, 'using_full_sphere', False):
self.points_mode_helper.choices = ['markers']
@@ -493,3 +504,97 @@ def __setgluestate__(cls, rec, context):
rec['values']['markers_visible'] = False
rec['values']['line_visible'] = True
return super(ScatterLayerState, cls).__setgluestate__(rec, context)
+
+
+class ScatterRegionLayerState(MatplotlibLayerState):
+ """
+ A state class that includes all the attributes for layers in a scatter region layer.
+ """
+ # Color
+
+ cmap_mode = DDSCProperty(docstring="Whether to use color to encode an attribute")
+ cmap_att = DDSCProperty(docstring="The attribute to use for the color")
+ cmap_vmin = DDCProperty(docstring="The lower level for the colormap")
+ cmap_vmax = DDCProperty(docstring="The upper level for the colormap")
+ cmap = DDCProperty(docstring="The colormap to use (when in colormap mode)")
+ percentile = DDSCProperty(docstring='The percentile value used to '
+ 'automatically calculate levels')
+
+ fill = DDCProperty(True, docstring="Whether to fill the regions")
+
+ def __init__(self, viewer_state=None, layer=None, **kwargs):
+
+ super().__init__(viewer_state=viewer_state, layer=layer)
+ self.limits_cache = {}
+
+ self.cmap_lim_helper = StateAttributeLimitsHelper(self, attribute='cmap_att',
+ lower='cmap_vmin', upper='cmap_vmax',
+ percentile='percentile',
+ limits_cache=self.limits_cache)
+
+ self.cmap_att_helper = ComponentIDComboHelper(self, 'cmap_att',
+ numeric=True, datetime=False,
+ categorical=True)
+
+ percentile_display = {100: 'Min/Max',
+ 99.5: '99.5%',
+ 99: '99%',
+ 95: '95%',
+ 90: '90%',
+ 'Custom': 'Custom'}
+
+ ScatterRegionLayerState.percentile.set_choices(self, [100, 99.5, 99, 95, 90, 'Custom'])
+ ScatterRegionLayerState.percentile.set_display_func(self, percentile_display.get)
+
+ ScatterRegionLayerState.cmap_mode.set_choices(self, ['Fixed', 'Linear'])
+
+ if self.viewer_state is not None:
+ self.viewer_state.add_callback('x_att', self._on_xy_change, priority=10000)
+ self.viewer_state.add_callback('y_att', self._on_xy_change, priority=10000)
+ self._on_xy_change()
+
+ self.add_callback('layer', self._on_layer_change)
+ if layer is not None:
+ self._on_layer_change()
+
+ self.cmap = colormaps.members[0][1]
+
+ self.add_callback('cmap_att', self._check_for_preferred_cmap)
+ self.update_from_dict(kwargs)
+
+ def _check_for_preferred_cmap(self, *args):
+ if isinstance(self.layer, BaseData):
+ layer = self.layer
+ else:
+ layer = self.layer.data
+ actual_component = layer.get_component(self.cmap_att)
+ if getattr(actual_component, 'preferred_cmap', False):
+ self.cmap = actual_component.preferred_cmap
+
+ def _on_layer_change(self, layer=None):
+ with delay_callback(self, 'cmap_vmin', 'cmap_vmax'):
+
+ if self.layer is None:
+ self.cmap_att_helper.set_multiple_data([])
+ else:
+ self.cmap_att_helper.set_multiple_data([self.layer])
+
+ def _on_xy_change(self, *event):
+
+ if self.viewer_state.x_att is None or self.viewer_state.y_att is None:
+ return
+
+ if isinstance(self.layer, BaseData):
+ layer = self.layer
+ else:
+ layer = self.layer.data
+
+ def flip_cmap(self):
+ """
+ Flip the cmap_vmin/cmap_vmax limits.
+ """
+ self.cmap_lim_helper.flip_limits()
+
+ @property
+ def cmap_name(self):
+ return colormaps.name_from_cmap(self.cmap)
diff --git a/glue/viewers/scatter/tests/__init__.py b/glue/viewers/scatter/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/glue/viewers/scatter/tests/test_viewer.py b/glue/viewers/scatter/tests/test_viewer.py
new file mode 100644
index 000000000..e622b213d
--- /dev/null
+++ b/glue/viewers/scatter/tests/test_viewer.py
@@ -0,0 +1,105 @@
+import numpy as np
+from numpy.testing import assert_allclose
+
+import matplotlib.pyplot as plt
+
+from glue.tests.visual.helpers import visual_test
+
+from glue.viewers.scatter.viewer import SimpleScatterViewer
+from glue.core.application_base import Application
+from glue.core.data import Data
+from glue.core.link_helpers import LinkSame
+
+
+@visual_test
+def test_simple_viewer():
+
+ # Make sure the simple viewer can be instantiated
+
+ data1 = Data(x=[1, 2, 3], label='data1')
+ data2 = Data(y=[1, 2, 3], label='data2')
+
+ app = Application()
+ app.data_collection.append(data1)
+ app.data_collection.append(data2)
+
+ viewer = app.new_data_viewer(SimpleScatterViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ app.data_collection.new_subset_group(label='subset1', subset_state=data1.id['x'] > 2)
+
+ return viewer.figure
+
+
+@visual_test
+def test_scatter_density_map():
+
+ # Test the scatter density map
+
+ np.random.seed(12345)
+
+ app = Application()
+
+ x = np.random.normal(3, 1, 100)
+ y = np.random.normal(2, 1.5, 100)
+ c = np.hypot(x - 3, y - 2)
+ s = (x - 3)
+
+ data1 = app.add_data(a={"x": x, "y": y, "c": c, "s": s})[0]
+
+ xx = np.random.normal(3, 1, 1000000)
+ yy = np.random.normal(2, 1.5, 1000000)
+
+ data2 = app.add_data(a={"x": xx, "y": yy})[0]
+
+ app.data_collection.add_link(LinkSame(data1.id['x'], data2.id['x']))
+ app.data_collection.add_link(LinkSame(data1.id['y'], data2.id['y']))
+
+ viewer = app.new_data_viewer(SimpleScatterViewer)
+ viewer.add_data(data1)
+ viewer.add_data(data2)
+
+ viewer.state.layers[0].cmap_mode = 'Linear'
+ viewer.state.layers[0].cmap_att = data1.id['c']
+ viewer.state.layers[0].cmap = plt.cm.viridis
+ viewer.state.layers[0].size_mode = 'Linear'
+ viewer.state.layers[0].size_att = data1.id['s']
+
+ viewer.state.layers[1].zorder = 0.5
+
+ app.data_collection.new_subset_group(label='subset1', subset_state=data1.id['x'] > 2)
+
+ return viewer.figure
+
+
+def test_reset_limits():
+
+ data1 = Data(x=np.arange(1000), y=np.arange(1000) + 1000, label='data')
+
+ app = Application()
+ app.data_collection.append(data1)
+
+ viewer = app.new_data_viewer(SimpleScatterViewer)
+ viewer.add_data(data1)
+
+ viewer.state.reset_limits()
+
+ # Note that there is a margin included which is why the limits are not 0 to 999
+
+ assert_allclose(viewer.state.x_min, -39.96)
+ assert_allclose(viewer.state.x_max, 1038.96)
+
+ assert_allclose(viewer.state.y_min, 1000 - 39.96)
+ assert_allclose(viewer.state.y_max, 1000 + 1038.96)
+
+ viewer.state.x_limits_percentile = 90
+ viewer.state.y_limits_percentile = 80
+
+ viewer.state.reset_limits()
+
+ assert_allclose(viewer.state.x_min, 13.986)
+ assert_allclose(viewer.state.x_max, 985.014)
+
+ assert_allclose(viewer.state.y_min, 1000 + 67.932)
+ assert_allclose(viewer.state.y_max, 1000 + 931.068)
diff --git a/glue/viewers/scatter/viewer.py b/glue/viewers/scatter/viewer.py
index 9608d0ac2..c986fd706 100644
--- a/glue/viewers/scatter/viewer.py
+++ b/glue/viewers/scatter/viewer.py
@@ -5,9 +5,11 @@
from glue.utils import mpl_to_datetime64
from glue.viewers.scatter.compat import update_scatter_viewer_state
from glue.viewers.matplotlib.mpl_axes import init_mpl
+from glue.viewers.matplotlib.viewer import SimpleMatplotlibViewer
+from glue.viewers.scatter.state import ScatterViewerState
+from glue.viewers.scatter.layer_artist import ScatterLayerArtist
-
-__all__ = ['MatplotlibScatterMixin']
+__all__ = ['MatplotlibScatterMixin', 'SimpleScatterViewer']
class MatplotlibScatterMixin(object):
@@ -179,3 +181,14 @@ def apply_roi(self, roi, override_mode=None):
@staticmethod
def update_viewer_state(rec, context):
return update_scatter_viewer_state(rec, context)
+
+
+class SimpleScatterViewer(MatplotlibScatterMixin, SimpleMatplotlibViewer):
+
+ _state_cls = ScatterViewerState
+ _data_artist_cls = ScatterLayerArtist
+ _subset_artist_cls = ScatterLayerArtist
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ MatplotlibScatterMixin.setup_callbacks(self)
diff --git a/glue/viewers/table/qt/__init__.py b/glue/viewers/table/qt/__init__.py
deleted file mode 100644
index 6220498c9..000000000
--- a/glue/viewers/table/qt/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.table.qt is deprecated, use glue_qt.viewers.table instead', GlueDeprecationWarning)
-from glue_qt.viewers.table import * # noqa
diff --git a/glue/viewers/table/qt/data_viewer.py b/glue/viewers/table/qt/data_viewer.py
deleted file mode 100644
index e54734d0b..000000000
--- a/glue/viewers/table/qt/data_viewer.py
+++ /dev/null
@@ -1,4 +0,0 @@
-import warnings
-from glue.utils.error import GlueDeprecationWarning
-warnings.warn('Importing from glue.viewers.table.qt.data_viewer is deprecated, use glue_qt.viewers.table.data_viewer instead', GlueDeprecationWarning)
-from glue_qt.viewers.table.data_viewer import * # noqa
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..03bd076da
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,14 @@
+[build-system]
+requires = ["setuptools",
+ "setuptools_scm",
+ "wheel"]
+build-backend = 'setuptools.build_meta'
+
+[tool.gilesbot]
+[tool.gilesbot.circleci_artifacts]
+enabled = true
+
+[tool.gilesbot.circleci_artifacts.figure_report]
+url = "results/fig_comparison.html"
+message = "Click details to see the image test comparisons, for py311-test-visual"
+report_on_fail = true
diff --git a/setup.cfg b/setup.cfg
index c94696d1b..44e4f5bed 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,11 +38,7 @@ install_requires =
pvextractor>=0.2
importlib_resources>=1.3; python_version<'3.9'
importlib_metadata>=3.6; python_version<'3.10'
- # For now, we include a dependency on glue-qt so that imports of the
- # Qt-related functionality continue to work albeit with a deprecation
- # warning. Once the deprecation phase is over, we can remove this
- # dependency as well as all the compatibility imports.
- glue-qt>=0.1.0
+ shapely>=2.0
[options.entry_points]
glue.plugins =
@@ -85,6 +81,8 @@ test =
pytest-flake8
h5py>=2.10; platform_system=="Linux"
objgraph
+visualtest =
+ pytest-mpl
[options.package_data]
* = *.png, *.ui, *.glu, *.hdf5, *.fits, *.xlsx, *.txt, *.csv, *.svg, *.vot
@@ -107,13 +105,15 @@ filterwarnings =
[coverage:run]
omit =
- glue/*tests/*
- glue/core/odict.py,
- glue/core/glue_pickle.py
+ glue/tests/*
+ glue/*/tests/*
+ glue/*/*/tests/*
+ glue/*/*/*/tests/*
glue/external/*
- */glue/*tests/*
- */glue/core/odict.py,
- */glue/core/glue_pickle.py
+ */glue/tests/*
+ */glue/*/tests/*
+ */glue/*/*/tests/*
+ */glue/*/*/*/tests/*
*/glue/external/*
[coverage:paths]
diff --git a/tox.ini b/tox.ini
index 2fe7aa9c8..469416cda 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py{38,39,310,311}-{codestyle,test,docs}-all-{dev,legacy}
+ py{38,39,310,311}-{codestyle,test,docs}-all-{dev,legacy}{,-visual}
requires = pip >= 18.0
setuptools >= 30.3.0
@@ -11,6 +11,7 @@ passenv =
HOME
setenv =
dev: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
+ 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/images/{envname}/
whitelist_externals =
find
rm
@@ -44,9 +45,10 @@ extras =
test
all: all
docs: docs
+ visual: visualtest
commands =
test: pip freeze
- test: pytest --pyargs glue --cov glue --cov-config={toxinidir}/setup.cfg {posargs}
+ test: pytest --pyargs glue --cov glue --cov-config={toxinidir}/setup.cfg {env:MPLFLAGS} {posargs}
docs: sphinx-build -W -n -b html -d _build/doctrees . _build/html
[testenv:codestyle]