Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <style> | |
| html,body{margin:0;height:100%;background:#0f1117;color:#e8eaf0;font-family:'Inter','Segoe UI',sans-serif;overflow:hidden} | |
| #wrap{display:flex;height:100vh;gap:18px;padding:14px;box-sizing:border-box} | |
| #left{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;justify-content:center} | |
| #imgbox{position:relative;border-radius:8px;overflow:hidden;box-shadow:0 0 0 1px #2a2d3e} | |
| #imgbox img{display:block} | |
| #hl{position:absolute;left:0;top:0;pointer-events:none} | |
| #cap{font-size:12px;color:#7b8290;margin-top:8px;max-width:340px;text-align:center} | |
| #right{flex:1 1 auto;position:relative} | |
| svg{width:100%;height:100%} | |
| .edge{stroke:#3a3f55;stroke-width:1} | |
| .node{cursor:pointer;transition:r .08s ease,stroke-width .08s ease} | |
| .node:hover{stroke:#fff;stroke-width:2} | |
| #tip{position:absolute;pointer-events:none;background:#1a2240;border:1px solid #5b9cf6;border-radius:6px; | |
| padding:4px 8px;font-size:12px;color:#e8eaf0;opacity:0;transition:opacity .1s;white-space:nowrap} | |
| .legend{position:absolute;left:8px;bottom:8px;font-size:11px;color:#7b8290} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="wrap"> | |
| <div id="left"> | |
| <div id="imgbox"><img id="img"><canvas id="hl"></canvas></div> | |
| <div id="cap">Hover a node in the tree → its region lights up here.<br>Root = whole image; leaves = finest regions.</div> | |
| </div> | |
| <div id="right"> | |
| <svg id="svg"></svg> | |
| <div id="tip"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const STEM = new URLSearchParams(location.search).get('stem') || 'tennis_tree'; | |
| let T, G, NODES; | |
| const img = document.getElementById('img'), cv = document.getElementById('hl'), ctx = cv.getContext('2d'); | |
| const svg = document.getElementById('svg'), tip = document.getElementById('tip'), right = document.getElementById('right'); | |
| const NS='http://www.w3.org/2000/svg'; | |
| // ---- size the image square to fit the left column ---- | |
| function sizeImage(){ | |
| const S = Math.min(window.innerHeight*0.62, window.innerWidth*0.38, 460); | |
| img.style.width=S+'px'; img.style.height=S+'px'; | |
| cv.width=S; cv.height=S; cv.style.width=S+'px'; cv.style.height=S+'px'; | |
| } | |
| // attention color (low=blue, high=orange) | |
| function attColor(a){ | |
| const t=Math.min(1, a/25); // 25% saturates | |
| const c1=[150,158,176], c2=[244,162,97]; // attention heat: grey(low) -> orange(high) | |
| return `rgb(${Math.round(c1[0]+(c2[0]-c1[0])*t)},${Math.round(c1[1]+(c2[1]-c1[1])*t)},${Math.round(c1[2]+(c2[2]-c1[2])*t)})`; | |
| } | |
| function highlight(node){ | |
| const S=cv.width, cell=S/G; ctx.clearRect(0,0,S,S); | |
| // dim the whole image, then punch the segment back to full brightness | |
| ctx.fillStyle='rgba(8,10,18,0.72)'; ctx.fillRect(0,0,S,S); | |
| const set=new Set(node.patches); | |
| node.patches.forEach(p=>{ const r=Math.floor(p/G), c=p%G; ctx.clearRect(c*cell,r*cell,cell,cell); }); | |
| ctx.strokeStyle='rgba(91,156,246,0.95)'; ctx.lineWidth=1.5; // blue = "focused" region | |
| // outline boundary of the lit region | |
| node.patches.forEach(p=>{ const r=Math.floor(p/G), c=p%G; | |
| const nb=[[r-1,c],[r+1,c],[r,c-1],[r,c+1]]; | |
| nb.forEach(([rr,ccc])=>{ if(rr<0||rr>=G||ccc<0||ccc>=G||!set.has(rr*G+ccc)){ | |
| ctx.beginPath(); | |
| if(rr<r){ctx.moveTo(c*cell,r*cell);ctx.lineTo((c+1)*cell,r*cell);} | |
| else if(rr>r){ctx.moveTo(c*cell,(r+1)*cell);ctx.lineTo((c+1)*cell,(r+1)*cell);} | |
| else if(ccc<c){ctx.moveTo(c*cell,r*cell);ctx.lineTo(c*cell,(r+1)*cell);} | |
| else {ctx.moveTo((c+1)*cell,r*cell);ctx.lineTo((c+1)*cell,(r+1)*cell);} | |
| ctx.stroke(); | |
| }}); | |
| }); | |
| } | |
| function clearHL(){ ctx.clearRect(0,0,cv.width,cv.height); } | |
| let circles=[]; | |
| function drawTree(){ | |
| const W=right.clientWidth, H=right.clientHeight, pad=26; | |
| svg.setAttribute('viewBox',`0 0 ${W} ${H}`); | |
| const xs=x=>pad+(x/(Math.max(T.nleaves-1,1)))*(W-2*pad); | |
| const ys=d=>pad+(d/Math.max(T.maxdepth,1))*(H-2*pad); | |
| let s=''; | |
| NODES.forEach(n=>{ if(n.parent>=0){ const p=NODES[n.parent]; | |
| s+=`<line class="edge" x1="${xs(p.x)}" y1="${ys(p.depth)}" x2="${xs(n.x)}" y2="${ys(n.depth)}"/>`; }}); | |
| NODES.forEach(n=>{ const r=Math.min(22, 2.5+2.0*Math.sqrt(n.att)); // radius ∝ sqrt(attention %) | |
| s+=`<circle class="node" data-id="${n.id}" cx="${xs(n.x)}" cy="${ys(n.depth)}" r="${r}" | |
| fill="${attColor(n.att)}" stroke="#0f1117" stroke-width="1"/>`; }); | |
| svg.innerHTML=s; | |
| circles=[...svg.querySelectorAll('.node')]; | |
| circles.forEach(el=>{ | |
| const n=NODES[+el.dataset.id]; | |
| el.addEventListener('mouseenter',e=>{ highlight(n); | |
| tip.style.opacity=1; tip.innerHTML=`level ${n.depth} · ${n.n} patches · ${n.att}% of attention · mean attn ${n.attmv}`+(n.tau!=null?` · splits at τ=${n.tau}`:' · leaf'); | |
| }); | |
| el.addEventListener('mousemove',e=>{ const b=right.getBoundingClientRect(); | |
| tip.style.left=(e.clientX-b.left-tip.offsetWidth-12)+'px'; tip.style.top=(e.clientY-b.top+12)+'px'; }); | |
| el.addEventListener('mouseleave',()=>{ clearHL(); tip.style.opacity=0; }); | |
| }); | |
| } | |
| function layout(){ if(!T) return; sizeImage(); drawTree(); } | |
| window.addEventListener('resize',layout); | |
| // load the per-image data + image based on ?stem= | |
| let dataReady=false, imgReady=false; | |
| function tryLayout(){ if(dataReady && imgReady) layout(); } | |
| img.onload=()=>{ imgReady=true; tryLayout(); }; | |
| T=window.TREE; G=T.g; NODES=T.nodes; dataReady=true; | |
| img.src=window.IMG_SRC; // injected data URI | |
| </script> | |
| </body> | |
| </html> | |