audio-cpp's picture
Publish Audio Model Architecture Atlas
f4cd8a9 verified
Raw History Blame Contribute Delete
4.69 kB
/* SVG rendering for authored DAGs. Dagre handles branching and shared nodes. */
window.AtlasGraph = (() => {
let serial = 0;
const NS = 'http://www.w3.org/2000/svg';
const colors = {ar:['#315fc1','#e9effc'],flow:['#a63975','#fae9f3'],encoder:['#007e70','#e3f4ef'],codec:['#956609','#faf0d6'],head:['#557b31','#edf4e4'],dsp:['#58777d','#eef3f4'],input:['#75888b','#fff'],output:['#75888b','#fff'],model:['#263e43','#f3f7f5']};
const roles = {ar:'AR BACKBONE',flow:'GENERATION',encoder:'ENCODER',codec:'CODEC / VOCODER',head:'PREDICTION',dsp:'FRONTEND / DSP',input:'INPUT',output:'OUTPUT',model:'MODEL'};
function element(name,attrs={},text){const n=document.createElementNS(NS,name);for(const [k,v] of Object.entries(attrs))n.setAttribute(k,v);if(text!==undefined)n.textContent=text;return n;}
const measure=document.createElement('canvas').getContext('2d');
function wrap(text,width,font){measure.font=font;const out=[];let line='';for(const word of text.split(/\s+/)){const next=line?line+' '+word:word;if(line&&measure.measureText(next).width>width){out.push(line);line=word;}else line=next;}if(line)out.push(line);return out;}
function render(container,nodes,edges,{onNode,active,compact=false,residuals=[]}={}){
const graph=new dagre.graphlib.Graph({multigraph:true});
graph.setGraph({rankdir:'TB',ranksep:compact?28:66,nodesep:compact?25:34,marginx:28,marginy:25});graph.setDefaultEdgeLabel(()=>({}));
for(const node of nodes){const io=['input','output'].includes(node.role);const width=compact?205:230;
const lines=wrap(node.label,width-28,`700 ${compact?12:14}px Noto`);
graph.setNode(node.id,{...node,width,height:(io?27:40)+lines.length*(compact?18:20),lines});
}
edges.forEach((edge,i)=>graph.setEdge(edge.from,edge.to,{...edge,width:edge.label?135:0,height:edge.label?16:0},String(i)));
dagre.layout(graph);
const {width,height}=graph.graph(),id='arrow-'+serial++;
const svg=element('svg',{xmlns:NS,viewBox:`0 0 ${width} ${height}`,width,height,role:'group','aria-label':'Architecture diagram'});
const defs=element('defs'),marker=element('marker',{id,markerWidth:7,markerHeight:7,refX:6,refY:3.5,orient:'auto'});
marker.append(element('path',{d:'M0 0 L7 3.5 L0 7',fill:'none',stroke:'#90a6a9'}));defs.append(marker);svg.append(defs);
for(const e of graph.edges()){const data=graph.edge(e),points=data.points;
const path=element('path',{d:points.map((p,i)=>(i?'L':'M')+p.x+' '+p.y).join(' '),fill:'none',stroke:data.dim?'#c3cecf':'#92aaa9','stroke-width':data.highlight?2:1.3,'stroke-dasharray':data.optional?'5 4':'none','marker-end':`url(#${id})`,opacity:data.dim?.24:1});
path.append(element('title',{},data.label||'Data flow'));svg.append(path);
if(data.label){const label=element('text',{x:data.x,y:data.y+4,'text-anchor':'middle',fill:'#637b7e','font-size':10,'paint-order':'stroke',stroke:'white','stroke-width':4,'stroke-linejoin':'round'},data.label);svg.append(label);}
}
for(const [a,b] of residuals){const start=graph.node(a),end=graph.node(b);const x=Math.min(start.x-start.width/2,end.x-end.width/2)-15;
svg.append(element('path',{d:`M${start.x-start.width/2} ${start.y-start.height/2} H${x} V${end.y} H${end.x-end.width/2}`,stroke:'#789698',fill:'none','stroke-dasharray':'3 2'}));
}
for(const nodeId of graph.nodes()){const n=graph.node(nodeId),[ink,fill]=colors[n.role]||colors.dsp;
const g=element('g',{class:'node'+(n.dim?' dim':''),'data-node':n.id,transform:`translate(${n.x-n.width/2},${n.y-n.height/2})`});
if(n.action){g.setAttribute('tabindex','0');g.setAttribute('role','button');g.setAttribute('aria-label',n.actionLabel||n.label);if(n.pressed!==undefined)g.setAttribute('aria-pressed',n.pressed);}
const io=['input','output'].includes(n.role);
g.append(element('rect',{width:n.width,height:n.height,rx:io?2:5,fill,stroke:ink,'stroke-opacity':io?.3:.5,'stroke-width':n.id===active?2:1,'stroke-dasharray':n.optional?'5 4':'none'}));
g.append(element('text',{x:n.width/2,y:16,'text-anchor':'middle','font-size':8,fill:ink},n.sublabel??roles[n.role]??''));
n.lines.forEach((l,i)=>g.append(element('text',{x:n.width/2,y:io?33+i*18:38+i*(compact?18:20),'text-anchor':'middle','font-weight':io?400:700,'font-size':compact?12:14,fill:ink},l)));
g.append(element('title',{},n.description||n.label));
if(n.action){const invoke=()=>onNode(n);g.addEventListener('click',invoke);g.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();invoke();}});}
svg.append(g);
}
container.replaceChildren(svg);return {width,height,svg};
}
return {render,colors};
})();