File size: 5,391 Bytes
a3b5cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
003797e
a3b5cf9
 
 
 
 
 
 
 
003797e
a3b5cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
003797e
a3b5cf9
 
e16fbbb
a3b5cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<!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>