Skip to content

Commit 79527d0

Browse files
authored
Make GoslingWidget an anywidget (#162)
* 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
1 parent 134b34a commit 79527d0

File tree

13 files changed

+1052
-826
lines changed

13 files changed

+1052
-826
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ concurrency:
1414
env:
1515
PYTHONUNBUFFERED: "1"
1616
FORCE_COLOR: "1"
17+
SKIP_DENO_BUILD: "1"
1718

1819
jobs:
1920

.github/workflows/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ concurrency:
1414
group: "pages"
1515
cancel-in-progress: true
1616

17+
env:
18+
SKIP_DENO_BUILD: "1"
19+
1720
jobs:
1821

1922
Deploy:

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ jobs:
2020
- uses: actions/checkout@v4
2121
with:
2222
fetch-depth: 0
23+
- uses: denoland/setup-deno@v1
24+
with:
25+
deno-version: v2.x
2326
- uses: astral-sh/setup-uv@v3
2427
with:
2528
version: "0.5.x"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ node_modules/
1616
_build/
1717
generated/
1818
gallery/
19+
gosling/static/

deno.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"lock": false,
3+
"nodeModulesDir": "auto",
4+
"tasks": {
5+
"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",
6+
"build": "deno run -A --node-modules-dir npm:esbuild --bundle --minify --loader:.css=text --format=esm --outfile=gosling/static/widget.js frontend/widget.ts"
7+
},
8+
"imports": {
9+
"@anywidget/types": "npm:@anywidget/types@^0.2.0",
10+
"gosling.js": "npm:gosling.js@^0.17.0"
11+
},
12+
"fmt": {
13+
"useTabs": true
14+
},
15+
"lint": {
16+
"rules": {
17+
"exclude": ["prefer-const"]
18+
}
19+
}
20+
}

frontend/widget.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as gosling from "gosling.js";
2+
3+
interface Model {
4+
_viewconf: string;
5+
}
6+
7+
export default {
8+
async render({ model, el }: import("@anywidget/types").RenderProps<Model>) {
9+
const viewconf = JSON.parse(model.get("_viewconf"));
10+
const api = await gosling.embed(el, viewconf, { padding: 0 });
11+
model.on("msg:custom", (msg) => {
12+
msg = JSON.parse(msg);
13+
console.log(msg);
14+
try {
15+
const [fn, ...args] = msg;
16+
// @ts-expect-error - This is a dynamic call
17+
api[fn](...args);
18+
} catch (e) {
19+
console.error(e);
20+
}
21+
});
22+
},
23+
};

gosling/_widget.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
import pathlib
3+
from typing import Any, Dict
4+
import warnings
5+
6+
import anywidget
7+
import traitlets as t
8+
9+
_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
10+
11+
if not _esm.exists():
12+
warnings.warn("Widget front-end code not found. Assuming running in CI.")
13+
_esm = None
14+
15+
16+
class GoslingWidget(anywidget.AnyWidget):
17+
_esm = _esm
18+
_viewconf = t.Unicode("null").tag(sync=True)
19+
20+
location = t.List(t.Union([t.Float(), t.Tuple()]), read_only=True).tag(sync=True)
21+
22+
def __init__(self, viewconf: Dict[str, Any], **kwargs):
23+
super().__init__(_viewconf=json.dumps(viewconf), **kwargs)
24+
25+
def zoom_to(self, view_id: str, pos: str, padding: float = 0, duration: int = 1000):
26+
msg = json.dumps(["zoomTo", view_id, pos, padding, duration])
27+
self.send(msg)

gosling/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def save(
218218

219219
def widget(self):
220220
try:
221-
from gosling_widget import GoslingWidget
221+
from ._widget import GoslingWidget
222222
except ImportError:
223223
raise ImportError(
224224
"The 'gosling-widget' package is required to use the widget() method."

notebooks/example.ipynb

Lines changed: 20 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"cells": [
33
{
44
"cell_type": "code",
5-
"execution_count": 1,
5+
"execution_count": null,
66
"id": "2a014a14-3d7c-4288-8534-df01d7bf2432",
77
"metadata": {},
88
"outputs": [],
@@ -12,57 +12,10 @@
1212
},
1313
{
1414
"cell_type": "code",
15-
"execution_count": 2,
15+
"execution_count": null,
1616
"id": "61233841",
1717
"metadata": {},
18-
"outputs": [
19-
{
20-
"data": {
21-
"text/plain": [
22-
"Track({\n",
23-
" color: Color({\n",
24-
" shorthand: 's1:N'\n",
25-
" }),\n",
26-
" 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",
27-
" height: 130,\n",
28-
" mark: 'withinLink',\n",
29-
" opacity: OpacityValue({\n",
30-
" value: 0.2\n",
31-
" }),\n",
32-
" stroke: StrokeValue({\n",
33-
" value: 'black'\n",
34-
" }),\n",
35-
" strokeWidth: StrokeWidthValue({\n",
36-
" value: 0.5\n",
37-
" }),\n",
38-
" width: 600,\n",
39-
" x: X({\n",
40-
" domain: GenomicDomain({\n",
41-
" chromosome: '1',\n",
42-
" interval: [103900000, 104100000]\n",
43-
" }),\n",
44-
" shorthand: 's1:G'\n",
45-
" }),\n",
46-
" x1: X1({\n",
47-
" domain: GenomicDomain({\n",
48-
" chromosome: '1'\n",
49-
" }),\n",
50-
" shorthand: 's2:G'\n",
51-
" }),\n",
52-
" x1e: X1e({\n",
53-
" shorthand: 'e2:G'\n",
54-
" }),\n",
55-
" xe: Xe({\n",
56-
" shorthand: 'e1:G'\n",
57-
" })\n",
58-
"})"
59-
]
60-
},
61-
"execution_count": 2,
62-
"metadata": {},
63-
"output_type": "execute_result"
64-
}
65-
],
18+
"outputs": [],
6619
"source": [
6720
"csvData = gos.csv(\n",
6821
" url=\"https://raw.githubusercontent.com/sehilyi/gemini-datasets/master/data/circos-segdup-edited.txt\",\n",
@@ -87,141 +40,15 @@
8740
},
8841
{
8942
"cell_type": "code",
90-
"execution_count": 3,
43+
"execution_count": null,
9144
"id": "88f3a522",
92-
"metadata": {
93-
"scrolled": true
94-
},
95-
"outputs": [
96-
{
97-
"data": {
98-
"text/html": [
99-
"\n",
100-
"<!DOCTYPE html>\n",
101-
"<html>\n",
102-
"<head>\n",
103-
" <style>.error { color: red; }</style>\n",
104-
" <link rel=\"stylesheet\" href=\"https://unpkg.com/higlass@1.11/dist/hglib.css\">\n",
105-
"</head>\n",
106-
"<body>\n",
107-
" <div id=\"jupyter-gosling-b21fb3e30816413d991b4879dae37703\"></div>\n",
108-
" <script type=\"module\">\n",
109-
"\n",
110-
" async function loadScript(src) {\n",
111-
" return new Promise(resolve => {\n",
112-
" const script = document.createElement('script');\n",
113-
" script.onload = resolve;\n",
114-
" script.src = src;\n",
115-
" script.async = false;\n",
116-
" document.head.appendChild(script);\n",
117-
" });\n",
118-
" }\n",
119-
"\n",
120-
" async function loadGosling() {\n",
121-
" // Manually load scripts from window namespace since requirejs might not be\n",
122-
" // available in all browser environments.\n",
123-
" // https://github.com/DanielHreben/requirejs-toggle\n",
124-
" if (!window.gosling) {\n",
125-
" window.__requirejsToggleBackup = {\n",
126-
" define: window.define,\n",
127-
" require: window.require,\n",
128-
" requirejs: window.requirejs,\n",
129-
" };\n",
130-
" for (const field of Object.keys(window.__requirejsToggleBackup)) {\n",
131-
" window[field] = undefined;\n",
132-
" }\n",
133-
"\n",
134-
" // load dependencies sequentially\n",
135-
" const sources = [\n",
136-
" \"https://unpkg.com/react@17/umd/react.production.min.js\",\n",
137-
" \"https://unpkg.com/react-dom@17/umd/react-dom.production.min.js\",\n",
138-
" \"https://unpkg.com/pixi.js@6/dist/browser/pixi.min.js\",\n",
139-
" \"https://unpkg.com/gosling.js@0.9.4/dist/gosling.js\",\n",
140-
" ];\n",
141-
"\n",
142-
" for (const src of sources) await loadScript(src);\n",
143-
"\n",
144-
" // restore requirejs after scripts have loaded\n",
145-
" Object.assign(window, window.__requirejsToggleBackup);\n",
146-
" delete window.__requirejsToggleBackup;\n",
147-
" }\n",
148-
" return window.gosling;\n",
149-
" };\n",
150-
"\n",
151-
" var el = document.getElementById('jupyter-gosling-b21fb3e30816413d991b4879dae37703');\n",
152-
" 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",
153-
" var opt = {\"padding\": 0, \"theme\": null};\n",
154-
"\n",
155-
" loadGosling()\n",
156-
" .then(gosling => gosling.embed(el, spec, opt))\n",
157-
" .catch(err => {\n",
158-
" el.innerHTML = `<div class=\"error\">\n",
159-
" <p>JavaScript Error: ${error.message}</p>\n",
160-
" <p>This usually means there's a typo in your Gosling specification. See the javascript console for the full traceback.</p>\n",
161-
"</div>`;\n",
162-
" throw error;\n",
163-
" });\n",
164-
" </script>\n",
165-
"</body>\n",
166-
"</html>"
167-
],
168-
"text/plain": [
169-
"View({\n",
170-
" subtitle: 'Tutorial Examples',\n",
171-
" title: 'Basic Marks: Bar',\n",
172-
" tracks: [Track({\n",
173-
" color: Color({\n",
174-
" field: 's1',\n",
175-
" type: 'nominal'\n",
176-
" }),\n",
177-
" 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",
178-
" height: 130,\n",
179-
" mark: 'withinLink',\n",
180-
" opacity: OpacityValue({\n",
181-
" value: 0.2\n",
182-
" }),\n",
183-
" stroke: StrokeValue({\n",
184-
" value: 'black'\n",
185-
" }),\n",
186-
" strokeWidth: StrokeWidthValue({\n",
187-
" value: 0.5\n",
188-
" }),\n",
189-
" width: 600,\n",
190-
" x: X({\n",
191-
" domain: GenomicDomain({\n",
192-
" chromosome: '1',\n",
193-
" interval: [103900000, 104100000]\n",
194-
" }),\n",
195-
" field: 's1',\n",
196-
" type: 'genomic'\n",
197-
" }),\n",
198-
" x1: X1({\n",
199-
" domain: GenomicDomain({\n",
200-
" chromosome: '1'\n",
201-
" }),\n",
202-
" field: 's2',\n",
203-
" type: 'genomic'\n",
204-
" }),\n",
205-
" x1e: X1e({\n",
206-
" field: 'e2',\n",
207-
" type: 'genomic'\n",
208-
" }),\n",
209-
" xe: Xe({\n",
210-
" field: 'e1',\n",
211-
" type: 'genomic'\n",
212-
" })\n",
213-
" })]\n",
214-
"})"
215-
]
216-
},
217-
"execution_count": 3,
218-
"metadata": {},
219-
"output_type": "execute_result"
220-
}
221-
],
45+
"metadata": {},
46+
"outputs": [],
22247
"source": [
22348
"# just render a view (not a mutable object)\n",
224-
"track.view(title=\"Basic Marks: Bar\", subtitle=\"Tutorial Examples\")"
49+
"view = track.view(title=\"Basic Marks: Bar\", subtitle=\"Tutorial Examples\", id=\"aa\")\n",
50+
"w = view.widget()\n",
51+
"w"
22552
]
22653
},
22754
{
@@ -230,6 +57,16 @@
23057
"id": "7a82cfcc",
23158
"metadata": {},
23259
"outputs": [],
60+
"source": [
61+
"view.id"
62+
]
63+
},
64+
{
65+
"cell_type": "code",
66+
"execution_count": null,
67+
"id": "05d7ecf9-d336-4daa-8d49-447d062f7236",
68+
"metadata": {},
69+
"outputs": [],
23370
"source": []
23471
}
23572
],
@@ -249,7 +86,7 @@
24986
"name": "python",
25087
"nbconvert_exporter": "python",
25188
"pygments_lexer": "ipython3",
252-
"version": "3.9.7"
89+
"version": "3.12.7"
25390
}
25491
},
25592
"nbformat": 4,

0 commit comments

Comments
 (0)