Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add save hover dialog for Jupyter exporters #107

Merged
merged 11 commits into from
Nov 27, 2024
14 changes: 7 additions & 7 deletions glue_plotly/common/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,20 +317,19 @@
size=scatter_size_info(layer_state, mask),
sizemin=1)

if np.sum(hover_data) == 0:
if hover_data is None or np.sum(hover_data) == 0:

Check warning on line 320 in glue_plotly/common/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/common/image.py#L320

Added line #L320 was not covered by tests
hoverinfo = 'skip'
hovertext = None
else:
hoverinfo = 'text'
hovertext = ['' for _ in range(layer_state.layer.shape[0])]
for i in range(len(layer_state.layer.components)):
if hover_data[i]:
label = layer_state.layer.components[i].label
for component in layer_state.layer.components:
label = component.label
if hover_data.get(label, False):

Check warning on line 328 in glue_plotly/common/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/common/image.py#L326-L328

Added lines #L326 - L328 were not covered by tests
hover_values = layer_state.layer[label][mask]
for k in range(len(hover_values)):
hovertext[k] = (hovertext[k] + '{}: {} <br>'
.format(layer_state.layer.components[i].label,
hover_values[k]))
.format(label, hover_values[k]))

name = layer_state.layer.label
if add_data_label and not isinstance(layer_state.layer, BaseData):
Expand Down Expand Up @@ -418,8 +417,9 @@
traces += traces_for_nonpixel_subset_layer(viewer.state, layer.state, full_view, transpose)

for layer in layers['scatter']:
hover_data = hover_selections[layer.state.layer.label] if hover_selections else None

Check warning on line 420 in glue_plotly/common/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/common/image.py#L420

Added line #L420 was not covered by tests
traces += traces_for_scatter_layer(viewer.state, layer.state,
hover_data=hover_selections[layer.state.layer.label],
hover_data=hover_data,
add_data_label=add_data_label)

if secondary_x or secondary_y:
Expand Down
12 changes: 5 additions & 7 deletions glue_plotly/common/scatter2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,6 @@ def base_marker(layer_state, mask=None):

def trace_data_for_layer(viewer, layer_state, hover_data=None, add_data_label=True):
traces = {}
if hover_data is None:
hover_data = []

x = layer_state.layer[viewer.state.x_att].copy()
y = layer_state.layer[viewer.state.y_att].copy()
Expand Down Expand Up @@ -299,15 +297,15 @@ def trace_data_for_layer(viewer, layer_state, hover_data=None, add_data_label=Tr
if yerr_traces:
traces['yerr'] = yerr_traces

if np.sum(hover_data) == 0:
if hover_data is None or np.sum(hover_data) == 0:
hoverinfo = 'skip'
hovertext = None
else:
hoverinfo = 'text'
hovertext = ["" for _ in range((mask.shape[0]))]
for i in range(len(layer_state.layer.components)):
if hover_data[i]:
label = layer_state.layer.components[i].label
hovertext = ["" for _ in range(mask.size)]
for component in layer_state.layer.components:
label = component.label
if hover_data.get(label, False):
hover_values = layer_state.layer[label][mask]
for k in range(len(hover_values)):
hovertext[k] = (hovertext[k] + "{}: {} <br>"
Expand Down
8 changes: 4 additions & 4 deletions glue_plotly/common/scatter3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@
hovertext = None
else:
hoverinfo = 'text'
hovertext = ["" for _ in range((mask.shape[0]))]
for i in range(len(layer_state.layer.components)):
if hover_data[i]:
label = layer_state.layer.components[i].label
hovertext = ["" for _ in range(mask.size)]
for component in layer_state.layer.components:
label = component.label
if hover_data.get(label, False):

Check warning on line 133 in glue_plotly/common/scatter3d.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/common/scatter3d.py#L130-L133

Added lines #L130 - L133 were not covered by tests
hover_values = layer_state.layer[label][mask]
for k in range(len(hover_values)):
hovertext[k] = (hovertext[k] + "{}: {} <br>"
Expand Down
2 changes: 1 addition & 1 deletion glue_plotly/common/tests/test_scatter2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def test_rectilinear_traces(self):
self.layer.state.yerr_visible = True

hover_components = [self.data.id['x'], self.data.id['z']]
hover_data = [cid in hover_components for cid in self.layer.layer.components]
hover_data = {cid.label: cid in hover_components for cid in self.layer.layer.components}
traces = trace_data_for_layer(self.viewer, self.layer.state, hover_data=hover_data, add_data_label=True)
assert set(traces.keys()) == {'scatter', 'vector'}
scatter = traces['scatter'][0]
Expand Down
78 changes: 78 additions & 0 deletions glue_plotly/html_exporters/base_save_hover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from echo import SelectionCallbackProperty
from glue import config
from glue.core.data_combo_helper import ComponentIDComboHelper, DataCollectionComboHelper
from glue.core.state_objects import State


class SaveHoverState(State):

data = SelectionCallbackProperty()
subset = SelectionCallbackProperty()
component = SelectionCallbackProperty()
exporter = SelectionCallbackProperty()

def __init__(self, data_collection=None):

super(SaveHoverState, self).__init__()

self.data_helper = DataCollectionComboHelper(self, 'data', data_collection)
self.component_helper = ComponentIDComboHelper(self, 'component',
data_collection=data_collection)

self.add_callback('data', self._on_data_change)
self._on_data_change()

self._sync_data_exporters()

def _sync_data_exporters(self):

exporters = list(config.data_exporter)

def display_func(exporter):
if exporter.extension == '':
return "{0} (*)".format(exporter.label)

Check warning on line 33 in glue_plotly/html_exporters/base_save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/base_save_hover.py#L32-L33

Added lines #L32 - L33 were not covered by tests
else:
return "{0} ({1})".format(exporter.label, ' '.join('*.' + ext for ext in exporter.extension))

Check warning on line 35 in glue_plotly/html_exporters/base_save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/base_save_hover.py#L35

Added line #L35 was not covered by tests

SaveHoverState.exporter.set_choices(self, exporters)
SaveHoverState.exporter.set_display_func(self, display_func)

def _on_data_change(self, event=None):
self.component_helper.set_multiple_data([self.data])
self._sync_subsets()

def _sync_subsets(self):

def display_func(subset):
if subset is None:
return "All data (no subsets applied)"

Check warning on line 48 in glue_plotly/html_exporters/base_save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/base_save_hover.py#L47-L48

Added lines #L47 - L48 were not covered by tests
else:
return subset.label

Check warning on line 50 in glue_plotly/html_exporters/base_save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/base_save_hover.py#L50

Added line #L50 was not covered by tests

subsets = [None] + list(self.data.subsets)

SaveHoverState.subset.set_choices(self, subsets)
SaveHoverState.subset.set_display_func(self, display_func)


class BaseSaveHoverDialog:

def __init__(self, data_collection, checked_dictionary=None):

if checked_dictionary is not None:
self.checked_dictionary = checked_dictionary
else:
self.checked_dictionary = {

Check warning on line 65 in glue_plotly/html_exporters/base_save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/base_save_hover.py#L65

Added line #L65 was not covered by tests
data.label: {component.label: False for component in data.components}
for data in data_collection
}
self.state = SaveHoverState(data_collection=data_collection)

self.state.add_callback('component', self._on_component_change)
self.state.add_callback('data', self._on_data_change)

def _on_component_change(self, *args):
pass

def _on_data_change(self, *args):
pass

Check warning on line 78 in glue_plotly/html_exporters/base_save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/base_save_hover.py#L78

Added line #L78 was not covered by tests
30 changes: 30 additions & 0 deletions glue_plotly/html_exporters/hover_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from glue.core import Data, DataCollection
from numpy import ones


def hover_dummy_data(data_or_subset):
components = {}
for component in data_or_subset.components:
components[component.label] = ones(10)

dummy_data = Data(**components, label=data_or_subset.label)

seen = set()
for component in dummy_data.components:
if component.label in seen:
dummy_data.remove_component(component)
else:
seen.add(component.label)
return dummy_data


def default_layer_condition(layer):
return layer.enabled and layer.state.visible


def hover_data_collection_for_viewer(viewer,
layer_condition=None):
if layer_condition is None:
layer_condition = default_layer_condition

return DataCollection([hover_dummy_data(layer.layer) for layer in viewer.layers if layer_condition(layer)])
38 changes: 34 additions & 4 deletions glue_plotly/html_exporters/jupyter/image.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,47 @@
from glue.config import viewer_tool

from glue_plotly.common import data_count, layers_to_export
from glue_plotly.common.image import axes_data_from_bqplot, layout_config, traces
from IPython.display import display

from plotly.offline import plot
import plotly.graph_objs as go
from plotly.subplots import make_subplots

from ...jupyter_base_export_tool import JupyterBaseExportTool
from glue_plotly.common import data_count, layers_to_export
from glue_plotly.common.image import axes_data_from_bqplot, layers_by_type, layout_config, traces
from glue_plotly.html_exporters.hover_utils import hover_data_collection_for_viewer
from glue_plotly.html_exporters.jupyter.save_hover import JupyterSaveHoverDialog
from glue_plotly.jupyter_base_export_tool import JupyterBaseExportTool


@viewer_tool
class PlotlyImageBqplotExport(JupyterBaseExportTool):
tool_id = 'save:bqplot_plotlyimage2d'

def activate(self):
done = False

Check warning on line 20 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L20

Added line #L20 was not covered by tests

def on_cancel():

Check warning on line 22 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L22

Added line #L22 was not covered by tests
nonlocal done
done = True

Check warning on line 24 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L24

Added line #L24 was not covered by tests

layers = layers_by_type(self.viewer)
scatter_layers = layers["scatter"]

Check warning on line 27 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L26-L27

Added lines #L26 - L27 were not covered by tests

if len(scatter_layers) > 0:
dc_hover = hover_data_collection_for_viewer(

Check warning on line 30 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L29-L30

Added lines #L29 - L30 were not covered by tests
self.viewer,
layer_condition=lambda layer: layer.state.visible and layer.enabled and layer in scatter_layers
)

self.save_hover_dialog = \

Check warning on line 35 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L35

Added line #L35 was not covered by tests
JupyterSaveHoverDialog(data_collection=dc_hover,
checked_dictionary=None,
display=True,
on_cancel=on_cancel,
on_export=self.open_file_dialog)

with self.viewer.output_widget:
display(self.save_hover_dialog)

Check warning on line 43 in glue_plotly/html_exporters/jupyter/image.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/image.py#L42-L43

Added lines #L42 - L43 were not covered by tests

def save_figure(self, filepath):

if not filepath:
Expand All @@ -37,9 +65,11 @@
layout = go.Layout(**config)
fig = go.Figure(layout=layout)

checked_dictionary = self.save_hover_dialog.checked_dictionary if hasattr(self, 'save_hover_dialog') else None
traces_to_add = traces(self.viewer,
secondary_x=secondary_x,
secondary_y=secondary_y,
hover_selections=checked_dictionary,
add_data_label=add_data_label)
fig.add_traces(traces_to_add)

Expand Down
74 changes: 74 additions & 0 deletions glue_plotly/html_exporters/jupyter/save_hover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from ipyvuetify.VuetifyTemplate import VuetifyTemplate
from traitlets import Bool, Int, List, observe

from glue_jupyter.vuetify_helpers import link_glue_choices

from ..base_save_hover import BaseSaveHoverDialog


class JupyterSaveHoverDialog(BaseSaveHoverDialog, VuetifyTemplate):

template_file = (__file__, "save_hover.vue")
dialog_open = Bool().tag(sync=True)

data_items = List().tag(sync=True)
data_selected = Int().tag(sync=True)

component_items = List().tag(sync=True)
component_selected = List().tag(sync=True)

def __init__(self,
data_collection,
checked_dictionary=None,
on_cancel=None,
on_export=None,
display=False):

BaseSaveHoverDialog.__init__(self, data_collection=data_collection, checked_dictionary=checked_dictionary)
VuetifyTemplate.__init__(self)

Check warning on line 28 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L27-L28

Added lines #L27 - L28 were not covered by tests

self.on_cancel = on_cancel
self.on_export = on_export

Check warning on line 31 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L30-L31

Added lines #L30 - L31 were not covered by tests

if display:
self.dialog_open = True

Check warning on line 34 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L33-L34

Added lines #L33 - L34 were not covered by tests

link_glue_choices(self, self.state, 'data')

Check warning on line 36 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L36

Added line #L36 was not covered by tests

self._on_data_change()

Check warning on line 38 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L38

Added line #L38 was not covered by tests

def _on_data_change(self, *args):
super()._on_data_change(*args)
data_components = self.state.data.components
self.component_items = [

Check warning on line 43 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L41-L43

Added lines #L41 - L43 were not covered by tests
{"text": component.label, "value": index}
for index, component in enumerate(data_components)
]
current_selections = self.checked_dictionary.get(self.state.data.label,

Check warning on line 47 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L47

Added line #L47 was not covered by tests
{component.label: False for component in data_components})
self.component_selected = [i for i, component in enumerate(data_components)

Check warning on line 49 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L49

Added line #L49 was not covered by tests
if current_selections[component.label]]

@observe('component_selected')
def _on_component_selected_changed(self, change):
selections = change["new"]
current_layer = self.state.data.label
self.checked_dictionary[current_layer] = {component.label: i in selections

Check warning on line 56 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L54-L56

Added lines #L54 - L56 were not covered by tests
for i, component in enumerate(self.state.data.components)}

def vue_select_none(self, *args):
self.component_selected = []

Check warning on line 60 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L60

Added line #L60 was not covered by tests

def vue_select_all(self, *args):
self.component_selected = list(range(len(self.component_items)))

Check warning on line 63 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L63

Added line #L63 was not covered by tests

def vue_cancel_dialog(self, *args):
self.checked_dictionary = {}
self.dialog_open = False
if self.on_cancel is not None:
self.on_cancel()

Check warning on line 69 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L66-L69

Added lines #L66 - L69 were not covered by tests

def vue_export_viewer(self, *args):
self.dialog_open = False
if self.on_export is not None:
self.on_export()

Check warning on line 74 in glue_plotly/html_exporters/jupyter/save_hover.py

View check run for this annotation

Codecov / codecov/patch

glue_plotly/html_exporters/jupyter/save_hover.py#L72-L74

Added lines #L72 - L74 were not covered by tests
52 changes: 52 additions & 0 deletions glue_plotly/html_exporters/jupyter/save_hover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<v-dialog
v-model="dialog_open"
width="600px"
>
<v-card class="pa-3">
<v-card-title>
Save Hover
</v-card-title>

<p>Choose the layer you want to add hover info to:</p>
<v-select
v-model="data_selected"
:items="data_items"
>
</v-select>

<p>Choose component of the layer you want to appear in hover over:</p>
<v-list>
<v-list-item-group
v-model="component_selected"
multiple
>
<v-list-item
v-for="(component, index) in component_items"
:key="index"
:value="index"
>
{{ component.text }}
</v-list-item>
</v-list-item-group>
</v-list>
<v-row class="d-flex justify-space-around">
<v-btn @click="select_none">
Select None
</v-btn>
<v-btn @click="select_all">
Select All
</v-btn>
</v-row>
<v-row class="py-3">
<p>Once you click on "Export" you will be prompted to choose the filename.
</v-row>
<v-row>
<v-spacer></v-spacer>
<v-btn @click="cancel_dialog">Cancel</v-btn>
<v-btn @click="export_viewer">Export</v-btn>
</v-row>

</v-card>
<v-dialog>
</template>
Loading
Loading