higlass / higlass-python

Python bindings to and Jupyter Notebook+Lab integration for the HiGlass viewer
http://docs-python.higlass.io/
MIT License
52 stars 12 forks source link

feat: Jupyter comms server #145

Open manzt opened 7 months ago

manzt commented 7 months ago

Depends on https://github.com/higlass/higlass/pull/1194. I have just got this working locally with Vite.

Screen Recording 2024-02-15 at 11 17 26 PM

manzt commented 7 months ago

Notebooks test code with a widget to collect the Tilesets. A collection of tilesets (should be weakrefable), that handles fulfilling requests from the front end.

import higlass as hg
import ipywidgets
import traitlets
import itertools

class Tilesets(ipywidgets.DOMWidget):
    value = traitlets.Int(0).tag(sync=True)
    def __init__(self):
        super().__init__()
        self.on_msg(self._handle_custom_msg)
        self.ts = dict()

    def add(self, ts):
        self.ts[ts.tileset.uid] = ts.tileset
        return self

    def _handle_custom_msg(self, data, buffers):
        payload = data["payload"]
        uuid = data["uuid"]
        match payload:
            case { "type": "tileset_info", "tilesetUid": uid }:
                info = { uid: self.ts[uid].info() }
                self.send({ "uuid": uuid, "payload": info })
            case { "type": "tiles", "tileIds": tids }:
                all_tiles = []
                for uid, tids in itertools.groupby(
                    iterable=sorted(tids), key=lambda tid: tid.split(".")[0]
                ):
                    tiles = self.ts[uid].tiles(list(tids))
                    all_tiles.extend(tiles)
                data = {tid: tval for tid, tval in all_tiles}
                self.send({ "uuid": uuid, "payload": data })
            case _:
                raise ValueError("Something's wrong with the Internet")

ts = hg.cooler("./test.mcool")
tss = Tilesets().add(ts)
track = ts.track("heatmap")
hg.view(track, width=6).widget(ts=tss)
manzt commented 7 months ago

Just other ideas. A tile request "coordinator", that can be shared among tracks. It waits an animation frame to see all the desired tiles, and then dispatches a single server request.

class TileRequestCoordinator {
  #model;
  #requests;
  #frameRequested = false;
  constructor(model) {
    this.#model = model;
    this.#requests = [];
  }
  async fetchTilesetInfo({ tilesetUid }) {
    let { data } = await send(this.#model, { type: "tileset_info", tilesetUid });
    return data;
  }
  async fetchTiles({ tileIds }) {
    if (!this.#frameRequested) {
      this.#frameRequested = true;
      requestAnimationFrame(() => this.#processRequests());
    }
    let { promise, resolve, reject } = Promise.withResolvers();
    this.#requests.push({ tileIds, resolve, reject });
    return promise;
  }
  async #processRequests() {
    this.#frameRequested = false;
    let ids = [...new Set(this.#requests.flatMap((r) => r.tileIds))];
    let { data: resp } = await send(this.#model, { type: "tiles", tileIds: ids });
    let data = tileResponseToData(resp, "jupyter", ids);
    for (let { tileIds, resolve } of this.#requests) {
      let tileData = Object.fromEntries(tileIds.map((id) => [id, data[id]]));
      resolve(tileData);
    }
    this.#requests.length = 0;
  }
  registerTileset() {
    throw new Error("Not implemented");
  }
}
manzt commented 2 months ago

Relevant PR that benefits from a similar kind of architecture (and ability to drop jupyter-server-proxy):