# US-TRD-017: Zoom y Pan en el Chart ## Metadata | Campo | Valor | |-------|-------| | **ID** | US-TRD-017 | | **Épica** | OQI-003 - Trading y Charts | | **Módulo** | trading | | **Prioridad** | P1 | | **Story Points** | 3 | | **Sprint** | Sprint 5 | | **Estado** | Pendiente | | **Asignado a** | Por asignar | --- ## Historia de Usuario **Como** trader, **quiero** hacer zoom y desplazarme (pan) en el chart usando mouse, trackpad o touch, **para** analizar detalles específicos del precio en diferentes escalas temporales y niveles de zoom. ## Descripción Detallada El usuario debe poder navegar por el chart de forma fluida, haciendo zoom in/out para ver más o menos velas, y desplazándose horizontalmente para ver datos históricos. La navegación debe ser intuitiva usando mouse wheel, pinch gestures, o botones. ## Mockups/Wireframes ``` ┌─────────────────────────────────────────────────────────────────┐ │ BTCUSDT $97,234.50 +2.34% ▲ │ ├─────────────────────────────────────────────────────────────────┤ │ [1m] [5m] [15m] [1h] [4h] [1D] [1W] [Indicators ▼] │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ZOOM CONTROLS: │ │ [+] [-] [Fit] [Auto] │ │ │ │ ┌────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ ◄── Pan Left Chart Area Pan Right ──►│ │ │ │ │ │ │ │ ████ │ │ │ │ ████ ████ Zoom Level: 100% │ │ │ │ ████ ████ ████ Candles visible: 168 │ │ │ │ ████ ████ │ │ │ │ ████ │ │ │ │ │ │ │ │ [ Scroll to zoom ] [ Drag to pan ] │ │ │ │ [ Pinch gesture ] [ Double-click reset ] │ │ │ │ │ │ │ └────────────────────────────────────────────────────────┘ │ │ │ │ TIMELINE: │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ ░░░░░░░░░░████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ Nov 1 Dec 5 (Current view) Jan 1 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ GESTOS SOPORTADOS: ┌─────────────────────────────────────┐ │ Mouse: │ │ • Scroll wheel: Zoom in/out │ │ • Click + Drag: Pan left/right │ │ • Double-click: Reset zoom │ │ │ │ Trackpad: │ │ • Pinch: Zoom in/out │ │ • Two-finger drag: Pan │ │ │ │ Touch (Mobile): │ │ • Pinch: Zoom in/out │ │ • Swipe: Pan left/right │ │ • Double-tap: Reset zoom │ │ │ │ Keyboard: │ │ • +/-: Zoom in/out │ │ • Arrow keys: Pan left/right │ │ • Home/End: Go to start/end │ │ • 0: Reset zoom to 100% │ └─────────────────────────────────────┘ ``` --- ## Criterios de Aceptación **Escenario 1: Zoom in con mouse wheel** ```gherkin DADO que el usuario está viendo el chart con 168 velas visibles CUANDO hace scroll hacia adelante (wheel up) ENTONCES el chart hace zoom in Y muestra menos velas (ej: 84 velas) Y las velas se ven más grandes y detalladas Y el zoom se centra en la posición del cursor ``` **Escenario 2: Zoom out con mouse wheel** ```gherkin DADO que el usuario está viendo el chart con 168 velas CUANDO hace scroll hacia atrás (wheel down) ENTONCES el chart hace zoom out Y muestra más velas (ej: 336 velas) Y las velas se ven más pequeñas Y puede ver un rango temporal más amplio ``` **Escenario 3: Pan con click y drag** ```gherkin DADO que el usuario está viendo el chart CUANDO hace click y arrastra hacia la izquierda ENTONCES el chart se desplaza hacia la derecha Y muestra datos históricos más antiguos Y el desplazamiento es fluido y sigue el cursor CUANDO arrastra hacia la derecha ENTONCES el chart se desplaza hacia la izquierda Y muestra datos más recientes ``` **Escenario 4: Zoom con pinch gesture (mobile/trackpad)** ```gherkin DADO que el usuario está en mobile o usando trackpad CUANDO hace pinch out (separar dedos) ENTONCES el chart hace zoom in Y las velas se agrandan CUANDO hace pinch in (juntar dedos) ENTONCES el chart hace zoom out Y se ven más velas ``` **Escenario 5: Reset zoom con double-click** ```gherkin DADO que el usuario ha hecho zoom y pan CUANDO hace double-click en el chart ENTONCES el zoom se resetea a 100% Y se muestra el rango por defecto (168 velas) Y se centra en las velas más recientes ``` **Escenario 6: Zoom con botones** ```gherkin DADO que el usuario hace click en botón [+] ENTONCES el chart hace zoom in un 20% DADO que hace click en botón [-] ENTONCES el chart hace zoom out un 20% DADO que hace click en botón [Fit] ENTONCES el chart se ajusta para mostrar todas las velas disponibles DADO que hace click en botón [Auto] ENTONCES el chart vuelve al zoom automático (latest candles) ``` **Escenario 7: Pan con teclado** ```gherkin DADO que el usuario presiona flecha izquierda ENTONCES el chart se desplaza hacia la izquierda (muestra datos antiguos) DADO que presiona flecha derecha ENTONCES el chart se desplaza hacia la derecha (muestra datos recientes) DADO que presiona Home ENTONCES el chart va al inicio (primera vela disponible) DADO que presiona End ENTONCES el chart va al final (última vela - presente) ``` **Escenario 8: Límites de zoom** ```gherkin DADO que el usuario hace zoom in al máximo CUANDO intenta hacer más zoom ENTONCES el chart no hace más zoom Y muestra mínimo 20 velas (zoom máximo) DADO que hace zoom out al máximo ENTONCES muestra todas las velas disponibles Y no permite zoom out adicional ``` **Escenario 9: Timeline navigation** ```gherkin DADO que el usuario ve el timeline debajo del chart CUANDO hace click en una posición del timeline ENTONCES el chart salta a ese rango temporal Y se centra en la fecha clickeada ``` ## Criterios Adicionales - [ ] Animación suave de zoom y pan (60 FPS) - [ ] Indicador visual del rango visible en timeline - [ ] Mantener zoom level al cambiar timeframe - [ ] Auto-scroll al último precio cuando hay nuevas velas - [ ] Minimap para navegación rápida --- ## Tareas Técnicas **Database:** - No requiere cambios en DB **Backend:** - [ ] BE-TRD-094: Optimizar endpoint candles para rangos variables - [ ] BE-TRD-095: Implementar paginación eficiente de velas históricas **Frontend:** - [ ] FE-TRD-093: Configurar zoom en Lightweight Charts - [ ] FE-TRD-094: Implementar pan con mouse drag - [ ] FE-TRD-095: Implementar zoom con wheel - [ ] FE-TRD-096: Implementar pinch gestures (mobile/trackpad) - [ ] FE-TRD-097: Crear componente ZoomControls.tsx - [ ] FE-TRD-098: Crear componente Timeline.tsx - [ ] FE-TRD-099: Implementar keyboard shortcuts - [ ] FE-TRD-100: Implementar límites de zoom - [ ] FE-TRD-101: Implementar hook useChartNavigation **Tests:** - [ ] TEST-TRD-046: Test unitario zoom logic - [ ] TEST-TRD-047: Test integración pan y zoom - [ ] TEST-TRD-048: Test E2E navegación completa --- ## Dependencias **Depende de:** - [ ] US-TRD-001: Ver chart - Estado: Pendiente **Bloquea:** - Ninguna --- ## Notas Técnicas **Componentes UI:** - `ZoomControls`: Botones de zoom - `Timeline`: Barra de navegación temporal - `ChartContainer`: Wrapper que maneja eventos **Lightweight Charts Configuration:** ```typescript const chart = createChart(container, { timeScale: { rightOffset: 12, barSpacing: 3, fixLeftEdge: false, fixRightEdge: false, lockVisibleTimeRangeOnResize: true, rightBarStaysOnScroll: true, borderVisible: true, borderColor: '#fff000', visible: true, timeVisible: true, secondsVisible: false, shiftVisibleRangeOnNewBar: true, }, handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: false, }, handleScale: { axisPressedMouseMove: true, mouseWheel: true, pinch: true, }, }); ``` **Zoom Implementation:** ```typescript // Zoom with mouse wheel function handleWheel(event: WheelEvent) { event.preventDefault(); const chart = chartRef.current; if (!chart) return; const delta = event.deltaY; const zoomFactor = delta > 0 ? 0.9 : 1.1; // Zoom out / Zoom in const timeScale = chart.timeScale(); const visibleRange = timeScale.getVisibleRange(); if (!visibleRange) return; const from = visibleRange.from as number; const to = visibleRange.to as number; const center = (from + to) / 2; const newRange = (to - from) * zoomFactor; timeScale.setVisibleRange({ from: center - newRange / 2, to: center + newRange / 2, }); } // Zoom with buttons function zoomIn() { const timeScale = chart.timeScale(); const range = timeScale.getVisibleRange(); if (!range) return; const center = (range.from + range.to) / 2; const newRange = (range.to - range.from) * 0.8; // 20% zoom in timeScale.setVisibleRange({ from: center - newRange / 2, to: center + newRange / 2, }); } function zoomOut() { const timeScale = chart.timeScale(); const range = timeScale.getVisibleRange(); if (!range) return; const center = (range.from + range.to) / 2; const newRange = (range.to - range.from) * 1.2; // 20% zoom out timeScale.setVisibleRange({ from: center - newRange / 2, to: center + newRange / 2, }); } // Reset zoom function resetZoom() { chart.timeScale().fitContent(); } // Auto zoom (show latest) function autoZoom() { chart.timeScale().scrollToRealTime(); } ``` **Pan Implementation:** ```typescript // Pan with mouse drag let isDragging = false; let startX = 0; function handleMouseDown(event: MouseEvent) { isDragging = true; startX = event.clientX; container.style.cursor = 'grabbing'; } function handleMouseMove(event: MouseEvent) { if (!isDragging) return; const deltaX = event.clientX - startX; startX = event.clientX; const timeScale = chart.timeScale(); const range = timeScale.getVisibleRange(); if (!range) return; const rangeWidth = range.to - range.from; const containerWidth = container.clientWidth; const pixelToTime = rangeWidth / containerWidth; const shift = -deltaX * pixelToTime; timeScale.setVisibleRange({ from: range.from + shift, to: range.to + shift, }); } function handleMouseUp() { isDragging = false; container.style.cursor = 'default'; } ``` **Touch/Pinch Implementation:** ```typescript let lastDistance = 0; function handleTouchMove(event: TouchEvent) { if (event.touches.length === 2) { // Pinch zoom event.preventDefault(); const touch1 = event.touches[0]; const touch2 = event.touches[1]; const distance = Math.hypot( touch2.clientX - touch1.clientX, touch2.clientY - touch1.clientY ); if (lastDistance > 0) { const zoomFactor = distance / lastDistance; applyZoom(zoomFactor); } lastDistance = distance; } } function handleTouchEnd() { lastDistance = 0; } ``` **Keyboard Shortcuts:** ```typescript function handleKeyDown(event: KeyboardEvent) { const timeScale = chart.timeScale(); switch (event.key) { case '+': case '=': zoomIn(); break; case '-': case '_': zoomOut(); break; case 'ArrowLeft': pan(-50); // Pan left 50 pixels break; case 'ArrowRight': pan(50); // Pan right 50 pixels break; case 'Home': timeScale.scrollToPosition(0, false); break; case 'End': timeScale.scrollToRealTime(); break; case '0': resetZoom(); break; } } ``` **Zoom Limits:** ```typescript const MIN_VISIBLE_CANDLES = 20; const MAX_VISIBLE_CANDLES = 1000; function applyZoom(zoomFactor: number) { const timeScale = chart.timeScale(); const range = timeScale.getVisibleRange(); if (!range) return; const currentCandles = calculateVisibleCandles(range); const newCandles = currentCandles / zoomFactor; // Apply limits if (newCandles < MIN_VISIBLE_CANDLES || newCandles > MAX_VISIBLE_CANDLES) { return; // Don't zoom beyond limits } // Apply zoom const center = (range.from + range.to) / 2; const newRange = (range.to - range.from) * zoomFactor; timeScale.setVisibleRange({ from: center - newRange / 2, to: center + newRange / 2, }); } ``` **Timeline Component:** ```typescript function Timeline({ chart, candles }) { const [visibleRange, setVisibleRange] = useState(null); useEffect(() => { const timeScale = chart.timeScale(); const updateRange = () => { setVisibleRange(timeScale.getVisibleRange()); }; timeScale.subscribeVisibleTimeRangeChange(updateRange); return () => timeScale.unsubscribeVisibleTimeRangeChange(updateRange); }, [chart]); const handleTimelineClick = (position: number) => { // Jump to clicked position in timeline const timeScale = chart.timeScale(); const totalRange = candles[candles.length - 1].time - candles[0].time; const clickedTime = candles[0].time + (totalRange * position); timeScale.scrollToPosition(clickedTime, true); }; return (
{ const rect = e.currentTarget.getBoundingClientRect(); const position = (e.clientX - rect.left) / rect.width; handleTimelineClick(position); }}> {/* Render timeline visualization */}
); } ``` --- ## Definition of Ready (DoR) - [x] Historia claramente escrita - [x] Criterios de aceptación definidos - [x] Story points estimados - [x] Dependencias identificadas - [x] Sin bloqueadores - [ ] Diseño/mockup disponible - [ ] API spec disponible ## Definition of Done (DoD) - [ ] Código implementado según criterios - [ ] Tests unitarios escritos y pasando - [ ] Tests de integración pasando - [ ] Code review aprobado - [ ] Documentación actualizada - [ ] QA aprobado - [ ] Desplegado en ambiente de pruebas --- ## Historial de Cambios | Fecha | Cambio | Autor | |-------|--------|-------| | 2025-12-05 | Creación | Requirements-Analyst | --- **Creada por:** Requirements-Analyst **Fecha:** 2025-12-05 **Última actualización:** 2025-12-05