Spaces:
Running
Running
File size: 20,948 Bytes
23078ef d688ac7 23078ef 8e683b6 23078ef fb408b6 23078ef d2ea2f5 23078ef d688ac7 23078ef d2ea2f5 23078ef d2ea2f5 23078ef 8e683b6 23078ef d688ac7 fb408b6 23078ef d688ac7 fb408b6 8e683b6 fbc4cf6 23078ef fbc4cf6 8e683b6 cba18a0 fbc4cf6 cba18a0 c5eee7b cba18a0 d2ea2f5 23078ef e5675eb fbc4cf6 e5675eb 8e683b6 c3828f6 d688ac7 c3828f6 23078ef d2ea2f5 fbc4cf6 62181a5 fbc4cf6 e5675eb d688ac7 23078ef 8e683b6 23078ef 8e683b6 fbc4cf6 d688ac7 fbc4cf6 8e683b6 d2ea2f5 fbc4cf6 d2ea2f5 c3828f6 d2ea2f5 fbc4cf6 c3828f6 fbc4cf6 c3828f6 fbc4cf6 c3828f6 d2ea2f5 c5eee7b d688ac7 e5675eb fb408b6 fbc4cf6 e5675eb 23078ef | 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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | 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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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();
|