lvwerra HF Staff commited on
Commit
fbc4cf6
·
verified ·
1 Parent(s): 02fb0ca

Use click selection and anywhere deselection; restore sweep details and visible whole-tree reset

Browse files
.gitattributes CHANGED
@@ -51,3 +51,4 @@ dial-selection.png filter=lfs diff=lfs merge=lfs -text
51
  download-detail.png filter=lfs diff=lfs merge=lfs -text
52
  presentation-desktop.png filter=lfs diff=lfs merge=lfs -text
53
  presentation-mobile.png filter=lfs diff=lfs merge=lfs -text
 
 
51
  download-detail.png filter=lfs diff=lfs merge=lfs -text
52
  presentation-desktop.png filter=lfs diff=lfs merge=lfs -text
53
  presentation-mobile.png filter=lfs diff=lfs merge=lfs -text
54
+ sweep-info.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -27,27 +27,27 @@ The human assembly detail uses actual published GFF3 coordinates, with 23,767 pr
27
 
28
  ## Locus bars and atlas silhouettes
29
 
30
- The outer annulus aligns to the exact same ordered assembly tips as the tree. Published counts are available for **17,828 of the 33,588 displayed assemblies**, summing to **212,905,534 predicted coding loci**; the other 15,760 assemblies have unavailable counts in this metadata inventory. The default sums available locus counts across **5 consecutive assemblies**; 20-, 50- and 200-assembly bins and individual assembly bars are selectable in View options. Bins partition taxonomic display order, and can straddle named taxon boundaries. Bar length uses a linear scale, recalculated for each bin size and labelled above the tree. Hover reveals the count and number of measured assemblies. Clicking a bin switches to individual bars and zooms into its strands.
31
 
32
  Grey marks at the bar bases indicate missing counts. For bins, the grey mark spans the fraction of assemblies without measurements. A bin sums only available values: missing counts are not imputed, treated as measured zero, or scaled up to the full release. Green locus bars count all predicted loci in the covered assemblies; the blue/green colouring of the tree still describes annotation records at assembly level, not whether individual loci are new.
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 in View options 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
  ## Presentation and video capture
37
 
38
  The Space uses a single wide title, a centred full-width tree and five resource buttons: **Model, Database, Explorer, Paper and Blog**. The sidebar, animation button and all cumulative sweep counters have been removed. Search, bar settings, comparison modes and zoom controls are available under **View options**. **About the data** retains the subset limitations and icon credits.
39
 
40
- Click the page background, then press **Space** to start or replay a four-second clockwise sweep; Space again or Escape stops it. A thin centre hand follows the selected strand. Playback has no aggregate counters. Only the interaction canvas redraws each frame; the detailed tree stays cached.
41
 
42
- For browser automation, `POC.startSweep()` and `POC.stopSweep(false)` provide the same controls. On the app URL, `?record=1` hides the small utility controls and suppresses mouse interactions for a clean recording; adding `&animate=1` starts one sweep a second after loading. Resource links and the title remain visible. These parameters apply to the app itself; the normal private Space page remains the entry point for signed-in users.
43
 
44
  ## Layout and implementation
45
 
46
  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.
47
 
48
- 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 freezes the selected strand and its details. Hovering or clicking elsewhere inside the tree cannot replace it. Click outside the outer tree/locus ring, click the close button, or press Escape to release it. 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.
49
 
50
- 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. Hover anywhere inside the opening to select by direction; click to freeze, or drag to turn the hand before releasing to freeze the final selection. Dragging outside the dial pans the tree. Fine locus bars use shared explicit angular vertices instead of independently approximated Canvas arcs, avoiding gaps in narrow bins without changing their counts.
51
 
52
  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.
53
 
@@ -65,6 +65,6 @@ This layout change retains the 33,588 real assembly strands and available per-as
65
 
66
  ## Rebuild
67
 
68
- `python build.py` builds the self-contained HTML from the template, JavaScript, compact data and embedded release fonts. `python check_presentation.py` checks desktop/mobile layout, resource buttons, search, frozen selection, annotation access, themes and the counter-free sweep. `python export_still.py` exports the current presentation as a 2160 × 2700 PNG. `python verify_live.py` repeats the current checks against the published private Space.
69
 
70
  Earlier exploratory checks are retained for reference; their sidebar and counter expectations describe previous revisions. Data provenance and limitations are recorded in `provenance.json`. The original release video and database explorer are separate projects.
 
27
 
28
  ## Locus bars and atlas silhouettes
29
 
30
+ The outer annulus aligns to the exact same ordered assembly tips as the tree. Published counts are available for **17,828 of the 33,588 displayed assemblies**, summing to **212,905,534 predicted coding loci**; the other 15,760 assemblies have unavailable counts in this metadata inventory. The default sums available locus counts across **5 consecutive assemblies**; 20-, 50- and 200-assembly bins and individual assembly bars are selectable in View options. Bins partition taxonomic display order, and can straddle named taxon boundaries. Bar length uses a linear scale, recalculated for each bin size and labelled above the tree. Hover reveals the count and number of measured assemblies. Clicking a bar selects the assembly at its angle without changing the camera or bin size.
31
 
32
  Grey marks at the bar bases indicate missing counts. For bins, the grey mark spans the fraction of assemblies without measurements. A bin sums only available values: missing counts are not imputed, treated as measured zero, or scaled up to the full release. Green locus bars count all predicted loci in the covered assemblies; the blue/green colouring of the tree still describes annotation records at assembly level, not whether individual loci are new.
33
 
34
+ The original **Genome Atlas silhouettes**, without circle backgrounds, sit on their corresponding taxonomic nodes. Clicking an icon selects a nearby assembly strand without zooming. Icons appear progressively with zoom and are hidden when they would overlap; the checkbox in View options 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
  ## Presentation and video capture
37
 
38
  The Space uses a single wide title, a centred full-width tree and five resource buttons: **Model, Database, Explorer, Paper and Blog**. The sidebar, animation button and all cumulative sweep counters have been removed. Search, bar settings, comparison modes and zoom controls are available under **View options**. **About the data** retains the subset limitations and icon credits.
39
 
40
+ Click the page background, then press **Space** to start or replay a four-second clockwise sweep; Space again or Escape stops it. A thin centre hand follows the selected strand, and its assembly info box updates during playback. The final assembly remains selected after the sweep. Playback has no aggregate counters. Only the interaction canvas redraws each frame; the detailed tree stays cached.
41
 
42
+ For browser automation, `POC.startSweep()` and `POC.stopSweep(false)` provide the same controls. On the app URL, `?record=1` hides the small utility controls and suppresses mouse interactions for a clean recording while retaining the animated assembly info box; adding `&animate=1` starts one sweep a second after loading. Resource links and the title remain visible. These parameters apply to the app itself; the normal private Space page remains the entry point for signed-in users.
43
 
44
  ## Layout and implementation
45
 
46
  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.
47
 
48
+ 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. Clicking a strand, the centre dial, an icon or a bar selects a real assembly and opens its details without zooming. Hovering does not select a strand. Once an assembly is selected, the next click anywhere clears it, including clicks on another strand, the centre, an icon, a bar, the title, the footer or the info box. Links and buttons inside the box execute their action before clearing. Scroll or use explicit zoom controls to zoom, and drag to pan. A **Whole tree** button appears directly on the graphic whenever zoomed or panned, and resets both. Double-clicking does not zoom. Selection highlights reuse the exact sampled coordinates drawn by WebGL, so they stay aligned with the underlying strand at every zoom level.
49
 
50
+ 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. Click inside the opening to select by direction. Dragging pans the tree. Fine locus bars use shared explicit angular vertices instead of independently approximated Canvas arcs, avoiding gaps in narrow bins without changing their counts.
51
 
52
  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.
53
 
 
65
 
66
  ## Rebuild
67
 
68
+ `python build.py` builds the self-contained HTML from the template, JavaScript, compact data and embedded release fonts. `python check_presentation.py` checks desktop/mobile layout, resource buttons, search, click selection and deselection, zoom reset, annotation access, themes and the counter-free sweep. `python export_still.py` exports the current presentation as a 2160 × 2700 PNG. `python verify_live.py` repeats the current checks against the published private Space.
69
 
70
  Earlier exploratory checks are retained for reference; their sidebar and counter expectations describe previous revisions. Data provenance and limitations are recorded in `provenance.json`. The original release video and database explorer are separate projects.
app.js CHANGED
@@ -62,14 +62,14 @@ function draw(){if(!gl)return;let dpr=canvas.width/W;gl.viewport(0,0,canvas.widt
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
  // Select the strand in the pointer's direction anywhere inside the root opening.
71
- // A small neutral hub avoids unstable angles right at the pivot. Dial drags can
72
- // continue beyond the opening without turning into a pan gesture.
73
  function hitDial(x,y,dragging=false){
74
  const [wx,wy]=world(x,y),r=Math.hypot(wx,wy);
75
  if((!dragging&&r>92)||r*baseScale*zoom<3)return null;
@@ -89,53 +89,62 @@ function nearest(x,y,radius=7){
89
  }return found;
90
  }
91
  function closeViewOptions(){if($('#view-dialog').open)$('#view-dialog').close()}
 
92
  function focusAssembly(a,z=70){closeViewOptions();stopSweep(false);selected=a;zoom=z;let s=baseScale*zoom;pan={x:-a.tip[0]*s,y:-a.tip[1]*s};selectAssembly(a);schedule()}
93
  function clearSelection(){selected=pinnedAssembly=hoveredAssembly=null;$('#selection').hidden=true;scheduleInteraction()}
94
- function hoverAssembly(a){
95
- if(sweep.running||pinnedAssembly)return;
96
- hoveredAssembly=a;const next=a||pinnedAssembly;
97
- if(next===selected)return;
98
- if(next)selectAssembly(next,false);else{selected=null;$('#selection').hidden=true;scheduleInteraction()}
99
- }
100
- function selectAssembly(a,pin=true){if(!a)return;if(pin)pinnedAssembly=a;selected=a;
101
  $('#selection').dataset.pinned=String(a===pinnedAssembly);
102
- $('#sel-state').textContent=a===pinnedAssembly?'Frozen · click outside tree to release':'Hover preview · click to freeze';let r=a.row,species=a.lineage.find(n=>n.rank==='species');$('#selection').hidden=false;$('#sel-name').textContent=a.name;$('#sel-id').textContent=a.accession;$('#sel-id').href='https://www.ncbi.nlm.nih.gov/datasets/genome/'+a.accession+'/';$('#sel-lineage').textContent=a.lineage.filter(n=>['kingdom','phylum','class','order','family','genus'].includes(n.rank)).map(n=>n.common||n.name).join(' › ');
103
  const has=mode?r[5]:r[4]===1;
104
  $('#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';
105
  $('#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>`:'');
106
- updateDataAccess(a);$('#sel-loci').hidden=a.accession!=='GCA_000001405.29';scheduleInteraction()}
107
  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()}
108
- 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.';}
109
  function geneModal(){closeViewOptions();stopSweep();const g=D.example;$('#gene-dialog').showModal();$('#gene-summary').textContent=fmt(g.gene_count)+' candidate coding loci in this assembly';const svg=$('#gene-svg');let start=0,end=1000000,width=1100,left=30,right=1080;let x=p=>left+(p-start)/(end-start)*(right-left);let parts=[];for(let p=0;p<=end;p+=100000){parts.push(`<line x1="${x(p)}" y1="38" x2="${x(p)}" y2="198" stroke="${palette.grid}"/><text x="${x(p)}" y="24" text-anchor="middle">${p/1e6} Mb</text>`)}
110
  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.';
111
  }
112
  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;
113
  new ResizeObserver(resize).observe(stage);
114
  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});
115
- let down=null,moved=false,hoverPending=false,hoverEvent=null;
116
  const queueHover=e=>{hoverEvent=e;if(hoverPending)return;hoverPending=true;requestAnimationFrame(()=>{hoverPending=false;const event=hoverEvent;hoverEvent=null;if(!down&&event)hoverLayers(event)})};
117
- stage.addEventListener('pointerdown',e=>{stopSweep(false);hoverEvent=null;const b=stage.getBoundingClientRect(),a=pinnedAssembly?null:hitDial(e.clientX-b.left,e.clientY-b.top);down={x:e.clientX,y:e.clientY,dial:!!a,locked:!!pinnedAssembly};moved=false;if(a){selectAssembly(a,false);canvas.style.cursor='pointer'}stage.setPointerCapture(e.pointerId)});
 
 
 
 
 
118
  stage.addEventListener('pointermove',e=>{
119
  if(!down){queueHover(e);return}
120
- let dx=e.clientX-down.x,dy=e.clientY-down.y;
121
- if(down.dial){const b=stage.getBoundingClientRect(),a=hitDial(e.clientX-b.left,e.clientY-b.top,true);if(a&&a!==selected)selectAssembly(a,false);moved=moved||Math.hypot(dx,dy)>=4;return}
122
  if(!moved&&Math.hypot(dx,dy)<4)return;
123
- if(!moved)hoverAssembly(null);
124
  moved=true;canvas.style.cursor='grabbing';$('#layer-tooltip').hidden=true;
125
- pan.x+=dx;pan.y+=dy;down={...down,x:e.clientX,y:e.clientY};schedule();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  });
127
- stage.addEventListener('pointerup',e=>{if(!down)return;const dragged=moved,dial=down.dial,b=stage.getBoundingClientRect(),x=e.clientX-b.left,y=e.clientY-b.top;if(dial){const a=hitDial(x,y,true);if(a)selectAssembly(a)}else if(!dragged){if(!insideTree(x,y))clearSelection();else if(!down.locked&&!clickLayers(x,y)){const a=hitAssembly(x,y);if(a)selectAssembly(a)}}down=null;canvas.style.cursor=hitDial(x,y)?'pointer':'default';if(!dial&&!dragged&&e.pointerType!=='touch')hoverLayers(e)});
128
- stage.addEventListener('pointercancel',()=>{down=null;hoverAssembly(null);canvas.style.cursor='default'});
129
- 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()});
130
- document.addEventListener('pointerdown',e=>{if(pinnedAssembly&&!e.target.closest('#stage,#selection,dialog'))clearSelection()});
131
  document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!document.querySelector('dialog[open]'))clearSelection()});
132
- $('#selection').addEventListener('pointerleave',e=>{if(e.relatedTarget!==canvas)hoverAssembly(null)});
133
  $('#sel-download').onclick=()=>showDataAccess(selected);$('#data-close').onclick=()=>$('#data-dialog').close();$('#data-copy').onclick=copyDataCode;
134
  $('#theme-toggle').onclick=()=>setTheme(theme==='dark'?'light':'dark');
135
  $('#view-options').onclick=()=>{stopSweep(false);$('#view-dialog').showModal()};$('#view-close').onclick=closeViewOptions;
136
  $('#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();
137
- stage.addEventListener('dblclick',e=>{let b=canvas.getBoundingClientRect(),x=e.clientX-b.left,y=e.clientY-b.top;if(!hitDial(x,y))zoomAt(3,x,y)});
138
- $('#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();
139
  const params=new URLSearchParams(location.search);
140
  if(params.get('animate')==='1')setTimeout(startSweep,1000);
141
  }catch(e){if($('#loading'))$('#loading').textContent='Unable to load the tree: '+e.message;console.error(e)}}
 
62
  drawAtlasIcons();
63
  if(sweep.running)cacheSweepTree();
64
  drawInteraction();
65
+ const away=zoom!==1||Math.abs(pan.x)>1||Math.abs(pan.y)>1;$('#home').hidden=!away;$('.stage-wrap').dataset.zoomed=String(away);
66
  $('#zoom-readout').textContent=zoom.toFixed(1)+'×';$('#zoomout').disabled=zoom<=1;$('#zoomin').disabled=zoom>=320;
67
  window.POC&&(window.POC.zoom=zoom);
68
  }
69
+ function zoomAt(factor,x=W/2,y=H/2){stopSweep();$('#layer-tooltip').hidden=true;hoverBin=null;let old=zoom,n=Math.max(1,Math.min(320,zoom*factor));let ratio=n/old;pan.x=(pan.x+W/2-x)*ratio+x-W/2;pan.y=(pan.y+H/2-y)*ratio+y-H/2;zoom=n;if(zoom===1)pan={x:0,y:0};schedule()}
70
  function reset(){stopSweep(false);$('#layer-tooltip').hidden=true;hoverBin=null;zoom=1;pan={x:0,y:0};clearSelection();schedule()}
71
  // Select the strand in the pointer's direction anywhere inside the root opening.
72
+ // A small neutral hub avoids unstable angles right at the pivot.
 
73
  function hitDial(x,y,dragging=false){
74
  const [wx,wy]=world(x,y),r=Math.hypot(wx,wy);
75
  if((!dragging&&r>92)||r*baseScale*zoom<3)return null;
 
89
  }return found;
90
  }
91
  function closeViewOptions(){if($('#view-dialog').open)$('#view-dialog').close()}
92
+ function chooseAssembly(a){closeViewOptions();stopSweep(false);selectAssembly(a)}
93
  function focusAssembly(a,z=70){closeViewOptions();stopSweep(false);selected=a;zoom=z;let s=baseScale*zoom;pan={x:-a.tip[0]*s,y:-a.tip[1]*s};selectAssembly(a);schedule()}
94
  function clearSelection(){selected=pinnedAssembly=hoveredAssembly=null;$('#selection').hidden=true;scheduleInteraction()}
95
+ function selectAssembly(a,pin=true,redraw=true){if(!a)return;if(pin)pinnedAssembly=a;selected=a;
 
 
 
 
 
 
96
  $('#selection').dataset.pinned=String(a===pinnedAssembly);
97
+ $('#sel-state').textContent=sweep.running?'Scanning assemblies':'Selected · click anywhere to clear';let r=a.row,species=a.lineage.find(n=>n.rank==='species');$('#selection').hidden=false;$('#sel-name').textContent=a.name;$('#sel-id').textContent=a.accession;$('#sel-id').href='https://www.ncbi.nlm.nih.gov/datasets/genome/'+a.accession+'/';$('#sel-lineage').textContent=a.lineage.filter(n=>['kingdom','phylum','class','order','family','genus'].includes(n.rank)).map(n=>n.common||n.name).join(' › ');
98
  const has=mode?r[5]:r[4]===1;
99
  $('#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';
100
  $('#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>`:'');
101
+ updateDataAccess(a);$('#sel-loci').hidden=a.accession!=='GCA_000001405.29';if(redraw)scheduleInteraction()}
102
  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()}
103
+ function search(q){q=q.trim().toLowerCase();let out=[];if(q.length>1){out=leaves.filter(a=>a.accession.toLowerCase().includes(q)||a.name.toLowerCase().includes(q)||(nodes.get(a.taxid).common||'').toLowerCase().includes(q)).slice(0,7);}$('#results').replaceChildren(...out.map(a=>{let b=document.createElement('button');b.innerHTML=`<strong>${esc(a.name)}</strong><span>${esc(a.accession)}</span>`;b.onclick=()=>{chooseAssembly(a);$('#results').replaceChildren();$('#search').value=''};return b}));if(q.length>1&&!out.length)$('#results').textContent='No match in this PoC subset.';}
104
  function geneModal(){closeViewOptions();stopSweep();const g=D.example;$('#gene-dialog').showModal();$('#gene-summary').textContent=fmt(g.gene_count)+' candidate coding loci in this assembly';const svg=$('#gene-svg');let start=0,end=1000000,width=1100,left=30,right=1080;let x=p=>left+(p-start)/(end-start)*(right-left);let parts=[];for(let p=0;p<=end;p+=100000){parts.push(`<line x1="${x(p)}" y1="38" x2="${x(p)}" y2="198" stroke="${palette.grid}"/><text x="${x(p)}" y="24" text-anchor="middle">${p/1e6} Mb</text>`)}
105
  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.';
106
  }
107
  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;
108
  new ResizeObserver(resize).observe(stage);
109
  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});
110
+ let down=null,moved=false,suppressChartClick=false,clickPoint=null,hoverPending=false,hoverEvent=null;
111
  const queueHover=e=>{hoverEvent=e;if(hoverPending)return;hoverPending=true;requestAnimationFrame(()=>{hoverPending=false;const event=hoverEvent;hoverEvent=null;if(!down&&event)hoverLayers(event)})};
112
+ stage.addEventListener('pointerdown',e=>{
113
+ if(e.button!==0)return;
114
+ hoverEvent=null;suppressChartClick=false;clickPoint=null;
115
+ down={x:e.clientX,y:e.clientY};moved=false;
116
+ stage.setPointerCapture(e.pointerId);
117
+ });
118
  stage.addEventListener('pointermove',e=>{
119
  if(!down){queueHover(e);return}
120
+ const dx=e.clientX-down.x,dy=e.clientY-down.y;
 
121
  if(!moved&&Math.hypot(dx,dy)<4)return;
122
+ if(!moved)stopSweep(false);
123
  moved=true;canvas.style.cursor='grabbing';$('#layer-tooltip').hidden=true;
124
+ pan.x+=dx;pan.y+=dy;down={x:e.clientX,y:e.clientY};schedule();
125
+ });
126
+ stage.addEventListener('pointerup',e=>{if(!down)return;clickPoint={x:e.clientX,y:e.clientY};suppressChartClick=moved;down=null;canvas.style.cursor='default'});
127
+ stage.addEventListener('pointercancel',()=>{down=null;suppressChartClick=true;canvas.style.cursor='default'});
128
+ stage.addEventListener('pointerleave',()=>{hoverEvent=null;$('#layer-tooltip').hidden=true;if(hoverBin){hoverBin=null;schedule()}if(!down)canvas.style.cursor='default'});
129
+ // Snapshot before target handlers run so actions inside the info box can use
130
+ // the selected assembly before this same click clears it.
131
+ document.addEventListener('click',e=>{e.hadAssemblySelection=!!pinnedAssembly},true);
132
+ document.addEventListener('click',e=>{
133
+ if(e.target.closest('#stage')){
134
+ if(suppressChartClick){suppressChartClick=false;return}
135
+ stopSweep(false);$('#layer-tooltip').hidden=true;
136
+ if(e.hadAssemblySelection){clearSelection();return}
137
+ const box=stage.getBoundingClientRect(),p=clickPoint||{x:e.clientX,y:e.clientY},x=p.x-box.left,y=p.y-box.top;clickPoint=null;
138
+ const a=assemblyAtClick(x,y);if(a)selectAssembly(a);else clearSelection();
139
+ }else if(e.hadAssemblySelection){clearSelection()}
140
  });
 
 
 
 
141
  document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!document.querySelector('dialog[open]'))clearSelection()});
 
142
  $('#sel-download').onclick=()=>showDataAccess(selected);$('#data-close').onclick=()=>$('#data-dialog').close();$('#data-copy').onclick=copyDataCode;
143
  $('#theme-toggle').onclick=()=>setTheme(theme==='dark'?'light':'dark');
144
  $('#view-options').onclick=()=>{stopSweep(false);$('#view-dialog').showModal()};$('#view-close').onclick=closeViewOptions;
145
  $('#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();
146
+ $('#home').onclick=reset;
147
+ $('#zoomin').onclick=()=>zoomAt(2);$('#zoomout').onclick=()=>zoomAt(.5);$('#reset').onclick=reset;$('#search').addEventListener('input',e=>search(e.target.value));$('#search').addEventListener('keydown',e=>{if(e.key==='Escape')$('#results').replaceChildren();if(e.key==='Enter')$('#results button')?.click()});$('#scope').onchange=e=>{mode=Number(e.target.value);updateLegend()};$('#existing-only').onchange=e=>{view=e.target.checked?'existing':'all';schedule()};$('#human').onclick=()=>chooseAssembly(leaves.find(a=>a.accession==='GCA_000001405.29'));$('#detail').onclick=()=>focusAssembly(leaves.find(a=>a.accession==='GCA_000001405.29'),95);$('#sel-loci').onclick=geneModal;$('#gene-open').onclick=geneModal;$('#gene-close').onclick=()=>$('#gene-dialog').close();$('#sel-close').onclick=()=>{clearSelection();schedule()};$('#sel-focus').onclick=()=>focusAssembly(selected,100);$('#about').onclick=()=>{stopSweep();$('#about-dialog').showModal()};$('#about-close').onclick=()=>$('#about-dialog').close();
148
  const params=new URLSearchParams(location.search);
149
  if(params.get('animate')==='1')setTimeout(startSweep,1000);
150
  }catch(e){if($('#loading'))$('#loading').textContent='Unable to load the tree: '+e.message;console.error(e)}}
carbon-a-strand-source.zip CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9de0766b160cc3dcf0552de0d4f4af06f11dd79adbe46644359de982e819b115
3
- size 15180642
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:09d40a78b0d612469caf0b4e285b020f225dd4639c76d8a613fdbb3156c30132
3
+ size 15181594
check_presentation.py CHANGED
@@ -22,24 +22,50 @@ def check(url=None,headers=None):
22
  assert page.locator('#view-dialog').is_hidden()
23
  assert page.locator('footer').bounding_box()['y']>=page.locator('.stage-wrap').bounding_box()['y']+page.locator('.stage-wrap').bounding_box()['height']
24
  page.screenshot(path=str(R/'presentation-desktop.png'))
25
- # Hover, frozen selection, and outside release still use the same geometry.
26
- def point(i):
27
- return page.evaluate('''i=>{const q=screen(polar(62,leaves[i].angle)),b=stage.getBoundingClientRect();return [b.x+q[0],b.y+q[1]]}''',i)
28
- page.mouse.move(*point(12345));page.wait_for_function('selected?.i===12345')
29
- assert page.locator('#selection').is_visible()
30
- page.mouse.click(*point(12345));page.mouse.move(*point(25000));page.wait_for_timeout(60)
31
- assert page.evaluate('pinnedAssembly.i')==12345
32
- page.mouse.click(*point(25000));assert page.evaluate('selected.i')==12345
33
- box=page.locator('#stage').bounding_box();page.mouse.click(box['x']+2,box['y']+2)
34
- assert page.evaluate('pinnedAssembly===null')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # Search is accessible through the dialog; selecting a result closes it.
36
  page.locator('#view-options').click();page.locator('#search').fill('GCA_000001405.29')
37
  page.locator('#results button').first.click()
38
  assert page.locator('#view-dialog').is_hidden()
39
  assert page.locator('#sel-id').inner_text()=='GCA_000001405.29'
 
40
  assert page.locator('#sel-files').get_attribute('href').endswith('/GCA_000001405.29')
41
  page.locator('#sel-download').click();compile(page.locator('#data-code').inner_text(),'download_example','exec')
42
- page.locator('#data-close').click();page.locator('#sel-close').click()
 
43
  page.locator('#view-options').click();page.locator('#scope').select_option('1');page.locator('#locus-mode').select_option('20')
44
  assert page.evaluate('mode')==1 and page.evaluate('locusMode')=='20'
45
  page.locator('#scope').select_option('0');page.locator('#locus-mode').select_option('5');page.locator('#reset').click();page.locator('#view-close').click()
@@ -53,22 +79,25 @@ def check(url=None,headers=None):
53
  page.reload(wait_until='domcontentloaded')
54
  page.wait_for_function('window.POC && !document.querySelector("#loading")',timeout=60000)
55
  # Keyboard playback, smooth full turn, exact highlighted geometry and no UI counters.
56
- page.evaluate('''()=>{document.activeElement.blur();window.framesSeen=[];window.treeRedraws=0;const oldDraw=gl.drawArrays.bind(gl);gl.drawArrays=(...a)=>{treeRedraws++;oldDraw(...a)};const oldUpdate=updateSweep;updateSweep=t=>{oldUpdate(t);framesSeen.push({t:performance.now(),p:sweep.progress,angle:sweep.angle,selected:selected?.i??null})}}''')
57
  page.keyboard.press('Space');page.wait_for_function('sweep.running && sweep.progress>.1')
58
- assert page.locator('#selection').is_hidden()
59
  try: page.wait_for_function('sweep.status==="complete"',timeout=12000)
60
  except Exception:
61
  print('sweep failure',page.evaluate('({sweep,hidden:document.hidden,frames:window.framesSeen?.length,focus:document.activeElement.tagName,now:performance.now(),redraws:treeRedraws,pending,zoom})'),errors,flush=True);page.screenshot(path=str(R/'sweep-failure.png'));print('after screenshot',page.evaluate('sweep'),flush=True);raise
62
- result=page.evaluate('({frames:framesSeen,redraws:treeRedraws,angle:sweep.angle,selected,zoom})')
63
  frames=result['frames'];duration=frames[-1]['t']-frames[1]['t']
64
  assert 3900<=duration<4700,duration
65
  assert len(frames)>35,len(frames)
66
  assert result['redraws']<6,result['redraws']
67
  assert abs(result['angle']-1.5*math.pi)<1e-9
68
- assert result['selected'] is None and result['zoom']==1
 
 
 
69
  assert all(a['angle']<=b['angle'] for a,b in zip(frames,frames[1:]))
70
  page.keyboard.press('Space');page.wait_for_function('sweep.progress>.1 && sweep.running');page.keyboard.press('Escape')
71
- assert not page.evaluate('sweep.running')
72
  # A phone layout has no horizontal overflow and retains all resource buttons.
73
  page.evaluate('reset()');page.mouse.move(0,0);page.set_viewport_size({'width':390,'height':844})
74
  page.wait_for_timeout(150)
@@ -76,9 +105,16 @@ def check(url=None,headers=None):
76
  assert page.locator('footer').bounding_box()['y']+page.locator('footer').bounding_box()['height']<=845
77
  assert all(x.is_visible() for x in page.locator('.resource-links a').all())
78
  page.screenshot(path=str(R/'presentation-mobile.png'))
 
 
 
 
 
 
 
79
  assert not errors,errors
80
  browser.close()
81
- return {'sweep_duration_ms':round(duration),'sweep_frames':len(frames),'background_redraws':result['redraws'],'layout':'desktop and mobile passed','hover_freeze_and_release':'passed','search_and_downloads':'passed','resource_links':5,'counter_and_button':'removed','errors':errors}
82
 
83
  if __name__=='__main__':
84
  result=check();(R/'presentation-verification.json').write_text(json.dumps(result,indent=2));print(json.dumps(result))
 
22
  assert page.locator('#view-dialog').is_hidden()
23
  assert page.locator('footer').bounding_box()['y']>=page.locator('.stage-wrap').bounding_box()['y']+page.locator('.stage-wrap').bounding_box()['height']
24
  page.screenshot(path=str(R/'presentation-desktop.png'))
25
+ # Hover never selects. Every chart click selects without zooming, and the
26
+ # next click anywhere clears without immediately selecting a replacement.
27
+ def point(i,radius=62):
28
+ return page.evaluate("""([i,radius])=>{const q=screen(polar(radius,leaves[i].angle)),b=stage.getBoundingClientRect();return [b.x+q[0],b.y+q[1]]}""",[i,radius])
29
+ page.mouse.move(*point(12345));page.wait_for_timeout(80)
30
+ assert page.evaluate('selected===null') and page.locator('#selection').is_hidden()
31
+ page.mouse.click(*point(12345));assert page.evaluate('pinnedAssembly.i')==12345
32
+ assert page.locator('#selection').is_visible() and page.evaluate('zoom')==1
33
+ page.mouse.move(*point(25000));assert page.evaluate('pinnedAssembly.i')==12345
34
+ page.mouse.click(*point(25000));assert page.evaluate('selected===null')
35
+ assert page.locator('#selection').is_hidden()
36
+ page.mouse.click(*point(25000));assert page.evaluate('pinnedAssembly.i')==25000
37
+ page.locator('h1').click();assert page.evaluate('selected===null')
38
+ # Plain strands, icons and binned bars all use selection, without zoom or
39
+ # changing bar mode. Clicking info-box text also deselects.
40
+ targets=[point(12345,730)]
41
+ targets+=page.evaluate("""()=>{const r=stage.getBoundingClientRect(),ic=iconHits[0],bin=locusBins.find(b=>b.known&&b.value>10000),q=screen(polar(R+BAR_GAP+4,leaves[Math.floor(bin.mid)].angle));return [[r.x+ic.x,r.y+ic.y],[r.x+q[0],r.y+q[1]]]}""")
42
+ for xy in targets:
43
+ page.mouse.click(*xy);assert page.evaluate('selected!==null')
44
+ assert page.evaluate('zoom')==1 and page.evaluate('locusMode')=='5'
45
+ page.locator('#sel-name').click();assert page.evaluate('selected===null')
46
+ page.mouse.dblclick(*point(12345,730));assert page.evaluate('zoom')==1
47
+ assert page.evaluate('selected===null')
48
+ # Wheel zoom and pan have an always-accessible reset on the graphic.
49
+ page.mouse.move(*point(10000));page.mouse.wheel(0,-450)
50
+ page.wait_for_function('zoom>1 && !document.querySelector("#home").hidden')
51
+ page.locator('#home').click();page.wait_for_function('zoom===1 && pan.x===0 && pan.y===0')
52
+ page.locator('#home').wait_for(state='hidden')
53
+ box=page.locator('#stage').bounding_box()
54
+ page.mouse.move(box['x']+20,box['y']+30);page.mouse.down()
55
+ page.mouse.move(box['x']+75,box['y']+60,steps=4);page.mouse.up()
56
+ page.wait_for_function('!document.querySelector("#home").hidden')
57
+ assert page.evaluate('selected===null')
58
+ page.locator('#home').click();assert page.evaluate('pan.x===0 && pan.y===0')
59
  # Search is accessible through the dialog; selecting a result closes it.
60
  page.locator('#view-options').click();page.locator('#search').fill('GCA_000001405.29')
61
  page.locator('#results button').first.click()
62
  assert page.locator('#view-dialog').is_hidden()
63
  assert page.locator('#sel-id').inner_text()=='GCA_000001405.29'
64
+ assert page.evaluate('zoom')==1
65
  assert page.locator('#sel-files').get_attribute('href').endswith('/GCA_000001405.29')
66
  page.locator('#sel-download').click();compile(page.locator('#data-code').inner_text(),'download_example','exec')
67
+ assert page.evaluate('selected===null') # the info action runs before clearing
68
+ page.locator('#data-close').click()
69
  page.locator('#view-options').click();page.locator('#scope').select_option('1');page.locator('#locus-mode').select_option('20')
70
  assert page.evaluate('mode')==1 and page.evaluate('locusMode')=='20'
71
  page.locator('#scope').select_option('0');page.locator('#locus-mode').select_option('5');page.locator('#reset').click();page.locator('#view-close').click()
 
79
  page.reload(wait_until='domcontentloaded')
80
  page.wait_for_function('window.POC && !document.querySelector("#loading")',timeout=60000)
81
  # Keyboard playback, smooth full turn, exact highlighted geometry and no UI counters.
82
+ page.evaluate('''()=>{document.activeElement.blur();window.framesSeen=[];window.treeRedraws=0;const oldDraw=gl.drawArrays.bind(gl);gl.drawArrays=(...a)=>{treeRedraws++;oldDraw(...a)};const oldUpdate=updateSweep;updateSweep=t=>{oldUpdate(t);framesSeen.push({t:performance.now(),p:sweep.progress,angle:sweep.angle,selected:selected?.i??null,info:document.querySelector('#sel-id').textContent,expected:selected?.accession})}}''')
83
  page.keyboard.press('Space');page.wait_for_function('sweep.running && sweep.progress>.1')
84
+ assert page.locator('#selection').is_visible()
85
  try: page.wait_for_function('sweep.status==="complete"',timeout=12000)
86
  except Exception:
87
  print('sweep failure',page.evaluate('({sweep,hidden:document.hidden,frames:window.framesSeen?.length,focus:document.activeElement.tagName,now:performance.now(),redraws:treeRedraws,pending,zoom})'),errors,flush=True);page.screenshot(path=str(R/'sweep-failure.png'));print('after screenshot',page.evaluate('sweep'),flush=True);raise
88
+ result=page.evaluate('({frames:framesSeen,redraws:treeRedraws,angle:sweep.angle,selected:selected?.i,pinned:pinnedAssembly?.i,zoom})')
89
  frames=result['frames'];duration=frames[-1]['t']-frames[1]['t']
90
  assert 3900<=duration<4700,duration
91
  assert len(frames)>35,len(frames)
92
  assert result['redraws']<6,result['redraws']
93
  assert abs(result['angle']-1.5*math.pi)<1e-9
94
+ assert result['selected']==33587 and result['pinned']==33587 and result['zoom']==1
95
+ assert all(f['info']==f['expected'] for f in frames), 'Info must match the highlighted strand every frame'
96
+ assert page.locator('#selection').is_visible()
97
+ page.screenshot(path=str(R/'sweep-info.png'))
98
  assert all(a['angle']<=b['angle'] for a,b in zip(frames,frames[1:]))
99
  page.keyboard.press('Space');page.wait_for_function('sweep.progress>.1 && sweep.running');page.keyboard.press('Escape')
100
+ assert not page.evaluate('sweep.running') and page.evaluate('selected===null')
101
  # A phone layout has no horizontal overflow and retains all resource buttons.
102
  page.evaluate('reset()');page.mouse.move(0,0);page.set_viewport_size({'width':390,'height':844})
103
  page.wait_for_timeout(150)
 
105
  assert page.locator('footer').bounding_box()['y']+page.locator('footer').bounding_box()['height']<=845
106
  assert all(x.is_visible() for x in page.locator('.resource-links a').all())
107
  page.screenshot(path=str(R/'presentation-mobile.png'))
108
+ page.mouse.click(*point(11000));assert page.locator('#selection').is_visible()
109
+ page.locator('h1').click();assert page.evaluate('selected===null')
110
+ # Capture mode must retain the info box too.
111
+ page.evaluate("document.documentElement.dataset.recording='true';POC.startSweep()")
112
+ page.wait_for_function('sweep.progress>.1 && sweep.running')
113
+ assert page.locator('#selection').is_visible()
114
+ page.evaluate('POC.stopSweep(false)')
115
  assert not errors,errors
116
  browser.close()
117
+ return {'sweep_duration_ms':round(duration),'sweep_frames':len(frames),'background_redraws':result['redraws'],'layout':'desktop and mobile passed','click_selection_and_anywhere_deselection':'passed','clicks_do_not_zoom':'passed','visible_home_reset':'passed','sweep_info_matches_strand':'passed','search_and_downloads':'passed','resource_links':5,'counter_and_button':'removed','errors':errors}
118
 
119
  if __name__=='__main__':
120
  result=check();(R/'presentation-verification.json').write_text(json.dumps(result,indent=2));print(json.dumps(result))
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
layers.js CHANGED
@@ -79,17 +79,22 @@ 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(sweep.running)return;if(pinnedAssembly){$('#layer-tooltip').hidden=true;canvas.style.cursor='default';return;}if(document.querySelector('dialog[open]'))return;let box=stage.getBoundingClientRect(),x=e.clientX-box.left,y=e.clientY-box.top,dial=hitDial(x,y),ic=dial?null:hitIcon(x,y),bin=dial?null:hitBar(x,y),tip=$('#layer-tooltip');
83
- if(!ic&&!bin){tip.hidden=true;if(hoverBin){hoverBin=null;schedule()}const a=hitAssembly(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);
86
  if(!ic&&locusMode==='raw'){tip.hidden=true;if(hoverBin!==bin){hoverBin=bin;schedule()}return}
87
- if(ic){tip.innerHTML=`<b>${esc(ic.n.common||ic.n.name)}</b><span>${esc(ic.n.name)} · ${fmt(ic.n.count)} assemblies</span><span>${fmt(mode?ic.n.refMatches:ic.n.existing)} ${mode?'match the RefSeq comparison corpus':'have GenBank annotation records'}</span><small>Assembly counts, not gene counts · click to explore</small>`}
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;const wrap=$('.stage-wrap').getBoundingClientRect();tip.style.left=box.left-wrap.left+Math.max(8,Math.min(W-tip.offsetWidth-8,x+16))+'px';tip.style.top=box.top-wrap.top+Math.max(8,Math.min(H-tip.offsetHeight-8,y+15))+'px';
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;
 
 
 
 
 
 
95
  }
 
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(pinnedAssembly){$('#layer-tooltip').hidden=true;canvas.style.cursor='pointer';return;}if(document.querySelector('dialog[open]'))return;let box=stage.getBoundingClientRect(),x=e.clientX-box.left,y=e.clientY-box.top,dial=hitDial(x,y),ic=dial?null:hitIcon(x,y),bin=dial?null:hitBar(x,y),tip=$('#layer-tooltip');
83
+ if(!ic&&!bin){tip.hidden=true;if(hoverBin){hoverBin=null;schedule()}const a=hitAssembly(x,y,3);canvas.style.cursor=a?'pointer':'default';return}
84
  canvas.style.cursor='pointer';
 
85
  if(!ic&&locusMode==='raw'){tip.hidden=true;if(hoverBin!==bin){hoverBin=bin;schedule()}return}
86
+ if(ic){tip.innerHTML=`<b>${esc(ic.n.common||ic.n.name)}</b><span>${esc(ic.n.name)} · ${fmt(ic.n.count)} assemblies</span><span>${fmt(mode?ic.n.refMatches:ic.n.existing)} ${mode?'match the RefSeq comparison corpus':'have GenBank annotation records'}</span><small>Assembly counts, not gene counts · click to select a strand</small>`}
87
+ 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 select an assembly'}</small>`;if(hoverBin!==bin){hoverBin=bin;schedule()}}
88
  tip.hidden=false;const wrap=$('.stage-wrap').getBoundingClientRect();tip.style.left=box.left-wrap.left+Math.max(8,Math.min(W-tip.offsetWidth-8,x+16))+'px';tip.style.top=box.top-wrap.top+Math.max(8,Math.min(H-tip.offsetHeight-8,y+15))+'px';
89
  }
90
+ // Icons and bar bins resolve to real strands without changing the camera.
91
+ function assemblyAtClick(x,y){
92
+ const dial=hitDial(x,y);if(dial)return dial;
93
+ const bin=hitBar(x,y);
94
+ if(bin){const p=world(x,y);let angle=Math.atan2(p[1],p[0]);while(angle<START)angle+=TAU;
95
+ const i=Math.max(bin.lo,Math.min(bin.hi-1,Math.floor((angle-START)/SPAN*leaves.length)));
96
+ return leaves[i];
97
+ }
98
+ const icon=hitIcon(x,y);
99
+ return nearest(x,y,7)||(icon?leaves[Math.round(icon.n.mid)]:null);
100
  }
presentation-mobile.png CHANGED

Git LFS Details

  • SHA256: c8bb6a25d205dc799f5ea44d1f6c37a1d7eccc00357c61c90c536ce04cecb330
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB

Git LFS Details

  • SHA256: ad76654c48834bd1f918e8f163bf4c7016b73b3e2007f3f120930c8fc0d17685
  • Pointer size: 131 Bytes
  • Size of remote file: 151 kB
presentation-verification.json CHANGED
@@ -1,9 +1,12 @@
1
  {
2
- "sweep_duration_ms": 4008,
3
- "sweep_frames": 73,
4
  "background_redraws": 0,
5
  "layout": "desktop and mobile passed",
6
- "hover_freeze_and_release": "passed",
 
 
 
7
  "search_and_downloads": "passed",
8
  "resource_links": 5,
9
  "counter_and_button": "removed",
 
1
  {
2
+ "sweep_duration_ms": 4013,
3
+ "sweep_frames": 125,
4
  "background_redraws": 0,
5
  "layout": "desktop and mobile passed",
6
+ "click_selection_and_anywhere_deselection": "passed",
7
+ "clicks_do_not_zoom": "passed",
8
+ "visible_home_reset": "passed",
9
+ "sweep_info_matches_strand": "passed",
10
  "search_and_downloads": "passed",
11
  "resource_links": 5,
12
  "counter_and_button": "removed",
provenance.json CHANGED
@@ -71,7 +71,8 @@
71
  "presentation": {
72
  "updated_at": "2026-10-12",
73
  "layout": "Wide title, centered tree and five resource buttons; no sidebar",
74
- "animation": "4-second sweep; no counters or visible playback button; keyboard Space or POC.startSweep()",
75
- "data_changed": false
 
76
  }
77
  }
 
71
  "presentation": {
72
  "updated_at": "2026-10-12",
73
  "layout": "Wide title, centered tree and five resource buttons; no sidebar",
74
+ "animation": "4-second sweep with matching assembly info each frame; final assembly remains selected; no counters or playback button.",
75
+ "data_changed": false,
76
+ "interaction": "Click to select; next click anywhere clears. Icons, bars, double-clicks and search do not zoom. Visible Whole tree resets zoom/pan."
77
  }
78
  }
sweep-info.png ADDED

Git LFS Details

  • SHA256: 157a9eadf0d8b10ae0c2dee814e6ebcd8f6147e2b70c2d2e8351265b95c9fe6a
  • Pointer size: 131 Bytes
  • Size of remote file: 612 kB
sweep.js CHANGED
@@ -41,7 +41,8 @@ function updateSweep(progress){
41
  const t=sweep.progress,eased=t*t*(3-2*t);
42
  sweep.angle=NOON+TAU*eased;
43
  const n=leaves.length,count=Math.max(0,Math.min(n,Math.floor((sweep.angle-START)/SPAN*n+.5)));
44
- selected=count&&sweep.angle<=START+SPAN?leaves[count-1]:null;
 
45
  }
46
 
47
  function updateSweepState(){
@@ -70,7 +71,8 @@ function startSweep(){
70
  updateSweep((now-sweep.startedAt)/SWEEP_DURATION);drawInteraction();
71
  if(sweep.progress<1){sweep.frame=requestAnimationFrame(step);return}
72
  sweep.running=false;sweep.status='complete';sweep.frame=0;
73
- selected=pinnedAssembly=hoveredAssembly=null;
 
74
  updateSweepState();
75
  $('#sweep-announcement').textContent='Sweep complete.';
76
  drawInteraction();
@@ -81,6 +83,6 @@ function stopSweep(pin=true){
81
  if(!sweep.running)return;
82
  cancelAnimationFrame(sweep.frame);sweep.frame=0;sweep.running=false;sweep.status='stopped';
83
  updateSweepState();$('#sweep-announcement').textContent='Sweep stopped.';
84
- if(pin&&selected)selectAssembly(selected);else selected=pinnedAssembly=hoveredAssembly=null;
85
  scheduleInteraction();
86
  }
 
41
  const t=sweep.progress,eased=t*t*(3-2*t);
42
  sweep.angle=NOON+TAU*eased;
43
  const n=leaves.length,count=Math.max(0,Math.min(n,Math.floor((sweep.angle-START)/SPAN*n+.5)));
44
+ const a=leaves[Math.max(0,Math.min(n-1,count-1))];
45
+ if(a!==selected||$('#selection').hidden)selectAssembly(a,false,false);
46
  }
47
 
48
  function updateSweepState(){
 
71
  updateSweep((now-sweep.startedAt)/SWEEP_DURATION);drawInteraction();
72
  if(sweep.progress<1){sweep.frame=requestAnimationFrame(step);return}
73
  sweep.running=false;sweep.status='complete';sweep.frame=0;
74
+ // Leave the final assembly visible so its details can be read after playback.
75
+ if(selected)selectAssembly(selected,true,false);
76
  updateSweepState();
77
  $('#sweep-announcement').textContent='Sweep complete.';
78
  drawInteraction();
 
83
  if(!sweep.running)return;
84
  cancelAnimationFrame(sweep.frame);sweep.frame=0;sweep.running=false;sweep.status='stopped';
85
  updateSweepState();$('#sweep-announcement').textContent='Sweep stopped.';
86
+ if(pin&&selected)selectAssembly(selected);else clearSelection();
87
  scheduleInteraction();
88
  }
template.html CHANGED
@@ -9,6 +9,7 @@ h1{font-family:Manrope,Arial,sans-serif;font-size:clamp(29px,3.4vw,55px);font-we
9
  .wordmark{font-weight:800;white-space:nowrap}.wordmark em{font-style:normal;color:var(--green)}.title-separator{color:var(--muted);font-weight:400;margin:0 .24em}
10
  .stage-wrap{position:relative;min-width:0;min-height:0;width:100%;max-width:1600px;justify-self:center;overflow:hidden}
11
  #stage{position:absolute;inset:6px 24px 0;touch-action:none}canvas{position:absolute;width:100%;height:100%;inset:0}#tree{cursor:default;touch-action:none}#overlay,#interaction,#tree-still{pointer-events:none}#tree-still{visibility:hidden}[data-sweeping="true"] #tree,[data-sweeping="true"] #overlay{visibility:hidden}[data-sweeping="true"] #tree-still{visibility:visible}
 
12
  .resources{padding:12px 24px 18px;text-align:center}
13
  .resource-links{display:flex;justify-content:center;gap:10px;flex-wrap:wrap}
14
  .resource-links a{display:flex;align-items:center;justify-content:space-between;gap:28px;padding:12px 16px;min-width:120px;border:1px solid var(--rule);border-radius:4px;text-decoration:none;font-size:13px;font-weight:500;background:transparent}
@@ -16,7 +17,7 @@ h1{font-family:Manrope,Arial,sans-serif;font-size:clamp(29px,3.4vw,55px);font-we
16
  .utilities{display:flex;justify-content:center;align-items:center;gap:14px;margin-top:9px}
17
  .utilities button{border:0;background:transparent;color:var(--muted);padding:6px 2px;font-size:10px}.utilities button:hover{color:var(--ink)}
18
  #theme-toggle{display:grid;place-items:center;width:26px;height:26px;padding:4px;flex-shrink:0}#theme-toggle svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.5;fill:none}[data-theme="dark"] .theme-moon,[data-theme="light"] .theme-sun{display:none}
19
- [data-recording="true"] .utilities{visibility:hidden}[data-recording="true"] #stage{pointer-events:none}[data-recording="true"] #selection,[data-recording="true"] #layer-tooltip{display:none!important}
20
  .tag{font-size:10px;letter-spacing:1.3px;color:var(--muted);text-transform:uppercase}
21
  #loading{position:absolute;inset:0;display:grid;place-content:center;background:var(--bg);color:var(--muted);z-index:10}
22
  #selection{position:absolute;right:24px;top:18px;width:295px;max-height:calc(100% - 36px);overflow:auto;background:var(--panel);border:1px solid var(--rule);box-shadow:0 8px 40px var(--shadow);z-index:5;padding:23px}
@@ -29,16 +30,17 @@ dialog{background:var(--panel);border:1px solid var(--rule);padding:32px;max-wid
29
  .legend,.locus-controls{border-top:1px solid var(--rule);padding-top:18px;margin-top:18px}.legend select,.locus-controls select{width:100%;border:0;border-bottom:1px solid var(--rule);padding:9px 0;margin:6px 0 12px;background:var(--panel);font-size:12px}.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,.locus-controls p{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(--green)}.locus-controls strong{display:block;font-size:11px;font-weight:500;margin-top:10px}.icon-toggle{margin-top:12px}.quick{display:flex;gap:6px;margin:20px 0 6px}.quick button{font-size:11px;padding:8px 10px}.controls{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:18px}.controls button{font-size:11px;padding:7px 10px}.controls span{font-size:10px;padding:0 5px;min-width:42px;text-align:center;color:var(--muted)}
30
  #icon-credits{max-height:60vh;overflow:auto}#icon-credits table{border-collapse:collapse;width:100%;font-size:11px;text-align:left}#icon-credits th,#icon-credits td{padding:10px 8px;border-bottom:1px solid var(--rule)}#icon-credits a,#data-dialog a{color:var(--green)}.gene-label{font-size:11px;margin-top:28px;color:var(--muted)}#gene-svg{width:100%;height:auto;font:11px Inter;fill:var(--muted);margin:16px 0}.gene-notes{border-top:1px solid var(--rule);padding-top:15px;font-size:11px}#data-dialog pre{max-height:45vh;overflow:auto;border:1px solid var(--rule);background:var(--bg);padding:18px;font-size:11px;line-height:1.7;white-space:pre}#copy-status{font-size:11px;margin-left:12px;color:var(--green)}#data-dialog .data-setup{font-size:11px}
31
  .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}
32
- @media(max-width:700px){.presentation{min-height:650px}.hero{padding:25px 22px 4px}h1{font-size:clamp(26px,5.8vw,38px);letter-spacing:-1px}.title-separator{display:none}.title-topic{display:block;margin-top:6px;font-size:.8em}#stage{inset:0 4px}.resource-links{gap:7px}.resource-links a{min-width:0;padding:10px 11px;gap:12px;font-size:11px}.resources{padding:8px 12px 14px}.utilities{margin-top:8px}#selection{width:275px;right:12px;top:10px;max-height:calc(100% - 20px)}dialog{padding:24px}}
 
33
  @media(prefers-reduced-motion:no-preference){button,.resource-links a{transition:background .15s,border-color .15s}}
34
  </style></head><body>
35
  <main class="presentation">
36
  <header class="hero"><h1><span class="wordmark">Carbon-<em>A</em></span><span class="title-separator" aria-hidden="true"> · </span><span class="title-topic">Across the tree of life</span></h1></header>
37
  <section class="stage-wrap" aria-label="Carbon-A assembly tree">
38
  <span id="sweep-announcement" class="visually-hidden" role="status"></span>
39
- <div id="stage"><canvas id="tree" aria-label="Interactive taxonomic tree of assembly strands. Hover to inspect, click to freeze, and scroll to zoom. Search is in View options."></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 the assembly tree…</div></div>
40
- <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><div class="data-access"><a id="sel-files" target="_blank" rel="noopener">Annotation files ↗</a><br><button id="sel-download">Get annotation data ↗</button><p id="sel-availability"></p></div><button id="sel-focus">Follow this strand ↗</button><button id="sel-loci" hidden>See coding regions →</button></section></section>
41
  <footer class="resources"><nav class="resource-links" aria-label="Carbon-A resources"><a href="https://huggingface.co/HuggingFaceBio/Carbon-A-1.2B" target="_blank" rel="noopener">Model<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations" target="_blank" rel="noopener">Database<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/spaces/HuggingFaceBio/carbon-a-database-explorer" target="_blank" rel="noopener">Explorer<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/buckets/HuggingFaceBio/Carbon-A-Technical-Report" target="_blank" rel="noopener">Paper<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/blog/HuggingFaceBio/carbon-annotator-genbank-genome-annotation" target="_blank" rel="noopener">Blog<span aria-hidden="true">↗</span></a></nav><div class="utilities"><button id="view-options">View options</button><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></footer>
42
  </main>
43
- <dialog id="view-dialog"><button id="view-close" class="dialog-close" aria-label="Close view options">×</button><h2>Explore the tree</h2><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><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><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="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><div class="quick"><button id="human">Find a human assembly ↗</button><button id="gene-open">Its coding regions →</button></div><p>Hover to inspect · Click to freeze · Click outside to release.<br>Scroll to zoom · Drag to pan · Space to animate.</p></dialog>
44
- <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 <span id="shown">33,588</span> strands across <span id="taxa-shown">16,976</span> taxa is a distinct accession from the explorer’s covered-assembly table, mapped through the NCBI taxonomy. This is a subset of the release. 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. Hover or drag inside the centre to select by direction; click to freeze and click outside the tree to release. For video capture, press Space to start or replay a four-second clockwise sweep. Escape stops it. The sweep has no counters.</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/carbon-a-database-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><dialog id="data-dialog"><button id="data-close" class="dialog-close" aria-label="Close download instructions">×</button><h2>Get annotation data</h2><p id="data-assembly"></p><p id="data-description"></p><p><a id="data-browse" target="_blank" rel="noopener">Browse annotation files ↗</a></p><p class="data-setup">Install <code>huggingface_hub</code> and run <code>hf auth login</code> with an account that can access the bucket.</p><pre><code id="data-code"></code></pre><button id="data-copy">Copy Python code</button><span id="copy-status" role="status"></span><p><a href="https://huggingface.co/docs/huggingface_hub/guides/buckets#download-files" target="_blank" rel="noopener">Hugging Face download documentation ↗</a></p></dialog><script id="dataset" type="application/octet-stream">__DATA__</script><script>__SCRIPT__</script></body></html>
 
9
  .wordmark{font-weight:800;white-space:nowrap}.wordmark em{font-style:normal;color:var(--green)}.title-separator{color:var(--muted);font-weight:400;margin:0 .24em}
10
  .stage-wrap{position:relative;min-width:0;min-height:0;width:100%;max-width:1600px;justify-self:center;overflow:hidden}
11
  #stage{position:absolute;inset:6px 24px 0;touch-action:none}canvas{position:absolute;width:100%;height:100%;inset:0}#tree{cursor:default;touch-action:none}#overlay,#interaction,#tree-still{pointer-events:none}#tree-still{visibility:hidden}[data-sweeping="true"] #tree,[data-sweeping="true"] #overlay{visibility:hidden}[data-sweeping="true"] #tree-still{visibility:visible}
12
+ .home-control{position:absolute;left:24px;bottom:14px;z-index:6;display:flex;align-items:center;gap:8px;background:var(--panel);font-size:12px;padding:10px 14px;border-radius:4px}.home-control svg{width:15px;height:15px;stroke:var(--green);stroke-width:1.5;fill:none}[data-zoomed="true"] #selection{max-height:calc(100% - 82px)}
13
  .resources{padding:12px 24px 18px;text-align:center}
14
  .resource-links{display:flex;justify-content:center;gap:10px;flex-wrap:wrap}
15
  .resource-links a{display:flex;align-items:center;justify-content:space-between;gap:28px;padding:12px 16px;min-width:120px;border:1px solid var(--rule);border-radius:4px;text-decoration:none;font-size:13px;font-weight:500;background:transparent}
 
17
  .utilities{display:flex;justify-content:center;align-items:center;gap:14px;margin-top:9px}
18
  .utilities button{border:0;background:transparent;color:var(--muted);padding:6px 2px;font-size:10px}.utilities button:hover{color:var(--ink)}
19
  #theme-toggle{display:grid;place-items:center;width:26px;height:26px;padding:4px;flex-shrink:0}#theme-toggle svg{width:15px;height:15px;stroke:currentColor;stroke-width:1.5;fill:none}[data-theme="dark"] .theme-moon,[data-theme="light"] .theme-sun{display:none}
20
+ [data-recording="true"] .utilities{visibility:hidden}[data-recording="true"] #stage{pointer-events:none}[data-recording="true"] #layer-tooltip,[data-recording="true"] #home{display:none!important}[data-recording="true"] #selection{pointer-events:none}
21
  .tag{font-size:10px;letter-spacing:1.3px;color:var(--muted);text-transform:uppercase}
22
  #loading{position:absolute;inset:0;display:grid;place-content:center;background:var(--bg);color:var(--muted);z-index:10}
23
  #selection{position:absolute;right:24px;top:18px;width:295px;max-height:calc(100% - 36px);overflow:auto;background:var(--panel);border:1px solid var(--rule);box-shadow:0 8px 40px var(--shadow);z-index:5;padding:23px}
 
30
  .legend,.locus-controls{border-top:1px solid var(--rule);padding-top:18px;margin-top:18px}.legend select,.locus-controls select{width:100%;border:0;border-bottom:1px solid var(--rule);padding:9px 0;margin:6px 0 12px;background:var(--panel);font-size:12px}.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,.locus-controls p{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(--green)}.locus-controls strong{display:block;font-size:11px;font-weight:500;margin-top:10px}.icon-toggle{margin-top:12px}.quick{display:flex;gap:6px;margin:20px 0 6px}.quick button{font-size:11px;padding:8px 10px}.controls{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:18px}.controls button{font-size:11px;padding:7px 10px}.controls span{font-size:10px;padding:0 5px;min-width:42px;text-align:center;color:var(--muted)}
31
  #icon-credits{max-height:60vh;overflow:auto}#icon-credits table{border-collapse:collapse;width:100%;font-size:11px;text-align:left}#icon-credits th,#icon-credits td{padding:10px 8px;border-bottom:1px solid var(--rule)}#icon-credits a,#data-dialog a{color:var(--green)}.gene-label{font-size:11px;margin-top:28px;color:var(--muted)}#gene-svg{width:100%;height:auto;font:11px Inter;fill:var(--muted);margin:16px 0}.gene-notes{border-top:1px solid var(--rule);padding-top:15px;font-size:11px}#data-dialog pre{max-height:45vh;overflow:auto;border:1px solid var(--rule);background:var(--bg);padding:18px;font-size:11px;line-height:1.7;white-space:pre}#copy-status{font-size:11px;margin-left:12px;color:var(--green)}#data-dialog .data-setup{font-size:11px}
32
  .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}
33
+ @media(max-width:700px){.presentation{min-height:650px}.hero{padding:25px 22px 4px}h1{font-size:clamp(26px,5.8vw,38px);letter-spacing:-1px}.title-separator{display:none}.title-topic{display:block;margin-top:6px;font-size:.8em}#stage{inset:0 4px}.resource-links{gap:7px}.resource-links a{min-width:0;padding:10px 11px;gap:12px;font-size:11px}.home-control{left:12px;bottom:10px}
34
+ .resources{padding:8px 12px 14px}.utilities{margin-top:8px}#selection{width:275px;right:12px;top:10px;max-height:calc(100% - 20px)}dialog{padding:24px}}
35
  @media(prefers-reduced-motion:no-preference){button,.resource-links a{transition:background .15s,border-color .15s}}
36
  </style></head><body>
37
  <main class="presentation">
38
  <header class="hero"><h1><span class="wordmark">Carbon-<em>A</em></span><span class="title-separator" aria-hidden="true"> · </span><span class="title-topic">Across the tree of life</span></h1></header>
39
  <section class="stage-wrap" aria-label="Carbon-A assembly tree">
40
  <span id="sweep-announcement" class="visually-hidden" role="status"></span>
41
+ <div id="stage"><canvas id="tree" aria-label="Interactive taxonomic tree of assembly strands. Click a strand to select it; click anywhere again to clear. Scroll to zoom. Search is in View options."></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 the assembly tree…</div></div>
42
+ <button id="home" class="home-control" hidden aria-label="Return to the whole tree"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="m2 9 8-7 8 7M4 8v9h5v-5h2v5h5V8"/></svg>Whole tree</button><div id="layer-tooltip" hidden></div><section id="selection" hidden><button id="sel-close" aria-label="Close assembly details">×</button><div id="sel-state">Selected</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><div class="data-access"><a id="sel-files" target="_blank" rel="noopener">Annotation files ↗</a><br><button id="sel-download">Get annotation data ↗</button><p id="sel-availability"></p></div><button id="sel-focus">Zoom to this strand ↗</button><button id="sel-loci" hidden>See coding regions →</button></section></section>
43
  <footer class="resources"><nav class="resource-links" aria-label="Carbon-A resources"><a href="https://huggingface.co/HuggingFaceBio/Carbon-A-1.2B" target="_blank" rel="noopener">Model<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations" target="_blank" rel="noopener">Database<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/spaces/HuggingFaceBio/carbon-a-database-explorer" target="_blank" rel="noopener">Explorer<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/buckets/HuggingFaceBio/Carbon-A-Technical-Report" target="_blank" rel="noopener">Paper<span aria-hidden="true">↗</span></a><a href="https://huggingface.co/blog/HuggingFaceBio/carbon-annotator-genbank-genome-annotation" target="_blank" rel="noopener">Blog<span aria-hidden="true">↗</span></a></nav><div class="utilities"><button id="view-options">View options</button><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></footer>
44
  </main>
45
+ <dialog id="view-dialog"><button id="view-close" class="dialog-close" aria-label="Close view options">×</button><h2>Explore the tree</h2><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><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><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="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><div class="quick"><button id="human">Find a human assembly ↗</button><button id="gene-open">Its coding regions →</button></div><p>Click a strand to select · Click anywhere again to clear.<br>Scroll to zoom · Drag to pan · Space to animate.</p></dialog>
46
+ <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 <span id="shown">33,588</span> strands across <span id="taxa-shown">16,976</span> taxa is a distinct accession from the explorer’s covered-assembly table, mapped through the NCBI taxonomy. This is a subset of the release. 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. Click a strand or inside the centre to select an assembly. Click anywhere again to clear the selection. Clicking icons and bars also selects an assembly without zooming; use the scroll wheel or zoom controls to zoom and Whole tree to return. For video capture, press Space to start or replay a four-second clockwise sweep. Escape stops it. The assembly info box follows the highlighted strand and remains on the final assembly when playback finishes. The sweep has no counters.</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 select a strand at that node. <button id="credits-open">Icon credits and licences</button></p><p><a href="https://huggingface.co/spaces/HuggingFaceBio/carbon-a-database-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><dialog id="data-dialog"><button id="data-close" class="dialog-close" aria-label="Close download instructions">×</button><h2>Get annotation data</h2><p id="data-assembly"></p><p id="data-description"></p><p><a id="data-browse" target="_blank" rel="noopener">Browse annotation files ↗</a></p><p class="data-setup">Install <code>huggingface_hub</code> and run <code>hf auth login</code> with an account that can access the bucket.</p><pre><code id="data-code"></code></pre><button id="data-copy">Copy Python code</button><span id="copy-status" role="status"></span><p><a href="https://huggingface.co/docs/huggingface_hub/guides/buckets#download-files" target="_blank" rel="noopener">Hugging Face download documentation ↗</a></p></dialog><script id="dataset" type="application/octet-stream">__DATA__</script><script>__SCRIPT__</script></body></html>