Try using async widgets
Opened this issue · 4 comments
ipywidgets
supports async widgets via threading. Rather than waiting for data, turning it into HTML, and directly displaying the HTML, I could return a loading HTML
widget and use threading to update the widget contents once data is retrieved from the server and formatted. This would make the experience more similar to the code editor by not blocking the entire kernel.
The main downside would be adding a dependency on ipywidgets
, but if most users are using this alongside geemap
then that's not a big issue. Other considerations would be:
Can I throw all the JS and CSS into theHTML
widget like I currently do, or do I need to handle that differently?- No.
HTML
widgets do not run scripts.
- No.
- Would reprs inside an
HTML
widget display correctly when rendered statically like they currently do? That's not a dealbreaker, but it would be nice. - Are there performance drawbacks in rendering time?
- What will the
ipywidgets
compatibility be? I've had issues in the past withipywidgets>7
, especially in Jupyter Lab, so ideally this would work in version 7 or 8. - Calling
_repr_html_
should return an HTML string, not a widget, so I think I would need to use_ipython_display_
or_repr_mimebundle_
instead and return the corresponding method from the associated widget.
Here's a rough implementation idea:
def _ipython_display_(obj: ee.Element):
"""Display an Earth Engine object in an async HTML widget"""
html = ipywidgets.HTML("<span>Loading...</span>")
threading.Thread().start(build_repr, args=(obj, html))
return html._ipython_display_()
def _build_repr(obj: ee.Element, html: ipywidgets.HTML) -> None:
"""Format an HTML repr string and add it to an HTML widget"""
info = obj.getInfo()
rep = _format_repr(info)
html.value = rep
Regarding VS Code now supports ipywidgets
compatibility, VS Code doesn't currently support >7
(microsoft/vscode-jupyter#8552). That's okay assuming I can get everything working in 7.7.2
.ipywidgets 8
🎉
Roadblock: ipywidgets
doesn't support running Javascript within an HTML
widget (jupyter-widgets/ipywidgets#3079), so this is dead in the water until a) that changes or b) I can get a pure CSS solution for collapsing, which is pending widespread support of the has
selector (see #5).
We can work around the limited functionality of the HTML
widget by using a different widget where collapsing is built-in.
ipytree has all the functionality I need and would dramatically simplify the process of building the repr, but performance seems to be very slow. A client-side image collection with three images took about 4.8s to build with ipytree
compared to around 4ms with HTML (~1000x slower), and there was also a longer delay in displaying the widget that I didn't measure. The prototype code I tested is below.
from ipytree import Tree, Node
from eerepr.html import _build_label
def build_node(obj):
if isinstance(obj, list):
obj_str = str(obj)
if len(obj_str) > 50:
obj_str = f"List ({len(obj)} objects)"
node = Node(obj_str, opened=False)
for item in obj:
sub_node = build_node(item)
node.add_node(sub_node)
elif isinstance(obj, dict):
obj_str = _build_label(obj)
node = Node(obj_str, opened=False)
for k, v in obj.items():
key_node = build_node(k)
value_node = build_node(v)
key_node.add_node(value_node)
node.add_node(key_node)
else:
node = Node(str(obj), opened=False)
return node
def ipytree_repr(obj):
"""Recursively build an ipytree.Tree from an object."""
tree = Tree(stripes=True)
node = build_node(obj)
tree.add_node(node)
return tree
For reference, with the test below...
info = ee.ImageCollection("COPERNICUS/S2_SR").limit(3).getInfo()
ipytree_repr(info)
...the build_node
function gets called 2655 times. That's a lot, but not enough to where overhead from Python looping, function calls, or instantiation should cause a ~1000x slowdown, so the bottleneck may be within ipytree
. I should do some more careful benchmarking and profiling to get a better idea of whether there's room for optimization.
Another option is building a custom widget. I've experimented some with anywidget by building a custom EEReprWidget
class that is initialized with an Earth Engine object, displays a loading spinner while server-side info is fetched, then sets and renders HTML content in a div built by the _esm
hook. This gave me performance comparable with a pure HTML repr with the ability for async loading. It will take some more work to fix some bugs and incompatibilities between ipywidgets
versions and Jupyter environments, and I should consider building it without anywidget
to save the dependency, but I think this is going to be the solution.
It will take some more work to fix some bugs and incompatibilities between ipywidgets versions and Jupyter environments
Note that ipywidgets 8 support was finally added to VS Code, which should make this a little less painful!