let bgChart = null; let bgReferenceData = null; let bgContinuumData = null; // Reset zoom button — fully restores the energy range document.getElementById('reset-zoom-bg')?.addEventListener('click', () => { if (bgChart) { bgChart.resetZoom(); delete bgChart._panRange; const firstPt = bgChart.data.datasets[0]?.data?.[0]; const lastPt = bgChart.data.datasets[0]?.data?.[bgChart.data.datasets[0].data.length - 1]; const fullMin = firstPt?.x ?? 0; const fullMax = lastPt?.x ?? 3000; bgChart.options.scales.x.min = fullMin; bgChart.options.scales.x.max = fullMax; bgChart.update(); document.getElementById('reset-zoom-bg').style.display = 'none'; } }); async function loadBgReference() { try { const resp = await fetch(`${API_BASE}/api/background/reference`); if (!resp.ok) return; bgReferenceData = 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 reducing statistical variation. */ 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 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 showLog = document.getElementById('bg-scale-log')?.checked; const ctx = document.getElementById('background-chart').getContext('2d'); const showRef = document.getElementById('show-bg-reference')?.checked && bgReferenceData; const showSmooth = document.getElementById('show-bg-smooth')?.checked; const showContinuum = document.getElementById('show-bg-continuum')?.checked && bgContinuumData; const energy = spec.energy_kev; const toData = (arr) => arr.map((v, i) => ({ x: energy[i], y: v })); const datasets = [{ label: 'Background (live)', data: toData(spec.counts), borderColor: '#ff9800', backgroundColor: 'rgba(255, 152, 0, 0.1)', borderWidth: 1, pointRadius: 0, fill: true, }]; if (showSmooth) { const smoothed = smoothGaussian(spec.counts, 8); datasets.push({ label: 'Lissé', data: toData(smoothed), borderColor: 'rgba(233, 30, 99, 0.9)', backgroundColor: 'rgba(233, 30, 99, 0.05)', borderWidth: 2, pointRadius: 0, fill: false, }); } if (showContinuum) { datasets.push({ label: 'Continuum', data: toData(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; const refEnergy = bgReferenceData.energy_kev || energy; datasets.push({ label: `Référence 24h (×${scale.toFixed(1)})`, data: bgReferenceData.counts.map((c, i) => ({ x: refEnergy[i] ?? energy[i], y: 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 = { datasets: datasets, }; // Preserve pan range only if user has explicitly zoomed/panned const panRange = bgChart?._panRange; const firstX = datasets[0].data[0]?.x ?? 0; const lastX = datasets[0].data[datasets[0].data.length - 1]?.x ?? 3000; const xMin = panRange ? panRange[0] : firstX; const xMax = panRange ? panRange[1] : lastX; const options = { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: '#e0e0e0' } }, tooltip: { enabled: true, mode: 'index', intersect: false, callbacks: { title: (items) => `${items[0].parsed.x.toFixed(1)} keV`, label: (item) => `${item.dataset.label}: ${item.parsed.y.toFixed(1)} counts` } }, zoom: { zoom: { wheel: { enabled: true }, pinch: { enabled: true }, drag: { enabled: false }, mode: 'x', onZoomComplete: () => { document.getElementById('reset-zoom-bg').style.display = 'inline-block'; } } } }, scales: { x: { type: 'linear', min: xMin, max: xMax, title: { display: true, text: 'Énergie (keV)', color: '#888' }, ticks: { color: '#888', maxTicksLimit: 20 }, grid: { color: '#333' }, }, y: { type: showLog ? 'logarithmic' : 'linear', min: showLog ? 0.5 : undefined, title: { display: true, text: `Comptages (${showLog ? 'log' : 'lin'})`, color: '#888' }, 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 }); const firstX = datasets[0].data[0]?.x ?? 0; const lastX = datasets[0].data[datasets[0].data.length - 1]?.x ?? 3000; enablePan(bgChart, 'reset-zoom-bg', firstX, lastX); } } 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-continuum')?.addEventListener('change', () => { if (document.getElementById('show-bg-continuum').checked && !bgContinuumData) { loadBgContinuum(6.0, 3600).then(() => refreshBackground()); } else { refreshBackground(); } }); document.getElementById('show-bg-smooth')?.addEventListener('change', () => refreshBackground()); document.getElementById('bg-scale-log')?.addEventListener('change', () => refreshBackground());