/* =========================================================== 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=/^]+xmlns=/.test(xml)?xml:xml.replace(/^\n'+src); const a=document.createElement('a'); a.href=url; a.download='temporal_graph.svg'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } } customElements.define('temporal-graph-canva', TemporalGraphCanva); /* All rights reserved Michael Anthony 2025 */