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 */
|