let bgChart = null;
let bgReferenceData = 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 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 datasets = [{
label: 'Background (live)',
data: 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: 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: 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,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { labels: { color: '#e0e0e0' } },
tooltip: {
enabled: true,
mode: 'index',
intersect: false,
filter: (item) => item.raw != null,
callbacks: {
title: (items) => `${spec.energy_kev[items[0].dataIndex]} keV`,
label: (item) => `${item.dataset.label}: ${item.raw.toFixed(1)} counts`
}
},
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: null,
},
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
drag: { enabled: false },
mode: 'x',
limits: { x: { min: 30, max: 3000 } },
onZoom: () => { document.getElementById('reset-zoom-bg').style.display = 'inline-block'; }
}
}
},
scales: {
x: {
type: 'linear',
title: { display: true, text: 'Énergie (keV)', color: '#888' },
ticks: { color: '#888', maxTicksLimit: 20 },
grid: { color: '#333' },
},
y: {
type: showLog ? 'logarithmic' : 'linear',
title: { display: true, text: `Comptages (${showLog ? 'log' : 'lin'})`, color: '#888' },
...(showLog ? { 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-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());
// Reset zoom button
document.getElementById('reset-zoom-bg')?.addEventListener('click', () => {
if (bgChart) {
bgChart.resetZoom();
document.getElementById('reset-zoom-bg').style.display = 'none';
}
});
// Show/hide reset button based on zoom state — wrapped via options plugin
const _origBgOptions = options => options;