19 std::shared_ptr<TreeState>
state;
29 : impl_(std::make_unique<
Impl>())
31 impl_->state = std::move(state);
39 impl_->thread = std::thread([
this]() {
45 if (
impl_->svr.is_running())
impl_->svr.stop();
46 if (
impl_->thread.joinable())
impl_->thread.join();
53 svr.Get(
"/", [
this](
const httplib::Request&, httplib::Response& res) {
58 svr.Get(
"/pe", [
this](
const httplib::Request&, httplib::Response& res) {
63 svr.Get(
"/api/tree", [
this](
const httplib::Request&, httplib::Response& res) {
66 std::lock_guard<std::mutex> lock(
state->mu);
67 j[
"sequences"] = json::array();
68 for (
auto& r :
state->completed) {
69 j[
"sequences"].push_back({
70 {
"id", r.sequence_id},
71 {
"parent_id", r.parent_sequence_id},
72 {
"branch_time", r.branch_time},
73 {
"end_time", r.end_time},
74 {
"num_children", r.num_branches_generated},
75 {
"worker", r.worker_rank}
79 {
"completed",
state->sequences_completed},
80 {
"queue_depth",
state->queue_depth},
81 {
"free_clients",
state->num_free_clients},
82 {
"total_clients",
state->total_clients},
83 {
"memory_mb",
state->memory_mb},
84 {
"elapsed_s", MPI_Wtime() -
state->start_time}
87 res.set_content(j.dump(),
"application/json");
91 svr.Get(
"/api/events", [
this](
const httplib::Request&, httplib::Response& res) {
92 json j = json::array();
94 std::lock_guard<std::mutex> lock(
state->mu);
95 for (
auto& [seq_id, data] :
state->output_data) {
96 json entry = json::object();
97 entry[
"sequence_id"] = seq_id;
98 entry[
"events"] = json::array();
99 for (
auto& ev : data.events) {
100 entry[
"events"].push_back({
102 {
"statement", ev.statement},
109 res.set_content(j.dump(),
"application/json");
113 svr.Get(
"/api/end-states", [
this](
const httplib::Request&, httplib::Response& res) {
114 json j = json::array();
116 std::lock_guard<std::mutex> lock(
state->mu);
117 for (
auto& [seq_id, data] :
state->output_data) {
119 {
"sequence_id", data.end_state.sequence_id},
120 {
"type_of_consequence", data.end_state.type_of_consequence},
121 {
"mother_branch_id", data.end_state.mother_branch_id},
122 {
"consequence_description", data.end_state.consequence_description},
123 {
"time", data.end_state.time},
124 {
"probability", data.end_state.probability}
128 res.set_content(j.dump(),
"application/json");
135 return R
"html(<!DOCTYPE html>
138<meta charset="UTF-8">
139<meta name="viewport" content="width=device-width, initial-scale=1.0">
140<title>ADS-IDAC Dynamic Event Tree</title>
143 --bg: #0c0e14; --sf: #13151e; --bd: #1e2130;
144 --tx: #e4e4e8; --dim: #a0a0a8; --mut: #6a6a75; --acc: #3b82f6;
146* { margin: 0; padding: 0; box-sizing: border-box; }
147body { background: var(--bg); color: var(--tx); font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; font-size: 13px; overflow: hidden; height: 100vh; }
148#header { display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; background: var(--sf); border-bottom: 1px solid var(--bd); }
149#header h1 { font-size: 15px; font-weight: 600; letter-spacing: 0.5px; }
150#header h1 span { color: var(--acc); }
151#stats { display: flex; gap: 24px; font-size: 12px; }
152.stat-label { color: var(--mut); margin-right: 4px; }
153.stat-value { color: var(--tx); font-weight: 600; }
154.stat-value.active { color: #22c55e; }
155#controls-btn { background: var(--bd); border: 1px solid #2a2d42; color: var(--dim); padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; font-family: inherit; margin-left: 16px; }
156#controls-btn:hover { background: #252840; color: var(--tx); }
157#controls-panel { display: none; position: absolute; top: 42px; right: 230px; background: var(--sf); border: 1px solid var(--bd); border-radius: 6px; padding: 12px 16px; z-index: 20; min-width: 220px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
158#controls-panel.open { display: block; }
159#controls-panel h3 { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--mut); margin-bottom: 8px; }
160.ctrl-row { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
161.ctrl-btn { background: var(--bd); border: 1px solid #2a2d42; color: var(--dim); padding: 3px 10px; border-radius: 3px; cursor: pointer; font-size: 11px; font-family: inherit; }
162.ctrl-btn:hover { background: #252840; color: var(--tx); }
163.ctrl-btn.active { background: var(--acc); color: white; border-color: var(--acc); }
164.collapse-indicator { fill: var(--dim); font-size: 9px; cursor: pointer; user-select: none; }
165.collapse-indicator:hover { fill: var(--tx); }
166.expand-icon { fill: #22c55e; font-size: 11px; cursor: pointer; user-select: none; }
167.expand-icon:hover { fill: #4ade80; }
168.seq-bar { cursor: pointer; }
169.branch-dot { cursor: pointer; stroke: white; stroke-width: 2; transition: r 0.15s; }
170.branch-dot:hover { r: 8; stroke-width: 2.5; filter: drop-shadow(0 0 3px white); }
171.branch-hit { fill: none; stroke: transparent; stroke-width: 20; cursor: pointer; }
172.branch-line:hover { opacity: 0.9 !important; stroke-width: 3; }
173#tooltip { display: none; position: fixed; background: var(--sf); border: 1px solid var(--bd); border-radius: 4px; padding: 8px 10px; font-size: 11px; color: var(--tx); z-index: 30; pointer-events: none; line-height: 1.6; box-shadow: 0 4px 12px rgba(0,0,0,0.5); max-width: 280px; }
174#tooltip b { color: var(--acc); }
175#search-box { display: flex; gap: 4px; margin-bottom: 12px; }
176#search-input { background: var(--bg); border: 1px solid var(--bd); color: var(--tx); padding: 4px 8px; border-radius: 3px; font-size: 11px; font-family: inherit; width: 100%; }
177#search-input::placeholder { color: var(--mut); }
178#search-input:focus { outline: none; border-color: var(--acc); }
180#select-box { display: none; position: absolute; border: 1.5px dashed var(--acc); background: rgba(59,130,246,0.08); pointer-events: none; z-index: 5; }
181#select-toolbar { display: none; position: absolute; z-index: 15; background: var(--sf); border: 1px solid var(--bd); border-radius: 5px; padding: 4px; box-shadow: 0 4px 16px rgba(0,0,0,0.5); }
182#select-toolbar button { background: var(--bd); border: 1px solid #2a2d42; color: var(--dim); padding: 4px 10px; border-radius: 3px; cursor: pointer; font-size: 11px; font-family: inherit; margin: 0 2px; }
183#select-toolbar button:hover { background: #252840; color: var(--tx); }
184.zoom-controls { position: absolute; bottom: 32px; right: 230px; display: flex; gap: 2px; z-index: 10; }
185.zoom-btn { background: var(--sf); border: 1px solid var(--bd); color: var(--dim); width: 28px; height: 28px; border-radius: 3px; cursor: pointer; font-size: 14px; font-family: inherit; display: flex; align-items: center; justify-content: center; }
186.zoom-btn:hover { background: #252840; color: var(--tx); }
187#seq-filter { max-height: 180px; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(52px, 1fr)); gap: 3px; }
188#seq-filter::-webkit-scrollbar { width: 4px; }
189#seq-filter::-webkit-scrollbar-thumb { background: var(--bd); border-radius: 2px; }
190.seq-toggle { background: var(--bd); border: 1px solid #2a2d42; color: var(--dim); padding: 2px 4px; border-radius: 3px; cursor: pointer; font-size: 10px; font-family: inherit; text-align: center; }
191.seq-toggle:hover { background: #252840; color: var(--tx); }
192.seq-toggle.active { background: var(--acc); color: white; border-color: var(--acc); }
193.ctrl-row-between { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }
194.ctrl-row-between h3 { margin: 0; }
195#main { display: flex; height: calc(100vh - 42px); }
196#tree-container { flex: 1; overflow: auto; position: relative; }
197svg { display: block; }
198#sidebar { width: 220px; background: var(--sf); border-left: 1px solid var(--bd); padding: 14px; overflow-y: auto; flex-shrink: 0; }
199#sidebar h2 { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--mut); margin-bottom: 10px; }
200.worker-row { display: flex; align-items: center; gap: 8px; padding: 5px 0; border-bottom: 1px solid var(--bd); font-size: 12px; }
201.worker-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
202.worker-dot.busy { animation: pulse 1.2s ease-in-out infinite; }
203@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
204.worker-label { color: var(--dim); }
205.worker-seq { color: var(--tx); font-weight: 600; margin-left: auto; }
206#status-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 24px; background: var(--sf); border-top: 1px solid var(--bd); display: flex; align-items: center; padding: 0 16px; font-size: 11px; color: var(--mut); gap: 16px; z-index: 10; }
207.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; }
208.status-dot.done { background: var(--mut); }
209.branch-line { fill: none; stroke-width: 1.5; opacity: 0.5; pointer-events: none; }
210.seq-bar { rx: 3; ry: 3; }
211.seq-label { font-size: 10px; fill: white; font-weight: 600; pointer-events: none; }
212.axis-line { stroke: var(--bd); stroke-width: 1; }
213.axis-label { fill: var(--mut); font-size: 10px; font-family: inherit; }
214.grid-line { stroke: var(--bd); stroke-width: 0.5; stroke-dasharray: 2,4; }
215.time-marker { fill: var(--mut); font-size: 9px; font-family: inherit; }
220 <h1><span>ADS-IDAC</span> Dynamic Event Tree</h1>
222 <div><span class="stat-label">Sequences</span><span class="stat-value" id="st-seq">0</span></div>
223 <div><span class="stat-label">Queue</span><span class="stat-value" id="st-queue">0</span><span id="st-queue-arrow" style="display:inline-block;width:1.2em;text-align:center"></span></div>
224 <div><span class="stat-label">Workers</span><span class="stat-value" id="st-workers">0/0</span></div>
225 <div><span class="stat-label">Memory</span><span class="stat-value" id="st-mem">0 MB</span></div>
226 <div><span class="stat-label">Elapsed</span><span class="stat-value" id="st-time">0.0s</span></div>
227 <button id="controls-btn" onclick="toggleControls()">Controls</button>
230<div id="controls-panel" onclick="event.stopPropagation()">
232 <div class="ctrl-row">
233 <button class="ctrl-btn" onclick="expandAll()">Expand All</button>
234 <button class="ctrl-btn" onclick="collapseAll()">Collapse All</button>
235 <button class="ctrl-btn" onclick="rootOnly()">Root Only</button>
236 <button class="ctrl-btn" onclick="fitToScreen()">Fit to Screen</button>
238 <h3>Collapse to Depth</h3>
239 <div class="ctrl-row" id="depth-btns"></div>
240 <h3>Dead Branches</h3>
241 <div class="ctrl-row">
242 <button class="ctrl-btn" id="dead-all-btn" onclick="setDeadFilter('all')" style="opacity:1">All</button>
243 <button class="ctrl-btn" id="dead-hide-btn" onclick="setDeadFilter('hide')">Hide Dead</button>
244 <button class="ctrl-btn" id="dead-only-btn" onclick="setDeadFilter('only')">Only Dead</button>
246 <h3>Filter Workers</h3>
247 <div class="ctrl-row" id="worker-filter"></div>
248 <div class="ctrl-row-between">
249 <h3>Filter Sequences</h3>
250 <div style="display:flex;gap:4px">
251 <button class="ctrl-btn" onclick="showAllSeqs()" style="padding:1px 6px;font-size:10px">All</button>
252 <button class="ctrl-btn" onclick="hideAllSeqs()" style="padding:1px 6px;font-size:10px">None</button>
255 <div id="seq-filter"></div>
258 <div id="tree-container">
259 <svg id="tree-svg"></svg>
260 <div id="select-box"></div>
261 <div id="select-toolbar">
262 <button onclick="selectionZoom()">Zoom</button>
263 <button onclick="selectionCollapse()">Collapse</button>
264 <button onclick="selectionHide()">Hide</button>
265 <button onclick="clearSelection()">Cancel</button>
267 <div class="zoom-controls">
268 <button class="zoom-btn" onclick="setZoom(zoomLevel * 1.2)">+</button>
269 <button class="zoom-btn" onclick="setZoom(1.0)">1:1</button>
270 <button class="zoom-btn" onclick="setZoom(zoomLevel / 1.2)">−</button>
274 <div id="search-box">
275 <input id="search-input" type="text" placeholder="Go to S..." onkeydown="if(event.key==='Enter')searchSequence()">
278 <div id="worker-list"></div>
282 <div class="status-dot" id="status-dot"></div>
283 <span id="status-text">Connecting...</span>
285<div id="tooltip"></div>
289 '#3b82f6','#ef4444','#22c55e','#f59e0b','#a855f7',
290 '#ec4899','#06b6d4','#f97316','#84cc16','#6366f1',
291 '#14b8a6','#e11d48','#0ea5e9','#d946ef'
293const BASE_ROW_H = 32, BASE_BAR_H = 20;
294const PAD_TOP = 40, PAD_LEFT = 60, PAD_RIGHT = 30, PAD_BOT = 30;
295let cachedData = null;
296let prevQueueDepth = null;
297const collapsedSet = new Set();
298const hiddenWorkers = new Set();
299const hiddenSequences = new Set();
300const foldedBranches = new Set();
301let highlightedId = null;
302let deadBranchFilter = 'all'; // 'all', 'hide', 'only'
303const DEAD_THRESHOLD = 1.0; // seconds — branches shorter than this are "dead"
307function color(rank) { return COLORS[(rank - 1) % COLORS.length]; }
309function buildTree(sequences) {
312 for (const s of sequences) {
314 const pid = s.parent_id;
315 if (!byParent[pid]) byParent[pid] = [];
316 byParent[pid].push(s);
318 for (const k in byParent) byParent[k].sort((a, b) => a.branch_time - b.branch_time);
319 return { byParent, byId };
322function computeDepths(sequences) {
323 const { byParent, byId } = buildTree(sequences);
325 function walk(id, d) {
327 if (byParent[id]) for (const c of byParent[id]) walk(c.id, d + 1);
329 const roots = sequences.filter(s => s.parent_id === -1 || !byId[s.parent_id]);
330 for (const r of roots) walk(r.id, 0);
334function hasChildren(id, byParent) {
335 return byParent[id] && byParent[id].length > 0;
338function countDescendants(id, byParent) {
341 for (const c of byParent[id]) count += 1 + countDescendants(c.id, byParent);
347 return (s.end_time - s.branch_time) < DEAD_THRESHOLD;
350function isFilteredNode(s) {
351 if (hiddenWorkers.has(s.worker) || hiddenSequences.has(s.id)) return true;
352 if (deadBranchFilter === 'hide' && isDead(s)) return true;
353 if (deadBranchFilter === 'only' && !isDead(s)) return true;
357function dfsOrder(sequences) {
358 const { byParent, byId } = buildTree(sequences);
361 // Folded branches: skip node AND all descendants
362 if (foldedBranches.has(id)) return;
363 const node = byId[id];
364 if (node && !isFilteredNode(node)) order.push(node);
365 if (collapsedSet.has(id)) return;
366 if (byParent[id]) for (const c of byParent[id]) dfs(c.id);
368 const roots = sequences.filter(s => s.parent_id === -1 || !byId[s.parent_id]);
369 roots.sort((a, b) => a.id - b.id);
370 for (const r of roots) dfs(r.id);
374// --- Ancestry highlight ---
375function getAncestryPath(id, byId) {
376 const path = new Set();
378 while (cur >= 0 && byId[cur]) {
380 cur = byId[cur].parent_id;
385function highlightAncestry(id, e) {
386 if (e) e.stopPropagation();
387 highlightedId = (highlightedId === id) ? null : id;
388 if (cachedData) render(cachedData);
391// --- Fold branch (hide node + entire subtree from branch dot) ---
392function toggleFold(id, e) {
393 if (e) e.stopPropagation();
394 if (foldedBranches.has(id)) foldedBranches.delete(id);
395 else foldedBranches.add(id);
396 if (cachedData) render(cachedData);
399// --- Collapse/expand ---
400function toggleCollapse(id, e) {
401 if (e) e.stopPropagation();
402 if (collapsedSet.has(id)) collapsedSet.delete(id);
403 else collapsedSet.add(id);
404 if (cachedData) render(cachedData);
407function expandSubtree(id, e) {
408 if (e) e.stopPropagation();
409 if (!cachedData) return;
410 const { byParent } = buildTree(cachedData.sequences || []);
411 function recurse(nid) {
412 collapsedSet.delete(nid);
413 if (byParent[nid]) for (const c of byParent[nid]) recurse(c.id);
419function expandAll() { collapsedSet.clear(); if (cachedData) render(cachedData); }
421function collapseAll() {
422 if (!cachedData) return;
423 const { byParent } = buildTree(cachedData.sequences || []);
424 for (const s of cachedData.sequences || []) {
425 if (hasChildren(s.id, byParent)) collapsedSet.add(s.id);
431 if (!cachedData) return;
432 collapsedSet.clear();
433 for (const r of (cachedData.sequences || []).filter(s => s.parent_id === -1)) collapsedSet.add(r.id);
437function collapseToDepth(d) {
438 if (!cachedData) return;
439 const depths = computeDepths(cachedData.sequences || []);
440 const { byParent } = buildTree(cachedData.sequences || []);
441 collapsedSet.clear();
442 for (const s of cachedData.sequences || []) {
443 if (depths[s.id] >= d && hasChildren(s.id, byParent)) collapsedSet.add(s.id);
448function fitToScreen() {
449 if (!cachedData) return;
450 const container = document.getElementById('tree-container');
451 const maxRows = Math.floor((container.clientHeight - PAD_TOP - PAD_BOT - 24) / ROW_H);
452 const seqs = cachedData.sequences || [];
453 if (seqs.length === 0) return;
454 const depths = computeDepths(seqs);
455 const { byParent } = buildTree(seqs);
456 for (let d = maxDepth; d >= 0; d--) {
457 collapsedSet.clear();
458 for (const s of seqs) {
459 if (depths[s.id] >= d && hasChildren(s.id, byParent)) collapsedSet.add(s.id);
461 if (dfsOrder(seqs).length <= maxRows) break;
467function toggleWorker(rank) {
468 if (hiddenWorkers.has(rank)) hiddenWorkers.delete(rank);
469 else hiddenWorkers.add(rank);
470 if (cachedData) render(cachedData);
473function toggleSeq(id) {
474 if (hiddenSequences.has(id)) hiddenSequences.delete(id);
475 else hiddenSequences.add(id);
476 if (cachedData) render(cachedData);
479function showAllSeqs() { hiddenSequences.clear(); if (cachedData) render(cachedData); }
481function hideAllSeqs() {
482 if (!cachedData) return;
483 for (const s of cachedData.sequences || []) hiddenSequences.add(s.id);
487function setDeadFilter(val) {
488 deadBranchFilter = val;
489 document.getElementById('dead-all-btn').style.opacity = val === 'all' ? '1' : '0.5';
490 document.getElementById('dead-hide-btn').style.opacity = val === 'hide' ? '1' : '0.5';
491 document.getElementById('dead-only-btn').style.opacity = val === 'only' ? '1' : '0.5';
492 if (cachedData) render(cachedData);
495function toggleControls() {
496 document.getElementById('controls-panel').classList.toggle('open');
500function showTooltip(e, id) {
501 const tt = document.getElementById('tooltip');
502 if (!cachedData) return;
503 const { byParent, byId } = buildTree(cachedData.sequences);
506 const depths = computeDepths(cachedData.sequences);
507 const nch = byParent[id] ? byParent[id].length : 0;
508 const dur = (s.end_time - s.branch_time).toFixed(1);
509 tt.innerHTML = '<b>S' + s.id + '</b> \u2014 Rank ' + s.worker + '<br>' +
510 'Time: ' + s.branch_time.toFixed(1) + 's \u2192 ' + s.end_time.toFixed(1) + 's (' + dur + 's)<br>' +
511 'Parent: ' + (s.parent_id >= 0 ? 'S' + s.parent_id : 'root') + '<br>' +
512 'Children: ' + nch + ' | Depth: ' + (depths[s.id] || 0);
513 tt.style.display = 'block';
517function positionTooltip(e) {
518 const tt = document.getElementById('tooltip');
519 const x = Math.min(e.clientX + 12, window.innerWidth - 290);
520 const y = Math.min(e.clientY - 10, window.innerHeight - 100);
521 tt.style.left = x + 'px';
522 tt.style.top = y + 'px';
525function hideTooltip() {
526 document.getElementById('tooltip').style.display = 'none';
530function setZoom(level) {
531 zoomLevel = Math.max(0.3, Math.min(4, level));
532 if (cachedData) render(cachedData);
536function searchSequence() {
537 const input = document.getElementById('search-input');
538 const val = parseInt(input.value);
539 if (isNaN(val) || !cachedData) return;
540 const { byId } = buildTree(cachedData.sequences || []);
541 if (!byId[val]) return;
542 // Expand ancestors so it's visible
544 while (cur >= 0 && byId[cur]) {
545 collapsedSet.delete(cur);
546 cur = byId[cur].parent_id;
547 if (cur >= 0) collapsedSet.delete(cur);
552 const bar = document.querySelector('[data-seq="' + val + '"]');
553 if (bar) bar.scrollIntoView({ behavior: 'smooth', block: 'center' });
558function render(data) {
560 const svg = document.getElementById('tree-svg');
561 const container = document.getElementById('tree-container');
562 const seqs = data.sequences || [];
563 const stats = data.stats || {};
566 document.getElementById('st-seq').textContent = stats.completed || 0;
567 const qd = stats.queue_depth || 0;
568 document.getElementById('st-queue').textContent = qd;
569 const qArr = document.getElementById('st-queue-arrow');
570 if (prevQueueDepth !== null && qd !== prevQueueDepth) {
571 qArr.textContent = qd > prevQueueDepth ? '\u25B2' : '\u25BC';
572 qArr.style.color = qd > prevQueueDepth ? '#e74c3c' : '#2ecc71';
575 const busy = (stats.total_clients || 0) - (stats.free_clients || 0);
576 document.getElementById('st-workers').textContent = busy + '/' + (stats.total_clients || 0);
577 document.getElementById('st-workers').className = 'stat-value' + (busy > 0 ? ' active' : '');
578 document.getElementById('st-mem').textContent = (stats.memory_mb || 0) + ' MB';
579 document.getElementById('st-time').textContent = (stats.elapsed_s || 0).toFixed(1) + 's';
582 const dot = document.getElementById('status-dot');
583 const stxt = document.getElementById('status-text');
584 if (stats.queue_depth > 0 || busy > 0) {
585 dot.className = 'status-dot';
586 stxt.textContent = 'Simulation running \u2014 ' + seqs.length + ' sequences completed';
587 } else if (seqs.length > 0) {
588 dot.className = 'status-dot done';
589 stxt.textContent = 'Simulation complete \u2014 ' + seqs.length + ' sequences, ' + (stats.elapsed_s || 0).toFixed(1) + 's';
591 dot.className = 'status-dot';
592 stxt.textContent = 'Waiting for sequences...';
595 if (seqs.length === 0) {
596 svg.innerHTML = '<text x="50%" y="50%" text-anchor="middle" fill="#6a6a75" font-size="14" font-family="inherit">Waiting for first sequence to complete...</text>';
597 svg.setAttribute('width', container.clientWidth);
598 svg.setAttribute('height', container.clientHeight - 24);
602 const { byParent, byId } = buildTree(seqs);
603 const depths = computeDepths(seqs);
604 maxDepth = Math.max(...Object.values(depths));
605 const ancestryPath = highlightedId !== null ? getAncestryPath(highlightedId, byId) : null;
607 // Controls panel updates
608 const depthDiv = document.getElementById('depth-btns');
610 for (let d = 0; d <= Math.min(maxDepth, 8); d++) {
611 dhtml += '<button class="ctrl-btn" onclick="collapseToDepth(' + d + ')">' + d + '</button>';
613 depthDiv.innerHTML = dhtml;
615 const usedRanks = [...new Set(seqs.map(s => s.worker))].sort((a, b) => a - b);
616 const wfDiv = document.getElementById('worker-filter');
618 for (const r of usedRanks) {
619 const checked = !hiddenWorkers.has(r);
620 wfhtml += '<button class="ctrl-btn' + (checked ? ' active' : '') + '" onclick="toggleWorker(' + r + ')" style="border-left: 3px solid ' + color(r) + '">R' + r + '</button>';
622 wfDiv.innerHTML = wfhtml;
624 const sfDiv = document.getElementById('seq-filter');
625 const allIds = seqs.map(s => s.id).sort((a, b) => a - b);
627 for (const id of allIds) {
628 const shown = !hiddenSequences.has(id);
629 const c = byId[id] ? color(byId[id].worker) : '#666';
630 sfhtml += '<button class="seq-toggle' + (shown ? ' active' : '') + '" onclick="toggleSeq(' + id + ')" style="border-left: 2px solid ' + c + '">S' + id + '</button>';
632 sfDiv.innerHTML = sfhtml;
635 const ordered = dfsOrder(seqs);
637 ordered.forEach((s, i) => { yMap[s.id] = i; });
639 const ROW_H = BASE_ROW_H * zoomLevel;
640 const BAR_H = BASE_BAR_H * zoomLevel;
641 const maxTime = Math.max(...seqs.map(s => s.end_time), 1);
642 const W = Math.max(container.clientWidth - 10, 600);
643 const H = PAD_TOP + ordered.length * ROW_H + PAD_BOT;
644 const xScale = (t) => PAD_LEFT + (t / maxTime) * (W - PAD_LEFT - PAD_RIGHT);
645 const yScale = (i) => PAD_TOP + i * ROW_H + (ROW_H - BAR_H) / 2;
647 svg.setAttribute('width', W);
648 svg.setAttribute('height', H);
653 const nTicks = Math.min(Math.floor((W - PAD_LEFT - PAD_RIGHT) / 80), 12);
654 const niceStep = niceNum(maxTime / nTicks);
655 for (let t = 0; t <= maxTime + niceStep * 0.01; t += niceStep) {
657 if (x < PAD_LEFT - 1 || x > W - PAD_RIGHT + 1) continue;
658 html += '<line class="grid-line" x1="' + x + '" y1="' + PAD_TOP + '" x2="' + x + '" y2="' + (H - PAD_BOT) + '"/>';
659 html += '<text class="time-marker" x="' + x + '" y="' + (PAD_TOP - 8) + '" text-anchor="middle">' + t.toFixed(1) + 's</text>';
661 html += '<line class="axis-line" x1="' + PAD_LEFT + '" y1="' + (H - PAD_BOT) + '" x2="' + (W - PAD_RIGHT) + '" y2="' + (H - PAD_BOT) + '"/>';
662 html += '<text class="axis-label" x="' + ((PAD_LEFT + W - PAD_RIGHT) / 2) + '" y="' + (H - 8) + '" text-anchor="middle">Simulation Time (s)</text>';
664 // Branch curves (behind bars)
665 const branchData = [];
666 for (const s of ordered) {
667 if (s.parent_id < 0) continue;
668 let ancestorId = s.parent_id;
669 while (ancestorId >= 0 && yMap[ancestorId] === undefined) {
670 const a = byId[ancestorId];
672 ancestorId = a.parent_id;
674 if (ancestorId < 0 || yMap[ancestorId] === undefined) continue;
675 const ancestor = byId[ancestorId];
676 if (!ancestor) continue;
678 const inPath = ancestryPath && (ancestryPath.has(s.id) && ancestryPath.has(ancestorId));
679 const dimmed = ancestryPath && !inPath;
681 const px = xScale(s.branch_time);
682 const py = yScale(yMap[ancestorId]) + BAR_H / 2;
683 const cx = xScale(s.branch_time);
684 const cy = yScale(yMap[s.id]) + BAR_H / 2;
685 const midY = (py + cy) / 2;
686 const curve = 'M' + px + ',' + py + ' C' + px + ',' + midY + ' ' + cx + ',' + midY + ' ' + cx + ',' + cy;
689 html += '<path class="branch-line" stroke="' + color(s.worker) + '" d="' + curve + '" opacity="' + (dimmed ? 0.08 : 0.5) + '"/>';
690 // Save data for dots + hit areas (rendered after bars)
691 branchData.push({ s, ancestor, px, py, cx, cy, curve, dimmed });
695 for (const s of ordered) {
696 const yi = yMap[s.id];
697 const x1 = xScale(s.branch_time);
698 const x2 = xScale(s.end_time);
699 const y = yScale(yi);
700 const w = Math.max(x2 - x1, 2);
701 const c = color(s.worker);
702 const isCollapsed = collapsedSet.has(s.id);
703 const hasCh = hasChildren(s.id, byParent);
705 const inPath = ancestryPath && ancestryPath.has(s.id);
706 const dimmed = ancestryPath && !inPath;
707 const barOpacity = dimmed ? 0.08 : 0.7;
709 // Bar — click for ancestry highlight
710 html += '<rect data-seq="' + s.id + '" class="seq-bar" x="' + x1 + '" y="' + y + '" width="' + w + '" height="' + BAR_H + '" fill="' + c + '" opacity="' + barOpacity + '" onclick="highlightAncestry(' + s.id + ', event)" onmouseover="showTooltip(event,' + s.id + ')" onmousemove="positionTooltip(event)" onmouseout="hideTooltip()"/>';
713 html += '<rect x="' + x1 + '" y="' + y + '" width="3" height="' + BAR_H + '" fill="' + c + '" opacity="' + (dimmed ? 0.08 : 1) + '" rx="3" ry="3" pointer-events="none"/>';
715 // Ancestry highlight outline
717 html += '<rect x="' + x1 + '" y="' + y + '" width="' + w + '" height="' + BAR_H + '" fill="none" stroke="white" stroke-width="2" rx="3" ry="3" pointer-events="none"/>';
721 if (w > 30 && !dimmed) {
722 html += '<text class="seq-label" x="' + (x1 + 6) + '" y="' + (y + BAR_H / 2 + 4) + '">S' + s.id + '</text>';
725 // Collapse/expand icons (separate from bar click)
726 if (hasCh && !dimmed) {
728 const nHidden = countDescendants(s.id, byParent);
729 html += '<text class="collapse-indicator" x="' + (x2 + 6) + '" y="' + (y + BAR_H / 2 + 4) + '" onclick="toggleCollapse(' + s.id + ', event)">\u25B6 (+' + nHidden + ')</text>';
730 html += '<text class="expand-icon" x="' + (x2 + 60 + String(nHidden).length * 5) + '" y="' + (y + BAR_H / 2 + 4) + '" onclick="expandSubtree(' + s.id + ', event)">\u229E</text>';
732 html += '<text class="collapse-indicator" x="' + (x2 + 6) + '" y="' + (y + BAR_H / 2 + 4) + '" onclick="toggleCollapse(' + s.id + ', event)">\u25BC</text>';
737 html += '<text class="axis-label" x="' + (PAD_LEFT - 8) + '" y="' + (y + BAR_H / 2 + 4) + '" text-anchor="end" opacity="' + (dimmed ? 0.12 : 1) + '">S' + s.id + '</text>';
740 // Branch dots + hit areas (ON TOP of bars so they're clickable)
741 for (const b of branchData) {
742 html += '<path class="branch-hit" d="' + b.curve + '" onclick="toggleFold(' + b.s.id + ', event)"/>';
743 html += '<circle class="branch-dot" cx="' + b.px + '" cy="' + b.py + '" r="5" fill="' + color(b.ancestor.worker) + '" opacity="' + (b.dimmed ? 0.1 : 0.9) + '" onclick="toggleFold(' + b.s.id + ', event)"/>';
746 // Folded branch indicators (dots for children that are folded — not in ordered)
747 for (const s of seqs) {
748 if (!foldedBranches.has(s.id)) continue;
749 if (s.parent_id < 0) continue;
750 // Find visible ancestor
751 let ancestorId = s.parent_id;
752 while (ancestorId >= 0 && yMap[ancestorId] === undefined) {
753 const a = byId[ancestorId];
755 ancestorId = a.parent_id;
757 if (ancestorId < 0 || yMap[ancestorId] === undefined) continue;
758 const ancestor = byId[ancestorId];
759 if (!ancestor) continue;
760 const px = xScale(s.branch_time);
761 const py = yScale(yMap[ancestorId]) + BAR_H / 2;
762 const nHidden = 1 + countDescendants(s.id, byParent);
763 // Outer circle with inner dot = folded indicator
764 html += '<circle class="branch-dot" cx="' + px + '" cy="' + py + '" r="6" fill="' + color(ancestor.worker) + '" opacity="0.9" onclick="toggleFold(' + s.id + ', event)"/>';
765 html += '<circle cx="' + px + '" cy="' + py + '" r="2" fill="white" pointer-events="none"/>';
766 html += '<text class="collapse-indicator" x="' + (px + 10) + '" y="' + (py + 4) + '" onclick="toggleFold(' + s.id + ', event)" style="cursor:pointer">(+' + nHidden + ')</text>';
769 svg.innerHTML = html;
773function updateWorkers(data) {
774 const stats = data.stats || {};
775 const seqs = data.sequences || [];
776 const total = stats.total_clients || 0;
777 const free = stats.free_clients || 0;
779 const list = document.getElementById('worker-list');
781 for (let r = 1; r <= total; r++) {
783 const isBusy = r <= (total - free);
784 html += '<div class="worker-row">';
785 html += '<div class="worker-dot' + (isBusy ? ' busy' : '') + '" style="background:' + c + '"></div>';
786 html += '<span class="worker-label">Rank ' + r + '</span>';
787 html += '<span class="worker-seq">' + (isBusy ? 'busy' : 'idle') + '</span>';
790 list.innerHTML = html;
795 if (x <= 0) return 1;
796 const exp = Math.floor(Math.log10(x));
797 const frac = x / Math.pow(10, exp);
799 if (frac <= 1.5) nice = 1;
800 else if (frac <= 3.5) nice = 2;
801 else if (frac <= 7.5) nice = 5;
803 return nice * Math.pow(10, exp);
806// --- Box selection (Shift+drag) ---
807let selecting = false;
808let selStart = { x: 0, y: 0 };
811const treeContainer = document.getElementById('tree-container');
813treeContainer.addEventListener('mousedown', (e) => {
814 if (!e.shiftKey || e.button !== 0) return;
817 const rect = treeContainer.getBoundingClientRect();
818 selStart = { x: e.clientX - rect.left + treeContainer.scrollLeft, y: e.clientY - rect.top + treeContainer.scrollTop };
819 const box = document.getElementById('select-box');
820 box.style.left = selStart.x + 'px';
821 box.style.top = selStart.y + 'px';
822 box.style.width = '0';
823 box.style.height = '0';
824 box.style.display = 'block';
825 document.getElementById('select-toolbar').style.display = 'none';
828document.addEventListener('mousemove', (e) => {
829 if (!selecting) return;
830 const rect = treeContainer.getBoundingClientRect();
831 const curX = e.clientX - rect.left + treeContainer.scrollLeft;
832 const curY = e.clientY - rect.top + treeContainer.scrollTop;
833 const box = document.getElementById('select-box');
834 const x = Math.min(selStart.x, curX);
835 const y = Math.min(selStart.y, curY);
836 const w = Math.abs(curX - selStart.x);
837 const h = Math.abs(curY - selStart.y);
838 box.style.left = x + 'px';
839 box.style.top = y + 'px';
840 box.style.width = w + 'px';
841 box.style.height = h + 'px';
844document.addEventListener('mouseup', (e) => {
845 if (!selecting) return;
847 const box = document.getElementById('select-box');
848 const bx = parseFloat(box.style.left);
849 const by = parseFloat(box.style.top);
850 const bw = parseFloat(box.style.width);
851 const bh = parseFloat(box.style.height);
852 if (bw < 5 || bh < 5) { box.style.display = 'none'; return; }
854 // Find sequences whose bars intersect the selection box
855 if (!cachedData) return;
856 const { byParent, byId } = buildTree(cachedData.sequences || []);
857 const ordered = dfsOrder(cachedData.sequences || []);
858 const ROW_H = BASE_ROW_H * zoomLevel;
859 const BAR_H = BASE_BAR_H * zoomLevel;
860 const maxTime = Math.max(...(cachedData.sequences || []).map(s => s.end_time), 1);
861 const W = Math.max(treeContainer.clientWidth - 10, 600);
862 const xScale = (t) => PAD_LEFT + (t / maxTime) * (W - PAD_LEFT - PAD_RIGHT);
864 ordered.forEach((s, i) => { yMap[s.id] = i; });
867 const selSet = new Set();
869 for (const s of ordered) {
870 const yi = yMap[s.id];
871 const x1 = xScale(s.branch_time);
872 const x2 = xScale(s.end_time);
873 const y = PAD_TOP + yi * ROW_H + (ROW_H - BAR_H) / 2;
874 if (x2 >= bx && x1 <= bx + bw && y + BAR_H >= by && y <= by + bh) {
878 // Check branch curves (vertical lines from parent to child)
879 for (const s of ordered) {
880 if (s.parent_id < 0 || selSet.has(s.id)) continue;
881 let ancestorId = s.parent_id;
882 while (ancestorId >= 0 && yMap[ancestorId] === undefined) {
883 const a = byId[ancestorId];
885 ancestorId = a.parent_id;
887 if (ancestorId < 0 || yMap[ancestorId] === undefined) continue;
888 const cx = xScale(s.branch_time);
889 const py = PAD_TOP + yMap[ancestorId] * ROW_H + ROW_H / 2;
890 const cy = PAD_TOP + yMap[s.id] * ROW_H + ROW_H / 2;
891 const minY = Math.min(py, cy);
892 const maxY = Math.max(py, cy);
893 // Curve is vertical at x=cx; check if it passes through selection box
894 if (cx >= bx && cx <= bx + bw && maxY >= by && minY <= by + bh) {
898 selectedIds = [...selSet];
900 if (selectedIds.length === 0) { box.style.display = 'none'; return; }
902 // Show toolbar near selection
903 const tb = document.getElementById('select-toolbar');
904 const containerRect = treeContainer.getBoundingClientRect();
905 tb.style.left = Math.min(bx + bw + 4, treeContainer.clientWidth - 200) + 'px';
906 tb.style.top = Math.max(by - 4, 0) + 'px';
907 tb.style.display = 'flex';
910function selectionZoom() {
911 if (selectedIds.length === 0) return;
912 // Collapse everything that's not an ancestor or descendant of selected
913 if (!cachedData) return;
914 const { byParent, byId } = buildTree(cachedData.sequences || []);
915 // Keep selected + all their ancestors expanded
916 const keep = new Set();
917 for (const id of selectedIds) {
919 while (cur >= 0 && byId[cur]) { keep.add(cur); cur = byId[cur].parent_id; }
921 // Collapse everything with children that isn't in keep
922 collapsedSet.clear();
923 for (const s of cachedData.sequences || []) {
924 if (hasChildren(s.id, byParent) && !keep.has(s.id)) {
925 collapsedSet.add(s.id);
932function selectionCollapse() {
933 if (selectedIds.length === 0) return;
934 if (!cachedData) return;
935 const { byParent } = buildTree(cachedData.sequences || []);
936 for (const id of selectedIds) {
937 if (hasChildren(id, byParent)) collapsedSet.add(id);
943function selectionHide() {
944 if (selectedIds.length === 0) return;
945 for (const id of selectedIds) hiddenSequences.add(id);
947 if (cachedData) render(cachedData);
950function clearSelection() {
951 document.getElementById('select-box').style.display = 'none';
952 document.getElementById('select-toolbar').style.display = 'none';
956// --- Event handlers ---
958// Close controls on outside click
959document.addEventListener('click', (e) => {
960 const panel = document.getElementById('controls-panel');
961 const btn = document.getElementById('controls-btn');
962 if (panel.classList.contains('open') && !panel.contains(e.target) && e.target !== btn) {
963 panel.classList.remove('open');
967// Click empty SVG space to clear ancestry highlight
968document.getElementById('tree-svg').addEventListener('click', (e) => {
969 if (e.target.tagName === 'svg') {
970 highlightedId = null;
971 if (cachedData) render(cachedData);
975// Zoom with Ctrl/Cmd + wheel
976document.getElementById('tree-container').addEventListener('wheel', (e) => {
977 if (e.ctrlKey || e.metaKey) {
979 setZoom(zoomLevel * (e.deltaY > 0 ? 0.9 : 1.1));
981}, { passive: false });
985async function poll() {
988 const resp = await fetch('/api/tree');
989 if (resp.ok) render(await resp.json());
991 document.getElementById('status-text').textContent = 'Connection lost \u2014 retrying...';
993 await new Promise(r => setTimeout(r, 500));
998window.addEventListener('resize', () => { if (cachedData) render(cachedData); });
1008 return R
"html(<!DOCTYPE html>
1011<meta charset="UTF-8">
1012<meta name="viewport" content="width=device-width, initial-scale=1.0">
1013<title>ADS-IDAC Pivotal Event Graph</title>
1016 --bg: #0c0e14; --sf: #13151e; --bd: #1e2130;
1017 --tx: #e4e4e8; --dim: #a0a0a8; --mut: #6a6a75; --acc: #3b82f6;
1019* { margin: 0; padding: 0; box-sizing: border-box; }
1020body { background: var(--bg); color: var(--tx); font-family: 'SF Mono','Fira Code','Consolas',monospace; font-size: 13px; overflow: hidden; height: 100vh; }
1021#header { display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; background: var(--sf); border-bottom: 1px solid var(--bd); }
1022#header h1 { font-size: 15px; font-weight: 600; letter-spacing: 0.5px; }
1023#header h1 span { color: var(--acc); }
1024#stats { display: flex; gap: 24px; font-size: 12px; }
1025.stat-label { color: var(--mut); margin-right: 4px; }
1026.stat-value { color: var(--tx); font-weight: 600; }
1027#controls { display: flex; gap: 8px; align-items: center; }
1028#controls button { background: var(--bd); border: 1px solid #2a2d42; color: var(--dim); padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 11px; font-family: inherit; }
1029#controls button:hover { background: #252840; color: var(--tx); }
1030#controls button.active { background: var(--acc); color: white; border-color: var(--acc); }
1031#search-box { background: var(--bd); border: 1px solid #2a2d42; color: var(--tx); padding: 4px 8px; border-radius: 4px; font-size: 11px; font-family: inherit; width: 140px; }
1032#search-box::placeholder { color: var(--mut); }
1033#canvas-wrap { position: relative; width: 100%; height: calc(100vh - 42px); overflow: hidden; cursor: grab; }
1034#canvas-wrap.dragging { cursor: grabbing; }
1035svg { position: absolute; top: 0; left: 0; }
1036#detail-panel { position: fixed; top: 42px; right: 0; width: 320px; height: calc(100vh - 42px); background: var(--sf); border-left: 1px solid var(--bd); overflow-y: auto; padding: 16px; display: none; z-index: 10; }
1037#detail-panel.open { display: block; }
1038#detail-panel h2 { font-size: 14px; margin-bottom: 12px; }
1039#detail-close { position: absolute; top: 8px; right: 12px; background: none; border: none; color: var(--dim); cursor: pointer; font-size: 16px; }
1040.detail-section { margin-bottom: 12px; }
1041.detail-section h3 { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--mut); margin-bottom: 6px; }
1042.detail-item { font-size: 11px; color: var(--dim); padding: 3px 0; border-bottom: 1px solid #1a1c28; }
1043.badge { display: inline-block; padding: 1px 6px; border-radius: 3px; font-size: 9px; font-weight: 600; margin-right: 4px; }
1044.badge-hw { background: #92400e; color: #fbbf24; }
1045.badge-cog { background: #4c1d95; color: #a78bfa; }
1046.badge-other { background: #1e2130; color: var(--dim); }
1047#time-axis { position: absolute; bottom: 0; left: 0; right: 0; height: 24px; background: rgba(19,21,30,0.9); border-top: 1px solid var(--bd); pointer-events: none; z-index: 5; }
1048#nav-link { position: absolute; bottom: 30px; left: 12px; font-size: 11px; z-index: 5; }
1049#nav-link a { color: var(--acc); text-decoration: none; }
1050#nav-link a:hover { text-decoration: underline; }
1051#sel-rect { position: absolute; border: 1.5px dashed #3b82f6; background: rgba(59,130,246,0.08); pointer-events: none; display: none; z-index: 6; }
1052#zoom-label { position: absolute; bottom: 30px; right: 12px; font-size: 10px; color: var(--mut); z-index: 5; }
1057 <h1><span>PE</span> Pivotal Event Graph</h1>
1059 <div><span class="stat-label">nodes</span><span class="stat-value" id="st-nodes">0</span></div>
1060 <div><span class="stat-label">edges</span><span class="stat-value" id="st-edges">0</span></div>
1061 <div><span class="stat-label">sequences</span><span class="stat-value" id="st-seqs">0</span></div>
1064 <input id="search-box" type="text" placeholder="Search PE_...">
1065 <button id="btn-zin" title="Zoom in">+</button>
1066 <button id="btn-zout" title="Zoom out">−</button>
1067 <button id="btn-fit" title="Fit to screen">Fit</button>
1068 <button id="btn-reset" title="Reset selection">Reset</button>
1069 <span style="color:var(--mut);font-size:10px;margin-left:4px">shift+drag to zoom region</span>
1072<div id="canvas-wrap">
1073 <svg id="graph"></svg>
1074 <div id="sel-rect"></div>
1075 <div id="time-axis"></div>
1076 <div id="nav-link"><a href="/">← DET Tree</a></div>
1077 <div id="zoom-label"></div>
1079<div id="detail-panel">
1080 <button id="detail-close">×</button>
1081 <h2 id="detail-title"></h2>
1082 <div id="detail-body"></div>
1089let nodes = []; // {id, label, x, y, time, isTerminal, outDegree, traversals, details}
1090let edges = []; // {source, target, count}
1091let selectedId = null;
1092let ancestorIds = new Set();
1093let descendantIds = new Set();
1094let highlightedEdges = new Set();
1095let searchMatch = new Set();
1096let sequences = {}; // seq_id -> [PE_N, PE_N, ...]
1099let vx = 0, vy = 0, scale = 1;
1100const CANVAS_W = 16000, NODE_Y_SPACING = 46;
1102// ---- Data processing (mirrors pe-plotter parser.ts + layout.ts) ----
1104function processEvents(apiData) {
1105 // apiData: [{sequence_id, events: [{time, statement, pe_id}]}]
1106 const eventSequences = {}; // seq_id -> [PE_N, PE_N, ...]
1107 const eventTimes = {}; // PE_N -> first occurrence time
1108 const eventDetails = {}; // PE_N -> [description, ...]
1110 for (const seq of apiData) {
1112 for (const ev of seq.events) {
1113 const pe = 'PE_' + ev.pe_id;
1114 const desc = ev.statement || '';
1117 if (!eventDetails[pe]) eventDetails[pe] = [];
1118 if (desc) eventDetails[pe].push(desc);
1120 // Deduplicate consecutive same-PE
1121 if (peList.length === 0 || peList[peList.length - 1] !== pe) {
1123 if (!(pe in eventTimes)) eventTimes[pe] = ev.time;
1126 if (peList.length > 0) eventSequences[seq.sequence_id] = peList;
1129 // Build edges (consecutive PE pairs), tally
1130 const counter = new Map();
1131 for (const peList of Object.values(eventSequences)) {
1132 for (let i = 0; i < peList.length - 1; i++) {
1133 const key = peList[i] + '|||' + peList[i + 1];
1134 counter.set(key, (counter.get(key) || 0) + 1);
1138 const edgeList = [];
1139 const sourceSet = new Set(), targetSet = new Set();
1140 for (const [key, count] of counter) {
1141 const [source, target] = key.split('|||');
1142 edgeList.push({ source, target, count });
1143 sourceSet.add(source);
1144 targetSet.add(target);
1147 // Terminal nodes: appear as target but never as source
1148 const terminalNodes = new Set();
1149 for (const t of targetSet) {
1150 if (!sourceSet.has(t)) terminalNodes.add(t);
1153 // Count traversals per node
1154 const traversals = {};
1155 for (const peList of Object.values(eventSequences)) {
1156 const seen = new Set();
1157 for (const pe of peList) {
1158 if (!seen.has(pe)) { traversals[pe] = (traversals[pe] || 0) + 1; seen.add(pe); }
1162 // Compute out-degree
1164 for (const e of edgeList) outDeg[e.source] = (outDeg[e.source] || 0) + 1;
1166 // Layout: x = time-proportional, y = stacking within same time bucket
1167 const allPEs = new Set([...Object.keys(eventTimes), ...sourceSet, ...targetSet]);
1168 const peList = Array.from(allPEs);
1169 if (peList.length === 0) return { nodes: [], edges: [], seqCount: 0 };
1171 const times = peList.map(pe => eventTimes[pe] || 0);
1172 const minTime = Math.min(...times), maxTime = Math.max(...times);
1173 const timeRange = maxTime - minTime || 1;
1174 const NUM_BUCKETS = 200;
1176 const buckets = new Map();
1177 for (const pe of peList) {
1178 const t = eventTimes[pe] || 0;
1179 const b = Math.min(Math.floor(((t - minTime) / timeRange) * NUM_BUCKETS), NUM_BUCKETS - 1);
1180 if (!buckets.has(b)) buckets.set(b, []);
1181 buckets.get(b).push(pe);
1184 // Sort each bucket by PE index
1185 const peIdx = pe => parseInt(pe.replace('PE_', ''), 10);
1186 for (const arr of buckets.values()) arr.sort((a, b) => peIdx(a) - peIdx(b));
1188 const maxTrav = Math.max(...Object.values(traversals), 1);
1189 const nodeList = [];
1190 for (const [bucket, pes] of buckets) {
1191 const count = pes.length;
1192 const yStart = -((count - 1) * NODE_Y_SPACING) / 2;
1193 const x = 80 + (bucket / NUM_BUCKETS) * (CANVAS_W - 160);
1194 pes.forEach((pe, i) => {
1197 label: pe.replace('PE_', ''),
1198 x, y: yStart + i * NODE_Y_SPACING,
1199 time: eventTimes[pe] || 0,
1200 isTerminal: terminalNodes.has(pe),
1201 outDegree: outDeg[pe] || 0,
1202 traversals: traversals[pe] || 1,
1203 maxTraversals: maxTrav,
1204 details: eventDetails[pe] || []
1209 return { nodes: nodeList, edges: edgeList, seqCount: Object.keys(eventSequences).length, sequences: eventSequences };
1212// ---- Graph helpers ----
1214function buildAdjacency() {
1215 const children = {}, parents = {};
1216 for (const e of edges) {
1217 if (!children[e.source]) children[e.source] = [];
1218 children[e.source].push(e.target);
1219 if (!parents[e.target]) parents[e.target] = [];
1220 parents[e.target].push(e.source);
1222 return { children, parents };
1225function walkGraph(startId, adjMap) {
1226 const visited = new Set();
1227 const queue = [startId];
1228 while (queue.length) {
1229 const id = queue.shift();
1230 if (visited.has(id)) continue;
1232 for (const next of (adjMap[id] || [])) queue.push(next);
1234 visited.delete(startId);
1238function selectNode(id) {
1239 if (selectedId === id) { clearSelection(); return; }
1241 // Find all sequences that contain this PE, highlight their full paths
1242 ancestorIds = new Set();
1243 descendantIds = new Set();
1244 highlightedEdges = new Set();
1245 for (const peList of Object.values(sequences)) {
1246 if (!peList.includes(id)) continue;
1247 // All PEs in this sequence are highlighted
1248 const idx = peList.indexOf(id);
1249 for (let i = 0; i < peList.length; i++) {
1250 if (peList[i] === id) continue;
1251 if (i < idx) ancestorIds.add(peList[i]);
1252 else descendantIds.add(peList[i]);
1254 // All edges in this sequence are highlighted
1255 for (let i = 0; i < peList.length - 1; i++) {
1256 highlightedEdges.add(peList[i] + '|||' + peList[i + 1]);
1263function clearSelection() {
1265 ancestorIds.clear();
1266 descendantIds.clear();
1267 highlightedEdges.clear();
1269 document.getElementById('detail-panel').classList.remove('open');
1272function showDetail(id) {
1273 const node = nodes.find(n => n.id === id);
1275 const panel = document.getElementById('detail-panel');
1276 document.getElementById('detail-title').textContent = node.id;
1277 const body = document.getElementById('detail-body');
1280 const hw = [], cog = [], other = [];
1281 for (const d of node.details) {
1282 if (d.startsWith('New Alarm:') || d.includes('alarm') || d.includes('Alarm'))
1284 else if (d.startsWith('Mental Belief:') || d.startsWith('Procedure:') || d.startsWith('Info_gather_mode:') || d.includes('crew') || d.includes('Crew') || d.includes('ODM') || d.includes('OAT'))
1290 let html = `<div class="detail-section"><h3>Info</h3>
1291 <div class="detail-item">Time: ${node.time.toFixed(2)} s</div>
1292 <div class="detail-item">Type: ${node.isTerminal ? 'Terminal' : node.outDegree > 1 ? 'Branch' : 'Transit'}</div>
1293 <div class="detail-item">Traversals: ${node.traversals}</div>
1294 <div class="detail-item">In ${Object.values(sequences).filter(s => s.includes(id)).length} sequences</div>
1297 const renderSection = (title, items, badgeClass) => {
1298 if (items.length === 0) return '';
1299 let s = `<div class="detail-section"><h3>${title} (${items.length})</h3>`;
1300 for (const d of items) s += `<div class="detail-item"><span class="badge ${badgeClass}">${title}</span>${escHtml(d)}</div>`;
1304 html += renderSection('HW', hw, 'badge-hw');
1305 html += renderSection('COG', cog, 'badge-cog');
1306 html += renderSection('Other', other, 'badge-other');
1308 body.innerHTML = html;
1309 panel.classList.add('open');
1312function escHtml(s) { return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
1314// ---- Rendering ----
1316function renderGraph() {
1317 const svg = document.getElementById('graph');
1318 const wrap = document.getElementById('canvas-wrap');
1319 const W = wrap.clientWidth, H = wrap.clientHeight;
1320 svg.setAttribute('width', W);
1321 svg.setAttribute('height', H);
1323 const maxCount = Math.max(...edges.map(e => e.count), 1);
1325 for (const n of nodes) nodeMap[n.id] = n;
1327 // Build SVG content
1328 let svgContent = `<defs>
1329 <filter id="glow-amber" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="4" result="blur"/><feFlood flood-color="#f59e0b" flood-opacity="0.4"/><feComposite in2="blur" operator="in"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>
1330 <filter id="glow-cyan" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="4" result="blur"/><feFlood flood-color="#22d3ee" flood-opacity="0.4"/><feComposite in2="blur" operator="in"/><feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge></filter>
1331 </defs><g transform="translate(${vx},${vy}) scale(${scale})">`;
1334 for (const e of edges) {
1335 const src = nodeMap[e.source], tgt = nodeMap[e.target];
1336 if (!src || !tgt) continue;
1338 const key = e.source + '|||' + e.target;
1339 const w = 1 + (e.count / maxCount) * 7;
1340 let stroke, opacity;
1343 if (highlightedEdges.has(key)) {
1344 const isAnc = ancestorIds.has(e.source) || e.source === selectedId;
1345 stroke = isAnc && (ancestorIds.has(e.target) || e.target === selectedId) ? '#f59e0b' : '#34d399';
1351 } else if (searchMatch.size > 0) {
1352 opacity = (searchMatch.has(e.source) || searchMatch.has(e.target)) ? 0.5 : 0.03;
1360 const dx = tgt.x - src.x;
1361 const cx1 = src.x + dx * 0.4, cy1 = src.y;
1362 const cx2 = tgt.x - dx * 0.4, cy2 = tgt.y;
1363 svgContent += `<path d="M${src.x},${src.y} C${cx1},${cy1} ${cx2},${cy2} ${tgt.x},${tgt.y}" fill="none" stroke="${stroke}" stroke-width="${w}" opacity="${opacity}" stroke-linecap="round"/>`;
1366 // Nodes — compact pills scaled by traversal count
1367 for (const n of nodes) {
1368 const t = Math.sqrt(n.traversals / n.maxTraversals);
1369 let bg, fg, strokeC, nodeOpacity, shadow = '';
1370 const isSelected = selectedId === n.id;
1371 const isAnc = ancestorIds.has(n.id);
1372 const isDesc = descendantIds.has(n.id);
1373 const isDimmed = (selectedId && !isSelected && !isAnc && !isDesc) ||
1374 (searchMatch.size > 0 && !searchMatch.has(n.id) && !selectedId);
1377 bg = '#1e1e2e'; fg = '#45475a'; strokeC = '#252740'; nodeOpacity = 0.2;
1378 } else if (searchMatch.size > 0 && searchMatch.has(n.id) && !isSelected && !isAnc && !isDesc) {
1379 bg = '#0891b2'; fg = '#fff'; strokeC = '#22d3ee'; nodeOpacity = 1; shadow = 'filter="url(#glow-cyan)"';
1380 } else if (isSelected) {
1381 bg = '#f8fafc'; fg = '#1e1e2e'; strokeC = '#f59e0b'; nodeOpacity = 1; shadow = 'filter="url(#glow-amber)"';
1383 bg = '#f59e0b'; fg = '#1c1917'; strokeC = '#d97706'; nodeOpacity = 1;
1384 } else if (isDesc) {
1385 bg = '#34d399'; fg = '#064e3b'; strokeC = '#059669'; nodeOpacity = 1;
1386 } else if (n.outDegree > 1) {
1387 bg = '#7c3aed'; fg = '#e8e0ff'; strokeC = '#6d28d9'; nodeOpacity = 1;
1388 } else if (n.isTerminal) {
1389 bg = '#f43f5e'; fg = '#fff'; strokeC = '#e11d48'; nodeOpacity = 1;
1391 bg = '#0ea5e9'; fg = '#f0f9ff'; strokeC = '#0284c7'; nodeOpacity = 1;
1394 // Compact pill: size scales with traversal count
1395 const r = isSelected ? 18 : 12 + Math.round(t * 8);
1396 const fSize = isSelected ? 12 : 9 + Math.round(t * 2);
1398 svgContent += `<g class="pe-node" data-id="${n.id}" opacity="${nodeOpacity}" style="cursor:pointer">`;
1399 svgContent += `<circle cx="${n.x}" cy="${n.y}" r="${r}" fill="${bg}" stroke="${strokeC}" stroke-width="${isSelected ? 2.5 : 1}" ${shadow}/>`;
1400 svgContent += `<text x="${n.x}" y="${n.y + 1}" text-anchor="middle" dominant-baseline="central" fill="${fg}" font-size="${fSize}" font-weight="600" font-family="'SF Mono','Fira Code','Consolas',monospace">${n.label}</text>`;
1401 if (n.traversals > 1 && !isDimmed) {
1402 svgContent += `<text x="${n.x}" y="${n.y + r + 10}" text-anchor="middle" fill="${isDimmed ? fg : '#8888aa'}" font-size="8" font-family="inherit">${n.traversals}x</text>`;
1404 if (isSelected || isAnc || isDesc) {
1405 svgContent += `<text x="${n.x}" y="${n.y - r - 4}" text-anchor="middle" fill="#a0a0b0" font-size="8" font-family="inherit">${n.time.toFixed(1)}s</text>`;
1407 svgContent += '</g>';
1410 svgContent += '</g>';
1415 svg.innerHTML = svgContent;
1417 // Attach click handlers
1418 for (const g of svg.querySelectorAll('.pe-node')) {
1419 g.addEventListener('click', (e) => {
1420 e.stopPropagation();
1421 selectNode(g.dataset.id);
1426function renderTimeAxis() {
1427 const wrap = document.getElementById('canvas-wrap');
1428 const W = wrap.clientWidth;
1429 const axDiv = document.getElementById('time-axis');
1430 if (nodes.length === 0) { axDiv.innerHTML = ''; return; }
1432 const times = nodes.map(n => n.time);
1433 const minT = Math.min(...times), maxT = Math.max(...times);
1435 let html = '<svg width="' + W + '" height="24">';
1436 const numTicks = Math.min(12, Math.floor(W / 100));
1437 for (let i = 0; i <= numTicks; i++) {
1438 const t = minT + (maxT - minT) * (i / numTicks);
1439 const nodeX = 80 + ((t - minT) / (maxT - minT || 1)) * (CANVAS_W - 160);
1440 const screenX = nodeX * scale + vx;
1441 if (screenX < 0 || screenX > W) continue;
1442 html += `<text x="${screenX}" y="16" text-anchor="middle" fill="#6a6a75" font-size="10" font-family="inherit">${t.toFixed(1)}s</text>`;
1445 axDiv.innerHTML = html;
1448// ---- Pan, zoom & shift+drag selection ----
1450const wrap = document.getElementById('canvas-wrap');
1451const selRect = document.getElementById('sel-rect');
1452const zoomLabel = document.getElementById('zoom-label');
1453let dragging = false, dragX = 0, dragY = 0;
1454let selecting = false, selX0 = 0, selY0 = 0;
1456function updateZoomLabel() { zoomLabel.textContent = Math.round(scale * 100) + '%'; }
1458function zoomAt(cx, cy, factor) {
1459 const oldScale = scale;
1460 scale = Math.max(0.01, Math.min(20, scale * factor));
1461 vx = cx - (cx - vx) * (scale / oldScale);
1462 vy = cy - (cy - vy) * (scale / oldScale);
1467wrap.addEventListener('mousedown', (e) => {
1468 if (e.target.closest('.pe-node')) return;
1470 // Shift+drag: box selection for zoom
1472 const rect = wrap.getBoundingClientRect();
1473 selX0 = e.clientX - rect.left;
1474 selY0 = e.clientY - rect.top;
1475 selRect.style.left = selX0 + 'px';
1476 selRect.style.top = selY0 + 'px';
1477 selRect.style.width = '0'; selRect.style.height = '0';
1478 selRect.style.display = 'block';
1479 wrap.style.cursor = 'crosshair';
1482 dragX = e.clientX; dragY = e.clientY;
1483 wrap.classList.add('dragging');
1487window.addEventListener('mousemove', (e) => {
1489 const rect = wrap.getBoundingClientRect();
1490 const mx = e.clientX - rect.left, my = e.clientY - rect.top;
1491 selRect.style.left = Math.min(selX0, mx) + 'px';
1492 selRect.style.top = Math.min(selY0, my) + 'px';
1493 selRect.style.width = Math.abs(mx - selX0) + 'px';
1494 selRect.style.height = Math.abs(my - selY0) + 'px';
1497 if (!dragging) return;
1498 vx += e.clientX - dragX; vy += e.clientY - dragY;
1499 dragX = e.clientX; dragY = e.clientY;
1503window.addEventListener('mouseup', (e) => {
1506 selRect.style.display = 'none';
1507 wrap.style.cursor = '';
1508 const rect = wrap.getBoundingClientRect();
1509 const mx = e.clientX - rect.left, my = e.clientY - rect.top;
1510 const sx0 = Math.min(selX0, mx), sy0 = Math.min(selY0, my);
1511 const sw = Math.abs(mx - selX0), sh = Math.abs(my - selY0);
1512 if (sw > 10 && sh > 10) {
1513 // Convert screen rect to world coords, zoom to fit
1514 const W = wrap.clientWidth, H = wrap.clientHeight;
1515 const wx0 = (sx0 - vx) / scale, wy0 = (sy0 - vy) / scale;
1516 const wx1 = (sx0 + sw - vx) / scale, wy1 = (sy0 + sh - vy) / scale;
1517 const ww = wx1 - wx0, wh = wy1 - wy0;
1518 scale = Math.min(W / ww, H / wh) * 0.95;
1519 scale = Math.max(0.01, Math.min(20, scale));
1520 vx = W / 2 - (wx0 + ww / 2) * scale;
1521 vy = H / 2 - (wy0 + wh / 2) * scale;
1528 wrap.classList.remove('dragging');
1531wrap.addEventListener('wheel', (e) => {
1533 const rect = wrap.getBoundingClientRect();
1534 const mx = e.clientX - rect.left, my = e.clientY - rect.top;
1535 zoomAt(mx, my, e.deltaY > 0 ? 0.9 : 1.1);
1536}, { passive: false });
1538// Click on background clears selection
1539wrap.addEventListener('click', (e) => {
1540 if (!e.target.closest('.pe-node') && !e.shiftKey) clearSelection();
1543// ---- Controls ----
1545document.getElementById('btn-zin').addEventListener('click', () => {
1546 const W = wrap.clientWidth, H = wrap.clientHeight;
1547 zoomAt(W / 2, H / 2, 1.4);
1549document.getElementById('btn-zout').addEventListener('click', () => {
1550 const W = wrap.clientWidth, H = wrap.clientHeight;
1551 zoomAt(W / 2, H / 2, 1 / 1.4);
1554document.getElementById('btn-fit').addEventListener('click', () => {
1555 if (nodes.length === 0) return;
1556 const W = wrap.clientWidth, H = wrap.clientHeight;
1557 const xs = nodes.map(n => n.x), ys = nodes.map(n => n.y);
1558 const minX = Math.min(...xs) - 60, maxX = Math.max(...xs) + 60;
1559 const minY = Math.min(...ys) - 60, maxY = Math.max(...ys) + 60;
1560 const sw = maxX - minX || 1, sh = maxY - minY || 1;
1561 scale = Math.min(W / sw, H / sh, 3) * 0.9;
1562 vx = (W - sw * scale) / 2 - minX * scale;
1563 vy = (H - sh * scale) / 2 - minY * scale;
1568document.getElementById('btn-reset').addEventListener('click', clearSelection);
1570document.getElementById('detail-close').addEventListener('click', () => {
1571 document.getElementById('detail-panel').classList.remove('open');
1575const searchBox = document.getElementById('search-box');
1576searchBox.addEventListener('input', () => {
1577 const q = searchBox.value.trim().toUpperCase();
1578 searchMatch.clear();
1580 for (const n of nodes) {
1581 if (n.id.toUpperCase().includes(q) || ('PE_' + n.label).toUpperCase().includes(q)) searchMatch.add(n.id);
1584 if (!selectedId) renderGraph();
1589async function poll() {
1592 const resp = await fetch('/api/events');
1594 const data = await resp.json();
1595 const result = processEvents(data);
1596 nodes = result.nodes;
1597 edges = result.edges;
1598 sequences = result.sequences;
1599 document.getElementById('st-nodes').textContent = nodes.length;
1600 document.getElementById('st-edges').textContent = edges.length;
1601 document.getElementById('st-seqs').textContent = result.seqCount;
1605 await new Promise(r => setTimeout(r, 1000));
1609// Initial fit after first data
1610let firstRender = true;
1611const origRender = renderGraph;
1612renderGraph = function() {
1614 if (firstRender && nodes.length > 0) {
1615 firstRender = false;
1616 document.getElementById('btn-fit').click();
1621window.addEventListener('resize', renderGraph);
MPI server (rank 0): dispatches DET sequences to worker clients.
HTTP server for real-time DET visualization on MPI rank 0.
void stop()
Stop the HTTP server and join the thread.
VisualizationServer(std::shared_ptr< TreeState > state, int port)
std::unique_ptr< Impl > impl_
void start()
Launch the HTTP listener in a background thread.
std::shared_ptr< TreeState > state
static std::string buildIndexHtml()
static std::string buildPEHtml()