// Measured annotation counts aligned to the same assembly ordering as the tree. let locusMode='5',locusBins=[],locusMax=1,iconImages=new Map(),themedIcons=new Map(),iconHits=[],hoverBin=null,showIcons=true; const BAR_GAP=14,BAR_HEIGHT=174; function shortCount(n){return n>=1e6?(n/1e6).toLocaleString('en-US',{maximumFractionDigits:2})+'M':n>=1000?(n/1000).toLocaleString('en-US',{maximumFractionDigits:1})+'k':fmt(n)} function niceCeiling(n){if(!n)return 1;let p=10**Math.floor(Math.log10(n)),f=n/p;return (f<=1?1:f<=2?2:f<=5?5:10)*p} function makeLocusBins(){ const size=locusMode==='raw'?1:Number(locusMode)||5;locusBins=[]; for(let lo=0;lob.value)));hoverBin=null; $('#locus-readout').textContent=shortCount(D.loci.summary.sum_available_loci)+' loci · '+fmt(D.loci.summary.available_assemblies)+' assemblies with counts'; $('#locus-explain').textContent=locusMode==='off'?'Locus bars hidden.':locusMode==='raw'?'One bar per assembly. Length = predicted coding loci. Grey ticks mark missing counts.':'Each bar sums available loci across up to '+size+' neighbouring assembly strands. Grey base marks show the fraction with missing counts.'; if(window.POC)Object.assign(POC,{locusBins,locusMax,locusMode,locusTotal:D.loci.summary.sum_available_loci,locusAvailable:D.loci.summary.available_assemblies}); schedule(); } function barHeight(v){return BAR_HEIGHT*v/locusMax} // Tiny Canvas arcs can be tessellated differently at their two radii, creating // dashed slivers. Explicit vertices keep both radial edges continuous. Larger // sectors share angular subdivisions with <0.1 CSS px chord error at any zoom. function wedge(a,b,r0,r1,color){ if(r1<=r0||b<=a)return; const s=baseScale*zoom,steps=Math.max(1,Math.ceil((b-a)/Math.min(.04,Math.sqrt(.8/(r1*s))))); ctx.beginPath(); for(let j=0;j<=steps;j++){const p=screen(polar(r1,a+(b-a)*j/steps));j?ctx.lineTo(...p):ctx.moveTo(...p)} for(let j=steps;j>=0;j--)ctx.lineTo(...screen(polar(r0,a+(b-a)*j/steps))); ctx.closePath();ctx.fillStyle=color;ctx.fill(); } function drawLoci(){ if(locusMode==='off')return; const s=baseScale*zoom,c=screen([0,0]),base=R+BAR_GAP; // A common radial zero and linear scale; missing measurements are distinct marks. if(zoom<3){ctx.strokeStyle=palette.grid;ctx.lineWidth=.7;ctx.setLineDash([2,5]);for(let v of [0,locusMax/2,locusMax]){ctx.beginPath();ctx.arc(c[0],c[1],(base+barHeight(v))*s,START,START+SPAN);ctx.stroke()}ctx.setLineDash([]);} for(const b of locusBins){ let a0=START+SPAN*b.lo/leaves.length,a1=START+SPAN*b.hi/leaves.length,ang=(a0+a1)/2,p=screen(polar(base+barHeight(b.value)/2,ang)),h=(barHeight(b.value)+(a1-a0)*(base+barHeight(b.value)))*s; // Cull only when the entire bar is outside the viewport. if((p[0]<-h-20||p[0]>W+h+20||p[1]<-h-20||p[1]>H+h+20)&&zoom>2)continue; if(locusMode==='raw'){ if(b.known){ctx.strokeStyle=b===hoverBin?palette.barHover:palette.bar;ctx.lineWidth=Math.max(.3,Math.min(3,(a1-a0)*base*s*.7));ctx.globalAlpha=Math.min(1,Math.max(.14,(a1-a0)*base*s));ctx.beginPath();ctx.moveTo(...screen(polar(base,ang)));ctx.lineTo(...screen(polar(base+barHeight(b.value),ang)));ctx.stroke();ctx.globalAlpha=1;if(b.value===0){ctx.fillStyle=palette.bar;let q=screen(polar(base,ang));ctx.fillRect(q[0]-.6,q[1]-.6,1.2,1.2)}} else{ctx.strokeStyle=palette.missing;ctx.lineWidth=Math.max(.35,Math.min(2,(a1-a0)*base*s*.7));ctx.beginPath();ctx.moveTo(...screen(polar(R+4,ang)));ctx.lineTo(...screen(polar(R+4+Math.min(5,3/s),ang)));ctx.stroke()} }else{ let pad=Math.min((a1-a0)*.16,.0007); if(b.known)wedge(a0+pad,a1-pad,base,base+barHeight(b.value),b===hoverBin?palette.barHover:palette.bar); if(b.knownnew Promise(resolve=>{let img=new Image();img.onload=()=>{iconImages.set(uid,img);resolve()};img.onerror=()=>resolve();img.src='data:image/svg+xml;base64,'+btoa(unescape(encodeURIComponent(info.svg)))}))); const rows=items.map(([uid,v])=>`${esc(v.attribution||v.contributor||'PhyloPic contributor')}${esc(v.license.includes('/zero/')?'CC0':v.license.includes('/mark/')?'Public domain':v.license.includes('/by/3.')?'CC BY 3.0':'CC BY 4.0')}`).join(''); $('#icon-credits').innerHTML=''+rows+'
Original image / creditLicence
'; } function themedIcon(uid,size){ const key=theme+':'+uid+':'+size;if(themedIcons.has(key))return themedIcons.get(key); const im=iconImages.get(uid),ratio=im.naturalWidth/im.naturalHeight||1; const iw=ratio>1?size:size*ratio,ih=ratio>1?size/ratio:size,pad=3,dpr=2; const width=iw+pad*2,height=ih+pad*2,sprite=document.createElement('canvas'); sprite.width=Math.ceil(width*dpr);sprite.height=Math.ceil(height*dpr); const m=sprite.getContext('2d');m.setTransform(dpr,0,0,dpr,0,0); // Dilate the original silhouette alpha, then place its fill over the top. // Padding keeps the contour intact, including antennae and narrow limbs. for(const [color,outline] of [[palette.iconOutline,true],[palette.icon,false]]){ const mask=document.createElement('canvas');mask.width=sprite.width;mask.height=sprite.height; const ink=mask.getContext('2d');ink.setTransform(dpr,0,0,dpr,0,0); ink.drawImage(im,pad,pad,iw,ih);ink.globalCompositeOperation='source-in'; ink.fillStyle=color;ink.fillRect(0,0,width,height); if(outline){ const radius=size<18?.4:.55; for(let i=0;i<16;i++){const a=i*Math.PI/8;m.drawImage(mask,Math.cos(a)*radius,Math.sin(a)*radius,mask.width/dpr,mask.height/dpr)} }else m.drawImage(mask,0,0,mask.width/dpr,mask.height/dpr); } const result={image:sprite,width:sprite.width/dpr,height:sprite.height/dpr,iw,ih,pad}; themedIcons.set(key,result);return result; } function drawAtlasIcons(){ iconHits=[];if(window.POC)POC.iconsDrawn=[];if(!showIcons)return; const candidates=[...nodes.values()].filter(n=>n.id!==2759&&n.r&&D.icons.taxa[n.id]&&n.count>=Math.max(2,leaves.length/(zoom*2400))).sort((a,b)=>b.count-a.count); const viewScale=Math.max(.68,Math.min(1,Math.sqrt(baseScale/.4))),max=W<600?34:zoom<1.5?50:64; for(const n of candidates){ const uid=D.icons.taxa[n.id],im=iconImages.get(uid);if(!im)continue; const p=screen(point(n,n.mid)),size=Math.round(Math.max(12,Math.min(32,(11+3.7*Math.log10(n.count)+2*Math.log2(zoom))*viewScale))),x=p[0],y=p[1]; const ratio=im.naturalWidth/im.naturalHeight||D.icons.images[uid].ratio||1,iw=ratio>1?size:size*ratio,ih=ratio>1?size/ratio:size; if(x-iw/2<6||x+iw/2>W-6||y-ih/2<6||y+ih/2>H-6)continue; // Actual silhouette bounds make room for smaller branches without overlap. if(iconHits.some(h=>h.uid===uid||(Math.abs(h.x-x)<(h.w+iw)/2+6&&Math.abs(h.y-y)<(h.h+ih)/2+6)))continue; const sprite=themedIcon(uid,size); ctx.drawImage(sprite.image,x-iw/2-sprite.pad,y-ih/2-sprite.pad,sprite.width,sprite.height); iconHits.push({x,y,w:iw,h:ih,size,r:size/2+3,n,uid}); if(iconHits.length>=max)break; } if(window.POC)POC.iconsDrawn=iconHits.map(h=>h.n.id); } function hitIcon(x,y){return iconHits.find(h=>Math.abs(h.x-x)<=h.w/2+3&&Math.abs(h.y-y)<=h.h/2+3)} function hitBar(x,y){if(locusMode==='off')return null;let p=world(x,y),rad=Math.hypot(...p);if(radR+BAR_GAP+BAR_HEIGHT+8)return null;let angle=Math.atan2(p[1],p[0]);while(angleSTART+SPAN)return null;let i=Math.floor((angle-START)/SPAN*leaves.length),size=locusMode==='raw'?1:Number(locusMode);return locusBins[Math.floor(i/size)]||null} function hoverLayers(e){if(sweep.running)return;if(pinnedAssembly){$('#layer-tooltip').hidden=true;canvas.style.cursor='pointer';return;}if(document.querySelector('dialog[open]'))return;let box=stage.getBoundingClientRect(),x=e.clientX-box.left,y=e.clientY-box.top,dial=hitDial(x,y),ic=dial?null:hitIcon(x,y),bin=dial?null:hitBar(x,y),tip=$('#layer-tooltip'); if(!ic&&!bin){tip.hidden=true;if(hoverBin){hoverBin=null;schedule()}const a=hitAssembly(x,y,3);hoverAssembly(a);canvas.style.cursor=a?'pointer':'default';return} canvas.style.cursor='pointer'; if(!ic&&locusMode==='raw'){tip.hidden=true;if(hoverBin!==bin){hoverBin=bin;schedule()}hoverAssembly(leaves[bin.lo]);return} hoverAssembly(null); if(ic){tip.innerHTML=`${esc(ic.n.common||ic.n.name)}${esc(ic.n.name)} · ${fmt(ic.n.count)} assemblies${fmt(mode?ic.n.refMatches:ic.n.existing)} ${mode?'match the RefSeq comparison corpus':'have GenBank annotation records'}Assembly counts, not gene counts · click to select a strand`} else{const n=bin.hi-bin.lo,one=n===1,a=leaves[bin.lo];tip.innerHTML=`${one?esc(a.name):shortCount(bin.value)+' predicted coding loci'}${one?(bin.known?fmt(bin.value)+' predicted coding loci':'Locus count unavailable'):fmt(bin.known)+' of '+fmt(n)+' assemblies have counts'}${one?esc(a.accession):'Sum of available counts · click to select an assembly'}`;if(hoverBin!==bin){hoverBin=bin;schedule()}} tip.hidden=false;const wrap=$('.stage-wrap').getBoundingClientRect();tip.style.left=box.left-wrap.left+Math.max(8,Math.min(W-tip.offsetWidth-8,x+16))+'px';tip.style.top=box.top-wrap.top+Math.max(8,Math.min(H-tip.offsetHeight-8,y+15))+'px'; } // Icons and bar bins resolve to real strands without changing the camera. function assemblyAtClick(x,y){ const dial=hitDial(x,y);if(dial)return dial; const bin=hitBar(x,y); if(bin){const p=world(x,y);let angle=Math.atan2(p[1],p[0]);while(angle