Skip to content

Commit b9f4447

Browse files
authored
refactor(frontend): Custom data fetcher setup (#179)
* Disable deno no-window error * Refactor data fetcher registration * Update examples * Registration * Add linting and typechecking with deno * cleanup notebook
1 parent 8965585 commit b9f4447

File tree

6 files changed

+101
-84
lines changed

6 files changed

+101
-84
lines changed

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ jobs:
3434
env:
3535
UV_PYTHON: ${{ matrix.python-version }}
3636

37+
LintJavaScript:
38+
name: JavaScript / Lint
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
- uses: denoland/setup-deno@v2
43+
with:
44+
deno-version: v2.x
45+
- run: |
46+
deno fmt --check
47+
deno lint
48+
49+
TypecheckJavaScript:
50+
name: JavaScript / Typecheck
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v4
54+
- uses: denoland/setup-deno@v2
55+
with:
56+
deno-version: v2.x
57+
- run: |
58+
deno check src/higlass/widget.js
59+
3760
Release:
3861
needs: [Lint, Test]
3962
runs-on: ubuntu-latest

deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"lint": {
88
"rules": {
9-
"exclude": ["prefer-const"]
9+
"exclude": ["prefer-const", "no-window"]
1010
}
1111
},
1212
"fmt": {

examples/JupyterServer.ipynb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"source": [
2020
"import higlass as hg\n",
2121
"\n",
22-
"ts = hg.cooler(\"./test.mcool\") # local tileset\n",
23-
"hg.view(ts.track(\"heatmap\"), width=6)"
22+
"ts1 = hg.cooler(\"./test.mcool\") # local tileset\n",
23+
"hg.view(ts1.track(\"heatmap\"), width=6)"
2424
]
2525
},
2626
{
@@ -47,8 +47,8 @@
4747
" return tileset_info(self.path)\n",
4848
"\n",
4949
"\n",
50-
"ts = MyCustomCoolerTileset(\"test.mcool\")\n",
51-
"hg.view(ts.track())"
50+
"ts2 = MyCustomCoolerTileset(\"test.mcool\")\n",
51+
"hg.view(ts2.track())"
5252
]
5353
},
5454
{

examples/Plugin.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
"\n",
124124
"This can seem a bit verbose, but supplying the type parameter explicitly is only necessary when deserializing an unknown config, e.g.\n",
125125
"\n",
126-
"```python\n",
126+
"```py\n",
127127
"hg.Viewconf.parse_file('./pileup-example.json') # error\n",
128128
"hg.Viewconf[PileupTrack].parse_file('./pileup-example.json') # works!\n",
129129
"```\n",
@@ -237,12 +237,12 @@
237237
"source": [
238238
"from typing import Optional\n",
239239
"\n",
240-
"from pydantic import BaseModel, Extra\n",
240+
"from pydantic import BaseModel\n",
241241
"\n",
242242
"\n",
243243
"class SeqeuenceTrackData(BaseModel):\n",
244244
" class Config:\n",
245-
" extra = Extra.forbid\n",
245+
" extra = \"forbid\"\n",
246246
"\n",
247247
" type: Literal[\"fasta\"]\n",
248248
" fastaUrl: str\n",
@@ -283,7 +283,7 @@
283283
"metadata": {},
284284
"outputs": [],
285285
"source": [
286-
"track.data.json()"
286+
"track.data.model_dump_json()"
287287
]
288288
},
289289
{
@@ -293,7 +293,7 @@
293293
"metadata": {},
294294
"outputs": [],
295295
"source": [
296-
"track.data.dict()"
296+
"track.data.model_dump()"
297297
]
298298
},
299299
{

src/higlass/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type TileSource<T> = {
3838
registerTileset: (request: RegisterTilesetRequest) => Promise<Response>;
3939
};
4040

41-
type DataFetcher = {
41+
export type DataFetcher = {
4242
// Not available at runtime! (just used to mark the type for typescript)
4343
_tag: "DataFetcher";
4444
};

src/higlass/widget.js

Lines changed: 67 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import * as hglib from "https://esm.sh/[email protected]?deps=react@17,react-dom@17,pixi.js@6";
22
import { v4 } from "https://esm.sh/@lukeed/[email protected]";
33

4-
/** @import { HGC, PluginDataFetcherConstructor, GenomicLocation, Viewconf } from "./types.ts" */
4+
/** @import { HGC, PluginDataFetcherConstructor, GenomicLocation, Viewconf, DataFetcher} from "./types.ts" */
55

6-
// Make sure plugins are registered and enabled
7-
window.higlassDataFetchersByType = window.higlassDataFetchersByType ||
8-
{};
6+
const NAME = "jupyter";
97

108
/**
119
* @param {string} href
@@ -47,7 +45,8 @@ async function requireScripts(pluginUrls) {
4745
// @ts-expect-error - not on the window
4846
requirejs: window.requirejs,
4947
};
50-
for (const field of Object.keys(backup)) {
48+
for (let field of Object.keys(backup)) {
49+
// @ts-expect-error - not on the window
5150
window[field] = undefined;
5251
}
5352

@@ -170,10 +169,10 @@ function resolveJupyterServers(viewConfig) {
170169
let copy = JSON.parse(JSON.stringify(viewConfig));
171170
for (let view of copy.views) {
172171
for (let track of Object.values(view.tracks).flat()) {
173-
if (track?.server === "jupyter") {
172+
if (track?.server === NAME) {
174173
delete track.server;
175174
track.data = track.data || {};
176-
track.data.type = "jupyter";
175+
track.data.type = NAME;
177176
track.data.tilesetUid = track.tilesetUid;
178177
}
179178
}
@@ -182,32 +181,34 @@ function resolveJupyterServers(viewConfig) {
182181
}
183182

184183
/**
185-
* @param {import("npm:@anywidget/types").AnyModel} model
186-
* @returns{PluginDataFetcherConstructor}
187-
*/
188-
function createDataFetcherForModel(model) {
189-
/**
190-
* @param {HGC} hgc
191-
* @param {Record<string, unknown>} dataConfig
192-
* @param {unknown} pubSub
193-
*/
194-
const DataFetcher = function createDataFetcher(hgc, dataConfig, pubSub) {
195-
let config = { ...dataConfig, server: "jupyter" };
184+
* @param {import("npm:@anywidget/[email protected]").AnyModel<State>} model */
185+
async function registerJupyterHiGlassDataFetcher(model) {
186+
if (window?.higlassDataFetchersByType?.[NAME]) {
187+
return;
188+
}
189+
190+
let tModel = await model.widget_manager.get_model(
191+
model.get("_tileset_client").slice("IPY_MODEL_".length),
192+
);
193+
194+
/** @type {(...args: ConstructorParameters<PluginDataFetcherConstructor>) => DataFetcher} */
195+
function DataFetcher(hgc, dataConfig, pubSub) {
196+
let config = { ...dataConfig, server: NAME };
196197
return new hgc.dataFetchers.DataFetcher(config, pubSub, {
197198
async fetchTilesetInfo({ server, tilesetUid }) {
198-
assert(server === "jupyter", "must be a jupyter server");
199-
let response = await sendCustomMessage(model, {
199+
assert(server === NAME, "must be a jupyter server");
200+
let response = await sendCustomMessage(tModel, {
200201
payload: { type: "tileset_info", tilesetUid },
201202
});
202203
return response.payload;
203204
},
204205
async fetchTiles({ tileIds }) {
205-
let response = await sendCustomMessage(model, {
206+
let response = await sendCustomMessage(tModel, {
206207
payload: { type: "tiles", tileIds },
207208
});
208209
let result = hgc.services.tileResponseToData(
209210
response.payload,
210-
config.server,
211+
NAME,
211212
tileIds,
212213
);
213214
return result;
@@ -216,9 +217,14 @@ function createDataFetcherForModel(model) {
216217
throw new Error("Not implemented");
217218
},
218219
});
219-
};
220+
}
220221

221-
return /** @type{any} */ (DataFetcher);
222+
/** @type {PluginDataFetcherConstructor} */
223+
// @ts-expect-error - classic function definition (above) supports `new` invocation
224+
let dataFetcher = DataFetcher;
225+
226+
window.higlassDataFetchersByType ??= {};
227+
window.higlassDataFetchersByType[NAME] = { name: NAME, dataFetcher };
222228
}
223229

224230
/**
@@ -250,61 +256,49 @@ function addEventListenersTo(el) {
250256
* @typedef State
251257
* @property {Viewconf} _viewconf
252258
* @property {Record<string, unknown>} _options
253-
* @property {`IPYMODEL_${string}`} _tileset_client
259+
* @property {`IPY_MODEL_${string}`} _tileset_client
254260
* @property {Array<number> | Array<Array<number>>} location
255261
* @property {Array<string>} _plugin_urls
256262
*/
257263

258-
export default () => {
259-
/** @type {Promise<void>} */
260-
let scriptsPromise = Promise.resolve();
261-
return {
262-
/** @type {import("npm:@anywidget/[email protected]").Initialize<State>} */
263-
async initialize({ model }) {
264-
scriptsPromise = requireScripts(model.get("_plugin_urls"));
265-
let tilesetClientModel = await model.widget_manager.get_model(
266-
model.get("_tileset_client").slice("IPY_MODEL_".length),
267-
);
268-
window.higlassDataFetchersByType["jupyter"] = {
269-
name: "jupyter",
270-
dataFetcher: createDataFetcherForModel(tilesetClientModel),
271-
};
272-
},
273-
/** @type {import("npm:@anywidget/types").Render<State>} */
274-
async render({ model, el }) {
275-
await scriptsPromise;
276-
let viewconf = resolveJupyterServers(
277-
model.get("_viewconf"),
278-
);
279-
let options = model.get("_options") ?? {};
280-
let api = await hglib.viewer(el, viewconf, options);
281-
let unlisten = addEventListenersTo(el);
264+
export default {
265+
/** @type {import("npm:@anywidget/types").Render<State>} */
266+
async render({ model, el }) {
267+
await Promise.all([
268+
requireScripts(model.get("_plugin_urls")),
269+
registerJupyterHiGlassDataFetcher(model),
270+
]);
271+
let viewconf = resolveJupyterServers(
272+
model.get("_viewconf"),
273+
);
274+
let options = model.get("_options") ?? {};
275+
let api = await hglib.viewer(el, viewconf, options);
276+
let unlisten = addEventListenersTo(el);
282277

283-
model.on("msg:custom", (msg) => {
284-
msg = JSON.parse(msg);
285-
let [fn, ...args] = msg;
286-
api[fn](...args);
287-
});
278+
model.on("msg:custom", (msg) => {
279+
msg = JSON.parse(msg);
280+
let [fn, ...args] = msg;
281+
api[fn](...args);
282+
});
288283

289-
if (viewconf.views.length === 1) {
290-
api.on("location", (/** @type {GenomicLocation} */ loc) => {
291-
model.set("location", locationToCoordinates(loc));
284+
if (viewconf.views.length === 1) {
285+
api.on("location", (/** @type {GenomicLocation} */ loc) => {
286+
model.set("location", locationToCoordinates(loc));
287+
model.save_changes();
288+
}, viewconf.views[0].uid);
289+
} else {
290+
viewconf.views.forEach((view, idx) => {
291+
api.on("location", (/** @type{GenomicLocation} */ loc) => {
292+
let location = model.get("location").slice();
293+
location[idx] = locationToCoordinates(loc);
294+
model.set("location", location);
292295
model.save_changes();
293-
}, viewconf.views[0].uid);
294-
} else {
295-
viewconf.views.forEach((view, idx) => {
296-
api.on("location", (/** @type{GenomicLocation} */ loc) => {
297-
let location = model.get("location").slice();
298-
location[idx] = locationToCoordinates(loc);
299-
model.set("location", location);
300-
model.save_changes();
301-
}, view.uid);
302-
});
303-
}
296+
}, view.uid);
297+
});
298+
}
304299

305-
return () => {
306-
unlisten();
307-
};
308-
},
309-
};
300+
return () => {
301+
unlisten();
302+
};
303+
},
310304
};

0 commit comments

Comments
 (0)