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 +
glue_qt.utils.colors
+
+ + QColormapCombo + QComboBox +
glue_qt.utils.colors
+
+
+ + +
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.*