Zoom & Pan
Enable interactive exploration of large datasets with zoom and pan controls.
Overview
While vuer-viz charts are stateless (they don't manage zoom/pan internally), you can easily implement these features by controlling the xMin/xMax props and handling mouse events. This approach gives you complete control over the interaction behavior.
Key Concepts:
- Charts accept
xMin,xMax,yMin,yMaxprops to control the visible range - Mouse wheel events (with modifiers) can implement zoom
- Mouse drag events can implement pan
- Combine both for full interactive exploration
Horizontal Zoom
Zoom horizontally using mouse wheel + modifier key (Option/Alt):
Hold Option/Alt + Scroll to zoom. Range: 0.0 - 199.0 (99.5% visible).
import { useState } from "react";
import { LineChart, TimeSeries, VerticalMarker, useZoom } from "@vuer-ai/vuer-viz";
const x = Array.from({ length: 200 }, (_, i) => i);
const y = x.map(i => 50 + Math.sin(i / 10) * 30 + Math.random() * 10);
export default function HorizontalZoomExample() {
const [xMin, setXMin] = useState(0);
const [xMax, setXMax] = useState(199);
const [hoverX, setHoverX] = useState<number | null>(null);
const { plotRef } = useZoom({min : xMin, max : xMax, setMin : setXMin, setMax : setXMax, minLimit : 0, maxLimit : 199});
const visibleRange = ((xMax - xMin) / 200 * 100).toFixed(1);
return (
<div>
<LineChart
plotRef={plotRef}
width={700}
height={300}
xLabel="Sample"
yLabel="Value"
title="Horizontal Zoom (Option + Scroll)"
xMin={xMin}
xMax={xMax}
onChange={(e) => e.type === "hover" && setHoverX(e.x)}
>
<TimeSeries label="Signal" x={x} y={y} color="#3498db" />
<VerticalMarker x={hoverX} color="#34495e" lineWidth={1} dashArray="4,2" />
</LineChart>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Hold Option/Alt + Scroll to zoom. Range: {xMin.toFixed(1)} - {xMax.toFixed(1)} ({visibleRange}% visible).
</p>
</div>
);
}
Implementation details:
- Listen to
onWheelevents on a wrapper div - Check for
e.altKeyto require Option/Alt modifier - Calculate new range based on zoom factor (1.1 for zoom out, 0.9 for zoom in)
- Update
xMinandxMaxprops to reflect new range - Clamp values to prevent zooming beyond data bounds
Why use a modifier key? Requiring Option/Alt prevents accidental zooming during normal page scrolling.
Pan with Drag
Pan horizontally by clicking and dragging:
Click and drag to pan. Viewing x = 0.0 to 99.0.
import { useState } from "react";
import { LineChart, usePan } from "@vuer-ai/vuer-viz";
const x = Array.from({ length: 200 }, (_, i) => i);
const y = x.map(i => 50 + Math.sin(i / 10) * 30 + Math.random() * 10);
export default function PanDragExample() {
const [xMin, setXMin] = useState(0);
const [xMax, setXMax] = useState(99);
const { plotRef } = usePan({min : xMin, max : xMax, setMin : setXMin, setMax : setXMax, minLimit : 0, maxLimit : 199});
const series = [
{
label: "Temperature",
x,
y,
color: "#e74c3c",
},
];
return (
<div>
<LineChart
plotRef={plotRef}
series={series}
width={700}
height={300}
xLabel="Time"
yLabel="Temperature (°C)"
title="Pan with Drag"
xMin={xMin}
xMax={xMax}
enableInteractiveCursor={true}
/>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Click and drag to pan. Viewing x = {xMin.toFixed(1)} to {xMax.toFixed(1)}.
</p>
</div>
);
}
Implementation details:
- Track drag state with
useStateanduseRef - On mouse down: record starting position and current x range
- On mouse move: calculate pixel delta, convert to data units, update range
- Update cursor style:
grabwhen idle,grabbingwhen dragging - Clamp to data bounds to prevent panning beyond limits
Combined Zoom & Pan
Combine both interactions for complete dataset exploration:
Option + Scroll to zoom, Click + Drag to pan. Viewing x = 0.0 to 199.0 (39.8% of data).
import { useState } from "react";
import { LineChart, VerticalMarker, useZoomPan } from "@vuer-ai/vuer-viz";
const x = Array.from({ length: 500 }, (_, i) => i);
const y = x.map(i => 50 + Math.sin(i / 20) * 30 + Math.cos(i / 15) * 20 + Math.random() * 10);
export default function CombinedZoomPanExample() {
const [xMin, setXMin] = useState(0);
const [xMax, setXMax] = useState(199);
const [hoverX, setHoverX] = useState<number | null>(null);
const { plotRef } = useZoomPan({min : xMin, max : xMax, setMin : setXMin, setMax : setXMax, minLimit : 0, maxLimit : 499});
const series = [
{
label: "Sensor Data",
x,
y,
color: "#9b59b6",
lineWidth: 2,
},
];
const visiblePercent = ((xMax - xMin) / 500 * 100).toFixed(1);
return (
<div>
<LineChart
plotRef={plotRef}
series={series}
width={700}
height={350}
xLabel="Time (samples)"
yLabel="Amplitude"
title="Combined Zoom & Pan"
xMin={xMin}
xMax={xMax}
onChange={(e) => e.type === "hover" && setHoverX(e.x)}
enableInteractiveCursor={true}
>
<VerticalMarker x={hoverX} color="#34495e" />
{/* Mini range indicator */}
<rect
x={60}
y={15}
width={Math.max(0, (xMin / 500) * 620)}
height={8}
fill="#95a5a6"
opacity={0.3}
/>
<rect
x={60 + (xMin / 500) * 620}
y={15}
width={((xMax - xMin) / 500) * 620}
height={8}
fill="#9b59b6"
opacity={0.7}
/>
<rect
x={60 + (xMax / 500) * 620}
y={15}
width={Math.max(0, ((500 - xMax) / 500) * 620)}
height={8}
fill="#95a5a6"
opacity={0.3}
/>
</LineChart>
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
Option + Scroll to zoom, Click + Drag to pan. Viewing x = {xMin.toFixed(1)} to {xMax.toFixed(1)} ({visiblePercent}% of data).
</p>
</div>
);
}
Features in this example:
- Option + Scroll to zoom horizontally
- Click + Drag to pan
- Hover to show cursor position
- Mini range indicator showing current view within full dataset
- Reset and zoom-out buttons for quick navigation
Implementation Patterns
Basic Zoom Handler
const handleWheel = useCallback((e: React.WheelEvent) => { if (!e.altKey) return; // Require modifier key e.preventDefault(); const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9; const currentRange = xMax - xMin; const newRange = currentRange * zoomFactor; // Limit zoom range if (newRange < minRange || newRange > maxRange) return; // Zoom toward center const center = (xMax + xMin) / 2; setXMin(center - newRange / 2); setXMax(center + newRange / 2); }, [xMin, xMax]);
Basic Pan Handler
const dragStartRef = useRef<{x: number, xMin: number, xMax: number} | null>(null); const handleMouseDown = (e: React.MouseEvent) => { dragStartRef.current = { x: e.clientX, xMin, xMax, }; }; const handleMouseMove = (e: React.MouseEvent) => { if (!dragStartRef.current) return; const deltaPixels = e.clientX - dragStartRef.current.x; const plotWidth = 620; // Approximate plot area width const dataRange = dragStartRef.current.xMax - dragStartRef.current.xMin; // Convert pixel delta to data delta (negative because drag right = pan left) const deltaData = -(deltaPixels / plotWidth) * dataRange; setXMin(dragStartRef.current.xMin + deltaData); setXMax(dragStartRef.current.xMax + deltaData); }; const handleMouseUp = () => { dragStartRef.current = null; };
Zoom Toward Mouse Position
For more intuitive zooming, zoom toward the mouse cursor instead of the center:
const handleWheelAtCursor = useCallback((e: React.WheelEvent) => { if (!e.altKey) return; e.preventDefault(); const rect = e.currentTarget.getBoundingClientRect(); const mouseX = e.clientX - rect.left; const plotWidth = 620; // Approximate plot width // Calculate mouse position in data coordinates const mouseDataX = xMin + (mouseX / plotWidth) * (xMax - xMin); const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9; const newRange = (xMax - xMin) * zoomFactor; // Zoom while keeping mouse position fixed const leftRatio = (mouseDataX - xMin) / (xMax - xMin); setXMin(mouseDataX - newRange * leftRatio); setXMax(mouseDataX + newRange * (1 - leftRatio)); }, [xMin, xMax]);
Range Indicators
Show users where they are in the full dataset:
function MiniRangeIndicator({ xMin, xMax, dataMin, dataMax, width }) { const totalRange = dataMax - dataMin; const startPercent = (xMin - dataMin) / totalRange; const endPercent = (xMax - dataMin) / totalRange; return ( <g> {/* Background (unviewed regions) */} <rect x={60} y={15} width={startPercent * width} height={8} fill="#95a5a6" opacity={0.3} /> {/* Current view */} <rect x={60 + startPercent * width} y={15} width={(endPercent - startPercent) * width} height={8} fill="#3498db" opacity={0.7} /> {/* Remaining */} <rect x={60 + endPercent * width} y={15} width={(1 - endPercent) * width} height={8} fill="#95a5a6" opacity={0.3} /> </g> ); } <LineChart xMin={xMin} xMax={xMax}> <MiniRangeIndicator xMin={xMin} xMax={xMax} dataMin={0} dataMax={500} width={620} /> </LineChart>
Vertical Zoom
Zoom vertically by controlling yMin and yMax:
const [yMin, setYMin] = useState(0); const [yMax, setYMax] = useState(100); const handleVerticalZoom = useCallback((e: React.WheelEvent) => { if (!e.shiftKey) return; // Shift + Scroll for vertical e.preventDefault(); const zoomFactor = e.deltaY > 0 ? 1.1 : 0.9; const center = (yMax + yMin) / 2; const newRange = (yMax - yMin) * zoomFactor; setYMin(center - newRange / 2); setYMax(center + newRange / 2); }, [yMin, yMax]); <div onWheel={handleVerticalZoom}> <LineChart series={data} yMin={yMin} yMax={yMax} /> </div>
Constraints and Boundaries
Limit Zoom Levels
const MIN_RANGE = 10; const MAX_RANGE = 1000; if (newRange < MIN_RANGE || newRange > MAX_RANGE) { return; // Don't apply zoom }
Clamp to Data Bounds
let newXMin = center - newRange / 2; let newXMax = center + newRange / 2; // Clamp to data boundaries if (newXMin < dataMin) { newXMin = dataMin; newXMax = dataMin + newRange; } if (newXMax > dataMax) { newXMax = dataMax; newXMin = dataMax - newRange; } setXMin(newXMin); setXMax(newXMax);
Smooth Transitions
Add smooth transitions with CSS or animation libraries:
import { useSpring, animated } from '@react-spring/web'; const [{ xMin, xMax }, api] = useSpring(() => ({ xMin: 0, xMax: 100, })); // Animate to new range const zoomTo = (newXMin: number, newXMax: number) => { api.start({ xMin: newXMin, xMax: newXMax }); }; <LineChart xMin={xMin.get()} xMax={xMax.get()} />
Keyboard Navigation
Enhance accessibility with keyboard controls:
useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const panAmount = (xMax - xMin) * 0.1; switch (e.key) { case 'ArrowLeft': setXMin(xMin - panAmount); setXMax(xMax - panAmount); break; case 'ArrowRight': setXMin(xMin + panAmount); setXMax(xMax + panAmount); break; case '+': // Zoom in const zoomInRange = (xMax - xMin) * 0.9; const center = (xMax + xMin) / 2; setXMin(center - zoomInRange / 2); setXMax(center + zoomInRange / 2); break; case '-': // Zoom out const zoomOutRange = (xMax - xMin) * 1.1; const center2 = (xMax + xMin) / 2; setXMin(center2 - zoomOutRange / 2); setXMax(center2 + zoomOutRange / 2); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [xMin, xMax]);
Touch Support
Add touch gestures for mobile devices:
const [touchStart, setTouchStart] = useState<{x: number, y: number} | null>(null); const handleTouchStart = (e: React.TouchEvent) => { if (e.touches.length === 1) { setTouchStart({ x: e.touches[0].clientX, y: e.touches[0].clientY }); } }; const handleTouchMove = (e: React.TouchEvent) => { if (!touchStart || e.touches.length !== 1) return; const deltaX = e.touches[0].clientX - touchStart.x; const plotWidth = 620; const dataRange = xMax - xMin; const deltaData = -(deltaX / plotWidth) * dataRange; setXMin(xMin + deltaData); setXMax(xMax + deltaData); }; <div onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={() => setTouchStart(null)} > <LineChart ... /> </div>
Best Practices
1. Use Modifier Keys
Require modifier keys (Alt/Option, Shift) for zoom to avoid conflicts with page scrolling:
if (!e.altKey) return; // Require Option/Alt for zoom
2. Provide Visual Feedback
- Change cursor during drag (
grab→grabbing) - Show range indicators
- Display current zoom level
- Add loading states for large datasets
3. Add Reset Button
Always provide a way to reset to the default view:
<button onClick={() => { setXMin(0); setXMax(dataLength); }}> Reset Zoom </button>
4. Debounce Updates
For expensive re-renders, debounce range updates:
import { useDebouncedCallback } from 'use-debounce'; const debouncedSetRange = useDebouncedCallback( (newXMin, newXMax) => { setXMin(newXMin); setXMax(newXMax); }, 16 // ~60fps );
5. Clamp Values
Always validate and clamp range values to prevent invalid states:
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value)); setXMin(clamp(newXMin, dataMin, dataMax));
Performance Considerations
Canvas Rendering for Large Datasets
Use canvas={true} for better performance with large datasets:
<LineChart series={largeDataset} canvas={true} // Render data on canvas xMin={xMin} xMax={xMax} > {/* Overlays still render as SVG */} <VerticalMarker x={hoverX} /> </LineChart>
Data Windowing
Only pass visible data to the chart:
const visibleData = useMemo(() => { return fullData.filter(d => d.x >= xMin && d.x <= xMax); }, [fullData, xMin, xMax]); <LineChart series={[{ label: "Data", data: visibleData }]} />
Related Guides
- Event Handling - Learn about the
onChangeevent API - Markers & Ranges - Add annotations and range indicators
- Canvas Rendering - Performance optimization for large datasets