Skip to content

Commit

Permalink
Make catalog tables interactive (spacetelescope#3099)
Browse files Browse the repository at this point in the history
* adding the table feature for the import from file option

Resolved merge conflict in catalogs.py and catalogs.vue#

Resolved merge conflict in catalogs.py and catalogs.vue

adding new markers

added x,y pixel coords to table; will be used for markers later

small changes

orginal markers are not being plotted

Added user marks

prep for rebase

* fixing codestyle

* added a function that zooms in on selected sources

* adjusting zoom for single select case

* Fix commented out code for style check

* Fix bug and add zoom in button

* Address review comments

* Update change log

* Add tests and update docs

* Enable row selection and zoom for Gaia catalog

* Address review comments

* Move code to init

---------

Co-authored-by: Katherine Carver <kcarver@osx-summer2401.local.stsci.edu>
Co-authored-by: Katherine Carver <kcarver@osx-summer2401.stsci.edu>
Co-authored-by: Jesse Averbukh <javerbukh@gmail.com>
  • Loading branch information
4 people authored Sep 13, 2024
1 parent 88ff8da commit 6b83d77
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 37 deletions.
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Cubeviz
Imviz
^^^^^

- Added a table with catalog search results. [#2915, #3101]
- Added a table with catalog search results. [#2915, #3101, #3099]

- "Imviz Line Profiles (XY)" plugin is renamed to "Image Profiles (XY)". [#3121]

Expand Down
8 changes: 5 additions & 3 deletions docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ catalog dropdown menu.

.. note::

This plugin is still under active development. As a result, the search only uses the SDSS DR17 catalog
and works best when you only have a single image loaded in a viewer.
This plugin is still under active development. As a result, the search only uses the SDSS DR17 catalog and
the Gaia catalog and works best when you only have a single image loaded in a viewer.

To load a catalog from a supported `JWST ECSV catalog file <https://jwst-pipeline.readthedocs.io/en/latest/jwst/source_catalog/main.html#output-products>`_, choose "From File...".
The file must be able to be parsed by `astropy.table.Table.read` and contains the following columns:
Expand All @@ -403,7 +403,9 @@ are not stored. To save the current result before submitting a new query, you ca
portion of the image. Additional steps will be needed to filter out these points, if necessary.

Performing a search populates a table that contains the
right ascension, declination, and the object ID of the found sources.
right ascension, declination, and the object ID of the found sources. Checkboxes next to the rows
can be selected and the corresponding marks in the viewer will change to orange circles. When :guilabel:`Zoom to Selected`,
the viewer will zoom to encompass the selected rows in the table.

.. _imviz-footprints:

Expand Down
130 changes: 102 additions & 28 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from astropy import units as u
from astropy.table import QTable
from astropy.coordinates import SkyCoord
from traitlets import List, Unicode, Bool, Int
from traitlets import List, Unicode, Bool, Int, observe

from jdaviz.core.events import SnackbarMessage
from jdaviz.core.registries import tray_registry
Expand All @@ -12,8 +12,12 @@
with_spinner)
from jdaviz.core.custom_traitlets import IntHandleEmpty

from jdaviz.core.marks import CatalogMark

from jdaviz.core.template_mixin import TableMixin
from jdaviz.core.user_api import PluginUserApi
from echo import delay_callback


__all__ = ['Catalogs']

Expand All @@ -31,6 +35,7 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
"""
template_file = __file__, "catalogs.vue"
uses_active_status = Bool(True).tag(sync=True)
catalog_items = List([]).tag(sync=True)
catalog_selected = Unicode("").tag(sync=True)
results_available = Bool(False).tag(sync=True)
Expand All @@ -41,7 +46,10 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl
_default_table_values = {
'Right Ascension (degrees)': np.nan,
'Declination (degrees)': np.nan,
'Object ID': np.nan}
'Object ID': np.nan,
'id': np.nan,
'x_coord': np.nan,
'y_coord': np.nan}

@property
def user_api(self):
Expand All @@ -60,11 +68,15 @@ def __init__(self, *args, **kwargs):
self._marker_name = 'catalog_results'

# initializing the headers in the table that is displayed in the UI
headers = ['Right Ascension (degrees)', 'Declination (degrees)', 'Object ID']
headers = ['Right Ascension (degrees)', 'Declination (degrees)',
'Object ID', 'x_coord', 'y_coord']

self.table.headers_avail = headers
self.table.headers_visible = headers
self.table._default_values_by_colname = self._default_table_values
self.table._selected_rows_changed_callback = lambda msg: self.plot_selected_points()
self.table.item_key = 'id'
self.table.show_rowselect = True

@staticmethod
def _file_parser(path):
Expand Down Expand Up @@ -163,15 +175,6 @@ def search(self, error_on_fail=False):
query_region_result['dec'],
unit='deg')

# adding in coords + Id's into table
# NOTE: If performance becomes a problem, see
# https://docs.astropy.org/en/stable/table/index.html#performance-tips
for row in self.app._catalog_source_table:
row_info = {'Right Ascension (degrees)': row['ra'],
'Declination (degrees)': row['dec'],
'Object ID': row['objid'].astype(str)}
self.table.add_item(row_info)

elif self.catalog_selected == 'Gaia':
from astroquery.gaia import Gaia, conf

Expand All @@ -180,12 +183,6 @@ def search(self, error_on_fail=False):
columns=('source_id', 'ra', 'dec'))
self.app._catalog_source_table = sources
skycoord_table = SkyCoord(sources['ra'], sources['dec'], unit='deg')
# adding in coords + Id's into table
for row in sources:
row_info = {'Right Ascension (degrees)': row['ra'],
'Declination (degrees)': row['dec'],
'Source ID': row['SOURCE_ID']}
self.table.add_item(row_info)

elif self.catalog_selected == 'From File...':
# all exceptions when going through the UI should have prevented setting this path
Expand All @@ -194,14 +191,6 @@ def search(self, error_on_fail=False):
self.app._catalog_source_table = table
skycoord_table = table['sky_centroid']

# NOTE: If performance becomes a problem, see
# https://docs.astropy.org/en/stable/table/index.html#performance-tips
for row in self.app._catalog_source_table:
row_info = {'Right Ascension (degrees)': row['sky_centroid'].ra.deg,
'Declination (degrees)': row['sky_centroid'].dec.deg,
'Object ID': str(row.get('label', 'N/A'))}
self.table.add_item(row_info)

else:
self.results_available = False
self.number_of_results = 0
Expand All @@ -228,6 +217,38 @@ def search(self, error_on_fail=False):
filtered_pair_pixel_table = np.array(np.hsplit(filtered_table, 2))
x_coordinates = np.squeeze(filtered_pair_pixel_table[0])
y_coordinates = np.squeeze(filtered_pair_pixel_table[1])

if self.catalog_selected in ["SDSS", "Gaia"]:
for row, x_coord, y_coord in zip(self.app._catalog_source_table,
x_coordinates, y_coordinates):
if self.catalog_selected == "SDSS":
row_id = row["objid"]
elif self.catalog_selected == "Gaia":
row_id = row["SOURCE_ID"]
# Check if the row contains the required keys
row_info = {'Right Ascension (degrees)': row['ra'],
'Declination (degrees)': row['dec'],
'Object ID': row_id.astype(str),
'id': len(self.table),
'x_coord': x_coord,
'y_coord': y_coord}
self.table.add_item(row_info)

# NOTE: If performance becomes a problem, see
# https://docs.astropy.org/en/stable/table/index.html#performance-tips
if self.catalog_selected == 'From File...':
for row, x_coord, y_coord in zip(self.app._catalog_source_table,
x_coordinates, y_coordinates):
# Check if the row contains the required keys
row_info = {'Right Ascension (degrees)': row['sky_centroid'].ra.deg,
'Declination (degrees)': row['sky_centroid'].dec.deg,
'Object ID': str(row.get('label', 'N/A')),
'id': len(self.table),
'x_coord': x_coord,
'y_coord': y_coord}

self.table.add_item(row_info)

filtered_skycoord_table = viewer.state.reference_data.coords.pixel_to_world(x_coordinates,
y_coordinates)

Expand All @@ -236,11 +257,64 @@ def search(self, error_on_fail=False):

self.number_of_results = len(catalog_results)
# markers are added to the viewer based on the table
viewer.marker = {'color': 'red', 'alpha': 0.8, 'markersize': 5, 'fill': False}
viewer.marker = {'color': 'blue', 'alpha': 0.8, 'markersize': 30, 'fill': False}
viewer.add_markers(table=catalog_results, use_skycoord=True, marker_name=self._marker_name)

return skycoord_table

def _get_mark(self, viewer):
matches = [mark for mark in viewer.figure.marks if isinstance(mark, CatalogMark)]
if len(matches):
return matches[0]
mark = CatalogMark(viewer)
viewer.figure.marks = viewer.figure.marks + [mark]
return mark

@property
def marks(self):
return {viewer_id: self._get_mark(viewer)
for viewer_id, viewer in self.app._viewer_store.items()
if hasattr(viewer, 'figure')}

@observe('is_active')
def _on_is_active_changed(self, *args):
if self.disabled_msg:
return

for mark in self.marks.values():
mark.visible = self.is_active

def plot_selected_points(self):
selected_rows = self.table.selected_rows

x = [float(coord['x_coord']) for coord in selected_rows]
y = [float(coord['y_coord']) for coord in selected_rows]
self._get_mark(self.viewer.selected_obj).update_xy(getattr(x, 'value', x),
getattr(y, 'value', y))

def vue_zoom_in(self, *args, **kwargs):
"""This function will zoom into the image based on the selected points"""
selected_rows = self.table.selected_rows

x = [float(coord['x_coord']) for coord in selected_rows]
y = [float(coord['y_coord']) for coord in selected_rows]

# this works with single selected points
# zooming when the range is too large is not performing correctly
x_min = min(x) - 50
x_max = max(x) + 50
y_min = min(y) - 50
y_max = max(y) + 50

imview = self.app._jdaviz_helper._default_viewer

with delay_callback(imview.state, 'x_min', 'x_max', 'y_min', 'y_max'):
imview.state.x_min = x_min
imview.state.x_max = x_max
imview.state.y_min = y_min
imview.state.y_max = y_max

return (x_min, x_max), (y_min, y_max)

def import_catalog(self, catalog):
"""
Import a catalog from a file path.
Expand Down
14 changes: 12 additions & 2 deletions jdaviz/configs/imviz/plugins/catalogs/catalogs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
<j-tray-plugin
:description="docs_description || 'Queries an area encompassed by the viewer using a specified catalog and marks all the objects found within the area.'"
:link="docs_link || 'https://jdaviz.readthedocs.io/en/'+vdocs+'/'+config+'/plugins.html#catalog-search'"
:uses_active_status="uses_active_status"
@plugin-ping="plugin_ping($event)"
:keep_active_sync="keep_active"
:popout_button="popout_button"
:scroll_to.sync="scroll_to">

Expand Down Expand Up @@ -64,15 +67,22 @@
Search
</plugin-action-button>
</v-col>
<v-col>
<plugin-action-button
:results_isolated_to_plugin="true"
@click="zoom_in"
>
Zoom to Selected
</plugin-action-button>
</v-col>
</v-row>

<v-row>
<p class="font-weight-bold">Results:</p>
<span style='padding-left: 4px' v-if="results_available">{{number_of_results}}</span>
</v-row>

<jupyter-widget :widget="table_widget"></jupyter-widget>

<jupyter-widget :widget="table_widget"></jupyter-widget>

</j-tray-plugin>
</template>
31 changes: 31 additions & 0 deletions jdaviz/configs/imviz/tests/test_catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ 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]
assert len(catalogs_plugin.table.selected_rows) == 2

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 2047.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 1488.5

catalogs_plugin.vue_zoom_in()

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == 858.24969
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 958.38461
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == 278.86265
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 378.8691


def test_from_file_parsing(imviz_helper, tmp_path):
catalogs_plugin = imviz_helper.plugins["Catalog Search"]._obj
Expand Down Expand Up @@ -174,3 +189,19 @@ def test_offline_ecsv_catalog(imviz_helper, image_2d_wcs, tmp_path):
catalogs_plugin.clear(hide_only=False)
assert not catalogs_plugin.results_available
assert len(imviz_helper.app.data_collection) == 1 # markers gone for good

catalogs_plugin.table.selected_rows = [catalogs_plugin.table.items[0]]

assert len(catalogs_plugin.table.selected_rows) == 1

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 9.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -0.5
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 9.5

catalogs_plugin.vue_zoom_in()

assert imviz_helper.viewers['imviz-0']._obj.state.x_min == -49.99966
assert imviz_helper.viewers['imviz-0']._obj.state.x_max == 50.00034
assert imviz_helper.viewers['imviz-0']._obj.state.y_min == -48.99999
assert imviz_helper.viewers['imviz-0']._obj.state.y_max == 51.00001
10 changes: 8 additions & 2 deletions jdaviz/core/marks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
'PluginMark', 'LinesAutoUnit', 'PluginLine', 'PluginScatter',
'LineAnalysisContinuum', 'LineAnalysisContinuumCenter',
'LineAnalysisContinuumLeft', 'LineAnalysisContinuumRight',
'LineUncertainties', 'ScatterMask', 'SelectedSpaxel', 'MarkersMark', 'FootprintOverlay',
'ApertureMark']
'LineUncertainties', 'ScatterMask', 'SelectedSpaxel', 'MarkersMark',
'CatalogMark', 'FootprintOverlay', 'ApertureMark']

accent_color = "#c75d2c"

Expand Down Expand Up @@ -597,6 +597,12 @@ def __init__(self, viewer, **kwargs):
super().__init__(viewer, **kwargs)


class CatalogMark(PluginScatter):
def __init__(self, viewer, **kwargs):
kwargs.setdefault('marker', 'circle')
super().__init__(viewer, **kwargs)


class FootprintOverlay(PluginLine):
def __init__(self, viewer, overlay, **kwargs):
self._overlay = overlay
Expand Down
9 changes: 8 additions & 1 deletion jdaviz/core/template_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4501,9 +4501,11 @@ class Table(PluginSubcomponent):
item_key = Unicode().tag(sync=True) # Unique field to identify row for selection
selected_rows = List().tag(sync=True) # List of selected rows

def __init__(self, plugin, name='table', *args, **kwargs):
def __init__(self, plugin, name='table', selected_rows_changed_callback=None,
*args, **kwargs):
self._qtable = None
self._table_name = name
self._selected_rows_changed_callback = selected_rows_changed_callback
super().__init__(plugin, 'Table', *args, **kwargs)

plugin.session.hub.broadcast(PluginTableAddedMessage(sender=self))
Expand All @@ -4527,6 +4529,11 @@ def default_value_for_column(self, colname=None, value=None):
def _new_col_visible(colname):
return True

@observe('selected_rows')
def _selected_rows_changed(self, msg):
if self._selected_rows_changed_callback is not None:
self._selected_rows_changed_callback(msg)

def add_item(self, item):
"""
Add an item/row to the table.
Expand Down

0 comments on commit 6b83d77

Please sign in to comment.