/ concept-collection / ephys_compression_tests
concept-collection / ephys_compression_tests
change timeseries zoom behavior
Jeremy Magland <jmagland@flatironinstitute.org> committed commit d36478f612f7 parent c6f25e2 Browse files
1 changed file+122−22
web-ui/src/components/dataset/TimeseriesView.tsxmodified+122−22View file
@@ -33,7 +33,10 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
3333 useState<HTMLCanvasElement | null>(null);
3434 const [state, dispatch] = useReducer(timeseriesViewReducer, initialState);
3535 const { selectedIndex, isDragging, lastDragX, xRange } = state;
36- const [isWheelEnabled, setIsWheelEnabled] = useState(false);
36+ // Wheel zooming is disabled because it causes the page to scroll when the user
37+ // tries to scroll the timeseries view, creating a poor user experience.
38+ // Instead, we provide explicit zoom control buttons.
39+ const [isWheelEnabled] = useState(false); // Keep state for potential future use, but always false
3740 const [showHint, setShowHint] = useState(true);
3841
3942 // Hide hint when user interacts with the graph
@@ -128,6 +131,7 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
128131 return; // Allow page scrolling if wheel zoom not enabled
129132 }
130133 e.preventDefault();
134+ e.stopPropagation();
131135
132136 const rect = container.getBoundingClientRect();
133137 const x = e.clientX - rect.left;
@@ -158,7 +162,7 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
158162 return () => {
159163 container.removeEventListener("wheel", handleWheel);
160164 };
161- }, [container, client, width, margins, xRange, isWheelEnabled]);
165+ }, [container, client, width, margins, isWheelEnabled]);
162166
163167 // Set up mouse event listeners for panning
164168 useEffect(() => {
@@ -349,11 +353,6 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
349353 const handleCanvasClick = (e: React.MouseEvent<HTMLDivElement>) => {
350354 if (!overlayCanvasElement || (!dataY && !dataYAll) || isDragging) return;
351355
352- // Enable wheel zooming on first click
353- if (!isWheelEnabled) {
354- setIsWheelEnabled(true);
355- }
356-
357356 const rect = overlayCanvasElement.getBoundingClientRect();
358357 const x = e.clientX - rect.left;
359358 const xRatio = (x - margins.left) / (width - margins.left - margins.right);
@@ -363,6 +362,39 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
363362 }
364363 };
365364
365+ // Zoom control functions - zoom centered on the selected index (current timepoint)
366+ const handleZoomIn = () => {
367+ if (!client) return;
368+ // Use selectedIndex as center if set, otherwise use view center
369+ const center = selectedIndex !== -1 ? selectedIndex : (xRange.min + xRange.max) / 2;
370+ const currentRange = xRange.max - xRange.min;
371+ const newRange = currentRange / 1.5; // Zoom in by 1.5x
372+ const newMin = Math.max(0, center - newRange / 2);
373+ const newMax = Math.min(client.getShape() - 1, center + newRange / 2);
374+ dispatch({ type: "SET_X_RANGE", range: { min: newMin, max: newMax } });
375+ };
376+
377+ const handleZoomOut = () => {
378+ if (!client) return;
379+ // Use selectedIndex as center if set, otherwise use view center
380+ const center = selectedIndex !== -1 ? selectedIndex : (xRange.min + xRange.max) / 2;
381+ const currentRange = xRange.max - xRange.min;
382+ const newRange = currentRange * 1.5; // Zoom out by 1.5x
383+ const shape = client.getShape();
384+ const newMin = Math.max(0, center - newRange / 2);
385+ const newMax = Math.min(shape - 1, center + newRange / 2);
386+ dispatch({ type: "SET_X_RANGE", range: { min: newMin, max: newMax } });
387+ };
388+
389+ const handleZoomReset = () => {
390+ if (!client) return;
391+ const shape = client.getShape();
392+ dispatch({
393+ type: "SET_X_RANGE",
394+ range: { min: 0, max: Math.min(999, shape - 1) },
395+ });
396+ };
397+
366398 return (
367399 <div style={{ position: "relative", width, height: height + 50 }}>
368400 <div style={{ marginBottom: 10, height: 20 }}>
@@ -439,23 +471,91 @@ const TimeseriesView: React.FC<TimeseriesViewProps> = ({
439471 <path d="M15 3h2v5h-2V3zm4 0h2v5h-2V3zm-4 7h2v5h-2v-5zm4 0h2v5h-2v-5zm-4 7h2v5h-2v-5zm4 0h2v5h-2v-5z" />
440472 </svg>
441473 </div>
442- <div
443- style={{
444- display: "flex",
445- alignItems: "center",
446- gap: "4px",
447- backgroundColor: "rgba(255, 255, 255, 0.9)",
448- padding: "2px 6px",
449- borderRadius: "4px",
450- }}
451- >
452- <span>Scroll to zoom</span>
453- <svg width="14" height="14" viewBox="0 0 24 24" fill="#666">
454- <path d="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zm0 16c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zm1-11h-2v3H8v2h3v3h2v-3h3v-2h-3V8z" />
455- </svg>
456- </div>
457474 </div>
458475 )}
476+ {/* Zoom control buttons - positioned at bottom right to avoid blocking channel selector */}
477+ <div
478+ style={{
479+ position: "absolute",
480+ bottom: margins.bottom + 10,
481+ right: margins.right + 10,
482+ display: "flex",
483+ gap: "6px",
484+ zIndex: 10,
485+ }}
486+ >
487+ <button
488+ onClick={handleZoomIn}
489+ disabled={!client}
490+ style={{
491+ padding: "6px 8px",
492+ fontSize: "14px",
493+ borderRadius: "4px",
494+ border: "1px solid #ccc",
495+ backgroundColor: "white",
496+ cursor: client ? "pointer" : "not-allowed",
497+ opacity: client ? 1 : 0.5,
498+ display: "flex",
499+ alignItems: "center",
500+ justifyContent: "center",
501+ }}
502+ title="Zoom in"
503+ >
504+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
505+ <circle cx="11" cy="11" r="8" />
506+ <path d="M21 21l-4.35-4.35" />
507+ <line x1="8" y1="11" x2="14" y2="11" />
508+ <line x1="11" y1="8" x2="11" y2="14" />
509+ </svg>
510+ </button>
511+ <button
512+ onClick={handleZoomOut}
513+ disabled={!client}
514+ style={{
515+ padding: "6px 8px",
516+ fontSize: "14px",
517+ borderRadius: "4px",
518+ border: "1px solid #ccc",
519+ backgroundColor: "white",
520+ cursor: client ? "pointer" : "not-allowed",
521+ opacity: client ? 1 : 0.5,
522+ display: "flex",
523+ alignItems: "center",
524+ justifyContent: "center",
525+ }}
526+ title="Zoom out"
527+ >
528+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
529+ <circle cx="11" cy="11" r="8" />
530+ <path d="M21 21l-4.35-4.35" />
531+ <line x1="8" y1="11" x2="14" y2="11" />
532+ </svg>
533+ </button>
534+ <button
535+ onClick={handleZoomReset}
536+ disabled={!client}
537+ style={{
538+ padding: "6px 8px",
539+ fontSize: "14px",
540+ borderRadius: "4px",
541+ border: "1px solid #ccc",
542+ backgroundColor: "white",
543+ cursor: client ? "pointer" : "not-allowed",
544+ opacity: client ? 1 : 0.5,
545+ display: "flex",
546+ alignItems: "center",
547+ justifyContent: "center",
548+ }}
549+ title="Reset zoom"
550+ >
551+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
552+ <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
553+ <path d="M21 3v5h-5" />
554+ <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
555+ <path d="M3 21v-5h5" />
556+ </svg>
557+ </button>
558+ </div>
459559 <div
460560 ref={setContainer}
461561 style={{ position: "relative", width, height }}