Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions jdaviz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@
global _current_index

_apps = []
_new_app_hooks = []


def _register_new_app_hook(hook):
"""
Register a callable to be invoked on every new jdaviz App instance.

The hook is also immediately applied to all existing App instances,
so packages can safely call this regardless of import order.

Parameters
----------
hook : callable
A callable that accepts a single argument: the new ``App`` instance.

Examples
--------
Register a hook that patches every app with custom behaviour::

import jdaviz
jdaviz.register_new_app_hook(my_package.patch_app)
"""
_new_app_hooks.append(hook)
for app in _apps:
hook(app)


def new_app(replace=False, set_as_current=True):
Expand Down Expand Up @@ -64,6 +89,8 @@ def new_app(replace=False, set_as_current=True):
# rename the internal Application instance and/or merge functionality in with the
# App class to avoid confusion.
ca = App(api_hints_obj='jd')
for hook in _new_app_hooks:
hook(ca)
if replace:
_apps[_current_index] = ca
else:
Expand Down
2 changes: 2 additions & 0 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ class ApplicationState(State):
0, docstring="Index of the active subtab in the info sidebar.")
jdaviz_version = CallbackProperty(
__version__, docstring="Version of Jdaviz.")
downstream_packages = ListCallbackProperty(
docstring="List of downstream packages registered with this app instance.")
global_search = CallbackProperty(
'', docstring="Global search string.")
global_search_menu = CallbackProperty(
Expand Down
1 change: 1 addition & 0 deletions jdaviz/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
:api_hints_obj="api_hints_obj"
:api_hints_enabled="state.show_api_hints"
:about_widget="state.tray_items[state.tray_items.map(ti => ti.label).indexOf('About')].widget"
:downstream_packages="state.downstream_packages"
:force_open_about.sync="force_open_about"
></j-about-menu>

Expand Down
8 changes: 7 additions & 1 deletion jdaviz/components/about_menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
@click="() => {popup_open = !popup_open}"
style="font-family: monospace; font-size: 10pt; text-transform: lowercase; margin-left: 4px; margin-right: 6px; padding: 2px">
v{{ jdaviz_version }}
<span
v-if="downstream_packages && downstream_packages.length > 0"
style="margin-left: 4px; border-radius: 10px; padding: 0 4px; font-size: 9pt; line-height: 1.6"
>
+ {{ downstream_packages.length == 1 ? downstream_packages[0].abbreviation : downstream_packages.length }}
</span>
</v-btn>
</j-tooltip>
</template>
Expand All @@ -30,7 +36,7 @@

<script>
module.exports = {
props: ['jdaviz_version', 'api_hints_obj', 'api_hints_enabled', 'about_widget', 'force_open_about'],
props: ['jdaviz_version', 'api_hints_obj', 'api_hints_enabled', 'about_widget', 'force_open_about', 'downstream_packages'],
data: function () {
return {
popup_open: false,
Expand Down
22 changes: 21 additions & 1 deletion jdaviz/configs/cubeviz/plugins/slice/slice.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ def _initialize_location(self, *args):
# middle of the first found layer)
self._clear_cache()
for viewer in self.slice_indicator_viewers:
x_att_pixel = getattr(viewer.state, 'x_att_pixel', None)
x_att_pixel_str = str(x_att_pixel) if x_att_pixel is not None else ''
if (str(viewer.state.x_att) not in self.valid_slice_att_names and
str(viewer.state.x_att_pixel) not in self.valid_slice_att_names):
x_att_pixel_str not in self.valid_slice_att_names):
# avoid setting value to degs, before x_att is changed to wavelength, for example
continue
if (self._app._get_display_unit(viewer.slice_display_unit_name) == ''
Expand Down Expand Up @@ -430,6 +432,24 @@ def __init__(self, *args, **kwargs):
self.allow_disable_snapping = False

self.viewer.add_filter(lambda viewer: isinstance(viewer, (CubevizImageView, CubevizProfileView))) # noqa
if self.config == 'deconfigged':
self.hub.subscribe(self, RemoveDataMessage, handler=self._set_relevant)
self._set_relevant()

@observe('viewer_items')
def _set_relevant(self, *args):
if (self.config == 'deconfigged' and
not any(d.meta.get('_importer') == 'Spectrum3DImporter'
for d in self._app.data_collection)):
self.irrelevant_msg = 'No spectral cube data loaded'
return
super()._set_relevant(*args)

def _on_add_data(self, msg):
self._set_relevant()
super()._on_add_data(msg)

def _on_remove_data(self, msg):
self._set_relevant()

@observe('vdocs')
Expand Down
14 changes: 13 additions & 1 deletion jdaviz/configs/cubeviz/plugins/sonify_data/sonify_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def __init__(self, *args, **kwargs):
self.sound_device_indexes = None
self.refresh_device_list()

self.add_to_viewer_selected = 'flux-viewer'
self._set_default_viewer_selected()
self.observe(self._on_add_to_viewer_items_changed, names=['add_to_viewer_items'])
self.sonified_cube = None
self.sonified_viewers = []
self.sonification_wl_ranges = None
Expand All @@ -100,8 +101,19 @@ def __init__(self, *args, **kwargs):
handler=self._data_added_to_viewer)

if self.config == "deconfigged":
self.dataset.add_filter('is_flux_cube')
self.observe_traitlets_for_relevancy(traitlets_to_observe=['dataset_items'])

def _set_default_viewer_selected(self):
"""Set add_to_viewer_selected to 'flux-viewer' if it is a valid choice."""
valid_labels = [item.get('label') for item in self.add_to_viewer_items]
if 'flux-viewer' in valid_labels and self.add_to_viewer_selected in ('', 'None'):
self.add_to_viewer_selected = 'flux-viewer'

def _on_add_to_viewer_items_changed(self, change):
"""Defer setting the default viewer until choices are populated."""
self._set_default_viewer_selected()

def _get_supported_viewers(self):
"""Return viewer types that can display sonified data."""
return [{'label': '3D Spectrum', 'reference': 'cubeviz-image-viewer'}]
Expand Down
23 changes: 22 additions & 1 deletion jdaviz/configs/default/plugins/about/about.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import requests
from packaging.version import Version
from traitlets import Bool, Unicode, observe
from traitlets import Bool, List, Unicode, observe

from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin
Expand Down Expand Up @@ -28,6 +28,7 @@ class About(PluginTemplateMixin):
jdaviz_version = Unicode("unknown").tag(sync=True)
jdaviz_pypi = Unicode("unknown").tag(sync=True)
not_is_latest = Bool(False).tag(sync=True)
downstream_packages = List([]).tag(sync=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -67,6 +68,26 @@ def _on_plugin_opened(self, *args):
def show_popup(self):
self._app.force_open_about = True

def register_downstream_package(self, name, version, abbreviation=None):
"""
Register a downstream package to be listed in the About panel.

Parameters
----------
name : str
The display name of the downstream package.
version : str
The installed version string.
abbreviation : str, optional
Short label shown as a badge on the version button in the app bar.
Defaults to the first two characters of ``name`` in lowercase.
"""
entry = {'name': name, 'version': version,
'abbreviation': abbreviation or name[:2].lower()}
if entry not in self.downstream_packages:
self.downstream_packages = self.downstream_packages + [entry]
self._app.state.downstream_packages = self._app.state.downstream_packages + [entry]

@property
def user_api(self):
if self.config != 'deconfigged':
Expand Down
31 changes: 22 additions & 9 deletions jdaviz/configs/default/plugins/about/about.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@
<v-row>
<v-text-field class="v-messages v-messages__message text--secondary"
v-model="jdaviz_version"
label="Version"
label="jdaviz version"
hint="Version of installed Jdaviz."
readonly>
</v-text-field>
</v-row>

<v-row v-if="not_is_latest">
<span class="v-messages v-messages__message text--secondary" style="color: red !important">
A newer version ({{ jdaviz_pypi }}) is available from PyPI.
</span>
<j-docs-link link="https://pypi.org/project/jdaviz/" linktext="Go to PyPI">
Please update Jdaviz and restart your session.
</j-docs-link>
</v-row>
<div v-if="not_is_latest">
<v-row>
<span class="v-messages v-messages__message text--secondary" style="color: red !important">
A newer version ({{ jdaviz_pypi }}) is available from PyPI.
</span>
</v-row>
<v-row>
<j-docs-link link="https://pypi.org/project/jdaviz/" linktext="Go to PyPI">
Please update Jdaviz and restart your session.
</j-docs-link>
</v-row>
</div>

<v-row>
<j-docs-link link="https://github.com/spacetelescope/jdaviz/blob/main/CHANGES.rst" linktext="Change Log">
Expand All @@ -40,5 +44,14 @@
</j-docs-link>
</v-row>

<v-row v-for="pkg in downstream_packages" :key="pkg.name">
<v-text-field class="v-messages v-messages__message text--secondary"
:value="pkg.version"
:label="pkg.name + ' version'"
:hint="'Version of installed ' + pkg.name + '.'"
readonly>
</v-text-field>
</v-row>

</j-tray-plugin>
</template>
23 changes: 18 additions & 5 deletions jdaviz/configs/default/plugins/markers/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ class Markers(PluginTemplateMixin, ViewerSelectMixin, TableMixin):
'value:unreliable': None,
'index': np.nan}

# Downstream configs may extend the markers table by setting these at import time.
# _extra_headers: list of additional column names appended to the config-specific headers.
# _extra_default_values: dict of {column_name: default_value} for extra columns.
_extra_headers = []
_extra_default_values = {}

# When True, headers_visible and export_table() will omit fully-unpopulated columns.
_table_skip_empty_columns = True

@property
def coords_info(self):
"""Convenience accessor for the CoordsInfo toolbar tool."""
return self._app.session.application._tools.get('g-coords-info')

@property
def user_api(self):
return PluginUserApi(self, expose=('table', 'measurements_table',
Expand Down Expand Up @@ -92,10 +106,13 @@ def __init__(self, *args, **kwargs):
# allow downstream configs to override headers
headers = kwargs.get('headers', [])

headers += list(self._extra_headers)
headers += ['data_label']
self.table.headers_avail = headers
self.table.headers_visible = headers
self.table._default_values_by_colname = self._default_table_values
default_values = {**self._default_table_values, **self._extra_default_values}
self.table._default_values_by_colname = default_values
self.table._skip_empty_columns = self._table_skip_empty_columns

self._distance_marks = {}
self._distance_first_point = None
Expand Down Expand Up @@ -470,10 +487,6 @@ def marks(self):
for viewer_id, viewer in self._app._viewer_store.items()
if hasattr(viewer, 'figure')}

@property
def coords_info(self):
return self._app.session.application._tools['g-coords-info']

@observe('is_active')
def _on_is_active_changed(self, *args):
if self.disabled_msg:
Expand Down
36 changes: 36 additions & 0 deletions jdaviz/configs/default/plugins/plot_options/plot_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,32 @@ class PlotOptions(PluginTemplateMixin, ViewerSelectMixin):
table_columns_visible_value = List().tag(sync=True)
table_columns_visible_sync = Dict().tag(sync=True)

# Downstream configs may register extra layer filter functions and user_api
# modifications at import time via the classmethods below, rather than subclassing.
_layer_filter_hooks = []
_user_api_expose_extra = []
_user_api_remove = []

@classmethod
def register_layer_filter(cls, filter_factory):
"""Register a layer filter factory.

The factory is called once per plugin instance during ``__init__`` with
the plugin instance as its only argument, and must return a filter
callable ``(layer_state) -> bool``. Using a factory (rather than a bare
filter function) lets the filter close over plugin attributes such as
``self.layer`` that are not available until instantiation.
"""
cls._layer_filter_hooks = list(cls._layer_filter_hooks) + [filter_factory]

@classmethod
def register_user_api(cls, expose=None, remove=None):
"""Register extra user_api attributes to expose or remove."""
if expose:
cls._user_api_expose_extra = list(cls._user_api_expose_extra) + list(expose)
if remove:
cls._user_api_remove = list(cls._user_api_remove) + list(remove)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand All @@ -497,6 +523,10 @@ def __init__(self, *args, **kwargs):
'catalog_has_correct_coords_based_on_link_type',
'not_in_table_viewer']

# Apply any layer filter factories registered by downstream configs
for filter_factory in self._layer_filter_hooks:
self.layer.add_filter(filter_factory(self))

self.swatches_palette = [
['#FF0000', '#AA0000', '#550000'],
['#FFD300', '#AAAA00', '#555500'],
Expand Down Expand Up @@ -830,6 +860,12 @@ def user_api(self):
'marker_color_col', 'marker_colormap',
'marker_colormap_vmin', 'marker_colormap_vmax']

# Apply any user_api modifications registered by downstream configs
if self._user_api_expose_extra:
expose += [e for e in self._user_api_expose_extra if e not in expose]
if self._user_api_remove:
expose = [e for e in expose if e not in self._user_api_remove]

return PluginUserApi(self, expose)

@property
Expand Down
7 changes: 7 additions & 0 deletions jdaviz/configs/default/plugins/subset_tools/subset_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ class SubsetTools(PluginTemplateMixin, LoadersMixin):
can_freeze = Bool(False).tag(sync=True)
server_is_remote = Bool(False).tag(sync=True)

# Downstream configs can set _default_can_freeze = True at import time to enable
# the freeze button without subclassing this plugin.
_default_can_freeze = False

icon_replace = Unicode(read_icon(os.path.join(icon_path("glue_replace", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa
icon_or = Unicode(read_icon(os.path.join(icon_path("glue_or", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa
icon_and = Unicode(read_icon(os.path.join(icon_path("glue_and", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa
Expand All @@ -141,6 +145,9 @@ class SubsetTools(PluginTemplateMixin, LoadersMixin):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Apply can_freeze default (downstream configs may set _default_can_freeze = True)
self.can_freeze = self._default_can_freeze

# Initialize server_is_remote from settings
self.server_is_remote = self.app.state.settings.get('server_is_remote', False)

Expand Down
Loading
Loading