From 6c78c136229caa6e55cf8e9e120cccdcb2ecb55a Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Wed, 20 May 2026 00:50:56 +0200 Subject: [PATCH] Fix: add data boundaries to spectrum pan + bump cache versions 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 --- web/static/index.html | 7 +-- web/static/js/app.js | 27 +++++++++++ web/static/js/background.js | 17 ++++--- web/static/js/chart_pan.js | 89 ++++++++++++++++++++++++++++++++++++ web/static/js/cps.js | 16 ++++--- web/static/js/detectors.json | 75 ++++++++++++++++++++++++++++++ web/static/js/spectrum.js | 17 ++++--- 7 files changed, 225 insertions(+), 23 deletions(-) create mode 100644 web/static/js/chart_pan.js create mode 100644 web/static/js/detectors.json diff --git a/web/static/index.html b/web/static/index.html index 2735fde..647d9ac 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -84,10 +84,11 @@ - + + - - + + \ No newline at end of file diff --git a/web/static/js/app.js b/web/static/js/app.js index 0b24635..4b6fdc1 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -2,6 +2,33 @@ const API_BASE = ''; let refreshInterval = null; const REFRESH_MS = 30000; // 30 seconds +// Load detector config +const DETECTOR_CONFIG = {}; +async function loadDetectorConfig() { + try { + const resp = await fetch(`${API_BASE}/static/js/detectors.json`); + if (!resp.ok) return; + const data = await resp.json(); + Object.assign(DETECTOR_CONFIG, data); + if (DETECTOR_CONFIG.default) { + const d = DETECTOR_CONFIG.detectors[DETECTOR_CONFIG.default]?.display || {}; + Object.assign(DETECTOR_CONFIG, d); + // Apply default chart settings from config + if (typeof d.default_log_scale === 'boolean') { + const el = document.getElementById('log-scale'); + if (el) el.checked = d.default_log_scale; + const el2 = document.getElementById('bg-scale-log'); + if (el2) el2.checked = d.default_log_scale; + } + if (typeof d.default_smooth === 'boolean') { + const el = document.getElementById('show-bg-smooth'); + if (el) el.checked = d.default_smooth; + } + } + } catch { /* use defaults */ } +} +loadDetectorConfig(); + // Tab navigation document.querySelectorAll('nav a').forEach(link => { link.addEventListener('click', e => { diff --git a/web/static/js/background.js b/web/static/js/background.js index 9db3bd4..7027581 100644 --- a/web/static/js/background.js +++ b/web/static/js/background.js @@ -151,6 +151,10 @@ function updateBackgroundChart(spec) { 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, @@ -168,24 +172,22 @@ function updateBackgroundChart(spec) { } }, 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'; } + 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' }, @@ -206,6 +208,7 @@ function updateBackgroundChart(spec) { 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]); } } diff --git a/web/static/js/chart_pan.js b/web/static/js/chart_pan.js new file mode 100644 index 0000000..7d1dc96 --- /dev/null +++ b/web/static/js/chart_pan.js @@ -0,0 +1,89 @@ +/** + * Manual pan handler for Chart.js charts with zoom. + * Enables click-and-drag to pan on the X axis, clamped to data boundaries. + * Call enablePan(chart, chartId, dataMin, dataMax) after chart creation. + */ +function enablePan(chart, resetBtnId, dataMin, dataMax) { + const canvas = chart.canvas; + let isPanning = false; + let startClientX = 0; + let startMin = 0; + let startMax = 0; + let xScaleWidth = 0; + + canvas.addEventListener('mousedown', (e) => { + if (e.button !== 0) return; + isPanning = true; + startClientX = e.clientX; + const xScale = chart.scales.x; + startMin = xScale.min; + startMax = xScale.max; + xScaleWidth = xScale.width; + canvas.style.cursor = 'grabbing'; + e.preventDefault(); + e.stopPropagation(); + }); + + const onMove = (clientX) => { + if (!isPanning) return; + const xScale = chart.scales.x; + const pixelDiff = clientX - startClientX; + const valuePerPixel = (startMax - startMin) / xScaleWidth; + const shift = -pixelDiff * valuePerPixel; + + let newMin = startMin + shift; + let newMax = startMax + shift; + + // Clamp to data boundaries + if (newMin < dataMin) { + newMin = dataMin; + newMax = newMin + (startMax - startMin); + } + if (newMax > dataMax) { + newMax = dataMax; + newMin = newMax - (startMax - startMin); + } + + // Set via options to persist through update + // Set via chart.options to persist, and store on chart for next update + chart.options.scales.x.min = newMin; + chart.options.scales.x.max = newMax; + chart._panRange = [newMin, newMax]; + chart.update('none'); + }; + + canvas.addEventListener('mousemove', (e) => { + onMove(e.clientX); + }); + + canvas.addEventListener('mouseup', () => { + if (isPanning) { + isPanning = false; + canvas.style.cursor = ''; + const btn = document.getElementById(resetBtnId); + if (btn) btn.style.display = 'inline-block'; + } + }); + + canvas.addEventListener('mouseleave', () => { + if (isPanning) { + isPanning = false; + canvas.style.cursor = ''; + } + }); + + // Document-level listeners for drag-outside-canvas + const docUp = () => { + if (isPanning) { + isPanning = false; + canvas.style.cursor = ''; + const btn = document.getElementById(resetBtnId); + if (btn) btn.style.display = 'inline-block'; + } + }; + const docMove = (e) => { + if (isPanning) onMove(e.clientX); + }; + document.addEventListener('mouseup', docUp); + document.addEventListener('mousemove', docMove); +} diff --git a/web/static/js/cps.js b/web/static/js/cps.js index 56ee1a9..c2823f9 100644 --- a/web/static/js/cps.js +++ b/web/static/js/cps.js @@ -41,6 +41,10 @@ function updateCpsChart(labels, values) { }] }; + const existingMin = cpsChart?.scales.x?.min; + const existingMax = cpsChart?.scales.x?.max; + const xMin = existingMin ?? labels[0]; + const xMax = existingMax ?? labels[labels.length - 1]; const options = { responsive: true, maintainAspectRatio: false, @@ -61,23 +65,22 @@ function updateCpsChart(labels, values) { } }, zoom: { - pan: { - enabled: true, - mode: 'x', - modifierKey: null, - }, zoom: { wheel: { enabled: true }, pinch: { enabled: true }, drag: { enabled: false }, mode: 'x', - onZoom: () => { document.getElementById('reset-zoom-cps').style.display = 'inline-block'; } + onZoomComplete: () => { + document.getElementById('reset-zoom-cps').style.display = 'inline-block'; + } } } }, scales: { x: { type: 'time', + min: xMin, + max: xMax, time: { tooltipFormat: 'dd/MM HH:mm', displayFormats: { minute: 'HH:mm', hour: 'HH:mm', day: 'dd/MM' } @@ -105,6 +108,7 @@ function updateCpsChart(labels, values) { script.src = 'https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js'; script.onload = () => { cpsChart = new Chart(ctx, { type: 'line', data: chartData, ...options }); + enablePan(cpsChart, 'reset-zoom-cps', labels[0], labels[labels.length - 1]); }; document.head.appendChild(script); } diff --git a/web/static/js/detectors.json b/web/static/js/detectors.json new file mode 100644 index 0000000..0af145e --- /dev/null +++ b/web/static/js/detectors.json @@ -0,0 +1,75 @@ +{ + "default": "radiacode103", + "detectors": { + "radiacode103": { + "name": "Radiacode 103", + "scintillator": "CsI(Tl)", + "energy_kev": { "min": 30, "max": 3000 }, + "channels": 1024, + "calibration": { "offset": 0.33, "slope": 2.97 }, + "fwhm_at_662": 8.4, + "sensitivity": "30 cps / 1 uSv/h (Cs-137)", + "display": { + "zoom_limits_x": { "min": 30, "max": 3000 }, + "default_log_scale": true, + "default_smooth": true + } + }, + "csi50": { + "name": "CsI 50x50mm", + "scintillator": "CsI(Tl)", + "energy_kev": { "min": 20, "max": 3000 }, + "channels": 1024, + "calibration": { "offset": 0, "slope": 3.0 }, + "fwhm_at_662": 7.0, + "sensitivity": "—", + "display": { + "zoom_limits_x": { "min": 20, "max": 3000 }, + "default_log_scale": true, + "default_smooth": true + } + }, + "naicl": { + "name": "NaI(Tl)", + "scintillator": "NaI(Tl)", + "energy_kev": { "min": 20, "max": 3000 }, + "channels": 1024, + "calibration": { "offset": 0, "slope": 3.0 }, + "fwhm_at_662": 7.0, + "sensitivity": "—", + "display": { + "zoom_limits_x": { "min": 20, "max": 3000 }, + "default_log_scale": true, + "default_smooth": true + } + }, + "hpge": { + "name": "HPGe", + "scintillator": "Ge (semiconductor)", + "energy_kev": { "min": 5, "max": 6000 }, + "channels": 4096, + "calibration": { "offset": 0, "slope": 1.5 }, + "fwhm_at_662": 1.8, + "sensitivity": "—", + "display": { + "zoom_limits_x": { "min": 5, "max": 6000 }, + "default_log_scale": true, + "default_smooth": false + } + }, + "lacl": { + "name": "LaBr3(Ce)", + "scintillator": "LaBr₃:Ce", + "energy_kev": { "min": 20, "max": 3500 }, + "channels": 2048, + "calibration": { "offset": 0, "slope": 1.7 }, + "fwhm_at_662": 3.5, + "sensitivity": "—", + "display": { + "zoom_limits_x": { "min": 20, "max": 3500 }, + "default_log_scale": true, + "default_smooth": false + } + } + } +} diff --git a/web/static/js/spectrum.js b/web/static/js/spectrum.js index a638745..cecbfbd 100644 --- a/web/static/js/spectrum.js +++ b/web/static/js/spectrum.js @@ -58,6 +58,10 @@ function updateSpectrumChart(data) { annotations = buildIsotopeAnnotations(detectedOnly, (data.isotopes_detected || []).map(i => i.isotope)); } + const existingMin = spectrumChart?.scales.x?.min; + const existingMax = spectrumChart?.scales.x?.max; + const xMin = existingMin ?? data.energy_kev[0]; + const xMax = existingMax ?? data.energy_kev[data.energy_kev.length - 1]; const options = { responsive: true, maintainAspectRatio: false, @@ -82,24 +86,22 @@ function updateSpectrumChart(data) { annotations: annotations }, 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-spectrum').style.display = 'inline-block'; } + onZoomComplete: () => { + document.getElementById('reset-zoom-spectrum').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' }, @@ -120,6 +122,7 @@ function updateSpectrumChart(data) { spectrumChart.update(); } else { spectrumChart = new Chart(ctx, { type: 'line', data: chartData, ...options }); + enablePan(spectrumChart, 'reset-zoom-spectrum', data.energy_kev[0], data.energy_kev[data.energy_kev.length - 1]); } }