diff --git a/holoviews/__init__.py b/holoviews/__init__.py index 46c225ab4b..d971d24635 100644 --- a/holoviews/__init__.py +++ b/holoviews/__init__.py @@ -1,4 +1,4 @@ -import os, io +import io, os, sys import numpy as np # noqa (API import) import param @@ -44,6 +44,9 @@ class notebook_extension(param.ParameterizedFunction): def __call__(self, *args, **opts): # noqa (dummy signature) raise Exception("IPython notebook not available: use hv.extension instead.") +if '_pyodide' in sys.modules: + from .pyodide import pyodide_extension as extension # noqa (API import) + # A single holoviews.rc file may be executed if found. for rcfile in [os.environ.get("HOLOVIEWSRC", ''), os.path.abspath(os.path.join(os.path.split(__file__)[0], diff --git a/holoviews/pyodide.py b/holoviews/pyodide.py new file mode 100644 index 0000000000..9a7f2ec944 --- /dev/null +++ b/holoviews/pyodide.py @@ -0,0 +1,92 @@ +import asyncio +import sys + +from js import document + +from bokeh.embed.elements import script_for_render_items +from bokeh.embed.util import standalone_docs_json_and_render_items +from bokeh.embed.wrappers import wrap_in_script_tag +from panel.io.pyodide import _link_docs +from panel.pane import panel as as_panel + +from ..core.dimension import LabelledData +from ..core.options import Store +from ..util import extension as _extension + + +#----------------------------------------------------------------------------- +# Private API +#----------------------------------------------------------------------------- + +async def _link(ref, doc): + from js import Bokeh + rendered = Bokeh.index.object_keys() + if ref not in rendered: + await asyncio.sleep(0.1) + await _link(ref, doc) + return + views = Bokeh.index.object_values() + view = views[rendered.indexOf(ref)] + _link_docs(doc, view.model.document) + +def render_html(obj): + if hasattr(sys.stdout, '_out'): + out = sys.stdout._out # type: ignore + else: + raise ValueError("Could not determine target node to write to.") + doc = Document() + as_panel(obj).server_doc(doc, location=False) + docs_json, [render_item,] = standalone_docs_json_and_render_items( + doc.roots, suppress_callback_warning=True + ) + for root in doc.roots: + render_item.roots._roots[root] = target + document.getElementById(target).classList.add('bk-root') + script = script_for_render_items(docs_json, [render_item]) + asyncio.create_task(_link(doc.roots[0].ref['id'], doc)) + return {'text/html': wrap_in_script_tag(script)}, {} + +def render_image(element, fmt): + """ + Used to render elements to an image format (svg or png) if requested + in the display formats. + """ + if fmt not in Store.display_formats:b + return None + + backend = Store.current_backend + if type(element) not in Store.registry[backend]: + return None + renderer = Store.renderers[backend] + plot = renderer.get_plot(element) + + # Current renderer does not support the image format + if fmt not in renderer.param.objects('existing')['fig'].objects: + return None + + data, info = renderer(plot, fmt=fmt) + return {info['mime_type']: data}, {} + +def render_png(obj): + return render_image(element, 'png') + +def render_svg(obj): + return render_image(element, 'svg') + + +#----------------------------------------------------------------------------- +# Public API +#----------------------------------------------------------------------------- + +class pyodide_extension(_extension): + + _loaded = False + + def __call__(self, *args, **params): + super().__call__(*args, **params) + if not self._loaded: + Store.output_settings.initialize(list(Store.renderers.keys())) + Store.set_display_hook('html+js', LabelledData, render_html) + Store.set_display_hook('png', LabelledData, render_png) + Store.set_display_hook('svg', LabelledData, render_svg) + pyodide_extension._loaded = True diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 0ccca84447..70d9f1e04a 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -648,6 +648,8 @@ class extension(_pyviz_extension): # Hooks run when a backend is loaded _backend_hooks = defaultdict(list) + _loaded = False + def __call__(self, *args, **params): # Get requested backends config = params.pop('config', {})