Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Jupyter comms server #145

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
Expand Down Expand Up @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions src/higlass/_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import anywidget
import traitlets as t
import ipywidgets


class HiGlassWidget(anywidget.AnyWidget):
Expand All @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion src/higlass/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
127 changes: 95 additions & 32 deletions src/higlass/widget.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,104 @@
import hglib from "https://esm.sh/[email protected]?deps=react@17,react-dom@17,pixi.js@6";
// import hglib from "https://esm.sh/[email protected]?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";
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing with Vite locally and PR from higlass


// Make sure plugins are registered and enabled
window.higlassDataFetchersByType = window.higlassDataFetchersByType || {};

/**
* @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 = globalThis.crypto.randomUUID();
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-${v4().split("-")[0]}`;
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);
},
};
};
Loading