lvwerra HF Staff commited on
Commit
23078ef
·
verified ·
1 Parent(s): 5dd01bb

Add real-assembly strand tree proof of concept with annotation comparisons

Browse files
Files changed (16) hide show
  1. .gitattributes +5 -0
  2. README.md +32 -4
  3. app.js +95 -0
  4. build.py +22 -0
  5. carbon-a-strand-poc.png +3 -0
  6. carbon-a-strand-source.zip +3 -0
  7. check.py +26 -0
  8. detail.png +3 -0
  9. export_still.py +13 -0
  10. genes.png +3 -0
  11. index.html +0 -0
  12. overview.png +3 -0
  13. prepare_data.py +44 -0
  14. provenance.json +50 -0
  15. refseq.png +3 -0
  16. template.html +6 -0
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ overview.png filter=lfs diff=lfs merge=lfs -text
37
+ detail.png filter=lfs diff=lfs merge=lfs -text
38
+ genes.png filter=lfs diff=lfs merge=lfs -text
39
+ refseq.png filter=lfs diff=lfs merge=lfs -text
40
+ carbon-a-strand-poc.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,10 +1,38 @@
1
  ---
2
- title: Carbon A Tree Preview
3
- emoji: ⚡
4
  colorFrom: green
5
- colorTo: green
6
  sdk: static
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Carbon-A Assembly Strands
3
+ emoji: 🌿
4
  colorFrom: green
5
+ colorTo: blue
6
  sdk: static
7
+ app_file: index.html
8
  pinned: false
9
  ---
10
 
11
+ # Carbon-A assembly strands
12
+
13
+ An interactive proof of concept: a separate, continuous strand for every real assembly in the reusable taxonomy snapshot. Shared ancestors form bundles; zooming separates the strands. Green and blue distinguish assemblies with and without existing annotation evidence under the selected comparison.
14
+
15
+ The PoC contains **33,588 unique assemblies across 16,976 taxa**, taken from the existing [GenBank annotation explorer](https://huggingface.co/spaces/HuggingFaceBio/genbank-annotation-explorer). The full release totals supplied on 8 October 2026 are **48,167 assemblies, 22,617 taxa, 566.34M coding loci and 27.18 Tbp**. Those totals are displayed separately: this PoC does not invent missing assembly identities or apportion the gene total using assumed weights.
16
+
17
+ Two comparison modes are available:
18
+
19
+ - **NCBI annotation records:** blue means a named annotation provider appears in the cached NCBI GenBank assembly summary. Missing records are not proof that an assembly has never been annotated.
20
+ - **RefSeq comparison corpus:** blue means an exact versioned match between the listed paired RefSeq accession and the project's 2,055-assembly comparison corpus. This corpus is not the whole of RefSeq.
21
+
22
+ Colours classify assemblies, not the novelty of their individual predicted loci. A paired GCA and GCF may have different sequences and coordinates.
23
+
24
+ The human assembly detail uses actual published GFF3 coordinates, with 23,767 predicted loci across GCA_000001405.29 and 13 beginning in the first megabase of CM000663.2. CDS rectangles are actual coding segments. These are predictions, not experimental validation.
25
+
26
+ ## Layout and implementation
27
+
28
+ 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.
29
+
30
+ 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. Zoom, pan, search and clicking reveal the underlying accession.
31
+
32
+ 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.
33
+
34
+ ## Rebuild
35
+
36
+ `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`, the data files, and the existing release font assets. `python check.py` runs browser checks and captures previews.
37
+
38
+ Data provenance and limitations are recorded in `provenance.json`. The original release video and explorer were not modified.
app.js ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const $=s=>document.querySelector(s), fmt=n=>n.toLocaleString('en-US');
2
+ const GREEN='#00b86b',BLUE='#4167ec';
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]));
9
+ function polar(r,a){return [r*Math.cos(a),r*Math.sin(a)]}
10
+ function point(n,i){let a=START+SPAN*(n.mid+.5)/leaves.length,off=(i-n.mid)*.012;let [x,y]=polar(n.r,a);return [x-Math.sin(a)*off,y+Math.cos(a)*off]}
11
+ function controls(p,q){let rp=Math.hypot(...p),rq=Math.hypot(...q),delta=(rq-rp)*.46;return [[p[0]*(1+delta/rp),p[1]*(1+delta/rp)],[q[0]*(1-delta/rq),q[1]*(1-delta/rq)]]}
12
+ function bez(p,c,d,q,t){let s=1-t;return [s*s*s*p[0]+3*s*s*t*c[0]+3*s*t*t*d[0]+t*t*t*q[0],s*s*s*p[1]+3*s*s*t*c[1]+3*s*t*t*d[1]+t*t*t*q[1]]}
13
+ function prepare(){
14
+ nodes=new Map(D.nodes.map(r=>[r[0],{id:r[0],parent:r[1],name:r[2],rank:r[3],common:r[4],children:[],own:[],count:0}]));
15
+ for(const n of nodes.values())if(n.parent)nodes.get(n.parent)?.children.push(n);
16
+ D.assemblies.forEach((r,index)=>{nodes.get(r[1]).own.push({row:r,index,accession:r[0],taxid:r[1],name:r[2],flag:(r[4]===1?1:0)+(r[5]?2:0)});});
17
+ const root=nodes.get(2759);
18
+ function count(n){n.count=n.own.length;for(let c of n.children)n.count+=count(c);n.children.sort((a,b)=>b.count-a.count||a.id-b.id);return n.count}count(root);
19
+ leaves=[];
20
+ function order(n){n.lo=leaves.length;for(let c of n.children)order(c);n.own.sort((a,b)=>a.accession.localeCompare(b.accession));leaves.push(...n.own);n.hi=leaves.length;n.mid=(n.lo+n.hi-1)/2;for(let a of n.own)a.i=leaves.indexOf(a,n.lo);}
21
+ order(root);
22
+ for(let i=0;i<leaves.length;i++){
23
+ const a=leaves[i];a.i=i;let n=nodes.get(a.taxid),lineage=[];
24
+ while(n){lineage.unshift(n);n=nodes.get(n.parent)}a.lineage=lineage;
25
+ const keep=lineage.filter(n=>ranks[n.rank]);let max=92;a.points=[];
26
+ // A circular root keeps every assembly's lane distinct at the centre as well.
27
+ a.points.push(polar(92,START+SPAN*(i+.5)/leaves.length));
28
+ for(n of keep){let r=ranks[n.rank];if(r<=max||r>700)continue;n.r=r;max=r;a.points.push(point(n,i));}
29
+ a.angle=START+SPAN*(i+.5)/leaves.length;a.tip=polar(R,a.angle);a.points.push(a.tip);
30
+ }
31
+ groups=[33208,4751,33090,33630,554915,2763,554296,5761].map(id=>nodes.get(id)).filter(n=>n&&n.count>40);
32
+ return root;
33
+ }
34
+ function shader(type,source){const s=gl.createShader(type);gl.shaderSource(s,source);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(s));return s}
35
+ function stepsFor(p,q){let rp=Math.hypot(...p),rq=Math.hypot(...q),a=Math.acos(Math.max(-1,Math.min(1,(p[0]*q[0]+p[1]*q[1])/(rp*rq))));return Math.min(32,Math.max(4,Math.ceil(a*24+Math.abs(rq-rp)/50)))}
36
+ function setupGL(){
37
+ gl=canvas.getContext('webgl',{alpha:false,antialias:true,preserveDrawingBuffer:true});if(!gl)throw Error('WebGL is unavailable in this browser.');
38
+ const vs=shader(gl.VERTEX_SHADER,`attribute vec4 a;uniform vec2 size;uniform vec2 pan;uniform float scale;uniform float dpr;uniform float mode;uniform float only;varying vec4 color;void main(){vec2 p=a.xy*scale+size*.5+pan;gl_Position=vec4(p.x/size.x*2.-1.,1.-p.y/size.y*2.,0.,1.);float blue=mode<.5?mod(a.w,2.):step(1.5,a.w);vec3 ink=mix(vec3(0.,.69,.39),vec3(.20,.34,.90),blue);float opacity=clamp(a.z*scale*dpr,0.,.95);if(only>.5){if(blue<.5)opacity*=.06;else opacity=min(.95,max(.14,opacity*4.));}color=vec4(ink,opacity);}`);
39
+ const fs=shader(gl.FRAGMENT_SHADER,`precision mediump float;varying vec4 color;void main(){gl_FragColor=color;}`);
40
+ program=gl.createProgram();gl.attachShader(program,vs);gl.attachShader(program,fs);gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program));
41
+ gl.useProgram(program);gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
42
+ // Every real assembly has its own complete path. No generated placeholder leaves.
43
+ let len=leaves.reduce((n,a)=>n+a.points.slice(1).reduce((m,p,j)=>m+stepsFor(a.points[j],p)*2*4,0),0),data=new Float32Array(len),k=0;hitOwner=new Uint32Array(len/8);
44
+ const shuffled=leaves.slice().sort((a,b)=>hash(a.i)-hash(b.i));
45
+ for(const a of shuffled)for(let j=1;j<a.points.length;j++){
46
+ let p=a.points[j-1],q=a.points[j],[c,d]=controls(p,q),prev=p,steps=stepsFor(p,q);
47
+ for(let t=1;t<=steps;t++){let next=bez(p,c,d,q,t/steps);hitOwner[k/8]=a.i;for(let v of [prev,next]){data[k++]=v[0];data[k++]=v[1];data[k++]=.008+.054*Math.pow(Math.min(1,Math.hypot(...v)/R),3);data[k++]=a.flag;}prev=next;}
48
+ }
49
+ hitData=data;vertexCount=k/4;const buffer=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);const loc=gl.getAttribLocation(program,'a');gl.enableVertexAttribArray(loc);gl.vertexAttribPointer(loc,4,gl.FLOAT,false,0,0);
50
+ window.POC={assemblies:leaves.length,vertices:vertexCount,prior:D.meta.classification_counts['1'],refseq:D.meta.refseq_matches,data:D,leaves,nodes,select:selectAssembly,focus:focusAssembly};
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*2+180),H/(R*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(1,1,1,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.drawArrays(gl.LINES,0,vertexCount);
58
+ ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);
59
+ if(zoom<2){let c=screen([0,0]);ctx.textAlign='center';ctx.fillStyle='#14221c';ctx.font='700 20px Manrope';ctx.fillText(fmt(leaves.length),c[0],c[1]+1);ctx.fillStyle='#748078';ctx.font='10px Inter';ctx.fillText('assembly strands',c[0],c[1]+18);}
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=[];for(const n of labels){let angle=START+SPAN*(n.mid+.5)/leaves.length;let [x,y]=screen(polar(R+45,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';let tw=ctx.measureText(name).width,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='#273b30';ctx.strokeStyle='#fff';ctx.lineWidth=4;ctx.strokeText(name,x,y);ctx.fillText(name,x,y);ctx.font='10px Inter';ctx.fillStyle='#7a857f';ctx.strokeText(fmt(n.count)+' assemblies',x,y+15);ctx.fillText(fmt(n.count)+' assemblies',x,y+15);}
62
+ if(selected){const a=selected;ctx.strokeStyle='#15392c';ctx.lineWidth=1.8;ctx.beginPath();let p=screen(a.points[0]);ctx.moveTo(...p);for(let j=1;j<a.points.length;j++){let [c,d]=controls(a.points[j-1],a.points[j]);ctx.bezierCurveTo(...screen(c),...screen(d),...screen(a.points[j]));}ctx.stroke();let q=screen(a.tip);ctx.fillStyle='#15392c';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){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(){zoom=1;pan={x:0,y:0};selected=null;$('#selection').hidden=true;schedule()}
68
+ function nearest(x,y){
69
+ const [wx,wy]=world(x,y),pad=7/(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];
71
+ if(wx<Math.min(ax,bx)-pad||wx>Math.max(ax,bx)+pad||wy<Math.min(ay,by)-pad||wy>Math.max(ay,by)+pad)continue;
72
+ let dx=bx-ax,dy=by-ay,l=dx*dx+dy*dy,t=l?Math.max(0,Math.min(1,((wx-ax)*dx+(wy-ay)*dy)/l)):0;
73
+ let d=(wx-ax-t*dx)**2+(wy-ay-t*dy)**2;
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 selectAssembly(a){if(!a)return;selected=a;let r=a.row;$('#selection').hidden=false;$('#sel-name').textContent=a.name;$('#sel-id').textContent=a.accession;$('#sel-id').href='https://www.ncbi.nlm.nih.gov/datasets/genome/'+a.accession+'/';$('#sel-lineage').textContent=a.lineage.filter(n=>['kingdom','phylum','class','order','family','genus'].includes(n.rank)).map(n=>n.common||n.name).join(' › ');
79
+ const has=mode?r[5]:r[4]===1;
80
+ $('#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>Existing annotation listed<br><small>${esc(r[7])} · ${esc(r[9])}</small>`:'<i class="dot green"></i>No annotation listed in this GenBank snapshot';
81
+ $('#sel-data').innerHTML=(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>`:'');
82
+ $('#sel-loci').hidden=a.accession!=='GCA_000001405.29';schedule()}
83
+ 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':'Existing annotation listed';$('#green-label').textContent=mode?'Outside that comparison corpus':'No annotation listed';$('#scope-note').textContent=mode?'Exact paired-accession matches to the project’s RefSeq comparison corpus. This is not all of RefSeq; green does not mean a gene is new.':'Existing annotations: a named annotation provider in the NCBI GenBank assembly summary. A missing record does not establish that an assembly was never annotated.';if(selected)selectAssembly(selected);schedule()}
84
+ 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.';}
85
+ 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="#edf0ed"/><text x="${x(p)}" y="24" text-anchor="middle">${p/1e6} Mb</text>`)}
86
+ 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="#81d8ae"/>`);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="${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.';
87
+ }
88
+ 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 document.fonts.ready;setupGL();resize();updateLegend();$('#loading').remove();$('#shown').textContent=fmt(leaves.length);$('#taxa-shown').textContent=fmt(D.meta.taxa);$('#search').disabled=false;
89
+ new ResizeObserver(resize).observe(stage);
90
+ 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});
91
+ let down=null,moved=false;canvas.addEventListener('pointerdown',e=>{down={x:e.clientX,y:e.clientY};moved=false;canvas.setPointerCapture(e.pointerId);canvas.style.cursor='grabbing'});canvas.addEventListener('pointermove',e=>{if(!down)return;let dx=e.clientX-down.x,dy=e.clientY-down.y;if(Math.abs(dx)+Math.abs(dy)>2)moved=true;pan.x+=dx;pan.y+=dy;down={x:e.clientX,y:e.clientY};schedule()});canvas.addEventListener('pointerup',e=>{if(!moved){let b=canvas.getBoundingClientRect();selectAssembly(nearest(e.clientX-b.left,e.clientY-b.top))}down=null;canvas.style.cursor='grab'});canvas.addEventListener('pointercancel',()=>down=null);
92
+ canvas.addEventListener('dblclick',e=>{let b=canvas.getBoundingClientRect();zoomAt(3,e.clientX-b.left,e.clientY-b.top)});
93
+ $('#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=()=>{selected=null;$('#selection').hidden=true;schedule()};$('#sel-focus').onclick=()=>focusAssembly(selected,100);$('#about').onclick=()=>$('#about-dialog').showModal();$('#about-close').onclick=()=>$('#about-dialog').close();
94
+ }catch(e){$('#loading').textContent='Unable to load the tree: '+e.message;console.error(e)}}
95
+ init();
build.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import base64,json,gzip
3
+ ROOT=Path(__file__).parent
4
+ D=json.loads((ROOT/'data/tree-data.json').read_text()); meta=json.loads((ROOT/'data/human-metadata.json').read_text());genes=[];last=None;count=0
5
+ with gzip.open(ROOT/'data/human-annotations.gff3.gz','rt') as f:
6
+ for line in f:
7
+ if line.startswith('#'):continue
8
+ r=line.rstrip().split('\t')
9
+ if r[2]=='gene':count+=1
10
+ if r[0]!='CM000663.2' or int(r[3])>1000000:continue
11
+ if r[2]=='gene':
12
+ attr=dict(x.split('=',1) for x in r[8].split(';') if '=' in x)
13
+ last={'id':attr['ID'],'start':int(r[3]),'end':int(r[4]),'strand':r[6],'cds':[]};genes.append(last)
14
+ if r[2]=='CDS' and last:last['cds'].append([int(r[3]),int(r[4])])
15
+ assert count==meta['gene_count'],(count,meta['gene_count'])
16
+ D['example']={'accession':meta['genbank_accession'],'gene_count':meta['gene_count'],'genes':genes}
17
+ 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'])
18
+ payload=base64.b64encode(gzip.compress(json.dumps(D,separators=(',',':'),ensure_ascii=False).encode(),mtime=0)).decode()
19
+ s=(ROOT/'template.html').read_text().replace('__FONTS__',css).replace('__DATA__',payload).replace('__SCRIPT__',(ROOT/'app.js').read_text())
20
+ (ROOT/'index.html').write_text(s)
21
+ (ROOT/'data/example-genes.json').write_text(json.dumps(D['example'],indent=2))
22
+ print('html',len(s),'real genes',count,'window loci',len(genes))
carbon-a-strand-poc.png ADDED

Git LFS Details

  • SHA256: 58ac3d2343a1e19d2a53aedb4ed0bda6b5cf71a5dc9a575cc8cae69b076db47f
  • Pointer size: 132 Bytes
  • Size of remote file: 3.52 MB
carbon-a-strand-source.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5b01380db7827213d2390aacc6ee95773c877e6986dc729c056e0f19d4aeec62
3
+ size 13552312
check.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ROOT=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',headless=True,args=['--no-sandbox','--enable-webgl','--use-gl=angle','--use-angle=swiftshader','--enable-unsafe-swiftshader'])
8
+ page=b.new_page(viewport={'width':1500,'height':1050},device_scale_factor=1)
9
+ errors=[];page.on('pageerror',lambda e:errors.append(str(e)))
10
+ start=time.time();page.goto((ROOT/'index.html').as_uri());page.wait_for_function('window.POC && !document.querySelector("#loading")',timeout=90000);page.wait_for_timeout(1000)
11
+ print('loaded',round(time.time()-start,2),page.evaluate('({assemblies:POC.assemblies,vertices:POC.vertices,prior:POC.prior,refseq:POC.refseq})'),flush=True)
12
+ page.screenshot(path=str(ROOT/'overview.png'))
13
+ page.locator('#detail').click();page.wait_for_timeout(800);page.screenshot(path=str(ROOT/'detail.png'))
14
+ page.locator('#sel-close').click(); bounds=page.locator('#stage').bounding_box(); page.mouse.click(bounds['x']+bounds['width']/2,bounds['y']+bounds['height']/2); assert page.locator('#sel-id').inner_text()=='GCA_000001405.29'; page.locator('#sel-loci').click();page.wait_for_timeout(200);page.screenshot(path=str(ROOT/'genes.png'));page.locator('#gene-close').click()
15
+ page.locator('#reset').click();page.locator('#scope').select_option('1');page.locator('#existing-only').check();page.wait_for_timeout(500);page.screenshot(path=str(ROOT/'refseq.png'))
16
+ page.locator('#search').fill('Drosophila melanogaster');page.wait_for_timeout(100);print('search results',page.locator('#results button').count(),flush=True);page.locator('#results button').first.click();print('selected',page.locator('#sel-id').inner_text(),flush=True)
17
+ assert page.locator('#blue-count').inner_text()=='1,323'
18
+ assert page.evaluate('POC.assemblies')==33588
19
+ assert page.evaluate('POC.prior')==4238
20
+ page.locator('#reset').click(); bounds=page.locator('#stage').bounding_box(); page.mouse.move(bounds['x']+bounds['width']*.65,bounds['y']+bounds['height']*.6); page.mouse.wheel(0,-600); page.wait_for_timeout(250); assert page.evaluate('POC.zoom')>1
21
+ page.mouse.down(); page.mouse.move(bounds['x']+bounds['width']*.65+40,bounds['y']+bounds['height']*.6+30,steps=3); page.mouse.up(); page.wait_for_timeout(100)
22
+ assert page.evaluate('POC.leaves.every(a => a.lineage[0].id===2759)')
23
+ assert page.evaluate('new Set(POC.leaves.map(a=>a.accession)).size')==33588
24
+ assert not errors,errors
25
+ page.set_viewport_size({'width':430,'height':932});page.locator('#reset').click();page.wait_for_timeout(300);page.screenshot(path=str(ROOT/'mobile.png'),full_page=True)
26
+ print('errors',errors,flush=True);b.close()
detail.png ADDED

Git LFS Details

  • SHA256: 0b89c97fa826c714190a9762d2741e572fabe8b61731746295260cbc27349748
  • Pointer size: 131 Bytes
  • Size of remote file: 138 kB
export_still.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sys
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',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':1370},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}main{display:block;height:1280px;min-height:1280px}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:#6e7b73;margin:0}.poster-footer{position:absolute;bottom:20px;left:50px;right:50px;border-top:1px solid #e6ece8;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:#00b86b;display:inline-block}.poster-key i.blue{background:#4167ec}.poster-footer p{font-size:11px;line-height:1.7;color:#6e7b73;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 real assemblies, connected through the NCBI taxonomy.</p></div><div class="poster-footer"><div class="poster-key"><span><i></i>29,350 with no annotation record listed</span><span><i class="blue"></i>4,238 with existing 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>A missing record does not establish that an assembly was never annotated. Branch lengths do not represent evolutionary time.</p></div>`)}''')
12
+ 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 ADDED

Git LFS Details

  • SHA256: 0eb213fe31bf31ca1b5277586b5f9cc2daa7932ffe6280bb1033fe0b0321085c
  • Pointer size: 131 Bytes
  • Size of remote file: 270 kB
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
overview.png ADDED

Git LFS Details

  • SHA256: ebb4d014d436ec682fe2d7c3cb42e1ca912823421c7178cfaf6abff64a2689e5
  • Pointer size: 131 Bytes
  • Size of remote file: 788 kB
prepare_data.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import sqlite3,json,csv,gzip,hashlib
3
+ from collections import Counter
4
+ ROOT=Path(__file__).parent; D=ROOT/'data'
5
+ con=sqlite3.connect(D/'taxonomy.sqlite')
6
+ taxa={r[0]:r for r in con.execute('SELECT taxid,parent_id,name,rank FROM taxa')}
7
+ assemblies=dict(con.execute('SELECT accession,taxid FROM assemblies'))
8
+ ref={r['assembly_accession'] for r in csv.DictReader(open(D/'refseq_assembly_taxids.csv'))}
9
+ common=json.loads((D/'common_names.json').read_text())['names']
10
+ metadata={}
11
+ source=Path('/home/node/local/git/genbank-annotation-explorer/.cache/coverage/assembly_summary_genbank.txt')
12
+ with source.open() as f:
13
+ for line in f:
14
+ if line.startswith('##'): continue
15
+ if line.startswith('#'): header=line[1:].strip().split('\t'); continue
16
+ a=line.split('\t',1)[0]
17
+ if a not in assemblies: continue
18
+ r=dict(zip(header,line.rstrip('\n').split('\t')))
19
+ def num(k):
20
+ try:return int(r.get(k,''))
21
+ except:return None
22
+ annotation=bool(r.get('annotation_provider') not in ('na','',None))
23
+ metadata[a]={'organism':r['organism_name'],'bases':num('genome_size'),'paired':r['gbrs_paired_asm'] if r['gbrs_paired_asm']!='na' else '', 'pair_match':r['paired_asm_comp'],'prior':int(annotation),'ref':int(r['gbrs_paired_asm'] in ref),'provider':r['annotation_provider'] if annotation else '', 'annotation_date':r['annotation_date'] if annotation else '', 'prior_genes':num('protein_coding_gene_count') if annotation else None}
24
+ print('metadata',len(metadata),flush=True)
25
+ # Keep the real taxonomy topology; omit taxa without displayed descendants.
26
+ used=set()
27
+ for t in assemblies.values():
28
+ seen=set()
29
+ while t in taxa and t not in seen:
30
+ seen.add(t);used.add(t)
31
+ if t==2759:break
32
+ t=taxa[t][1]
33
+ # Sanity: all leaves belong to Eukaryota.
34
+ assert 1 not in used
35
+ rows=[]
36
+ for a,t in sorted(assemblies.items()):
37
+ m=metadata.get(a,{'prior':-1,'ref':0})
38
+ rows.append([a,t,m.get('organism',taxa[t][2]),m.get('bases'),m.get('prior',-1),m.get('ref',0),m.get('paired',''),m.get('provider',''),m.get('prior_genes'),m.get('annotation_date',''),m.get('pair_match','')])
39
+ node_rows=[[t,0 if t==2759 else taxa[t][1],taxa[t][2],taxa[t][3],common.get(str(t),'')] for t in sorted(used)]
40
+ out={'meta':{'title':'Carbon-A assembly strands — proof of concept','root':2759,'assemblies':len(rows),'taxa':len(set(assemblies.values())),'release':{'assemblies':48167,'taxa':22617,'loci':566340000,'bases':27180000000000},'snapshot':'Explorer taxonomy and covered-assembly mapping, 7 Oct 2026','prior_annotation':'NCBI GenBank assembly summary, cached 7 Oct 2026; named annotation provider required','refseq':'Exact paired GCF accession match to the project’s 2,055-assembly RefSeq comparison corpus; not all of RefSeq','classification_counts':dict(Counter(r[4] for r in rows)),'refseq_matches':sum(r[5] for r in rows),'layout':'NCBI taxonomy; branch lengths are for layout, not evolutionary distance. Each strand is one assembly. Equal strand widths encode assembly counts, not gene counts.'},'node_fields':['taxid','parent','scientific_name','rank','common_name'],'nodes':node_rows,'assembly_fields':['accession','taxid','organism','bases','prior_annotation','refseq_corpus','paired_refseq','annotation_provider','prior_protein_coding_genes','annotation_date','pair_comparison'],'assemblies':rows}
41
+ raw=json.dumps(out,separators=(',',':'),ensure_ascii=False).encode()
42
+ (D/'tree-data.json').write_bytes(raw)
43
+ (D/'tree-data.json.gz').write_bytes(gzip.compress(raw,mtime=0))
44
+ print(json.dumps(out['meta'],indent=2)); print('bytes',len(raw),'gzip',(D/'tree-data.json.gz').stat().st_size,'nodes',len(node_rows))
provenance.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "snapshot": {
3
+ "title": "Carbon-A assembly strands \u2014 proof of concept",
4
+ "root": 2759,
5
+ "assemblies": 33588,
6
+ "taxa": 16976,
7
+ "release": {
8
+ "assemblies": 48167,
9
+ "taxa": 22617,
10
+ "loci": 566340000,
11
+ "bases": 27180000000000
12
+ },
13
+ "snapshot": "Explorer taxonomy and covered-assembly mapping, 7 Oct 2026",
14
+ "prior_annotation": "NCBI GenBank assembly summary, cached 7 Oct 2026; named annotation provider required",
15
+ "refseq": "Exact paired GCF accession match to the project\u2019s 2,055-assembly RefSeq comparison corpus; not all of RefSeq",
16
+ "classification_counts": {
17
+ "1": 4238,
18
+ "0": 29350
19
+ },
20
+ "refseq_matches": 1323,
21
+ "layout": "NCBI taxonomy; branch lengths are for layout, not evolutionary distance. Each strand is one assembly. Equal strand widths encode assembly counts, not gene counts."
22
+ },
23
+ "sources": {
24
+ "taxonomy_repository": "https://huggingface.co/spaces/HuggingFaceBio/genbank-annotation-explorer",
25
+ "taxonomy_repository_commit": "c6b2b4ac351eaf9680e8141d89bf1f6e76633670",
26
+ "genbank_summary": "https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_genbank.txt",
27
+ "genbank_summary_cache_date": "2026-10-07",
28
+ "human_metadata": "HuggingFaceBio/genbank-annotations/cds/vertebrate_mammalian/GCA_000001405.29/metadata.json",
29
+ "human_gff3": "HuggingFaceBio/genbank-annotations/cds/vertebrate_mammalian/GCA_000001405.29/annotations.gff3.gz",
30
+ "human_pipeline_commit": "6ffa159",
31
+ "refseq_comparison": "HF_GENERanno_organized/analysis/training_data_stats/refseq_assembly_taxids.csv",
32
+ "release_totals": "User-supplied values, 2026-10-08"
33
+ },
34
+ "sha256": {
35
+ "taxonomy.sqlite": "88e8b10a82026dfe21a3022ae788e50d82a4ca521f948a29f4a219e111b185b1",
36
+ "common_names.json": "b46396e6db5fc2326617d9dc20e0f219bbbe79ef3b9329ac2e287671ea3c4812",
37
+ "refseq_assembly_taxids.csv": "336a74474136597863c08188d4c8bc3113057e3409e665a1e937838c7b4f071b",
38
+ "human-metadata.json": "a4b825732a128d0745cfc6b0fa0b8c28c1909bd5a6c1f04eec48ee9584d0b76a",
39
+ "human-annotations.gff3.gz": "c7fa0b8df740641718f65f65a0ef7c9d76c14cf537070d5e6222071c36a68087",
40
+ "tree-data.json": "ee3b3b9373bc9dc9109febef8d8027405b0da0d7fb21a0aea69cac06e42e6dad",
41
+ "example-genes.json": "d9640190ee771eb099df5bb1e059a837552bfbdeb734b1daa929d7a76cec98d7"
42
+ },
43
+ "important_limits": [
44
+ "This is the 33,588-accession explorer subset, not the complete 48,167-assembly release.",
45
+ "No per-assembly coding-locus distribution has been inferred.",
46
+ "Existing annotation evidence is at assembly level, not evidence that an individual Carbon-A locus is novel.",
47
+ "RefSeq comparison corpus is a filtered project corpus, not all RefSeq.",
48
+ "Green indicates no listed match under the chosen comparison, not confirmed absence of previous annotation."
49
+ ]
50
+ }
refseq.png ADDED

Git LFS Details

  • SHA256: 1540725ea980c817dfa2a60c4be792b6f5e1ce28ef3f046b3cb7705945878d6d
  • Pointer size: 131 Bytes
  • Size of remote file: 426 kB
template.html ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Carbon-A — Assembly strands</title><style>
2
+ __FONTS__
3
+ :root{--green:#00b86b;--blue:#4167ec;--ink:#14221c;--muted:#6e7b73;--rule:#e6ece8}*{box-sizing:border-box}body{margin:0;color:var(--ink);background:#fff;font-family:Inter,Arial,sans-serif;font-size:13px}button,input,select{font:inherit}button,a,input,select{-webkit-tap-highlight-color:transparent}button{cursor:pointer;color:inherit;background:#fff;border:1px solid var(--rule);padding:9px 13px}button:hover{border-color:#80978a;background:#f6faf7}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--green);outline-offset:3px}button:disabled{opacity:.35;cursor:default}a{color:inherit}header{height:87px;padding:0 40px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rule)}.brand{font-family:Manrope,Arial,sans-serif;font-weight:800;font-size:26px;letter-spacing:-1px}.brand em{font-style:normal;color:var(--green)}.tag{font-size:10px;letter-spacing:1.3px;color:var(--muted);text-transform:uppercase}.header-right{display:flex;gap:24px;align-items:center}main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 87px);min-height:720px}aside{padding:34px 28px 20px 40px;position:relative;z-index:3;border-right:1px solid var(--rule);overflow-y:auto}h1{font-family:Manrope,Arial,sans-serif;font-size:32px;letter-spacing:-1.5px;line-height:1.12;font-weight:700;margin:0 0 16px}.intro{font-size:13px;line-height:1.6;color:var(--muted);margin:0 0 24px}.release{border-top:1px solid var(--rule);padding-top:19px;margin-bottom:24px}.release .big{font-family:Manrope;font-size:38px;letter-spacing:-1.7px;font-weight:700;margin-top:10px;line-height:1.1}.release .big em{font-style:normal;color:var(--green)}.release .sub{font-size:12px;color:var(--muted);margin-top:5px}.stats{display:grid;grid-template-columns:1fr 1fr;gap:14px 18px;margin-top:20px}.stats b{font-family:Manrope;font-size:21px;display:block;letter-spacing:-.6px}.stats span{font-size:10px;color:var(--muted)}.stat-wide{grid-column:1/-1}.legend{border-top:1px solid var(--rule);padding-top:20px}.legend select{width:100%;border:0;border-bottom:1px solid var(--rule);padding:7px 0 9px;margin:6px 0 12px;background:#fff;font-size:12px;font-weight:500}.key{display:flex;align-items:center;gap:8px;font-size:11px;margin-bottom:10px}.key strong{margin-left:auto;font-weight:500;font-variant-numeric:tabular-nums}.sample-line{height:2px;width:21px;display:inline-block;background:var(--green);flex-shrink:0}.sample-line.blue{background:var(--blue)}.scope-note{font-size:10px;color:var(--muted);line-height:1.5;margin:10px 0 13px}.checkbox{font-size:11px;display:flex;gap:7px;align-items:center}.checkbox input{accent-color:var(--blue)}.subset{font-size:10px;line-height:1.6;color:var(--muted);margin-top:20px;padding-top:16px;border-top:1px solid var(--rule)}.subset b{color:#3a4d42;font-weight:500}.quick{display:flex;gap:6px;margin:20px 0 6px}.quick button{font-size:11px;padding:8px 10px}.stage-wrap{position:relative;min-width:0;overflow:hidden}#stage{position:absolute;inset:64px 14px 44px}canvas{position:absolute;width:100%;height:100%;inset:0}#tree{cursor:grab;touch-action:none}#overlay{pointer-events:none}.topbar{position:absolute;top:18px;left:25px;right:25px;display:flex;align-items:center;justify-content:space-between;gap:10px;z-index:4}.searchbox{position:relative;width:280px}#search{width:100%;padding:10px 12px;border:1px solid var(--rule);background:#fff;font-size:11px}#results{position:absolute;top:39px;width:100%;background:#fff;box-shadow:0 10px 30px #10291a0d;font-size:11px}#results:empty{display:none}#results button{display:block;width:100%;text-align:left;padding:11px;border:0;border-bottom:1px solid var(--rule)}#results strong,#results span{display:block;font-size:11px;font-weight:500}#results span{font-size:10px;color:var(--muted);margin-top:3px}.one-line{font-size:11px;color:var(--muted)}.controls{display:flex;align-items:center;gap:4px;position:absolute;right:26px;bottom:21px;z-index:3}.controls button{padding:7px 12px}.controls span{font-size:10px;padding:0 7px;min-width:42px;text-align:center;color:var(--muted)}.help{position:absolute;bottom:25px;left:25px;font-size:10px;line-height:1.7;color:var(--muted);pointer-events:none}.help b{color:#435a4c;font-weight:500}#selection{position:absolute;right:22px;top:69px;width:295px;background:#fff;border:1px solid #dfe8e2;box-shadow:0 8px 40px #2035280c;z-index:5;padding:23px}#sel-close{position:absolute;top:6px;right:8px;border:0;background:transparent;font-size:20px;padding:0 4px;color:var(--muted)}#sel-name{font-size:18px;letter-spacing:-.5px;font-family:Manrope;margin:0 0 6px;line-height:1.25}#sel-id{font-size:11px;color:var(--green);font-weight:500;text-decoration:none}#sel-lineage{font-size:10px;line-height:1.7;color:var(--muted);margin-top:15px}#sel-status{font-size:11px;line-height:1.6;border-top:1px solid var(--rule);padding-top:13px;margin-top:14px}#sel-status small{font-size:10px;color:var(--muted);margin-left:13px}.dot{display:inline-block;width:6px;height:6px;background:var(--green);border-radius:50%;margin-right:6px}.dot.blue{background:var(--blue)}#sel-data{display:grid;grid-template-columns:1fr;gap:3px;margin:16px 0;font-size:10px}#sel-data span{color:var(--muted);margin-top:7px}#sel-data b{font-weight:500;font-size:12px}#selection button:not(#sel-close){font-size:10px;margin:4px 0;padding:8px 10px}#loading{position:absolute;inset:0;display:grid;place-content:center;background:white;color:var(--muted);z-index:10}dialog{border:1px solid var(--rule);padding:36px;max-width:calc(100vw - 40px);width:1000px;color:var(--ink)}dialog::backdrop{background:#10271930;backdrop-filter:blur(3px)}dialog h2{font-family:Manrope;font-size:28px;letter-spacing:-1px;margin:0 0 7px}dialog p{font-size:13px;line-height:1.7;color:var(--muted)}.dialog-close{float:right;border:0;font-size:22px;padding:0 4px}.gene-label{font-size:11px;margin-top:28px;color:var(--muted)}#gene-svg{width:100%;height:auto;font:11px Inter;fill:#718279;margin:16px 0}.gene-notes{border-top:1px solid var(--rule);padding-top:15px;font-size:11px}#about-dialog{width:680px}#about-dialog p{font-size:12px}#about-dialog ul{padding-left:18px;font-size:12px;line-height:1.9;color:var(--muted)}
4
+ @media(min-width:1600px){main{grid-template-columns:365px 1fr}aside{padding:40px}.release .big{font-size:44px}h1{font-size:38px}}
5
+ @media(max-width:850px){header{padding:0 20px;height:72px}.brand{font-size:22px}.header-right .tag{display:none}main{height:auto;min-height:0;display:flex;flex-direction:column}aside{padding:25px 22px;border-right:0;border-bottom:1px solid var(--rule);overflow:visible}h1{font-size:30px;max-width:420px}.intro{max-width:520px;margin-bottom:18px}.release{display:none}.legend{padding-top:10px;border-top:0;max-width:520px}.legend .tag{display:none}.scope-note{max-width:500px}.subset{margin-top:15px;padding-top:10px}.quick{margin-top:15px}.stage-wrap{height:740px}.topbar{left:20px;right:20px}.one-line{display:none}#stage{inset:68px 0 85px}.help{left:20px;bottom:62px}.controls{left:20px;right:auto}#selection{right:15px;top:68px;width:275px}dialog{padding:25px}.header-right{gap:10px}}
6
+ </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></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><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">Highlight existing annotations</label><select id="scope"><option value="0">NCBI 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 existing annotations</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><p class="subset"><b>This PoC draws <span id="shown">33,588</span> verified assemblies across <span id="taxa-shown">16,976</span> taxa.</b><br>The reusable explorer snapshot is a subset of the full release. Strand width represents assemblies; the release gene total is not yet distributed across branches.</p></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><section id="selection" hidden><button id="sel-close" aria-label="Close assembly details">×</button><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>Scroll to zoom · Drag to pan · Click a strand</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: 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>Per-assembly Carbon-A locus counts are not complete in this snapshot. They have not been estimated from genome size or from existing annotations.</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><script id="dataset" type="application/octet-stream">__DATA__</script><script>__SCRIPT__</script></body></html>