-
-
Notifications
You must be signed in to change notification settings - Fork 403
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 support for popups on selection streams #6168
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6168 +/- ##
==========================================
- Coverage 88.69% 88.11% -0.59%
==========================================
Files 316 318 +2
Lines 66017 66846 +829
==========================================
+ Hits 58554 58899 +345
- Misses 7463 7947 +484
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
I know this is a draft but this looks like a decent API to me! Initially I thought you were supplying a callback instead of a concrete layout to |
I had the same thought and yes I think that should be supported. |
Here's an example with the callback support: points = hv.Points(np.random.randn(1000, 2))
hv.streams.Selection1D(source=points, popup=lambda index: points.iloc[index].dframe().describe() if index else None)
points.opts(tools=['box_select', 'tap', 'lasso_select'], height=500, width=500, size=6, color='black', fill_color=None) |
How is the exact location of the popup decided? I assume there are multiple different policies that might be sensible depending on the stream? I suppose the default I would expect is the position of the cursor after the interaction generating the popup... |
Currently it's: Tap: x/y location But yes, clearly for actual selections like Selection1D it'd be better if it was based on the overall selection rather than the geometry of the last selection. |
Beautiful! I'd expect this example to show Tap selecting (and highlighting) a point in the same way that the lasso and box select do; is there something more that needs to be added to provide selection of a data point and not simply tapping a location? |
If you look closely it does show that, the default tap selection mode is to combine multiple selections though so you end up returning selections even when you click a random location (if a previous selection already exists). |
This would be more intuitive if the popup was placed relative to the overall selection rather than the last selection geometry, as I had suggested above. |
Oh, so if Tap were the first thing you had used (and/or if you had been closer to a single point) it would have highlighted that point? If so, sounds good. |
Yes, and if I hadn't hit a point it wouldn't have popped up anything. |
Great. I guess there are two very different modes to think about how to use this functionality: (1) Annotating or calculating info or otherwise working with existing data points, where the tools are for data-point selection, and (2) Adding new entities, typically a range on an axis, or a bounding box or bounding region, then annotating that thing (e.g. "data drops out here", in a region where there are no data points). Presumably the same pop-up approach works for both, but figured I would bring it up just to be sure. |
In holonote, a Also, I imagine you could use your stream to create whatever element in a |
Just tried it on browser and it encountered the pending write issue. I think it has to due with the async task from holoviz/panel#6521
import panel as pn
import numpy as np
import holoviews as hv
hv.extension("bokeh")
points = hv.Points(np.random.randn(1000, 2))
def form(name):
text_input = pn.widgets.TextInput(name='Description')
button = pn.widgets.Button(name='Save', on_click=lambda _: layout.param.update(visible=False))
layout = pn.Column(f'# {name}', text_input, button)
return layout
hv.streams.BoundsXY(source=points, popup=form('Bounds'))
hv.streams.Lasso(source=points, popup=form('Lasso'))
hv.streams.Tap(source=points, popup=form('Tap'))
pn.panel(points.opts(tools=['box_select', 'lasso_select', 'tap'], width=600, height=600, size=6, color='black', fill_color=None)).show() Task exception was never retrieved
future: <Task finished name='Task-95' coro=<PopupMixin._populate() done, defined at [/Users/ahuang/repos/holoviews/holoviews/plotting/bokeh/callbacks.py:631](https://file+.vscode-resource.vscode-cdn.net/Users/ahuang/repos/holoviews/holoviews/plotting/bokeh/callbacks.py:631)> exception=RuntimeError('_pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes')>
Traceback (most recent call last):
File "/Users/ahuang/repos/holoviews/holoviews/plotting/bokeh/callbacks.py", line 664, in _populate
self._panel.elements = [model]
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/core/has_props.py", line 338, in __setattr__
return super().__setattr__(name, value)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/core/property/descriptors.py", line 333, in __set__
self._set(obj, old, value, setter=setter)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/core/property/descriptors.py", line 621, in _set
self._trigger(obj, old, value, hint=hint, setter=setter)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/core/property/descriptors.py", line 699, in _trigger
obj.trigger(self.name, old, value, hint, setter)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/model/model.py", line 571, in trigger
super().trigger(descriptor.name, old, new, hint=hint, setter=setter)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/util/callback_manager.py", line 188, in trigger
self.document.callbacks.notify_change(cast(Model, self), attr, old, new, hint, setter, invoke)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 249, in notify_change
self.trigger_on_change(event)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 413, in trigger_on_change
invoke_with_curdoc(doc, invoke_callbacks)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 443, in invoke_with_curdoc
return f()
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 412, in invoke_callbacks
cb(event)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/callbacks.py", line 276, in <lambda>
self._change_callbacks[receiver] = lambda event: event.dispatch(receiver)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/events.py", line 353, in dispatch
super().dispatch(receiver)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/document/events.py", line 219, in dispatch
cast(DocumentPatchedMixin, receiver)._document_patched(self)
File "/Users/ahuang/miniconda3/envs/holoviews/lib/python3.10/site-packages/bokeh/server/session.py", line 244, in _document_patched
raise RuntimeError("_pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes")
RuntimeError: _pending_writes should be non-None when we have a document lock, and we should have the lock when the document changes |
Multiple taps, without closing the first one, results in UnknownReferenceError: can't resolve reference 'p1128' Caused by elif self._popup:
print("POPUP_2")
if position:
self._popup.visible = True
self._panel.position = XY(**position)
if self.plot.comm:
push_on_root(self.plot.root.ref['id'])
return |
Okay refactored with comments; the two issues above are fixed. Base notebook; no more Screen.Recording.2024-04-04.at.9.32.02.PM.movServer; to fix Screen.Recording.2024-04-04.at.9.32.34.PM.movCallbacks Screen.Recording.2024-04-04.at.9.34.47.PM.mov |
Okay, I think this is ready for review now. |
@philippjfr I think this is in a good place for you to take back over to handle async / delayed stream results |
This approach may also be how to support datashaded or rasterized plots, creating a bounding box around the region selected, then running a query against the original dataset.
More precisely, our matplotlib backend doesn't support that. Matplotlib itself offers Jupyter-based interactivity via
An app definitely needs to be robust against a failure in the callback or in any other computation resulting from a UI action. Maybe a JS console warning could be raised, at best, since everyone ignores those anyway. I think it's more likely to be noise than signal to print something on the Python terminal, because users can end up selecting things that just don't work with the callback, and the app shouldn't be affected by that. |
The developer should be made aware of this so they can add the appropriate error handling themselves. |
Thanks for looking into this. I couldn't get the actual contents to show up, but it might still be in progress. Screen.Recording.2024-04-15.at.12.07.27.PM.movAlso, using import panel as pn
import holoviews as hv
import numpy as np
hv.extension("bokeh")
points = hv.Points(np.random.randn(1000, 2))
hv.streams.Selection1D(source=points, popup=lambda index: points.iloc[index].dframe().describe() if index else None)
points.opts(tools=['box_select', 'tap', 'lasso_select'], height=500, width=500, size=6, color='black', fill_color=None) Lastly, using File "/Users/ahuang/repos/holoviews/holoviews/plotting/bokeh/callbacks.py", line 661, in on_msg
event = self._selection_event
AttributeError: 'Selection1DCallback' object has no attribute '_selection_event' |
Resolved the remaining issues, would appreciate more testing. |
Thanks! I think most of them are resolved, but I couldn't get lasso to work on server / vscode: import holoviews as hv
import panel as pn
import numpy as np
pn.extension()
hv.extension("bokeh")
points = hv.Points(np.random.randn(1000, 2))
def form(name):
text_input = pn.widgets.TextInput(name='Description')
button = pn.widgets.Button(name='Save', on_click=lambda _: layout.param.update(visible=False))
layout = pn.Column(f'# {name}', text_input, button)
return layout
hv.streams.BoundsXY(source=points, popup=form('Bounds'))
hv.streams.Lasso(source=points, popup=form('Lasso'))
hv.streams.Tap(source=points, popup=form('Tap'))
points.opts(tools=['box_select', 'lasso_select', 'tap'], width=600, height=600, size=6, color='black', fill_color=None) Screen.Recording.2024-04-16.at.11.32.53.AM.mov |
Separately, if I click too fast and furiously on init, I once got, but I can't reproduce now. def popup_distribution(index):
x, y = points.iloc[index].data.T
return hv.Distribution((x, y)).opts(
width=100,
height=100,
toolbar=None,
yaxis="bare",
xlabel="",
xticks=[-1, 0, 1],
xlim=(-2, 2),
)
points = hv.Points(np.random.randn(1000, 2))
hv.streams.Selection1D(
source=points,
popup=popup_distribution,
)
points.opts(
tools=["box_select", "lasso_select", "tap"],
active_tools=["lasso_select"],
size=6,
color="black",
fill_color=None,
width=500,
height=500
) However, there is an issue with positioning of x/y Tap; it sticks to the original. Oh I think _watch_position is just getting the farthest top right value of all the ones that are selected rather than the last clicked. Screen.Recording.2024-04-16.at.11.36.58.AM.mov |
I decided it'd make more sense to use the last point clicked as popup. However, I discovered Lasso event seems to have a bug; it's never final (see all the Screen.Recording.2024-04-16.at.12.14.13.PM.mov |
Okay, lasso select for Selection1D does show import holoviews as hv
import panel as pn
import numpy as np
pn.extension()
hv.extension("bokeh")
def popup_distribution(index):
print(index)
x, y = points.iloc[index].data.T
return hv.Distribution((x, y)).opts(
width=100,
height=100,
toolbar=None,
yaxis="bare",
xlabel="",
xticks=[-1, 0, 1],
xlim=(-2, 2),
)
points = hv.Points(np.random.randn(1000, 2))
hv.streams.Selection1D(
source=points,
popup=popup_distribution,
)
pn.serve(points.opts(
tools=["box_select", "lasso_select"],
active_tools=["lasso_select"],
size=6,
color="black",
fill_color=None,
width=500,
height=500
)) Screen.Recording.2024-04-16.at.1.27.24.PM.mov |
Please test again, I think I've now resolved the lasso select issues. Code isn't the prettiest tbh. |
Thanks! It works great. I added a few ui tests for it. I think this is ready for review and merge. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
This PR exposes the ability to pop up floating panes added in Bokeh 3.4 after a selection is made. The contents of the popup can be added to the
Stream
and will be populated when the user makes the first selection. The contents can be any Panel object or any component that can be rendered with Panel. To control the visibility of the popup this PR currently links thevisible
parameter of the provided component to the visibility of the popup itself.Here's a simple example adding three distinct forms to each type of supported stream:
visible
approach is sensible