|
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', 'plotly'] |
|
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 panel as pn |
|
from holoviews import opts |
|
from panel.template import FastListTemplate |
|
import plotly.io as pio |
|
import pandas as pd |
|
|
|
@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(100vh - 125px) !important; |
|
} |
|
""" |
|
|
|
def _plotly_hooks(plot, element): |
|
"""Used by HoloViews to give plots plotly plots special treatment""" |
|
fig = plot.state |
|
|
|
fig["layout"]["dragmode"] = "select" |
|
fig["config"]["displayModeBar"] = True |
|
if isinstance(element, hv.Histogram): |
|
# Constrain histogram selection direction to horizontal |
|
fig["layout"]["selectdirection"] = "h" |
|
|
|
|
|
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, size=10, hooks=[_plotly_hooks], width=700, height=400), |
|
) |
|
hist = selection_linker(hist).opts( |
|
opts.Histogram(color=ACCENT, hooks=[_plotly_hooks], width=700, height=400) |
|
) |
|
|
|
return scatter, hist |
|
|
|
|
|
def create_app(): |
|
"""Returns the app in a nice FastListTemplate""" |
|
if pn.config.theme == "dark": |
|
pio.templates.default = "plotly_dark" |
|
else: |
|
pio.templates.default = "plotly_white" |
|
scatter, hist = get_linked_plots() |
|
scatter_panel = pn.pane.HoloViews(scatter, sizing_mode="stretch_both", backend="plotly") |
|
hist_panel = pn.pane.HoloViews(hist, sizing_mode="stretch_both", backend="plotly") |
|
|
|
def reset(event): |
|
scatter, hist = get_linked_plots() |
|
scatter_panel.object=scatter |
|
hist_panel.object=hist |
|
|
|
reset_button = pn.widgets.Button(name="RESET PLOTS", on_click=reset, description="Resets the plots. Plotly does not have a built in way to do this.") |
|
|
|
template = FastListTemplate( |
|
site="Awesome Panel", |
|
site_url="https://awesome-panel.org", |
|
title="Crossfiltering with HoloViews and Plotly", |
|
accent=ACCENT, |
|
main=[ |
|
# We need to wrap in Columns to get them to stretch properly |
|
pn.Column(reset_button, scatter_panel, pn.layout.Spacer(height=20), hist_panel, height=870, sizing_mode="stretch_width"), |
|
], |
|
main_max_width="850px", |
|
) |
|
return template |
|
|
|
pn.extension("plotly", raw_css=[CSS]) |
|
hv.extension("plotly") |
|
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() |