diff --git a/glue_qt/utils/colors.py b/glue_qt/utils/colors.py
index 806cc63a..e89cdffc 100644
--- a/glue_qt/utils/colors.py
+++ b/glue_qt/utils/colors.py
@@ -195,6 +195,18 @@ def __init__(self, *args, **kwargs):
self.addItem(label, userData=UserDataWrapper(cmap))
self._update_icons()
+ def refresh_options(self, colormaps=None):
+ if colormaps is None:
+ self.clear()
+ for label, cmap in config.colormaps:
+ self.addItem(label, userData=UserDataWrapper(cmap))
+ self._update_icons()
+ else:
+ self.clear()
+ for label, cmap in colormaps:
+ self.addItem(label, userData=UserDataWrapper(cmap))
+ self._update_icons()
+
def _update_icons(self):
self.setIconSize(QtCore.QSize(self.width(), 15))
for index in range(self.count()):
diff --git a/glue_qt/viewers/image/data_viewer.py b/glue_qt/viewers/image/data_viewer.py
index 330f722c..01a710c2 100644
--- a/glue_qt/viewers/image/data_viewer.py
+++ b/glue_qt/viewers/image/data_viewer.py
@@ -1,6 +1,6 @@
from glue_qt.viewers.matplotlib.data_viewer import MatplotlibDataViewer
-from glue_qt.viewers.scatter.layer_style_editor import ScatterLayerStyleEditor
-from glue.viewers.scatter.layer_artist import ScatterLayerArtist
+from glue_qt.viewers.scatter.layer_style_editor import ScatterLayerStyleEditor, ScatterRegionLayerStyleEditor
+from glue.viewers.scatter.layer_artist import ScatterLayerArtist, ScatterRegionLayerArtist
from glue_qt.viewers.image.layer_style_editor import ImageLayerStyleEditor
from glue_qt.viewers.image.layer_style_editor_subset import ImageLayerSubsetStyleEditor
from glue.viewers.image.layer_artist import ImageLayerArtist, ImageSubsetLayerArtist
@@ -26,7 +26,8 @@ class ImageViewer(MatplotlibImageMixin, MatplotlibDataViewer):
_default_mouse_mode_cls = RoiClickAndDragMode
_layer_style_widget_cls = {ImageLayerArtist: ImageLayerStyleEditor,
ImageSubsetLayerArtist: ImageLayerSubsetStyleEditor,
- ScatterLayerArtist: ScatterLayerStyleEditor}
+ ScatterLayerArtist: ScatterLayerStyleEditor,
+ ScatterRegionLayerArtist: ScatterRegionLayerStyleEditor}
_state_cls = ImageViewerState
_options_cls = ImageOptionsWidget
diff --git a/glue_qt/viewers/image/tests/test_display_region_data.py b/glue_qt/viewers/image/tests/test_display_region_data.py
new file mode 100644
index 00000000..91e57762
--- /dev/null
+++ b/glue_qt/viewers/image/tests/test_display_region_data.py
@@ -0,0 +1,219 @@
+# pylint: disable=I0011,W0613,W0201,W0212,E1101,E1103
+
+import os
+
+import numpy as np
+import shapely
+from shapely.geometry import MultiPolygon, Polygon, Point
+
+from glue.core.data_region import RegionData
+from glue.core.data import Data
+from glue.core.component import Component
+from glue_qt.utils import combo_as_string, process_events
+from glue_qt.app import GlueApplication
+from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE
+from glue.core.link_helpers import LinkSame
+
+from astropy.wcs import WCS
+
+
+from ..data_viewer import ImageViewer
+
+DATA = os.path.join(os.path.dirname(__file__), 'data')
+
+
+class TestRegionScatterViewer(object):
+
+ def setup_method(self, method):
+
+ 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])
+
+ my_geoms = np.array([poly_1, poly_2, polygons])
+
+ representative_points = [s.representative_point() for s in my_geoms]
+
+ cell_number = Component(np.array([1, 2, 3]))
+
+ self.region_data = RegionData(regions=my_geoms,
+ cell_number=cell_number)
+
+ random_2d_array = np.random.randint(1, 20, size=(100, 100))
+ self.data_2d = Data(label='image_data', z=random_2d_array)
+ self.catalog = Data(label='catalog', c=[1, 3, 2], d=[4, 3, 3])
+
+ self.application = GlueApplication()
+
+ self.session = self.application.session
+
+ self.hub = self.session.hub
+
+ self.data_collection = self.session.data_collection
+ self.data_collection.append(self.region_data)
+ self.data_collection.append(self.data_2d)
+ self.data_collection.append(self.catalog)
+
+ self.viewer = self.application.new_data_viewer(ImageViewer)
+
+ self.data_collection.register_to_hub(self.hub)
+ self.viewer.register_to_hub(self.hub)
+
+ self.options_widget = self.viewer.options_widget()
+
+ def teardown_method(self, method):
+
+ # Properly close viewer and application
+ self.viewer.close()
+ self.viewer = None
+ self.application.close()
+ self.application = None
+
+ # Make sure cache is empty
+ if len(PIXEL_CACHE) > 0:
+ raise Exception("Pixel cache contains {0} elements".format(len(PIXEL_CACHE)))
+ if len(ARRAY_CACHE) > 0:
+ raise Exception("Array cache contains {0} elements".format(len(ARRAY_CACHE)))
+
+ def test_link_first_then_add(self):
+
+ # Check defaults when we add data
+
+ self.viewer.add_data(self.data_2d)
+ link1 = LinkSame(self.region_data.center_x_id, self.data_2d.pixel_component_ids[0])
+ link2 = LinkSame(self.region_data.center_y_id, self.data_2d.pixel_component_ids[1])
+
+ self.data_collection.add_link(link1)
+ self.data_collection.add_link(link2)
+ process_events()
+
+ assert len(self.viewer.state.layers) == 1
+
+ self.viewer.add_data(self.region_data)
+
+ assert len(self.viewer.state.layers) == 2
+ assert self.viewer.layers[0].enabled # image
+ assert self.viewer.layers[1].enabled # regions
+
+ def test_add_first_then_link(self):
+
+ # Check defaults when we add data
+
+ self.viewer.add_data(self.data_2d)
+
+ assert combo_as_string(self.options_widget.ui.combosel_x_att_world) == 'Coordinate components:Pixel Axis 0 [y]:Pixel Axis 1 [x]'
+ assert combo_as_string(self.options_widget.ui.combosel_y_att_world) == 'Coordinate components:Pixel Axis 0 [y]:Pixel Axis 1 [x]'
+
+ assert self.viewer.axes.get_xlabel() == 'Pixel Axis 1 [x]'
+ assert self.viewer.state.x_att_world is self.data_2d.id['Pixel Axis 1 [x]']
+ assert self.viewer.state.x_att is self.data_2d.pixel_component_ids[1]
+
+ assert self.viewer.axes.get_ylabel() == 'Pixel Axis 0 [y]'
+ assert self.viewer.state.y_att_world is self.data_2d.id['Pixel Axis 0 [y]']
+ assert self.viewer.state.y_att is self.data_2d.pixel_component_ids[0]
+
+ assert not self.viewer.state.x_log
+ assert not self.viewer.state.y_log
+
+ assert len(self.viewer.state.layers) == 1
+
+ self.viewer.add_data(self.region_data)
+
+ assert len(self.viewer.state.layers) == 2
+ assert self.viewer.layers[0].enabled # image
+ assert not self.viewer.layers[1].enabled # regions
+
+ process_events()
+
+ link1 = LinkSame(self.region_data.center_x_id, self.data_2d.pixel_component_ids[0])
+ link2 = LinkSame(self.region_data.center_y_id, self.data_2d.pixel_component_ids[1])
+
+ self.data_collection.add_link(link1)
+ self.data_collection.add_link(link2)
+ process_events()
+
+ assert len(self.viewer.state.layers) == 2
+ assert self.viewer.layers[0].enabled # image
+ assert self.viewer.layers[1].enabled # regions
+
+ def test_subset(self):
+ self.viewer.add_data(self.data_2d)
+
+ self.viewer.add_data(self.region_data)
+ link1 = LinkSame(self.region_data.center_x_id, self.data_2d.pixel_component_ids[0])
+ link2 = LinkSame(self.region_data.center_y_id, self.data_2d.pixel_component_ids[1])
+
+ self.data_collection.add_link(link1)
+ self.data_collection.add_link(link2)
+
+ self.data_collection.new_subset_group(subset_state=self.region_data.center_x_id > 20)
+
+ process_events()
+
+ assert self.viewer.layers[0].enabled # image
+ assert self.viewer.layers[1].enabled # scatter
+ assert self.viewer.layers[2].enabled # image subset
+ assert self.viewer.layers[3].enabled # scatter subset
+
+
+class TestWCSRegionDisplay(object):
+ def setup_method(self, method):
+
+ wcs1 = WCS(naxis=2)
+ wcs1.wcs.ctype = 'DEC--TAN', 'RA---TAN'
+ wcs1.wcs.set()
+
+ self.image1 = Data(label='image1', a=[[3, 3], [2, 2]], b=[[4, 4], [3, 2]],
+ coords=wcs1)
+ SHAPELY_CIRCLE_ARRAY = np.array([Point(2.5, 2.5).buffer(1), Point(1, 1).buffer(1)])
+ self.region_data = RegionData(label='My Regions',
+ color=np.array(['red', 'blue']),
+ area=shapely.area(SHAPELY_CIRCLE_ARRAY),
+ boundary=SHAPELY_CIRCLE_ARRAY)
+ self.application = GlueApplication()
+
+ self.session = self.application.session
+
+ self.hub = self.session.hub
+
+ self.data_collection = self.session.data_collection
+ self.data_collection.append(self.image1)
+ self.data_collection.append(self.region_data)
+
+ self.viewer = self.application.new_data_viewer(ImageViewer)
+
+ self.data_collection.register_to_hub(self.hub)
+ self.viewer.register_to_hub(self.hub)
+
+ def teardown_method(self, method):
+
+ # Properly close viewer and application
+ self.viewer.close()
+ self.viewer = None
+ self.application.close()
+ self.application = None
+
+ # Make sure cache is empty
+ if len(PIXEL_CACHE) > 0:
+ raise Exception("Pixel cache contains {0} elements".format(len(PIXEL_CACHE)))
+ if len(ARRAY_CACHE) > 0:
+ raise Exception("Array cache contains {0} elements".format(len(ARRAY_CACHE)))
+
+ def test_basics(self):
+ self.viewer.add_data(self.image1)
+
+ link1 = LinkSame(self.region_data.center_x_id, self.image1.world_component_ids[0])
+ link2 = LinkSame(self.region_data.center_y_id, self.image1.world_component_ids[1])
+
+ self.data_collection.add_link(link1)
+ self.data_collection.add_link(link2)
+
+ self.viewer.add_data(self.region_data)
+
+ self.viewer.state._display_world
+ assert len(self.viewer.state.layers) == 2
+ assert self.viewer.layers[0].enabled
+ assert self.viewer.layers[1].enabled
diff --git a/glue_qt/viewers/scatter/data_viewer.py b/glue_qt/viewers/scatter/data_viewer.py
index 38d14beb..8b7e21f5 100644
--- a/glue_qt/viewers/scatter/data_viewer.py
+++ b/glue_qt/viewers/scatter/data_viewer.py
@@ -15,7 +15,9 @@
class ScatterViewer(MatplotlibScatterMixin, MatplotlibDataViewer):
LABEL = '2D Scatter'
- _layer_style_widget_cls = ScatterLayerStyleEditor
+ # We don't yet allow ScatterRegionLayerArtists directly on a ScatterViewer.
+ # If we wanted to do so, we would need to expand these options.
+ _layer_style_widget_cls = {ScatterLayerArtist: ScatterLayerStyleEditor}
_state_cls = ScatterViewerState
_options_cls = ScatterOptionsWidget
_data_artist_cls = ScatterLayerArtist
diff --git a/glue_qt/viewers/scatter/layer_style_editor.py b/glue_qt/viewers/scatter/layer_style_editor.py
index bcb39edf..1cefbebe 100644
--- a/glue_qt/viewers/scatter/layer_style_editor.py
+++ b/glue_qt/viewers/scatter/layer_style_editor.py
@@ -4,6 +4,7 @@
from qtpy import QtWidgets, QtGui
from qtpy.QtCore import Qt
+from echo import delay_callback
from glue.core import BaseData
from echo.qt import autoconnect_callbacks_to_qt, connect_value
@@ -49,6 +50,8 @@ def __init__(self, layer, parent=None):
self.layer_state.viewer_state.add_callback('x_att', self._update_checkboxes)
self.layer_state.viewer_state.add_callback('y_att', self._update_checkboxes)
+ self.layer_state.add_callback('cmap_att', self._update_cmaps, priority=10000)
+
if hasattr(self.layer_state.viewer_state, 'plot_mode'):
self.layer_state.viewer_state.add_callback('plot_mode', self._update_vectors_visible)
@@ -226,3 +229,80 @@ def _update_cmap_mode(self, cmap_mode=None):
self.ui.combodata_cmap.show()
self.ui.label_colormap.show()
self.ui.color_color.hide()
+
+ def _update_cmaps(self, *args):
+
+ with delay_callback(self.layer_state, 'cmap'):
+ if isinstance(self.layer_state.layer, BaseData):
+ layer = self.layer_state.layer
+ else:
+ layer = self.layer_state.layer.data
+
+ actual_component = layer.get_component(self.layer_state.cmap_att)
+ if getattr(actual_component, 'preferred_cmap', False):
+ cmap = actual_component.preferred_cmap
+ name = actual_component.cmap_name
+ self.ui.combodata_cmap.refresh_options(colormaps=[(name, cmap)])
+ else:
+ self.ui.combodata_cmap.refresh_options()
+
+
+class ScatterRegionLayerStyleEditor(QtWidgets.QWidget):
+
+ def __init__(self, layer, parent=None):
+
+ super().__init__(parent=parent)
+
+ self.ui = load_ui('region_layer_style_editor.ui', self,
+ directory=os.path.dirname(__file__))
+
+ connect_kwargs = {'alpha': dict(value_range=(0, 1))}
+ self._connections = autoconnect_callbacks_to_qt(layer.state, self.ui, connect_kwargs)
+
+ self.layer_state = layer.state
+
+ self.layer_state.add_callback('cmap_mode', self._update_cmap_mode)
+
+ self._update_cmap_mode()
+ self.layer_state.add_callback('cmap_att', self._update_cmaps, priority=10000)
+
+ def _update_cmap_mode(self, cmap_mode=None):
+
+ if self.layer_state.cmap_mode == 'Fixed':
+ self.ui.label_cmap_attribute.hide()
+ self.ui.combosel_cmap_att.hide()
+ self.ui.label_cmap_limits.hide()
+ self.ui.valuetext_cmap_vmin.hide()
+ self.ui.valuetext_cmap_vmax.hide()
+ self.ui.button_flip_cmap.hide()
+ self.ui.combodata_cmap.hide()
+ self.ui.label_colormap.hide()
+ self.ui.color_color.show()
+ self.ui.combosel_percentile.hide()
+ else:
+ self.ui.label_cmap_attribute.show()
+ self.ui.combosel_cmap_att.show()
+ self.ui.label_cmap_limits.show()
+ self.ui.valuetext_cmap_vmin.show()
+ self.ui.valuetext_cmap_vmax.show()
+ self.ui.button_flip_cmap.show()
+ self.ui.combodata_cmap.show()
+ self.ui.label_colormap.show()
+ self.ui.color_color.hide()
+ self.ui.combosel_percentile.show()
+
+ def _update_cmaps(self, *args):
+
+ with delay_callback(self.layer_state, 'cmap'):
+ if isinstance(self.layer_state.layer, BaseData):
+ layer = self.layer_state.layer
+ else:
+ layer = self.layer_state.layer.data
+
+ actual_component = layer.get_component(self.layer_state.cmap_att)
+ if getattr(actual_component, 'preferred_cmap', False):
+ cmap = actual_component.preferred_cmap
+ name = actual_component.cmap_name
+ self.ui.combodata_cmap.refresh_options(colormaps=[(name, cmap)])
+ else:
+ self.ui.combodata_cmap.refresh_options()
diff --git a/glue_qt/viewers/scatter/layer_style_editor.ui b/glue_qt/viewers/scatter/layer_style_editor.ui
index dee4b110..03583d42 100644
--- a/glue_qt/viewers/scatter/layer_style_editor.ui
+++ b/glue_qt/viewers/scatter/layer_style_editor.ui
@@ -1051,4 +1051,4 @@
-
+
\ No newline at end of file
diff --git a/glue_qt/viewers/scatter/region_layer_style_editor.ui b/glue_qt/viewers/scatter/region_layer_style_editor.ui
new file mode 100644
index 00000000..4c8de5e5
--- /dev/null
+++ b/glue_qt/viewers/scatter/region_layer_style_editor.ui
@@ -0,0 +1,251 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 394
+ 296
+
+
+
+ Form
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ color
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 80
+ 16777215
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ attribute
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ limits
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 132
+ 20
+
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ padding: 0px
+
+
+ ⇄
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ colormap
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QComboBox::AdjustToMinimumContentsLengthWithIcon
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ opacity
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+ 100
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ fill
+
+
+ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 54
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 278
+ 20
+
+
+
+
+ -
+
+
+ QComboBox::AdjustToMinimumContentsLengthWithIcon
+
+
+
+
+
+
+
+ QColorBox
+ QLabel
+
+
+
+ QColormapCombo
+ QComboBox
+
+
+
+
+
+
diff --git a/setup.cfg b/setup.cfg
index e214b88d..a14e6663 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,7 +26,7 @@ packages = find:
python_requires = >=3.8
setup_requires = setuptools_scm
install_requires =
- glue-core>=1.13.1
+ glue-core>=1.15.0
numpy>=1.17
matplotlib>=3.2
scipy>=1.1
diff --git a/tox.ini b/tox.ini
index 0fe6ef58..7c490678 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,9 @@ deps =
pyqt514: PyQt5==5.14.*
pyqt515: PyQt5==5.15.*
pyqt63: PyQt6==6.3.*
+ pyqt63: PyQt6-Qt6==6.3.*
pyqt64: PyQt6==6.4.*
+ pyqt64: PyQt6-Qt6==6.4.*
pyside514: PySide2==5.14.*
pyside515: PySide2==5.15.*
pyside63: PySide6==6.3.*