Spaces:
Running
Running
Upload 3 files
Browse files- index.html +92 -18
- temporal-graph-canva.js +217 -0
- temporal-graph-timestep.js +128 -0
index.html
CHANGED
|
@@ -1,19 +1,93 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Workflow Visualizer</title>
|
| 7 |
+
<link rel="icon" href="data:,">
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script type="module">
|
| 10 |
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
| 11 |
+
window.mermaid = mermaid;
|
| 12 |
+
mermaid.initialize({
|
| 13 |
+
startOnLoad: true,
|
| 14 |
+
theme: 'base',
|
| 15 |
+
themeVariables: {
|
| 16 |
+
'primaryColor': '#ffffff',
|
| 17 |
+
'primaryTextColor': '#000000',
|
| 18 |
+
'primaryBorderColor': '#000000',
|
| 19 |
+
'lineColor': '#000000',
|
| 20 |
+
'secondaryColor': '#ffffff',
|
| 21 |
+
'tertiaryColor': '#ffffff',
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
</script>
|
| 25 |
+
<style>
|
| 26 |
+
.mermaid svg {
|
| 27 |
+
max-width: 100%;
|
| 28 |
+
max-height: 100%;
|
| 29 |
+
}
|
| 30 |
+
textarea {
|
| 31 |
+
font-family: monospace;
|
| 32 |
+
}
|
| 33 |
+
</style>
|
| 34 |
+
<script type="module" src="temporal-graph-timestep.js"></script>
|
| 35 |
+
<script type="module" src="temporal-graph-canva.js"></script>
|
| 36 |
+
</head>
|
| 37 |
+
<body class="bg-white min-h-screen flex flex-col relative">
|
| 38 |
+
|
| 39 |
+
<div class="container mx-auto px-4 py-8 h-screen flex flex-col">
|
| 40 |
+
<h1 class="text-3xl font-bold mb-4 text-center shrink-0">Workflow Visualizer</h1>
|
| 41 |
+
<temporal-graph-canva id="mygraph" class="h-full flex-grow flex flex-col" current-timestep="0" view-mode="single"></temporal-graph-canva>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div class="absolute top-4 right-4 z-50 bg-white rounded-lg shadow-2xl border border-gray-200 w-1/3 max-w-lg">
|
| 45 |
+
<button id="toggleInputBtn" class="w-full text-left p-3 font-bold text-lg hover:bg-gray-50 rounded-t-lg">
|
| 46 |
+
Workflow Data ✏️
|
| 47 |
+
</button>
|
| 48 |
+
<div id="inputContainer" class="p-4 border-t border-gray-200">
|
| 49 |
+
<label for="tsvInput" class="block text-sm font-medium text-gray-700 mb-1">Paste your TSV data here:</label>
|
| 50 |
+
<textarea id="tsvInput" rows="10" class="w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-[#8590F8] focus:border-[#8590F8]"></textarea>
|
| 51 |
+
<button id="visualizeBtn" class="mt-2 px-4 py-2 bg-[#8590F8] text-white rounded hover:bg-[#7E7E7E] transition-colors">Visualize</button>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
<script>
|
| 57 |
+
const tsvInput = document.getElementById('tsvInput');
|
| 58 |
+
const visualizeBtn = document.getElementById('visualizeBtn');
|
| 59 |
+
const graphCanva = document.getElementById('mygraph');
|
| 60 |
+
|
| 61 |
+
function parseTSVToKnowledgeGraph(tsvText) {
|
| 62 |
+
if (!tsvText) return [];
|
| 63 |
+
const lines = tsvText.trim().split('\n').slice(1);
|
| 64 |
+
return lines.map(line => {
|
| 65 |
+
const parts = line.split('\t');
|
| 66 |
+
if (parts.length < 4) return null;
|
| 67 |
+
const [subject, relation, object, stageStr] = parts;
|
| 68 |
+
const stage = parseInt(stageStr.replace(/\D/g, ''), 10) || 0;
|
| 69 |
+
return [subject.trim(), relation.trim(), object.trim(), stage];
|
| 70 |
+
}).filter(Boolean);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function renderGraph() {
|
| 74 |
+
try {
|
| 75 |
+
const knowledge_graph = parseTSVToKnowledgeGraph(tsvInput.value);
|
| 76 |
+
graphCanva.render(knowledge_graph);
|
| 77 |
+
} catch (error) {
|
| 78 |
+
console.error('Error parsing or rendering the graph:', error);
|
| 79 |
+
alert('Could not parse or render the graph. Please check the console for errors.');
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
visualizeBtn.addEventListener('click', renderGraph);
|
| 84 |
+
|
| 85 |
+
const toggleInputBtn = document.getElementById('toggleInputBtn');
|
| 86 |
+
const inputContainer = document.getElementById('inputContainer');
|
| 87 |
+
toggleInputBtn.addEventListener('click', () => {
|
| 88 |
+
inputContainer.classList.toggle('hidden');
|
| 89 |
+
});
|
| 90 |
+
</script>
|
| 91 |
+
</body>
|
| 92 |
</html>
|
| 93 |
+
<!-- /* All rights reserved Michael Anthony 2025 */-->
|
temporal-graph-canva.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ===========================================================
|
| 2 |
+
temporal-graph-canva.js
|
| 3 |
+
=========================================================== */
|
| 4 |
+
import './temporal-graph-timestep.js';
|
| 5 |
+
|
| 6 |
+
class TemporalGraphCanva extends HTMLElement {
|
| 7 |
+
constructor() {
|
| 8 |
+
super();
|
| 9 |
+
this._knowledge_graph = [];
|
| 10 |
+
this._maxTimestep = 0;
|
| 11 |
+
this._cursorIndex = -1;
|
| 12 |
+
this._keyHandler = this._keyHandler.bind(this);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* ---------- observed attributes ---------- */
|
| 16 |
+
static get observedAttributes() { return ['current-timestep','view-mode']; }
|
| 17 |
+
get currentTimestep() { return parseInt(this.getAttribute('current-timestep') || '0'); }
|
| 18 |
+
set currentTimestep(v){ this.setAttribute('current-timestep', v); this._cursorIndex=-1; }
|
| 19 |
+
get viewMode() { return this.getAttribute('view-mode') || 'single'; }
|
| 20 |
+
set viewMode(v) { this.setAttribute('view-mode', v); this._cursorIndex=-1; }
|
| 21 |
+
|
| 22 |
+
/* ---------- lifecycle ---------- */
|
| 23 |
+
connectedCallback() { document.addEventListener('keydown', this._keyHandler); }
|
| 24 |
+
disconnectedCallback(){ document.removeEventListener('keydown', this._keyHandler); }
|
| 25 |
+
async attributeChangedCallback(n,o,v){ if(o!==v) await this._render(); }
|
| 26 |
+
async render(kg){
|
| 27 |
+
this._knowledge_graph=kg;
|
| 28 |
+
if (!kg || kg.length === 0) {
|
| 29 |
+
this._maxTimestep = 0;
|
| 30 |
+
} else {
|
| 31 |
+
this._maxTimestep=Math.max(...kg.map(r=>r[3]));
|
| 32 |
+
}
|
| 33 |
+
await this._render();
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* ---------- keyboard ---------- */
|
| 37 |
+
_keyHandler(e){
|
| 38 |
+
const k=e.key.toLowerCase();
|
| 39 |
+
if(this.viewMode==='single' && (k==='q'||k==='e')){
|
| 40 |
+
const total=this._relationCount();
|
| 41 |
+
if(total){
|
| 42 |
+
if(k==='e'){ this._cursorIndex++; if(this._cursorIndex>total-1) this._cursorIndex=-1; }
|
| 43 |
+
else { this._cursorIndex--; if(this._cursorIndex< -1) this._cursorIndex=total-1; }
|
| 44 |
+
this._render();
|
| 45 |
+
}
|
| 46 |
+
return;
|
| 47 |
+
}
|
| 48 |
+
if(this.viewMode==='single'){
|
| 49 |
+
if(k==='arrowleft'||k==='a') this._navigate(-1);
|
| 50 |
+
else if(k==='arrowright'||k==='d') this._navigate(1);
|
| 51 |
+
else if(k==='s') this.currentTimestep=this._maxTimestep+1;
|
| 52 |
+
}
|
| 53 |
+
if(k==='f') this._toggleView();
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* ---------- helpers ---------- */
|
| 57 |
+
_relationCount(){
|
| 58 |
+
const t=this.currentTimestep;
|
| 59 |
+
if(t===this._maxTimestep+1){
|
| 60 |
+
return new Set(this._knowledge_graph.map(([s,r,t])=>`${s}|${r}|${t}`)).size;
|
| 61 |
+
}
|
| 62 |
+
if(t<0 || t>this._maxTimestep) return 0;
|
| 63 |
+
return new Set(
|
| 64 |
+
this._knowledge_graph.filter(r=>r[3]===t).map(([s,r,t])=>`${s}|${r}|${t}`)
|
| 65 |
+
).size;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/* ---------- render ---------- */
|
| 69 |
+
async _render(){
|
| 70 |
+
this.innerHTML='';
|
| 71 |
+
|
| 72 |
+
const wrapper=document.createElement('div');
|
| 73 |
+
wrapper.className='h-full flex flex-col bg-white rounded-lg shadow-lg p-4 flex-grow overflow-hidden';
|
| 74 |
+
|
| 75 |
+
const container=document.createElement('div');
|
| 76 |
+
container.className=this.viewMode==='single'
|
| 77 |
+
? 'flex justify-center items-center w-full h-full'
|
| 78 |
+
: 'grid grid-cols-1 md:grid-cols-2 gap-0 w-full h-full overflow-auto';
|
| 79 |
+
|
| 80 |
+
if (!this._knowledge_graph || this._knowledge_graph.length === 0) {
|
| 81 |
+
container.innerHTML = `<div class="flex items-center justify-center h-full text-gray-500">Paste TSV data and click Visualize.</div>`;
|
| 82 |
+
} else {
|
| 83 |
+
/* each real timestep */
|
| 84 |
+
for(let ts=0;ts<=this._maxTimestep;ts++){
|
| 85 |
+
const el=document.createElement('temporal-graph-timestep');
|
| 86 |
+
el.data={
|
| 87 |
+
knowledge_graph:this._knowledge_graph,
|
| 88 |
+
timestep:ts,
|
| 89 |
+
cursorIndex:(this.viewMode==='single' && ts===this.currentTimestep) ? this._cursorIndex : -1
|
| 90 |
+
};
|
| 91 |
+
if(this.viewMode==='single') el.style.display = ts===this.currentTimestep ? 'flex':'none';
|
| 92 |
+
container.appendChild(el);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* summary page */
|
| 96 |
+
const summary=document.createElement('temporal-graph-timestep');
|
| 97 |
+
summary.data={
|
| 98 |
+
knowledge_graph:this._knowledge_graph,
|
| 99 |
+
timestep:'summary',
|
| 100 |
+
cursorIndex:(this.viewMode==='single' && this.currentTimestep===this._maxTimestep+1)
|
| 101 |
+
? this._cursorIndex : -1
|
| 102 |
+
};
|
| 103 |
+
if(this.viewMode==='single')
|
| 104 |
+
summary.style.display = this.currentTimestep===this._maxTimestep+1 ? 'flex':'none';
|
| 105 |
+
container.appendChild(summary);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
wrapper.appendChild(container);
|
| 109 |
+
this.appendChild(wrapper);
|
| 110 |
+
this.appendChild(this._buildNav());
|
| 111 |
+
this._updateNavState();
|
| 112 |
+
|
| 113 |
+
/* run Mermaid only on visible diagrams */
|
| 114 |
+
try {
|
| 115 |
+
const sel='.mermaid:not([style*="display: none"])';
|
| 116 |
+
if(this.querySelector(sel)) await window.mermaid.run({querySelector:sel});
|
| 117 |
+
} catch(err) { console.warn('Mermaid render warning:', err); }
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ---------- navigation bar ---------- */
|
| 121 |
+
_buildNav(){
|
| 122 |
+
const nav=document.createElement('div');
|
| 123 |
+
nav.className='bg-white shadow-lg p-4 flex flex-wrap justify-center items-center space-x-4';
|
| 124 |
+
|
| 125 |
+
const mkBtn=(txt,fn)=>{
|
| 126 |
+
const b=document.createElement('button'); b.textContent=txt;
|
| 127 |
+
b.className='px-4 py-2 bg-[#8590F8] text-white rounded hover:bg-[#7E7E7E] transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
|
| 128 |
+
b.addEventListener('click',fn); return b;
|
| 129 |
+
};
|
| 130 |
+
|
| 131 |
+
const prev = mkBtn('Previous', ()=>this._navigate(-1));
|
| 132 |
+
const next = mkBtn('Next', ()=>this._navigate( 1));
|
| 133 |
+
const toggle=mkBtn(this.viewMode==='single'?'View All':'View Single', ()=>this._toggleView());
|
| 134 |
+
const dl = mkBtn('Download SVG', ()=>this._downloadSVG());
|
| 135 |
+
|
| 136 |
+
const indicators=document.createElement('div');
|
| 137 |
+
indicators.className='flex flex-wrap justify-center space-x-2 my-2';
|
| 138 |
+
for(let i=0;i<=this._maxTimestep+1;i++){
|
| 139 |
+
const b=document.createElement('button');
|
| 140 |
+
b.textContent=i===this._maxTimestep+1?'S':i+1;
|
| 141 |
+
b.className='w-8 h-8 rounded-full bg-[#C5C5C5] text-[#1A1A1A] flex items-center justify-center font-bold hover:bg-[#7E7E7E] hover:text-white transition-colors m-1';
|
| 142 |
+
b.addEventListener('click',()=>{this.currentTimestep=i;});
|
| 143 |
+
indicators.appendChild(b);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
nav.append(prev,indicators,next,toggle,dl);
|
| 147 |
+
this._prevB=prev; this._nextB=next; this._toggleB=toggle; this._indWrap=indicators;
|
| 148 |
+
return nav;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
_navigate(dx){
|
| 152 |
+
const total=this._maxTimestep+2;
|
| 153 |
+
let n=this.currentTimestep+dx;
|
| 154 |
+
if(n<0) n=0; if(n>=total) n=total-1;
|
| 155 |
+
this.currentTimestep=n;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
_toggleView(){
|
| 159 |
+
this.viewMode = this.viewMode==='single' ? 'all' : 'single';
|
| 160 |
+
this._toggleB.textContent = this.viewMode==='single' ? 'View All' : 'View Single';
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
_updateNavState(){
|
| 164 |
+
const total=this._maxTimestep+2;
|
| 165 |
+
const noData = !this._knowledge_graph || this._knowledge_graph.length === 0;
|
| 166 |
+
|
| 167 |
+
if(this.viewMode==='all' || noData){
|
| 168 |
+
this._prevB.disabled=this._nextB.disabled=true;
|
| 169 |
+
if(this._indWrap) this._indWrap.querySelectorAll('button').forEach(b=>b.disabled=true);
|
| 170 |
+
} else {
|
| 171 |
+
this._prevB.disabled = this.currentTimestep===0;
|
| 172 |
+
this._nextB.disabled = this.currentTimestep===total-1;
|
| 173 |
+
if (this._indWrap) this._indWrap.querySelectorAll('button').forEach((b,i)=>{
|
| 174 |
+
b.disabled=false;
|
| 175 |
+
if(i===this.currentTimestep){
|
| 176 |
+
b.classList.replace('bg-[#C5C5C5]','bg-[#8590F8]');
|
| 177 |
+
b.classList.replace('text-[#1A1A1A]','text-white');
|
| 178 |
+
} else {
|
| 179 |
+
b.classList.replace('bg-[#8590F8]','bg-[#C5C5C5]');
|
| 180 |
+
b.classList.replace('text-white','text-[#1A1A1A]');
|
| 181 |
+
}
|
| 182 |
+
});
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* ---------- download SVG ---------- */
|
| 187 |
+
async _downloadSVG(){
|
| 188 |
+
let svg;
|
| 189 |
+
if(this.viewMode==='single'){
|
| 190 |
+
svg=this.querySelector('.mermaid:not([style*="display: none"]) svg');
|
| 191 |
+
} else {
|
| 192 |
+
const svgs=[...this.querySelectorAll('.mermaid svg')];
|
| 193 |
+
const combo=document.createElementNS('http://www.w3.org/2000/svg','svg');
|
| 194 |
+
let y=0;
|
| 195 |
+
svgs.forEach(s=>{
|
| 196 |
+
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
|
| 197 |
+
g.innerHTML=s.innerHTML;
|
| 198 |
+
g.setAttribute('transform',`translate(0,${y})`);
|
| 199 |
+
combo.appendChild(g);
|
| 200 |
+
y+=parseInt(s.getAttribute('height'))+20||20;
|
| 201 |
+
});
|
| 202 |
+
combo.setAttribute('width',Math.max(...svgs.map(s=>parseInt(s.getAttribute('width'))||0)));
|
| 203 |
+
combo.setAttribute('height',y);
|
| 204 |
+
svg=combo;
|
| 205 |
+
}
|
| 206 |
+
if(!svg) return console.error('downloadSVG: no SVG element');
|
| 207 |
+
const xml=new XMLSerializer().serializeToString(svg);
|
| 208 |
+
const src=/^<svg[^>]+xmlns=/.test(xml)?xml:xml.replace(/^<svg/,'<svg xmlns="http://www.w3.org/2000/svg"');
|
| 209 |
+
const url='data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<?xml version="1.0"?>\n'+src);
|
| 210 |
+
const a=document.createElement('a'); a.href=url; a.download='temporal_graph.svg';
|
| 211 |
+
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
customElements.define('temporal-graph-canva', TemporalGraphCanva);
|
| 216 |
+
|
| 217 |
+
/* All rights reserved Michael Anthony 2025 */
|
temporal-graph-timestep.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ===========================================================
|
| 2 |
+
temporal-graph-timestep.js
|
| 3 |
+
=========================================================== */
|
| 4 |
+
class TemporalGraphTimestep extends HTMLElement {
|
| 5 |
+
constructor() {
|
| 6 |
+
super();
|
| 7 |
+
this._knowledge_graph = [];
|
| 8 |
+
this._timestep = 0;
|
| 9 |
+
this._cursorIndex = -1;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
connectedCallback() {
|
| 13 |
+
this.className =
|
| 14 |
+
'mermaid w-[100%] h-[100%] flex items-center justify-center';
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/** Accepts { knowledge_graph, timestep, cursorIndex } */
|
| 18 |
+
set data(v) {
|
| 19 |
+
this._knowledge_graph = v.knowledge_graph;
|
| 20 |
+
this._timestep = v.timestep;
|
| 21 |
+
this._cursorIndex = 'cursorIndex' in v ? v.cursorIndex : -1;
|
| 22 |
+
this.render();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
generateMermaidDiagram() {
|
| 26 |
+
/* ---------- Mermaid init block: 36px with !important ---------- */
|
| 27 |
+
const initCSS = `%%{init:{
|
| 28 |
+
"themeCSS": ".node text{font-size:36px !important;} .edgeLabel text{font-size:36px !important;}"
|
| 29 |
+
}}%%`;
|
| 30 |
+
|
| 31 |
+
const first = this._knowledge_graph.find(r => r[3] === 0);
|
| 32 |
+
const title = first ? first[2] : 'Temporal Graph';
|
| 33 |
+
|
| 34 |
+
/* ---------- preprocess ---------- */
|
| 35 |
+
const unique = [];
|
| 36 |
+
const times = {};
|
| 37 |
+
const seq = {};
|
| 38 |
+
const firstOcc = {};
|
| 39 |
+
const allNodes = new Set();
|
| 40 |
+
|
| 41 |
+
this._knowledge_graph.forEach(([s,r,t,ts])=>{
|
| 42 |
+
const k=`${s}|${r}|${t}`;
|
| 43 |
+
allNodes.add(s); allNodes.add(t);
|
| 44 |
+
|
| 45 |
+
if(!times[k]){unique.push({s,r,t});times[k]=[];}
|
| 46 |
+
times[k].push(ts);
|
| 47 |
+
|
| 48 |
+
if(!seq[ts]) seq[ts]={cnt:1};
|
| 49 |
+
if(!seq[ts][k]){
|
| 50 |
+
seq[ts][k]=seq[ts].cnt++;
|
| 51 |
+
if(!firstOcc[k]) firstOcc[k]={ts,seq:seq[ts][k]};
|
| 52 |
+
}
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
const global=unique.map(o=>{
|
| 56 |
+
const k=`${o.s}|${o.r}|${o.t}`;return {...o,...firstOcc[k]};
|
| 57 |
+
}).sort((a,b)=>a.ts===b.ts? a.seq-b.seq : a.ts-b.ts);
|
| 58 |
+
|
| 59 |
+
// **FIXED HERE**: The bug from before was a typo in this line.
|
| 60 |
+
const orderIdx={}; global.forEach((o,i)=>orderIdx[`${o.s}|${o.r}|${o.t}`]=i);
|
| 61 |
+
|
| 62 |
+
/* ---------- build diagram ---------- */
|
| 63 |
+
let code = `${initCSS}
|
| 64 |
+
---
|
| 65 |
+
title: ${title}
|
| 66 |
+
---
|
| 67 |
+
graph LR
|
| 68 |
+
subgraph " "
|
| 69 |
+
direction LR
|
| 70 |
+
`;
|
| 71 |
+
let linkStyle='', nodeStyle='', linkIdx=0;
|
| 72 |
+
const activeNodes=new Set();
|
| 73 |
+
|
| 74 |
+
unique.forEach(({s,r,t})=>{
|
| 75 |
+
const k=`${s}|${r}|${t}`;
|
| 76 |
+
// **RESTORED**: Using original logic for node IDs.
|
| 77 |
+
const sId=s.replace(/\s+/g,'');
|
| 78 |
+
const tId=t.replace(/\s+/g,'');
|
| 79 |
+
const active=(this._timestep==='summary')||times[k].includes(this._timestep);
|
| 80 |
+
|
| 81 |
+
let lb=r, sq, stp;
|
| 82 |
+
if(active){
|
| 83 |
+
if(this._timestep==='summary'){sq=firstOcc[k].seq;stp=firstOcc[k].ts+1;}
|
| 84 |
+
else{sq=seq[this._timestep][k];stp=this._timestep+1;}
|
| 85 |
+
lb=`${stp}.${sq} ${r}`;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const highlight=
|
| 89 |
+
active && this._cursorIndex>=0 &&
|
| 90 |
+
(
|
| 91 |
+
(this._timestep==='summary' && orderIdx[k]===this._cursorIndex) ||
|
| 92 |
+
(this._timestep!=='summary' && sq-1===this._cursorIndex)
|
| 93 |
+
);
|
| 94 |
+
|
| 95 |
+
// **RESTORED**: Using original logic for creating nodes and links.
|
| 96 |
+
if(active){
|
| 97 |
+
code += ` ${sId}[${s}] -->|${lb}| ${tId}[${t}]\n`;
|
| 98 |
+
activeNodes.add(s); activeNodes.add(t);
|
| 99 |
+
if(highlight){
|
| 100 |
+
linkStyle+=` linkStyle ${linkIdx} stroke:#8590F8,stroke-width:4px,color:#8590F8\n`;
|
| 101 |
+
nodeStyle+=
|
| 102 |
+
` style ${sId} fill:#1A1A1A,stroke:#1A1A1A,color:#ffffff\n`+
|
| 103 |
+
` style ${tId} fill:#8590F8,stroke:#8590F8,color:#ffffff\n`;
|
| 104 |
+
}
|
| 105 |
+
} else {
|
| 106 |
+
code += ` ${sId}[${s}] -.-|${lb}| ${tId}[${t}]\n`;
|
| 107 |
+
linkStyle+=` linkStyle ${linkIdx} stroke:#ffffff,stroke-width:2px,color:#ffffff\n`;
|
| 108 |
+
}
|
| 109 |
+
linkIdx++;
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
// **RESTORED**: Using original logic for hiding inactive nodes.
|
| 113 |
+
allNodes.forEach(n=>{
|
| 114 |
+
if(!activeNodes.has(n)){
|
| 115 |
+
nodeStyle+=` style ${n.replace(/\s+/g,'')} fill:#ffffff,stroke:#ffffff,color:#ffffff\n`;
|
| 116 |
+
}
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
return code + nodeStyle + linkStyle + 'end\n';
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// **RESTORED**: Using original, simple render method.
|
| 123 |
+
render() { this.textContent = this.generateMermaidDiagram(); }
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
customElements.define('temporal-graph-timestep', TemporalGraphTimestep);
|
| 127 |
+
|
| 128 |
+
/* All rights reserved Michael Anthony */
|