/// import type { Config, Data, Layout, // deno-lint-ignore no-unused-vars newPlot, // deno-lint-ignore no-unused-vars Plots, } from "npm:@types/plotly.js-dist-min"; export { Config, Data, Layout }; import webview from "npm:webview"; import { open } from "jsr:@opensrc/deno-open"; export type PlotOptions = { width: number; height: number; title: string; engine: "webview" | "browser"; }; export class Plot { static #port: number; static #server = Deno.serve({ port: 0, onListen: ({ port }) => this.#port = port, }, (req: Request) => { const { pathname, searchParams } = new URL(req.url); if (pathname === "/") { return new Response(html(), { headers: { "Content-Type": "text/html; charset=utf-8", }, }); } if (pathname === "/favicon.ico") { return new Response(null); } if (pathname === "/ws") { const { socket, response } = Deno.upgradeWebSocket(req); socket.addEventListener( "open", () => { const uuid = searchParams.get("uuid") ?? ""; this.#wsList.set(uuid, socket); this.#connectionPool.get(uuid)?.(); this.#connectionPool.delete(uuid); }, ); return response; } return new Response(null, { status: 400 }); }); static #wsList = new Map(); static #windows: Deno.ChildProcess[] = []; static #connectionPool = new Map void>(); #window: Deno.ChildProcess | undefined; #size: [number, number]; #title: string | undefined; #uuid = crypto.randomUUID(); #ws = () => Plot.#wsList.get(this.#uuid); #unbined = false; #engine: "webview" | "browser"; constructor(options: Partial = {}) { this.#title = options.title; this.#size = [options.width ?? 400, options.height ?? 400]; this.#engine = options.engine ?? "webview"; } update( data: Partial, layout: Partial | null = null, ) { this.#ws()?.send( JSON.stringify({ kind: "plot.update", value: { data, layout } }), ); } async plot( data: Partial, layout: Partial = {}, config: Partial = {}, ) { const cmd = new Deno.Command(webview.binaryPath, { args: [ `--url=http://localhost:${Plot.#port}?uuid=${this.#uuid}`, `--title="${this.#title ?? `Plot ${Plot.#port}`}"`, `--width=${this.#size[0]}`, `--height=${this.#size[1]}`, ], }); if (this.#engine === "browser") { open(`http://localhost:${Plot.#port}?uuid=${this.#uuid}`); } else { this.#window = cmd.spawn(); } Plot.#windows.push(this.#window); const { promise, resolve } = Promise.withResolvers(); Plot.#connectionPool.set(this.#uuid, resolve); await promise; this.#ws()?.send( JSON.stringify({ kind: "plot.plot", value: { data, layout, config } }), ); } #moveWindow(x = 0, y = 0) { Plot.#wsList.get(this.#uuid)?.send( JSON.stringify({ kind: "window.move", value: { x, y } }), ); } #resizeWindow(width: number, height: number) { this.#ws()?.send( JSON.stringify({ kind: "window.resize", value: { width, height } }), ); } get window(): { move: (x: number, y: number) => void; resize: (width: number, height: number) => void; } { return { move: this.#moveWindow, resize: this.#resizeWindow }; } unbind() { this.#unbined = true; } [Symbol.dispose]() { if (this.#unbined) return; this.#ws()?.close(0, "instance dispose"); if (this.#engine === "webview") { this.#window?.kill(); } } static [Symbol.dispose]() { this.#wsList.forEach((socket) => socket.close(0, "instance dispose")); this.#wsList.clear(); this.#server.shutdown(); } } function html() { return `
`; } function client() { if ("Deno" in globalThis) throw new Error("client only function"); const url = new URL(location.href); const uuid = url.searchParams.get("uuid") ?? crypto.randomUUID(); const ws = new WebSocket(`${location.origin}/ws?uuid=${uuid}`); const root = document.querySelector("#root")!; // ws.addEventListener("open", () => {}); ws.addEventListener("close", () => { close(); }); ws.addEventListener("message", ({ data }) => { const { kind, value } = JSON.parse(data); if (kind === "plot.plot") { // use webgl as default type (value.data as Record[]).forEach((data) => data.type = data?.type ?? "scattergl" ); //@ts-expect-error client side load from script tag Plotly.newPlot( root, value.data, value.layout, { responsive: true, ...(value.config ?? {}) }, ); } else if (kind === "plot.update") { //@ts-expect-error client side load from script tag Plotly.react(root, value.data, value.layout); } else if (kind === "window.move") { globalThis.moveTo(value.x, value.y); } else if (kind === "window.resize") { globalThis.resizeTo(value.width, value.height); } }); addEventListener("beforeunload", () => { ws.close(1000, "window closed"); }); }