The spectrum chart was missing xMin/xMax persistence and data boundary clamping in enablePan(). Align with the background/cps chart patterns. Bump cache versions to force browser refresh. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
258 lines
9.0 KiB
JavaScript
258 lines
9.0 KiB
JavaScript
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 = '<p style="color:#888">Background non disponible</p>';
|
||
return;
|
||
}
|
||
|
||
const info = await infoResp.json();
|
||
const spec = await specResp.json();
|
||
|
||
// Stats
|
||
document.getElementById('bg-stats').innerHTML = `
|
||
<div class="bg-stat"><div class="bg-stat-value">${info.elapsed_hours.toFixed(1)}h</div><div class="bg-stat-label">Durée</div></div>
|
||
<div class="bg-stat"><div class="bg-stat-value">${info.live_time_s.toFixed(0)}s</div><div class="bg-stat-label">Live time</div></div>
|
||
<div class="bg-stat"><div class="bg-stat-value">${info.total_counts.toFixed(0)}</div><div class="bg-stat-label">Coups</div></div>
|
||
<div class="bg-stat"><div class="bg-stat-value">${info.cps.toFixed(2)}</div><div class="bg-stat-label">CPS</div></div>
|
||
`;
|
||
|
||
// 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 existingMin = bgChart?.scales.x?.min;
|
||
const existingMax = bgChart?.scales.x?.max;
|
||
const xMin = existingMin ?? spec.energy_kev[0];
|
||
const xMax = existingMax ?? spec.energy_kev[spec.energy_kev.length - 1];
|
||
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: {
|
||
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',
|
||
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 });
|
||
enablePan(bgChart, 'reset-zoom-bg', spec.energy_kev[0], spec.energy_kev[spec.energy_kev.length - 1]);
|
||
}
|
||
}
|
||
|
||
function updatePeaksTable(peaks) {
|
||
const container = document.getElementById('peaks-table');
|
||
if (!peaks || peaks.length === 0) {
|
||
container.innerHTML = '<p style="color:#888;text-align:center;padding:8px;">Pas assez de données pour identifier les pics</p>';
|
||
return;
|
||
}
|
||
|
||
let html = '<h3 style="margin-bottom:8px;color:#ff9800;">Pics détectés</h3>';
|
||
peaks.forEach(p => {
|
||
html += `<div class="peak-row">
|
||
<span style="color:#4fc3f7">${p.energy_kev.toFixed(1)} keV</span>
|
||
<span>${p.counts.toFixed(0)} cts</span>
|
||
</div>`;
|
||
});
|
||
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; |