let bgChart = null; let bgReferenceData = null; let bgTheoreticalData = null; let bgContinuumData = null; async function loadBgReference() { try { const resp = await fetch(`${API_BASE}/api/background/reference`); if (!resp.ok) return; bgReferenceData = await resp.json(); } catch {} } async function loadBgTheoretical(cps, liveTime) { try { const resp = await fetch(`${API_BASE}/api/background/theoretical?cps=${cps}&live_time_s=${liveTime}`); if (!resp.ok) return; bgTheoreticalData = await resp.json(); } catch {} } async function loadBgContinuum(cps, liveTime) { try { const resp = await fetch(`${API_BASE}/api/background/continuum?cps=${cps}&live_time_s=${liveTime}`); if (!resp.ok) return; bgContinuumData = await resp.json(); } catch {} } /** * Gaussian kernel smoothing. * Convolves the data with a Gaussian kernel of given sigma (in channels). * Preserves peak shapes while removing statistical noise. */ function smoothGaussian(data, sigma) { if (!data || data.length === 0) return data; const kernelRadius = Math.ceil(sigma * 3); const kernel = []; for (let i = -kernelRadius; i <= kernelRadius; i++) { kernel.push(Math.exp(-0.5 * (i / sigma) ** 2)); } const result = new Array(data.length); for (let i = 0; i < data.length; i++) { let sum = 0; let wSum = 0; for (let k = -kernelRadius; k <= kernelRadius; k++) { const idx = i + k; if (idx < 0 || idx >= data.length) continue; const w = kernel[k + kernelRadius]; sum += data[idx] * w; wSum += w; } result[i] = wSum > 0 ? sum / wSum : 0; } return result; } async function refreshBackground() { try { const [infoResp, specResp] = await Promise.all([ fetch(`${API_BASE}/api/background`), fetch(`${API_BASE}/api/background/spectrum`) ]); if (!infoResp.ok || !specResp.ok) { document.getElementById('bg-stats').innerHTML = '

Background non disponible

'; return; } const info = await infoResp.json(); const spec = await specResp.json(); // Stats document.getElementById('bg-stats').innerHTML = `
${info.elapsed_hours.toFixed(1)}h
Durée
${info.live_time_s.toFixed(0)}s
Live time
${info.total_counts.toFixed(0)}
Coups
${info.cps.toFixed(2)}
CPS
`; // Load theoretical curve on first load if (!bgTheoreticalData && spec.live_time_s > 0) { await loadBgTheoretical(info.cps || 6.0, spec.live_time_s); } // Load CsI(Tl) continuum on first load if (!bgContinuumData && spec.live_time_s > 0) { await loadBgContinuum(info.cps || 6.0, spec.live_time_s); } // Chart updateBackgroundChart(spec); // Peaks table updatePeaksTable(info.top_peaks || []); // Show/hide toggles const refToggle = document.getElementById('show-bg-reference'); if (refToggle) refToggle.parentElement.style.display = spec.reference_available ? 'flex' : 'none'; } catch {} } function updateBackgroundChart(spec) { const ctx = document.getElementById('background-chart').getContext('2d'); const showRef = document.getElementById('show-bg-reference')?.checked && bgReferenceData; const showTheory = document.getElementById('show-bg-theoretical')?.checked && bgTheoreticalData; const showSmooth = document.getElementById('show-bg-smooth')?.checked; const showContinuum = document.getElementById('show-bg-continuum')?.checked && bgContinuumData; const datasets = [{ label: 'Background (live)', data: spec.counts, borderColor: '#ff9800', backgroundColor: 'rgba(255, 152, 0, 0.1)', borderWidth: 1, pointRadius: 0, fill: true, }]; if (showSmooth) { // Smoothed version of live data — sigma=8 channels (~24 keV) // Wide enough to remove noise, narrow enough to preserve the 100 keV peak const smoothed = smoothGaussian(spec.counts, 8); datasets.push({ label: 'Lissé', data: smoothed, borderColor: 'rgba(233, 30, 99, 0.9)', backgroundColor: 'rgba(233, 30, 99, 0.05)', borderWidth: 2, pointRadius: 0, fill: false, }); } if (showTheory) { datasets.push({ label: 'Théorique', data: bgTheoreticalData.counts, borderColor: 'rgba(76, 175, 80, 0.7)', backgroundColor: 'rgba(76, 175, 80, 0.05)', borderWidth: 1.5, pointRadius: 0, fill: true, borderDash: [6, 3], }); } if (showContinuum) { datasets.push({ label: 'Continuum CsI(Tl)', data: bgContinuumData.counts, borderColor: 'rgba(156, 39, 176, 0.8)', backgroundColor: 'rgba(156, 39, 176, 0.05)', borderWidth: 2, pointRadius: 0, fill: false, borderDash: [8, 4], }); } if (showRef) { const scale = spec.live_time_s > 0 && bgReferenceData.live_time_s > 0 ? spec.live_time_s / bgReferenceData.live_time_s : 1; datasets.push({ label: `Référence 24h (×${scale.toFixed(1)})`, data: bgReferenceData.counts.map(c => c * scale), borderColor: 'rgba(79, 195, 247, 0.8)', backgroundColor: 'rgba(79, 195, 247, 0.08)', borderWidth: 1, pointRadius: 0, fill: true, borderDash: [4, 2], }); } const chartData = { labels: spec.energy_kev, datasets: datasets, }; const options = { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#e0e0e0' } }, tooltip: { callbacks: { title: (items) => `${spec.energy_kev[items[0].dataIndex]} keV`, label: (item) => `${item.dataset.label}: ${item.raw.toFixed(1)} counts` } } }, scales: { x: { type: 'linear', title: { display: true, text: 'Énergie (keV)', color: '#888' }, ticks: { color: '#888', maxTicksLimit: 20 }, grid: { color: '#333' }, }, y: { type: 'logarithmic', title: { display: true, text: 'Comptages (log)', color: '#888' }, min: 0.9, ticks: { color: '#888' }, grid: { color: '#333' }, } } }; if (bgChart) { bgChart.data = chartData; bgChart.options = options; bgChart.update(); } else { bgChart = new Chart(ctx, { type: 'line', data: chartData, options }); } } function updatePeaksTable(peaks) { const container = document.getElementById('peaks-table'); if (!peaks || peaks.length === 0) { container.innerHTML = '

Pas assez de données pour identifier les pics

'; return; } let html = '

Pics détectés

'; peaks.forEach(p => { html += `
${p.energy_kev.toFixed(1)} keV ${p.counts.toFixed(0)} cts
`; }); container.innerHTML = html; } document.querySelector('[data-tab="background"]').addEventListener('click', () => { refreshBackground(); loadBgReference(); }); // Toggle handlers document.getElementById('show-bg-reference')?.addEventListener('change', () => refreshBackground()); document.getElementById('show-bg-theoretical')?.addEventListener('change', () => { if (document.getElementById('show-bg-theoretical').checked && !bgTheoreticalData) { loadBgTheoretical(6.0, 3600).then(() => refreshBackground()); } else { refreshBackground(); } }); document.getElementById('show-bg-continuum')?.addEventListener('change', () => { if (document.getElementById('show-bg-continuum').checked && !bgContinuumData) { const info = document.getElementById('bg-stats'); loadBgContinuum(6.0, 3600).then(() => refreshBackground()); } else { refreshBackground(); } }); document.getElementById('show-bg-smooth')?.addEventListener('change', () => refreshBackground());