| importScripts("https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js"); | |
| function sendPatch(patch, buffers, msg_id) { | |
| self.postMessage({ | |
| type: 'patch', | |
| patch: patch, | |
| buffers: buffers | |
| }) | |
| } | |
| async function startApplication() { | |
| console.log("Loading pyodide!"); | |
| self.postMessage({type: 'status', msg: 'Loading pyodide'}) | |
| self.pyodide = await loadPyodide(); | |
| self.pyodide.globals.set("sendPatch", sendPatch); | |
| console.log("Loaded!"); | |
| await self.pyodide.loadPackage("micropip"); | |
| const env_spec = ['https://cdn.holoviz.org/panel/wheels/bokeh-3.3.2-py3-none-any.whl', 'https://cdn.holoviz.org/panel/1.3.6/dist/wheels/panel-1.3.6-py3-none-any.whl', 'pyodide-http==0.2.1', 'holoviews', 'pandas'] | |
| for (const pkg of env_spec) { | |
| let pkg_name; | |
| if (pkg.endsWith('.whl')) { | |
| pkg_name = pkg.split('/').slice(-1)[0].split('-')[0] | |
| } else { | |
| pkg_name = pkg | |
| } | |
| self.postMessage({type: 'status', msg: `Installing ${pkg_name}`}) | |
| try { | |
| await self.pyodide.runPythonAsync(` | |
| import micropip | |
| await micropip.install('${pkg}'); | |
| `); | |
| } catch(e) { | |
| console.log(e) | |
| self.postMessage({ | |
| type: 'status', | |
| msg: `Error while installing ${pkg_name}` | |
| }); | |
| } | |
| } | |
| console.log("Packages loaded!"); | |
| self.postMessage({type: 'status', msg: 'Executing code'}) | |
| const code = ` | |
| import asyncio | |
| from panel.io.pyodide import init_doc, write_doc | |
| init_doc() | |
| """*Linked Brushing* is a very powerful technique. It's also often called | |
| *Linked Selections* or *Crossfiltering*. | |
| This example is inspired by the HoloViews [Linked Brushing Reference Guide]\ | |
| (http://holoviews.org/user_guide/Linked_Brushing.html) and the Plotly blog post | |
| [Introducing Dash HoloViews]\ | |
| (https://medium.com/plotly/introducing-dash-holoviews-6a05c088ebe5). | |
| This example uses the *Iris* dataset. | |
| """ | |
| from typing import Tuple | |
| import holoviews as hv | |
| import pandas as pd | |
| import panel as pn | |
| from holoviews import opts | |
| from panel.template import FastListTemplate | |
| @pn.cache | |
| def get_iris_data(): | |
| return pd.read_csv("https://cdn.awesome-panel.org/resources/crossfiltering_holoviews/iris.csv.gz") | |
| ACCENT = "#F08080" | |
| CSS = """ | |
| .main .card-margin.stretch_both { | |
| height: calc(50vh - 65px) !important; | |
| } | |
| """ | |
| if not CSS in pn.config.raw_css: | |
| pn.config.raw_css.append(CSS) | |
| BOKEH_TOOLS = { | |
| "tools": ["hover"], "active_tools": ["box_select"] | |
| } | |
| def get_linked_plots() -> Tuple: | |
| """Returns a tuple (scatter, hist) of linked plots | |
| See http://holoviews.org/user_guide/Linked_Brushing.html | |
| """ | |
| dataset = hv.Dataset(get_iris_data()) | |
| scatter = hv.Scatter(dataset, kdims=["sepal_length"], vdims=["sepal_width"]) | |
| hist = hv.operation.histogram(dataset, dimension="petal_width", normed=False) | |
| # pylint: disable=no-value-for-parameter | |
| selection_linker = hv.selection.link_selections.instance() | |
| # pylint: disable=no-member | |
| scatter = selection_linker(scatter).opts( | |
| opts.Scatter(color=ACCENT, responsive=True, size=10, **BOKEH_TOOLS), | |
| ) | |
| hist = selection_linker(hist).opts( | |
| opts.Histogram(color=ACCENT, responsive=True, **BOKEH_TOOLS) | |
| ) | |
| return scatter, hist | |
| def create_app(): | |
| """Returns the app in a nice FastListTemplate""" | |
| scatter, hist = get_linked_plots() | |
| scatter_panel = pn.pane.HoloViews(scatter, sizing_mode="stretch_both") | |
| hist_panel = pn.pane.HoloViews(hist, sizing_mode="stretch_both") | |
| template = FastListTemplate( | |
| site="Awesome Panel", | |
| site_url="https://awesome-panel.org", | |
| title="Crossfiltering with HoloViews and Bokeh", | |
| accent=ACCENT, | |
| main=[ | |
| # We need to wrap in Columns to get them to stretch properly | |
| pn.Column(scatter_panel, sizing_mode="stretch_both"), | |
| pn.Column(hist_panel, sizing_mode="stretch_both"), | |
| ], | |
| ) | |
| return template | |
| pn.extension() | |
| hv.extension("bokeh") | |
| create_app().servable() | |
| await write_doc() | |
| ` | |
| try { | |
| const [docs_json, render_items, root_ids] = await self.pyodide.runPythonAsync(code) | |
| self.postMessage({ | |
| type: 'render', | |
| docs_json: docs_json, | |
| render_items: render_items, | |
| root_ids: root_ids | |
| }) | |
| } catch(e) { | |
| const traceback = `${e}` | |
| const tblines = traceback.split('\n') | |
| self.postMessage({ | |
| type: 'status', | |
| msg: tblines[tblines.length-2] | |
| }); | |
| throw e | |
| } | |
| } | |
| self.onmessage = async (event) => { | |
| const msg = event.data | |
| if (msg.type === 'rendered') { | |
| self.pyodide.runPythonAsync(` | |
| from panel.io.state import state | |
| from panel.io.pyodide import _link_docs_worker | |
| _link_docs_worker(state.curdoc, sendPatch, setter='js') | |
| `) | |
| } else if (msg.type === 'patch') { | |
| self.pyodide.globals.set('patch', msg.patch) | |
| self.pyodide.runPythonAsync(` | |
| state.curdoc.apply_json_patch(patch.to_py(), setter='js') | |
| `) | |
| self.postMessage({type: 'idle'}) | |
| } else if (msg.type === 'location') { | |
| self.pyodide.globals.set('location', msg.location) | |
| self.pyodide.runPythonAsync(` | |
| import json | |
| from panel.io.state import state | |
| from panel.util import edit_readonly | |
| if state.location: | |
| loc_data = json.loads(location) | |
| with edit_readonly(state.location): | |
| state.location.param.update({ | |
| k: v for k, v in loc_data.items() if k in state.location.param | |
| }) | |
| `) | |
| } | |
| } | |
| startApplication() |