diff --git a/CHANGES.rst b/CHANGES.rst index dcd5e31261..edc73564c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,12 @@ New Features - Viewer data-menus are now found in the legend on the right of the viewer. [#3281] +- Added 'select_rows' method to plugin tables to enable changing + curent selection by indicies or slice. Also added 'select_all' and 'select_none' + methods to change active selection to all table items or clear all selected + items without clearing the table. [#3381] + + Cubeviz ^^^^^^^ diff --git a/jdaviz/configs/imviz/plugins/catalogs/catalogs.py b/jdaviz/configs/imviz/plugins/catalogs/catalogs.py index 0fdbafbdbf..a70e89e4ee 100644 --- a/jdaviz/configs/imviz/plugins/catalogs/catalogs.py +++ b/jdaviz/configs/imviz/plugins/catalogs/catalogs.py @@ -55,7 +55,8 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl @property def user_api(self): return PluginUserApi(self, expose=('clear_table', 'export_table', - 'zoom_to_selected')) + 'zoom_to_selected', 'select_rows', + 'select_all', 'select_none')) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/jdaviz/configs/imviz/tests/test_catalogs.py b/jdaviz/configs/imviz/tests/test_catalogs.py index 956c32af68..6fffda86b2 100644 --- a/jdaviz/configs/imviz/tests/test_catalogs.py +++ b/jdaviz/configs/imviz/tests/test_catalogs.py @@ -135,7 +135,7 @@ def test_plugin_image_with_result(self, imviz_helper, tmp_path): assert catalogs_plugin.results_available assert catalogs_plugin.number_of_results == prev_results - catalogs_plugin.table.selected_rows = catalogs_plugin.table.items[0:2] + catalogs_plugin.select_rows(slice(0, 2)) assert len(catalogs_plugin.table.selected_rows) == 2 # test Gaia catalog @@ -308,7 +308,7 @@ def test_zoom_to_selected(imviz_helper, image_2d_wcs, tmp_path): catalogs_plugin._obj.search() # select both sources - catalogs_plugin._obj.table.selected_rows = catalogs_plugin._obj.table.items + catalogs_plugin.select_all() # check viewer limits before zoom xmin, xmax, ymin, ymax = imviz_helper.app._jdaviz_helper._default_viewer.get_limits() @@ -392,3 +392,65 @@ def test_offline_ecsv_catalog_with_extra_columns(imviz_helper, image_2d_wcs, tmp assert item['is_extended'] == tbl['is_extended'][idx] assert float(item['roundness']) == tbl['roundness'][idx] assert float(item['sharpness']) == tbl['sharpness'][idx] + + +def test_select_catalog_table_rows(imviz_helper, image_2d_wcs, tmp_path): + + """Test the ``select_rows`` functionality on table in plugin.""" + + arr = np.ones((500, 500)) + ndd = NDData(arr, wcs=image_2d_wcs) + imviz_helper.load_data(ndd) + + # write out table to load back in + # NOTE: if we ever support loading Table obj directly, replace this and + # remove tmp_path + sky_coord = SkyCoord(ra=[337.49, 337.46, 337.47, 337.48, 337.49, 337.50], + dec=[-20.81, -20.78, -20.79, -20.80, -20.77, -20.76], + unit='deg') + tbl = Table({'sky_centroid': [sky_coord], + 'label': ['Source_1', 'Source_2', 'Source_3', 'Source_4', + 'Source_5', 'Source_6']}) + tbl_file = str(tmp_path / 'test_catalog.ecsv') + tbl.write(tbl_file, overwrite=True) + + catalogs_plugin = imviz_helper.plugins['Catalog Search'] + plugin_table = catalogs_plugin._obj.table + + # load catalog + catalogs_plugin._obj.from_file = tbl_file + catalogs_plugin._obj.search() + + # select a single row: + catalogs_plugin.select_rows(3) + assert len(plugin_table.selected_rows) == 1 + assert plugin_table.selected_rows[0]['Right Ascension (degrees)'] == '337.48000' + + # select multiple rows by indices + catalogs_plugin.select_rows([3, 2, 4]) + assert len(plugin_table.selected_rows) == 3 + assert plugin_table.selected_rows[0]['Right Ascension (degrees)'] == '337.48000' + assert plugin_table.selected_rows[1]['Right Ascension (degrees)'] == '337.47000' + assert plugin_table.selected_rows[2]['Right Ascension (degrees)'] == '337.49000' + + # select a range of rows with a slice + catalogs_plugin.select_rows(slice(0, 2)) + assert len(plugin_table.selected_rows) == 2 + assert plugin_table.selected_rows[0]['Right Ascension (degrees)'] == '337.49000' + assert plugin_table.selected_rows[1]['Right Ascension (degrees)'] == '337.46000' + + # select rows with multi dim. numpy slice + catalogs_plugin.select_rows(np.s_[0:2, 3:5]) + assert len(plugin_table.selected_rows) == 4 + assert plugin_table.selected_rows[0]['Right Ascension (degrees)'] == '337.49000' + assert plugin_table.selected_rows[1]['Right Ascension (degrees)'] == '337.46000' + assert plugin_table.selected_rows[2]['Right Ascension (degrees)'] == '337.48000' + assert plugin_table.selected_rows[3]['Right Ascension (degrees)'] == '337.49000' + + # test select_all + catalogs_plugin.select_all() + assert len(plugin_table.selected_rows) == 6 + + # test select_none + catalogs_plugin.select_none() + assert len(plugin_table.selected_rows) == 0 diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index a678570a95..5348957fcd 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -4713,7 +4713,9 @@ def __init__(self, plugin, name='table', selected_rows_changed_callback=None, @property def user_api(self): - return UserApiWrapper(self, ('clear_table', 'export_table')) + return UserApiWrapper(self, ('clear_table', 'export_table', + 'select_rows', 'select_all', + 'select_none')) def default_value_for_column(self, colname=None, value=None): if colname in self._default_values_by_colname: @@ -4829,6 +4831,45 @@ def clear_table(self): self._qtable = None self._plugin.session.hub.broadcast(PluginTableModifiedMessage(sender=self)) + def select_rows(self, rows): + """ + Select rows from the current table by index, indices, or slice. + + Parameters + ---------- + rows : int, list of int, slice, or tuple of slice + The rows to select. This can be: + - An integer specifying a single row index. + - A list of integers specifying multiple row indices. + - A slice object specifying a range of rows. + - A tuple of slices (e.g using numpy slice) + + """ + + if isinstance(rows, (list, tuple)): + selected = [] + if isinstance(rows[0], slice): + for sl in rows: + selected += self.items[sl] + else: + selected = [self.items[i] for i in rows] + + elif isinstance(rows, slice): + selected = self.items[rows] + elif isinstance(rows, int): + selected = [self.items[rows]] + + # apply new selection + self.selected_rows = selected + + def select_all(self): + """ Select all rows in table.""" + self.select_rows(slice(0, len(self) + 1)) + + def select_none(self): + """ Deselect all rows in table.""" + self.selected_rows = [] + def vue_clear_table(self, data=None): # if the plugin (or via the TableMixin) has its own clear_table implementation, # call that, otherwise call the one defined here @@ -4860,6 +4901,8 @@ class TableMixin(VuetifyTemplate, HubListener): * :meth:`clear_table` * :meth:`export_table` + * :meth:`select_rows` + * :meth:`select_all` To render in the plugin's vue file:: @@ -4898,6 +4941,31 @@ def export_table(self, filename=None, overwrite=False): """ return self.table.export_table(filename=filename, overwrite=overwrite) + def select_rows(self, rows): + """ + Select rows from the current table by index, indices, or slice. + + Parameters + ---------- + rows : int, list of int, slice, or tuple of slice + The rows to select. This can be: + - An integer specifying a single row index. + - A list of integers specifying multiple row indices. + - A slice object specifying a range of rows. + - A tuple of slices (e.g using numpy slice) + + """ + + self.table.select_rows(rows) + + def select_all(self): + """ Select all rows in table.""" + self.table.select_all() + + def select_none(self): + """ Deselect all rows in table.""" + self.table.select_none() + class Plot(PluginSubcomponent): """