lvwerra HF Staff commited on
Commit
8e683b6
·
verified ·
1 Parent(s): d688ac7

Add a watch hand and a 2.5-second tree sweep with cumulative counts

Browse files
.gitattributes CHANGED
@@ -45,3 +45,5 @@ selection-aligned.png filter=lfs diff=lfs merge=lfs -text
45
  hover-detail.png filter=lfs diff=lfs merge=lfs -text
46
  light-overview.png filter=lfs diff=lfs merge=lfs -text
47
  bars-closeup.png filter=lfs diff=lfs merge=lfs -text
 
 
 
45
  hover-detail.png filter=lfs diff=lfs merge=lfs -text
46
  light-overview.png filter=lfs diff=lfs merge=lfs -text
47
  bars-closeup.png filter=lfs diff=lfs merge=lfs -text
48
+ sweep-complete.png filter=lfs diff=lfs merge=lfs -text
49
+ sweep-detail.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -33,18 +33,24 @@ Grey marks at the bar bases indicate missing counts. For bins, the grey mark spa
33
 
34
  The original **Genome Atlas silhouettes**, without circle backgrounds, sit on their corresponding taxonomic nodes. Clicking an icon focuses the branch. Icons appear progressively with zoom and are hidden when they would overlap; the checkbox hides them all. All original PhyloPic source links and licence/attribution records are retained in the interface under **About the data → Icon credits and licences** and in the source data. The original SVG artwork is preserved; its displayed fill is adapted to each theme with an alpha mask, without backgrounds.
35
 
 
 
 
 
 
 
36
  ## Layout and implementation
37
 
38
  The taxonomy and plain-language names reuse the earlier explorer work. Only ancestor nodes on paths to included assemblies are retained. The drawing simplifies intervening taxonomy to standard ranks; full lineage metadata is preserved. Radius is a layout choice, not evolutionary time or branch length.
39
 
40
  Every assembly has its own lane through parent bundles and its own endpoint. A common taper makes individual paths finer towards the root; all assemblies use the same width at a given radius. Density and bundle width encode assembly counts, not coding-locus counts. At overview scale, many paths occupy the same pixel. Hovering a strand highlights its exact path and opens its assembly details; clicking pins those details while other strands can be previewed on hover. Zoom, pan and search reveal individual accessions. The cursor is neutral over empty space and becomes a pointer over selectable strands; it becomes a drag cursor only during an actual pan. Selection highlights reuse the exact sampled coordinates drawn by WebGL, so they stay aligned with the underlying strand at every zoom level.
41
 
42
- Dark mode is the default, with charcoal surfaces, emerald and blue strands, and pale atlas silhouettes. The sun/moon control switches to the light theme and saves the preference locally. The root opening is deliberately empty. Fine locus bars use shared explicit angular vertices instead of independently approximated Canvas arcs, avoiding gaps in narrow bins without changing their counts.
43
 
44
  The self-contained HTML includes compressed data, JavaScript and two local font families (Manrope and Inter). It makes no network requests to draw the tree. Links to source records open only when selected. WebGL is required. Source and data preparation scripts are included for reuse. The metadata counts include source paths, content hashes and pipeline revisions for auditing. The icon data includes each original source page and licence.
45
 
46
  ## Rebuild
47
 
48
- `python prepare_data.py` reproduces the compact dataset from the copied taxonomy database and cached NCBI summary (paths at the top of the script). `python build.py` builds the self-contained HTML from `template.html`, `app.js`, `layers.js`, `theme.js`, the data files, and the existing release font assets. `python check_hover_theme.py` verifies hover previews, pinned fallback, the empty centre, theme persistence and continuous subpixel bars. `python check_selection.py` verifies neutral/selection/drag cursor states, the human comparison counts, and alignment of the Canvas highlight with the rendered WebGL path. `python check.py` runs the base browser checks; `python check_layers.py` checks bin totals, missing-count handling, raw/binned switching, clickable bars, node icons and attribution links. `python fetch_loci.py` refreshes the available metadata counts and `python prepare_icons.py` prepares the atlas icons.
49
 
50
  Data provenance and limitations are recorded in `provenance.json`. The original release video and explorer were not modified.
 
33
 
34
  The original **Genome Atlas silhouettes**, without circle backgrounds, sit on their corresponding taxonomic nodes. Clicking an icon focuses the branch. Icons appear progressively with zoom and are hidden when they would overlap; the checkbox hides them all. All original PhyloPic source links and licence/attribution records are retained in the interface under **About the data → Icon credits and licences** and in the source data. The original SVG artwork is preserved; its displayed fill is adapted to each theme with an alpha mask, without backgrounds.
35
 
36
+ ## Sweep animation
37
+
38
+ **Animate sweep** returns to the whole tree and makes one clockwise turn in 2.5 seconds. The thin centre hand follows the changing highlighted strand. Species, assembly and coding-locus counters accumulate as it moves. Every crossed assembly is counted, including those passed between animation frames; each species is counted once using its NCBI species-rank ancestor. Missing locus counts remain excluded. The completed subset totals are **16,374 species, 33,588 assemblies and 212,905,534 available coding loci** (across 17,828 assemblies with counts). Species-rank ancestors are deduplicated separately from the 16,976 submitted taxon IDs.
39
+
40
+ Replay starts the counters from zero. The button stops a running sweep; Escape, zooming or direct chart interaction also stop it. Normal hover resumes afterwards. A separate interaction canvas draws the hand and exact strand highlight, leaving the detailed static tree in place throughout playback.
41
+
42
  ## Layout and implementation
43
 
44
  The taxonomy and plain-language names reuse the earlier explorer work. Only ancestor nodes on paths to included assemblies are retained. The drawing simplifies intervening taxonomy to standard ranks; full lineage metadata is preserved. Radius is a layout choice, not evolutionary time or branch length.
45
 
46
  Every assembly has its own lane through parent bundles and its own endpoint. A common taper makes individual paths finer towards the root; all assemblies use the same width at a given radius. Density and bundle width encode assembly counts, not coding-locus counts. At overview scale, many paths occupy the same pixel. Hovering a strand highlights its exact path and opens its assembly details; clicking pins those details while other strands can be previewed on hover. Zoom, pan and search reveal individual accessions. The cursor is neutral over empty space and becomes a pointer over selectable strands; it becomes a drag cursor only during an actual pan. Selection highlights reuse the exact sampled coordinates drawn by WebGL, so they stay aligned with the underlying strand at every zoom level.
47
 
48
+ Dark mode is the default, with charcoal surfaces, emerald and blue strands, and pale atlas silhouettes. The sun/moon control switches to the light theme and saves the preference locally. The root opening contains a fine watch hand that points to the selected strand. Fine locus bars use shared explicit angular vertices instead of independently approximated Canvas arcs, avoiding gaps in narrow bins without changing their counts.
49
 
50
  The self-contained HTML includes compressed data, JavaScript and two local font families (Manrope and Inter). It makes no network requests to draw the tree. Links to source records open only when selected. WebGL is required. Source and data preparation scripts are included for reuse. The metadata counts include source paths, content hashes and pipeline revisions for auditing. The icon data includes each original source page and licence.
51
 
52
  ## Rebuild
53
 
54
+ `python prepare_data.py` reproduces the compact dataset from the copied taxonomy database and cached NCBI summary (paths at the top of the script). `python build.py` builds the self-contained HTML from `template.html`, `app.js`, `layers.js`, `theme.js`, `sweep.js`, the data files, and the existing release font assets. `python check_sweep.py` verifies hand alignment, cumulative counts, replay, cancellation, timing and stationary background rendering. `python check_hover_theme.py` verifies hover previews, pinned fallback, the empty centre, theme persistence and continuous subpixel bars. `python check_selection.py` verifies neutral/selection/drag cursor states, the human comparison counts, and alignment of the Canvas highlight with the rendered WebGL path. `python check.py` runs the base browser checks; `python check_layers.py` checks bin totals, missing-count handling, raw/binned switching, clickable bars, node icons and attribution links. `python fetch_loci.py` refreshes the available metadata counts and `python prepare_icons.py` prepares the atlas icons.
55
 
56
  Data provenance and limitations are recorded in `provenance.json`. The original release video and explorer were not modified.
app.js CHANGED
@@ -3,6 +3,7 @@ let pinnedAssembly=null,hoveredAssembly=null;
3
  let hitData,hitOwner;
4
  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;
5
  const canvas=$('#tree'),overlay=$('#overlay'),ctx=overlay.getContext('2d'),stage=$('#stage');
 
6
  const R=740,TAU=Math.PI*2,START=-Math.PI/2+.04,SPAN=TAU-.08;
7
  const ranks={kingdom:175,phylum:272,class:355,order:437,family:520,genus:598,species:665,subspecies:696};
8
  const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
@@ -51,7 +52,7 @@ function setupGL(){
51
  }
52
  function hash(x){x=((x>>>16)^x)*0x45d9f3b;x=((x>>>16)^x)*0x45d9f3b;return ((x>>>16)^x)>>>0}
53
  function schedule(){if(!pending){pending=true;requestAnimationFrame(()=>{pending=false;draw()})}}
54
- function resize(){const b=stage.getBoundingClientRect();W=b.width;H=b.height;let dpr=Math.min(devicePixelRatio,2);canvas.width=overlay.width=Math.round(W*dpr);canvas.height=overlay.height=Math.round(H*dpr);baseScale=Math.min(W/((R+BAR_HEIGHT)*2+180),H/((R+BAR_HEIGHT)*2+180));schedule()}
55
  function screen(p){let s=baseScale*zoom;return [W/2+pan.x+p[0]*s,H/2+pan.y+p[1]*s]}
56
  function world(x,y){let s=baseScale*zoom;return [(x-W/2-pan.x)/s,(y-H/2-pan.y)/s]}
57
  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);
@@ -59,12 +60,13 @@ function draw(){if(!gl)return;let dpr=canvas.width/W;gl.viewport(0,0,canvas.widt
59
  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);
60
  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);}
61
  drawAtlasIcons();
62
- if(selected){const a=selected;ctx.strokeStyle=palette.highlight;ctx.lineWidth=1.8;ctx.beginPath();ctx.moveTo(...screen([hitData[a.geometryStart],hitData[a.geometryStart+1]]));for(let k=a.geometryStart+4;k<a.geometryEnd;k+=8){ctx.lineTo(...screen([hitData[k],hitData[k+1]]));}ctx.stroke();let q=screen(a.tip);ctx.fillStyle=palette.highlight;ctx.beginPath();ctx.arc(...q,4,0,TAU);ctx.fill();}
 
63
  $('#zoom-readout').textContent=zoom.toFixed(1)+'×';$('#zoomout').disabled=zoom<=1;$('#zoomin').disabled=zoom>=320;
64
  window.POC&&(window.POC.zoom=zoom);
65
  }
66
- function zoomAt(factor,x=W/2,y=H/2){hoverAssembly(null);$('#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()}
67
- function reset(){$('#layer-tooltip').hidden=true;hoverBin=null;zoom=1;pan={x:0,y:0};clearSelection();schedule()}
68
  function nearest(x,y,radius=7){
69
  const [wx,wy]=world(x,y),pad=radius/(baseScale*zoom);let best=pad*pad,found=null;
70
  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];
@@ -74,12 +76,13 @@ function nearest(x,y,radius=7){
74
  if(d<best){best=d;found=leaves[hitOwner[k/8]];}
75
  }return found;
76
  }
77
- function focusAssembly(a,z=70){selected=a;zoom=z;let s=baseScale*zoom;pan={x:-a.tip[0]*s,y:-a.tip[1]*s};selectAssembly(a);schedule()}
78
- function clearSelection(){selected=pinnedAssembly=hoveredAssembly=null;$('#selection').hidden=true;schedule()}
79
  function hoverAssembly(a){
 
80
  hoveredAssembly=a;const next=a||pinnedAssembly;
81
  if(next===selected)return;
82
- if(next)selectAssembly(next,false);else{selected=null;$('#selection').hidden=true;schedule()}
83
  }
84
  function selectAssembly(a,pin=true){if(!a)return;if(pin)pinnedAssembly=a;selected=a;
85
  $('#selection').dataset.pinned=String(a===pinnedAssembly);
@@ -87,19 +90,19 @@ function selectAssembly(a,pin=true){if(!a)return;if(pin)pinnedAssembly=a;selecte
87
  const has=mode?r[5]:r[4]===1;
88
  $('#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';
89
  $('#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>`:'');
90
- $('#sel-loci').hidden=a.accession!=='GCA_000001405.29';schedule()}
91
- 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.';if(selected)selectAssembly(selected,false);schedule()}
92
  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=()=>{focusAssembly(a);$('#results').replaceChildren();$('#search').value=''};return b}));if(q.length>1&&!out.length)$('#results').textContent='No match in this PoC subset.';}
93
- function geneModal(){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>`)}
94
  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.';
95
  }
96
- 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();setTheme(theme);makeLocusBins();resize();updateLegend();$('#loading').remove();$('#shown').textContent=fmt(leaves.length);$('#taxa-shown').textContent=fmt(D.meta.taxa);$('#search').disabled=false;
97
  new ResizeObserver(resize).observe(stage);
98
- canvas.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});
99
  let down=null,moved=false,hoverPending=false,hoverEvent=null;
100
  const queueHover=e=>{hoverEvent=e;if(hoverPending)return;hoverPending=true;requestAnimationFrame(()=>{hoverPending=false;const event=hoverEvent;hoverEvent=null;if(!down&&event)hoverLayers(event)})};
101
- canvas.addEventListener('pointerdown',e=>{hoverEvent=null;down={x:e.clientX,y:e.clientY};moved=false;canvas.setPointerCapture(e.pointerId)});
102
- canvas.addEventListener('pointermove',e=>{
103
  if(!down){queueHover(e);return}
104
  let dx=e.clientX-down.x,dy=e.clientY-down.y;
105
  if(!moved&&Math.hypot(dx,dy)<4)return;
@@ -107,13 +110,13 @@ async function init(){try{const raw=Uint8Array.from(atob($('#dataset').textConte
107
  moved=true;canvas.style.cursor='grabbing';$('#layer-tooltip').hidden=true;
108
  pan.x+=dx;pan.y+=dy;down={x:e.clientX,y:e.clientY};schedule();
109
  });
110
- canvas.addEventListener('pointerup',e=>{if(!down)return;const dragged=moved;if(!dragged){let b=canvas.getBoundingClientRect();if(!clickLayers(e.clientX-b.left,e.clientY-b.top)){const a=nearest(e.clientX-b.left,e.clientY-b.top);if(a)selectAssembly(a);else clearSelection()}}down=null;canvas.style.cursor='default';if(!dragged&&e.pointerType!=='touch')hoverLayers(e)});
111
- canvas.addEventListener('pointercancel',()=>{down=null;hoverAssembly(null);canvas.style.cursor='default'});
112
- canvas.addEventListener('pointerleave',e=>{hoverEvent=null;$('#layer-tooltip').hidden=true;hoverBin=null;if(!down){canvas.style.cursor='default';if(!e.relatedTarget?.closest?.('#selection'))hoverAssembly(null)}schedule()});
113
  $('#selection').addEventListener('pointerleave',e=>{if(e.relatedTarget!==canvas)hoverAssembly(null)});
114
  $('#theme-toggle').onclick=()=>setTheme(theme==='dark'?'light':'dark');
115
  $('#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();
116
- canvas.addEventListener('dblclick',e=>{let b=canvas.getBoundingClientRect();zoomAt(3,e.clientX-b.left,e.clientY-b.top)});
117
- $('#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=()=>focusAssembly(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=()=>$('#about-dialog').showModal();$('#about-close').onclick=()=>$('#about-dialog').close();
118
  }catch(e){$('#loading').textContent='Unable to load the tree: '+e.message;console.error(e)}}
119
  init();
 
3
  let hitData,hitOwner;
4
  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;
5
  const canvas=$('#tree'),overlay=$('#overlay'),ctx=overlay.getContext('2d'),stage=$('#stage');
6
+ const interaction=$('#interaction'),fx=interaction.getContext('2d',{willReadFrequently:true});
7
  const R=740,TAU=Math.PI*2,START=-Math.PI/2+.04,SPAN=TAU-.08;
8
  const ranks={kingdom:175,phylum:272,class:355,order:437,family:520,genus:598,species:665,subspecies:696};
9
  const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
 
52
  }
53
  function hash(x){x=((x>>>16)^x)*0x45d9f3b;x=((x>>>16)^x)*0x45d9f3b;return ((x>>>16)^x)>>>0}
54
  function schedule(){if(!pending){pending=true;requestAnimationFrame(()=>{pending=false;draw()})}}
55
+ 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()}
56
  function screen(p){let s=baseScale*zoom;return [W/2+pan.x+p[0]*s,H/2+pan.y+p[1]*s]}
57
  function world(x,y){let s=baseScale*zoom;return [(x-W/2-pan.x)/s,(y-H/2-pan.y)/s]}
58
  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);
 
60
  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);
61
  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);}
62
  drawAtlasIcons();
63
+ if(sweep.running)cacheSweepTree();
64
+ drawInteraction();
65
  $('#zoom-readout').textContent=zoom.toFixed(1)+'×';$('#zoomout').disabled=zoom<=1;$('#zoomin').disabled=zoom>=320;
66
  window.POC&&(window.POC.zoom=zoom);
67
  }
68
+ function zoomAt(factor,x=W/2,y=H/2){stopSweep();hoverAssembly(null);$('#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()}
69
+ function reset(){stopSweep(false);$('#layer-tooltip').hidden=true;hoverBin=null;zoom=1;pan={x:0,y:0};clearSelection();schedule()}
70
  function nearest(x,y,radius=7){
71
  const [wx,wy]=world(x,y),pad=radius/(baseScale*zoom);let best=pad*pad,found=null;
72
  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];
 
76
  if(d<best){best=d;found=leaves[hitOwner[k/8]];}
77
  }return found;
78
  }
79
+ function focusAssembly(a,z=70){stopSweep(false);selected=a;zoom=z;let s=baseScale*zoom;pan={x:-a.tip[0]*s,y:-a.tip[1]*s};selectAssembly(a);schedule()}
80
+ function clearSelection(){selected=pinnedAssembly=hoveredAssembly=null;$('#selection').hidden=true;scheduleInteraction()}
81
  function hoverAssembly(a){
82
+ if(sweep.running)return;
83
  hoveredAssembly=a;const next=a||pinnedAssembly;
84
  if(next===selected)return;
85
+ if(next)selectAssembly(next,false);else{selected=null;$('#selection').hidden=true;scheduleInteraction()}
86
  }
87
  function selectAssembly(a,pin=true){if(!a)return;if(pin)pinnedAssembly=a;selected=a;
88
  $('#selection').dataset.pinned=String(a===pinnedAssembly);
 
90
  const has=mode?r[5]:r[4]===1;
91
  $('#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';
92
  $('#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>`:'');
93
+ $('#sel-loci').hidden=a.accession!=='GCA_000001405.29';scheduleInteraction()}
94
+ 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.';if(selected&&!sweep.running)selectAssembly(selected,false);schedule()}
95
  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=()=>{focusAssembly(a);$('#results').replaceChildren();$('#search').value=''};return b}));if(q.length>1&&!out.length)$('#results').textContent='No match in this PoC subset.';}
96
+ function geneModal(){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>`)}
97
  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.';
98
  }
99
+ 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;
100
  new ResizeObserver(resize).observe(stage);
101
+ 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});
102
  let down=null,moved=false,hoverPending=false,hoverEvent=null;
103
  const queueHover=e=>{hoverEvent=e;if(hoverPending)return;hoverPending=true;requestAnimationFrame(()=>{hoverPending=false;const event=hoverEvent;hoverEvent=null;if(!down&&event)hoverLayers(event)})};
104
+ stage.addEventListener('pointerdown',e=>{stopSweep(false);hoverEvent=null;down={x:e.clientX,y:e.clientY};moved=false;stage.setPointerCapture(e.pointerId)});
105
+ stage.addEventListener('pointermove',e=>{
106
  if(!down){queueHover(e);return}
107
  let dx=e.clientX-down.x,dy=e.clientY-down.y;
108
  if(!moved&&Math.hypot(dx,dy)<4)return;
 
110
  moved=true;canvas.style.cursor='grabbing';$('#layer-tooltip').hidden=true;
111
  pan.x+=dx;pan.y+=dy;down={x:e.clientX,y:e.clientY};schedule();
112
  });
113
+ stage.addEventListener('pointerup',e=>{if(!down)return;const dragged=moved;if(!dragged){let b=canvas.getBoundingClientRect();if(!clickLayers(e.clientX-b.left,e.clientY-b.top)){const a=nearest(e.clientX-b.left,e.clientY-b.top);if(a)selectAssembly(a);else clearSelection()}}down=null;canvas.style.cursor='default';if(!dragged&&e.pointerType!=='touch')hoverLayers(e)});
114
+ stage.addEventListener('pointercancel',()=>{down=null;hoverAssembly(null);canvas.style.cursor='default'});
115
+ stage.addEventListener('pointerleave',e=>{hoverEvent=null;$('#layer-tooltip').hidden=true;hoverBin=null;if(!down){canvas.style.cursor='default';if(!e.relatedTarget?.closest?.('#selection'))hoverAssembly(null)}schedule()});
116
  $('#selection').addEventListener('pointerleave',e=>{if(e.relatedTarget!==canvas)hoverAssembly(null)});
117
  $('#theme-toggle').onclick=()=>setTheme(theme==='dark'?'light':'dark');
118
  $('#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();
119
+ stage.addEventListener('dblclick',e=>{let b=canvas.getBoundingClientRect();zoomAt(3,e.clientX-b.left,e.clientY-b.top)});
120
+ $('#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=()=>focusAssembly(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();
121
  }catch(e){$('#loading').textContent='Unable to load the tree: '+e.message;console.error(e)}}
122
  init();
bars-closeup.png CHANGED

Git LFS Details

  • SHA256: 2e0b324c083419b03ce76b2f8cfd7ebf11e324339a8667706461b02f33b6f857
  • Pointer size: 132 Bytes
  • Size of remote file: 2.24 MB

Git LFS Details

  • SHA256: 9f34b6ef4311b2df9a28a1696a07f5cdf99b99fb701a05ecc5057272017f646a
  • Pointer size: 132 Bytes
  • Size of remote file: 2.06 MB
build.py CHANGED
@@ -19,7 +19,7 @@ D['loci']={'counts':{a:v['loci'] for a,v in counts['records'].items()},'summary'
19
  D['icons']=json.loads((ROOT/'data/atlas-icons.json').read_text())
20
  css=''.join(f"@font-face{{font-family:{n};src:url(data:font/ttf;base64,{base64.b64encode((ROOT.parent/'assets'/f'{n}.ttf').read_bytes()).decode()}) format('truetype');font-weight:100 900;font-display:swap}}" for n in ['Inter','Manrope'])
21
  payload=base64.b64encode(gzip.compress(json.dumps(D,separators=(',',':'),ensure_ascii=False).encode(),mtime=0)).decode()
22
- s=(ROOT/'template.html').read_text().replace('__FONTS__',css).replace('__DATA__',payload).replace('__SCRIPT__',(ROOT/'theme.js').read_text()+'\n'+(ROOT/'layers.js').read_text()+'\n'+(ROOT/'app.js').read_text())
23
  (ROOT/'index.html').write_text(s)
24
  (ROOT/'data/example-genes.json').write_text(json.dumps(D['example'],indent=2))
25
  print('html',len(s),'real genes',count,'window loci',len(genes))
 
19
  D['icons']=json.loads((ROOT/'data/atlas-icons.json').read_text())
20
  css=''.join(f"@font-face{{font-family:{n};src:url(data:font/ttf;base64,{base64.b64encode((ROOT.parent/'assets'/f'{n}.ttf').read_bytes()).decode()}) format('truetype');font-weight:100 900;font-display:swap}}" for n in ['Inter','Manrope'])
21
  payload=base64.b64encode(gzip.compress(json.dumps(D,separators=(',',':'),ensure_ascii=False).encode(),mtime=0)).decode()
22
+ s=(ROOT/'template.html').read_text().replace('__FONTS__',css).replace('__DATA__',payload).replace('__SCRIPT__',(ROOT/'theme.js').read_text()+'\n'+(ROOT/'layers.js').read_text()+'\n'+(ROOT/'sweep.js').read_text()+'\n'+(ROOT/'app.js').read_text())
23
  (ROOT/'index.html').write_text(s)
24
  (ROOT/'data/example-genes.json').write_text(json.dumps(D['example'],indent=2))
25
  print('html',len(s),'real genes',count,'window loci',len(genes))
carbon-a-strand-poc.png CHANGED

Git LFS Details

  • SHA256: 870ca83dbde93553cebad665abe8e0307e1618e01a8d88f49e8742205a495edd
  • Pointer size: 132 Bytes
  • Size of remote file: 3.09 MB

Git LFS Details

  • SHA256: 21735fc52668014bd7fe05d8d3af0f653a63edb1ac6724f7b3f7f75492ada8fc
  • Pointer size: 132 Bytes
  • Size of remote file: 3.09 MB
carbon-a-strand-source.zip CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:85818171165edce23f46278ae13b9b44dc69407f32c7ce8fcef84870e235ec84
3
- size 15052708
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64a0348410353e921b1074d036ce96bc4f122c0e45ae52de31461bb480888dee
3
+ size 15058217
check_selection.py CHANGED
@@ -14,7 +14,7 @@ with sync_playwright() as p:
14
  page.mouse.down();assert page.locator('#tree').evaluate('e=>getComputedStyle(e).cursor')!='grabbing';page.mouse.up();assert page.locator('#sel-id').inner_text()=='GCA_000001405.29'
15
  assert '4 / 1,412' in page.locator('#sel-data').inner_text();page.locator('#scope').select_option('1');assert '2 / 1,412' in page.locator('#sel-data').inner_text()
16
  # Inspect the path actually sent to Canvas, compared with the GPU's sampled line.
17
- result=page.evaluate('''()=>{let path=[],observed=null;const oldBegin=ctx.beginPath,oldMove=ctx.moveTo,oldLine=ctx.lineTo,oldStroke=ctx.stroke;ctx.beginPath=function(){path=[];return oldBegin.call(this)};ctx.moveTo=function(x,y){path.push([x,y]);return oldMove.call(this,x,y)};ctx.lineTo=function(x,y){path.push([x,y]);return oldLine.call(this,x,y)};ctx.stroke=function(){if(this.strokeStyle===palette.highlight)observed=path.slice();return oldStroke.call(this)};draw();ctx.beginPath=oldBegin;ctx.moveTo=oldMove;ctx.lineTo=oldLine;ctx.stroke=oldStroke;let a=selected,expected=[screen([hitData[a.geometryStart],hitData[a.geometryStart+1]])];for(let k=a.geometryStart+4;k<a.geometryEnd;k+=8)expected.push(screen([hitData[k],hitData[k+1]]));return {count:observed.length,expected:expected.length,error:Math.max(...observed.map((v,i)=>Math.hypot(v[0]-expected[i][0],v[1]-expected[i][1])))}}''')
18
  assert result['count']==result['expected'] and result['error']<.001,result
19
  print('highlight agreement',result,flush=True)
20
  page.screenshot(path=str(R/'selection-aligned.png'))
 
14
  page.mouse.down();assert page.locator('#tree').evaluate('e=>getComputedStyle(e).cursor')!='grabbing';page.mouse.up();assert page.locator('#sel-id').inner_text()=='GCA_000001405.29'
15
  assert '4 / 1,412' in page.locator('#sel-data').inner_text();page.locator('#scope').select_option('1');assert '2 / 1,412' in page.locator('#sel-data').inner_text()
16
  # Inspect the path actually sent to Canvas, compared with the GPU's sampled line.
17
+ result=page.evaluate('''()=>{let path=[],observed=null;const oldBegin=fx.beginPath,oldMove=fx.moveTo,oldLine=fx.lineTo,oldStroke=fx.stroke;fx.beginPath=function(){path=[];return oldBegin.call(this)};fx.moveTo=function(x,y){path.push([x,y]);return oldMove.call(this,x,y)};fx.lineTo=function(x,y){path.push([x,y]);return oldLine.call(this,x,y)};fx.stroke=function(){if(this.strokeStyle===palette.highlight&&this.lineWidth>1.7)observed=path.slice();return oldStroke.call(this)};draw();fx.beginPath=oldBegin;fx.moveTo=oldMove;fx.lineTo=oldLine;fx.stroke=oldStroke;let a=selected,expected=[screen([hitData[a.geometryStart],hitData[a.geometryStart+1]])];for(let k=a.geometryStart+4;k<a.geometryEnd;k+=8)expected.push(screen([hitData[k],hitData[k+1]]));return {count:observed.length,expected:expected.length,error:Math.max(...observed.map((v,i)=>Math.hypot(v[0]-expected[i][0],v[1]-expected[i][1])))}}''')
18
  assert result['count']==result['expected'] and result['error']<.001,result
19
  print('highlight agreement',result,flush=True)
20
  page.screenshot(path=str(R/'selection-aligned.png'))
check_sweep.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys,json,time
3
+ sys.path.insert(0,'/home/node/local/carbon-a-render/python')
4
+ from playwright.sync_api import sync_playwright
5
+ R=Path(__file__).parent
6
+ with sync_playwright() as p:
7
+ b=p.chromium.launch(executable_path='/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell',args=['--no-sandbox','--enable-webgl','--use-gl=angle','--use-angle=swiftshader','--enable-unsafe-swiftshader'])
8
+ page=b.new_page(viewport={'width':1500,'height':1050});errors=[];page.on('pageerror',lambda e:errors.append(str(e)))
9
+ page.goto((R/'index.html').as_uri());page.wait_for_function('window.POC && !document.querySelector("#loading")')
10
+ assert page.evaluate('POC.sweepTotals')=={'species':16374,'assemblies':33588,'loci':212905534,'known':17828}
11
+ # The hand is thinner than the highlighted path and touches its exact root.
12
+ hand=page.evaluate('''()=>{const a=leaves[1200];selectAssembly(a);let paths=[],path=[];const ob=fx.beginPath,om=fx.moveTo,ol=fx.lineTo,os=fx.stroke;fx.beginPath=function(){path=[];return ob.call(this)};fx.moveTo=function(x,y){path.push([x,y]);return om.call(this,x,y)};fx.lineTo=function(x,y){path.push([x,y]);return ol.call(this,x,y)};fx.stroke=function(){paths.push({width:this.lineWidth,path:path.slice()});return os.call(this)};drawInteraction();fx.beginPath=ob;fx.moveTo=om;fx.lineTo=ol;fx.stroke=os;const hand=paths[0],strand=paths[1];return {handWidth:hand.width,strandWidth:strand.width,gap:Math.hypot(hand.path[1][0]-strand.path[0][0],hand.path[1][1]-strand.path[0][1])}}''')
13
+ assert hand['handWidth']<hand['strandWidth'] and hand['gap']<.001,hand
14
+ # Count all crossings, including many strands between rendered frames.
15
+ page.evaluate('''()=>{window.sweepFrames=[];window.treeRedraws=0;const old=gl.drawArrays.bind(gl);gl.drawArrays=(...args)=>{treeRedraws++;old(...args)};const original=updateSweep;updateSweep=function(t){original(t);sweepFrames.push({t:performance.now(),p:sweep.progress,count:sweep.count,species:sweep.species,loci:sweep.loci,angle:sweep.angle,id:selected?.i??null})}}''')
16
+ page.locator('#animate-sweep').click();page.wait_for_function('sweep.progress>.25 && sweep.running')
17
+ # Hover does not steal the animated selection.
18
+ box=page.locator('#stage').bounding_box();page.mouse.move(box['x']+box['width']/2,box['y']+box['height']/2)
19
+ page.wait_for_function('sweep.status==="complete"',timeout=10000)
20
+ result=page.evaluate('''()=>({frames:sweepFrames,redraws:treeRedraws,totals:{species:sweep.species,count:sweep.count,loci:sweep.loci,known:sweep.known},angle:sweep.angle,selected,zoom})''')
21
+ f=result['frames'];assert all(a['count']<=z['count'] and a['species']<=z['species'] and a['loci']<=z['loci'] for a,z in zip(f,f[1:]))
22
+ assert result['totals']=={'species':16374,'count':33588,'loci':212905534,'known':17828}
23
+ assert abs(result['angle']-4.71238898038469)<1e-9 and result['zoom']==1
24
+ duration=f[-1]['t']-f[1]['t'];assert 2400<=duration<3000,duration
25
+ assert len(f)>35,len(f)
26
+ assert result['redraws']<6,result['redraws']
27
+ assert any(z['count']-a['count']>1 for a,z in zip(f,f[1:]))
28
+ for row in f:
29
+ if row['id'] is not None:assert row['id']==row['count']-1
30
+ assert page.locator('#seen-species').inner_text()=='16,374'
31
+ assert page.locator('#seen-assemblies').inner_text()=='33,588'
32
+ assert page.locator('#seen-loci').inner_text()=='212,905,534'
33
+ page.screenshot(path=str(R/'sweep-complete.png'))
34
+ # Replay resets counts; stopping holds exact partial totals and a selection.
35
+ page.locator('#animate-sweep').click();page.wait_for_function('sweep.count>100 && sweep.count<33588')
36
+ page.locator('#animate-sweep').click();assert page.evaluate('sweep.status')=='stopped';count=page.evaluate('sweep.count');assert count<33588
37
+ page.wait_for_timeout(200);assert page.evaluate('sweep.count')==count
38
+ assert page.locator('#selection').is_visible()
39
+ page.screenshot(path=str(R/'sweep-detail.png'))
40
+ # Starting from a detail view restores the whole tree; Escape can stop playback.
41
+ page.locator('#detail').click();assert page.evaluate('zoom')>1
42
+ page.locator('#animate-sweep').click();assert page.evaluate('zoom')==1
43
+ page.keyboard.press('Escape');assert not page.evaluate('sweep.running')
44
+ page.locator('#theme-toggle').click();page.locator('#animate-sweep').click();page.wait_for_function('sweep.progress>.1');page.locator('#scope').select_option('1');assert page.locator('#selection').is_hidden();page.locator('#reset').click();assert not page.evaluate('sweep.running')
45
+ assert not errors,errors
46
+ print(json.dumps({'hand':hand,'duration_ms':round(duration),'frames':len(f),'background_redraws':result['redraws'],'totals':result['totals'],'errors':errors}),flush=True)
47
+ b.close()
detail.png CHANGED

Git LFS Details

  • SHA256: 9a9651309da0761500560c7558570ad7ab8f6d6853fa8031ff4ea3bdb7e6295b
  • Pointer size: 131 Bytes
  • Size of remote file: 164 kB

Git LFS Details

  • SHA256: e96ce731cd47191b0844f29294101e45b3d4483363cf680b1d02f125fa7c37d8
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
export_still.py CHANGED
@@ -7,7 +7,7 @@ with sync_playwright() as p:
7
  b=p.chromium.launch(executable_path='/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell',headless=True,args=['--no-sandbox','--enable-webgl','--use-gl=angle','--use-angle=swiftshader','--enable-unsafe-swiftshader'])
8
  page=b.new_page(viewport={'width':1200,'height':1420},device_scale_factor=2)
9
  page.goto((R/'index.html').as_uri());page.wait_for_function('window.POC && !document.querySelector("#loading")')
10
- page.add_style_tag(content='''header{height:90px;padding:0 50px}.brand{font-size:29px}.header-right button{display:none!important}main{display:block;height:1330px;min-height:1330px}aside,.topbar,.controls,.help,#selection{display:none!important}.stage-wrap{height:100%}#stage{inset:132px 22px 120px}.poster-heading{position:absolute;left:50px;top:35px}.poster-heading h1{font-size:39px;letter-spacing:-1.5px;margin-bottom:11px}.poster-heading p{font-size:15px;color:var(--muted);margin:0}.poster-footer{position:absolute;bottom:20px;left:50px;right:50px;border-top:1px solid var(--rule);padding-top:22px}.poster-key{display:flex;justify-content:space-between;font-size:16px;font-weight:500}.poster-key span{display:flex;align-items:center;gap:12px}.poster-key i{width:34px;height:2px;background:var(--green);display:inline-block}.poster-key i.blue{background:var(--blue)}.poster-footer p{font-size:11px;line-height:1.7;color:var(--muted);margin-top:18px}''')
11
  page.evaluate('''() => {document.querySelector('.stage-wrap').insertAdjacentHTML('beforeend',`<div class="poster-heading"><h1>One assembly. One strand.</h1><p>33,588 assemblies · outer bars sum predicted coding loci across 5 adjacent strands.</p></div><div class="poster-footer"><div class="poster-key"><span><i></i>29,350 with no GenBank annotation record</span><span><i class="blue"></i>4,238 with GenBank annotation records</span></div><p>Proof of concept: 33,588 of the release’s 48,167 assemblies. Colours reflect GenBank annotation records in the 7 October 2026 snapshot.<br>Outer bars sum available loci across up to 5 assembly strands; grey base marks show missing counts. Branch length is not evolutionary time.<br>Genome Atlas silhouettes: PhyloPic. Original image credits and licences are included in the interactive preview.</p></div>`)}''')
12
  page.evaluate("document.querySelector('.poster-heading p').textContent='33,588 assemblies · '+shortCount(POC.locusTotal)+' predicted loci across '+fmt(POC.locusAvailable)+' assemblies with counts.'");page.wait_for_timeout(800);page.screenshot(path=str(R/'carbon-a-strand-poc.png'));b.close()
13
  print(R/'carbon-a-strand-poc.png')
 
7
  b=p.chromium.launch(executable_path='/opt/pw-browsers/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell',headless=True,args=['--no-sandbox','--enable-webgl','--use-gl=angle','--use-angle=swiftshader','--enable-unsafe-swiftshader'])
8
  page=b.new_page(viewport={'width':1200,'height':1420},device_scale_factor=2)
9
  page.goto((R/'index.html').as_uri());page.wait_for_function('window.POC && !document.querySelector("#loading")')
10
+ page.add_style_tag(content='''header{height:90px;padding:0 50px}.brand{font-size:29px}.header-right button{display:none!important}main{display:block;height:1330px;min-height:1330px}aside,.topbar,.controls,.help,#selection,.sweep-meter{display:none!important}.stage-wrap{height:100%}#stage{inset:132px 22px 120px}.poster-heading{position:absolute;left:50px;top:35px}.poster-heading h1{font-size:39px;letter-spacing:-1.5px;margin-bottom:11px}.poster-heading p{font-size:15px;color:var(--muted);margin:0}.poster-footer{position:absolute;bottom:20px;left:50px;right:50px;border-top:1px solid var(--rule);padding-top:22px}.poster-key{display:flex;justify-content:space-between;font-size:16px;font-weight:500}.poster-key span{display:flex;align-items:center;gap:12px}.poster-key i{width:34px;height:2px;background:var(--green);display:inline-block}.poster-key i.blue{background:var(--blue)}.poster-footer p{font-size:11px;line-height:1.7;color:var(--muted);margin-top:18px}''')
11
  page.evaluate('''() => {document.querySelector('.stage-wrap').insertAdjacentHTML('beforeend',`<div class="poster-heading"><h1>One assembly. One strand.</h1><p>33,588 assemblies · outer bars sum predicted coding loci across 5 adjacent strands.</p></div><div class="poster-footer"><div class="poster-key"><span><i></i>29,350 with no GenBank annotation record</span><span><i class="blue"></i>4,238 with GenBank annotation records</span></div><p>Proof of concept: 33,588 of the release’s 48,167 assemblies. Colours reflect GenBank annotation records in the 7 October 2026 snapshot.<br>Outer bars sum available loci across up to 5 assembly strands; grey base marks show missing counts. Branch length is not evolutionary time.<br>Genome Atlas silhouettes: PhyloPic. Original image credits and licences are included in the interactive preview.</p></div>`)}''')
12
  page.evaluate("document.querySelector('.poster-heading p').textContent='33,588 assemblies · '+shortCount(POC.locusTotal)+' predicted loci across '+fmt(POC.locusAvailable)+' assemblies with counts.'");page.wait_for_timeout(800);page.screenshot(path=str(R/'carbon-a-strand-poc.png'));b.close()
13
  print(R/'carbon-a-strand-poc.png')
genes.png CHANGED

Git LFS Details

  • SHA256: 3e0f6708d6900ad0ac901ae45fbbe59706ff1e00d9ff3cbdce1628e8746b7334
  • Pointer size: 131 Bytes
  • Size of remote file: 280 kB

Git LFS Details

  • SHA256: 3cb76386c2db85136f00649157848ae6eb4a601e1aea9a567397ad36a8c4155c
  • Pointer size: 131 Bytes
  • Size of remote file: 280 kB
hover-detail.png CHANGED

Git LFS Details

  • SHA256: fc3d6bce7e1b3dd81ed5cff5cb9d3d42d3d22b19cf3afa1dff9f39b10ff773ac
  • Pointer size: 131 Bytes
  • Size of remote file: 163 kB

Git LFS Details

  • SHA256: 8f7cdb597dcc97239dde26414297fb41901df458d726783f7fcf9b3655e0bc84
  • Pointer size: 131 Bytes
  • Size of remote file: 174 kB
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
layers.js CHANGED
@@ -79,7 +79,7 @@ function drawAtlasIcons(){
79
  }
80
  function hitIcon(x,y){return iconHits.find(h=>Math.hypot(h.x-x,h.y-y)<h.r)}
81
  function hitBar(x,y){if(locusMode==='off')return null;let p=world(x,y),rad=Math.hypot(...p);if(rad<R+2||rad>R+BAR_GAP+BAR_HEIGHT+8)return null;let angle=Math.atan2(p[1],p[0]);while(angle<START)angle+=TAU;if(angle>START+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}
82
- function hoverLayers(e){if(document.querySelector('dialog[open]'))return;let box=stage.getBoundingClientRect(),x=e.clientX-box.left,y=e.clientY-box.top,ic=hitIcon(x,y),bin=hitBar(x,y),tip=$('#layer-tooltip');
83
  if(!ic&&!bin){tip.hidden=true;if(hoverBin){hoverBin=null;schedule()}const a=nearest(x,y,3);canvas.style.cursor=a?'pointer':'default';hoverAssembly(a);return}
84
  canvas.style.cursor='pointer';
85
  hoverAssembly(!ic&&locusMode==='raw'?leaves[bin.lo]:null);
@@ -88,7 +88,7 @@ function hoverLayers(e){if(document.querySelector('dialog[open]'))return;let box
88
  else{const n=bin.hi-bin.lo,one=n===1,a=leaves[bin.lo];tip.innerHTML=`<b>${one?esc(a.name):shortCount(bin.value)+' predicted coding loci'}</b><span>${one?(bin.known?fmt(bin.value)+' predicted coding loci':'Locus count unavailable'):fmt(bin.known)+' of '+fmt(n)+' assemblies have counts'}</span><small>${one?esc(a.accession):'Sum of available counts · click to inspect individual bars'}</small>`;if(hoverBin!==bin){hoverBin=bin;schedule()}}
89
  tip.hidden=false;tip.style.left=Math.max(8,Math.min(W-270,x+16))+14+'px';tip.style.top=Math.max(65,Math.min(H-75,y+15))+64+'px';
90
  }
91
- function focusNode(n){let pts=[point(n,n.mid)];for(let j=0;j<=16;j++)pts.push(polar(R,START+SPAN*(n.lo+(n.hi-n.lo)*j/16)/leaves.length));let xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]),minx=Math.min(...xs),maxx=Math.max(...xs),miny=Math.min(...ys),maxy=Math.max(...ys);zoom=Math.max(1,Math.min(35,Math.min(W/(maxx-minx+140),H/(maxy-miny+140))/baseScale));pan={x:-(minx+maxx)/2*baseScale*zoom,y:-(miny+maxy)/2*baseScale*zoom};clearSelection();$('#layer-tooltip').hidden=true;schedule()}
92
  function clickLayers(x,y){const ic=hitIcon(x,y);if(ic){focusNode(ic.n);return true}const b=hitBar(x,y);if(!b)return false;
93
  if(locusMode==='raw'){selectAssembly(leaves[b.lo]);return true}
94
  const mid=b.mid,p=polar(R+BAR_GAP,START+SPAN*(mid+.5)/leaves.length);locusMode='raw';$('#locus-mode').value='raw';makeLocusBins();zoom=24;pan={x:-p[0]*baseScale*zoom,y:-p[1]*baseScale*zoom};$('#layer-tooltip').hidden=true;schedule();return true;
 
79
  }
80
  function hitIcon(x,y){return iconHits.find(h=>Math.hypot(h.x-x,h.y-y)<h.r)}
81
  function hitBar(x,y){if(locusMode==='off')return null;let p=world(x,y),rad=Math.hypot(...p);if(rad<R+2||rad>R+BAR_GAP+BAR_HEIGHT+8)return null;let angle=Math.atan2(p[1],p[0]);while(angle<START)angle+=TAU;if(angle>START+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}
82
+ function hoverLayers(e){if(sweep.running)return;if(document.querySelector('dialog[open]'))return;let box=stage.getBoundingClientRect(),x=e.clientX-box.left,y=e.clientY-box.top,ic=hitIcon(x,y),bin=hitBar(x,y),tip=$('#layer-tooltip');
83
  if(!ic&&!bin){tip.hidden=true;if(hoverBin){hoverBin=null;schedule()}const a=nearest(x,y,3);canvas.style.cursor=a?'pointer':'default';hoverAssembly(a);return}
84
  canvas.style.cursor='pointer';
85
  hoverAssembly(!ic&&locusMode==='raw'?leaves[bin.lo]:null);
 
88
  else{const n=bin.hi-bin.lo,one=n===1,a=leaves[bin.lo];tip.innerHTML=`<b>${one?esc(a.name):shortCount(bin.value)+' predicted coding loci'}</b><span>${one?(bin.known?fmt(bin.value)+' predicted coding loci':'Locus count unavailable'):fmt(bin.known)+' of '+fmt(n)+' assemblies have counts'}</span><small>${one?esc(a.accession):'Sum of available counts · click to inspect individual bars'}</small>`;if(hoverBin!==bin){hoverBin=bin;schedule()}}
89
  tip.hidden=false;tip.style.left=Math.max(8,Math.min(W-270,x+16))+14+'px';tip.style.top=Math.max(65,Math.min(H-75,y+15))+64+'px';
90
  }
91
+ function focusNode(n){stopSweep(false);let pts=[point(n,n.mid)];for(let j=0;j<=16;j++)pts.push(polar(R,START+SPAN*(n.lo+(n.hi-n.lo)*j/16)/leaves.length));let xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]),minx=Math.min(...xs),maxx=Math.max(...xs),miny=Math.min(...ys),maxy=Math.max(...ys);zoom=Math.max(1,Math.min(35,Math.min(W/(maxx-minx+140),H/(maxy-miny+140))/baseScale));pan={x:-(minx+maxx)/2*baseScale*zoom,y:-(miny+maxy)/2*baseScale*zoom};clearSelection();$('#layer-tooltip').hidden=true;schedule()}
92
  function clickLayers(x,y){const ic=hitIcon(x,y);if(ic){focusNode(ic.n);return true}const b=hitBar(x,y);if(!b)return false;
93
  if(locusMode==='raw'){selectAssembly(leaves[b.lo]);return true}
94
  const mid=b.mid,p=polar(R+BAR_GAP,START+SPAN*(mid+.5)/leaves.length);locusMode='raw';$('#locus-mode').value='raw';makeLocusBins();zoom=24;pan={x:-p[0]*baseScale*zoom,y:-p[1]*baseScale*zoom};$('#layer-tooltip').hidden=true;schedule();return true;
light-overview.png CHANGED

Git LFS Details

  • SHA256: 222801a1e25a0b56b7f53925cd2cf1ce68f2399342eb024c2e420910635b31f4
  • Pointer size: 131 Bytes
  • Size of remote file: 637 kB

Git LFS Details

  • SHA256: 281e654177c769b0317a1cd71f36fa388f722c88b821c2acc180520c7cce6319
  • Pointer size: 131 Bytes
  • Size of remote file: 534 kB
loci-overview.png CHANGED

Git LFS Details

  • SHA256: b732ed2c8cb3cd8bc96e729ed98389e69f631b3ee7f4169a58f3b2859aa8d63a
  • Pointer size: 131 Bytes
  • Size of remote file: 736 kB

Git LFS Details

  • SHA256: f945c7a56888131242eafaa6475e25c62af5392c5ffef730de0724fc2d671626
  • Pointer size: 131 Bytes
  • Size of remote file: 625 kB
loci-raw.png CHANGED

Git LFS Details

  • SHA256: 75dd98311a847ff6c137b4cf5622b6a6e87e6aac4aba891c98320d14db8306c3
  • Pointer size: 131 Bytes
  • Size of remote file: 733 kB

Git LFS Details

  • SHA256: 1c0b540f33835eefe9da6773124614382c75b931a3b8f4f5933a8f6af991a7ab
  • Pointer size: 131 Bytes
  • Size of remote file: 623 kB
locus-detail.png CHANGED

Git LFS Details

  • SHA256: 717543e65d3910f87c3a0c24f08a5e6ec25dab2d34c1f492f469cdbc38c5f856
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB

Git LFS Details

  • SHA256: 5fdac6dd2d409dac39d93457de9f6ccd8f317a8ade6c9e24947821378656f11f
  • Pointer size: 131 Bytes
  • Size of remote file: 194 kB
overview.png CHANGED

Git LFS Details

  • SHA256: c7d856ef77df94d53898c75180940617182ad4ed5391d49a861908e28b8c052b
  • Pointer size: 131 Bytes
  • Size of remote file: 671 kB

Git LFS Details

  • SHA256: 934bd3402d7435bd2fb8a6bae3be74e11575d7271a5a8308fd36606920024007
  • Pointer size: 131 Bytes
  • Size of remote file: 567 kB
refseq.png CHANGED

Git LFS Details

  • SHA256: c8aec54cb1f2b1e8629b0635bcea207993da50938c0d9ba250460af030cafb21
  • Pointer size: 131 Bytes
  • Size of remote file: 502 kB

Git LFS Details

  • SHA256: 671dac949435ef3fd593b051fc9daa7cc452530d22fa43a8addaad4a0676b395
  • Pointer size: 131 Bytes
  • Size of remote file: 429 kB
selection-aligned.png CHANGED

Git LFS Details

  • SHA256: 941df39a9ad4ede96285fe64abff16778f1f84728feab7f119096e93a62efc86
  • Pointer size: 131 Bytes
  • Size of remote file: 164 kB

Git LFS Details

  • SHA256: c3339f808c1b5342024af1d75ec6540d380dbd087a85e6c9738abe28cc33bed0
  • Pointer size: 131 Bytes
  • Size of remote file: 175 kB
sweep-complete.png ADDED

Git LFS Details

  • SHA256: 3a847abf43b55ec2172a34ab68434b86c8d4e4187ff848b16128e62a6c6de6b9
  • Pointer size: 131 Bytes
  • Size of remote file: 571 kB
sweep-detail.png ADDED

Git LFS Details

  • SHA256: 4f91936deef7e6b13cc5f6d6675222572cd7f23d63a303b7eb05d299a9d63bd1
  • Pointer size: 131 Bytes
  • Size of remote file: 592 kB
sweep.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // The sweep counts every crossed strand, not just the ones displayed in frames.
2
+ const SWEEP_DURATION=2500,NOON=-Math.PI/2;
3
+ const sweep={running:false,status:'ready',progress:0,angle:NOON,count:0,species:0,loci:0,known:0,frame:0,startedAt:0};
4
+ let sweepSpecies,sweepLoci,sweepKnown,interactionPending=false;
5
+
6
+ function prepareSweep(){
7
+ const n=leaves.length,seen=new Set();
8
+ sweepSpecies=new Uint32Array(n+1);sweepLoci=new Float64Array(n+1);sweepKnown=new Uint32Array(n+1);
9
+ for(let i=0;i<n;i++){
10
+ const a=leaves[i],species=a.lineage.find(n=>n.rank==='species');
11
+ if(species)seen.add(species.id);
12
+ sweepSpecies[i+1]=seen.size;
13
+ sweepLoci[i+1]=sweepLoci[i]+(a.loci??0);
14
+ sweepKnown[i+1]=sweepKnown[i]+(a.loci!==null?1:0);
15
+ }
16
+ POC.sweep=sweep;POC.sweepTotals={species:seen.size,assemblies:n,loci:sweepLoci[n],known:sweepKnown[n]};
17
+ $('#animate-sweep').disabled=false;
18
+ $('#animate-sweep').onclick=()=>sweep.running?stopSweep():startSweep();
19
+ document.addEventListener('keydown',e=>{if(e.key==='Escape'&&sweep.running)stopSweep()});
20
+ document.addEventListener('visibilitychange',()=>{if(document.hidden&&sweep.running)stopSweep()});
21
+ }
22
+
23
+ function scheduleInteraction(){
24
+ if(interactionPending)return;
25
+ interactionPending=true;requestAnimationFrame(()=>{interactionPending=false;drawInteraction()});
26
+ }
27
+
28
+ function drawInteraction(){
29
+ const dpr=interaction.width/W;
30
+ fx.setTransform(dpr,0,0,dpr,0,0);fx.clearRect(0,0,W,H);
31
+ const angle=sweep.running?sweep.angle:selected?.angle??sweep.angle;
32
+ // A fine hand joins the selected strand at its root, with no dial or numerals.
33
+ const c=screen([0,0]),tip=screen(polar(92,angle));
34
+ fx.strokeStyle=palette.highlight;fx.lineWidth=.85;fx.lineCap='round';
35
+ fx.beginPath();fx.moveTo(...c);fx.lineTo(...tip);fx.stroke();
36
+ fx.fillStyle=palette.highlight;fx.beginPath();fx.arc(...c,1.5,0,TAU);fx.fill();
37
+ if(!selected)return;
38
+ const a=selected;fx.lineCap='butt';fx.strokeStyle=palette.highlight;fx.lineWidth=1.8;
39
+ fx.beginPath();fx.moveTo(...screen([hitData[a.geometryStart],hitData[a.geometryStart+1]]));
40
+ for(let k=a.geometryStart+4;k<a.geometryEnd;k+=8)fx.lineTo(...screen([hitData[k],hitData[k+1]]));
41
+ fx.stroke();fx.beginPath();fx.arc(...screen(a.tip),4,0,TAU);fx.fill();
42
+ }
43
+
44
+ function updateSweep(progress){
45
+ sweep.progress=Math.max(0,Math.min(1,progress));
46
+ // Gentle acceleration and deceleration; a complete turn still takes 2.5 s.
47
+ const t=sweep.progress,eased=t*t*(3-2*t);
48
+ sweep.angle=NOON+TAU*eased;
49
+ const n=leaves.length,count=Math.max(0,Math.min(n,Math.floor((sweep.angle-START)/SPAN*n+.5)));
50
+ sweep.count=count;sweep.species=sweepSpecies[count];sweep.loci=sweepLoci[count];sweep.known=sweepKnown[count];
51
+ selected=count&&sweep.angle<=START+SPAN?leaves[count-1]:null;
52
+ $('#seen-species').textContent=fmt(sweep.species);
53
+ $('#seen-assemblies').textContent=fmt(count);
54
+ $('#seen-loci').textContent=fmt(sweep.loci);
55
+ $('#seen-loci').title=fmt(sweep.known)+' assemblies with published counts; missing counts are excluded.';
56
+ }
57
+
58
+ function sweepButton(label){
59
+ $('#sweep-button-label').textContent=label;
60
+ $('#animate-sweep').setAttribute('aria-label',sweep.running?'Stop the sweep':'Animate a 2.5-second sweep of the whole tree');
61
+ $('.stage-wrap').dataset.sweeping=String(sweep.running);
62
+ }
63
+
64
+ function cacheSweepTree(){
65
+ const still=$('#tree-still');still.width=canvas.width;still.height=canvas.height;
66
+ const frozen=still.getContext('2d',{willReadFrequently:true});
67
+ frozen.drawImage(canvas,0,0);frozen.drawImage(overlay,0,0);
68
+ }
69
+
70
+ function startSweep(){
71
+ stopSweep(false);
72
+ const redraw=zoom!==1||pan.x!==0||pan.y!==0||hoverBin!==null;
73
+ zoom=1;pan={x:0,y:0};clearSelection();hoverBin=null;$('#layer-tooltip').hidden=true;
74
+ $('#results').replaceChildren();$('#selection').hidden=true;
75
+ sweep.running=true;sweep.status='running';sweep.startedAt=0;
76
+ updateSweep(0);sweepButton('Stop sweep');$('#sweep-status').textContent='Scanning the tree';
77
+ // Render the stationary tree once. Only the thin interaction canvas moves.
78
+ if(redraw)draw();else{cacheSweepTree();drawInteraction()}
79
+ sweep.frame=requestAnimationFrame(function step(){
80
+ const now=performance.now();
81
+ if(!sweep.running)return;
82
+ if(!sweep.startedAt)sweep.startedAt=now;
83
+ updateSweep((now-sweep.startedAt)/SWEEP_DURATION);drawInteraction();
84
+ if(sweep.progress<1){sweep.frame=requestAnimationFrame(step);return}
85
+ sweep.running=false;sweep.status='complete';sweep.frame=0;
86
+ selected=pinnedAssembly=hoveredAssembly=null;
87
+ sweepButton('Replay sweep');$('#sweep-status').textContent='Full tree seen';
88
+ $('#sweep-announcement').textContent=fmt(sweep.species)+' species, '+fmt(sweep.count)+' assemblies and '+fmt(sweep.loci)+' coding loci seen.';
89
+ drawInteraction();
90
+ });
91
+ }
92
+
93
+ function stopSweep(pin=true){
94
+ if(!sweep.running)return;
95
+ cancelAnimationFrame(sweep.frame);sweep.frame=0;sweep.running=false;sweep.status='stopped';
96
+ sweepButton('Replay sweep');$('#sweep-status').textContent='Sweep stopped';
97
+ if(pin&&selected)selectAssembly(selected);else selected=pinnedAssembly=hoveredAssembly=null;
98
+ scheduleInteraction();
99
+ }
template.html CHANGED
@@ -17,4 +17,24 @@ button:hover{border-color:var(--muted)}
17
  #selection[data-pinned="true"] #sel-state{color:var(--green)}
18
  @media(max-width:850px){.brand{font-size:20px;white-space:nowrap}.header-right{gap:6px}.header-right #about{font-size:10px;padding:9px 7px}header{gap:10px}#theme-toggle{width:32px;height:32px}}
19
  @media(prefers-reduced-motion:no-preference){button{transition:background .15s,border-color .15s}}
20
- </style></head><body><header><div class="brand">Carbon-<em>A</em> Database</div><div class="header-right"><span class="tag">Assembly strands · Proof of concept</span><button id="about">About the data</button><button id="theme-toggle" aria-label="Switch to light mode" title="Switch to light mode"><svg class="theme-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 1v3m0 16v3M1 12h3m16 0h3M4.2 4.2l2.1 2.1m11.4 11.4 2.1 2.1M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/></svg><svg class="theme-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.5 14.5A9 9 0 0 1 9.5 3.5a9 9 0 1 0 11 11Z"/></svg></button></div></header><main><aside><h1>The scale is<br>in the tree.</h1><p class="intro">One strand for every assembly.<br>Shared ancestry brings the strands together. Zoom in to follow them apart.</p><p class="subset"><b>PoC subset · <span id="shown">33,588</span> assemblies · <span id="taxa-shown">16,976</span> taxa.</b><br>The full release contains 48,167 assemblies.</p><section class="locus-controls"><label class="tag" for="locus-mode">Coding loci at the strand tips</label><select id="locus-mode"><option value="5">Binned · 5 assemblies per bar</option><option value="20">Binned · 20 assemblies per bar</option><option value="50">Binned · 50 assemblies per bar</option><option value="200">Binned · 200 assemblies per bar</option><option value="raw">Individual assemblies</option><option value="off">Hide locus bars</option></select><strong id="locus-readout"></strong><p id="locus-explain"></p><label class="checkbox icon-toggle"><input type="checkbox" id="show-icons" checked>Genome Atlas silhouettes</label></section><div class="release"><span class="tag">Full release</span><div class="big">566.34<em>M</em></div><div class="sub">candidate coding loci</div><div class="stats"><div><b>48,167</b><span>annotated assemblies</span></div><div><b>22,617</b><span>annotated taxa</span></div><div class="stat-wide"><b>27.18 Tbp</b><span>across included assemblies</span></div></div></div><section class="legend"><label class="tag" for="scope">Compare assembly records</label><select id="scope"><option value="0">GenBank annotation records</option><option value="1">RefSeq comparison corpus</option></select><div class="key"><i class="sample-line blue"></i><span id="blue-label"></span><strong id="blue-count"></strong></div><div class="key"><i class="sample-line"></i><span id="green-label"></span><strong id="green-count"></strong></div><label class="checkbox"><input id="existing-only" type="checkbox">Emphasise matching assemblies</label><p id="scope-note" class="scope-note"></p></section><div class="quick"><button id="human">Find a human assembly ↗</button></div><button id="gene-open" style="font-size:11px;border:0;padding:6px 0;color:var(--green)">See its coding regions →</button></aside><section class="stage-wrap"><div class="topbar"><div class="searchbox"><input id="search" type="search" aria-label="Search assemblies or species" placeholder="Search a species or assembly…" disabled autocomplete="off"><div id="results"></div></div><span class="one-line">Real assemblies · NCBI taxonomy</span></div><div id="stage"><canvas id="tree" aria-label="Interactive taxonomic tree of 33,588 assembly strands. Use search or the human assembly button to inspect individual records."></canvas><canvas id="overlay" aria-hidden="true"></canvas><div id="loading">Preparing 33,588 assembly strands…</div></div><div id="layer-tooltip" hidden></div><section id="selection" hidden><button id="sel-close" aria-label="Close assembly details">×</button><div id="sel-state">Hover preview</div><h2 id="sel-name"></h2><a id="sel-id" target="_blank" rel="noopener"></a><div id="sel-lineage"></div><div id="sel-status"></div><div id="sel-data"></div><button id="sel-focus">Follow this strand ↗</button><button id="sel-loci" hidden>See coding regions →</button></section><div class="help"><b>Hover to inspect · Click to pin · Scroll to zoom · Drag to pan</b><br>Topology follows taxonomy. Branch length does not represent evolutionary time.</div><div class="controls"><button id="detail">Strand detail</button><button id="zoomout" aria-label="Zoom out">−</button><span id="zoom-readout">1.0×</span><button id="zoomin" aria-label="Zoom in">+</button><button id="reset">Whole tree</button></div></section></main><dialog id="gene-dialog"><button class="dialog-close" id="gene-close" aria-label="Close coding-region view">×</button><span class="tag">From an assembly to its annotations</span><h2 style="margin-top:16px">Homo sapiens</h2><p id="gene-summary"></p><div class="gene-label">GCA_000001405.29 · chromosome 1, CM000663.2 · first 1,000,000 bases<br>Upper track: forward strand · lower track: reverse strand</div><svg id="gene-svg" viewBox="0 0 1110 210" role="img" aria-label="Actual predicted coding segments in the first one million bases of human chromosome 1"></svg><p id="gene-count-window"></p><p class="gene-notes">Real coordinates from the published Carbon-A GFF3. These are model predictions, not experimentally confirmed genes. The blue/green tree colours describe annotation records for an assembly; they do not classify individual loci as known or new.</p></dialog><dialog id="about-dialog"><button class="dialog-close" id="about-close" aria-label="Close data information">×</button><h2>What is being drawn</h2><p>Each of the 33,588 strands is a distinct accession from the explorer’s covered-assembly table, mapped through the NCBI taxonomy. No assemblies or taxonomic branches have been invented to fill out the release total.</p><ul><li>Tree and common names: the existing HuggingFaceBio GenBank annotation explorer, 7 October 2026.</li><li>Existing annotation evidence: the cached NCBI GenBank assembly summary, 7 October 2026; a named annotation provider is required.</li><li>RefSeq mode counts matching assemblies, not genes: exact versioned matches between an assembly’s paired GCF accession and the project’s 2,055-assembly comparison corpus. It does not cover all RefSeq annotations.</li><li>Gene detail: actual published GFF3 coordinates and metadata for GCA_000001405.29.</li><li>Release totals supplied on 8 October 2026: 48,167 assemblies, 22,617 taxa, 566.34M coding loci and 27.18 Tbp.</li></ul><p>Equal-width assembly strands preserve count through shared ancestors. At overview scale many strands fall within a pixel; zoom reveals them separately. Radius and branch length are layout choices, not dates or evolutionary distances.</p><p>For example, the human branch contains 1,412 assemblies: 2 match the project’s RefSeq comparison corpus, while 4 have annotation-provider records in this GenBank snapshot. These small counts describe assembly records, not human genes. RefSeq annotations are maintained separately from GenBank submission annotations. <a href="https://www.ncbi.nlm.nih.gov/refseq/about/human/" target="_blank" rel="noopener">NCBI’s explanation of human reference annotation</a>.</p><p>The outer bars sum measured gene_count values from published per-assembly metadata. Bins contain consecutive assembly tips in taxonomy order. Grey base marks indicate the fraction of assemblies without counts; these are not treated as measured zeros. The radial scale is linear and recalculated for each bin size. Colours within the tree classify assemblies; the green outer bars count all predicted loci, regardless of prior annotation status.</p><p>Silhouettes are the original PhyloPic assets and taxon assignments used by the Genome Atlas. Click an icon to explore its branch. <button id="credits-open">Icon credits and licences</button></p><p><a href="https://huggingface.co/spaces/HuggingFaceBio/genbank-annotation-explorer" target="_blank" rel="noopener">Source explorer ↗</a> · <a href="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/reference-docs/ftp-browsing/" target="_blank" rel="noopener">NCBI metadata ↗</a></p></dialog><dialog id="credits-dialog"><button id="credits-close" class="dialog-close" aria-label="Close icon credits">×</button><h2>Genome Atlas silhouettes</h2><p>Original PhyloPic silhouettes, reused with the atlas’s taxonomic assignments. Their fill colour is adapted to the selected theme; original SVG files are preserved in the source data. Each image’s source page and licence are linked below.</p><div id="icon-credits"></div></dialog><script id="dataset" type="application/octet-stream">__DATA__</script><script>__SCRIPT__</script></body></html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  #selection[data-pinned="true"] #sel-state{color:var(--green)}
18
  @media(max-width:850px){.brand{font-size:20px;white-space:nowrap}.header-right{gap:6px}.header-right #about{font-size:10px;padding:9px 7px}header{gap:10px}#theme-toggle{width:32px;height:32px}}
19
  @media(prefers-reduced-motion:no-preference){button{transition:background .15s,border-color .15s}}
20
+
21
+ #stage{touch-action:none}
22
+ #interaction,#tree-still{pointer-events:none}
23
+ #tree-still{visibility:hidden}
24
+ [data-sweeping="true"] #tree,[data-sweeping="true"] #overlay{visibility:hidden}
25
+ [data-sweeping="true"] #tree-still{visibility:visible}
26
+ #animate-sweep{display:flex;align-items:center;justify-content:center;gap:8px;color:var(--green);min-width:146px;font-size:11px;padding:10px 12px}
27
+ #animate-sweep svg{width:12px;height:12px;fill:currentColor}
28
+ .sweep-stop{display:none}
29
+ [data-sweeping="true"] .sweep-play{display:none}
30
+ [data-sweeping="true"] .sweep-stop{display:block}
31
+ .sweep-meter{position:absolute;left:25px;right:25px;top:70px;display:grid;grid-template-columns:1fr 1fr 1.3fr;gap:0 24px;max-width:650px;z-index:2}
32
+ .sweep-meter strong{display:block;font-family:Manrope,Arial,sans-serif;font-size:26px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.7px;line-height:1.15}
33
+ .sweep-meter>div:last-of-type strong{color:var(--green)}
34
+ .sweep-meter>div>span{display:block;font-size:10px;color:var(--muted);margin-top:5px}
35
+ .sweep-meter p{grid-column:1/-1;display:flex;gap:18px;font-size:9px;color:var(--muted);margin:12px 0 0;line-height:1.4}
36
+ #sweep-status{color:var(--secondary)}
37
+ #stage{top:159px}#selection{top:159px}
38
+ .visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
39
+ @media(max-width:850px){.searchbox{width:auto;min-width:0;flex:1}#animate-sweep{min-width:129px;padding:10px 9px;font-size:10px}.sweep-meter{left:20px;right:20px;gap:0 12px;top:74px}.sweep-meter strong{font-size:21px}.sweep-meter p{display:block;font-size:8px}.sweep-meter p span{display:block}.sweep-meter p span+span{margin-top:3px}#stage{top:171px}#selection{top:171px}}
40
+ </style></head><body><header><div class="brand">Carbon-<em>A</em> Database</div><div class="header-right"><span class="tag">Assembly strands · Proof of concept</span><button id="about">About the data</button><button id="theme-toggle" aria-label="Switch to light mode" title="Switch to light mode"><svg class="theme-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 1v3m0 16v3M1 12h3m16 0h3M4.2 4.2l2.1 2.1m11.4 11.4 2.1 2.1M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/></svg><svg class="theme-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.5 14.5A9 9 0 0 1 9.5 3.5a9 9 0 1 0 11 11Z"/></svg></button></div></header><main><aside><h1>The scale is<br>in the tree.</h1><p class="intro">One strand for every assembly.<br>Shared ancestry brings the strands together. Zoom in to follow them apart.</p><p class="subset"><b>PoC subset · <span id="shown">33,588</span> assemblies · <span id="taxa-shown">16,976</span> taxa.</b><br>The full release contains 48,167 assemblies.</p><section class="locus-controls"><label class="tag" for="locus-mode">Coding loci at the strand tips</label><select id="locus-mode"><option value="5">Binned · 5 assemblies per bar</option><option value="20">Binned · 20 assemblies per bar</option><option value="50">Binned · 50 assemblies per bar</option><option value="200">Binned · 200 assemblies per bar</option><option value="raw">Individual assemblies</option><option value="off">Hide locus bars</option></select><strong id="locus-readout"></strong><p id="locus-explain"></p><label class="checkbox icon-toggle"><input type="checkbox" id="show-icons" checked>Genome Atlas silhouettes</label></section><div class="release"><span class="tag">Full release</span><div class="big">566.34<em>M</em></div><div class="sub">candidate coding loci</div><div class="stats"><div><b>48,167</b><span>annotated assemblies</span></div><div><b>22,617</b><span>annotated taxa</span></div><div class="stat-wide"><b>27.18 Tbp</b><span>across included assemblies</span></div></div></div><section class="legend"><label class="tag" for="scope">Compare assembly records</label><select id="scope"><option value="0">GenBank annotation records</option><option value="1">RefSeq comparison corpus</option></select><div class="key"><i class="sample-line blue"></i><span id="blue-label"></span><strong id="blue-count"></strong></div><div class="key"><i class="sample-line"></i><span id="green-label"></span><strong id="green-count"></strong></div><label class="checkbox"><input id="existing-only" type="checkbox">Emphasise matching assemblies</label><p id="scope-note" class="scope-note"></p></section><div class="quick"><button id="human">Find a human assembly ↗</button></div><button id="gene-open" style="font-size:11px;border:0;padding:6px 0;color:var(--green)">See its coding regions →</button></aside><section class="stage-wrap"><div class="topbar"><div class="searchbox"><input id="search" type="search" aria-label="Search assemblies or species" placeholder="Search a species or assembly…" disabled autocomplete="off"><div id="results"></div></div><button id="animate-sweep" disabled aria-label="Animate a 2.5-second sweep of the whole tree"><svg class="sweep-play" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2.5 13 8 4 13.5Z"/></svg><svg class="sweep-stop" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4h8v8H4Z"/></svg><span id="sweep-button-label">Animate sweep</span></button></div><div class="sweep-meter" aria-label="Cumulative counts in the current sweep"><div><strong id="seen-species">0</strong><span>Species seen</span></div><div><strong id="seen-assemblies">0</strong><span>Assemblies seen</span></div><div><strong id="seen-loci">0</strong><span>Coding loci seen</span></div><p><span id="sweep-status">Ready · 2.5 seconds</span><span>Shown subset · unique species · available locus counts</span></p></div><span id="sweep-announcement" class="visually-hidden" role="status"></span><div id="stage"><canvas id="tree" aria-label="Interactive taxonomic tree of 33,588 assembly strands. Use search or the human assembly button to inspect individual records."></canvas><canvas id="tree-still" aria-hidden="true"></canvas><canvas id="overlay" aria-hidden="true"></canvas><canvas id="interaction" aria-hidden="true"></canvas><div id="loading">Preparing 33,588 assembly strands…</div></div><div id="layer-tooltip" hidden></div><section id="selection" hidden><button id="sel-close" aria-label="Close assembly details">×</button><div id="sel-state">Hover preview</div><h2 id="sel-name"></h2><a id="sel-id" target="_blank" rel="noopener"></a><div id="sel-lineage"></div><div id="sel-status"></div><div id="sel-data"></div><button id="sel-focus">Follow this strand ↗</button><button id="sel-loci" hidden>See coding regions →</button></section><div class="help"><b>Hover to inspect · Click to pin · Scroll to zoom · Drag to pan</b><br>Topology follows taxonomy. Branch length does not represent evolutionary time.</div><div class="controls"><button id="detail">Strand detail</button><button id="zoomout" aria-label="Zoom out">−</button><span id="zoom-readout">1.0×</span><button id="zoomin" aria-label="Zoom in">+</button><button id="reset">Whole tree</button></div></section></main><dialog id="gene-dialog"><button class="dialog-close" id="gene-close" aria-label="Close coding-region view">×</button><span class="tag">From an assembly to its annotations</span><h2 style="margin-top:16px">Homo sapiens</h2><p id="gene-summary"></p><div class="gene-label">GCA_000001405.29 · chromosome 1, CM000663.2 · first 1,000,000 bases<br>Upper track: forward strand · lower track: reverse strand</div><svg id="gene-svg" viewBox="0 0 1110 210" role="img" aria-label="Actual predicted coding segments in the first one million bases of human chromosome 1"></svg><p id="gene-count-window"></p><p class="gene-notes">Real coordinates from the published Carbon-A GFF3. These are model predictions, not experimentally confirmed genes. The blue/green tree colours describe annotation records for an assembly; they do not classify individual loci as known or new.</p></dialog><dialog id="about-dialog"><button class="dialog-close" id="about-close" aria-label="Close data information">×</button><h2>What is being drawn</h2><p>Each of the 33,588 strands is a distinct accession from the explorer’s covered-assembly table, mapped through the NCBI taxonomy. No assemblies or taxonomic branches have been invented to fill out the release total.</p><ul><li>Tree and common names: the existing HuggingFaceBio GenBank annotation explorer, 7 October 2026.</li><li>Existing annotation evidence: the cached NCBI GenBank assembly summary, 7 October 2026; a named annotation provider is required.</li><li>RefSeq mode counts matching assemblies, not genes: exact versioned matches between an assembly’s paired GCF accession and the project’s 2,055-assembly comparison corpus. It does not cover all RefSeq annotations.</li><li>Gene detail: actual published GFF3 coordinates and metadata for GCA_000001405.29.</li><li>Release totals supplied on 8 October 2026: 48,167 assemblies, 22,617 taxa, 566.34M coding loci and 27.18 Tbp.</li></ul><p>The centre hand points to the selected strand. Animate sweep makes a full clockwise turn in 2.5 seconds and counts every assembly passed, including strands crossed between animation frames. Species are deduplicated by their NCBI species-rank ancestor; coding loci sum only published counts. A complete sweep of this subset reaches 16,374 species, 33,588 assemblies and 212,905,534 loci. The counters reset on replay.</p><p>Equal-width assembly strands preserve count through shared ancestors. At overview scale many strands fall within a pixel; zoom reveals them separately. Radius and branch length are layout choices, not dates or evolutionary distances.</p><p>For example, the human branch contains 1,412 assemblies: 2 match the project’s RefSeq comparison corpus, while 4 have annotation-provider records in this GenBank snapshot. These small counts describe assembly records, not human genes. RefSeq annotations are maintained separately from GenBank submission annotations. <a href="https://www.ncbi.nlm.nih.gov/refseq/about/human/" target="_blank" rel="noopener">NCBI’s explanation of human reference annotation</a>.</p><p>The outer bars sum measured gene_count values from published per-assembly metadata. Bins contain consecutive assembly tips in taxonomy order. Grey base marks indicate the fraction of assemblies without counts; these are not treated as measured zeros. The radial scale is linear and recalculated for each bin size. Colours within the tree classify assemblies; the green outer bars count all predicted loci, regardless of prior annotation status.</p><p>Silhouettes are the original PhyloPic assets and taxon assignments used by the Genome Atlas. Click an icon to explore its branch. <button id="credits-open">Icon credits and licences</button></p><p><a href="https://huggingface.co/spaces/HuggingFaceBio/genbank-annotation-explorer" target="_blank" rel="noopener">Source explorer ↗</a> · <a href="https://www.ncbi.nlm.nih.gov/datasets/docs/v2/reference-docs/ftp-browsing/" target="_blank" rel="noopener">NCBI metadata ↗</a></p></dialog><dialog id="credits-dialog"><button id="credits-close" class="dialog-close" aria-label="Close icon credits">×</button><h2>Genome Atlas silhouettes</h2><p>Original PhyloPic silhouettes, reused with the atlas’s taxonomic assignments. Their fill colour is adapted to the selected theme; original SVG files are preserved in the source data. Each image’s source page and licence are linked below.</p><div id="icon-credits"></div></dialog><script id="dataset" type="application/octet-stream">__DATA__</script><script>__SCRIPT__</script></body></html>