Spaces:
Running
Running
| /* =========================================================== | |
| 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 = `<div class="flex items-center justify-center h-full text-gray-500">Paste TSV data and click Visualize.</div>`; | |
| } 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=/^<svg[^>]+xmlns=/.test(xml)?xml:xml.replace(/^<svg/,'<svg xmlns="http://www.w3.org/2000/svg"'); | |
| const url='data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<?xml version="1.0"?>\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 */ |