From f6dc701bd7392893ae55de7b69807651d9e38e8d Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 13 Mar 2024 15:56:43 -0400 Subject: [PATCH 01/12] "register" plugin table instances and expose as options in export plugin --- jdaviz/app.py | 20 ++- .../configs/default/plugins/export/export.py | 23 +--- .../configs/default/plugins/export/export.vue | 4 +- jdaviz/core/events.py | 17 ++- jdaviz/core/helpers.py | 4 + jdaviz/core/template_mixin.py | 123 +++++++++++++++++- jdaviz/core/user_api.py | 11 +- 7 files changed, 173 insertions(+), 29 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 2a462fc546..63caee5b6b 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -301,6 +301,11 @@ def __init__(self, configuration=None, *args, **kwargs): if cur_cm not in colormaps.members: colormaps.add(*cur_cm) + from jdaviz.core.events import PluginTableAddedMessage + self._plugin_tables = {} + self.hub.subscribe(self, PluginTableAddedMessage, + handler=self._on_plugin_table_added) + # Parse the yaml configuration file used to compose the front-end UI self.load_configuration(configuration) @@ -366,6 +371,13 @@ def __init__(self, configuration=None, *args, **kwargs): self.hub.subscribe(self, SubsetDeleteMessage, handler=self._on_layers_changed) + def _on_plugin_table_added(self, msg): + if msg.plugin._plugin_name is None: + # plugin was instantiated after the app was created, ignore + return + key = f"{msg.plugin._plugin_name}:{msg.table._table_name}" + self._plugin_tables.setdefault(key, msg.table.user_api) + @property def hub(self): """ @@ -2497,13 +2509,13 @@ def compose_viewer_area(viewer_area_items): optional_tray_kwargs[opt_kwarg] = opt_value - tray_item_instance = tray.get('cls')( - app=self, **optional_tray_kwargs - ) # store a copy of the tray name in the instance so it can be accessed by the # plugin itself tray_item_label = tray.get('label') - tray_item_instance._plugin_name = tray_item_label + + tray_item_instance = tray.get('cls')( + app=self, plugin_name=tray_item_label, **optional_tray_kwargs + ) self.state.tray_items.append({ 'name': name, diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index b08913c1b6..a0f0960585 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -7,7 +7,8 @@ from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import (PluginTemplateMixin, SelectPluginComponent, ViewerSelectMixin, DatasetMultiSelectMixin, - SubsetSelectMixin, MultiselectMixin, with_spinner) + SubsetSelectMixin, PluginTableSelectMixin, + MultiselectMixin, with_spinner) from jdaviz.core.events import AddDataMessage, SnackbarMessage from jdaviz.core.user_api import PluginUserApi @@ -25,7 +26,7 @@ @tray_registry('export', label="Export") class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin, - DatasetMultiSelectMixin, MultiselectMixin): + DatasetMultiSelectMixin, PluginTableSelectMixin, MultiselectMixin): """ See the :ref:`Export Plugin Documentation ` for more details. @@ -37,6 +38,7 @@ class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin, * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray` * ``viewer`` (:class:`~jdaviz.core.template_mixin.ViewerSelect`) * ``viewer_format`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`) + * ``table`` (:class:`~jdaviz.core.template_mixin.PluginTableSelect`) * ``filename`` * :meth:`export` """ @@ -45,13 +47,9 @@ class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin, # feature flag for cone support dev_dataset_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring dev_subset_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring - dev_table_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring dev_plot_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring dev_multi_support = Bool(False).tag(sync=True) # when enabling: add entries to docstring - table_items = List().tag(sync=True) - table_selected = Any().tag(sync=True) - plot_items = List().tag(sync=True) plot_selected = Any().tag(sync=True) @@ -71,13 +69,6 @@ class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin, def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.table = SelectPluginComponent(self, - items='table_items', - selected='table_selected', - multiselect='multiselect', - default_mode='empty', - manual_options=['table-tst1', 'table-tst2']) - self.plot = SelectPluginComponent(self, items='plot_items', selected='plot_selected', @@ -100,6 +91,8 @@ def __init__(self, *args, **kwargs): # default selection: self.dataset._default_mode = 'empty' + self.table._default_mode = 'empty' + self.table.select_default() self.viewer.select_default() self.filename = f"{self.app.config}_export" @@ -111,14 +104,12 @@ def user_api(self): # TODO: backwards compat for save_figure, save_movie, # i_start, i_end, movie_fps, movie_filename # TODO: expose export method once API is finalized - expose = ['viewer', 'viewer_format', 'filename', 'export'] + expose = ['viewer', 'viewer_format', 'table', 'filename', 'export'] if self.dev_dataset_support: expose += ['dataset'] if self.dev_subset_support: expose += ['subset'] - if self.dev_table_support: - expose += ['table'] if self.dev_plot_support: expose += ['plot'] if self.dev_multi_support: diff --git a/jdaviz/configs/default/plugins/export/export.vue b/jdaviz/configs/default/plugins/export/export.vue index 749e5582f9..f5b8fab88c 100644 --- a/jdaviz/configs/default/plugins/export/export.vue +++ b/jdaviz/configs/default/plugins/export/export.vue @@ -100,7 +100,7 @@ -
+
Plugin Tables
+ Export To + + """ + + def __init__(self, plugin, items, selected, + multiselect=None, + filters=[], + default_text=None, manual_options=[], + default_mode='first'): + """ + Parameters + ---------- + plugin + the parent plugin object + items : str + the name of the items traitlet defined in ``plugin`` + selected : str + the name of the selected traitlet defined in ``plugin`` + multiselect : str + the name of the traitlet defining whether the dropdown should accept multiple selections + filters : list + list of strings (for built-in filters) or callables to filter to only valid options. + default_text : str or None + the text to show for no selection. If not provided or None, no entry will be provided + in the dropdown for no selection. + manual_options: list + list of options to provide that are not automatically populated by datasets. If + ``default`` text is provided but not in ``manual_options`` it will still be included as + the first item in the list. + """ + super().__init__(plugin, items=items, selected=selected, + multiselect=multiselect, filters=filters, + default_text=default_text, manual_options=manual_options, + default_mode=default_mode) + from jdaviz.core.events import PluginTableAddedMessage + self.hub.subscribe(self, PluginTableAddedMessage, handler=self._on_tables_changed) + self._on_tables_changed() + + @observe('filters') + def _on_tables_changed(self, *args): + manual_items = [{'label': label} for label in self.manual_options] + self.items = manual_items + [{'label': k} for k in self.plugin.app._plugin_tables.keys()] + self._apply_default_selection() + # future improvement: only clear cache if the selected data entry was changed? + self._clear_cache(*self._cached_properties) + + @cached_property + def selected_obj(self): + return self.plugin.app._jdaviz_helper.plugin_tables.get(self.selected) + + +class PluginTableSelectMixin(VuetifyTemplate, HubListener): + table_items = List().tag(sync=True) + table_selected = Any().tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.table = PluginTableSelect(self, + 'table_items', + 'table_selected', + multiselect='multiselect' if hasattr(self, 'multiselect') else None) # noqa + + class DatasetSpectralSubsetValidMixin(VuetifyTemplate, HubListener): """ Adds a traitlet tracking whether self.dataset and self.spectral_subset @@ -2844,9 +2952,7 @@ class DatasetSelect(SelectPluginComponent): * :meth:`~SelectPluginComponent.select_default` * :meth:`~SelectPluginComponent.select_all` (only if ``is_multiselect``) * :meth:`~SelectPluginComponent.select_none` (only if ``is_multiselect``) - """ - """ Traitlets (in the object, custom traitlets in the plugin): * ``items`` (list of dicts with keys: label) @@ -3977,10 +4083,17 @@ class Table(PluginSubcomponent): headers_avail = List([]).tag(sync=True) # list of strings items = List().tag(sync=True) # list of dictionaries, pass single dict to add_row - def __init__(self, plugin, *args, **kwargs): + def __init__(self, plugin, name='table', *args, **kwargs): self._qtable = None + self._table_name = name super().__init__(plugin, 'Table', *args, **kwargs) + plugin.session.hub.broadcast(PluginTableAddedMessage(sender=self)) + + @property + def user_api(self): + return UserApiWrapper(self, ('clear_table', 'export_table')) + def default_value_for_column(self, colname=None, value=None): if colname in self._default_values_by_colname: return self._default_values_by_colname.get(colname) @@ -4109,7 +4222,7 @@ class TableMixin(VuetifyTemplate, HubListener): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.table = Table(self) + self.table = Table(self, name='table') self.table_widget = 'IPY_MODEL_'+self.table.model_id def clear_table(self): diff --git a/jdaviz/core/user_api.py b/jdaviz/core/user_api.py index 7f70045e36..058fdb34a4 100644 --- a/jdaviz/core/user_api.py +++ b/jdaviz/core/user_api.py @@ -3,7 +3,7 @@ __all__ = ['UserApiWrapper', 'PluginUserApi', 'ViewerUserApi'] -_internal_attrs = ('_obj', '_expose', '_readonly', '__doc__', '_deprecation_msg') +_internal_attrs = ('_obj', '_expose', '_items', '_readonly', '__doc__', '_deprecation_msg') class UserApiWrapper: @@ -23,7 +23,7 @@ def __dir__(self): return self._expose def __repr__(self): - return self._obj.__repr__() + return f'<{self._obj.__class__.__name__} API>' def __eq__(self, other): return self._obj.__eq__(other) @@ -82,6 +82,13 @@ def __setattr__(self, attr, value): return setattr(self._obj, attr, value) + def _items(self): + for attr in self._expose: + try: + yield attr, self.__getattr__(attr) + except AttributeError: + continue + class PluginUserApi(UserApiWrapper): """ From bc4ff1d5bc043c1a37f42414e8e39099da65b9a4 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 13 Mar 2024 16:14:02 -0400 Subject: [PATCH 02/12] implement support for exporting tables --- .../configs/default/plugins/export/export.py | 36 ++++++++++--------- jdaviz/core/template_mixin.py | 20 +++++++++-- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index a0f0960585..d9fde7c269 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -165,32 +165,34 @@ def export(self, filename=None, show_dialog=None): raise NotImplementedError("dataset export not yet supported") if self.subset.selected is not None and len(self.subset.selected): raise NotImplementedError("subset export not yet supported") - if self.table.selected is not None and len(self.table.selected): - raise NotImplementedError("table export not yet supported") if self.plot.selected is not None and len(self.plot.selected): raise NotImplementedError("plot export not yet supported") if self.multiselect: raise NotImplementedError("batch export not yet supported") - if not len(self.viewer.selected): - raise ValueError("no viewers selected to export") - - viewer = self.viewer.selected_obj filename = filename if filename is not None else self.filename - filetype = self.viewer_format.selected - # at this point, we can assume only a single figure is selected - if len(filename): - if not filename.endswith(filetype): - filename += f".{filetype}" - filename = Path(filename).expanduser() - else: - filename = None + # at this point, we can assume only a single export is selected + if len(self.viewer.selected): + viewer = self.viewer.selected_obj + filetype = self.viewer_format.selected + if len(filename): + if not filename.endswith(filetype): + filename += f".{filetype}" + filename = Path(filename).expanduser() + else: + filename = None - if filetype == "mp4": - self.save_movie(viewer, filename, filetype) + if filetype == "mp4": + self.save_movie(viewer, filename, filetype) + else: + self.save_figure(viewer, filename, filetype, show_dialog=show_dialog) + elif len(self.table.selected): + if not filename.endswith("csv"): + filename += ".csv" + self.table.selected_obj.export_table(filename) else: - self.save_figure(viewer, filename, filetype, show_dialog=show_dialog) + raise ValueError("nothing selected for export") def vue_export_from_ui(self, *args, **kwargs): try: diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index ec3a04ef37..7efc555779 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -4196,10 +4196,18 @@ def vue_clear_table(self, data=None): # call that, otherwise call the one defined here getattr(self._plugin, 'clear_table', self.clear_table)() - def export_table(self): + def export_table(self, filename=None): """ Export the QTable representation of the table. + + Parameters + ---------- + filename : str, optional + If provided, will write to the file, otherwise will just return the QTable + object. """ + if filename is not None: + self._qtable.write(filename) # TODO: default to only showing selected columns? return self._qtable @@ -4236,11 +4244,17 @@ def vue_clear_table(self, data=None): # (to also clear markers, etc) self.clear_table() - def export_table(self): + def export_table(self, filename=None): """ Export the QTable representation of the table. + + Parameters + ---------- + filename : str, optional + If provided, will write to the file, otherwise will just return the QTable + object. """ - return self.table.export_table() + return self.table.export_table(filename=filename) class Plot(PluginSubcomponent): From 6ce25979e6ae3b9fa48c9c5ca10f1da96716a0e8 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 13 Mar 2024 16:18:27 -0400 Subject: [PATCH 03/12] changelog entry --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c8bf57f547..dbef89a248 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,8 @@ New Features - "Export Plot" plugin is now replaced with the more general "Export" plugin. [#2722] +- "Export" plugin supports exporting plugin tables. [#2755] + Cubeviz ^^^^^^^ From 213294c4e349fae53454ad70f1aab0306fd86808 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 12:27:12 -0400 Subject: [PATCH 04/12] update tests --- .../default/plugins/plot_options/tests/test_plot_options.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py b/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py index f6ed010c12..9ded8b3a33 100644 --- a/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py @@ -144,7 +144,7 @@ def test_user_api(cubeviz_helper, spectrum1d_cube): po = cubeviz_helper.plugins['Plot Options'] assert po.multiselect is False - assert "multiselect" in po.viewer.__repr__() + assert "multiselect" in po.viewer._obj.__repr__() # regression test for https://github.com/spacetelescope/jdaviz/pull/1708 # user calls to select_default should revert even if current entry is valid @@ -177,9 +177,9 @@ def test_user_api(cubeviz_helper, spectrum1d_cube): # check a plot option with and without choices assert hasattr(po.stretch_preset, 'choices') assert len(po.stretch_preset.choices) > 1 - assert "choices" in po.stretch_preset.__repr__() + assert "choices" in po.stretch_preset._obj.__repr__() assert not hasattr(po.image_contrast, 'choices') - assert "choices" not in po.image_contrast.__repr__() + assert "choices" not in po.image_contrast._obj.__repr__() # try setting with both label and value po.stretch_preset = 90 From 7546679b6078a133f9979409e570add792760890 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 12:34:55 -0400 Subject: [PATCH 05/12] snackbar on successful export --- jdaviz/configs/default/plugins/export/export.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index d9fde7c269..0b753f781f 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -188,18 +188,23 @@ def export(self, filename=None, show_dialog=None): else: self.save_figure(viewer, filename, filetype, show_dialog=show_dialog) elif len(self.table.selected): - if not filename.endswith("csv"): - filename += ".csv" - self.table.selected_obj.export_table(filename) + if "." not in filename: + filename += ".ecsv" + self.table.selected_obj.export_table(filename, overwrite=True) else: raise ValueError("nothing selected for export") + return filename def vue_export_from_ui(self, *args, **kwargs): try: - self.export(show_dialog=True) + filename = self.export(show_dialog=True) except Exception as e: self.hub.broadcast(SnackbarMessage( f"Export failed with: {e}", sender=self, color="error")) + else: + if filename is not None: + self.hub.broadcast(SnackbarMessage( + f"Exported to {filename}", sender=self, color="success")) def save_figure(self, viewer, filename=None, filetype="png", show_dialog=False): if filetype == "png": From 022059fbec1e675db03f278e5cfb11b3b52d0141 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 12:37:20 -0400 Subject: [PATCH 06/12] code cleanup --- jdaviz/core/template_mixin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 7efc555779..82529599c6 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -71,6 +71,7 @@ 'DatasetSpectralSubsetValidMixin', 'SpectralContinuumMixin', 'ViewerSelect', 'ViewerSelectMixin', 'LayerSelect', 'LayerSelectMixin', + 'PluginTableSelect', 'PluginTableSelectMixin', 'NonFiniteUncertaintyMismatchMixin', 'DatasetSelect', 'DatasetSelectMixin', 'DatasetMultiSelectMixin', 'FileImportSelectPluginComponent', 'HasFileImportSelect', @@ -2406,7 +2407,6 @@ def __init__(self, plugin, items, selected, multiselect=multiselect, filters=filters, default_text=default_text, manual_options=manual_options, default_mode=default_mode) - from jdaviz.core.events import PluginTableAddedMessage self.hub.subscribe(self, PluginTableAddedMessage, handler=self._on_tables_changed) self._on_tables_changed() From d5d12a433128fdafd5c574fc208b62c947cbd91d Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 12:42:38 -0400 Subject: [PATCH 07/12] update docs --- docs/imviz/plugins.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/imviz/plugins.rst b/docs/imviz/plugins.rst index f354674c18..43dea59964 100644 --- a/docs/imviz/plugins.rst +++ b/docs/imviz/plugins.rst @@ -405,4 +405,7 @@ Due to browser limitations, Canvas Rotation is only available on Chromium-based Export ====== -This plugin allows exporting the plot in a given viewer to a PNG or SVG file. +This plugin allows exporting: + +* the plot in a given viewer to a PNG or SVG file, +* a table in a plugin to ecsv From dabcf5fa2ed553f7fc7f08b17ab9a0764b721bc0 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 12:54:18 -0400 Subject: [PATCH 08/12] overwrite support and basic test coverage --- .../plugins/markers/tests/test_markers_plugin.py | 6 ++++++ .../plugins/plot_options/tests/test_plot_options.py | 2 +- jdaviz/core/template_mixin.py | 12 ++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py b/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py index 4021ab7965..d943cd3c33 100644 --- a/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py +++ b/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py @@ -124,6 +124,12 @@ def test_markers_cubeviz(cubeviz_helper, spectrum1d_cube): assert len(_get_markers_from_viewer(fv).x) == 1 assert len(_get_markers_from_viewer(sv).x) == 2 + # appears as option in export plugin and exports successfully + exp = cubeviz_helper.plugins['Export'] + assert "Markers:table" in exp.table.choices + exp.table = "Markers:table" + exp.export() + # clearing table clears markers mp.clear_table() assert mp.export_table() is None diff --git a/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py b/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py index 9ded8b3a33..5ee021de6f 100644 --- a/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/tests/test_plot_options.py @@ -144,7 +144,7 @@ def test_user_api(cubeviz_helper, spectrum1d_cube): po = cubeviz_helper.plugins['Plot Options'] assert po.multiselect is False - assert "multiselect" in po.viewer._obj.__repr__() + assert "multiselect" in po.viewer.__repr__() # regression test for https://github.com/spacetelescope/jdaviz/pull/1708 # user calls to select_default should revert even if current entry is valid diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 82529599c6..d1a81fcc8c 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -4196,7 +4196,7 @@ def vue_clear_table(self, data=None): # call that, otherwise call the one defined here getattr(self._plugin, 'clear_table', self.clear_table)() - def export_table(self, filename=None): + def export_table(self, filename=None, overwrite=False): """ Export the QTable representation of the table. @@ -4205,9 +4205,11 @@ def export_table(self, filename=None): filename : str, optional If provided, will write to the file, otherwise will just return the QTable object. + overwrite : bool, optional + If ``filename`` already exists, should it be overwritten. """ if filename is not None: - self._qtable.write(filename) + self._qtable.write(filename, overwrite=overwrite) # TODO: default to only showing selected columns? return self._qtable @@ -4244,7 +4246,7 @@ def vue_clear_table(self, data=None): # (to also clear markers, etc) self.clear_table() - def export_table(self, filename=None): + def export_table(self, filename=None, overwrite=False): """ Export the QTable representation of the table. @@ -4253,8 +4255,10 @@ def export_table(self, filename=None): filename : str, optional If provided, will write to the file, otherwise will just return the QTable object. + overwrite : bool, optional + If ``filename`` already exists, should it be overwritten. """ - return self.table.export_table(filename=filename) + return self.table.export_table(filename=filename, overwrite=overwrite) class Plot(PluginSubcomponent): From 7d846e45440e35cc6b1eb3ecad10c711585be230 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 13:13:11 -0400 Subject: [PATCH 09/12] minor fixes --- .../spectral_extraction/tests/test_spectral_extraction.py | 2 +- jdaviz/core/template_mixin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py index c641b4a055..694319bc4d 100644 --- a/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py +++ b/jdaviz/configs/specviz2d/plugins/spectral_extraction/tests/test_spectral_extraction.py @@ -164,7 +164,7 @@ def test_user_api(specviz2d_helper): pext.bg_sub_add_results = 'override label' assert pext.bg_sub_add_results.label == 'override label' pext.bg_sub_add_results.label = 'override label 2' - assert "override label 2" in pext.bg_sub_add_results.__repr__() + assert "override label 2" in pext.bg_sub_add_results._obj.__repr__() assert "override label 2" in pext.bg_sub_add_results._obj.auto_label.__repr__() pext.export_bg_sub(add_data=True) diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index d1a81fcc8c..07a0b29acc 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -2369,7 +2369,7 @@ class PluginTableSelect(SelectPluginComponent): Example template (label and hint are optional):: - Date: Tue, 19 Mar 2024 14:25:08 -0400 Subject: [PATCH 10/12] API docstring entry for default_mode --- jdaviz/core/template_mixin.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 07a0b29acc..00e4590822 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -1335,10 +1335,13 @@ def __init__(self, plugin, items, selected, viewer, default_text : str or None the text to show for no selection. If not provided or None, no entry will be provided in the dropdown for no selection. - manual_options: list + manual_options : list list of options to provide that are not automatically populated by subsets. If ``default`` text is provided but not in ``manual_options`` it will still be included as the first item in the list. + default_mode : str, optional + What mode to use when making the default selection. Valid options: first, default_text, + empty. """ super().__init__(plugin, items=items, @@ -1700,6 +1703,9 @@ def __init__(self, plugin, items, selected, multiselect=None, selected_has_subre the first item in the list. filters : list list of strings (for built-in filters) or callables to filter to only valid options. + default_mode : str, optional + What mode to use when making the default selection. Valid options: first, default_text, + empty. """ super().__init__(plugin, items=items, @@ -2402,6 +2408,9 @@ def __init__(self, plugin, items, selected, list of options to provide that are not automatically populated by datasets. If ``default`` text is provided but not in ``manual_options`` it will still be included as the first item in the list. + default_mode : str, optional + What mode to use when making the default selection. Valid options: first, default_text, + empty. """ super().__init__(plugin, items=items, selected=selected, multiselect=multiselect, filters=filters, From 7886ba36d745acc1db797f317c260a7f285c98fe Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 19 Mar 2024 14:37:01 -0400 Subject: [PATCH 11/12] filter out empty tables --- .../plugins/markers/tests/test_markers_plugin.py | 5 ++++- jdaviz/core/events.py | 16 +++++++++++++++- jdaviz/core/template_mixin.py | 16 +++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py b/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py index d943cd3c33..19b51623e0 100644 --- a/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py +++ b/jdaviz/configs/default/plugins/markers/tests/test_markers_plugin.py @@ -29,6 +29,10 @@ def test_markers_cubeviz(cubeviz_helper, spectrum1d_cube): mp = cubeviz_helper.plugins['Markers'] mp.keep_active = True + exp = cubeviz_helper.plugins['Export'] + + # no marks yet, so table does not yet appear in export plugin + assert "Markers:table" not in exp.table.choices # test event in flux viewer label_mouseover._viewer_mouse_event(fv, @@ -125,7 +129,6 @@ def test_markers_cubeviz(cubeviz_helper, spectrum1d_cube): assert len(_get_markers_from_viewer(sv).x) == 2 # appears as option in export plugin and exports successfully - exp = cubeviz_helper.plugins['Export'] assert "Markers:table" in exp.table.choices exp.table = "Markers:table" exp.export() diff --git a/jdaviz/core/events.py b/jdaviz/core/events.py index 7117128130..c52915c00e 100644 --- a/jdaviz/core/events.py +++ b/jdaviz/core/events.py @@ -10,7 +10,7 @@ 'AstrowidgetMarkersChangedMessage', 'MarkersPluginUpdate', 'CanvasRotationChangedMessage', 'GlobalDisplayUnitChanged', 'ChangeRefDataMessage', - 'PluginTableAddedMessage'] + 'PluginTableAddedMessage', 'PluginTableModifiedMessage'] class NewViewerMessage(Message): @@ -434,3 +434,17 @@ def table(self): @property def plugin(self): return self.sender._plugin + + +class PluginTableModifiedMessage(Message): + '''Message generated when the items in a plugin table are changed''' + def __init__(self, sender): + super().__init__(sender) + + @property + def table(self): + return self.sender + + @property + def plugin(self): + return self.sender._plugin diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 00e4590822..c4196634f6 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -44,7 +44,7 @@ ViewerAddedMessage, ViewerRemovedMessage, ViewerRenamedMessage, SnackbarMessage, AddDataToViewerMessage, ChangeRefDataMessage, - PluginTableAddedMessage) + PluginTableAddedMessage, PluginTableModifiedMessage) from jdaviz.core.marks import (LineAnalysisContinuum, LineAnalysisContinuumCenter, @@ -2385,7 +2385,7 @@ class PluginTableSelect(SelectPluginComponent): def __init__(self, plugin, items, selected, multiselect=None, - filters=[], + filters=['not_empty_table'], default_text=None, manual_options=[], default_mode='first'): """ @@ -2417,12 +2417,14 @@ def __init__(self, plugin, items, selected, default_text=default_text, manual_options=manual_options, default_mode=default_mode) self.hub.subscribe(self, PluginTableAddedMessage, handler=self._on_tables_changed) + self.hub.subscribe(self, PluginTableModifiedMessage, handler=self._on_tables_changed) self._on_tables_changed() @observe('filters') def _on_tables_changed(self, *args): manual_items = [{'label': label} for label in self.manual_options] - self.items = manual_items + [{'label': k} for k in self.plugin.app._plugin_tables.keys()] + self.items = manual_items + [{'label': k} for k, v in self.plugin.app._plugin_tables.items() + if self._is_valid_item(v._obj)] self._apply_default_selection() # future improvement: only clear cache if the selected data entry was changed? self._clear_cache(*self._cached_properties) @@ -2431,6 +2433,12 @@ def _on_tables_changed(self, *args): def selected_obj(self): return self.plugin.app._jdaviz_helper.plugin_tables.get(self.selected) + def _is_valid_item(self, table): + def not_empty_table(table): + return len(table.items) > 0 + + return super()._is_valid_item(table, locals()) + class PluginTableSelectMixin(VuetifyTemplate, HubListener): table_items = List().tag(sync=True) @@ -4189,6 +4197,7 @@ def float_precision(column, item): # clean data to show in the UI self.items = self.items + [{k: json_safe(k, v) for k, v in item.items()}] + self._plugin.session.hub.broadcast(PluginTableAddedMessage(sender=self)) def __len__(self): return len(self.items) @@ -4199,6 +4208,7 @@ def clear_table(self): """ self.items = [] self._qtable = None + self._plugin.session.hub.broadcast(PluginTableModifiedMessage(sender=self)) def vue_clear_table(self, data=None): # if the plugin (or via the TableMixin) has its own clear_table implementation, From 05a63ae1828fe2ae1908c01425d12731644983c2 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 20 Mar 2024 12:48:12 -0400 Subject: [PATCH 12/12] add table_format choice selection --- .../configs/default/plugins/export/export.py | 20 ++++++++++++++++--- .../configs/default/plugins/export/export.vue | 12 +++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index 0b753f781f..2c3f876e4c 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -56,6 +56,9 @@ class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin, viewer_format_items = List().tag(sync=True) viewer_format_selected = Unicode().tag(sync=True) + table_format_items = List().tag(sync=True) + table_format_selected = Unicode().tag(sync=True) + filename = Unicode().tag(sync=True) # For Cubeviz movie. @@ -89,6 +92,14 @@ def __init__(self, *args, **kwargs): selected='viewer_format_selected', manual_options=viewer_format_options) + # NOTE: see self.table.selected_obj.write.list_formats() for full list of options, + # although not all support passing overwrite + table_format_options = ['ecsv', 'csv', 'fits'] + self.table_format = SelectPluginComponent(self, + items='table_format_items', + selected='table_format_selected', + manual_options=table_format_options) + # default selection: self.dataset._default_mode = 'empty' self.table._default_mode = 'empty' @@ -104,7 +115,9 @@ def user_api(self): # TODO: backwards compat for save_figure, save_movie, # i_start, i_end, movie_fps, movie_filename # TODO: expose export method once API is finalized - expose = ['viewer', 'viewer_format', 'table', 'filename', 'export'] + expose = ['viewer', 'viewer_format', + 'table', 'table_format', + 'filename', 'export'] if self.dev_dataset_support: expose += ['dataset'] @@ -188,8 +201,9 @@ def export(self, filename=None, show_dialog=None): else: self.save_figure(viewer, filename, filetype, show_dialog=show_dialog) elif len(self.table.selected): - if "." not in filename: - filename += ".ecsv" + filetype = self.table_format.selected + if not filename.endswith(filetype): + filename += f".{filetype}" self.table.selected_obj.export_table(filename, overwrite=True) else: raise ValueError("nothing selected for export") diff --git a/jdaviz/configs/default/plugins/export/export.vue b/jdaviz/configs/default/plugins/export/export.vue index f5b8fab88c..49397a1fe9 100644 --- a/jdaviz/configs/default/plugins/export/export.vue +++ b/jdaviz/configs/default/plugins/export/export.vue @@ -109,6 +109,18 @@ :single_select_allow_blank="true" > + + + +