visual-tree / tree_widget.html
ShesterG's picture
consistent palette (blue=focus, red=past only); tooltip shows % + raw mean attn value
003797e
Raw
History Blame Contribute Delete
5.39 kB
<!DOCTYPE html>
<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 &rarr; 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>