/* ===========================================================
temporal-graph-canva.js
=========================================================== */
import './temporal-graph-timestep.js';
class TemporalGraphCanva extends HTMLElement {
constructor() {
super();
this._knowledge_graph = [];
this._maxTimestep = 0;
this._cursorIndex = -1;
this._keyHandler = this._keyHandler.bind(this);
}
/* ---------- observed attributes ---------- */
static get observedAttributes() { return ['current-timestep','view-mode']; }
get currentTimestep() { return parseInt(this.getAttribute('current-timestep') || '0'); }
set currentTimestep(v){ this.setAttribute('current-timestep', v); this._cursorIndex=-1; }
get viewMode() { return this.getAttribute('view-mode') || 'single'; }
set viewMode(v) { this.setAttribute('view-mode', v); this._cursorIndex=-1; }
/* ---------- lifecycle ---------- */
connectedCallback() { document.addEventListener('keydown', this._keyHandler); }
disconnectedCallback(){ document.removeEventListener('keydown', this._keyHandler); }
async attributeChangedCallback(n,o,v){ if(o!==v) await this._render(); }
async render(kg){
this._knowledge_graph=kg;
if (!kg || kg.length === 0) {
this._maxTimestep = 0;
} else {
this._maxTimestep=Math.max(...kg.map(r=>r[3]));
}
await this._render();
}
/* ---------- keyboard ---------- */
_keyHandler(e){
const k=e.key.toLowerCase();
if(this.viewMode==='single' && (k==='q'||k==='e')){
const total=this._relationCount();
if(total){
if(k==='e'){ this._cursorIndex++; if(this._cursorIndex>total-1) this._cursorIndex=-1; }
else { this._cursorIndex--; if(this._cursorIndex< -1) this._cursorIndex=total-1; }
this._render();
}
return;
}
if(this.viewMode==='single'){
if(k==='arrowleft'||k==='a') this._navigate(-1);
else if(k==='arrowright'||k==='d') this._navigate(1);
else if(k==='s') this.currentTimestep=this._maxTimestep+1;
}
if(k==='f') this._toggleView();
}
/* ---------- helpers ---------- */
_relationCount(){
const t=this.currentTimestep;
if(t===this._maxTimestep+1){
return new Set(this._knowledge_graph.map(([s,r,t])=>`${s}|${r}|${t}`)).size;
}
if(t<0 || t>this._maxTimestep) return 0;
return new Set(
this._knowledge_graph.filter(r=>r[3]===t).map(([s,r,t])=>`${s}|${r}|${t}`)
).size;
}
/* ---------- render ---------- */
async _render(){
this.innerHTML='';
const wrapper=document.createElement('div');
wrapper.className='h-full flex flex-col bg-white rounded-lg shadow-lg p-4 flex-grow overflow-hidden';
const container=document.createElement('div');
container.className=this.viewMode==='single'
? 'flex justify-center items-center w-full h-full'
: 'grid grid-cols-1 md:grid-cols-2 gap-0 w-full h-full overflow-auto';
if (!this._knowledge_graph || this._knowledge_graph.length === 0) {
container.innerHTML = `
Paste TSV data and click Visualize.
`;
} else {
/* each real timestep */
for(let ts=0;ts<=this._maxTimestep;ts++){
const el=document.createElement('temporal-graph-timestep');
el.data={
knowledge_graph:this._knowledge_graph,
timestep:ts,
cursorIndex:(this.viewMode==='single' && ts===this.currentTimestep) ? this._cursorIndex : -1
};
if(this.viewMode==='single') el.style.display = ts===this.currentTimestep ? 'flex':'none';
container.appendChild(el);
}
/* summary page */
const summary=document.createElement('temporal-graph-timestep');
summary.data={
knowledge_graph:this._knowledge_graph,
timestep:'summary',
cursorIndex:(this.viewMode==='single' && this.currentTimestep===this._maxTimestep+1)
? this._cursorIndex : -1
};
if(this.viewMode==='single')
summary.style.display = this.currentTimestep===this._maxTimestep+1 ? 'flex':'none';
container.appendChild(summary);
}
wrapper.appendChild(container);
this.appendChild(wrapper);
this.appendChild(this._buildNav());
this._updateNavState();
/* run Mermaid only on visible diagrams */
try {
const sel='.mermaid:not([style*="display: none"])';
if(this.querySelector(sel)) await window.mermaid.run({querySelector:sel});
} catch(err) { console.warn('Mermaid render warning:', err); }
}
/* ---------- navigation bar ---------- */
_buildNav(){
const nav=document.createElement('div');
nav.className='bg-white shadow-lg p-4 flex flex-wrap justify-center items-center space-x-4';
const mkBtn=(txt,fn)=>{
const b=document.createElement('button'); b.textContent=txt;
b.className='px-4 py-2 bg-[#8590F8] text-white rounded hover:bg-[#7E7E7E] transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
b.addEventListener('click',fn); return b;
};
const prev = mkBtn('Previous', ()=>this._navigate(-1));
const next = mkBtn('Next', ()=>this._navigate( 1));
const toggle=mkBtn(this.viewMode==='single'?'View All':'View Single', ()=>this._toggleView());
const dl = mkBtn('Download SVG', ()=>this._downloadSVG());
const indicators=document.createElement('div');
indicators.className='flex flex-wrap justify-center space-x-2 my-2';
for(let i=0;i<=this._maxTimestep+1;i++){
const b=document.createElement('button');
b.textContent=i===this._maxTimestep+1?'S':i+1;
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';
b.addEventListener('click',()=>{this.currentTimestep=i;});
indicators.appendChild(b);
}
nav.append(prev,indicators,next,toggle,dl);
this._prevB=prev; this._nextB=next; this._toggleB=toggle; this._indWrap=indicators;
return nav;
}
_navigate(dx){
const total=this._maxTimestep+2;
let n=this.currentTimestep+dx;
if(n<0) n=0; if(n>=total) n=total-1;
this.currentTimestep=n;
}
_toggleView(){
this.viewMode = this.viewMode==='single' ? 'all' : 'single';
this._toggleB.textContent = this.viewMode==='single' ? 'View All' : 'View Single';
}
_updateNavState(){
const total=this._maxTimestep+2;
const noData = !this._knowledge_graph || this._knowledge_graph.length === 0;
if(this.viewMode==='all' || noData){
this._prevB.disabled=this._nextB.disabled=true;
if(this._indWrap) this._indWrap.querySelectorAll('button').forEach(b=>b.disabled=true);
} else {
this._prevB.disabled = this.currentTimestep===0;
this._nextB.disabled = this.currentTimestep===total-1;
if (this._indWrap) this._indWrap.querySelectorAll('button').forEach((b,i)=>{
b.disabled=false;
if(i===this.currentTimestep){
b.classList.replace('bg-[#C5C5C5]','bg-[#8590F8]');
b.classList.replace('text-[#1A1A1A]','text-white');
} else {
b.classList.replace('bg-[#8590F8]','bg-[#C5C5C5]');
b.classList.replace('text-white','text-[#1A1A1A]');
}
});
}
}
/* ---------- download SVG ---------- */
async _downloadSVG(){
let svg;
if(this.viewMode==='single'){
svg=this.querySelector('.mermaid:not([style*="display: none"]) svg');
} else {
const svgs=[...this.querySelectorAll('.mermaid svg')];
const combo=document.createElementNS('http://www.w3.org/2000/svg','svg');
let y=0;
svgs.forEach(s=>{
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
g.innerHTML=s.innerHTML;
g.setAttribute('transform',`translate(0,${y})`);
combo.appendChild(g);
y+=parseInt(s.getAttribute('height'))+20||20;
});
combo.setAttribute('width',Math.max(...svgs.map(s=>parseInt(s.getAttribute('width'))||0)));
combo.setAttribute('height',y);
svg=combo;
}
if(!svg) return console.error('downloadSVG: no SVG element');
const xml=new XMLSerializer().serializeToString(svg);
const src=/^