trading-platform/docs/02-definicion-modulos/OQI-003-trading-charts/historias-usuario/US-TRD-017-zoom-pan-chart.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

16 KiB

id title type status priority epic story_points created_date updated_date
US-TRD-017 Zoom y Pan en el Chart User Story Done Alta OQI-003 3 2025-12-05 2026-01-04

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

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

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

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)

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

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

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

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

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

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:

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:

// 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:

// 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:

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:

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:

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:

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 (
    <div className="timeline" onClick={(e) => {
      const rect = e.currentTarget.getBoundingClientRect();
      const position = (e.clientX - rect.left) / rect.width;
      handleTimelineClick(position);
    }}>
      {/* Render timeline visualization */}
    </div>
  );
}

Definition of Ready (DoR)

  • Historia claramente escrita
  • Criterios de aceptación definidos
  • Story points estimados
  • Dependencias identificadas
  • 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