From 8905ea1e1bacdb14091bb9b9c799552db535778d Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 22 Mar 2024 10:31:10 -0400 Subject: [PATCH 1/8] Add vertical and horizonal zoom tools. --- glue_plotly/viewers/common/tools.py | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/glue_plotly/viewers/common/tools.py b/glue_plotly/viewers/common/tools.py index 067ae63..a1f885c 100644 --- a/glue_plotly/viewers/common/tools.py +++ b/glue_plotly/viewers/common/tools.py @@ -73,6 +73,52 @@ def _on_selection(self, _trace, _points, selector): viewer_state.y_max = ymax +@viewer_tool +class PlotlyHZoomMode(PlotlySelectionMode): + + icon = 'glue_zoom_to_rect' + tool_id = 'plotly:hzoom' + action_text = 'Horizontal zoom' + tool_tip = 'Horizontal zoom' + + def __init__(self, viewer): + super().__init__(viewer, 'select') + + def activate(self): + super().activate() + self.viewer.figure.update_layout(selectdirection="h") + + def _on_selection(self, _trace, _points, selector): + xmin, xmax = selector.xrange + viewer_state = self.viewer.state + with self.viewer.figure.batch_update(), delay_callback(viewer_state, 'x_min', 'x_max'): + viewer_state.x_min = xmin + viewer_state.x_max = xmax + + +@viewer_tool +class PlotlyVZoomMode(PlotlySelectionMode): + + icon = 'glue_zoom_to_rect' + tool_id = 'plotly:vzoom' + action_text = 'Vertical zoom' + tool_tip = 'Vertical zoom' + + def __init__(self, viewer): + super().__init__(viewer, 'select') + + def activate(self): + super().activate() + self.viewer.figure.update_layout(selectdirection="v") + + def _on_selection(self, _trace, _points, selector): + ymin, ymax = selector.xrange + viewer_state = self.viewer.state + with self.viewer.figure.batch_update(), delay_callback(viewer_state, 'y_min', 'y_max'): + viewer_state.y_min = ymin + viewer_state.y_max = ymax + + @viewer_tool class PlotlyPanMode(PlotlyDragMode): From b368ae2f2db13712f530685c5c7fe75c913c6a5a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 22 Mar 2024 15:31:27 -0400 Subject: [PATCH 2/8] Add hover tool. --- glue_plotly/viewers/common/tools.py | 16 ++++++++++++++++ glue_plotly/viewers/common/viewer.py | 1 + glue_plotly/viewers/histogram/layer_artist.py | 2 +- glue_plotly/viewers/histogram/viewer.py | 2 +- glue_plotly/viewers/scatter/layer_artist.py | 2 +- glue_plotly/viewers/scatter/viewer.py | 2 +- 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/glue_plotly/viewers/common/tools.py b/glue_plotly/viewers/common/tools.py index a1f885c..d6c778f 100644 --- a/glue_plotly/viewers/common/tools.py +++ b/glue_plotly/viewers/common/tools.py @@ -226,3 +226,19 @@ class PlotlyHomeTool(Tool): def activate(self): with self.viewer.figure.batch_update(): self.viewer.state.reset_limits() + + +@viewer_tool +class PlotlyHoverTool(CheckableTool): + + icon = 'glue_point' + tool_id = 'plotly:hover' + action_text = 'Hover' + tool_tip = 'Show hover info' + + def activate(self): + self.viewer.figure.update_layout(hovermode="closest") + + + def deactivate(self): + self.viewer.figure.update_layout(hovermode=False) diff --git a/glue_plotly/viewers/common/viewer.py b/glue_plotly/viewers/common/viewer.py index 194a1a0..9020fbb 100644 --- a/glue_plotly/viewers/common/viewer.py +++ b/glue_plotly/viewers/common/viewer.py @@ -20,6 +20,7 @@ class PlotlyBaseView(IPyWidgetView): LAYOUT_SETTINGS = dict( include_dimensions=False, + hovermode=False, hoverdistance=1, dragmode=False, showlegend=False, grid=None, newselection=dict(line=dict(color=INTERACT_COLOR), mode='immediate'), modebar=dict(remove=['toimage', 'zoom', 'pan', 'lasso', 'zoomIn2d', diff --git a/glue_plotly/viewers/histogram/layer_artist.py b/glue_plotly/viewers/histogram/layer_artist.py index dc6fa67..d36db34 100644 --- a/glue_plotly/viewers/histogram/layer_artist.py +++ b/glue_plotly/viewers/histogram/layer_artist.py @@ -115,7 +115,7 @@ def _update_data(self): bars = traces_for_layer(self.view.state, self.state, add_data_label=True) for bar in bars: - bar.update(unselected=dict(marker=dict(opacity=self.state.alpha))) + bar.update(hoverinfo='all', unselected=dict(marker=dict(opacity=self.state.alpha))) self._bars_id = bars[0].meta if bars else None self.view.figure.add_traces(bars) diff --git a/glue_plotly/viewers/histogram/viewer.py b/glue_plotly/viewers/histogram/viewer.py index 7d7bc6c..db86a50 100644 --- a/glue_plotly/viewers/histogram/viewer.py +++ b/glue_plotly/viewers/histogram/viewer.py @@ -13,7 +13,7 @@ @viewer_registry("plotly_histogram") class PlotlyHistogramView(PlotlyBaseView): - tools = ['plotly:home', 'plotly:zoom', 'plotly:pan', 'plotly:xrange'] + tools = ['plotly:home', 'plotly:zoom', 'plotly:pan', 'plotly:xrange', 'plotly:hover'] allow_duplicate_data = False allow_duplicate_subset = False diff --git a/glue_plotly/viewers/scatter/layer_artist.py b/glue_plotly/viewers/scatter/layer_artist.py index ef96431..793f633 100644 --- a/glue_plotly/viewers/scatter/layer_artist.py +++ b/glue_plotly/viewers/scatter/layer_artist.py @@ -129,7 +129,7 @@ def _create_scatter(self): scatter_info = dict(mode=scatter_mode(self.state), name=name, - hoverinfo='skip', + hoverinfo='all', unselected=dict(marker=dict(opacity=self.state.alpha)), meta=self._scatter_id) if self._viewer_state.using_rectilinear: diff --git a/glue_plotly/viewers/scatter/viewer.py b/glue_plotly/viewers/scatter/viewer.py index d106c03..527cc38 100644 --- a/glue_plotly/viewers/scatter/viewer.py +++ b/glue_plotly/viewers/scatter/viewer.py @@ -17,7 +17,7 @@ class PlotlyScatterView(PlotlyBaseView): tools = ['plotly:home', 'plotly:zoom', 'plotly:pan', 'plotly:xrange', - 'plotly:yrange', 'plotly:rectangle', 'plotly:lasso'] + 'plotly:yrange', 'plotly:rectangle', 'plotly:lasso', 'plotly:hover'] allow_duplicate_data = False allow_duplicate_subset = False From b244a1a48be5b6079a03941c4962fef72f1137f0 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 22 Mar 2024 15:46:46 -0400 Subject: [PATCH 3/8] Don't allow axis panning when pan tool isn't activated. --- glue_plotly/viewers/common/viewer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/glue_plotly/viewers/common/viewer.py b/glue_plotly/viewers/common/viewer.py index 9020fbb..9d3be12 100644 --- a/glue_plotly/viewers/common/viewer.py +++ b/glue_plotly/viewers/common/viewer.py @@ -71,7 +71,10 @@ def selection_layer(self): return next(self.figure.select_traces(dict(meta=self.selection_layer_id))) def _create_layout_config(self): - return base_layout_config(self, **self.LAYOUT_SETTINGS, width=1200, height=800) + config = base_layout_config(self, **self.LAYOUT_SETTINGS, width=1200, height=800) + config['xaxis']['fixedrange'] = True + config['yaxis']['fixedrange'] = True + return config def _remove_trace_index(self, trace): # TODO: It feels like there has to be a better way to do this @@ -85,7 +88,6 @@ def _remove_traces(self, traces): self.figure.data = [t for t in self.figure.data if t not in traces] def _clear_traces(self): - print("In _clear_traces") self.figure.data = [self.selection_layer] @property From 1c088d5a0c8e2709a966f0c0e1eba7f7608d2b5e Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 22 Mar 2024 16:19:50 -0400 Subject: [PATCH 4/8] Fix typo in vzoom tool. --- glue_plotly/viewers/common/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_plotly/viewers/common/tools.py b/glue_plotly/viewers/common/tools.py index d6c778f..2ab979f 100644 --- a/glue_plotly/viewers/common/tools.py +++ b/glue_plotly/viewers/common/tools.py @@ -112,7 +112,7 @@ def activate(self): self.viewer.figure.update_layout(selectdirection="v") def _on_selection(self, _trace, _points, selector): - ymin, ymax = selector.xrange + ymin, ymax = selector.yrange viewer_state = self.viewer.state with self.viewer.figure.batch_update(), delay_callback(viewer_state, 'y_min', 'y_max'): viewer_state.y_min = ymin From c3266321fc38d9eb99f1f5b8b65377b59f07161e Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 22 Mar 2024 16:22:44 -0400 Subject: [PATCH 5/8] Codestyle fixes. --- glue_plotly/viewers/common/tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/glue_plotly/viewers/common/tools.py b/glue_plotly/viewers/common/tools.py index 2ab979f..4d66020 100644 --- a/glue_plotly/viewers/common/tools.py +++ b/glue_plotly/viewers/common/tools.py @@ -230,7 +230,7 @@ def activate(self): @viewer_tool class PlotlyHoverTool(CheckableTool): - + icon = 'glue_point' tool_id = 'plotly:hover' action_text = 'Hover' @@ -238,7 +238,6 @@ class PlotlyHoverTool(CheckableTool): def activate(self): self.viewer.figure.update_layout(hovermode="closest") - def deactivate(self): self.viewer.figure.update_layout(hovermode=False) From 9b37f3225ee6fe597450e002000745dd7b5327c0 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 25 Mar 2024 17:47:45 -0400 Subject: [PATCH 6/8] Adjust where fixedrange is set in Plotly config. --- glue_plotly/common/common.py | 1 + glue_plotly/viewers/common/viewer.py | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/glue_plotly/common/common.py b/glue_plotly/common/common.py index 97adc82..4574ef5 100644 --- a/glue_plotly/common/common.py +++ b/glue_plotly/common/common.py @@ -78,6 +78,7 @@ def base_rectilinear_axis(viewer_state, axis): ticks='outside', showline=True, showgrid=False, + fixedrange=True, showticklabels=True, tickfont=dict( family=DEFAULT_FONT, diff --git a/glue_plotly/viewers/common/viewer.py b/glue_plotly/viewers/common/viewer.py index 9d3be12..5fdca6c 100644 --- a/glue_plotly/viewers/common/viewer.py +++ b/glue_plotly/viewers/common/viewer.py @@ -71,10 +71,7 @@ def selection_layer(self): return next(self.figure.select_traces(dict(meta=self.selection_layer_id))) def _create_layout_config(self): - config = base_layout_config(self, **self.LAYOUT_SETTINGS, width=1200, height=800) - config['xaxis']['fixedrange'] = True - config['yaxis']['fixedrange'] = True - return config + return base_layout_config(self, **self.LAYOUT_SETTINGS, width=1200, height=800) def _remove_trace_index(self, trace): # TODO: It feels like there has to be a better way to do this From 357dfd8b63c642d6f898a9392f83b3d33b65841c Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 25 Mar 2024 17:48:10 -0400 Subject: [PATCH 7/8] Fix some small behavior bugs in tools. --- glue_plotly/viewers/common/tools.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/glue_plotly/viewers/common/tools.py b/glue_plotly/viewers/common/tools.py index 4d66020..34dfb80 100644 --- a/glue_plotly/viewers/common/tools.py +++ b/glue_plotly/viewers/common/tools.py @@ -37,6 +37,7 @@ def deactivate(self): self.viewer.set_selection_callback(None) self.viewer.set_selection_active(False) self.viewer.figure.on_edits_completed(self._clear_selection) + super().deactivate() def _clear_selection(self): self.viewer.figure.plotly_relayout({'selections': [], 'dragmode': False}) @@ -130,6 +131,16 @@ class PlotlyPanMode(PlotlyDragMode): def __init__(self, viewer): super().__init__(viewer, 'pan') + def activate(self): + super().activate() + self.viewer.figure.layout['xaxis']['fixedrange'] = False + self.viewer.figure.layout['yaxis']['fixedrange'] = False + + def deactivate(self): + self.viewer.figure.layout['xaxis']['fixedrange'] = True + self.viewer.figure.layout['yaxis']['fixedrange'] = True + super().deactivate() + @viewer_tool class PlotlyHRangeSelectionMode(PlotlySelectionMode): From 570170bcee5558b5f99993116b5aeb569f53a155 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Mon, 25 Mar 2024 17:48:37 -0400 Subject: [PATCH 8/8] Add basic testing for common viewer tools. --- glue_plotly/viewers/common/tests/__init__.py | 0 .../viewers/common/tests/test_tools.py | 104 ++++++++++++++++++ glue_plotly/viewers/common/tests/utils.py | 43 ++++++++ 3 files changed, 147 insertions(+) create mode 100644 glue_plotly/viewers/common/tests/__init__.py create mode 100644 glue_plotly/viewers/common/tests/test_tools.py create mode 100644 glue_plotly/viewers/common/tests/utils.py diff --git a/glue_plotly/viewers/common/tests/__init__.py b/glue_plotly/viewers/common/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/glue_plotly/viewers/common/tests/test_tools.py b/glue_plotly/viewers/common/tests/test_tools.py new file mode 100644 index 0000000..72c23f7 --- /dev/null +++ b/glue_plotly/viewers/common/tests/test_tools.py @@ -0,0 +1,104 @@ +from glue_jupyter import jglue + +from glue_plotly.viewers.common.tests.utils import ExampleViewer + + +class TestTools(object): + + def setup_method(self, method): + self.app = jglue() + self.viewer = self.app.new_data_viewer(ExampleViewer) + + def teardown_method(self, method): + pass + + def get_tool(self, id): + return self.viewer.toolbar.tools[id] + + def test_rectzoom(self): + tool = self.get_tool('plotly:zoom') + tool.activate() + assert self.viewer.figure.layout['selectdirection'] == 'any' + assert self.viewer.figure.layout['dragmode'] == 'select' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_hzoom(self): + tool = self.get_tool('plotly:hzoom') + tool.activate() + assert self.viewer.figure.layout['selectdirection'] == 'h' + assert self.viewer.figure.layout['dragmode'] == 'select' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_vzoom(self): + tool = self.get_tool('plotly:vzoom') + tool.activate() + assert self.viewer.figure.layout['selectdirection'] == 'v' + assert self.viewer.figure.layout['dragmode'] == 'select' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_pan(self): + tool = self.get_tool('plotly:pan') + tool.activate() + assert self.viewer.figure.layout['dragmode'] == 'pan' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_hrange_select(self): + tool = self.get_tool('plotly:xrange') + tool.activate() + assert self.viewer.figure.layout['selectdirection'] == 'h' + assert self.viewer.figure.layout['dragmode'] == 'select' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_vrange_select(self): + tool = self.get_tool('plotly:yrange') + tool.activate() + assert self.viewer.figure.layout['selectdirection'] == 'v' + assert self.viewer.figure.layout['dragmode'] == 'select' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_rect_select(self): + tool = self.get_tool('plotly:rectangle') + tool.activate() + assert self.viewer.figure.layout['selectdirection'] == 'any' + assert self.viewer.figure.layout['dragmode'] == 'select' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_lasso_select(self): + tool = self.get_tool('plotly:lasso') + tool.activate() + assert self.viewer.figure.layout['dragmode'] == 'lasso' + tool.deactivate() + assert self.viewer.figure.layout['dragmode'] is False + + def test_home(self): + xmin, xmax = self.viewer.state.x_min, self.viewer.state.x_max + ymin, ymax = self.viewer.state.y_min, self.viewer.state.y_max + self.viewer.state.x_min = 10 + self.viewer.state.x_max = 27 + self.viewer.state.y_min = -5 + self.viewer.state.y_max = 13 + tool = self.get_tool('plotly:home') + tool.activate() + print(self.viewer.state) + assert self.viewer.state.x_min == xmin + assert self.viewer.state.x_max == xmax + assert self.viewer.state.y_min == ymin + assert self.viewer.state.y_max == ymax + xaxis = self.viewer.figure.layout.xaxis + assert xaxis.range == (xmin, xmax) + yaxis = self.viewer.figure.layout.yaxis + assert yaxis.range == (ymin, ymax) + + def test_hover(self): + tool = self.get_tool('plotly:hover') + tool.activate() + assert self.viewer.figure.layout['hovermode'] == 'closest' + tool.deactivate() + assert self.viewer.figure.layout['hovermode'] is False diff --git a/glue_plotly/viewers/common/tests/utils.py b/glue_plotly/viewers/common/tests/utils.py new file mode 100644 index 0000000..a16c861 --- /dev/null +++ b/glue_plotly/viewers/common/tests/utils.py @@ -0,0 +1,43 @@ +from echo import CallbackProperty +from glue.viewers.common.state import ViewerState +from ipywidgets import VBox + +from glue_plotly.viewers.common import PlotlyBaseView + + +class ExampleState(ViewerState): + x_axislabel = CallbackProperty("") + y_axislabel = CallbackProperty("") + x_min = CallbackProperty(0) + x_max = CallbackProperty(1) + y_min = CallbackProperty(0) + y_max = CallbackProperty(1) + show_axes = CallbackProperty(True) + + def reset_limits(self): + self.x_min = 0 + self.x_max = 1 + self.y_min = 0 + self.y_max = 1 + + +class ExampleOptions(VBox): + + def __init__(self, viewer_state): + + self.state = viewer_state + super().__init__([]) + + +class ExampleViewer(PlotlyBaseView): + + _state_cls = ExampleState + _options_cls = ExampleOptions + + tools = ['plotly:zoom', 'plotly:hzoom', 'plotly:vzoom', + 'plotly:pan', 'plotly:xrange', 'plotly:yrange', + 'plotly:rectangle', 'plotly:lasso', 'plotly:home', + 'plotly:hover'] + + def __init__(self, session, state=None): + super().__init__(session, state)