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:
Jacquin Antoine
2026-05-20 00:50:56 +02:00
parent c764a5c264
commit 6c78c13622
7 changed files with 225 additions and 23 deletions

View 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);
}