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