Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export plugin: table support #2755

Merged
merged 12 commits into from
Mar 20, 2024
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
^^^^^^^

Expand Down
5 changes: 4 additions & 1 deletion docs/imviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 16 additions & 4 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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,
Expand Down
78 changes: 45 additions & 33 deletions jdaviz/configs/default/plugins/export/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <imviz-export-plot>` for more details.

Expand All @@ -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`
"""
Expand All @@ -45,19 +47,18 @@ 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)

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.
Expand All @@ -71,13 +72,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',
Expand All @@ -98,8 +92,18 @@ 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'
self.table.select_default()
self.viewer.select_default()
self.filename = f"{self.app.config}_export"

Expand All @@ -111,14 +115,14 @@ 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', 'table_format',
'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:
Expand Down Expand Up @@ -174,39 +178,47 @@ 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):
# 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)
else:
self.save_figure(viewer, filename, filetype, show_dialog=show_dialog)
elif len(self.table.selected):
filetype = self.table_format.selected
if not filename.endswith(filetype):
filename += f".{filetype}"
filename = Path(filename).expanduser()
else:
filename = None

if filetype == "mp4":
self.save_movie(viewer, filename, filetype)
self.table.selected_obj.export_table(filename, overwrite=True)
else:
self.save_figure(viewer, filename, filetype, show_dialog=show_dialog)
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":
Expand Down
16 changes: 15 additions & 1 deletion jdaviz/configs/default/plugins/export/export.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
</plugin-inline-select>
</div>

<div v-if="dev_table_support && table_items.length > 0">
<div v-if="table_items.length > 0">
cshanahan1 marked this conversation as resolved.
Show resolved Hide resolved
<j-plugin-section-header style="margin-top: 12px">Plugin Tables</j-plugin-section-header>
<plugin-inline-select
:items="table_items"
Expand All @@ -109,6 +109,18 @@
:single_select_allow_blank="true"
>
</plugin-inline-select>
<v-row v-if="table_selected.length > 0" class="row-min-bottom-padding">
<v-select
:menu-props="{ left: true }"
attach
v-model="table_format_selected"
:items="table_format_items.map(i => i.label)"
label="Format"
hint="File format for exporting plugin tables."
persistent-hint
>
</v-select>
</v-row>
</div>

<div v-if="dev_plot_support && plot_items.length > 0">
Expand All @@ -122,6 +134,8 @@
</plugin-inline-select>
</div>

<j-plugin-section-header style="margin-top: 12px">Export To</j-plugin-section-header>

<v-row>
<v-text-field
v-model="filename"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -124,6 +128,11 @@ 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
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
31 changes: 30 additions & 1 deletion jdaviz/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'TableClickMessage', 'LinkUpdatedMessage', 'ExitBatchLoadMessage',
'AstrowidgetMarkersChangedMessage', 'MarkersPluginUpdate',
'CanvasRotationChangedMessage',
'GlobalDisplayUnitChanged', 'ChangeRefDataMessage']
'GlobalDisplayUnitChanged', 'ChangeRefDataMessage',
'PluginTableAddedMessage', 'PluginTableModifiedMessage']


class NewViewerMessage(Message):
Expand Down Expand Up @@ -419,3 +420,31 @@ def axis(self):
@property
def unit(self):
return u.Unit(self._unit)


class PluginTableAddedMessage(Message):
'''Message generated when a plugin table is initialized'''
def __init__(self, sender):
super().__init__(sender)

@property
def table(self):
return self.sender

@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
4 changes: 4 additions & 0 deletions jdaviz/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ def plugins(self):

return plugins

@property
def plugin_tables(self):
return self.app._plugin_tables

@property
def viewers(self):
"""
Expand Down
Loading