const state = {data: null, cache: new Map(), dimensionality: new Map()}; const $ = id => document.getElementById(id); const species = () => state.data.species; const speciesBySlug = slug => species().find(x => x.slug === slug); const displaySource = id => ({ EggNOG_OG: 'EggNOG OG', GO_EggNOG: 'GO (EggNOG)', GO_ESM2_150M_MF: 'GO (ESM2-150M)', GO_ESMC_600M_ProteinFunction: 'GO (ESMC-600M)', ESM_localization: 'ESM localization', GO_union: 'GO union', Pfam_HMMER: 'Pfam / HMMER', EC_EggNOG: 'EC (EggNOG)', KEGG_KO_EggNOG: 'KEGG KO (EggNOG)', KEGG_KO_KofamScan: 'KEGG KO (KofamScan)', KEGG_KO_union: 'KEGG KO union' }[id] || id); function esc(value){ return String(value ?? '').replaceAll('&','&').replaceAll('<','<') .replaceAll('>','>').replaceAll('"','"').replaceAll("'",'''); } function table(target, rows, columns){ if(!rows || !rows.length){$(target).innerHTML='

No rows available.

';return;} const cols=columns || Object.keys(rows[0]); $(target).innerHTML=`${cols.map(c=>`${esc(c)}`).join('')}`+ rows.map(row=>`${cols.map(c=>`${esc(row[c])}`).join('')}`).join('')+''; } function records(payload){return (payload.data||payload.rows||[]).map(row=>Object.fromEntries(payload.columns.map((c,i)=>[c,row[i]])));} async function json(path){ if(!state.cache.has(path)) state.cache.set(path, fetch(path).then(r=>{if(!r.ok)throw Error(path);return r.json()})); return state.cache.get(path); } function fillSelect(id, values, value){ $(id).innerHTML=values.map(v=>``).join(''); if(value!==undefined) $(id).value=value; } function setSpeciesSelect(id, initial='species01'){ fillSelect(id, species().map(x=>x.slug), initial); $(id).querySelectorAll('option').forEach(o=>{const s=speciesBySlug(o.value);o.textContent=s.label}); } function contrastEntries(slug){ const seen=new Map(); for(const x of state.data.deIndex[slug]||[]) if(!seen.has(x.contrast_id))seen.set(x.contrast_id,x); return [...seen.values()].sort((a,b)=>a.contrast_id.localeCompare(b.contrast_id)); } function setupContrastSelect(speciesId, contrastId){ const entries=contrastEntries($(speciesId).value); fillSelect(contrastId, entries.map(x=>x.contrast_id), entries[0]?.contrast_id); } function csvDownload(filename, rows, columns){ const body=[columns, ...rows.map(r=>columns.map(c=>r[c] ?? ''))] .map(row=>row.map(v=>`"${String(v).replaceAll('"','""')}"`).join(',')).join('\n'); const url=URL.createObjectURL(new Blob([body],{type:'text/csv;charset=utf-8'})); const a=document.createElement('a');a.href=url;a.download=filename;a.click();URL.revokeObjectURL(url); } function fmt(n){return Number(n).toLocaleString('en-US')} function pFmt(n){n=Number(n);return Number.isFinite(n)?(n<.001?n.toExponential(2):n.toPrecision(3)):'NA'} function plotLogP(rows, column){ const positive=rows.map(r=>Number(r[column])).filter(x=>Number.isFinite(x)&&x>0); const observedMax=positive.length?Math.max(...positive.map(x=>-Math.log10(x))):0; const floor=positive.length?Math.min(...positive)/10:1e-12; return { value:n=>{n=Number(n);return Number.isFinite(n)&&n>0?-Math.log10(n):-Math.log10(floor)}, max:Math.max(observedMax,-Math.log10(floor)), floored:n=>{n=Number(n);return !Number.isFinite(n)||n<=0} }; } function plotPText(value, label, floored){ const n=Number(value); if(!Number.isFinite(n))return `${label}: unavailable`; if(n<=0)return `${label}: 0 (reported as 0; plotted at finite floor)`; return `${label}: ${pFmt(n)}${floored?' (finite floor used)':''}`; } function contrastDirectionText(entry){ if(!entry)return ''; return `Positive fold change (up) indicates higher expression in ${entry.group_a} relative to ${entry.group_b}; negative fold change (down) indicates higher expression in ${entry.group_b} relative to ${entry.group_a}.`; } function renderOverview(){ table('assembly-table', state.data.assemblySummary, Object.keys(state.data.assemblySummary[0])); const render=()=>{ const slug=$('overview-species').value, s=speciesBySlug(slug); $('annotation-title').textContent=`${s.label} | Universe: ${fmt(s.genes)} genes, ${fmt(s.proteins)} predicted proteins.`; table('annotation-table', state.data.annotationExamples[s.label]); }; setSpeciesSelect('overview-species');$('overview-species').onchange=render;render(); } async function renderExpression(){ const slug=$('dim-species').value, method=$('dim-method').value, genes=$('dim-genes').value; const path=`data/dimensionality/${slug}.json`, all=await json(path), p=all[`${method}_${genes}`]; const axes=p.axes, dimensionLabels=['Dimension 1','Dimension 2','Dimension 3'], rows=p.rows, symbols=['circle','diamond','square','x','cross','triangle-up']; const temps=[...new Set(rows.map(r=>String(r.temperature)))], symbolMap=Object.fromEntries(temps.map((x,i)=>[x,symbols[i%symbols.length]])); const stages=[...new Set(rows.map(r=>String(r.development_stage)))]; const stageColors=['#176b91','#e27d43','#58a66b','#8b63b6','#c94f66','#d3a42b']; const traces=stages.map((stage,i)=>{ const x=rows.filter(r=>String(r.development_stage)===stage); return {type:'scatter3d',mode:'markers',name:stage,x:x.map(r=>r[axes[0]]),y:x.map(r=>r[axes[1]]),z:x.map(r=>r[axes[2]]), text:x.map(r=>r.sample_id),customdata:x.map(r=>[r.temperature,r.development_stage,r.replicate]), marker:{size:8,color:stageColors[i%stageColors.length],symbol:x.map(r=>symbolMap[String(r.temperature)])}, hovertemplate:'%{text}
Temperature: %{customdata[0]}
Development stage: %{customdata[1]}
Replicate: %{customdata[2]}'}; }); const temperatureLegend=temps.map(temp=>({type:'scatter3d',mode:'markers',name:`Temperature: ${temp}`,x:[null],y:[null],z:[null], marker:{size:8,color:'#6b7f8f',symbol:symbolMap[temp]},legendgroup:'temperature',showlegend:true,hoverinfo:'skip'})); Plotly.newPlot('dim-plot',[...traces,...temperatureLegend],{template:'plotly_white',paper_bgcolor:'#fff',plot_bgcolor:'#f7fbfd',font:{color:'#123b5d'},height:650, scene:{xaxis:{title:{text:dimensionLabels[0]}},yaxis:{title:{text:dimensionLabels[1]}},zaxis:{title:{text:dimensionLabels[2]}}},title:`${speciesBySlug(slug).label} · ${method}`},{responsive:true}); const explained=p.explained_variance?` Explained variance: ${p.explained_variance.map((x,i)=>`${dimensionLabels[i]} ${(x*100).toFixed(1)}%`).join(', ')}.`:''; $('dim-status').textContent=`${speciesBySlug(slug).label}: ${rows.length} samples projected from ${genes==='all'?'all':fmt(p.selected_gene_count)} variable genes after log2(TMM + 1). The matrix contains ${fmt(p.expressed_gene_count)} non-constant genes.${explained}`; table('dim-table', rows, ['sample_id','temperature','development_stage','replicate',...axes]); } async function loadDe(){ const slug=$('de-species').value, contrast=$('de-contrast').value, method=$('de-method').value; const entry=(state.data.deIndex[slug]||[]).find(x=>x.contrast_id===contrast&&x.method===method); return entry?{entry,rows:records(await json(`data/${entry.file}`))}:{entry:null,rows:[]}; } function deFiltered(rows, method){ const p=+$('de-pvalue').value, q=+$('de-adjusted').value, fold=+$('de-fold').value; return rows.filter(r=>Number(r[method==='DESeq2'?'pvalue':'PValue'])<=p&&Number(r[method==='DESeq2'?'padj':'FDR'])<=q&&Math.abs(Number(r[method==='DESeq2'?'log2FoldChange':'logFC']))>=fold); } async function renderDE(){ const method=$('de-method').value, loaded=await loadDe(), rows=deFiltered(loaded.rows,method), entry=loaded.entry; if(!entry){$('de-summary').textContent='No differential-expression result is available for this selection.';return;} const foldCol=method==='DESeq2'?'log2FoldChange':'logFC', pCol=$('de-y').value==='pvalue'?(method==='DESeq2'?'pvalue':'PValue'):(method==='DESeq2'?'padj':'FDR'); const yLabel=$('de-y').value==='pvalue'?'p-value':(method==='DESeq2'?'padj':'FDR'); const yTitle=`-log10(${yLabel})`, logP=plotLogP(rows,pCol); const traces=['down','up'].map(direction=>{const x=rows.filter(r=>r.direction===direction);return {type:'scatter',mode:'markers',name:direction,x:x.map(r=>r[foldCol]),y:x.map(r=>logP.value(r[pCol])),customdata:x.map(r=>[r.gene_id,plotPText(r[pCol],yLabel,logP.floored(r[pCol]))]),hovertemplate:'%{customdata[0]}
Fold change: %{x:.3f}
'+yTitle+': %{y:.2f}
%{customdata[1]}',marker:{size:6}}}); const xTitle=method==='DESeq2'?'log2 fold change':'log fold change'; Plotly.newPlot('de-plot',traces,{template:'plotly_white',paper_bgcolor:'#fff',plot_bgcolor:'#f7fbfd',font:{color:'#123b5d'},height:620,title:`Volcano plot: ${method}`,xaxis:{title:{text:xTitle}},yaxis:{title:{text:yTitle},range:[0,Math.max(logP.max*1.08,1)]},shapes:[{type:'line',x0:0,x1:0,y0:0,y1:1,yref:'paper',line:{dash:'dot',color:'#7890a0'}}]},{responsive:true}); $('de-summary').innerHTML=`${method} compared ${esc(entry.group_a)} (n = ${entry.n_group_a}) with ${esc(entry.group_b)} (n = ${entry.n_group_b}). Of the ${fmt(entry.n_genes_input)} genes in the expression matrix, ${fmt(entry.n_genes_tested)} were retained for statistical testing. Using p-value ≤ ${$('de-pvalue').value}, adjusted p-value / FDR ≤ ${$('de-adjusted').value}, and absolute fold change ≥ ${$('de-fold').value}, ${fmt(rows.length)} DEGs remain. ${contrastDirectionText(entry)}`; const cols=method==='DESeq2'?['gene_id','baseMean','log2FoldChange','lfcSE','stat','pvalue','padj','direction']:['gene_id','logFC','logCPM','F','PValue','FDR','direction']; table('de-table',rows.slice(0,500),cols);$('de-download').onclick=()=>csvDownload(`${slugSafe($('de-species').value)}_de_filtered.csv`,rows,cols); } function slugSafe(x){return x.toLowerCase().replaceAll(' ','_')} function activeEnrichmentSource(){return sourceFor($('en-source').value,$('en-hierarchy').value)} function sourceFor(base,h){ if(h==='Direct terms')return base; if(h==='GO ancestors')return `${base}__GO_ANCESTORS`; if(h==='EC level 1 class')return 'EC_EggNOG__EC_LEVEL1'; const prefix={'KEGG pathways':'KEGG_PATHWAY','KEGG level 1':'KEGG_LEVEL1','KEGG level 2':'KEGG_LEVEL2'}[h]; const suffix={KEGG_KO_EggNOG:'EggNOG',KEGG_KO_KofamScan:'KofamScan',KEGG_KO_union:'consensus'}[base]; return prefix&&suffix?`${prefix}_${suffix}`:base; } async function loadEnrichment(){ const slug=$('en-species').value, contrast=$('en-contrast').value, method=$('en-method').value; const index=(state.data.enrichmentIndex[slug]||[]).filter(x=>x.contrast_id===contrast&&x.method===method); if(!index.length)return {index,rows:[]}; const file=index[0].file;return {index,rows:records(await json(`data/${file}`))}; } async function renderEnrichment(){ const loaded=await loadEnrichment(), source=activeEnrichmentSource(), maxP=+$('en-pvalue').value,maxQ=+$('en-adjusted').value; const info=loaded.index.find(x=>x.source_id===source), rows=loaded.rows.filter(r=>r.source_id===source&&Number(r.pvalue)<=maxP&&Number(r.padj)<=maxQ); if(!info){$('en-summary').textContent='No enrichment snapshot is available for this hierarchy selection.';return;} const deEntry=(state.data.deIndex[$('en-species').value]||[]).find(x=>x.contrast_id===$('en-contrast').value&&x.method===$('en-method').value); const byTerm={};for(const r of rows){(byTerm[r.term]??=[]).push(r)} const terms=Object.values(byTerm).sort((a,b)=>Math.min(...a.map(x=>+x.padj))-Math.min(...b.map(x=>+x.padj))).slice(0,20); const agg=terms.map(group=>{const total=group.reduce((s,r)=>s+Number(r.overlap||0),0);const fe=group.reduce((s,r)=>s+Number(r.fold_enrichment||0)*Number(r.overlap||0),0)/(total||1);return group.map(r=>({...r,term_description:r.name||r.term,segment_fold_enrichment:fe*Number(r.overlap||0)/(total||1),fold_enrichment_total:fe,total_overlap:total}))}).flat().sort((a,b)=>Number(a.fold_enrichment_total)-Number(b.fold_enrichment_total)||a.term_description.localeCompare(b.term_description)); const categoryOrder=[...new Set(agg.map(r=>r.term_description))]; Plotly.newPlot('en-plot',[{type:'bar',orientation:'h',x:agg.map(r=>r.segment_fold_enrichment),y:agg.map(r=>r.term_description),customdata:agg.map(r=>[r.direction,r.overlap,r.fold_enrichment_total,r.padj]),name:'direction',marker:{color:agg.map(r=>r.direction==='up'?'#d55e48':'#247ba0')},hovertemplate:'%{y}
%{customdata[0]} genes: %{customdata[1]}
Fold enrichment: %{customdata[2]:.2f}
Adjusted p-value: %{customdata[3]:.3g}'}],{template:'plotly_white',paper_bgcolor:'#fff',plot_bgcolor:'#f7fbfd',font:{color:'#123b5d'},height:Math.max(520,350+terms.length*22),barmode:'stack',title:`${displaySource($('en-source').value)} · ${$('en-hierarchy').value}`,xaxis:{title:{text:'Fold enrichment'},range:[0,Math.max(...agg.map(r=>r.fold_enrichment_total),1)]},yaxis:{automargin:true,categoryorder:'array',categoryarray:categoryOrder}} ,{responsive:true}); const nEnriched=new Set(rows.map(r=>r.term)).size; $('en-summary').innerHTML=`The frozen over-representation analysis tested ${fmt(info.universe_terms)} unique terms or categories; ${fmt(nEnriched)} terms pass the displayed ORA filters. Bars show fold enrichment. For this contrast, positive fold change (up) indicates higher expression in ${esc(deEntry?.group_a||'the first group')} relative to ${esc(deEntry?.group_b||'the second group')}; negative fold change (down) indicates higher expression in ${esc(deEntry?.group_b||'the second group')} relative to ${esc(deEntry?.group_a||'the first group')}. When both directions contribute to a term, the bar is stacked proportionally; these directions describe expression differences and do not by themselves imply pathway activation or inhibition.`; table('en-table',rows.sort((a,b)=>Number(a.padj)-Number(b.padj)).slice(0,500),['source_id','direction','term','name','overlap','foreground_size','term_size','background_size','fold_enrichment','pvalue','padj','genes']); $('en-download').onclick=()=>csvDownload(`${slugSafe($('en-species').value)}_enrichment_filtered.csv`,rows,['source_id','direction','term','name','overlap','foreground_size','term_size','background_size','fold_enrichment','pvalue','padj','genes']); } function syncDESpecies(){setupContrastSelect('de-species','de-contrast')} function syncEnSpecies(){setupContrastSelect('en-species','en-contrast')} function init(){ setSpeciesSelect('dim-species');setSpeciesSelect('de-species');setSpeciesSelect('en-species'); renderOverview();syncDESpecies();syncEnSpecies(); $('de-species').onchange=()=>{syncDESpecies();renderDE();};$('en-species').onchange=()=>{syncEnSpecies();syncEnSource();renderEnrichment()}; $('dim-run').onclick=renderExpression;renderExpression(); renderDE(); $('de-run').onclick=renderDE;['de-method','de-contrast','de-pvalue','de-adjusted','de-fold','de-y'].forEach(id=>$(id).onchange=()=>{}); $('en-run').onclick=renderEnrichment; $('en-source').onchange=syncEnHierarchy; syncEnSource();renderEnrichment(); document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{document.querySelectorAll('.tab,.panel').forEach(x=>x.classList.remove('active'));b.classList.add('active');$(b.dataset.tab).classList.add('active');}); fetch('assets/inria_footer_logo.txt').then(r=>r.text()).then(x=>{$('inria-logo').innerHTML=`French Republic - Inria`}); } function syncEnSource(){const ids=state.data.sourceOrder;fillSelect('en-source',ids.map(displaySource));[...$('en-source').options].forEach((o,i)=>o.value=ids[i]);$('en-source').value=ids.includes('KEGG_KO_union')?'KEGG_KO_union':ids[0];syncEnHierarchy()} function syncEnHierarchy(){const base=$('en-source').value, options=state.data.hierarchies[base]||['Direct terms'];const preferred=options.includes('KEGG pathways')?'KEGG pathways':options[0];fillSelect('en-hierarchy',options,preferred);} fetch('data/app-data.json').then(r=>r.json()).then(d=>{state.data=d;init()}).catch(e=>{document.body.innerHTML+='

Unable to load the frozen data snapshot.

';console.error(e)});