diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f928f8..449479f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [v1.0.1](https://github.com/higlass/higlass-python/compare/v1.0.1...v1.0.0) + +- Pass kwargs in to Viewconf.widget() so that they can be passed on to the higlass widget and potentially make their way to the higlass component + ## [v0.4.8](https://github.com/higlass/higlass-python/compare/v0.4.8...v0.4.7) - Bumped higlass version diff --git a/docs/conf.py b/docs/conf.py index d383b4f..5fd84d5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import typing from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -213,7 +214,7 @@ class IframeVideo(Directive): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False - option_spec = { + option_spec: typing.ClassVar = { "height": directives.nonnegative_int, "width": directives.nonnegative_int, "align": align, diff --git a/pyproject.toml b/pyproject.toml index 43c6983..c007d62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ readme = "README.md" dependencies = [ "servir>=0.0.5", "higlass-schema>=0.0.6", - "higlass-widget>=0.0.7", + "anywidget>=0.6.3", "jinja2", "jupyter-server-proxy>=3.0", "typing-extensions ; python_version<'3.9'", @@ -39,8 +39,9 @@ urls = { homepage = "https://github.com/higlass/higlass-python" } dev = [ "black[jupyter]", "pytest", - "ruff", + "ruff==0.0.285", "jupyterlab", + "anywidget[dev]>=0.6.3", ] docs = [ "Sphinx", diff --git a/src/higlass/_widget.py b/src/higlass/_widget.py new file mode 100644 index 0000000..d046b2d --- /dev/null +++ b/src/higlass/_widget.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import json +import pathlib + +import anywidget +import traitlets as t + + +class HiGlassWidget(anywidget.AnyWidget): + _esm = pathlib.Path(__file__).parent / "widget.js" + _css = "https://esm.sh/higlass@1.12/dist/hglib.css" + + _viewconf = t.Dict(allow_none=False).tag(sync=True) + _options = t.Dict().tag(sync=True) + + # 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 reload(self, *items): + msg = json.dumps(["reload", items]) + self.send(msg) + + def zoom_to( + self, + view_id: str, + start1: int, + end1: int, + start2: int | None = None, + end2: int | None = None, + animate_time: int = 500, + ): + msg = json.dumps(["zoomTo", view_id, start1, end1, start2, end2, animate_time]) + self.send(msg) diff --git a/src/higlass/api.py b/src/higlass/api.py index 75a0f8c..5bffcf9 100644 --- a/src/higlass/api.py +++ b/src/higlass/api.py @@ -385,11 +385,11 @@ def _repr_mimebundle_(self, include=None, exclude=None): plugin_urls = [] if self.views is None else gather_plugin_urls(self.views) return renderer(self.dict(), plugin_urls=plugin_urls) - def widget(self): + def widget(self, **kwargs): """Create a Jupyter Widget display for this view config.""" - from higlass_widget import HiGlassWidget + from higlass._widget import HiGlassWidget - return HiGlassWidget(self.dict()) + return HiGlassWidget(self.dict(), **kwargs) @classmethod def from_url(cls, url: str, **kwargs): diff --git a/src/higlass/widget.js b/src/higlass/widget.js new file mode 100644 index 0000000..7e84d25 --- /dev/null +++ b/src/higlass/widget.js @@ -0,0 +1,41 @@ +import hglib from "https://esm.sh/higlass@1.12?deps=react@17,react-dom@17,pixi.js@6"; + +/** + * @param {{ + * xDomain: [number, number], + * yDomain: [number, number], + * }} location + */ +function toPts({ xDomain, yDomain }) { + let [x, xe] = xDomain; + let [y, ye] = yDomain; + return [x, xe, y, ye]; +} + +export async function render({ model, el }) { + let viewconf = model.get("_viewconf"); + let options = model.get("_options") ?? {}; + let api = await hglib.viewer(el, viewconf, options); + + model.on("msg:custom", (msg) => { + msg = JSON.parse(msg); + let [fn, ...args] = msg; + api[fn](...args); + }); + + 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); + }); + } +}