diff --git a/lcviz/helper.py b/lcviz/helper.py index 00b05049..9b2c6618 100644 --- a/lcviz/helper.py +++ b/lcviz/helper.py @@ -152,6 +152,31 @@ def get_data(self, data_label=None, cls=LightCurve, subset=None): """ return super()._get_data(data_label=data_label, mask_subset=subset, cls=cls) + @property + def _tray_tools(self): + """ + Access API objects for plugins in the app toolbar. + + Returns + ------- + plugins : dict + dict of plugin objects + """ + # TODO: provide user-friendly labels, user API, and move upstream to be public + # for now this is just useful for dev-debugging access to toolbar entries + from ipywidgets.widgets import widget_serialization + return {item['name']: widget_serialization['from_json'](item['widget'], None) + for item in self.app.state.tool_items} + + def _get_clone_viewer_reference(self, reference): + base_name = reference.split("[")[0] + name = base_name + ind = 0 + while name in self.viewers.keys(): + ind += 1 + name = f"{base_name}[{ind}]" + return name + def _phase_comp_lbl(self, component): return f'phase:{component}' diff --git a/lcviz/parsers.py b/lcviz/parsers.py index c7d20dea..97317dee 100644 --- a/lcviz/parsers.py +++ b/lcviz/parsers.py @@ -3,6 +3,8 @@ from jdaviz.core.registries import data_parser_registry import lightkurve +from lcviz.viewers import PhaseScatterView + __all__ = ["light_curve_parser"] @@ -45,10 +47,10 @@ def light_curve_parser(app, file_obj, data_label=None, show_in_viewer=True, **kw app.add_data_to_viewer(time_viewer_reference_name, new_data_label) # add to any known phase viewers - ephem_plugin = app._jdaviz_helper.plugins.get('Ephemeris', None) - if ephem_plugin is not None: - for viewer_id in ephem_plugin._obj.phase_viewer_ids: - app.add_data_to_viewer(viewer_id, new_data_label) + for viewer_id, viewer in app._viewer_store.items(): + if not isinstance(viewer, PhaseScatterView): + continue + app.add_data_to_viewer(viewer_id, new_data_label) def _data_with_reftime(app, light_curve): diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index 9a9cf2c8..494b5baf 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -143,20 +143,17 @@ def _phase_comp_lbl(self, component=None): def phase_comp_lbl(self): return self._phase_comp_lbl() - def _phase_viewer_id(self, component=None): + def _generate_phase_viewer_id(self, component=None): if component is None: component = self.component_selected - return f'flux-vs-phase:{component}' + return self.app._jdaviz_helper._get_clone_viewer_reference(f'flux-vs-phase:{component}') - @property - def phase_viewer_ids(self): - viewer_ids = self.app.get_viewer_ids() - return [self._phase_viewer_id(component) for component in self.component.choices - if self._phase_viewer_id(component) in viewer_ids] - - @property - def phase_viewer_id(self): - return self._phase_viewer_id() + def _get_phase_viewers(self, lbl=None): + if lbl is None: + lbl = self.component_selected + return [viewer for vid, viewer in self.app._viewer_store.items() + if isinstance(viewer, PhaseScatterView) + and viewer._ephemeris_component == lbl] @property def default_phase_viewer(self): @@ -166,13 +163,6 @@ def default_phase_viewer(self): # ephemeris component return self._get_phase_viewers()[0] - def _get_phase_viewers(self, lbl=None): - if lbl is None: - lbl = self.component_selected - return [viewer for vid, viewer in self.app._viewer_store.items() - if isinstance(viewer, PhaseScatterView) - and viewer.ephemeris_component == lbl] - @property def ephemerides(self): return self._ephemerides @@ -290,7 +280,8 @@ def create_phase_viewer(self, ephem_component=None): ephem_component : str, optional label of the component. If not provided or ``None``, will default to plugin value. """ - phase_viewer_id = self._phase_viewer_id(ephem_component) + if ephem_component is None: + ephem_component = self.component_selected phase_comp_lbl = self._phase_comp_lbl(ephem_component) dc = self.app.data_collection @@ -300,26 +291,37 @@ def create_phase_viewer(self, ephem_component=None): if phase_comp_lbl not in [comp.label for comp in dc[0].components]: self.update_ephemeris() # calls _update_all_phase_arrays - create_phase_viewer = len(self._get_phase_viewers(ephem_component)) == 0 - if create_phase_viewer: - # TODO: stack horizontally by default? - self.app._on_new_viewer(NewViewerMessage(PhaseScatterView, data=None, sender=self.app), - vid=phase_viewer_id, name=phase_viewer_id) - - time_viewer_item = self.app._get_viewer_item(self.app._jdaviz_helper._default_time_viewer_reference_name) # noqa - for data in dc: - data_id = self.app._data_id_from_label(data.label) - visible = time_viewer_item['selected_data_items'].get(data_id, 'hidden') - self.app.set_data_visibility(phase_viewer_id, data.label, visible == 'visible') + phase_viewer_id = self._generate_phase_viewer_id(ephem_component) + # TODO: stack horizontally by default? + self.app._on_new_viewer(NewViewerMessage(PhaseScatterView, data=None, sender=self.app), + vid=phase_viewer_id, name=phase_viewer_id) + # access new viewer, set bookkeeping for ephemeris component pv = self.app.get_viewer(phase_viewer_id) - if create_phase_viewer: - pv.state.x_min, pv.state.x_max = (self.wrap_at-1, self.wrap_at) - pv.state.x_att = self.app._jdaviz_helper._component_ids[phase_comp_lbl] + pv._ephemeris_component = ephem_component + # since we couldn't set ephemeris_component right away, _check_if_phase_viewer_exists + # might be out-of-date + self._check_if_phase_viewer_exists() + + # set default data visibility + time_viewer_item = self.app._get_viewer_item(self.app._jdaviz_helper._default_time_viewer_reference_name) # noqa + for data in dc: + data_id = self.app._data_id_from_label(data.label) + visible = time_viewer_item['selected_data_items'].get(data_id, 'hidden') + self.app.set_data_visibility(phase_viewer_id, data.label, visible == 'visible') + + # set x_att + phase_comp = self.app._jdaviz_helper._component_ids[phase_comp_lbl] + pv.state.x_att = phase_comp + + # set viewer limits + pv.state.x_min, pv.state.x_max = (self.wrap_at-1, self.wrap_at) + return pv.user_api def vue_create_phase_viewer(self, *args): - self.create_phase_viewer() + if not self.phase_viewer_exists: + self.create_phase_viewer() def vue_period_halve(self, *args): self.period /= 2 @@ -350,6 +352,7 @@ def _on_component_rename(self, old_lbl, new_lbl): viewer._ref_or_id.replace(old_lbl, new_lbl), update_id=True ) + viewer._ephemeris_component = new_lbl # update metadata entries so that they can be used for filtering applicable entries in # data menus @@ -465,7 +468,8 @@ def round_to_1(x): return round(x, -int(np.floor(np.log10(abs(x))))) # if phase-viewer doesn't yet exist in the app, create it now - self.create_phase_viewer() + if not self.phase_viewer_exists: + self.create_phase_viewer() # update value in the dictionary (to support multi-ephems) if event: @@ -478,9 +482,10 @@ def round_to_1(x): if event.get('name') == 'wrap_at': old = event.get('old') if event.get('old') != '' else self._prev_wrap_at if event.get('new') != '': - pvs = self.default_phase_viewer.state delta_phase = event.get('new') - old - pvs.x_min, pvs.x_max = pvs.x_min + delta_phase, pvs.x_max + delta_phase + for pv in self._get_phase_viewers(): + pvs = pv.state + pvs.x_min, pvs.x_max = pvs.x_min + delta_phase, pvs.x_max + delta_phase # we need to cache the old value since it could become a string # if the widget is cleared self._prev_wrap_at = event.get('new') diff --git a/lcviz/plugins/viewer_creator/viewer_creator.py b/lcviz/plugins/viewer_creator/viewer_creator.py index 70e64b67..8975973c 100644 --- a/lcviz/plugins/viewer_creator/viewer_creator.py +++ b/lcviz/plugins/viewer_creator/viewer_creator.py @@ -1,7 +1,6 @@ from jdaviz.configs.default.plugins import ViewerCreator from jdaviz.core.registries import tool_registry, viewer_registry from lcviz.events import EphemerisComponentChangedMessage -from lcviz.viewers import ephem_component_from_phase_viewer_name __all__ = ['ViewerCreator'] @@ -21,33 +20,23 @@ def _rebuild_available_viewers(self, *args): # and label (what appears in dropdown and the default label of the viewer) if self.app._jdaviz_helper is not None: - phase_viewers = [{'name': 'lcviz-phase-viewer', 'label': f'flux-vs-phase:{e}'} + phase_viewers = [{'name': f'lcviz-phase-viewer:{e}', 'label': f'flux-vs-phase:{e}'} for e in self.app._jdaviz_helper.plugins['Ephemeris'].component.choices] # noqa else: phase_viewers = [] self.viewer_types = [v for v in self.viewer_types if v['name'].startswith('lcviz') - and v['label'] != 'flux-vs-phase'] + phase_viewers + and not v['label'].startswith('flux-vs-phase')] + phase_viewers + self.send_state('viewer_types') def vue_create_viewer(self, name): - for viewer_item in self.viewer_types: - if viewer_item['name'] == name: - label = viewer_item['label'] - break - else: - label = viewer_registry.members[name]['label'] - - if label in self.app._jdaviz_helper.viewers: - # clone whenever possible - # TODO: update this to not rely directly on the label for phase-viewers, but rather - # checking for the same ephemeris - self.app._jdaviz_helper.viewers[label]._obj.clone_viewer() - return - - if name == 'lcviz-phase-viewer': - ephem_comp = ephem_component_from_phase_viewer_name(label) + if name.startswith('lcviz-phase-viewer') or name.startswith('flux-vs-phase'): + ephem_comp = name.split(':')[1] ephem_plg = self.app._jdaviz_helper.plugins['Ephemeris'] ephem_plg.create_phase_viewer(ephem_comp) return + if name == 'flux-vs-time': + # allow passing label and map to the name for upstream support + name = 'lcviz-time-viewer' super().vue_create_viewer(name) diff --git a/lcviz/viewers.py b/lcviz/viewers.py index 4f0f0ff9..fa853c74 100644 --- a/lcviz/viewers.py +++ b/lcviz/viewers.py @@ -20,11 +20,7 @@ from lightkurve import LightCurve -__all__ = ['TimeScatterView', 'PhaseScatterView', 'ephem_component_from_phase_viewer_name'] - - -def ephem_component_from_phase_viewer_name(label): - return label.split('[')[0].split(':')[-1] +__all__ = ['TimeScatterView', 'PhaseScatterView'] @viewer_registry("lcviz-time-viewer", label="flux-vs-time") @@ -214,17 +210,8 @@ def apply_roi(self, roi, use_current=False): super().apply_roi(roi, use_current=use_current) - def _get_clone_viewer_reference(self): - base_name = self.reference.split("[")[0] - name = base_name - ind = 0 - while name in self.jdaviz_helper.viewers.keys(): - ind += 1 - name = f"{base_name}[{ind}]" - return name - def clone_viewer(self): - name = self._get_clone_viewer_reference() + name = self.jdaviz_helper._get_clone_viewer_reference(self.reference) self.jdaviz_app._on_new_viewer(NewViewerMessage(self.__class__, data=None, @@ -240,7 +227,9 @@ def clone_viewer(self): # TODO: don't revert color when adding same data to a new viewer # (same happens when creating a phase-viewer from ephemeris plugin) - new_viewer = self.jdaviz_helper.viewers[name]._obj + new_viewer = self.jdaviz_app.get_viewer(name) + if hasattr(self, 'ephemeris_component'): + new_viewer._ephemeris_component = self._ephemeris_component for k, v in this_state.items(): if k in ('layers',): continue @@ -251,13 +240,13 @@ def clone_viewer(self): @viewer_registry("lcviz-phase-viewer", label="flux-vs-phase") class PhaseScatterView(TimeScatterView): - @property - def ephemeris_component(self): - return ephem_component_from_phase_viewer_name(self.reference) + def __init__(self, *args, **kwargs): + self._ephemeris_component = 'default' + super().__init__(*args, **kwargs) def _set_plot_x_axes(self, dc, component_labels, light_curve): # setting of y_att will be handled by ephemeris plugin - self.state.x_att = dc[0].components[component_labels.index(f'phase:{self.ephemeris_component}')] # noqa + self.state.x_att = dc[0].components[component_labels.index(f'phase:{self._ephemeris_component}')] # noqa self.figure.axes[0].label = 'phase' self.figure.axes[0].num_ticks = 5 @@ -266,4 +255,4 @@ def times_to_phases(self, times): if ephem is None: raise ValueError("must have ephemeris plugin loaded to convert") - return ephem.times_to_phases(times, ephem_component=self.ephemeris_component) + return ephem.times_to_phases(times, ephem_component=self._ephemeris_component)