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 <noreply@anthropic.com>
This commit is contained in:
@ -84,10 +84,11 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/static/js/isotope_lines.js?v=3"></script>
|
<script src="/static/js/isotope_lines.js?v=3"></script>
|
||||||
<script src="/static/js/spectrum.js?v=6"></script>
|
<script src="/static/js/chart_pan.js?v=3"></script>
|
||||||
|
<script src="/static/js/spectrum.js?v=9"></script>
|
||||||
<script src="/static/js/history.js?v=2"></script>
|
<script src="/static/js/history.js?v=2"></script>
|
||||||
<script src="/static/js/background.js?v=11"></script>
|
<script src="/static/js/background.js?v=17"></script>
|
||||||
<script src="/static/js/cps.js?v=5"></script>
|
<script src="/static/js/cps.js?v=7"></script>
|
||||||
<script src="/static/js/app.js?v=3"></script>
|
<script src="/static/js/app.js?v=3"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -2,6 +2,33 @@ const API_BASE = '';
|
|||||||
let refreshInterval = null;
|
let refreshInterval = null;
|
||||||
const REFRESH_MS = 30000; // 30 seconds
|
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
|
// Tab navigation
|
||||||
document.querySelectorAll('nav a').forEach(link => {
|
document.querySelectorAll('nav a').forEach(link => {
|
||||||
link.addEventListener('click', e => {
|
link.addEventListener('click', e => {
|
||||||
|
|||||||
@ -151,6 +151,10 @@ function updateBackgroundChart(spec) {
|
|||||||
datasets: datasets,
|
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 = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@ -168,24 +172,22 @@ function updateBackgroundChart(spec) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: {
|
|
||||||
enabled: true,
|
|
||||||
mode: 'x',
|
|
||||||
modifierKey: null,
|
|
||||||
},
|
|
||||||
zoom: {
|
zoom: {
|
||||||
wheel: { enabled: true },
|
wheel: { enabled: true },
|
||||||
pinch: { enabled: true },
|
pinch: { enabled: true },
|
||||||
drag: { enabled: false },
|
drag: { enabled: false },
|
||||||
mode: 'x',
|
mode: 'x',
|
||||||
limits: { x: { min: 30, max: 3000 } },
|
onZoomComplete: () => {
|
||||||
onZoom: () => { document.getElementById('reset-zoom-bg').style.display = 'inline-block'; }
|
document.getElementById('reset-zoom-bg').style.display = 'inline-block';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
|
min: xMin,
|
||||||
|
max: xMax,
|
||||||
title: { display: true, text: 'Énergie (keV)', color: '#888' },
|
title: { display: true, text: 'Énergie (keV)', color: '#888' },
|
||||||
ticks: { color: '#888', maxTicksLimit: 20 },
|
ticks: { color: '#888', maxTicksLimit: 20 },
|
||||||
grid: { color: '#333' },
|
grid: { color: '#333' },
|
||||||
@ -206,6 +208,7 @@ function updateBackgroundChart(spec) {
|
|||||||
bgChart.update();
|
bgChart.update();
|
||||||
} else {
|
} else {
|
||||||
bgChart = new Chart(ctx, { type: 'line', data: chartData, ...options });
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
web/static/js/chart_pan.js
Normal file
89
web/static/js/chart_pan.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
@ -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 = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@ -61,23 +65,22 @@ function updateCpsChart(labels, values) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: {
|
|
||||||
enabled: true,
|
|
||||||
mode: 'x',
|
|
||||||
modifierKey: null,
|
|
||||||
},
|
|
||||||
zoom: {
|
zoom: {
|
||||||
wheel: { enabled: true },
|
wheel: { enabled: true },
|
||||||
pinch: { enabled: true },
|
pinch: { enabled: true },
|
||||||
drag: { enabled: false },
|
drag: { enabled: false },
|
||||||
mode: 'x',
|
mode: 'x',
|
||||||
onZoom: () => { document.getElementById('reset-zoom-cps').style.display = 'inline-block'; }
|
onZoomComplete: () => {
|
||||||
|
document.getElementById('reset-zoom-cps').style.display = 'inline-block';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
|
min: xMin,
|
||||||
|
max: xMax,
|
||||||
time: {
|
time: {
|
||||||
tooltipFormat: 'dd/MM HH:mm',
|
tooltipFormat: 'dd/MM HH:mm',
|
||||||
displayFormats: { minute: 'HH:mm', hour: 'HH:mm', day: 'dd/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.src = 'https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js';
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
cpsChart = new Chart(ctx, { type: 'line', data: chartData, ...options });
|
cpsChart = new Chart(ctx, { type: 'line', data: chartData, ...options });
|
||||||
|
enablePan(cpsChart, 'reset-zoom-cps', labels[0], labels[labels.length - 1]);
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|||||||
75
web/static/js/detectors.json
Normal file
75
web/static/js/detectors.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,6 +58,10 @@ function updateSpectrumChart(data) {
|
|||||||
annotations = buildIsotopeAnnotations(detectedOnly, (data.isotopes_detected || []).map(i => i.isotope));
|
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 = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
@ -82,24 +86,22 @@ function updateSpectrumChart(data) {
|
|||||||
annotations: annotations
|
annotations: annotations
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
pan: {
|
|
||||||
enabled: true,
|
|
||||||
mode: 'x',
|
|
||||||
modifierKey: null,
|
|
||||||
},
|
|
||||||
zoom: {
|
zoom: {
|
||||||
wheel: { enabled: true },
|
wheel: { enabled: true },
|
||||||
pinch: { enabled: true },
|
pinch: { enabled: true },
|
||||||
drag: { enabled: false },
|
drag: { enabled: false },
|
||||||
mode: 'x',
|
mode: 'x',
|
||||||
limits: { x: { min: 30, max: 3000 } },
|
onZoomComplete: () => {
|
||||||
onZoom: () => { document.getElementById('reset-zoom-spectrum').style.display = 'inline-block'; }
|
document.getElementById('reset-zoom-spectrum').style.display = 'inline-block';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: 'linear',
|
type: 'linear',
|
||||||
|
min: xMin,
|
||||||
|
max: xMax,
|
||||||
title: { display: true, text: 'Énergie (keV)', color: '#888' },
|
title: { display: true, text: 'Énergie (keV)', color: '#888' },
|
||||||
ticks: { color: '#888', maxTicksLimit: 20 },
|
ticks: { color: '#888', maxTicksLimit: 20 },
|
||||||
grid: { color: '#333' },
|
grid: { color: '#333' },
|
||||||
@ -120,6 +122,7 @@ function updateSpectrumChart(data) {
|
|||||||
spectrumChart.update();
|
spectrumChart.update();
|
||||||
} else {
|
} else {
|
||||||
spectrumChart = new Chart(ctx, { type: 'line', data: chartData, ...options });
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user