diff --git a/pyproject.toml b/pyproject.toml index 37d5371..0247b7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ readme = "README.md" dependencies = [ "servir>=0.0.5", "higlass-schema>=0.0.6", - "anywidget>=0.6.3", + "anywidget>=0.9.0", "jinja2", "jupyter-server-proxy>=3.0", "typing-extensions ; python_version<'3.9'", @@ -74,6 +74,7 @@ test = "pytest ." [tool.hatch.envs.docs] features = ["docs"] +python = "3.11" [tool.hatch.envs.docs.scripts] build = "sphinx-build -b html ./docs ./docs/_build/html" diff --git a/src/higlass/_widget.py b/src/higlass/_widget.py index d046b2d..faeff92 100644 --- a/src/higlass/_widget.py +++ b/src/higlass/_widget.py @@ -5,6 +5,7 @@ import anywidget import traitlets as t +import ipywidgets class HiGlassWidget(anywidget.AnyWidget): @@ -13,12 +14,13 @@ class HiGlassWidget(anywidget.AnyWidget): _viewconf = t.Dict(allow_none=False).tag(sync=True) _options = t.Dict().tag(sync=True) + _ts = t.Any().tag(sync=True, **ipywidgets.widget_serialization) # readonly properties location = t.List(t.Union([t.Float(), t.Tuple()]), read_only=True).tag(sync=True) - def __init__(self, viewconf: dict, **viewer_options): - super().__init__(_viewconf=viewconf, _options=viewer_options) + def __init__(self, viewconf: dict, ts, **viewer_options): + super().__init__(_viewconf=viewconf, _ts=ts, _options=viewer_options) def reload(self, *items): msg = json.dumps(["reload", items]) diff --git a/src/higlass/server.py b/src/higlass/server.py index 1648993..376720f 100644 --- a/src/higlass/server.py +++ b/src/higlass/server.py @@ -41,7 +41,7 @@ def track(self, type_: TrackType | None = None, **kwargs): ) t = track( type_=type_, - server=self.server, + server="jupyter", tilesetUid=self.tileset.uid, **kwargs, ) diff --git a/src/higlass/widget.js b/src/higlass/widget.js index 7e84d25..7042d05 100644 --- a/src/higlass/widget.js +++ b/src/higlass/widget.js @@ -1,41 +1,108 @@ -import hglib from "https://esm.sh/higlass@1.12?deps=react@17,react-dom@17,pixi.js@6"; +// import hglib from "https://esm.sh/higlass@1.12?deps=react@17,react-dom@17,pixi.js@6"; +import * as hglib from "http://localhost:5173/app/scripts/hglib.jsx"; +import { v4 } from "https://esm.sh/@lukeed/uuid@2"; + +// Make sure plugins are registered and enabled +window.higlassDataFetchersByType = window.higlassDataFetchersByType || {}; + +function uid() { + return v4().split("-")[0]; +} /** - * @param {{ - * xDomain: [number, number], - * yDomain: [number, number], - * }} location + * @template T + * @param {import("npm:@anyiwdget/types").AnyModel} model + * @param {unknown} payload + * @param {{ timeout?: number }} [options] + * @returns {Promise<{ data: T, buffers: DataView[] }>} */ -function toPts({ xDomain, yDomain }) { - let [x, xe] = xDomain; - let [y, ye] = yDomain; - return [x, xe, y, ye]; +function send(model, payload, { timeout = 3000 } = {}) { + let uuid = uid(); + return new Promise((resolve, reject) => { + let timer = setTimeout(() => { + reject(new Error(`Promise timed out after ${timeout} ms`)); + model.off("msg:custom", handler); + }, timeout); + /** + * @param {{ uuid: string, payload: T }} msg + * @param {DataView[]} buffers + */ + function handler(msg, buffers) { + if (!(msg.uuid === uuid)) return; + clearTimeout(timer); + resolve({ data: msg.payload, buffers }); + model.off("msg:custom", handler); + } + model.on("msg:custom", handler); + model.send({ payload, uuid }); + }); } -export async function render({ model, el }) { - let viewconf = model.get("_viewconf"); - let options = model.get("_options") ?? {}; - let api = await hglib.viewer(el, viewconf, options); +/** + * Detects server { server: 'jupyter' }, and creates a custom data entry for it. + * @example + * resolveJupyterServers({ views: [{ tracks: { top: [{ server: 'jupyter', tilesetUid: 'abc' }] } }] }, 'jupyter-123') + * // { views: [{ tracks: { top: [{ tilesetUid: 'abc', data: { type: 'jupyter-123', tilesetUid: 'abc' } }] } }] } + */ +function resolveJupyterServers(viewConfig, dataFetcherId) { + let copy = JSON.parse(JSON.stringify(viewConfig)); + for (let view of copy.views) { + for (let track of Object.values(view.tracks).flat()) { + if (track?.server === "jupyter") { + delete track.server; + track.data = track.data || {}; + track.data.type = dataFetcherId; + track.data.tilesetUid = track.tilesetUid; + } + } + } + return copy; +} - model.on("msg:custom", (msg) => { - msg = JSON.parse(msg); - let [fn, ...args] = msg; - api[fn](...args); - }); +function assert(condition, message) { + if (!condition) throw new Error(message); +} - if (viewconf.views.length === 1) { - api.on("location", (loc) => { - model.set("location", toPts(loc)); - model.save_changes(); - }, viewconf.views[0].uid); - } else { - viewconf.views.forEach((view, idx) => { - api.on("location", (loc) => { - let copy = model.get("location").slice(); - copy[idx] = toPts(loc); - model.set("location", copy); - model.save_changes(); - }, view.uid); +function createDataFetcherForModel(model) { + return function createDataFetcher(hgc, dataConfig, pubSub) { + let config = { ...dataConfig, server: "jupyter" }; + return new hgc.dataFetchers.DataFetcher(config, pubSub, { + async fetchTiles({ id, server, tileIds }) { + let { data } = await send(model, { type: "tiles", tileIds }); + let result = hgc.services.tileResponseToData(data, "jupyter", tileIds); + return result; + }, + async fetchTilesetInfo({ server, tilesetUid }) { + assert(server === "jupyter", "must be a jupyter server"); + let url = `${server}-${tilesetUid}`; + let { data } = await send(model, { type: "tileset_info", tilesetUid }); + return data; + }, + registerTileset() { + throw new Error("Not implemented"); + }, }); - } + }; } + +export default () => { + let id = `jupyter-${uid()}`; + return { + async initialize({ model }) { + let tsId = model.get("_ts"); + let tsModel = await model.widget_manager.get_model( + tsId.slice("IPY_MODEL_".length) + ); + window.higlassDataFetchersByType[tsId] = { + name: id, + dataFetcher: createDataFetcherForModel(tsModel), + }; + }, + async render({ model, el }) { + let viewconf = model.get("_viewconf"); + let options = model.get("_options") ?? {}; + let resolved = resolveJupyterServers(viewconf, model.get("_ts")); + let api = await hglib.viewer(el, resolved, options); + }, + }; +};