Multichannel — 1D / 2D / 3D / Regional maxima

Load files

Sample (*.txt) — can be split
Blank (required)

Visualization

—
1.0
No blank aligned.

Metadata

No data.

References

  1. Xiaoran Ning, Ivan W. Selesnick, Laurent Duval, Chromatogram baseline estimation and denoising using sparsity (BEADS), Chemometrics and Intelligent Laboratory Systems, 2014. https://doi.org/10.1016/j.chemolab.2014.09.014.
  2. João T.V. Matos, Regina M.B.O. Duarte, Armando C. Duarte, A simple approach to reduce dimensionality from comprehensive two-dimensional liquid chromatography coupled with a multichannel detector, Analytica Chimica Acta, 2013, https://doi.org/10.1016/j.aca.2013.10.033.
`; const blob=new Blob([html], {type:'text/html'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=filename; a.click(); setTimeout(()=>URL.revokeObjectURL(url), 1000); } // export 2D/3D document.getElementById('png1D').addEventListener('click', ()=>{ const div=document.getElementById('vizPlot'); Plotly.toImage(div,{format:'png',height:480,width:1100}).then(url=>{ const a=document.createElement('a'); a.href=url; a.download='chromatograma_1d.png'; a.click(); }); }); document.getElementById('csv1D').addEventListener('click', ()=>{ const S=window.__last1D; if(!S) return; const {t, lam, bw, ySample, ySub, yBlankA, yBeads} = S; let out='t_min,amostra,amostra_menos_branco,branco_alinhado,amostra_beads\n'; for(let i=0;iURL.revokeObjectURL(url), 1000); }); document.getElementById('png2D').addEventListener('click', ()=>{ const div=document.getElementById('vizPlot'); Plotly.toImage(div,{format:'png',height:480,width:1100}).then(url=>{ const a=document.createElement('a'); a.href=url; a.download='chromatograma_2d.png'; a.click(); }); }); document.getElementById('csv2D').addEventListener('click', ()=>{ const S=window.__last2D; if(!S) return; const {Z,x,y,lam,Tm_min}=S; let out='t1_min,t2_s,val\n'; for(let j=0;jURL.revokeObjectURL(url), 1000); }); document.getElementById('reset3D').addEventListener('click', ()=> Plotly.relayout('vizPlot', {scene:{camera:{}}})); document.getElementById('png3D').addEventListener('click', ()=> Plotly.toImage('vizPlot', {format:'png', height:560, width:980}).then(url=>{ const a=document.createElement('a'); a.href=url; a.download='peaks3d.png'; a.click(); })); document.getElementById('csv3D').addEventListener('click', ()=>{ if(!MAXIMA3D.length) return; // mesmo filtro de λ do modo gráfico const minInp=document.getElementById('lambdaMin3D'); const maxInp=document.getElementById('lambdaMax3D'); let lamMin = minInp ? parseFloat(minInp.value) : NaN; let lamMax = maxInp ? parseFloat(maxInp.value) : NaN; if (!Number.isFinite(lamMin)) lamMin = (wlAxis && wlAxis.length) ? wlAxis[0] : -Infinity; if (!Number.isFinite(lamMax)) lamMax = (wlAxis && wlAxis.length) ? wlAxis[wlAxis.length-1] : +Infinity; if (lamMin > lamMax){ const t=lamMin; lamMin=lamMax; lamMax=t; } let csv='t1_min,t2_min,lambda_nm,intensity\n'; const dt_min=(timeAxis && timeAxis.length>1) ? (timeAxis[1]-timeAxis[0]) : 0.1; const Tm_min=parseFloat(document.getElementById('modTm').value||'0'); for (const p of MAXIMA3D){ const lam = wlAxis[p.k]; if (lam < lamMin || lam > lamMax) continue; // <-- filtro const t1=p.i*Tm_min, t2=p.j*dt_min; csv += `${t1.toFixed(5)},${t2.toFixed(5)},${lam.toFixed(2)},${p.v}\n`; } const blob=new Blob([csv], {type:'text/csv'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='peaks3d.csv'; a.click(); setTimeout(()=>URL.revokeObjectURL(url), 1000); }); // === HTML (standalone) === document.getElementById('html1D')?.addEventListener('click', ()=>{ exportCurrentPlotToHTML('chromatograma_1d.html'); }); document.getElementById('html2D')?.addEventListener('click', ()=>{ exportCurrentPlotToHTML('chromatograma_2d.html'); }); document.getElementById('html3D')?.addEventListener('click', ()=>{ // Funciona para “3D (surface)” e para “Regional maxima” exportCurrentPlotToHTML( (window.__mode==='3d') ? 'chromatograma_3d_superficie.html' : 'peaks3d.html' ); }); // === CSV para 3D (surface) === document.getElementById('csv3DSurf')?.addEventListener('click', ()=>{ const S = window.__last3DSurf; if(!S || !S.Z || !S.x || !S.y){ alert('Generate 3D (surface) first.'); return; } // Guardamos a grelha (t1_min, t2_s, intensidade) — tal como no 2D let out='t1_min,t2_s,val\n'; for(let j=0;jURL.revokeObjectURL(url), 1000); }); // Botão para limpar edições 2D const clearBtn = document.getElementById('clear2D'); if (clearBtn) clearBtn.addEventListener('click', ()=>{ // apaga o mapa de edição e refaz o 2D if (window.__edit2D && window.__edit2D.E){ for (let j=0;j { const disp = el.dataset && el.dataset.display ? el.dataset.display : 'inline-block'; el.style.display = on ? disp : 'none'; }); } // NOVO: garante que o modo de edição 2D é desligado limpo function ensureEdit2DOff(){ if (window.__editOn) { window.__editOn = false; const btn = document.getElementById('edit2D'); const gd = document.getElementById('vizPlot'); const card= document.getElementById('viz-card'); if (btn){ btn.classList.remove('primary'); btn.classList.add('secondary'); btn.textContent='Edit 2D'; } if (gd){ Plotly.relayout(gd, { dragmode:'zoom', hovermode:'closest', xaxis:{showspikes:true}, yaxis:{showspikes:true} }) .then(()=>{ gd.style.cursor=''; card && card.classList.remove('edit2d'); }); } else { card && card.classList.remove('edit2d'); } // limpa o overlay do pincel se existir const ov = document.getElementById('brushOverlay'); if (ov) { const ctx=ov.getContext('2d'); ctx && ctx.clearRect(0,0,ov.width,ov.height); } } } function setMode(mode){ const m = String(mode||'1d').toLowerCase(); // NOVO: se vamos sair do 2D, desliga edição if (m !== '2d') ensureEdit2DOff(); setActive(btn1, m==='1d'); setActive(btn2, m==='2d'); setActive(btn3, m==='3d'); setActive(btn4, m==='max'); showCls('.only-1d', m==='1d'); showCls('.only-2d', m==='2d'); showCls('.only-3d', m==='3d' || m==='max'); // Correção: only-3d deve aparecer em ambos os modos 3D if(peaksInfo) peaksInfo.style.display = (m==='max') ? '' : 'none'; window.__mode = m; // útil para depurar “botão selecionado vs. plot mostrado” if (m === 'max' && wlAxis && wlAxis.length){ const mi = document.getElementById('lambdaMin3D'); const ma = document.getElementById('lambdaMax3D'); if (mi && !mi.value) mi.value = Number(wlAxis[0]).toFixed(1); if (ma && !ma.value) ma.value = Number(wlAxis[wlAxis.length-1]).toFixed(1); } } btn1.addEventListener('click', ()=>{ setMode('1d'); draw1DFromState(); }); btn2.addEventListener('click', ()=>{ setMode('2d'); renderChrom2D(); }); btn3.addEventListener('click', ()=>{ setMode('3d'); draw3DSurfaceFromState(); }); btn4.addEventListener('click', ()=>{ setMode('max'); draw3DFromState(); }); setMode('1d'); })(); // Sync slider → λ livre (and label) (function(){ const slider = document.getElementById('lambdaSlider'); const free = document.getElementById('lambdaFree'); const label = document.getElementById('lambdaSliderVal'); if(!slider) return; slider.addEventListener('input', (e)=>{ const wlAxisLen = (wlAxis && wlAxis.length) ? wlAxis.length : 1; const k = Math.max(0, Math.min(parseInt(slider.value||'0',10), wlAxisLen - 1)); const lam = (wlAxis && wlAxis.length) ? wlAxis[k] : parseFloat(free && free.value ? free.value : '254') || 254; if(free) free.value = Number(lam).toFixed(2); if(label) label.textContent = Number(lam).toFixed(2) + ' nm'; }); })(); // Process document.getElementById('btnProcess').addEventListener('click', ()=>{ const is1d = document.getElementById('btnMode1d').classList.contains('primary'); const is2d = document.getElementById('btnMode2d').classList.contains('primary'); const is3d = document.getElementById('btnMode3d').classList.contains('primary'); // 3D superfície const isMax= document.getElementById('btnModeMax').classList.contains('primary'); // máximos regionais if(is1d) draw1DFromState(); else if(is2d) renderChrom2D(); else if(is3d) draw3DSurfaceFromState(); else if(isMax)draw3DFromState(); }); (function(el){ if(!el) return; el.addEventListener('change', () => { const is2d = document.getElementById('btnMode2d').classList.contains('primary'); if (is2d) renderChrom2D(); }); })(document.getElementById('palette2d')); ['lambdaMin3D','lambdaMax3D'].forEach(id=>{ const el=document.getElementById(id); if(el){ el.addEventListener('change', ()=>{ const isMax = document.getElementById('btnModeMax').classList.contains('primary'); if (isMax) draw3DFromState(); }); } }); // Parse parseBtn.addEventListener('click', async ()=>{ const files=[...fileInput.files]; if(!files.length) return; datasets=[]; for(const f of files){ const txt=await readTextFile(f); datasets.push(parsePDA(txt)); } merged=mergeDatasets(datasets); buildAxes(merged); renderMeta(merged); subtracted=null; const ai=document.getElementById('alignInfo'); if(ai) ai.textContent='No blank aligned.'; const filesB=[...fileInputB.files]; if(filesB.length){ datasetsB=[]; for(const f of filesB){ const txt=await readTextFile(f); datasetsB.push(parsePDA(txt)); } mergedB=mergeDatasets(datasetsB); } setTimeout(()=>{ const is1d = document.getElementById('btnMode1d').classList.contains('primary'); const is2d = document.getElementById('btnMode2d').classList.contains('primary'); const is3d = document.getElementById('btnMode3d').classList.contains('primary'); const isMax= document.getElementById('btnModeMax').classList.contains('primary'); if (is1d) draw1DFromState(); else if (is2d) renderChrom2D(); else if (is3d) draw3DSurfaceFromState(); else if (isMax)draw3DFromState(); }, 0); }); // === DENOISE/ENHANCE HELPERS === function gaussianKernel1D(sigma){ const r=Math.max(1, Math.ceil(3*sigma)); const k=[]; let sum=0; for(let i=-r;i<=r;i++){ const v=Math.exp(-(i*i)/(2*sigma*sigma)); k.push(v); sum+=v; } return k.map(v=>v/sum); } function convolveSeparable(Z, k){ if (!Z || Z.length === 0 || Z[0].length === 0) return []; const H=Z.length, W=Z[0].length; const r=(k.length-1)/2; const tmp=Array.from({length:H}, ()=>Array(W).fill(0)); for(let y=0;y=W) xx=W-1; s+=Z[y][xx]*k[i+r]; } tmp[y][x]=s; } } const out=Array.from({length:H}, ()=>Array(W).fill(0)); for(let y=0;y=H) yy=H-1; s+=tmp[yy][x]*k[i+r]; } out[y][x]=s; } } return out; } function gaussianBlur2D(Z, sigma){ const k=gaussianKernel1D(sigma); return convolveSeparable(Z, k); } // ==== Edição 2D (suavização local) ==== // guarda a edição num plano do mesmo tamanho do heatmap mostrado window.__edit2D = null; // {E, x, y} E tem shape [ny][nx] (mesmos eixos que __last2D) window.__editOn = false; // kernel gauss 2D rápido em grelha function makeBrush(radiusPx){ const r = Math.max(1, Math.floor(radiusPx/2)); const sigma = r/1.5; const k = []; let sum = 0; for (let j=-r;j<=r;j++){ const row = []; for (let i=-r;i<=r;i++){ const v = Math.exp(-(i*i+j*j)/(2*sigma*sigma)); row.push(v); sum += v; } k.push(row); } // normaliza para máximo=1 (comportamento de "apagador" por intensidade) const norm = Math.max(1e-12, k.reduce((m,row)=>Math.max(m, ...row), 0)); for (let j=0;jt2=%{y:.1f} s' }; Plotly.addTraces(gd, [trace]); window.__peakHL2DTraceIdx = gd.data.length - 1; } // idem para 3D superfície function highlightPeak3D(idx){ const P = window.__peaks3Dsurf; const Zs = window.__last3DSurfScaled; if(!P || !Zs) return; const gd = document.getElementById('vizPlot'); if (typeof window.__peakHL3DTraceIdx === 'number'){ try{ Plotly.deleteTraces(gd, window.__peakHL3DTraceIdx); }catch(e){} window.__peakHL3DTraceIdx = null; } if (idx==null || !P.peaks[idx]) return; const p = P.peaks[idx]; const x = P.x[p.i], y = P.y[p.j], z = Zs[p.j][p.i]; const trace = { x:[x], y:[y], z:[z], type:'scatter3d', mode:'markers', marker:{ size:6, symbol:'x' }, name:'selected peak', hovertemplate:'t1=%{x:.3f} min
t2=%{y:.1f} s' }; Plotly.addTraces(gd, [trace]); window.__peakHL3DTraceIdx = gd.data.length - 1; } // tabela/lista de peaks com seleção e remoção function renderPeaksList(peaks, xAxis, yAxis, targetId, maxN=500, onDelete, onSelect, selectedIdx=null){ const showLambda = (targetId !== "peaksList2d"); const el = document.getElementById(targetId); if(!el) return; if(!peaks || !peaks.length){ el.innerHTML = '
No peaks.
'; return; } const n = Math.min(maxN, peaks.length); let html = ''; html += '' + '' + '' + '' + (showLambda ? '' : '') + '' + '' + ''; for(let k=0;k` + `` + `` + `` + (showLambda ? `` : '') + `` + `` + ``; } html += '
#t1 (min)t2 (s)λ (nm)I
${k+1}${Number(t1).toFixed(4)}${Number(t2).toFixed(2)}\${(p.lambda!=null? Number(p.lambda).toFixed(2) : '—')}${p.z.toExponential(3)}×
'; if (peaks.length > n) html += `
${peaks.length-n} more…
`; el.innerHTML = html; // delegação de eventos (apagar vs selecionar) el.onclick = (ev)=>{ const tr = ev.target.closest('tr[data-idx]'); if(!tr) return; const idx = parseInt(tr.getAttribute('data-idx'),10); if (ev.target.closest('[data-action="del"]')) { if (typeof onDelete === 'function') onDelete(idx); } else { if (typeof onSelect === 'function') onSelect(idx); } }; } function redrawPeaksOverlay2D(){ const P = window.__peaks2D; if(!P) return; const xs = P.peaks.map(p=> p.x); const ys = P.peaks.map(p=> p.y); const trace = { x: xs, y: ys, type:'scatter', mode:'markers', marker:{size:6, symbol:'circle-open', line:{width:1}}, name:'peaks', hovertemplate: '#%{customdata} t1=%{x:.3f} min
t2=%{y:.1f} s', customdata: P.peaks.map((_,k)=>k+1) }; const gd = document.getElementById('vizPlot'); // remove overlay anterior (se existir) if (typeof window.__peaks2DTraceIdx === 'number'){ try{ Plotly.deleteTraces(gd, window.__peaks2DTraceIdx); }catch(e){} } Plotly.addTraces(gd, [trace]); window.__peaks2DTraceIdx = gd.data.length - 1; } function redrawPeaksOverlay3D(){ const P = window.__peaks3Dsurf; const Zs = window.__last3DSurfScaled; if(!P || !Zs) return; const xs = P.peaks.map(p=> P.x[p.i]); const ys = P.peaks.map(p=> P.y[p.j]); const zs = P.peaks.map(p=> Zs[p.j][p.i]); const trace = { x: xs, y: ys, z: zs, type:'scatter3d', mode:'markers', marker:{ size:4, symbol:'diamond' }, name:'peaks', hovertemplate: '#%{customdata} t1=%{x:.3f} min
t2=%{y:.1f} s', customdata: P.peaks.map((_,k)=>k+1) }; const gd = document.getElementById('vizPlot'); if (typeof window.__peaks3DTraceIdx === 'number'){ try{ Plotly.deleteTraces(gd, window.__peaks3DTraceIdx); }catch(e){} } Plotly.addTraces(gd, [trace]); window.__peaks3DTraceIdx = gd.data.length - 1; } // --- peaks 2D/3D (mínimo indispensável) --- (function(){ function percentileFlat99p5(Z){ const a = []; for (let j=0;jx-y); const idx = 0.995*(a.length-1), lo=Math.floor(idx), hi=Math.ceil(idx), f=idx-lo; return (1-f)*a[lo] + f*a[hi]; } // núcleo: devolve lista de peaks sobre uma matriz 2D Z (linhas=y, colunas=x) function detectPeaks2DCore(Z, xAxis, yAxis, thrRel=0.2, footPx=5, minSep=3){ const zcut = percentileFlat99p5(Z) * Math.max(0, thrRel||0); const r = Math.max(1, Math.floor((footPx||3)/2)); const H = Z.length, W = Z[0]?.length || 0; const cand=[]; for(let j=r;jzcut)) continue; let isMax = true; for(let dj=-r; dj<=r && isMax; dj++){ for(let di=-r; di<=r; di++){ if(di===0 && dj===0) continue; if (Z[j+dj][i+di] >= v){ isMax=false; break; } } } if(isMax) cand.push([i,j,v]); } } cand.sort((a,b)=>b[2]-a[2]); // non-maximum suppression por distância mínima em px const keep=[]; const dmin=Math.max(1, (minSep|0)); for(const [i,j,v] of cand){ let ok=true; for(const [ii,jj,_] of keep){ if(Math.hypot(i-ii, j-jj) <= dmin){ ok=false; break; } } if(ok) keep.push([i,j,v]); if(keep.length>2000) break; } return keep.map(([i,j,v])=>({ i, j, x:xAxis[i], y:yAxis[j], z:v })); } // 2D peak detection — use ONLY the chromatogram shown on screen (no 3D/regional-max dependency) function runDetect2D(){ // Expect the current heatmap to be stored by renderChrom2D const S = window.__last2D; if (!S || !S.Z || !S.x || !S.y) { alert('Generate the 2D chromatogram first.'); return; } // Tunable parameters (feel free to surface as UI later) // thrRel: relative cut vs. 99.5th percentile; footPx: neighbourhood half-size in pixels; minSep: min distance between peaks const thrRel = 0.20; // 20% of P99.5 — robust to scale const footPx = 7; // ~15×15 window const minSeparation = 5; // avoid near-duplicate peaks // Detect directly on the displayed 2D image (includes smoothing/edits/transforms) const peaks = detectPeaks2DCore(S.Z, S.x, S.y, thrRel, footPx, minSeparation); // Reset previous overlay (if any) and store try { if (typeof window.__peaks2DTraceIdx === 'number') { const gd = document.getElementById('vizPlot'); Plotly.deleteTraces(gd, window.__peaks2DTraceIdx); } } catch(e) { /* no-op */ } window.__peaks2DTraceIdx = null; window.__peaks2D = { peaks, x: S.x, y: S.y }; // Draw overlay if we have peaks if (peaks.length > 0) { redrawPeaksOverlay2D(); } // Render list with delete/select handlers (kept identical to previous behavior) renderPeaksList( window.__peaks2D.peaks, S.x, S.y, 'peaksList2d', 500, // onDelete: (idx)=>{ window.__peaks2D.peaks.splice(idx,1); if (window.__peakSel2D === idx) window.__peakSel2D = null; else if (window.__peakSel2D > idx) window.__peakSel2D--; redrawPeaksOverlay2D(); renderPeaksList(window.__peaks2D.peaks, S.x, S.y, 'peaksList2d', 500, arguments.callee, onSelect2D, window.__peakSel2D); highlightPeak2D(window.__peakSel2D); }, // onSelect: onSelect2D, // selectedIdx: window.__peakSel2D ); function onSelect2D(idx){ window.__peakSel2D = idx; renderPeaksList(window.__peaks2D.peaks, S.x, S.y, 'peaksList2d', 500, (i)=>{ window.__peaks2D.peaks.splice(i,1); if (window.__peakSel2D === i) window.__peakSel2D = null; else if (window.__peakSel2D > i) window.__peakSel2D--; redrawPeaksOverlay2D(); renderPeaksList(window.__peaks2D.peaks, S.x, S.y, 'peaksList2d', 500, arguments.callee, onSelect2D, window.__peakSel2D); highlightPeak2D(window.__peakSel2D); }, onSelect2D, window.__peakSel2D); highlightPeak2D(idx); // Auto-scroll to the selected row const host = document.getElementById('peaksList2d'); const row = host && host.querySelector(`tr[data-idx="${idx}"]`); if (row && host) host.scrollTo({ top: row.offsetTop - 24, behavior: 'smooth' }); } // Update the small info line with peak count const info = document.getElementById('info2d'); if (info) { info.textContent = (info.textContent||'').split('• peaks=')[0] + ` • peaks=${peaks.length}`; } } // 3D (surface): usa Z (intensidade “bruta”) para detetar e zScaled para posicionar na superfície function runDetect3D(){ const S = window.__last3DSurf, H = window.__last3DSurfScaled; if(!S || !S.Z || !S.x || !S.y || !H){ alert('Generate 3D (surface) first.'); return; } const peaks = detectPeaks2DCore(S.Z, S.x, S.y, 0.2, 5, 3); try{ if(typeof window.__peaks3DTraceIdx === 'number'){ Plotly.deleteTraces('vizPlot', window.__peaks3DTraceIdx); } }catch(e){} window.__peaks3DTraceIdx = null; if(!peaks.length) return; window.__peaks3Dsurf = {peaks, x:S.x, y:S.y}; redrawPeaksOverlay3D(); renderPeaksList( window.__peaks3Dsurf.peaks, S.x, S.y, 'peaksList3d', 500, // onDelete: (idx)=>{ window.__peaks3Dsurf.peaks.splice(idx,1); if (window.__peakSel3D === idx) window.__peakSel3D = null; else if (window.__peakSel3D > idx) window.__peakSel3D--; redrawPeaksOverlay3D(); renderPeaksList(window.__peaks3Dsurf.peaks, S.x, S.y, 'peaksList3d', 500, arguments.callee, onSelect3D, window.__peakSel3D); highlightPeak3D(window.__peakSel3D); }, // onSelect: onSelect3D, // selectedIdx: window.__peakSel3D ); function onSelect3D(idx){ window.__peakSel3D = idx; renderPeaksList(window.__peaks3Dsurf.peaks, S.x, S.y, 'peaksList3d', 500, (i)=>{ window.__peaks3Dsurf.peaks.splice(i,1); if (window.__peakSel3D === i) window.__peakSel3D = null; else if (window.__peakSel3D > i) window.__peakSel3D--; redrawPeaksOverlay3D(); renderPeaksList(window.__peaks3Dsurf.peaks, S.x, S.y, 'peaksList3d', 500, arguments.callee, onSelect3D, window.__peakSel3D); highlightPeak3D(window.__peakSel3D); }, onSelect3D, window.__peakSel3D); highlightPeak3D(idx); const host = document.getElementById('peaksList3d'); const row = host.querySelector(`tr[data-idx="${idx}"]`); if (row && host) host.scrollTo({ top: row.offsetTop - 24, behavior: 'smooth' }); } } // listeners para os botões novos const b2 = document.getElementById('detect2D'); const b3 = document.getElementById('detect3D'); // This ID doesn't exist in the HTML, so this listener will not attach to anything. if(b2) b2.addEventListener('click', runDetect2D); if(b3) b3.addEventListener('click', runDetect3D); })();