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

import logging
import math
import os
import matplotlib
import numpy as np

from astropy.visualization import (
    ManualInterval, ContrastBiasStretch, PercentileInterval
)
from echo import delay_callback
from traitlets import Any, Dict, Float, Bool, Int, List, Unicode, observe

from glue.core.subset_group import GroupedSubset
from glue.config import colormaps, stretches
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 jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelect, LayerSelect,
                                        PlotOptionsSyncState, Plot,
                                        skip_if_no_updates_since_last_active, with_spinner)
from jdaviz.core.events import ChangeRefDataMessage
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.tools import ICON_DIR
from jdaviz.core.custom_traitlets import IntHandleEmpty
from jdaviz.utils import is_not_wcs_only


from scipy.interpolate import PchipInterpolator
from photutils.utils import make_random_cmap

__all__ = ['PlotOptions']


def _register_random_cmap(
    cmap_name,
    bkg_color=[0, 0, 0],
    bkg_alpha=1,
    seed=42,
    ncolors=10_000
):
    """
    Custom random colormap, useful for rendering image
    segmentation maps. The default background for
    `label==0` is *transparent*. If the segmentation map
    contains more than 10,000 labels, adjust the `ncolors`
    kwarg to ensure uniqueness.
    """
    cmap = make_random_cmap(ncolors=ncolors, seed=seed)
    cmap.colors[0] = bkg_color + [bkg_alpha]
    cmap.name = cmap_name
    colormaps.add(cmap_name, cmap)


_register_random_cmap('Random', bkg_alpha=1)


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 stretches:
    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 np.round(step, decimals), decimals


[docs] @tray_registry('g-plot-options', label="Plot Options") class PlotOptions(PluginTemplateMixin): """ 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 * ``collapse_function`` (:class:`~jdaviz.core.template_mixin.PlotOptionsSyncState`): only exposed for Cubeviz * ``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 "Monochromatic". * ``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". """ 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_multiselect = Bool(False).tag(sync=True) viewer_items = List().tag(sync=True) viewer_selected = Any().tag(sync=True) # Any needed for multiselect 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 # profile/line viewer/layer options: line_visible_value = Bool().tag(sync=True) line_visible_sync = Dict().tag(sync=True) collapse_func_value = Unicode().tag(sync=True) collapse_func_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 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 show_viewer_labels = Bool(True).tag(sync=True) cmap_samples = Dict().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) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.viewer = ViewerSelect(self, 'viewer_items', 'viewer_selected', 'viewer_multiselect') self.layer = LayerSelect(self, 'layer_items', 'layer_selected', 'viewer_selected', 'layer_multiselect') self.layer.filters += [is_not_wcs_only] 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 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 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.collapse_function = PlotOptionsSyncState(self, self.viewer, self.layer, 'function', 'collapse_func_value', 'collapse_func_sync') 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 # 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.stretch_histogram = Plot(self, name='stretch_hist', viewer_type='histogram') # Add the stretch bounds tool to the default Plot viewer. self.stretch_histogram.tools_nested.append(["jdaviz:stretch_bounds"]) self.stretch_histogram._initialize_toolbar(["jdaviz:stretch_bounds"]) self.stretch_histogram._add_data('histogram', x=[0, 1]) self.stretch_histogram.add_line('vmin', x=[0, 0], y=[0, 1], ynorm=True, color='#c75d2c') self.stretch_histogram.add_line('vmax', x=[0, 0], y=[0, 1], ynorm='vmin', color='#c75d2c') self.stretch_histogram.add_line( label='stretch_curve', x=[], y=[], ynorm='vmin', color="#007BA1", # "inactive" blue opacities=[0.5], ) self.stretch_histogram.add_scatter( label='stretch_knots', x=[], y=[], ynorm='vmin', color="#c75d2c", # "active" orange (tool enabled by default) ) self.stretch_histogram.add_scatter('colorbar', x=[], y=[], ynorm='vmin', marker='square', stroke_width=33) # noqa: E501 self.stretch_histogram.viewer.state.update_bins_on_reset_limits = False self.stretch_histogram.viewer.state.x_limits_percentile = 95 with self.stretch_histogram.figure.hold_sync(): self.stretch_histogram.figure.axes[0].label = 'pixel value' self.stretch_histogram.figure.axes[0].num_ticks = 3 self.stretch_histogram.figure.axes[0].tick_format = '0.1e' self.stretch_histogram.figure.axes[1].label = 'density' self.stretch_histogram.figure.axes[1].num_ticks = 2 self.stretch_histogram_widget = f'IPY_MODEL_{self.stretch_histogram.model_id}' 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') # 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) self.show_viewer_labels = self.app.state.settings['viewer_labels'] self.app.state.add_callback('settings', self._on_app_settings_changed) 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) self.hub.subscribe(self, ChangeRefDataMessage, handler=self._on_refdata_change) # give UI access to sampled version of the available colormap choices def hex_for_cmap(cmap): N = 50 cm_sampled = cmap.resampled(N) return [matplotlib.colors.to_hex(cm_sampled(i)) for i in range(N)] self.cmap_samples = {cmap[1].name: hex_for_cmap(cmap[1]) for cmap in colormaps.members} @property def user_api(self): expose = ['multiselect', 'viewer', 'viewer_multiselect', 'layer', 'layer_multiselect', 'select_all', 'subset_visible'] if self.config == "cubeviz": expose += ['collapse_function', 'uncertainty_visible'] 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'] return PluginUserApi(self, expose) @observe('show_viewer_labels') def _on_show_viewer_labels_changed(self, event): self.app.state.settings['viewer_labels'] = event['new'] def _on_app_settings_changed(self, value): self.show_viewer_labels = value['viewer_labels'] @property def multiselect(self): logging.warning(f"DeprecationWarning: 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") # noqa return self.viewer_multiselect or self.layer_multiselect @multiselect.setter def multiselect(self, value): logging.warning(f"DeprecationWarning: 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") # noqa self.viewer_multiselect = value self.layer_multiselect = value
[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._link_type.lower() == 'wcs': self.display_units['image'] = 'deg' else: self.display_units['image'] = 'pix' self.send_state('display_units') self._update_viewer_zoom_steps()
[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)
@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 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 Monochromatic.") # 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, _): # 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() @with_spinner('stretch_hist_spinner') def _update_stretch_histogram(self, msg={}): if not hasattr(self, 'viewer'): # pragma: no cover # plugin hasn't been fully initialized yet return if not isinstance(msg, dict): # pragma: no cover # then this is from the limits callbacks # IMPORTANT: this assumes the only non-observe callback to this method comes # from state callbacks from zoom limits. if not self.stretch_hist_zoom_limits: # there isn't anything to update, let's not waste resources return # override msg as an empty dict so that the rest of the logic doesn't have to check # its type 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(): # 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._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._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() arr = comp.data[y_min:y_max, x_min:x_max] if self.config == "imviz": # Downsample input data to about 400px (as per compass.vue) for performance. xstep = max(1, round(arr.shape[1] / 400)) ystep = max(1, round(arr.shape[0] / 400)) arr = arr[::ystep, ::xstep] sub_data = arr.ravel() 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].ravel() else: if self.config == "imviz": # Downsample input data to about 400px (as per compass.vue) for performance. xstep = max(1, round(data.shape[1] / 400)) ystep = max(1, round(data.shape[0] / 400)) arr = comp[::ystep, ::xstep] else: # include all data, regardless of zoom limits arr = comp.data sub_data = arr.ravel() # filter out nans (or else bqplot will fail) if np.any(np.isnan(sub_data)): sub_data = sub_data[~np.isnan(sub_data)] self.stretch_histogram._update_data('histogram', x=sub_data) if len(sub_data) > 0: interval = PercentileInterval(95) hist_lims = interval.get_limits(sub_data) # 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 = 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: # Monochromatic # 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 def _is_image_viewer(viewer): return isinstance(viewer, (ImvizImageView, CubevizImageView, MosvizImageView, MosvizProfile2DView)) viewers = self.viewer.selected_obj if not isinstance(viewers, list): viewers = [viewers] return np.all([_is_image_viewer(viewer) for viewer in viewers])
[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)