Dash web: crosshair, zoom/pan X, scale log/lin, continuum extraction, background resume

- Tooltip entier (intersect:false) + ligne verticale crosshair sur tous les graphes
- Zoom molette/pinch sur l'axe X, pan souris, limites clamped 30-3000 keV
- Toggle échelle log/linéaire onglet Background
- Extraction continuum détecteur (isotope peaks subtracted + Gaussian smoothing)
- Reprise snapshot précédent au démarrage capture_background.py
- Suppression refs "Théorique" et "Bruit capteur" de l'interface
- Plugin chartjs-plugin-zoom + hammerjs via CDN
- Fix Chart constructor spread operator

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jacquin Antoine
2026-05-19 23:26:28 +02:00
parent 0f2417bf88
commit c764a5c264
15 changed files with 975 additions and 221 deletions

View File

@ -1,6 +1,5 @@
let bgChart = null;
let bgReferenceData = null;
let bgTheoreticalData = null;
let bgContinuumData = null;
async function loadBgReference() {
@ -11,14 +10,6 @@ async function loadBgReference() {
} 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}`);
@ -30,7 +21,7 @@ async function loadBgContinuum(cps, liveTime) {
/**
* Gaussian kernel smoothing.
* Convolves the data with a Gaussian kernel of given sigma (in channels).
* Preserves peak shapes while removing statistical noise.
* Preserves peak shapes while reducing statistical variation.
*/
function smoothGaussian(data, sigma) {
if (!data || data.length === 0) return data;
@ -79,12 +70,7 @@ async function refreshBackground() {
<div class="bg-stat"><div class="bg-stat-value">${info.cps.toFixed(2)}</div><div class="bg-stat-label">CPS</div></div>
`;
// 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
// Load continuum on first load
if (!bgContinuumData && spec.live_time_s > 0) {
await loadBgContinuum(info.cps || 6.0, spec.live_time_s);
}
@ -102,9 +88,9 @@ async function refreshBackground() {
}
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 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;
@ -119,8 +105,6 @@ function updateBackgroundChart(spec) {
}];
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é',
@ -133,22 +117,9 @@ function updateBackgroundChart(spec) {
});
}
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)',
label: 'Continuum',
data: bgContinuumData.counts,
borderColor: 'rgba(156, 39, 176, 0.8)',
backgroundColor: 'rgba(156, 39, 176, 0.05)',
@ -183,13 +154,33 @@ function updateBackgroundChart(spec) {
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: {
@ -200,21 +191,21 @@ function updateBackgroundChart(spec) {
grid: { color: '#333' },
},
y: {
type: 'logarithmic',
title: { display: true, text: 'Comptages (log)', color: '#888' },
min: 0.9,
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) {
if (bgChart) {
bgChart.data = chartData;
bgChart.options = options;
bgChart.update();
} else {
bgChart = new Chart(ctx, { type: 'line', data: chartData, options });
bgChart = new Chart(ctx, { type: 'line', data: chartData, ...options });
}
}
@ -242,19 +233,23 @@ document.querySelector('[data-tab="background"]').addEventListener('click', () =
// 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());
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;