From 4e8c1b6c07556fad95dd99e296a5b2df52e869a5 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 11 Jul 2024 09:39:33 -0400 Subject: [PATCH] support exporting a plot from an unopened/inactive plugin (#2934) * support exporting a plot from an unopened/inactive plugin * prevent clearing plot selection (under most conditions) --- CHANGES.rst | 2 ++ .../configs/default/plugins/export/export.py | 18 +++++++--- .../plugins/plot_options/plot_options.py | 14 ++++++-- jdaviz/core/template_mixin.py | 34 +++++++++++-------- 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a93e1a86ba..db31205612 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Bug Fixes --------- +- Fixes exporting the stretch histogram from Plot Options before the Plot Options plugin is ever opened. [#2934] + Cubeviz ^^^^^^^ diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index eeba93203e..f6dd48eb38 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -73,6 +73,9 @@ class Export(PluginTemplateMixin, ViewerSelectMixin, SubsetSelectMixin, dataset_format_items = List().tag(sync=True) dataset_format_selected = Unicode().tag(sync=True) + # copy of widget of the selected plugin_plot in case the parent plugin is not opened + plugin_plot_selected_widget = Unicode().tag(sync=True) + plugin_plot_format_items = List().tag(sync=True) plugin_plot_format_selected = Unicode().tag(sync=True) @@ -439,11 +442,16 @@ def export(self, filename=None, show_dialog=None, overwrite=False, else: filename = None - with plot._plugin.as_active(): - # NOTE: could still take some time for the plot itself to update, - # for now we'll hardcode a short amount of time for the plot to render any updates - time.sleep(0.2) - self.save_figure(plot, filename, filetype, show_dialog=show_dialog) + if not plot._plugin.is_active: + # force an update to the plot. This requires the plot to have set + # update_callback when instantiated + plot._update() + + # create a copy of the widget shown off screen to enable rendering + # in case one was never created in the parent plugin + self.plugin_plot_selected_widget = f'IPY_MODEL_{plot.model_id}' + + self.save_figure(plot, filename, filetype, show_dialog=show_dialog) elif len(self.plugin_table.selected): filetype = self.plugin_table_format.selected diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 4dff3fbf69..18ccc4ecaa 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -573,7 +573,8 @@ def state_attr_for_line_visible(state): 'stretch_params_value', 'stretch_params_sync', state_filter=is_image) - self.stretch_histogram = Plot(self, name='stretch_hist', viewer_type='histogram') + self.stretch_histogram = Plot(self, name='stretch_hist', viewer_type='histogram', + update_callback=self._update_stretch_histogram) # Add the stretch bounds tool to the default Plot viewer. self.stretch_histogram.tools_nested.append(["jdaviz:stretch_bounds"]) self.stretch_histogram._initialize_toolbar(["jdaviz:stretch_bounds"]) @@ -892,8 +893,7 @@ def _update_stretch_hist_sync(self, msg={}): @observe('is_active', 'layer_selected', 'viewer_selected', 'stretch_hist_zoom_limits') @skip_if_no_updates_since_last_active() - @with_spinner('stretch_hist_spinner') - def _update_stretch_histogram(self, msg={}): + def _request_update_stretch_histogram(self, msg={}): if not hasattr(self, 'viewer'): # pragma: no cover # plugin hasn't been fully initialized yet return @@ -909,6 +909,14 @@ def _update_stretch_histogram(self, msg={}): # its type msg = {} + # NOTE: this method is separate from _update_stretch_histogram so that + # _update_stretch_histogram can be called manually (or from the + # update_callback on the Plot object itself) without going through + # the skip_if_no_updates_since_last_active check + self._update_stretch_histogram(msg) + + @with_spinner('stretch_hist_spinner') + def _update_stretch_histogram(self, msg={}): if not self.stretch_function_sync.get('in_subscribed_states'): # pragma: no cover # no (image) viewer with stretch function options return diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 5fdb7aa274..86aefc3b54 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -919,6 +919,7 @@ def _apply_default_selection(self, skip_if_current_valid=True): self.selected = self._default_text if self._default_text else default_empty else: self.selected = default_empty + self._clear_cache(*self._cached_properties) def _is_valid_item(self, item, filter_callables={}): for valid_filter in self.filters: @@ -2528,9 +2529,7 @@ def _on_tables_changed(self, *args): manual_items = [{'label': label} for label in self.manual_options] 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) + self._apply_default_selection(skip_if_current_valid=True) @cached_property def selected_obj(self): @@ -2643,8 +2642,7 @@ def _on_plots_changed(self, *args): manual_items = [{'label': label} for label in self.manual_options] self.items = manual_items + [{'label': k} for k, v in self.plugin.app._plugin_plots.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._apply_default_selection(skip_if_current_valid=True) self._clear_cache(*self._cached_properties) @cached_property @@ -2652,7 +2650,6 @@ def selected_obj(self): return self.plugin.app._jdaviz_helper.plugin_plots.get(self.selected) def _is_valid_item(self, plot): - def not_empty_plot(plot): # checks plot.figure.marks to determine if figure is of an empty plot # not sure if this is a foolproof way to do this? @@ -4562,12 +4559,14 @@ class Plot(PluginSubcomponent): figure = Any().tag(sync=True, **widget_serialization) toolbar = Any().tag(sync=True, **widget_serialization) - def __init__(self, plugin, name='plot', viewer_type='scatter', app=None, *args, **kwargs): + def __init__(self, plugin, name='plot', viewer_type='scatter', update_callback=None, + app=None, *args, **kwargs): super().__init__(plugin, 'Plot', *args, **kwargs) if app is None: app = jglue() self._app = app + self._update_callback = update_callback self._plugin = plugin self._plot_name = name self.viewer = app.new_data_viewer(viewer_type, show=False) @@ -4592,7 +4591,6 @@ def __init__(self, plugin, name='plot', viewer_type='scatter', app=None, *args, self._initialize_toolbar() plugin.session.hub.broadcast(PluginPlotAddedMessage(sender=self)) - plugin.session.hub.broadcast(PluginPlotModifiedMessage(sender=self)) def _initialize_toolbar(self, default_tool_priority=[]): self.toolbar = NestedJupyterToolbar(self.viewer, self.tools_nested, default_tool_priority) @@ -4618,12 +4616,19 @@ def _check_valid_components(self, **kwargs): # https://github.com/astrofrog/fast-histogram/issues/60 raise ValueError("histogram requires data entries with length > 1") - def _remove_data(self, label): + def _remove_data(self, label, broadcast=True): dc_entry = self.app.data_collection[label] self.viewer.remove_data(dc_entry) self.app.data_collection.remove(dc_entry) - self._plugin.session.hub.broadcast(PluginPlotModifiedMessage(sender=self)) + if broadcast: + self._plugin.session.hub.broadcast(PluginPlotModifiedMessage(sender=self)) + + def _update(self): + # call the update callback, if it exists, on the parent plugin. + # This is useful for updating the plot when a plugin is inactive. + if self._update_callback is not None: + self._update_callback() def _update_data(self, label, reset_lims=False, **kwargs): self._check_valid_components(**kwargs) @@ -4651,8 +4656,8 @@ def _update_data(self, label, reset_lims=False, **kwargs): style_state = self.layers[label].state.as_dict() else: style_state = {} - self._remove_data(label) - self._add_data(label, **kwargs) + self._remove_data(label, broadcast=False) + self._add_data(label, broadcast=False, **kwargs) self.update_style(label, **style_state) if reset_lims: self.viewer.state.reset_limits() @@ -4688,7 +4693,7 @@ def update_style(self, label, **kwargs): self._plugin.session.hub.broadcast(PluginPlotModifiedMessage(sender=self)) - def _add_data(self, label, **kwargs): + def _add_data(self, label, broadcast=True, **kwargs): self._check_valid_components(**kwargs) data = Data(label=label, **kwargs) dc = self.app.data_collection @@ -4703,7 +4708,8 @@ def _add_data(self, label, **kwargs): dc.add_link(links) self.viewer.add_data(dc_entry) - self._plugin.session.hub.broadcast(PluginPlotModifiedMessage(sender=self)) + if broadcast: + self._plugin.session.hub.broadcast(PluginPlotModifiedMessage(sender=self)) def _refresh_marks(self): # ensure all marks are drawn