From d9cacaf66e01eaa21d8ad2e0a9072f67f46b07fe Mon Sep 17 00:00:00 2001 From: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:53:18 -0400 Subject: [PATCH] Add stretch bounds tool to plot option histogram viewer (#2513) * Add stretch bounds tool to plot option histogram viewer * Add changelog, test * Change color based on tool active state * Add short sleep to test to avoid throttle * stretch function curve as inactive blue --------- Co-authored-by: Kyle Conroy --- CHANGES.rst | 2 + .../plugins/plot_options/plot_options.py | 9 +++- jdaviz/core/template_mixin.py | 3 ++ jdaviz/core/tests/test_tools.py | 28 +++++++++++++ jdaviz/core/tools.py | 42 +++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 989fdbb742..187cea1255 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,8 @@ New Features - Plots in plugins now include basic zoom/pan tools for Plot Options, Imviz Line Profiles, and Imviz's aperture photometry. [#2498] +- Histogram plot in Plot Options now includes tool to set stretch vmin and vmax. [#2513] + Cubeviz ^^^^^^^ diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index 1973ca4718..ae40a8c243 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -15,6 +15,7 @@ from glue_jupyter.bqplot.image.state import BqplotImageLayerState from glue_jupyter.common.toolbar_vuetify import read_icon +from jdaviz.components.toolbar_nested import NestedJupyterToolbar from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelect, LayerSelect, PlotOptionsSyncState, Plot, @@ -398,6 +399,12 @@ def state_attr_for_line_visible(state): state_filter=is_image) self.stretch_histogram = Plot(self, 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.toolbar = NestedJupyterToolbar(self.stretch_histogram.viewer, + self.stretch_histogram.tools_nested, + ["jdaviz:stretch_bounds"]) + # NOTE: this is a current workaround so the histogram viewer doesn't crash when replacing # data. Note also that passing x=[0] fails on SOME machines, so we'll pass [0, 1] instead self.stretch_histogram._add_data('ref', x=[0, 1]) @@ -709,7 +716,7 @@ def _update_stretch_curve(self, msg=None): x=curve_x, y=curve_y, ynorm=True, - color='#c75d2c', + color="#007BA1", # "inactive" blue opacities=[0.5], ) diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index 8d53cb70d9..29758dd692 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -3280,7 +3280,9 @@ def __init__(self, plugin, viewer_type='scatter', app=None, *args, **kwargs): app = jglue() self._app = app + self._plugin = plugin self.viewer = app.new_data_viewer(viewer_type, show=False) + self.viewer._plugin = plugin self._viewer_type = viewer_type if viewer_type == 'histogram': self._viewer_components = ('x',) @@ -3422,6 +3424,7 @@ def _add_mark(self, cls, label, xnorm=False, ynorm=False, **kwargs): raise ValueError(f"mark with label '{label}' already exists") mark = cls(scales={'x': bqplot.LinearScale() if xnorm else self.figure.axes[0].scale, 'y': bqplot.LinearScale() if ynorm else self.figure.axes[1].scale}, + labels=[label], **kwargs) self.figure.marks = self.figure.marks + [mark] self._marks[label] = mark diff --git a/jdaviz/core/tests/test_tools.py b/jdaviz/core/tests/test_tools.py index 81f4400276..48109d1347 100644 --- a/jdaviz/core/tests/test_tools.py +++ b/jdaviz/core/tests/test_tools.py @@ -1,3 +1,6 @@ +import time + +import numpy as np from numpy.testing import assert_allclose @@ -41,3 +44,28 @@ def test_rangezoom(specviz_helper, spectrum1d): t.interact.selected = [14, 15] t.on_update_zoom() assert_allclose(_get_lims(sv), [6500, 7000, 14, 15]) + + +def test_stretch_bounds(imviz_helper): + imviz_helper.load_data(np.ones((2, 2))) + + plot_options = imviz_helper.plugins['Plot Options']._obj + stretch_tool = plot_options.stretch_histogram.toolbar.tools["jdaviz:stretch_bounds"] + plot_options.stretch_histogram.toolbar.active_tool = stretch_tool + + min_msg = {'event': 'click', 'pixel': {'x': 40, 'y': 322}, + 'domain': {'x': 0.1, 'y': 342}, + 'button': 0, 'altKey': False, 'ctrlKey': False, 'metaKey': False} + + max_msg = {'event': 'click', 'pixel': {'x': 40, 'y': 322}, + 'domain': {'x': 1.3, 'y': 342}, + 'button': 0, 'altKey': False, 'ctrlKey': False, 'metaKey': False} + + stretch_tool.on_mouse_event(min_msg) + time.sleep(0.3) + stretch_tool.on_mouse_event(max_msg) + + assert plot_options.stretch_vmin_value == 0.1 + assert plot_options.stretch_vmax_value == 1.3 + + plot_options.stretch_histogram.toolbar.active_tool = None diff --git a/jdaviz/core/tools.py b/jdaviz/core/tools.py index 2d812a6242..60500e2bc0 100644 --- a/jdaviz/core/tools.py +++ b/jdaviz/core/tools.py @@ -1,4 +1,5 @@ import os +import time import numpy as np from echo import delay_callback @@ -338,6 +339,47 @@ def is_visible(self): return len([m for m in self.viewer.figure.marks if isinstance(m, SpectralLine)]) > 0 +@viewer_tool +class StretchBounds(CheckableTool): + icon = os.path.join(ICON_DIR, 'line_select.svg') + tool_id = 'jdaviz:stretch_bounds' + action_text = 'Set Stretch VMin and VMax' + tool_tip = 'Set closest stretch bound (VMin/VMax) with click or click+drag' + + def __init__(self, viewer, **kwargs): + self._time_last = 0 + super().__init__(viewer, **kwargs) + + def activate(self): + self.viewer.add_event_callback(self.on_mouse_event, + events=['dragmove', 'click']) + for mark in self.viewer.figure.marks: + if np.any([x in mark.labels for x in ('vmin', 'vmax')]): + mark.colors = ["#c75d2c"] + + def deactivate(self): + self.viewer.remove_event_callback(self.on_mouse_event) + for mark in self.viewer.figure.marks: + if np.any([x in mark.labels for x in ('vmin', 'vmax')]): + mark.colors = ["#007BA1"] + + def on_mouse_event(self, data): + if (time.time() - self._time_last) <= 0.05: + # throttle to 200ms + return + + event_x = data['domain']['x'] + current_bounds = [self.viewer._plugin.stretch_vmin_value, + self.viewer._plugin.stretch_vmax_value,] + att_names = ["stretch_vmin_value", "stretch_vmax_value"] + closest_bound_ind = np.argmin([abs(current_bounds[0] - event_x), + abs(current_bounds[1] - event_x)]) + + setattr(self.viewer._plugin, att_names[closest_bound_ind], event_x) + + self._time_last = time.time() + + class _BaseSidebarShortcut(Tool): plugin_name = None # define in subclass viewer_attr = 'viewer'