diff --git a/docs/plugins.rst b/docs/plugins.rst index 60f5f24c..2d848c47 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -179,6 +179,40 @@ visible when the plugin is opened. Jdaviz documentation on the Markers plugin. +.. _time-indicator: + +Time Selector +============== + +The time selector plugin allows defining the time indicated in all light curve viewers +(time and phase viewers) as well as the time at which all image cubes are displayed. + + +.. admonition:: User API Example + :class: dropdown + + See the :class:`~lcviz.plugins.time_selector.time_selector.TimeSelector` user API documentation for more details. + + .. code-block:: python + + from lcviz import LCviz + lc = search_lightcurve("HAT-P-11", mission="Kepler", + cadence="long", quarter=10).download().flatten() + lcviz = LCviz() + lcviz.load_data(lc) + lcviz.show() + + ts = lcviz.plugins['Time Selector'] + ts.open_in_tray() + + +.. seealso:: + + :ref:`Jdaviz Slice Plugin ` + Jdaviz documentation on the Slice plugin. + + + .. _flatten: Flatten diff --git a/docs/reference/api_plugins.rst b/docs/reference/api_plugins.rst index a366d19a..20d0818e 100644 --- a/docs/reference/api_plugins.rst +++ b/docs/reference/api_plugins.rst @@ -29,3 +29,6 @@ Plugins API .. automodapi:: lcviz.plugins.subset_plugin.subset_plugin :no-inheritance-diagram: + +.. automodapi:: lcviz.plugins.time_selector.time_selector + :no-inheritance-diagram: diff --git a/lcviz/helper.py b/lcviz/helper.py index 9f234d37..bbd6ecab 100644 --- a/lcviz/helper.py +++ b/lcviz/helper.py @@ -74,7 +74,8 @@ class LCviz(ConfigHelper): 'toolbar': ['g-data-tools', 'g-subset-tools', 'lcviz-coords-info'], 'tray': ['lcviz-metadata-viewer', 'flux-column', 'lcviz-plot-options', 'lcviz-subset-plugin', - 'lcviz-markers', 'flatten', 'frequency-analysis', 'ephemeris', + 'lcviz-markers', 'time-selector', + 'flatten', 'frequency-analysis', 'ephemeris', 'binning', 'lcviz-export-plot'], 'viewer_area': [{'container': 'col', 'children': [{'container': 'row', diff --git a/lcviz/marks.py b/lcviz/marks.py index b5c89da5..28d3b736 100644 --- a/lcviz/marks.py +++ b/lcviz/marks.py @@ -1,11 +1,21 @@ +from astropy import units as u import numpy as np -from jdaviz.core.marks import PluginLine, PluginScatter +from jdaviz.core.marks import PluginLine, PluginScatter, SliceIndicatorMarks from lcviz.viewers import PhaseScatterView __all__ = ['LivePreviewTrend', 'LivePreviewFlattened', 'LivePreviewBinning'] +def _slice_indicator_get_slice_axis(self, data): + if hasattr(data, 'time'): + return data.time.value * u.d + return [] * u.dimensionless_unscaled + + +SliceIndicatorMarks._get_slice_axis = _slice_indicator_get_slice_axis + + class WithoutPhaseSupport: def update_ty(self, times, y): self.times = np.asarray(times) diff --git a/lcviz/plugins/__init__.py b/lcviz/plugins/__init__.py index 2bf109b5..fe4c9879 100644 --- a/lcviz/plugins/__init__.py +++ b/lcviz/plugins/__init__.py @@ -6,6 +6,7 @@ from .flux_column.flux_column import * # noqa from .frequency_analysis.frequency_analysis import * # noqa from .markers.markers import * # noqa +from .time_selector.time_selector import * # noqa from .metadata_viewer.metadata_viewer import * # noqa from .plot_options.plot_options import * # noqa from .subset_plugin.subset_plugin import * # noqa diff --git a/lcviz/plugins/time_selector/__init__.py b/lcviz/plugins/time_selector/__init__.py new file mode 100644 index 00000000..0983e6d5 --- /dev/null +++ b/lcviz/plugins/time_selector/__init__.py @@ -0,0 +1 @@ +from .time_selector import * # noqa diff --git a/lcviz/plugins/time_selector/time_selector.py b/lcviz/plugins/time_selector/time_selector.py new file mode 100644 index 00000000..9620e361 --- /dev/null +++ b/lcviz/plugins/time_selector/time_selector.py @@ -0,0 +1,70 @@ +from jdaviz.configs.cubeviz.plugins import Slice +from jdaviz.core.registries import tray_registry + +from lcviz.viewers import CubeView, PhaseScatterView + +__all__ = ['TimeSelector'] + + +@tray_registry('time-selector', label="Time Selector") +class TimeSelector(Slice): + """ + See the :ref:`Time Selector Plugin Documentation ` for more details. + + Only the following attributes and methods are available through the + :ref:`public plugin API `: + + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show` + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray` + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray` + * ``value`` Time of the indicator. When setting this directly, it will + update automatically to the value corresponding to the nearest slice, if ``snap_to_slice`` is + enabled and a cube is loaded. + * ``show_indicator`` + Whether to show indicator in spectral viewer when slice tool is inactive. + * ``show_value`` + Whether to show slice value in label to right of indicator. + * ``snap_to_slice`` + Whether the indicator (and ``value``) should snap to the value of the nearest slice in the + cube (if one exists). + """ + _cube_viewer_cls = CubeView + _cube_viewer_default_label = 'image' + + def __init__(self, *args, **kwargs): + """ + + """ + super().__init__(*args, **kwargs) + self.docs_link = f"https://lcviz.readthedocs.io/en/{self.vdocs}/plugins.html#time-selector" + self.docs_description = "Select time to sync across all viewers (as an indicator in all time/phase viewers or to select the active slice in any image/cube viewers). The slice can also be changed interactively in any time viewer by activating the slice tool." # noqa + self.value_label = 'Time' + self.value_unit = 'd' + self.allow_disable_snapping = True + + @property + def slice_axis(self): + # global display unit "axis" corresponding to the slice axis + return 'time' + + @property + def valid_slice_att_names(self): + return ["time", "dt"] + + @property + def user_api(self): + api = super().user_api + # can be removed after deprecated upstream attributes for wavelength/wavelength_value + # are removed in the lowest supported version of jdaviz + api._expose = [e for e in api._expose if e not in ('slice', 'wavelength', + 'wavelength_value', 'show_wavelength')] + return api + + def _on_select_slice_message(self, msg): + viewer = msg.sender.viewer + if isinstance(viewer, PhaseScatterView): + prev_phase = viewer.times_to_phases(self.value) + new_phase = msg.value + self.value = self.value + (new_phase - prev_phase) * viewer.ephemeris.get('period', 1.0) + else: + super()._on_select_slice_message(msg) diff --git a/lcviz/viewers.py b/lcviz/viewers.py index f1670aef..71a9f88c 100644 --- a/lcviz/viewers.py +++ b/lcviz/viewers.py @@ -12,7 +12,8 @@ from jdaviz.core.events import NewViewerMessage from jdaviz.core.registries import viewer_registry -from jdaviz.configs.cubeviz.plugins.viewers import CubevizImageView +from jdaviz.configs.cubeviz.plugins.viewers import (CubevizImageView, + WithSliceIndicator, WithSliceSelection) from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView @@ -60,13 +61,14 @@ def clone_viewer(self): @viewer_registry("lcviz-time-viewer", label="flux-vs-time") -class TimeScatterView(JdavizViewerMixin, CloneViewerMixin, BqplotScatterView): +class TimeScatterView(JdavizViewerMixin, CloneViewerMixin, WithSliceIndicator, BqplotScatterView): # categories: zoom resets, zoom, pan, subset, select tools, shortcuts tools_nested = [ ['jdaviz:homezoom', 'jdaviz:prevzoom'], ['jdaviz:boxzoom', 'jdaviz:xrangezoom', 'jdaviz:yrangezoom'], ['jdaviz:panzoom', 'jdaviz:panzoom_x', 'jdaviz:panzoom_y'], ['bqplot:xrange', 'bqplot:yrange', 'bqplot:rectangle'], + ['jdaviz:selectslice'], ['lcviz:viewer_clone', 'jdaviz:sidebar_plot', 'jdaviz:sidebar_export'] ] default_class = LightCurve @@ -79,7 +81,7 @@ def __init__(self, *args, **kwargs): self.display_mask = False self.time_unit = kwargs.get('time_unit', u.d) - self.initialize_toolbar() + self.initialize_toolbar(default_tool_priority=['jdaviz:selectslice']) self._subscribe_to_layers_update() # hack to inherit a small subset of methods from SpecvizProfileView # TODO: refactor jdaviz so these can be included in some mixin @@ -89,6 +91,12 @@ def __init__(self, *args, **kwargs): self._clean_error = lambda: SpecvizProfileView._clean_error(self) self.density_map = kwargs.get('density_map', False) + @property + def slice_component_label(self): + # label of the component in the lightcurves corresponding to the slice axis + # calling data_collection_item.get_component(slice_component_label) must work + return 'dt' + def data(self, cls=None): data = [] @@ -252,6 +260,13 @@ class PhaseScatterView(TimeScatterView): def ephemeris_component(self): return self.reference.split('[')[0].split(':')[-1] + @property + def ephemeris(self): + ephem = self.jdaviz_helper.plugins.get('Ephemeris', None) + if ephem is None: + raise ValueError("must have ephemeris plugin loaded to access ephemeris") + return ephem.ephemerides.get(self.ephemeris_component) + 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 @@ -265,9 +280,13 @@ def times_to_phases(self, times): return ephem.times_to_phases(times, ephem_component=self.ephemeris_component) + def _set_slice_indicator_value(self, value): + # NOTE: on first call, this will initialize the indicator itself + self.slice_indicator.value = self.times_to_phases(value) + @viewer_registry("lcviz-cube-viewer", label="cube") -class CubeView(CloneViewerMixin, CubevizImageView): +class CubeView(CloneViewerMixin, CubevizImageView, WithSliceSelection): # categories: zoom resets, zoom, pan, subset, select tools, shortcuts tools_nested = [ ['jdaviz:homezoom', 'jdaviz:prevzoom'], @@ -297,6 +316,18 @@ def __init__(self, *args, **kwargs): # * _default_flux_viewer_reference_name # * _default_uncert_viewer_reference_name + @property + def slice_component_label(self): + # label of the component in the cubes corresponding to the slice axis + # calling data_collection_item.get_component(slice_component_label) on any + # input cube-data must work + return 'dt' + + @property + def slice_index(self): + # index in viewer.slices corresponding to the slice axis + return 0 + def _initial_x_axis(self, *args): # Make sure that the x_att/y_att is correct on data load # called via a callback set upstream in CubevizImageView when reference_data is changed diff --git a/pyproject.toml b/pyproject.toml index ae426903..e5bc6eb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ ] dependencies = [ "astropy>=5.2", - "jdaviz==3.8.*", + "jdaviz==3.9.*", "lightkurve>=2.4.1", ] dynamic = [