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); })();