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/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) diff --git a/glue_plotly/viewers/common/tools.py b/glue_plotly/viewers/common/tools.py index 067ae63..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}) @@ -73,6 +74,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.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 + viewer_state.y_max = ymax + + @viewer_tool class PlotlyPanMode(PlotlyDragMode): @@ -84,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): @@ -180,3 +237,18 @@ 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..5fdca6c 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', @@ -84,7 +85,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 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