Code4Scene / assets /pareto.js
KoeYe's picture
Claude Opus 5.5
Text-to-Scene / Image-to-Scene tabs in every section; restore the Image-to-Scene task, views, cases, takeaways and models
5bf5905
Raw History Blame Contribute Delete
6.16 kB
import {bindChartTooltip} from "./chart-interactions.js?v=20261004kit";
// Adapted from BuildingBench's figure-kit/pareto-frontier/render.py and live SVG.
// Domain, costs and scores are exclusively Code4Scene's historical paper evaluation.
const NS='http://www.w3.org/2000/svg';
const colors={openai:'#10A37F',claude:'#D97757',gemini:'#7b1fa2',meta:'#42a5f5',deepseek:'#3f51e0',zai:'#96650b',grok:'#111111',qwen:'#fe7016',thinkingmachines:'#686868'};
const data=await fetch('data/pareto.json?v=20261001b').then(r=>r.json());
await Promise.all([document.fonts.load('400 13px Inter'),document.fonts.load('700 16px Inter')]);
const measure=document.createElement('canvas').getContext('2d');
function node(tag,attrs,text){const n=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,v);if(text!==undefined)n.textContent=text;return n;}
const lum=c=>{const a=c.match(/\w\w/g).map(x=>parseInt(x,16)/255).map(x=>x<=.03928?x/12.92:((x+.055)/1.055)**2.4);return .2126*a[0]+.7152*a[1]+.0722*a[2];};
function labelColor(hex,front){let a=hex.slice(1).match(/../g).map(x=>parseInt(x,16)/255),max=Math.max(...a),min=Math.min(...a),l=(max+min)/2,h=0,s=0,d=max-min;if(d){s=d/(1-Math.abs(2*l-1));h=max===a[0]?((a[1]-a[2])/d)%6:max===a[1]?(a[2]-a[0])/d+2:(a[0]-a[1])/d+4;h=(h/6+1)%1;}s=Math.min(s,front?.92:.55);const rgb=l=>{const c=(1-Math.abs(2*l-1))*s,x=c*(1-Math.abs(h*6%2-1)),m=l-c/2;const q=h<1/6?[c,x,0]:h<2/6?[x,c,0]:h<3/6?[0,c,x]:h<4/6?[0,x,c]:h<5/6?[x,0,c]:[c,0,x];return '#'+q.map(v=>Math.round((v+m)*255).toString(16).padStart(2,'0')).join('');};const target=front?4.6:2.9;if(front&&(lum('fffefb')+.05)/(lum(hex.slice(1))+.05)>=target)return hex;let lo=0,hi=1;for(let i=0;i<26;i++){let mid=(lo+hi)/2;if((lum('fffefb')+.05)/(lum(rgb(mid).slice(1))+.05)>target)lo=mid;else hi=mid;}return rgb((lo+hi)/2);}
for(const svg of document.querySelectorAll('.pareto-svg')){
const rows=data[svg.dataset.mode],front=rows.filter(a=>!rows.some(b=>b.cost<=a.cost&&b.score>=a.score&&(b.cost<a.cost||b.score>a.score))).sort((a,b)=>b.cost-a.cost),on=new Set(front.map(d=>d.key));
const lo=Math.min(...rows.map(d=>-Math.log10(d.cost)))-.30,hi=Math.max(...rows.map(d=>-Math.log10(d.cost)))+.30;
const x=c=>96+(-Math.log10(c)-lo)/(hi-lo)*1074,y=s=>473.2-s/.96*448;
const add=(tag,attrs,text)=>{const n=node(tag,attrs,text);svg.append(n);return n;};
add('rect',{x:0,y:0,width:1200,height:560,fill:'#fffefb'});
for(const t of [0,.24,.48,.72,.96]){add('line',{x1:96,x2:1170,y1:y(t),y2:y(t),class:'grid'});add('text',{x:84,y:y(t)+4,class:'tick','text-anchor':'end'},t.toFixed(2));}
for(const c of [100,50,20,10,5,2,1,.5,.2,.1,.05,.02,.01,.005,.002,.001])if(-Math.log10(c)>lo&&-Math.log10(c)<hi){add('line',{x1:x(c),x2:x(c),y1:25.2,y2:473.2,class:'grid vertical'});add('text',{x:x(c),y:499,class:'tick','text-anchor':'middle'},'$'+c);}
add('line',{x1:96,x2:1170,y1:473.2,y2:473.2,class:'axis'});
add('text',{x:633,y:539,class:'axis-title','text-anchor':'middle'},'Mean Cost per Case (USD) · cheaper →');
add('text',{x:27,y:249,transform:'rotate(-90 27 249)',class:'axis-title','text-anchor':'middle'},{t2s:'Text-to-Scene Score',i2s:'Image-to-Scene Score'}[svg.dataset.mode]);
const path=front.map((d,i)=>(i?'L':'M')+x(d.cost)+','+y(d.score)).join(' ');
add('path',{d:path,class:'frontier-halo'});add('path',{d:path,class:'frontier-line'});
const points=rows.map(d=>({x:x(d.cost),y:y(d.score)}));const boxes=points.map(p=>[p.x-9,p.y-9,p.x+9,p.y+9]);
const linePoints=[];for(let i=1;i<front.length;i++){const a=front[i-1],b=front[i],n=Math.max(2,Math.ceil(Math.hypot(x(b.cost)-x(a.cost),y(b.score)-y(a.score))/5));for(let j=0;j<=n;j++)linePoints.push([x(a.cost)+(x(b.cost)-x(a.cost))*j/n,y(a.score)+(y(b.score)-y(a.score))*j/n]);}
for(const d of [...rows].sort((a,b)=>(on.has(b.key)-on.has(a.key))||b.score-a.score)){
const isFront=on.has(d.key),cx=x(d.cost),cy=y(d.score),font=isFront?16:12.5,name=d.name.replace(/^Claude /,'').replace(/\((.*?)\)/,(_,s)=>'('+s.replace(/\b\w/g,c=>c.toUpperCase())+')'),size=font;
measure.font=`${isFront?700:400} ${size}px Inter`;const tw=measure.measureText(name).width,th=size*.78,r=9,gap=5,pad=2.5,up=r+gap,dn=r+gap+th;
const candidates=[];for(let k=0;k<5;k++){const lift=k*th*1.55;candidates.push([0,-up-lift,'middle'],[0,dn+lift,'middle'],[r+gap,th*.4-lift,'start'],[-r-gap,th*.4-lift,'end'],[r+gap,th*.4+lift,'start'],[-r-gap,th*.4+lift,'end'],[r*.6,-up*.82-lift,'start'],[-r*.6,-up*.82-lift,'end'],[r*.6,dn*.9+lift,'start'],[-r*.6,dn*.9+lift,'end']);}
const hard=[...boxes,...linePoints.filter(p=>Math.hypot(p[0]-cx,p[1]-cy)>r*2.4).map(p=>[p[0]-4,p[1]-4,p[0]+4,p[1]+4])];let best,bestCost=Infinity;
for(let i=0;i<candidates.length;i++){const[dx,dy,anchor]=candidates[i],tx=cx+dx,ty=cy+dy,x0=anchor==='middle'?tx-tw/2:anchor==='end'?tx-tw:tx,b=[x0-pad,ty-th-pad,x0+tw+pad,ty+pad];let cost=i*.6;if(b[0]<58)cost+=400+58-b[0];if(b[2]>1180)cost+=400+b[2]-1180;if(b[1]<9)cost+=400+9-b[1];if(b[3]>475)cost+=400+b[3]-475;let overlap=0;for(const q of hard){const w=Math.min(b[2],q[2])-Math.max(b[0],q[0]),h=Math.min(b[3],q[3])-Math.max(b[1],q[1]);if(w>0&&h>0)overlap+=w*h;}cost+=overlap*4;if(cost<bestCost){bestCost=cost;best={dx,dy,anchor,b};}if(!overlap&&cost<400)break;}
boxes.push(best.b);const color=colors[d.logo]||'#686868',label=labelColor(color,isFront);
const g=add('g',{class:'pareto-node'+(isFront?' is-frontier':''),tabindex:0,role:'img','aria-label':`${d.name}, score ${d.score.toFixed(6)}, mean cost $${d.cost.toFixed(6)}${isFront?', Pareto frontier':''}`,'data-name':d.name,'data-score':d.score,'data-cost':d.cost,'data-frontier':isFront});
if(isFront)g.append(node('circle',{cx,cy,r:9,fill:'none',stroke:color,'stroke-width':1.3}));
g.append(node('rect',{x:cx-5,y:cy-5,width:10,height:10,rx:1,fill:color,stroke:'#fffefb','stroke-width':1.7,class:'chip'}));
g.append(node('text',{x:cx+best.dx,y:cy+best.dy,'text-anchor':best.anchor,fill:label,'font-size':font,'font-weight':isFront?700:400,class:'model-label'},name));
g.append(node('circle',{cx,cy,r:14,fill:'transparent'}));
bindChartTooltip(g,svg,d.key,svg.dataset.mode,'.chip');
}
svg.dataset.ready='true';
}