///
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");
});
}