Source code for jdaviz.configs.default.plugins.plot_options.plot_options

import math
import os
import warnings

import matplotlib
import numpy as np
from functools import cached_property
from echo import delay_callback
from astropy.visualization import ManualInterval, ContrastBiasStretch
from glue.core.subset_group import GroupedSubset
from glue.config import stretches as glue_stretches
from glue.viewers.histogram.state import HistogramViewerState
from glue.viewers.histogram.state import HistogramLayerState as BqplotHistogramLayerState
from glue.viewers.scatter.state import ScatterViewerState
from glue.viewers.profile.state import ProfileViewerState, ProfileLayerState
from glue.viewers.image.state import ImageSubsetLayerState, ImageViewerState
from glue.viewers.scatter.state import ScatterLayerState as BqplotScatterLayerState
from glue.viewers.image.composite_array import COLOR_CONVERTER
from glue_jupyter.bqplot.image.state import BqplotImageLayerState
from glue_jupyter.common.toolbar_vuetify import read_icon
from scipy.interpolate import PchipInterpolator
from traitlets import Any, Dict, Float, Bool, Int, List, Unicode, observe

from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelectMixin, LayerSelect,
                                        PlotOptionsSyncState, Plot,
                                        skip_if_no_updates_since_last_active, with_spinner)
from jdaviz.core.events import ChangeRefDataMessage, ViewerAddedMessage
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.tools import ICON_DIR
from jdaviz.core.custom_traitlets import IntHandleEmpty
from jdaviz.core.sonified_layers import SonifiedLayerState
# by importing from utils, glue_colormaps will include the custom Random colormap
from jdaviz.utils import is_not_wcs_only, cmap_samples, glue_colormaps

__all__ = ['PlotOptions']

RANDOM_SUBSET_SIZE = 10_000


class SplineStretch:
    """
    A class to represent spline stretches.

    Attributes
    ----------
    k : int
        Degree of the smoothing spline. Default is 3.
    bc_type : str or None
        Boundary condition type. Default is None.
    t : array-like or None
        Array of knot positions. Default is None.
    x : array-like
        The x-coordinates of the data points.
    y : array-like
        The y-coordinates of the data points.
    spline : object
        Interpolating spline.

    Raises
    ------
    ValueError
        If `x` and `y` have different lengths.
    """

    def __init__(self):
        # Default x, y values(0-1) range chosen for a typical initial spline shape.
        # Can be modified if required.
        self._x = np.array([0, 0.1, 0.2, 0.7, 1])
        self._y = np.array([0, 0.05, 0.3, 0.9, 1])
        self.update_knots(self._x, self._y)

    @property
    def knots(self):
        return (self._x, self._y)

    @knots.setter
    def knots(self, value):
        x, y = value
        if len(x) != len(y):
            # Silently return
            return
        self.update_knots(x, y)

    def __call__(self, values, out=None, clip=False):
        # For our uses, we can ignore `out` and `clip`, but those would need
        # to be implemented before contributing this class upstream.
        return self.spline(values)

    def update_knots(self, x, y):
        self._x = x
        self._y = y
        self.spline = PchipInterpolator(self._x, self._y)


# Add the spline stretch to the glue stretch registry if not registered
if "spline" not in glue_stretches:
    glue_stretches.add("spline", SplineStretch, display="Spline")


def _round_step(step):
    # round the step for a float input
    if step <= 0:
        return 1e-6, 6
    decimals = -int(np.log10(abs(step))) + 1 if step != 0 else 6
    if decimals < 0:
        decimals = 0
    return float(np.round(step, decimals)), decimals


[docs] @tray_registry('g-plot-options', label="Plot Options", category='core', sidebar='settings', subtab=0) class PlotOptions(PluginTemplateMixin, ViewerSelectMixin): """ The Plot Options Plugin gives access to per-viewer and per-layer options and enables setting across multiple viewers/layers simultaneously. Only the following attributes and methods are available through the :ref:`public plugin API <plugin-apis>`: * :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` * ``viewer`` (:class:`~jdaviz.core.template_mixin.ViewerSelect`): * ``viewer_multiselect`` * ``layer`` (:class:`~jdaviz.core.template_mixin.LayerSelect`): * ``layer_multiselect`` * :meth:`select_all` * ``subset_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): whether a subset should be visible. * ``subset_color`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``subset_opacity`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``axes_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``line_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``line_color`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``line_width`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``line_opacity`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``line_as_steps`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``uncertainty_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Imviz * ``stretch_function`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``stretch_preset`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``stretch_vmin`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``stretch_vmax`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``stretch_hist_zoom_limits`` : whether to show the histogram for the current zoom limits instead of all data within the layer; not exposed for Specviz. * ``stretch_hist_nbins`` : number of bins to use in creating the histogram; not exposed for Specviz. * ``stretch_curve_visible`` : bool whether the stretch histogram's colormap "curve" is visible; not exposed for Specviz. * ``image_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): whether the image bitmap is visible; not exposed for Specviz. * ``image_color_mode`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``image_color`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. This only applies when ``image_color_mode`` is "Color". * ``image_colormap`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. This only applies when ``image_color_mode`` is "Colormap". * ``image_opacity`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. Valid values are between 0 and 1, inclusive. Default is 1. * ``image_contrast`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. Valid values are between 0 and 4, inclusive. Default is 1. * ``image_bias`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. Valid values are between 0 and 1, inclusive. Default is 0.5. * ``contour_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): whether the contour is visible; not exposed for Specviz * ``contour_mode`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz * ``contour_min`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. This only applies when ``contour_mode`` is "Linear". * ``contour_max`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. This only applies when ``contour_mode`` is "Linear". * ``contour_nlevels`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. This only applies when ``contour_mode`` is "Linear". * ``contour_custom_levels`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. This only applies when ``contour_mode`` is "Custom". * ``volume_level`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. Set the volume for the selected sonified layer. * ``sonified_audible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): not exposed for Specviz. Set if the selected sonified layer will output audio. * ``xatt`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls the x-axis attribute for scatter and histogram plots. * ``yatt`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls the y-axis attribute for scatter plots. * ``hist_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Whether histogram is visible. * ``hist_color`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls histogram color. * ``hist_opacity`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls histogram opacity. * ``hist_xlog`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls x-axis log scaling for histograms. * ``hist_ylog`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls y-axis log scaling for histograms. * ``hist_n_bin`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls number of bins for histograms. * ``hist_x_min`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls x-axis minimum value for histograms. * ``hist_x_max`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls x-axis maximum value for histograms. * ``hist_cumulative`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls cumulative histogram display. * ``hist_normalize`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls histogram normalization. * ``hist_update_bins_on_reset_limits`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for deconfigged. Controls automatic bin updates when limits are reset. * ``marker_visible`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Whether markers are visible. * ``marker_fill`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker fill. * ``marker_opacity`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker opacity. * ``marker_size`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker size. * ``marker_size_scale`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker size scaling. * ``marker_color_mode`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker color mode. * ``marker_color`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker color. * ``marker_color_col`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker color column. * ``marker_colormap`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker colormap. * ``marker_colormap_vmin`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker colormap minimum value. * ``marker_colormap_vmax`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Imviz and deconfigged. Controls marker colormap maximum value. """ template_file = __file__, "plot_options.vue" uses_active_status = Bool(True).tag(sync=True) # read-only display units display_units = Dict().tag(sync=True) viewer_limits = Dict().tag(sync=True) layer_multiselect = Bool(False).tag(sync=True) layer_items = List().tag(sync=True) layer_selected = Any().tag(sync=True) # Any needed for multiselect xatt_value = Unicode().tag(sync=True) xatt_sync = Dict().tag(sync=True) yatt_value = Unicode().tag(sync=True) yatt_sync = Dict().tag(sync=True) # profile/line viewer/layer options: line_visible_value = Bool().tag(sync=True) line_visible_sync = Dict().tag(sync=True) line_color_value = Any().tag(sync=True) line_color_sync = Dict().tag(sync=True) line_width_value = Int().tag(sync=True) line_width_sync = Dict().tag(sync=True) line_opacity_value = Float().tag(sync=True) line_opacity_sync = Dict().tag(sync=True) line_as_steps_value = Bool().tag(sync=True) line_as_steps_sync = Dict().tag(sync=True) uncertainty_visible_value = Int().tag(sync=True) uncertainty_visible_sync = Dict().tag(sync=True) x_min_value = Float().tag(sync=True) x_min_sync = Dict().tag(sync=True) x_max_value = Float().tag(sync=True) x_max_sync = Dict().tag(sync=True) y_min_value = Float().tag(sync=True) y_min_sync = Dict().tag(sync=True) y_max_value = Float().tag(sync=True) y_max_sync = Dict().tag(sync=True) x_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value y_bound_step = Float(0.1).tag(sync=True) # dynamic based on maximum value zoom_center_x_value = Float().tag(sync=True) zoom_center_x_sync = Dict().tag(sync=True) zoom_center_y_value = Float().tag(sync=True) zoom_center_y_sync = Dict().tag(sync=True) zoom_radius_value = Float().tag(sync=True) zoom_radius_sync = Dict().tag(sync=True) zoom_step = Float(1).tag(sync=True) # scatter/marker options marker_visible_value = Bool().tag(sync=True) marker_visible_sync = Dict().tag(sync=True) marker_fill_value = Bool().tag(sync=True) marker_fill_sync = Dict().tag(sync=True) marker_opacity_value = Float().tag(sync=True) marker_opacity_sync = Dict().tag(sync=True) marker_size_mode_value = Unicode().tag(sync=True) marker_size_mode_sync = Dict().tag(sync=True) marker_size_value = Float().tag(sync=True) marker_size_sync = Dict().tag(sync=True) marker_size_scale_value = Float().tag(sync=True) marker_size_scale_sync = Dict().tag(sync=True) marker_size_col_value = Unicode().tag(sync=True) marker_size_col_sync = Dict().tag(sync=True) marker_size_vmin_value = Float().tag(sync=True) marker_size_vmin_sync = Dict().tag(sync=True) marker_size_vmax_value = Float().tag(sync=True) marker_size_vmax_sync = Dict().tag(sync=True) marker_color_mode_value = Unicode().tag(sync=True) marker_color_mode_sync = Dict().tag(sync=True) marker_color_value = Any().tag(sync=True) marker_color_sync = Dict().tag(sync=True) marker_color_col_value = Unicode().tag(sync=True) marker_color_col_sync = Dict().tag(sync=True) marker_colormap_value = Unicode().tag(sync=True) marker_colormap_sync = Dict().tag(sync=True) marker_colormap_vmin_value = Float().tag(sync=True) marker_colormap_vmin_sync = Dict().tag(sync=True) marker_colormap_vmax_value = Float().tag(sync=True) marker_colormap_vmax_sync = Dict().tag(sync=True) # image viewer/layer options active_layer = Unicode().tag(sync=True) stretch_function_value = Unicode().tag(sync=True) stretch_function_sync = Dict().tag(sync=True) stretch_preset_value = Any().tag(sync=True) # glue will pass either a float or string stretch_preset_sync = Dict().tag(sync=True) stretch_vstep = Float(0.1).tag(sync=True) # dynamic based on full range from image stretch_vmin_value = Float().tag(sync=True) stretch_vmin_sync = Dict().tag(sync=True) stretch_vmax_value = Float().tag(sync=True) stretch_vmax_sync = Dict().tag(sync=True) stretch_params_value = Dict().tag(sync=True) stretch_params_sync = Dict().tag(sync=True) stretch_hist_sync = Dict().tag(sync=True) stretch_hist_zoom_limits = Bool().tag(sync=True) stretch_hist_nbins = IntHandleEmpty(25).tag(sync=True) stretch_histogram_widget = Unicode().tag(sync=True) stretch_curve_visible = Bool(True).tag(sync=True) subset_visible_value = Bool().tag(sync=True) subset_visible_sync = Dict().tag(sync=True) subset_color_value = Unicode().tag(sync=True) subset_color_sync = Dict().tag(sync=True) subset_opacity_value = Float().tag(sync=True) subset_opacity_sync = Dict().tag(sync=True) image_visible_value = Bool().tag(sync=True) image_visible_sync = Dict().tag(sync=True) image_color_mode_value = Unicode().tag(sync=True) image_color_mode_sync = Dict().tag(sync=True) image_color_value = Any().tag(sync=True) image_color_sync = Dict().tag(sync=True) image_colormap_value = Unicode().tag(sync=True) image_colormap_sync = Dict().tag(sync=True) image_opacity_value = Float().tag(sync=True) image_opacity_sync = Dict().tag(sync=True) image_contrast_value = Float().tag(sync=True) image_contrast_sync = Dict().tag(sync=True) image_bias_value = Float().tag(sync=True) image_bias_sync = Dict().tag(sync=True) contour_spinner = Bool().tag(sync=True) contour_visible_value = Bool().tag(sync=True) contour_visible_sync = Dict().tag(sync=True) contour_mode_value = Unicode().tag(sync=True) contour_mode_sync = Dict().tag(sync=True) contour_min_value = Float().tag(sync=True) contour_min_sync = Dict().tag(sync=True) contour_max_value = Float().tag(sync=True) contour_max_sync = Dict().tag(sync=True) contour_nlevels_value = Int().tag(sync=True) contour_nlevels_sync = Dict().tag(sync=True) contour_custom_levels_value = List().tag(sync=True) contour_custom_levels_txt = Unicode().tag(sync=True) # controlled by vue contour_custom_levels_sync = Dict().tag(sync=True) axes_visible_value = Bool().tag(sync=True) axes_visible_sync = Dict().tag(sync=True) icon_radialtocheck = Unicode(read_icon(os.path.join(ICON_DIR, 'radialtocheck.svg'), 'svg+xml')).tag(sync=True) # noqa icon_checktoradial = Unicode(read_icon(os.path.join(ICON_DIR, 'checktoradial.svg'), 'svg+xml')).tag(sync=True) # noqa cmap_samples = Dict(cmap_samples).tag(sync=True) swatches_palette = List().tag(sync=True) apply_RGB_presets_spinner = Bool(False).tag(sync=True) stretch_hist_spinner = Bool(False).tag(sync=True) volume_value = IntHandleEmpty(50).tag(sync=True) volume_sync = Dict().tag(sync=True) sonified_audible_value = Bool(False).tag(sync=True) sonified_audible_sync = Dict().tag(sync=True) hist_visible_value = Bool().tag(sync=True) hist_visible_sync = Dict().tag(sync=True) hist_opacity_value = Float().tag(sync=True) hist_opacity_sync = Dict().tag(sync=True) hist_color_value = Any().tag(sync=True) hist_color_sync = Dict().tag(sync=True) hist_xlog_value = Bool().tag(sync=True) hist_xlog_sync = Dict().tag(sync=True) hist_ylog_value = Bool().tag(sync=True) hist_ylog_sync = Dict().tag(sync=True) hist_n_bin_value = Int().tag(sync=True) hist_n_bin_sync = Dict().tag(sync=True) hist_x_min_value = Float().tag(sync=True) hist_x_min_sync = Dict().tag(sync=True) hist_x_max_value = Float().tag(sync=True) hist_x_max_sync = Dict().tag(sync=True) hist_cumulative_value = Bool().tag(sync=True) hist_cumulative_sync = Dict().tag(sync=True) hist_normalize_value = Bool().tag(sync=True) hist_normalize_sync = Dict().tag(sync=True) hist_update_bins_on_reset_limits_value = Bool().tag(sync=True) hist_update_bins_on_reset_limits_sync = Dict().tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # description displayed under plugin title in tray self._plugin_description = 'Set viewer and layer display options.' self.layer = LayerSelect(self, 'layer_items', 'layer_selected', 'viewer_selected', 'layer_multiselect') self.layer.filters += [is_not_wcs_only, 'has_wcs_if_image_viewer_pixel_linked'] self.swatches_palette = [ ['#FF0000', '#AA0000', '#550000'], ['#FFD300', '#AAAA00', '#555500'], ['#4CFF00', '#00AA00', '#005500'], ['#00FF8E', '#00AAAA', '#005555'], ['#0089FF', '#5200FF', '#000055'] ] def is_profile(state): return isinstance(state, (ProfileViewerState, ProfileLayerState)) def not_profile(state): return not is_profile(state) def is_scatter(state): return isinstance(state, (ScatterViewerState, BqplotScatterLayerState)) def is_histogram(state): return isinstance(state, (HistogramViewerState, BqplotHistogramLayerState)) def is_scatter_or_histogram(state): return is_scatter(state) or is_histogram(state) def supports_line(state): return is_profile(state) or is_scatter(state) def is_image(state): return isinstance(state, BqplotImageLayerState) def not_image(state): return not is_image(state) def not_image_viewer(state): return not isinstance(state, ImageViewerState) def not_image_or_spatial_subset(state): return not is_image(state) and not is_spatial_subset(state) def is_spatial_subset(state): return isinstance(state, ImageSubsetLayerState) and is_not_wcs_only(state.layer) def is_not_subset(state): return not is_spatial_subset(state) def is_sonified(state): return isinstance(state, SonifiedLayerState) def line_visible(state): # exclude for scatter layers where the marker is shown instead of the line return getattr(state, 'line_visible', True) def state_attr_for_line_visible(state): if is_scatter(state): return 'line_visible' return 'visible' # Profile/line viewer/layer options: self.line_visible = PlotOptionsSyncState(self, self.viewer, self.layer, state_attr_for_line_visible, # noqa 'line_visible_value', 'line_visible_sync', state_filter=supports_line) self.line_color = PlotOptionsSyncState(self, self.viewer, self.layer, 'color', 'line_color_value', 'line_color_sync', state_filter=not_image_or_spatial_subset) self.line_width = PlotOptionsSyncState(self, self.viewer, self.layer, 'linewidth', 'line_width_value', 'line_width_sync', state_filter=supports_line) self.line_opacity = PlotOptionsSyncState(self, self.viewer, self.layer, 'alpha', 'line_opacity_value', 'line_opacity_sync', state_filter=supports_line) self.line_as_steps = PlotOptionsSyncState(self, self.viewer, self.layer, 'as_steps', 'line_as_steps_value', 'line_as_steps_sync') self.uncertainty_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'show_uncertainty', # noqa 'uncertainty_visible_value', 'uncertainty_visible_sync') # noqa self.xatt = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_att', 'xatt_value', 'xatt_sync', state_filter=is_scatter_or_histogram) self.yatt = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_att', 'yatt_value', 'yatt_sync', state_filter=is_scatter) # Viewer bounds self.x_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_min', 'x_min_value', 'x_min_sync', state_filter=not_image_viewer) self.x_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_max', 'x_max_value', 'x_max_sync', state_filter=not_image_viewer) self.y_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_min', 'y_min_value', 'y_min_sync', state_filter=not_image_viewer) self.y_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_max', 'y_max_value', 'y_max_sync', state_filter=not_image_viewer) self.zoom_center_x = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_x', 'zoom_center_x_value', 'zoom_center_x_sync') self.zoom_center_y = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_center_y', 'zoom_center_y_value', 'zoom_center_y_sync') self.zoom_radius = PlotOptionsSyncState(self, self.viewer, self.layer, 'zoom_radius', 'zoom_radius_value', 'zoom_radius_sync') # Scatter/marker options: # NOTE: marker_visible hides the entire layer (including the line) self.marker_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'visible', 'marker_visible_value', 'marker_visible_sync', state_filter=is_scatter) self.marker_fill = PlotOptionsSyncState(self, self.viewer, self.layer, 'fill', 'marker_fill_value', 'marker_fill_sync', state_filter=is_scatter) self.marker_opacity = PlotOptionsSyncState(self, self.viewer, self.layer, 'alpha', 'marker_opacity_value', 'marker_opacity_sync', state_filter=is_scatter) self.marker_size_mode = PlotOptionsSyncState(self, self.viewer, self.layer, 'size_mode', 'marker_size_mode_value', 'marker_size_mode_sync', # noqa state_filter=is_scatter) self.marker_size = PlotOptionsSyncState(self, self.viewer, self.layer, 'size', 'marker_size_value', 'marker_size_sync', state_filter=is_scatter) self.marker_size_scale = PlotOptionsSyncState(self, self.viewer, self.layer, 'size_scaling', 'marker_size_scale_value', 'marker_size_scale_sync', # noqa state_filter=is_scatter) self.marker_size_col = PlotOptionsSyncState(self, self.viewer, self.layer, 'size_att', 'marker_size_col_value', 'marker_size_col_sync', state_filter=is_scatter) self.marker_size_vmin = PlotOptionsSyncState(self, self.viewer, self.layer, 'size_vmin', 'marker_size_vmin_value', 'marker_size_vmin_sync', # noqa state_filter=is_scatter) self.marker_size_vmax = PlotOptionsSyncState(self, self.viewer, self.layer, 'size_vmax', 'marker_size_vmax_value', 'marker_size_vmax_sync', # noqa state_filter=is_scatter) # TODO: remove marker_ prefix if these also apply to the lines? self.marker_color_mode = PlotOptionsSyncState(self, self.viewer, self.layer, 'cmap_mode', 'marker_color_mode_value', 'marker_color_mode_sync', # noqa state_filter=is_scatter) self.marker_color = PlotOptionsSyncState(self, self.viewer, self.layer, 'color', 'marker_color_value', 'marker_color_sync', state_filter=is_scatter) self.marker_color_col = PlotOptionsSyncState(self, self.viewer, self.layer, 'cmap_att', 'marker_color_col_value', 'marker_color_col_sync', # noqa state_filter=is_scatter) self.marker_colormap = PlotOptionsSyncState(self, self.viewer, self.layer, 'cmap', 'marker_colormap_value', 'marker_colormap_sync', state_filter=is_scatter) self.marker_colormap_vmin = PlotOptionsSyncState(self, self.viewer, self.layer, 'cmap_vmin', 'marker_colormap_vmin_value', 'marker_colormap_vmin_sync', # noqa state_filter=is_scatter) self.marker_colormap_vmax = PlotOptionsSyncState(self, self.viewer, self.layer, 'cmap_vmax', 'marker_colormap_vmax_value', 'marker_colormap_vmax_sync', # noqa state_filter=is_scatter) # Image viewer/layer options: self.stretch_function = PlotOptionsSyncState(self, self.viewer, self.layer, 'stretch', 'stretch_function_value', 'stretch_function_sync', # noqa state_filter=is_image) # use add_observe to ensure that the glue state syncs with the traitlet choice: self.stretch_function.add_observe('stretch_function_value', self._update_stretch_curve) self.stretch_preset = PlotOptionsSyncState(self, self.viewer, self.layer, 'percentile', 'stretch_preset_value', 'stretch_preset_sync', state_filter=is_image) self.stretch_vmin = PlotOptionsSyncState(self, self.viewer, self.layer, 'v_min', 'stretch_vmin_value', 'stretch_vmin_sync', state_filter=is_image) self.stretch_vmax = PlotOptionsSyncState(self, self.viewer, self.layer, 'v_max', 'stretch_vmax_value', 'stretch_vmax_sync', state_filter=is_image) self.stretch_params = PlotOptionsSyncState(self, self.viewer, self.layer, 'stretch_parameters', # noqa 'stretch_params_value', 'stretch_params_sync', state_filter=is_image) self.subset_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'visible', 'subset_visible_value', 'subset_visible_sync', state_filter=is_spatial_subset) self.subset_color = PlotOptionsSyncState(self, self.viewer, self.layer, 'color', 'subset_color_value', 'subset_color_sync', state_filter=is_spatial_subset) self.subset_opacity = PlotOptionsSyncState(self, self.viewer, self.layer, 'alpha', 'subset_opacity_value', 'subset_opacity_sync', state_filter=is_spatial_subset) self.image_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'bitmap_visible', 'image_visible_value', 'image_visible_sync', state_filter=is_image) self.image_color_mode = PlotOptionsSyncState(self, self.viewer, self.layer, 'color_mode', # noqa 'image_color_mode_value', 'image_color_mode_sync') # noqa self.image_color = PlotOptionsSyncState(self, self.viewer, self.layer, 'color', 'image_color_value', 'image_color_sync', state_filter=is_image) self.image_colormap = PlotOptionsSyncState(self, self.viewer, self.layer, 'cmap', 'image_colormap_value', 'image_colormap_sync') self.image_opacity = PlotOptionsSyncState(self, self.viewer, self.layer, 'alpha', 'image_opacity_value', 'image_opacity_sync', state_filter=is_image) self.image_contrast = PlotOptionsSyncState(self, self.viewer, self.layer, 'contrast', 'image_contrast_value', 'image_contrast_sync') self.image_bias = PlotOptionsSyncState(self, self.viewer, self.layer, 'bias', 'image_bias_value', 'image_bias_sync') self.contour_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'contour_visible', # noqa 'contour_visible_value', 'contour_visible_sync', spinner='contour_spinner') self.contour_mode = PlotOptionsSyncState(self, self.viewer, self.layer, 'level_mode', 'contour_mode_value', 'contour_mode_sync', spinner='contour_spinner') self.contour_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'c_min', 'contour_min_value', 'contour_min_sync', spinner='contour_spinner') self.contour_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'c_max', 'contour_max_value', 'contour_max_sync', spinner='contour_spinner') self.contour_nlevels = PlotOptionsSyncState(self, self.viewer, self.layer, 'n_levels', 'contour_nlevels_value', 'contour_nlevels_sync', spinner='contour_spinner') self.contour_custom_levels = PlotOptionsSyncState(self, self.viewer, self.layer, 'levels', 'contour_custom_levels_value', 'contour_custom_levels_sync', # noqa spinner='contour_spinner') self.volume_level = PlotOptionsSyncState(self, self.viewer, self.layer, 'volume', 'volume_value', 'volume_sync', state_filter=is_sonified) self.sonified_audible = PlotOptionsSyncState(self, self.viewer, self.layer, 'audible', 'sonified_audible_value', 'sonified_audible_sync', state_filter=is_sonified) # Histogram layer options: self.hist_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'visible', 'hist_visible_value', 'hist_visible_sync', state_filter=is_histogram) self.hist_opacity = PlotOptionsSyncState(self, self.viewer, self.layer, 'alpha', 'hist_opacity_value', 'hist_opacity_sync', state_filter=is_histogram) self.hist_color = PlotOptionsSyncState(self, self.viewer, self.layer, 'color', 'hist_color_value', 'hist_color_sync', state_filter=is_histogram) # Axes options: # axes_visible hidden for imviz in plot_options.vue self.axes_visible = PlotOptionsSyncState(self, self.viewer, self.layer, 'show_axes', 'axes_visible_value', 'axes_visible_sync', state_filter=not_profile) sv = self.spectrum_viewer if sv is not None: sv.state.add_callback('x_display_unit', self._on_global_display_unit_changed) sv.state.add_callback('y_display_unit', self._on_global_display_unit_changed) # Histogram axes options: self.hist_xlog = PlotOptionsSyncState(self, self.viewer, self.layer, 'x_log', 'hist_xlog_value', 'hist_xlog_sync', state_filter=is_histogram) self.hist_ylog = PlotOptionsSyncState(self, self.viewer, self.layer, 'y_log', 'hist_ylog_value', 'hist_ylog_sync', state_filter=is_histogram) self.hist_n_bin = PlotOptionsSyncState(self, self.viewer, self.layer, 'hist_n_bin', 'hist_n_bin_value', 'hist_n_bin_sync', state_filter=is_histogram) self.hist_x_min = PlotOptionsSyncState(self, self.viewer, self.layer, 'hist_x_min', 'hist_x_min_value', 'hist_x_min_sync', state_filter=is_histogram) self.hist_x_max = PlotOptionsSyncState(self, self.viewer, self.layer, 'hist_x_max', 'hist_x_max_value', 'hist_x_max_sync', state_filter=is_histogram) self.hist_cumulative = PlotOptionsSyncState(self, self.viewer, self.layer, 'cumulative', 'hist_cumulative_value', 'hist_cumulative_sync', state_filter=is_histogram) self.hist_normalize = PlotOptionsSyncState(self, self.viewer, self.layer, 'normalize', 'hist_normalize_value', 'hist_normalize_sync', state_filter=is_histogram) self.hist_update_bins_on_reset_limits = PlotOptionsSyncState(self, self.viewer, self.layer, 'update_bins_on_reset_limits', # noqa 'hist_update_bins_on_reset_limits_value', # noqa 'hist_update_bins_on_reset_limits_sync', # noqa state_filter=is_histogram) # Add layer callback to image viewers to track active layer for viewer in self.app._viewer_store.values(): viewer.state.add_callback('layers', lambda msg: self._layers_changed(viewer=viewer)) self.hub.subscribe(self, ViewerAddedMessage, handler=self._on_viewer_added) self.hub.subscribe(self, ChangeRefDataMessage, handler=self._on_refdata_change) if self.config == 'deconfigged': self.observe_traitlets_for_relevancy(traitlets_to_observe=['viewer_items']) @property def user_api(self): expose = ['multiselect', 'viewer', 'viewer_multiselect', 'layer', 'layer_multiselect', 'select_all', 'subset_visible', 'reset_viewer_bounds'] if self.config == "cubeviz": expose += ['uncertainty_visible', 'volume_level', 'sonified_audible'] if self.config != "imviz": expose += ['x_min', 'x_max', 'y_min', 'y_max', 'axes_visible', 'line_visible', 'line_color', 'line_width', 'line_opacity', 'line_as_steps', 'uncertainty_visible'] if self.config != "specviz": expose += ['zoom_center_x', 'zoom_center_y', 'zoom_radius', 'subset_color', 'subset_opacity', 'stretch_function', 'stretch_preset', 'stretch_vmin', 'stretch_vmax', 'stretch_hist_zoom_limits', 'stretch_hist_nbins', 'image_visible', 'image_color_mode', 'image_color', 'image_colormap', 'image_opacity', 'image_contrast', 'image_bias', 'contour_visible', 'contour_mode', 'contour_min', 'contour_max', 'contour_nlevels', 'contour_custom_levels', 'stretch_curve_visible', 'apply_RGB_presets'] if self.config == 'deconfigged': expose += ['xatt', 'yatt', 'hist_visible', 'hist_color', 'hist_opacity', 'hist_xlog', 'hist_ylog', 'hist_n_bin', 'hist_x_min', 'hist_x_max', 'hist_cumulative', 'hist_normalize', 'hist_update_bins_on_reset_limits'] if self.config in ('imviz', 'deconfigged'): expose += ['marker_visible', 'marker_fill', 'marker_opacity', 'marker_size', 'marker_size_scale', 'marker_color_mode', 'marker_color', 'marker_color_col', 'marker_colormap', 'marker_colormap_vmin', 'marker_colormap_vmax'] return PluginUserApi(self, expose) @property def multiselect(self): warnings.warn( "multiselect has been replaced by separate viewer_multiselect and " "layer_multiselect and will be removed in the future. " "This currently evaluates viewer_multiselect or layer_multiselect", DeprecationWarning) return self.viewer_multiselect or self.layer_multiselect @multiselect.setter def multiselect(self, value): warnings.warn( "multiselect has been replaced by separate viewer_multiselect and " "layer_multiselect and will be removed in the future. " "This currently sets viewer_multiselect and layer_multiselect", DeprecationWarning) self.viewer_multiselect = value self.layer_multiselect = value @cached_property def stretch_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. stretch_histogram.tools_nested.append(["jdaviz:stretch_bounds"]) stretch_histogram._initialize_toolbar(["jdaviz:stretch_bounds"]) stretch_histogram._add_data('histogram', x=[0, 1]) stretch_histogram.add_line('vmin', x=[0, 0], y=[0, 1], ynorm=True, color='#c75d2c') stretch_histogram.add_line('vmax', x=[0, 0], y=[0, 1], ynorm='vmin', color='#c75d2c') stretch_histogram.add_line( label='stretch_curve', x=[], y=[], ynorm='vmin', color="#007BA1", # "inactive" blue opacities=[0.5], ) stretch_histogram.add_scatter( label='stretch_knots', x=[], y=[], ynorm='vmin', color="#c75d2c", # "active" orange (tool enabled by default) ) stretch_histogram.add_scatter('colorbar', x=[], y=[], ynorm='vmin', marker='square', stroke_width=33) # noqa: E501 stretch_histogram.viewer.state.update_bins_on_reset_limits = False stretch_histogram.viewer.state.x_limits_percentile = 95 with stretch_histogram.figure.hold_sync(): stretch_histogram.figure.axes[0].label = 'pixel value' stretch_histogram.figure.axes[0].num_ticks = 3 stretch_histogram.figure.axes[0].tick_format = '0.1e' stretch_histogram.figure.axes[1].label = 'density' stretch_histogram.figure.axes[1].num_ticks = 2 self.stretch_histogram_widget = f'IPY_MODEL_{stretch_histogram.model_id}' self.send_state('stretch_histogram_widget') return stretch_histogram
[docs] def select_all(self, viewers=True, layers=True): """ Enable multiselect mode and select all viewers and/or layers. Parameters ---------- viewers : bool Whether to set ``viewer_multiselect`` and select all viewers (default: True) layers: bool Whether to set ``layer_multiselect`` and select all layers (default: True) """ if viewers: self.viewer_multiselect = True self.viewer.select_all() if layers: self.layer_multiselect = True self.layer.select_all()
def _on_global_display_unit_changed(self, *args): sv = self.spectrum_viewer self.display_units['spectral'] = sv.state.x_display_unit self.display_units['flux'] = sv.state.y_display_unit self.send_state('display_units') def _on_refdata_change(self, *args): if self.app._align_by.lower() == 'wcs': self.display_units['image'] = 'deg' else: self.display_units['image'] = 'pix' self.send_state('display_units') self._update_viewer_zoom_steps() def _on_viewer_added(self, msg): viewer = self.app.get_viewer_by_id(msg.viewer_id) viewer.state.add_callback('layers', lambda msg: self._layers_changed(viewer=viewer)) @observe('viewer_selected') def _layers_changed(self, msg=None, viewer=None): # We need msg first in the keyword arguments to catch the msg value from the observe, # even though we don't end up using it if self.viewer_multiselect or not hasattr(self, 'viewer'): self.active_layer = "" return if viewer is None: viewer = self.viewer.selected_obj if viewer is self.viewer.selected_obj and self._viewer_is_image_viewer(): # noqa if viewer.active_image_layer is None: self.active_layer = "" return self.active_layer = viewer.active_image_layer.layer.label
[docs] def vue_unmix_state(self, names): if isinstance(names, str): names = [names] for name in names: sync_state = getattr(self, name) sync_state.unmix_state() if 'stretch_params' in names: # there is no way to call send_state to force the update to the layers, # so we'll force an update by clearing first stretch_params = dict(self.stretch_params_value) self.stretch_params_value = {} self.stretch_params_value = stretch_params
[docs] def vue_set_value(self, data): attr_name = data.get('name') value = data.get('value') setattr(self, attr_name, value)
[docs] @with_spinner('apply_RGB_presets_spinner') def apply_RGB_presets(self): """ Applies preset colors, opacities, and stretch settings to all visible layers (in all viewers) when in Color (Monochromatic) mode. """ if (self.image_color_mode_value != "One color per layer" or self.image_color_mode_sync['mixed']): raise ValueError("RGB presets can only be applied if color mode is Color.") # Preselected colors we want to use for 5 or less layers preset_colors = [self.swatches_palette[4][1], "#0000FF", "#00FF00", self.swatches_palette[1][0], self.swatches_palette[0][0], ] preset_inds = {2: [1, 4], 3: [1, 2, 4], 4: [1, 2, 3, 4]} # Switch back to this at the end initial_layer = self.layer_selected # Determine layers visible in selected viewer(s) - consider mixed to be visible visible_layers = [layer['label'] for layer in self.layer.items if not layer['is_subset'] and (layer['visible'] in (True, 'mixed'))] # noqa # Set opacity to something that seems sensible n_visible = len(visible_layers) default_opacity = 1 if n_visible > 2: default_opacity = 1 / math.log2(n_visible) # Sample along a colormap if we have too many layers if n_visible > len(preset_colors): cmap = matplotlib.colormaps['gist_rainbow'].resampled(n_visible) # Have to reverse the order of the cmap to make physical sense with # assumed wavelength order of layers. preset_colors = [matplotlib.colors.to_hex(cmap(i), keep_alpha=True) for i in range(n_visible - 1, -1, -1)] elif n_visible >= 2 and n_visible < len(preset_colors): preset_colors = [preset_colors[i] for i in preset_inds[n_visible]] for i in range(n_visible): self.layer_selected = visible_layers[i] self.image_opacity.unmix_state(default_opacity) self.image_color.unmix_state(preset_colors[i]) self.stretch_function.unmix_state("arcsinh") self.stretch_preset.unmix_state(99) self.layer_selected = initial_layer
[docs] def vue_apply_RGB_presets(self, data): self.apply_RGB_presets()
@observe('viewer_selected', 'x_min_value', 'x_max_value', 'y_min_value', 'y_max_value') def _update_viewer_bound_steps(self, msg={}): if not hasattr(self, 'viewer'): # pragma: no cover # plugin hasn't been fully initialized yet return if not self.viewer.selected or not self.x_min_sync['in_subscribed_states']: # nothing selected yet return for ax in ('x', 'y'): ax_min = getattr(self, f'{ax}_min_value') ax_max = getattr(self, f'{ax}_max_value') bound_step, decimals = _round_step((ax_max - ax_min) / 100.) decimals = -int(np.log10(abs(bound_step))) + 1 if bound_step != 0 else 6 setattr(self, f'{ax}_bound_step', bound_step) setattr(self, f'{ax}_min_value', np.round(ax_min, decimals=decimals)) setattr(self, f'{ax}_max_value', np.round(ax_max, decimals=decimals)) @observe('viewer_selected', 'zoom_center_x_value', 'zoom_center_y_value', 'zoom_radius_value') def _update_viewer_zoom_steps(self, msg={}): if not hasattr(self, 'viewer'): # pragma: no cover # plugin hasn't been fully initialized yet return if not self.viewer.selected or not self.zoom_radius_sync['in_subscribed_states']: # nothing selected yet return # in the case of multiple viewers, calculate based on the first # alternatively, we could find the most extreme by looping over all selected viewers viewers = self.viewer.selected_obj if self.viewer_multiselect else [self.viewer.selected_obj] # noqa for viewer in viewers: if hasattr(viewer.state, '_get_reset_limits'): break else: # no image viewer return x_min, x_max, y_min, y_max = viewer.state._get_reset_limits(return_as_world=True) self.zoom_step, _ = _round_step(max(x_max-x_min, y_max-y_min) / 100.)
[docs] def vue_reset_viewer_bounds(self, _): self.reset_viewer_bounds()
[docs] def reset_viewer_bounds(self): # This button is currently only exposed if only the spectrum viewer is selected viewers = [self.viewer.selected_obj] if not self.viewer_multiselect else self.viewer.selected_obj # noqa for viewer in viewers: viewer.toolbar.tools['jdaviz:homezoom'].activate()
@observe('stretch_function_sync', 'stretch_params_sync', 'stretch_vmin_sync', 'stretch_vmax_sync', 'image_color_mode_sync', 'image_color_sync', 'image_colormap_sync') def _update_stretch_hist_sync(self, msg={}): # the histogram should show as mixed if ANY of the input parameters are mixed # these should match in the @observe above, all_syncs here, as well as the strings # passed to unmix_state in the <glue-state-sync-wrapper> in plot_options.vue all_syncs = [self.stretch_function_sync, self.stretch_params_sync, self.stretch_vmin_sync, self.stretch_vmax_sync, self.image_color_mode_sync, self.image_color_sync, self.image_colormap_sync] self.stretch_hist_sync = {'in_subscribed_states': bool(np.any([sync.get('in_subscribed_states', False) for sync in all_syncs])), # noqa 'mixed': bool(np.any([sync.get('mixed', False) for sync in all_syncs]))} # noqa @observe('is_active', 'layer_selected', 'viewer_selected', 'stretch_hist_zoom_limits') @skip_if_no_updates_since_last_active() def _request_update_stretch_histogram(self, msg={}): if not hasattr(self, 'viewer'): # pragma: no cover # plugin hasn't been fully initialized yet return # 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 (and can therefore # be executed even if the plugin is not active) self._update_stretch_histogram(msg) def _zoom_limits_update_stretch_histogram(self, msg={}): if not self.stretch_hist_zoom_limits: # there isn't anything to update, let's not waste resources return self._update_stretch_histogram() @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 if not self.viewer.selected or not self.layer.selected: # pragma: no cover # nothing to plot, will be hidden in UI return if self.layer_multiselect and len(self.layer.selected) > 1: # currently only support single-layer, if multiple layers are selected, the plot # will be hidden in the UI return if not self._viewer_is_image_viewer() or not self._layer_is_image_layer(): # don't update histogram if selected viewer is not an image viewer: return viewer = self.viewer.selected_obj[0] if self.viewer_multiselect else self.viewer.selected_obj # noqa # manage viewer zoom limit callbacks if ((isinstance(msg, dict) and msg.get('name') == 'viewer_selected') or not self.stretch_hist_zoom_limits): vs = viewer.state for attr in ('x_min', 'x_max', 'y_min', 'y_max'): vs.add_callback(attr, self._zoom_limits_update_stretch_histogram) if isinstance(msg, dict) and msg.get('name') == 'viewer_selected': viewer_label_old = msg.get('old') if isinstance(viewer_label_old, list): viewer_label_old = viewer_label_old[0] # If the previously selected viewer was deleted, we don't need to do this. if viewer_label_old in self.app._viewer_store: vs_old = self.app.get_viewer(viewer_label_old).state for attr in ('x_min', 'x_max', 'y_min', 'y_max'): vs_old.remove_callback(attr, self._zoom_limits_update_stretch_histogram) if not len(self.layer.selected_obj): # skip further updates if no data are available: return if isinstance(self.layer.selected_obj[0], list): if not len(self.layer.selected_obj[0]): return # multiselect case (but we won't check multiselect since the selection can lag behind) layer = self.layer.selected_obj[0][0] else: layer = self.layer.selected_obj[0] data = layer.layer if isinstance(data, GroupedSubset): # don't update histogram for subsets: return comp = data.get_component(layer.state.attribute) # TODO: further optimization could be done by caching sub_data if self.stretch_hist_zoom_limits and (not self.layer_multiselect or len(self.layer_selected) == 1): # noqa if hasattr(viewer, '_get_zoom_limits'): # Viewer limits. This takes account of Imviz linking. xy_limits = viewer._get_zoom_limits(data).astype(int) x_limits = xy_limits[:, 0] y_limits = xy_limits[:, 1] x_min = max(x_limits.min(), 0) x_max = x_limits.max() y_min = max(y_limits.min(), 0) y_max = y_limits.max() sub_data = comp.data[y_min:y_max, x_min:x_max] else: # spectrum-2d-viewer, for example. We'll assume the viewer # limits correspond to the fixed data components from glue # and filter directly. x_data = data.get_component(data.components[1]).data y_data = data.get_component(data.components[0]).data inverted_x = getattr(viewer, 'inverted_x_axis', False) x_min = viewer.state.x_min if not inverted_x else viewer.state.x_max x_max = viewer.state.x_max if not inverted_x else viewer.state.x_min inds = np.where((x_data >= x_min) & (x_data <= x_max) & (y_data >= viewer.state.y_min) & (y_data <= viewer.state.y_max)) sub_data = comp.data[inds] else: # include all data, regardless of zoom limits sub_data = comp.data self.stretch_histogram.viewer.state.random_subset = RANDOM_SUBSET_SIZE self.stretch_histogram._update_data('histogram', x=sub_data) if len(sub_data) > 0: # Use glue to compute the statistics since this allows us to use # a random subset of the data to compute the histogram. # The 2.5 and 97.5 hardcoded here is equivalent to # PercentileInterval(95).get_limits(sub_data) glue_data = self.stretch_histogram.app.data_collection['histogram'] hist_lims = ( glue_data.compute_statistic('percentile', glue_data.id['x'], percentile=2.5, random_subset=RANDOM_SUBSET_SIZE), glue_data.compute_statistic('percentile', glue_data.id['x'], percentile=97.5, random_subset=RANDOM_SUBSET_SIZE) ) # set the stepsize for vmin/vmax to be approximately 1% of the range of the # histogram (within the percentile interval), rounded to 1-2 significant digits # to avoid random step sizes. This logic is somewhat arbitrary and can be safely # modified or eventually exposed to the user if that would be useful. stretch_vstep = (hist_lims[1] - hist_lims[0]) / 100. self.stretch_vstep = _round_step(stretch_vstep)[0] with delay_callback(self.stretch_histogram.viewer.state, 'hist_x_min', 'hist_x_max'): self.stretch_histogram.viewer.state.hist_x_min = hist_lims[0] self.stretch_histogram.viewer.state.hist_x_max = hist_lims[1] self.stretch_histogram.figure.title = f"{len(sub_data)} pixels" # update the n_bins since this may be a new layer self._histogram_nbins_changed() # update the curve/colorbar self._update_stretch_curve(msg) @observe('image_color_mode_value', 'image_color_value', 'image_colormap_value', 'image_contrast_value', 'image_bias_value', 'stretch_hist_nbins', 'stretch_curve_visible', 'stretch_function_value', 'stretch_vmin_value', 'stretch_vmax_value', 'stretch_params_value', 'stretch_preset_value', 'layer_multiselect' ) @skip_if_no_updates_since_last_active() def _update_stretch_curve(self, msg=None): if not self._viewer_is_image_viewer() or not hasattr(self, 'stretch_histogram'): # don't update histogram if selected viewer is not an image viewer, # or the stretch histogram hasn't been initialized: return if self.layer_multiselect and len(self.layer.selected) > 1: # currently only support single-layer, if multiple layers are selected, the plot # will be hidden in the UI return # could be multi or single-viewer and/or multi-layer with a single entry, # either way, we act on the first entry layer = self.layer.selected_obj[0] while isinstance(layer, list): if not len(layer): return layer = layer[0] if isinstance(layer.layer, GroupedSubset): # don't update histogram for subsets, will be hidden in UI return # create the new/updated stretch curve following the colormapping # procedure in glue's CompositeArray: interval = ManualInterval(self.stretch_vmin_value, self.stretch_vmax_value) contrast_bias = ContrastBiasStretch(self.image_contrast_value, self.image_bias_value) stretch = layer.state.stretch_object layer_cmap = layer.state.cmap # show the colorbar color_mode = self.image_color_mode_value # NOTE: Index 0 in marks is assumed to be the bin centers. x = self.stretch_histogram.figure.marks[0].x y = np.ones_like(x) # Copied from the __call__ internals of glue/viewers/image/composite_array.py data = interval(x) data = contrast_bias(data, out=data) data = stretch(data, out=data) if color_mode == 'Colormaps': cmap = glue_colormaps[self.image_colormap.text] if hasattr(cmap, "get_bad"): bad_color = cmap.get_bad().tolist()[:3] layer_cmap = cmap.with_extremes(bad=bad_color + [self.image_opacity_value]) else: layer_cmap = cmap # Compute colormapped image plane = layer_cmap(data) else: # Color (Monochromatic) if self.image_color_value is None: # do not crash if image_color_value is not yet assigned, # _update_stretch_curve observes image_color_value so will get called again return False # Get color color = COLOR_CONVERTER.to_rgba_array(self.image_color_value)[0] plane = data[:, np.newaxis] * color plane[:, 3] = 1 plane = np.clip(plane, 0, 1, out=plane) ipycolors = [matplotlib.colors.rgb2hex(p, keep_alpha=False) for p in plane] colorbar_mark = self.stretch_histogram.marks['colorbar'] colorbar_mark.x = x colorbar_mark.y = y colorbar_mark.colors = ipycolors # show "knot" locations if the stretch_function is a spline if isinstance(stretch, SplineStretch) and self.stretch_curve_visible: knot_mark = self.stretch_histogram.marks['stretch_knots'] knot_mark.x = (self.stretch_vmin_value + np.asarray(stretch._x) * (self.stretch_vmax_value - self.stretch_vmin_value)) # noqa # scale to 0.9 so always falls below colorbar (same as for stretch_curve) knot_mark.y = 0.9 * np.asarray(stretch._y) else: self.stretch_histogram.clear_marks('stretch_knots') if self.stretch_curve_visible: # create a photoshop style "curve" for the stretch function curve_x = np.linspace(self.stretch_vmin_value, self.stretch_vmax_value, 50) curve_y = interval(curve_x) curve_y = contrast_bias(curve_y) curve_y = stretch(curve_y) curve_mark = self.stretch_histogram.marks['stretch_curve'] curve_mark.x = curve_x curve_mark.y = 0.9 * curve_y else: self.stretch_histogram.clear_marks('stretch_curve') self.stretch_histogram._refresh_marks() @observe('stretch_vmin_value') def _stretch_vmin_changed(self, msg=None): self.stretch_histogram.marks['vmin'].x = [self.stretch_vmin_value, self.stretch_vmin_value] @observe('stretch_vmax_value') def _stretch_vmax_changed(self, msg=None): self.stretch_histogram.marks['vmax'].x = [self.stretch_vmax_value, self.stretch_vmax_value] @observe("stretch_hist_nbins") def _histogram_nbins_changed(self, msg={}): if self.stretch_histogram is None: return if self.stretch_hist_nbins == '' or self.stretch_hist_nbins < 1: return self.stretch_histogram.viewer.state.hist_n_bin = self.stretch_hist_nbins # for some reason, this resets the internal marks, so we need to ensure the manual # marks are still plotted self.stretch_histogram._refresh_marks()
[docs] def set_histogram_limits(self, x_min=None, x_max=None, y_min=None, y_max=None): # NOTE: leaving this out of user API until API is finalized with interactive setting self.stretch_histogram.set_limits(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max)
def _viewer_is_image_viewer(self): # Import here to prevent circular import (and not at the top of the method so the import # check is avoided, whenever possible). from jdaviz.configs.imviz.plugins.viewers import ImvizImageView from jdaviz.configs.cubeviz.plugins.viewers import CubevizImageView from jdaviz.configs.mosviz.plugins.viewers import MosvizImageView, MosvizProfile2DView from jdaviz.configs.rampviz.plugins.viewers import RampvizImageView def _is_image_viewer(viewer): return isinstance(viewer, (ImvizImageView, CubevizImageView, MosvizImageView, MosvizProfile2DView, RampvizImageView)) viewers = self.viewer.selected_obj if not isinstance(viewers, list): viewers = [viewers] return np.all([_is_image_viewer(viewer) for viewer in viewers]) def _layer_is_image_layer(self): from glue_jupyter.bqplot.image.state import BqplotImageLayerState return np.all([isinstance(lyr.state, BqplotImageLayerState) for lyr in self.layer.selected_obj_flattened])
[docs] def image_segmentation_map_presets(self, *args, **kwargs): # if 'Random' colormap is used for visualizing image segmentation, # ensure the stretch limits are the min and max, the stretch function # is linear, the contrast is 1.0, and the bias is 0.5. This ensures # that all label colors are unique: if self.image_colormap_value != 'Random': return self.stretch_preset.value = 100 self.stretch_function.value = 'linear' self.image_contrast_value = 1 self.image_bias_value = 0.5
[docs] def vue_image_segmentation_map_presets(self, *args, **kwargs): self.image_segmentation_map_presets(*args, **kwargs)