lvwerra's picture
lvwerra HF Staff
Add visible assembly colour legend and release title with tweet figures
62181a5 verified
Raw History Blame Contribute Delete
20.9 kB
const $=s=>document.querySelector(s), fmt=n=>n.toLocaleString('en-US');
let pinnedAssembly=null,hoveredAssembly=null;
let hitData,hitOwner;
let D,nodes,leaves,groups,selected=null,zoom=1,pan={x:0,y:0},baseScale=1,W=1,H=1,gl,program,vertexCount=0,mode=0,view='all',pending=false;
const canvas=$('#tree'),overlay=$('#overlay'),ctx=overlay.getContext('2d'),stage=$('#stage');
const interaction=$('#interaction'),fx=interaction.getContext('2d',{willReadFrequently:true});
const R=740,TAU=Math.PI*2,START=-Math.PI/2+.04,SPAN=TAU-.08;
const ranks={kingdom:175,phylum:272,class:355,order:437,family:520,genus:598,species:665,subspecies:696};
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function polar(r,a){return [r*Math.cos(a),r*Math.sin(a)]}
function point(n,i){let a=START+SPAN*(n.mid+.5)/leaves.length,off=(i-n.mid)*.012;let [x,y]=polar(n.r,a);return [x-Math.sin(a)*off,y+Math.cos(a)*off]}
function controls(p,q){let rp=Math.hypot(...p),rq=Math.hypot(...q),delta=(rq-rp)*.46;return [[p[0]*(1+delta/rp),p[1]*(1+delta/rp)],[q[0]*(1-delta/rq),q[1]*(1-delta/rq)]]}
function bez(p,c,d,q,t){let s=1-t;return [s*s*s*p[0]+3*s*s*t*c[0]+3*s*t*t*d[0]+t*t*t*q[0],s*s*s*p[1]+3*s*s*t*c[1]+3*s*t*t*d[1]+t*t*t*q[1]]}
function prepare(){
nodes=new Map(D.nodes.map(r=>[r[0],{id:r[0],parent:r[1],name:r[2],rank:r[3],common:r[4],children:[],own:[],count:0}]));
for(const n of nodes.values())if(n.parent)nodes.get(n.parent)?.children.push(n);
D.assemblies.forEach((r,index)=>{nodes.get(r[1]).own.push({row:r,index,accession:r[0],taxid:r[1],name:r[2],loci:D.loci.counts[r[0]]??null,flag:(r[4]===1?1:0)+(r[5]?2:0)});});
const root=nodes.get(2759);
function count(n){n.count=n.own.length;n.existing=n.own.filter(a=>a.flag&1).length;n.refMatches=n.own.filter(a=>a.flag&2).length;for(let c of n.children){n.count+=count(c);n.existing+=c.existing;n.refMatches+=c.refMatches}n.children.sort((a,b)=>b.count-a.count||a.id-b.id);return n.count}count(root);
leaves=[];
function order(n){n.lo=leaves.length;for(let c of n.children)order(c);n.own.sort((a,b)=>a.accession.localeCompare(b.accession));leaves.push(...n.own);n.hi=leaves.length;n.mid=(n.lo+n.hi-1)/2;for(let a of n.own)a.i=leaves.indexOf(a,n.lo);}
order(root);
for(let i=0;i<leaves.length;i++){
const a=leaves[i];a.i=i;let n=nodes.get(a.taxid),lineage=[];
while(n){lineage.unshift(n);n=nodes.get(n.parent)}a.lineage=lineage;
const keep=lineage.filter(n=>ranks[n.rank]);let max=92;a.points=[];
// A circular root keeps every assembly's lane distinct at the centre as well.
a.points.push(polar(92,START+SPAN*(i+.5)/leaves.length));
for(n of keep){let r=ranks[n.rank];if(r<=max||r>700)continue;n.r=r;max=r;a.points.push(point(n,i));}
a.angle=START+SPAN*(i+.5)/leaves.length;a.tip=polar(R,a.angle);a.points.push(a.tip);
}
groups=[33208,4751,33090,33630,554915,2763,554296,5761].map(id=>nodes.get(id)).filter(n=>n&&n.count>40);
return root;
}
function shader(type,source){const s=gl.createShader(type);gl.shaderSource(s,source);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(s));return s}
function stepsFor(p,q){let rp=Math.hypot(...p),rq=Math.hypot(...q),a=Math.acos(Math.max(-1,Math.min(1,(p[0]*q[0]+p[1]*q[1])/(rp*rq))));return Math.min(32,Math.max(4,Math.ceil(a*24+Math.abs(rq-rp)/50)))}
function setupGL(){
gl=canvas.getContext('webgl',{alpha:false,antialias:true,preserveDrawingBuffer:true});if(!gl)throw Error('WebGL is unavailable in this browser.');
const vs=shader(gl.VERTEX_SHADER,`attribute vec4 a;uniform vec2 size;uniform vec2 pan;uniform float scale;uniform float dpr;uniform float mode;uniform float only;uniform float exposure;uniform vec3 greenInk;uniform vec3 blueInk;varying vec4 color;void main(){vec2 p=a.xy*scale+size*.5+pan;gl_Position=vec4(p.x/size.x*2.-1.,1.-p.y/size.y*2.,0.,1.);float blue=mode<.5?mod(a.w,2.):step(1.5,a.w);vec3 ink=mix(greenInk,blueInk,blue);float opacity=clamp(a.z*scale*dpr*exposure,0.,.95);if(only>.5){if(blue<.5)opacity*=.06;else opacity=min(.95,max(.14,opacity*4.));}color=vec4(ink,opacity);}`);
const fs=shader(gl.FRAGMENT_SHADER,`precision mediump float;varying vec4 color;void main(){gl_FragColor=color;}`);
program=gl.createProgram();gl.attachShader(program,vs);gl.attachShader(program,fs);gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program));
gl.useProgram(program);gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
// Every real assembly has its own complete path. No generated placeholder leaves.
let len=leaves.reduce((n,a)=>n+a.points.slice(1).reduce((m,p,j)=>m+stepsFor(a.points[j],p)*2*4,0),0),data=new Float32Array(len),k=0;hitOwner=new Uint32Array(len/8);
const shuffled=leaves.slice().sort((a,b)=>hash(a.i)-hash(b.i));
for(const a of shuffled){a.geometryStart=k;for(let j=1;j<a.points.length;j++){
let p=a.points[j-1],q=a.points[j],[c,d]=controls(p,q),prev=p,steps=stepsFor(p,q);
for(let t=1;t<=steps;t++){let next=bez(p,c,d,q,t/steps);hitOwner[k/8]=a.i;for(let v of [prev,next]){data[k++]=v[0];data[k++]=v[1];data[k++]=.008+.054*Math.pow(Math.min(1,Math.hypot(...v)/R),3);data[k++]=a.flag;}prev=next;}
}a.geometryEnd=k;}
hitData=data;vertexCount=k/4;const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);const loc=gl.getAttribLocation(program,'a');gl.enableVertexAttribArray(loc);gl.vertexAttribPointer(loc,4,gl.FLOAT,false,0,0);
window.POC={assemblies:leaves.length,vertices:vertexCount,prior:D.meta.classification_counts['1'],refseq:D.meta.refseq_matches,data:D,leaves,nodes,select:selectAssembly,focus:focusAssembly};
}
function hash(x){x=((x>>>16)^x)*0x45d9f3b;x=((x>>>16)^x)*0x45d9f3b;return ((x>>>16)^x)>>>0}
function schedule(){if(!pending){pending=true;requestAnimationFrame(()=>{pending=false;draw()})}}
function resize(){const b=stage.getBoundingClientRect();W=b.width;H=b.height;let dpr=Math.min(devicePixelRatio,2);canvas.width=overlay.width=interaction.width=Math.round(W*dpr);canvas.height=overlay.height=interaction.height=Math.round(H*dpr);baseScale=Math.min(W/((R+BAR_HEIGHT)*2+180),H/((R+BAR_HEIGHT)*2+180));schedule()}
function screen(p){let s=baseScale*zoom;return [W/2+pan.x+p[0]*s,H/2+pan.y+p[1]*s]}
function world(x,y){let s=baseScale*zoom;return [(x-W/2-pan.x)/s,(y-H/2-pan.y)/s]}
function draw(){if(!gl)return;let dpr=canvas.width/W;gl.viewport(0,0,canvas.width,canvas.height);gl.clearColor(...rgb(palette.background),1);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(program);gl.uniform2f(gl.getUniformLocation(program,'size'),W,H);gl.uniform2f(gl.getUniformLocation(program,'pan'),pan.x,pan.y);gl.uniform1f(gl.getUniformLocation(program,'scale'),baseScale*zoom);gl.uniform1f(gl.getUniformLocation(program,'dpr'),dpr);gl.uniform1f(gl.getUniformLocation(program,'mode'),mode);gl.uniform1f(gl.getUniformLocation(program,'only'),view==='existing'?1:0);gl.uniform1f(gl.getUniformLocation(program,'exposure'),theme==='dark'?2:1);gl.uniform3fv(gl.getUniformLocation(program,'greenInk'),rgb(palette.green));gl.uniform3fv(gl.getUniformLocation(program,'blueInk'),rgb(palette.blue));gl.drawArrays(gl.LINES,0,vertexCount);
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);drawLoci();
const labels=zoom<2.5?groups:[...nodes.values()].filter(n=>['family','order','genus'].includes(n.rank)&&n.count>10&&n.count<leaves.length/zoom*.6);
let placed=locusMode!=='off'&&zoom<1.6?[{x:W/2+pan.x-36,y:screen(polar(R+BAR_GAP+BAR_HEIGHT,START))[1]-14,w:72}]:[];for(const n of labels){let angle=START+SPAN*(n.mid+.5)/leaves.length;let [x,y]=screen(polar(R+BAR_HEIGHT+42,angle));if(x<35||x>W-35||y<25||y>H-25)continue;let name=n.common||n.name;if(name.length>27)name=n.name;ctx.font=(zoom<2.5?'500 13px':'500 12px')+' Inter';ctx.save();ctx.font='10px Inter';let cw=ctx.measureText(fmt(n.count)+' assemblies').width;ctx.restore();let tw=Math.max(ctx.measureText(name).width,cw),anchor=Math.cos(angle)>.25?'left':Math.cos(angle)<-.25?'right':'center';let bx=anchor==='left'?x:anchor==='right'?x-tw:x-tw/2;if(bx<8){x+=8-bx;bx=8}if(bx+tw>W-8){x-=bx+tw-W+8;bx=W-8-tw}if(placed.some(b=>bx<b.x+b.w+8&&bx+tw>b.x-8&&Math.abs(y-b.y)<32))continue;placed.push({x:bx,y,w:tw});ctx.textAlign=anchor;ctx.fillStyle=palette.ink;ctx.strokeStyle=palette.background;ctx.lineWidth=4;ctx.strokeText(name,x,y);ctx.fillText(name,x,y);ctx.font='10px Inter';ctx.fillStyle=palette.muted;ctx.strokeText(fmt(n.count)+' assemblies',x,y+15);ctx.fillText(fmt(n.count)+' assemblies',x,y+15);}
drawAtlasIcons();
if(sweep.running)cacheSweepTree();
drawInteraction();
const away=zoom!==1||Math.abs(pan.x)>1||Math.abs(pan.y)>1;$('#home').hidden=!away;$('.stage-wrap').dataset.zoomed=String(away);
$('#zoom-readout').textContent=zoom.toFixed(1)+'×';$('#zoomout').disabled=zoom<=1;$('#zoomin').disabled=zoom>=320;
window.POC&&(window.POC.zoom=zoom);
}
function zoomAt(factor,x=W/2,y=H/2){stopSweep();$('#layer-tooltip').hidden=true;hoverBin=null;let old=zoom,n=Math.max(1,Math.min(320,zoom*factor));let ratio=n/old;pan.x=(pan.x+W/2-x)*ratio+x-W/2;pan.y=(pan.y+H/2-y)*ratio+y-H/2;zoom=n;if(zoom===1)pan={x:0,y:0};schedule()}
function reset(){stopSweep(false);$('#layer-tooltip').hidden=true;hoverBin=null;zoom=1;pan={x:0,y:0};clearSelection();schedule()}
// Select the strand in the pointer's direction anywhere inside the root opening.
// A small neutral hub avoids unstable angles right at the pivot.
function hitDial(x,y,dragging=false){
const [wx,wy]=world(x,y),r=Math.hypot(wx,wy);
if((!dragging&&r>92)||r*baseScale*zoom<3)return null;
const angle=((Math.atan2(wy,wx)-START)%TAU+TAU)%TAU;
if(angle>SPAN)return angle-SPAN<TAU-angle?leaves.at(-1):leaves[0];
return leaves[Math.min(leaves.length-1,Math.floor(angle/SPAN*leaves.length))];
}
function insideTree(x,y){return Math.hypot(...world(x,y))<=R+(locusMode==='off'?0:BAR_GAP+BAR_HEIGHT)+8}
function hitAssembly(x,y,radius=7){return hitDial(x,y)||nearest(x,y,radius)}
function nearest(x,y,radius=7){
const [wx,wy]=world(x,y),pad=radius/(baseScale*zoom);let best=pad*pad,found=null;
for(let k=0;k<hitData.length;k+=8){let ax=hitData[k],ay=hitData[k+1],bx=hitData[k+4],by=hitData[k+5];
if(wx<Math.min(ax,bx)-pad||wx>Math.max(ax,bx)+pad||wy<Math.min(ay,by)-pad||wy>Math.max(ay,by)+pad)continue;
let dx=bx-ax,dy=by-ay,l=dx*dx+dy*dy,t=l?Math.max(0,Math.min(1,((wx-ax)*dx+(wy-ay)*dy)/l)):0;
let d=(wx-ax-t*dx)**2+(wy-ay-t*dy)**2;
if(d<best){best=d;found=leaves[hitOwner[k/8]];}
}return found;
}
function closeViewOptions(){if($('#view-dialog').open)$('#view-dialog').close()}
function chooseAssembly(a){closeViewOptions();stopSweep(false);selectAssembly(a)}
function focusAssembly(a,z=70){closeViewOptions();stopSweep(false);selected=a;zoom=z;let s=baseScale*zoom;pan={x:-a.tip[0]*s,y:-a.tip[1]*s};selectAssembly(a);schedule()}
function clearSelection(){selected=pinnedAssembly=hoveredAssembly=null;$('#selection').hidden=true;scheduleInteraction()}
function hoverAssembly(a){
if(sweep.running||pinnedAssembly)return;
if(a===hoveredAssembly)return;
hoveredAssembly=a;
if(a)selectAssembly(a,false);else clearSelection();
}
function selectAssembly(a,pin=true,redraw=true){if(!a)return;if(pin){pinnedAssembly=a;hoveredAssembly=null}selected=a;
$('#selection').dataset.pinned=String(a===pinnedAssembly);
$('#sel-state').textContent=sweep.running?'Scanning assemblies':pinnedAssembly?'Selected · click anywhere to clear':'Preview · click strand to keep';let r=a.row,species=a.lineage.find(n=>n.rank==='species');$('#selection').hidden=false;$('#sel-name').textContent=a.name;$('#sel-id').textContent=a.accession;$('#sel-id').href='https://www.ncbi.nlm.nih.gov/datasets/genome/'+a.accession+'/';$('#sel-lineage').textContent=a.lineage.filter(n=>['kingdom','phylum','class','order','family','genus'].includes(n.rank)).map(n=>n.common||n.name).join(' › ');
const has=mode?r[5]:r[4]===1;
$('#sel-status').innerHTML=mode?`<i class="dot ${has?'blue':'green'}"></i>${has?'In the RefSeq comparison corpus':'Outside the RefSeq comparison corpus'}`:r[4]===1?`<i class="dot blue"></i>GenBank annotation record<br><small>${esc(r[7])} · ${esc(r[9])}</small>`:'<i class="dot green"></i>No annotation listed in this GenBank snapshot';
$('#sel-data').innerHTML=(species?`<span>${esc(species.common||species.name)} · assembly comparison</span><b>${fmt(mode?species.refMatches:species.existing)} / ${fmt(species.count)} ${mode?'in RefSeq corpus':'with GenBank records'}</b>`:'')+`<span>Predicted coding loci</span><b>${a.loci===null?'Count unavailable':fmt(a.loci)}</b>`+(r[3]?`<span>Assembly length</span><b>${(r[3]/1e6).toLocaleString('en-US',{maximumFractionDigits:2})} Mb</b>`:'')+(r[6]?`<span>RefSeq counterpart</span><b>${esc(r[6])}</b>`:'')+(mode&&r[4]===1?`<span>GenBank annotation provider</span><b>${esc(r[7])}</b>`:'');
updateDataAccess(a);$('#sel-loci').hidden=a.accession!=='GCA_000001405.29';if(redraw)scheduleInteraction()}
function updateLegend(){let n=mode?D.meta.refseq_matches:D.meta.classification_counts['1'];$('#blue-count').textContent=fmt(n);$('#green-count').textContent=fmt(leaves.length-n);$('#blue-label').textContent=mode?'In RefSeq comparison corpus':'GenBank annotation record';$('#green-label').textContent=mode?'Outside that comparison corpus':'No GenBank record listed';$('#scope-note').textContent=mode?'Counts are assemblies, not genes. Blue marks exact paired-accession matches to the project’s 2,055-assembly RefSeq corpus, not all RefSeq annotations. Green does not imply new genes.':'Counts are assemblies, not genes. This view checks GenBank submission metadata; it does not include annotations stored only on paired RefSeq records. Missing metadata is not proof of no prior annotation.';$('#strand-green-label').textContent=mode?'Outside RefSeq comparison corpus':'No GenBank annotation listed';$('#strand-blue-label').textContent=mode?'In RefSeq comparison corpus':'GenBank annotation listed';$('.strand-legend').title=$('#scope-note').textContent;if(selected&&!sweep.running)selectAssembly(selected,false);schedule()}
function search(q){q=q.trim().toLowerCase();let out=[];if(q.length>1){out=leaves.filter(a=>a.accession.toLowerCase().includes(q)||a.name.toLowerCase().includes(q)||(nodes.get(a.taxid).common||'').toLowerCase().includes(q)).slice(0,7);}$('#results').replaceChildren(...out.map(a=>{let b=document.createElement('button');b.innerHTML=`<strong>${esc(a.name)}</strong><span>${esc(a.accession)}</span>`;b.onclick=()=>{chooseAssembly(a);$('#results').replaceChildren();$('#search').value=''};return b}));if(q.length>1&&!out.length)$('#results').textContent='No match in this PoC subset.';}
function geneModal(){closeViewOptions();stopSweep();const g=D.example;$('#gene-dialog').showModal();$('#gene-summary').textContent=fmt(g.gene_count)+' candidate coding loci in this assembly';const svg=$('#gene-svg');let start=0,end=1000000,width=1100,left=30,right=1080;let x=p=>left+(p-start)/(end-start)*(right-left);let parts=[];for(let p=0;p<=end;p+=100000){parts.push(`<line x1="${x(p)}" y1="38" x2="${x(p)}" y2="198" stroke="${palette.grid}"/><text x="${x(p)}" y="24" text-anchor="middle">${p/1e6} Mb</text>`)}
for(let gene of g.genes){if(gene.start>end)continue;let y=gene.strand==='+'?92:158;parts.push(`<g><title>${esc(gene.id)} · ${fmt(gene.start)}–${fmt(gene.end)} bp · ${gene.strand}</title><line x1="${x(gene.start)}" x2="${x(Math.min(end,gene.end))}" y1="${y}" y2="${y}" stroke="${palette.bar}"/>`);for(let [s,e] of gene.cds)if(s<=end)parts.push(`<rect x="${x(s)}" y="${y-7}" width="${Math.max(.6,x(Math.min(end,e))-x(s))}" height="14" fill="${palette.green}"/>`);parts.push('</g>')};svg.innerHTML=parts.join('');$('#gene-count-window').textContent=g.genes.filter(g=>g.start<=end).length+' candidate loci in this 1 Mb window. Bars show predicted coding segments; connecting lines span intervening sequence.';
}
async function init(){try{const raw=Uint8Array.from(atob($('#dataset').textContent.trim()),c=>c.charCodeAt(0));D=JSON.parse(await new Response(new Blob([raw]).stream().pipeThrough(new DecompressionStream('gzip'))).text());prepare();await Promise.all([document.fonts.ready,loadAtlasIcons()]);setupGL();prepareSweep();setTheme(theme);makeLocusBins();resize();updateLegend();$('#loading').remove();$('#shown').textContent=fmt(leaves.length);$('#taxa-shown').textContent=fmt(D.meta.taxa);$('#search').disabled=false;
new ResizeObserver(resize).observe(stage);
stage.addEventListener('wheel',e=>{e.preventDefault();let r=canvas.getBoundingClientRect();zoomAt(Math.exp(-e.deltaY*.0017),e.clientX-r.left,e.clientY-r.top)},{passive:false});
let down=null,moved=false,suppressChartClick=false,clickPoint=null,hoverPending=false,hoverEvent=null;
const queueHover=e=>{hoverEvent=e;if(hoverPending)return;hoverPending=true;requestAnimationFrame(()=>{hoverPending=false;const event=hoverEvent;hoverEvent=null;if(!down&&event)hoverLayers(event)})};
stage.addEventListener('pointerdown',e=>{
if(e.button!==0)return;
hoverEvent=null;suppressChartClick=false;clickPoint=null;
down={x:e.clientX,y:e.clientY};moved=false;
stage.setPointerCapture(e.pointerId);
});
stage.addEventListener('pointermove',e=>{
if(!down){queueHover(e);return}
const dx=e.clientX-down.x,dy=e.clientY-down.y;
if(!moved&&Math.hypot(dx,dy)<4)return;
if(!moved){stopSweep(false);hoverAssembly(null)}
moved=true;canvas.style.cursor='grabbing';$('#layer-tooltip').hidden=true;
pan.x+=dx;pan.y+=dy;down={x:e.clientX,y:e.clientY};schedule();
});
stage.addEventListener('pointerup',e=>{if(!down)return;clickPoint={x:e.clientX,y:e.clientY};suppressChartClick=moved;down=null;canvas.style.cursor='default'});
stage.addEventListener('pointercancel',()=>{down=null;suppressChartClick=true;canvas.style.cursor='default'});
stage.addEventListener('pointerleave',e=>{hoverEvent=null;$('#layer-tooltip').hidden=true;if(hoverBin){hoverBin=null;schedule()}if(!e.relatedTarget?.closest?.('#selection'))hoverAssembly(null);if(!down)canvas.style.cursor='default'});
$('#selection').addEventListener('pointerleave',()=>hoverAssembly(null));
// Snapshot before target handlers run so actions inside the info box can use
// the selected assembly before this same click clears it.
document.addEventListener('click',e=>{e.hadAssemblySelection=!!pinnedAssembly||sweep.running;e.hadAssemblyPreview=!!hoveredAssembly},true);
document.addEventListener('click',e=>{
if(e.target.closest('#stage')){
if(suppressChartClick){suppressChartClick=false;return}
stopSweep(false);$('#layer-tooltip').hidden=true;
if(e.hadAssemblySelection){clearSelection();return}
const box=stage.getBoundingClientRect(),p=clickPoint||{x:e.clientX,y:e.clientY},x=p.x-box.left,y=p.y-box.top;clickPoint=null;
const a=assemblyAtClick(x,y);if(a)selectAssembly(a);else clearSelection();
}else if(e.hadAssemblySelection||e.hadAssemblyPreview){stopSweep(false);clearSelection()}
});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!document.querySelector('dialog[open]'))clearSelection()});
$('#sel-download').onclick=()=>showDataAccess(selected);$('#data-close').onclick=()=>$('#data-dialog').close();$('#data-copy').onclick=copyDataCode;
$('#theme-toggle').onclick=()=>setTheme(theme==='dark'?'light':'dark');
$('#view-options').onclick=()=>{stopSweep(false);$('#view-dialog').showModal()};$('#view-close').onclick=closeViewOptions;
$('#locus-mode').onchange=e=>{locusMode=e.target.value;makeLocusBins()};$('#show-icons').onchange=e=>{showIcons=e.target.checked;schedule()};$('#credits-open').onclick=()=>$('#credits-dialog').showModal();$('#credits-close').onclick=()=>$('#credits-dialog').close();
$('#home').onclick=reset;
$('#zoomin').onclick=()=>zoomAt(2);$('#zoomout').onclick=()=>zoomAt(.5);$('#reset').onclick=reset;$('#search').addEventListener('input',e=>search(e.target.value));$('#search').addEventListener('keydown',e=>{if(e.key==='Escape')$('#results').replaceChildren();if(e.key==='Enter')$('#results button')?.click()});$('#scope').onchange=e=>{mode=Number(e.target.value);updateLegend()};$('#existing-only').onchange=e=>{view=e.target.checked?'existing':'all';schedule()};$('#human').onclick=()=>chooseAssembly(leaves.find(a=>a.accession==='GCA_000001405.29'));$('#detail').onclick=()=>focusAssembly(leaves.find(a=>a.accession==='GCA_000001405.29'),95);$('#sel-loci').onclick=geneModal;$('#gene-open').onclick=geneModal;$('#gene-close').onclick=()=>$('#gene-dialog').close();$('#sel-close').onclick=()=>{clearSelection();schedule()};$('#sel-focus').onclick=()=>focusAssembly(selected,100);$('#about').onclick=()=>{stopSweep();$('#about-dialog').showModal()};$('#about-close').onclick=()=>$('#about-dialog').close();
const params=new URLSearchParams(location.search);
if(params.get('animate')==='1')setTimeout(startSweep,1000);
}catch(e){if($('#loading'))$('#loading').textContent='Unable to load the tree: '+e.message;console.error(e)}}
init();