Spaces:
Running
Running
Use click selection and anywhere deselection; restore sweep details and visible whole-tree reset
Browse files- .gitattributes +1 -0
- README.md +7 -7
- app.js +35 -26
- carbon-a-strand-source.zip +2 -2
- check_presentation.py +53 -17
- index.html +0 -0
- layers.js +14 -9
- presentation-mobile.png +2 -2
- presentation-verification.json +6 -3
- provenance.json +3 -2
- sweep-info.png +3 -0
- sweep.js +5 -3
- template.html +8 -6
.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
|
| 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
|
| 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.
|
| 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.
|
| 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,
|
| 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();
|
| 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.
|
| 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
|
| 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=
|
| 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=()=>{
|
| 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=>{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
stage.addEventListener('pointermove',e=>{
|
| 119 |
if(!down){queueHover(e);return}
|
| 120 |
-
|
| 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)
|
| 124 |
moved=true;canvas.style.cursor='grabbing';$('#layer-tooltip').hidden=true;
|
| 125 |
-
pan.x+=dx;pan.y+=dy;down={
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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=()=>
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
page.
|
| 31 |
-
assert page.evaluate('pinnedAssembly.i')==12345
|
| 32 |
-
page.
|
| 33 |
-
|
| 34 |
-
assert page.evaluate('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
|
|
| 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').
|
| 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']
|
|
|
|
|
|
|
|
|
|
| 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','
|
| 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='
|
| 83 |
-
if(!ic&&!bin){tip.hidden=true;if(hoverBin){hoverBin=null;schedule()}const a=hitAssembly(x,y,3);canvas.style.cursor=a?'pointer':'default';
|
| 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
|
| 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
|
| 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 |
-
|
| 92 |
-
function
|
| 93 |
-
|
| 94 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
Git LFS Details
|
presentation-verification.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"sweep_duration_ms":
|
| 3 |
-
"sweep_frames":
|
| 4 |
"background_redraws": 0,
|
| 5 |
"layout": "desktop and mobile passed",
|
| 6 |
-
"
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 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"] #
|
| 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}.
|
|
|
|
| 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.
|
| 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">
|
| 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>
|
| 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.
|
|
|
|
| 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>
|