summaryrefslogtreecommitdiff
path: root/graph/web/viewer.js
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-09-21 16:26:15 -0700
committerCliff Click <cliffc@acm.org>2026-09-21 16:26:15 -0700
commitd2ffb24d0a3181d107f80df76570c04e010833ed (patch)
treec78b75b2dcca46a4f7513e27fa9f20e43bab787c /graph/web/viewer.js
parent40b33cb6f8b8dc4d8551c3b522e7c5682a44540a (diff)
downloadSimple-d2ffb24d0a3181d107f80df76570c04e010833ed.tar.gz
Simple-d2ffb24d0a3181d107f80df76570c04e010833ed.zip
move the graph visualizer into shared code
Diffstat (limited to 'graph/web/viewer.js')
-rw-r--r--graph/web/viewer.js143
1 files changed, 143 insertions, 0 deletions
diff --git a/graph/web/viewer.js b/graph/web/viewer.js
new file mode 100644
index 00000000..3234617c
--- /dev/null
+++ b/graph/web/viewer.js
@@ -0,0 +1,143 @@
+// Connection and playback state are independent of renderer initialization.
+const program = document.getElementById("program");
+const status = document.getElementById("status");
+let socket;
+let renderer;
+let rendererReady = false;
+let rendering = false;
+let frames = [];
+let current = -1;
+let generation = 0;
+let done = false;
+let compiling = false;
+let connection = "Connecting to compiler...";
+let failure = "";
+
+function updateUI() {
+ status.textContent = failure || (connection !== "Connected" ? connection :
+ !rendererReady ? "Connected; initializing graph renderer..." :
+ compiling ? "Compiling... " + frames.length + " frames received" :
+ done ? frames.length + " frames ready" : "Connected");
+ document.getElementById("N").textContent = current < 0 ? "0" : String(current + 1);
+ document.getElementById("len").textContent = String(frames.length);
+ document.getElementById("doPrev").disabled = rendering || current <= 0;
+ document.getElementById("doNext").disabled =
+ !rendererReady || rendering || current + 1 >= frames.length;
+ document.getElementById("compile").disabled =
+ !socket || socket.readyState !== WebSocket.OPEN || compiling || rendering;
+}
+
+function reportError(error) {
+ failure = "Viewer error: " + (error && error.message || error || "Graph rendering failed");
+ rendering = false;
+ updateUI();
+ console.error(error);
+}
+
+// Surface startup failures instead of leaving a permanent "Connecting..." label.
+window.addEventListener("error", event => reportError(event.error || event.message));
+window.addEventListener("unhandledrejection", event => reportError(event.reason));
+
+function get_program() {
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
+ connection = "Compiler is not connected. Start make view and use its new tab.";
+ updateUI();
+ return;
+ }
+ if (compiling || rendering) return;
+ generation++;
+ frames = [];
+ current = -1;
+ done = false;
+ compiling = true;
+ failure = "";
+ updateUI();
+ socket.send(program.value);
+}
+
+function render(index) {
+ if (!rendererReady || rendering || index < 0 || index >= frames.length) return;
+ const frameGeneration = generation;
+ rendering = true;
+ updateUI();
+ const position = frames[index].match(/\/\/ POS:\s*(\d+)/);
+ if (position) program.setSelectionRange(Number(position[1]), Number(position[1]) + 1);
+ try {
+ renderer.renderDot(frames[index], () => {
+ rendering = false;
+ if (generation === frameGeneration) current = index;
+ updateUI();
+ });
+ } catch (error) {
+ reportError(error);
+ }
+}
+
+function doNext() { render(current + 1); }
+function doPrev() { render(current - 1); }
+function doExit() {
+ if (socket && socket.readyState === WebSocket.OPEN) socket.send("null");
+}
+
+program.value = "return 0;";
+program.addEventListener("keydown", event => {
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
+ event.preventDefault();
+ get_program();
+ }
+});
+document.getElementById("compile").addEventListener("click", get_program);
+
+try {
+ socket = new WebSocket("ws://" + (location.hostname || "127.0.0.1") + ":12345");
+ socket.onopen = () => {
+ connection = "Connected";
+ updateUI();
+ };
+ socket.onmessage = event => {
+ const message = event.data;
+ if (message === "!") {
+ get_program();
+ } else if (message === "#") {
+ done = true;
+ compiling = false;
+ updateUI();
+ } else if (message.startsWith("digraph")) {
+ frames.push(message);
+ socket.send("+");
+ updateUI();
+ if (frames.length === 1) render(0);
+ } else {
+ reportError("Unexpected compiler response: " + message);
+ }
+ };
+ socket.onclose = () => {
+ compiling = false;
+ connection = "Compiler disconnected. Restart make view and use its new tab.";
+ updateUI();
+ };
+ socket.onerror = () => {
+ connection = "Cannot connect to the compiler. Check the make view terminal.";
+ updateUI();
+ };
+
+ // The bundled renderer contains its own WASM engine; no worker script is needed.
+ const graph = document.getElementById("graph");
+ renderer = d3.select("#graph").graphviz({
+ useWorker: false, fit: true, width: graph.clientWidth, height: graph.clientHeight
+ })
+ .onerror(reportError)
+ .on("initEnd", () => {
+ rendererReady = true;
+ updateUI();
+ if (frames.length && current < 0) render(0);
+ })
+ .transition(() => d3.transition("main").ease(d3.easeLinear).duration(500));
+ window.addEventListener("resize", () => {
+ renderer.width(graph.clientWidth).height(graph.clientHeight);
+ if (current >= 0) render(current);
+ });
+} catch (error) {
+ reportError(error);
+}
+updateUI();