/** * 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); }