Skip to content

Commit 8960dda

Browse files
authored
Merge pull request #107 from Carifio24/jupyter-export-dialogs
Add save hover dialog for Jupyter exporters
2 parents 3cefb1e + b411788 commit 8960dda

19 files changed

+454
-245
lines changed

glue_plotly/common/image.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,20 +317,19 @@ def traces_for_scatter_layer(viewer_state, layer_state, hover_data=None, add_dat
317317
size=scatter_size_info(layer_state, mask),
318318
sizemin=1)
319319

320-
if np.sum(hover_data) == 0:
320+
if hover_data is None or np.sum(hover_data) == 0:
321321
hoverinfo = 'skip'
322322
hovertext = None
323323
else:
324324
hoverinfo = 'text'
325325
hovertext = ['' for _ in range(layer_state.layer.shape[0])]
326-
for i in range(len(layer_state.layer.components)):
327-
if hover_data[i]:
328-
label = layer_state.layer.components[i].label
326+
for component in layer_state.layer.components:
327+
label = component.label
328+
if hover_data.get(label, False):
329329
hover_values = layer_state.layer[label][mask]
330330
for k in range(len(hover_values)):
331331
hovertext[k] = (hovertext[k] + '{}: {} <br>'
332-
.format(layer_state.layer.components[i].label,
333-
hover_values[k]))
332+
.format(label, hover_values[k]))
334333

335334
name = layer_state.layer.label
336335
if add_data_label and not isinstance(layer_state.layer, BaseData):
@@ -418,8 +417,9 @@ def traces(viewer, secondary_x=False, secondary_y=False, hover_selections=None,
418417
traces += traces_for_nonpixel_subset_layer(viewer.state, layer.state, full_view, transpose)
419418

420419
for layer in layers['scatter']:
420+
hover_data = hover_selections[layer.state.layer.label] if hover_selections else None
421421
traces += traces_for_scatter_layer(viewer.state, layer.state,
422-
hover_data=hover_selections[layer.state.layer.label],
422+
hover_data=hover_data,
423423
add_data_label=add_data_label)
424424

425425
if secondary_x or secondary_y:

glue_plotly/common/scatter2d.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,6 @@ def base_marker(layer_state, mask=None):
262262

263263
def trace_data_for_layer(viewer, layer_state, hover_data=None, add_data_label=True):
264264
traces = {}
265-
if hover_data is None:
266-
hover_data = []
267265

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

302-
if np.sum(hover_data) == 0:
300+
if hover_data is None or np.sum(hover_data) == 0:
303301
hoverinfo = 'skip'
304302
hovertext = None
305303
else:
306304
hoverinfo = 'text'
307-
hovertext = ["" for _ in range((mask.shape[0]))]
308-
for i in range(len(layer_state.layer.components)):
309-
if hover_data[i]:
310-
label = layer_state.layer.components[i].label
305+
hovertext = ["" for _ in range(mask.size)]
306+
for component in layer_state.layer.components:
307+
label = component.label
308+
if hover_data.get(label, False):
311309
hover_values = layer_state.layer[label][mask]
312310
for k in range(len(hover_values)):
313311
hovertext[k] = (hovertext[k] + "{}: {} <br>"

glue_plotly/common/scatter3d.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ def traces_for_layer(viewer_state, layer_state, hover_data=None, add_data_label=
127127
hovertext = None
128128
else:
129129
hoverinfo = 'text'
130-
hovertext = ["" for _ in range((mask.shape[0]))]
131-
for i in range(len(layer_state.layer.components)):
132-
if hover_data[i]:
133-
label = layer_state.layer.components[i].label
130+
hovertext = ["" for _ in range(mask.size)]
131+
for component in layer_state.layer.components:
132+
label = component.label
133+
if hover_data.get(label, False):
134134
hover_values = layer_state.layer[label][mask]
135135
for k in range(len(hover_values)):
136136
hovertext[k] = (hovertext[k] + "{}: {} <br>"

glue_plotly/common/tests/test_scatter2d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def test_rectilinear_traces(self):
230230
self.layer.state.yerr_visible = True
231231

232232
hover_components = [self.data.id['x'], self.data.id['z']]
233-
hover_data = [cid in hover_components for cid in self.layer.layer.components]
233+
hover_data = {cid.label: cid in hover_components for cid in self.layer.layer.components}
234234
traces = trace_data_for_layer(self.viewer, self.layer.state, hover_data=hover_data, add_data_label=True)
235235
assert set(traces.keys()) == {'scatter', 'vector'}
236236
scatter = traces['scatter'][0]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from echo import SelectionCallbackProperty
2+
from glue import config
3+
from glue.core.data_combo_helper import ComponentIDComboHelper, DataCollectionComboHelper
4+
from glue.core.state_objects import State
5+
6+
7+
class SaveHoverState(State):
8+
9+
data = SelectionCallbackProperty()
10+
subset = SelectionCallbackProperty()
11+
component = SelectionCallbackProperty()
12+
exporter = SelectionCallbackProperty()
13+
14+
def __init__(self, data_collection=None):
15+
16+
super(SaveHoverState, self).__init__()
17+
18+
self.data_helper = DataCollectionComboHelper(self, 'data', data_collection)
19+
self.component_helper = ComponentIDComboHelper(self, 'component',
20+
data_collection=data_collection)
21+
22+
self.add_callback('data', self._on_data_change)
23+
self._on_data_change()
24+
25+
self._sync_data_exporters()
26+
27+
def _sync_data_exporters(self):
28+
29+
exporters = list(config.data_exporter)
30+
31+
def display_func(exporter):
32+
if exporter.extension == '':
33+
return "{0} (*)".format(exporter.label)
34+
else:
35+
return "{0} ({1})".format(exporter.label, ' '.join('*.' + ext for ext in exporter.extension))
36+
37+
SaveHoverState.exporter.set_choices(self, exporters)
38+
SaveHoverState.exporter.set_display_func(self, display_func)
39+
40+
def _on_data_change(self, event=None):
41+
self.component_helper.set_multiple_data([self.data])
42+
self._sync_subsets()
43+
44+
def _sync_subsets(self):
45+
46+
def display_func(subset):
47+
if subset is None:
48+
return "All data (no subsets applied)"
49+
else:
50+
return subset.label
51+
52+
subsets = [None] + list(self.data.subsets)
53+
54+
SaveHoverState.subset.set_choices(self, subsets)
55+
SaveHoverState.subset.set_display_func(self, display_func)
56+
57+
58+
class BaseSaveHoverDialog:
59+
60+
def __init__(self, data_collection, checked_dictionary=None):
61+
62+
if checked_dictionary is not None:
63+
self.checked_dictionary = checked_dictionary
64+
else:
65+
self.checked_dictionary = {
66+
data.label: {component.label: False for component in data.components}
67+
for data in data_collection
68+
}
69+
self.state = SaveHoverState(data_collection=data_collection)
70+
71+
self.state.add_callback('component', self._on_component_change)
72+
self.state.add_callback('data', self._on_data_change)
73+
74+
def _on_component_change(self, *args):
75+
pass
76+
77+
def _on_data_change(self, *args):
78+
pass
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from glue.core import Data, DataCollection
2+
from numpy import ones
3+
4+
5+
def hover_dummy_data(data_or_subset):
6+
components = {}
7+
for component in data_or_subset.components:
8+
components[component.label] = ones(10)
9+
10+
dummy_data = Data(**components, label=data_or_subset.label)
11+
12+
seen = set()
13+
for component in dummy_data.components:
14+
if component.label in seen:
15+
dummy_data.remove_component(component)
16+
else:
17+
seen.add(component.label)
18+
return dummy_data
19+
20+
21+
def default_layer_condition(layer):
22+
return layer.enabled and layer.state.visible
23+
24+
25+
def hover_data_collection_for_viewer(viewer,
26+
layer_condition=None):
27+
if layer_condition is None:
28+
layer_condition = default_layer_condition
29+
30+
return DataCollection([hover_dummy_data(layer.layer) for layer in viewer.layers if layer_condition(layer)])

glue_plotly/html_exporters/jupyter/image.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,47 @@
11
from glue.config import viewer_tool
2-
3-
from glue_plotly.common import data_count, layers_to_export
4-
from glue_plotly.common.image import axes_data_from_bqplot, layout_config, traces
2+
from IPython.display import display
53

64
from plotly.offline import plot
75
import plotly.graph_objs as go
86
from plotly.subplots import make_subplots
97

10-
from ...jupyter_base_export_tool import JupyterBaseExportTool
8+
from glue_plotly.common import data_count, layers_to_export
9+
from glue_plotly.common.image import axes_data_from_bqplot, layers_by_type, layout_config, traces
10+
from glue_plotly.html_exporters.hover_utils import hover_data_collection_for_viewer
11+
from glue_plotly.html_exporters.jupyter.save_hover import JupyterSaveHoverDialog
12+
from glue_plotly.jupyter_base_export_tool import JupyterBaseExportTool
1113

1214

1315
@viewer_tool
1416
class PlotlyImageBqplotExport(JupyterBaseExportTool):
1517
tool_id = 'save:bqplot_plotlyimage2d'
1618

19+
def activate(self):
20+
done = False
21+
22+
def on_cancel():
23+
nonlocal done
24+
done = True
25+
26+
layers = layers_by_type(self.viewer)
27+
scatter_layers = layers["scatter"]
28+
29+
if len(scatter_layers) > 0:
30+
dc_hover = hover_data_collection_for_viewer(
31+
self.viewer,
32+
layer_condition=lambda layer: layer.state.visible and layer.enabled and layer in scatter_layers
33+
)
34+
35+
self.save_hover_dialog = \
36+
JupyterSaveHoverDialog(data_collection=dc_hover,
37+
checked_dictionary=None,
38+
display=True,
39+
on_cancel=on_cancel,
40+
on_export=self.open_file_dialog)
41+
42+
with self.viewer.output_widget:
43+
display(self.save_hover_dialog)
44+
1745
def save_figure(self, filepath):
1846

1947
if not filepath:
@@ -37,9 +65,11 @@ def save_figure(self, filepath):
3765
layout = go.Layout(**config)
3866
fig = go.Figure(layout=layout)
3967

68+
checked_dictionary = self.save_hover_dialog.checked_dictionary if hasattr(self, 'save_hover_dialog') else None
4069
traces_to_add = traces(self.viewer,
4170
secondary_x=secondary_x,
4271
secondary_y=secondary_y,
72+
hover_selections=checked_dictionary,
4373
add_data_label=add_data_label)
4474
fig.add_traces(traces_to_add)
4575

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from ipyvuetify.VuetifyTemplate import VuetifyTemplate
2+
from traitlets import Bool, Int, List, observe
3+
4+
from glue_jupyter.vuetify_helpers import link_glue_choices
5+
6+
from ..base_save_hover import BaseSaveHoverDialog
7+
8+
9+
class JupyterSaveHoverDialog(BaseSaveHoverDialog, VuetifyTemplate):
10+
11+
template_file = (__file__, "save_hover.vue")
12+
dialog_open = Bool().tag(sync=True)
13+
14+
data_items = List().tag(sync=True)
15+
data_selected = Int().tag(sync=True)
16+
17+
component_items = List().tag(sync=True)
18+
component_selected = List().tag(sync=True)
19+
20+
def __init__(self,
21+
data_collection,
22+
checked_dictionary=None,
23+
on_cancel=None,
24+
on_export=None,
25+
display=False):
26+
27+
BaseSaveHoverDialog.__init__(self, data_collection=data_collection, checked_dictionary=checked_dictionary)
28+
VuetifyTemplate.__init__(self)
29+
30+
self.on_cancel = on_cancel
31+
self.on_export = on_export
32+
33+
if display:
34+
self.dialog_open = True
35+
36+
link_glue_choices(self, self.state, 'data')
37+
38+
self._on_data_change()
39+
40+
def _on_data_change(self, *args):
41+
super()._on_data_change(*args)
42+
data_components = self.state.data.components
43+
self.component_items = [
44+
{"text": component.label, "value": index}
45+
for index, component in enumerate(data_components)
46+
]
47+
current_selections = self.checked_dictionary.get(self.state.data.label,
48+
{component.label: False for component in data_components})
49+
self.component_selected = [i for i, component in enumerate(data_components)
50+
if current_selections[component.label]]
51+
52+
@observe('component_selected')
53+
def _on_component_selected_changed(self, change):
54+
selections = change["new"]
55+
current_layer = self.state.data.label
56+
self.checked_dictionary[current_layer] = {component.label: i in selections
57+
for i, component in enumerate(self.state.data.components)}
58+
59+
def vue_select_none(self, *args):
60+
self.component_selected = []
61+
62+
def vue_select_all(self, *args):
63+
self.component_selected = list(range(len(self.component_items)))
64+
65+
def vue_cancel_dialog(self, *args):
66+
self.checked_dictionary = {}
67+
self.dialog_open = False
68+
if self.on_cancel is not None:
69+
self.on_cancel()
70+
71+
def vue_export_viewer(self, *args):
72+
self.dialog_open = False
73+
if self.on_export is not None:
74+
self.on_export()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<template>
2+
<v-dialog
3+
v-model="dialog_open"
4+
width="600px"
5+
>
6+
<v-card class="pa-3">
7+
<v-card-title>
8+
Save Hover
9+
</v-card-title>
10+
11+
<p>Choose the layer you want to add hover info to:</p>
12+
<v-select
13+
v-model="data_selected"
14+
:items="data_items"
15+
>
16+
</v-select>
17+
18+
<p>Choose component of the layer you want to appear in hover over:</p>
19+
<v-list>
20+
<v-list-item-group
21+
v-model="component_selected"
22+
multiple
23+
>
24+
<v-list-item
25+
v-for="(component, index) in component_items"
26+
:key="index"
27+
:value="index"
28+
>
29+
{{ component.text }}
30+
</v-list-item>
31+
</v-list-item-group>
32+
</v-list>
33+
<v-row class="d-flex justify-space-around">
34+
<v-btn @click="select_none">
35+
Select None
36+
</v-btn>
37+
<v-btn @click="select_all">
38+
Select All
39+
</v-btn>
40+
</v-row>
41+
<v-row class="py-3">
42+
<p>Once you click on "Export" you will be prompted to choose the filename.
43+
</v-row>
44+
<v-row>
45+
<v-spacer></v-spacer>
46+
<v-btn @click="cancel_dialog">Cancel</v-btn>
47+
<v-btn @click="export_viewer">Export</v-btn>
48+
</v-row>
49+
50+
</v-card>
51+
<v-dialog>
52+
</template>

0 commit comments

Comments
 (0)