Skip to content

Commit

Permalink
Make GoslingWidget an anywidget (#162)
Browse files Browse the repository at this point in the history
* Include gosling-widget in project

* Update CI

* Replace dependency groups naming

* Rename build hook

* typo

* Include warning with missing widget

* clear notebooks

* remove try catch
  • Loading branch information
manzt authored Nov 20, 2024
1 parent 134b34a commit 79527d0
Show file tree
Hide file tree
Showing 13 changed files with 1,052 additions and 826 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ concurrency:
env:
PYTHONUNBUFFERED: "1"
FORCE_COLOR: "1"
SKIP_DENO_BUILD: "1"

jobs:

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ concurrency:
group: "pages"
cancel-in-progress: true

env:
SKIP_DENO_BUILD: "1"

jobs:

Deploy:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- uses: astral-sh/setup-uv@v3
with:
version: "0.5.x"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ node_modules/
_build/
generated/
gallery/
gosling/static/
20 changes: 20 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"lock": false,
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run -A --node-modules-dir npm:esbuild --bundle --minify --loader:.css=text --format=esm --outfile=gosling/static/widget.js frontend/widget.ts --sourcemap=inline --watch",
"build": "deno run -A --node-modules-dir npm:esbuild --bundle --minify --loader:.css=text --format=esm --outfile=gosling/static/widget.js frontend/widget.ts"
},
"imports": {
"@anywidget/types": "npm:@anywidget/types@^0.2.0",
"gosling.js": "npm:gosling.js@^0.17.0"
},
"fmt": {
"useTabs": true
},
"lint": {
"rules": {
"exclude": ["prefer-const"]
}
}
}
23 changes: 23 additions & 0 deletions frontend/widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as gosling from "gosling.js";

interface Model {
_viewconf: string;
}

export default {
async render({ model, el }: import("@anywidget/types").RenderProps<Model>) {
const viewconf = JSON.parse(model.get("_viewconf"));
const api = await gosling.embed(el, viewconf, { padding: 0 });
model.on("msg:custom", (msg) => {
msg = JSON.parse(msg);
console.log(msg);
try {
const [fn, ...args] = msg;
// @ts-expect-error - This is a dynamic call
api[fn](...args);
} catch (e) {
console.error(e);
}
});
},
};
27 changes: 27 additions & 0 deletions gosling/_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import json
import pathlib
from typing import Any, Dict
import warnings

import anywidget
import traitlets as t

_esm = pathlib.Path(__file__).parent / "static" / "widget.js"

if not _esm.exists():
warnings.warn("Widget front-end code not found. Assuming running in CI.")
_esm = None


class GoslingWidget(anywidget.AnyWidget):
_esm = _esm
_viewconf = t.Unicode("null").tag(sync=True)

location = t.List(t.Union([t.Float(), t.Tuple()]), read_only=True).tag(sync=True)

def __init__(self, viewconf: Dict[str, Any], **kwargs):
super().__init__(_viewconf=json.dumps(viewconf), **kwargs)

def zoom_to(self, view_id: str, pos: str, padding: float = 0, duration: int = 1000):
msg = json.dumps(["zoomTo", view_id, pos, padding, duration])
self.send(msg)
2 changes: 1 addition & 1 deletion gosling/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def save(

def widget(self):
try:
from gosling_widget import GoslingWidget
from ._widget import GoslingWidget
except ImportError:
raise ImportError(
"The 'gosling-widget' package is required to use the widget() method."
Expand Down
203 changes: 20 additions & 183 deletions notebooks/example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "2a014a14-3d7c-4288-8534-df01d7bf2432",
"metadata": {},
"outputs": [],
Expand All @@ -12,57 +12,10 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "61233841",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Track({\n",
" color: Color({\n",
" shorthand: 's1:N'\n",
" }),\n",
" data: {'type': 'csv', 'url': 'https://raw.githubusercontent.com/sehilyi/gemini-datasets/master/data/circos-segdup-edited.txt', 'chromosomeField': 'c2', 'genomicFields': ['s1', 'e1', 's2', 'e2']},\n",
" height: 130,\n",
" mark: 'withinLink',\n",
" opacity: OpacityValue({\n",
" value: 0.2\n",
" }),\n",
" stroke: StrokeValue({\n",
" value: 'black'\n",
" }),\n",
" strokeWidth: StrokeWidthValue({\n",
" value: 0.5\n",
" }),\n",
" width: 600,\n",
" x: X({\n",
" domain: GenomicDomain({\n",
" chromosome: '1',\n",
" interval: [103900000, 104100000]\n",
" }),\n",
" shorthand: 's1:G'\n",
" }),\n",
" x1: X1({\n",
" domain: GenomicDomain({\n",
" chromosome: '1'\n",
" }),\n",
" shorthand: 's2:G'\n",
" }),\n",
" x1e: X1e({\n",
" shorthand: 'e2:G'\n",
" }),\n",
" xe: Xe({\n",
" shorthand: 'e1:G'\n",
" })\n",
"})"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"csvData = gos.csv(\n",
" url=\"https://raw.githubusercontent.com/sehilyi/gemini-datasets/master/data/circos-segdup-edited.txt\",\n",
Expand All @@ -87,141 +40,15 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "88f3a522",
"metadata": {
"scrolled": true
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"<!DOCTYPE html>\n",
"<html>\n",
"<head>\n",
" <style>.error { color: red; }</style>\n",
" <link rel=\"stylesheet\" href=\"https://unpkg.com/higlass@1.11/dist/hglib.css\">\n",
"</head>\n",
"<body>\n",
" <div id=\"jupyter-gosling-b21fb3e30816413d991b4879dae37703\"></div>\n",
" <script type=\"module\">\n",
"\n",
" async function loadScript(src) {\n",
" return new Promise(resolve => {\n",
" const script = document.createElement('script');\n",
" script.onload = resolve;\n",
" script.src = src;\n",
" script.async = false;\n",
" document.head.appendChild(script);\n",
" });\n",
" }\n",
"\n",
" async function loadGosling() {\n",
" // Manually load scripts from window namespace since requirejs might not be\n",
" // available in all browser environments.\n",
" // https://github.com/DanielHreben/requirejs-toggle\n",
" if (!window.gosling) {\n",
" window.__requirejsToggleBackup = {\n",
" define: window.define,\n",
" require: window.require,\n",
" requirejs: window.requirejs,\n",
" };\n",
" for (const field of Object.keys(window.__requirejsToggleBackup)) {\n",
" window[field] = undefined;\n",
" }\n",
"\n",
" // load dependencies sequentially\n",
" const sources = [\n",
" \"https://unpkg.com/react@17/umd/react.production.min.js\",\n",
" \"https://unpkg.com/react-dom@17/umd/react-dom.production.min.js\",\n",
" \"https://unpkg.com/pixi.js@6/dist/browser/pixi.min.js\",\n",
" \"https://unpkg.com/gosling.js@0.9.4/dist/gosling.js\",\n",
" ];\n",
"\n",
" for (const src of sources) await loadScript(src);\n",
"\n",
" // restore requirejs after scripts have loaded\n",
" Object.assign(window, window.__requirejsToggleBackup);\n",
" delete window.__requirejsToggleBackup;\n",
" }\n",
" return window.gosling;\n",
" };\n",
"\n",
" var el = document.getElementById('jupyter-gosling-b21fb3e30816413d991b4879dae37703');\n",
" var spec = {\"tracks\": [{\"data\": {\"type\": \"csv\", \"url\": \"https://raw.githubusercontent.com/sehilyi/gemini-datasets/master/data/circos-segdup-edited.txt\", \"chromosomeField\": \"c2\", \"genomicFields\": [\"s1\", \"e1\", \"s2\", \"e2\"]}, \"height\": 130, \"mark\": \"withinLink\", \"width\": 600, \"color\": {\"field\": \"s1\", \"type\": \"nominal\"}, \"opacity\": {\"value\": 0.2}, \"stroke\": {\"value\": \"black\"}, \"strokeWidth\": {\"value\": 0.5}, \"x\": {\"domain\": {\"chromosome\": \"1\", \"interval\": [103900000, 104100000]}, \"field\": \"s1\", \"type\": \"genomic\"}, \"x1\": {\"domain\": {\"chromosome\": \"1\"}, \"field\": \"s2\", \"type\": \"genomic\"}, \"x1e\": {\"field\": \"e2\", \"type\": \"genomic\"}, \"xe\": {\"field\": \"e1\", \"type\": \"genomic\"}}], \"title\": \"Basic Marks: Bar\", \"subtitle\": \"Tutorial Examples\"};\n",
" var opt = {\"padding\": 0, \"theme\": null};\n",
"\n",
" loadGosling()\n",
" .then(gosling => gosling.embed(el, spec, opt))\n",
" .catch(err => {\n",
" el.innerHTML = `<div class=\"error\">\n",
" <p>JavaScript Error: ${error.message}</p>\n",
" <p>This usually means there's a typo in your Gosling specification. See the javascript console for the full traceback.</p>\n",
"</div>`;\n",
" throw error;\n",
" });\n",
" </script>\n",
"</body>\n",
"</html>"
],
"text/plain": [
"View({\n",
" subtitle: 'Tutorial Examples',\n",
" title: 'Basic Marks: Bar',\n",
" tracks: [Track({\n",
" color: Color({\n",
" field: 's1',\n",
" type: 'nominal'\n",
" }),\n",
" data: {'type': 'csv', 'url': 'https://raw.githubusercontent.com/sehilyi/gemini-datasets/master/data/circos-segdup-edited.txt', 'chromosomeField': 'c2', 'genomicFields': ['s1', 'e1', 's2', 'e2']},\n",
" height: 130,\n",
" mark: 'withinLink',\n",
" opacity: OpacityValue({\n",
" value: 0.2\n",
" }),\n",
" stroke: StrokeValue({\n",
" value: 'black'\n",
" }),\n",
" strokeWidth: StrokeWidthValue({\n",
" value: 0.5\n",
" }),\n",
" width: 600,\n",
" x: X({\n",
" domain: GenomicDomain({\n",
" chromosome: '1',\n",
" interval: [103900000, 104100000]\n",
" }),\n",
" field: 's1',\n",
" type: 'genomic'\n",
" }),\n",
" x1: X1({\n",
" domain: GenomicDomain({\n",
" chromosome: '1'\n",
" }),\n",
" field: 's2',\n",
" type: 'genomic'\n",
" }),\n",
" x1e: X1e({\n",
" field: 'e2',\n",
" type: 'genomic'\n",
" }),\n",
" xe: Xe({\n",
" field: 'e1',\n",
" type: 'genomic'\n",
" })\n",
" })]\n",
"})"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"metadata": {},
"outputs": [],
"source": [
"# just render a view (not a mutable object)\n",
"track.view(title=\"Basic Marks: Bar\", subtitle=\"Tutorial Examples\")"
"view = track.view(title=\"Basic Marks: Bar\", subtitle=\"Tutorial Examples\", id=\"aa\")\n",
"w = view.widget()\n",
"w"
]
},
{
Expand All @@ -230,6 +57,16 @@
"id": "7a82cfcc",
"metadata": {},
"outputs": [],
"source": [
"view.id"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "05d7ecf9-d336-4daa-8d49-447d062f7236",
"metadata": {},
"outputs": [],
"source": []
}
],
Expand All @@ -249,7 +86,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
"version": "3.12.7"
}
},
"nbformat": 4,
Expand Down
Loading

0 comments on commit 79527d0

Please sign in to comment.