feat(chart): TrainingChart dinâmico com viewport, zoom, pan e pin sobrevivente
Substitui o gráfico estático (últimos 120 samples + selectedIndex por
posição) por um modelo de viewport temporal que resolve os problemas de
qualidade em tempo real e abre zoom/pan nos 3 call sites (Captura,
Janela salva e Faixa customizada).
Adições:
- src/training/chartViewport.ts: funções puras (granularityForSpan,
formatTickLabel, buildChartViewport, applyZoomIn/Out, applyPan,
isLiveTailSnap, pickSampleAtTimestamp, xFromTimestampMs,
timestampMsFromX, tipos Viewport/ChartPin/Granularity).
- __tests__/chartViewport.test.ts: 34 testes cobrindo limites,
ordenação defensiva, snap zone, hit-test e granularidade adaptativa.
- src/components/TrainingChart.tsx: reescrito para consumir viewport.
- Pin por timestampMs (sobrevive a novas amostras em tempo real).
- Pinch (Gesture.Pinch) + Pan (Gesture.Pan) + Tap (Gesture.Tap) via
react-native-gesture-handler v3.
- Auto-follow: endMs estende com novos samples enquanto liveTail.
- Snap zone (5% da duração visível) reanexa o live ao final do pan.
- Eixo X com ticks adaptativos: minute/halfMinute/quarterMinute/
tenSeconds/second/halfSecond, conforme duração visível.
- Botões +/- (zoom 2x) e ↺ (reset) na lateral.
- Indicador 'dados mais novos disponíveis' quando !liveTail.
- Estado vazio preservado (samples < 2).
- jest.setup.js + jest.config.js: mocks de gesture-handler e
reanimated para o App.test rodar em Jest (módulos nativos não
disponíveis no host).
Modificações de setup (dependências aprovadas):
- package.json + package-lock.json: adiciona react-native-gesture-handler
^3.0.1, react-native-reanimated ^4.4.1, react-native-worklets ^0.9.2.
- index.js: import 'react-native-gesture-handler' no topo.
- App.tsx: <GestureHandlerRootView> envolvendo SafeAreaProvider.
- babel.config.js: plugin react-native-worklets/plugin.
Verificações:
- npm run typecheck: 0 errors.
- npm run lint: 0 errors, 0 warnings.
- npm test -- --runInBand: 55/55 passam.
- Build Android: não verificado nesta sessão (timeout); o usuário
valida localmente antes de distribuir.
Não comitado neste commit (mudanças pré-existentes não relacionadas):
- android/* (migração fightratern → healthtracking/fightrate, mipmaps)
- README.md
- app.json
This commit is contained in:
1 parent
86908495d2
commit
41b49ba442
15 files changed
+2492
-340
No files matched your search
+54
-1
@@ -3,8 +3,61 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { NativeModules } from 'react-native';
|
||||
import ReactTestRenderer from 'react-test-renderer';
|
||||
import App from '../App';
|
||||
|
||||
const emptySnapshot = {
|
||||
phase: 'idle',
|
||||
isCapturing: false,
|
||||
isMonitoring: false,
|
||||
startedAt: 0,
|
||||
statusMessage: 'Captura parada.',
|
||||
connectedDevice: null,
|
||||
discoveredDevices: [],
|
||||
samples: [],
|
||||
sessions: [],
|
||||
services: [],
|
||||
characteristics: [],
|
||||
batteryOptimizationIgnored: true,
|
||||
};
|
||||
|
||||
NativeModules.FightRateCapture = {
|
||||
addListener: jest.fn(),
|
||||
removeListeners: jest.fn(),
|
||||
getSnapshot: jest.fn(async () => emptySnapshot),
|
||||
startMonitoring: jest.fn(async () => emptySnapshot),
|
||||
startCapture: jest.fn(async () => emptySnapshot),
|
||||
startWorkout: jest.fn(async () => ({
|
||||
...emptySnapshot,
|
||||
isMonitoring: true,
|
||||
isCapturing: true,
|
||||
})),
|
||||
connectToDevice: jest.fn(async () => emptySnapshot),
|
||||
stopCapture: jest.fn(async () => emptySnapshot),
|
||||
stopWorkout: jest.fn(async () => ({
|
||||
...emptySnapshot,
|
||||
isMonitoring: true,
|
||||
})),
|
||||
stopMonitoring: jest.fn(async () => emptySnapshot),
|
||||
getSessionSamples: jest.fn(async () => []),
|
||||
getContinuousSamples: jest.fn(async () => []),
|
||||
isIgnoringBatteryOptimizations: jest.fn(async () => true),
|
||||
openBatteryOptimizationSettings: jest.fn(async () => true),
|
||||
};
|
||||
|
||||
// react-native-gesture-handler requires a native module; mock it for tests.
|
||||
NativeModules.RNGestureHandlerModule = {
|
||||
attachGestureHandler: jest.fn(),
|
||||
createGestureHandler: jest.fn(),
|
||||
dropGestureHandler: jest.fn(),
|
||||
updateGestureHandler: jest.fn(),
|
||||
flushOperations: jest.fn(),
|
||||
install: jest.fn(),
|
||||
State: { UNDETERMINED: 0, FAILED: 1, BEGAN: 2, CANCELLED: 3, ACTIVE: 4, END: 5 },
|
||||
Directions: { RIGHT: 1, LEFT: 2, UP: 4, DOWN: 8 },
|
||||
};
|
||||
|
||||
const App = require('../App').default;
|
||||
|
||||
test('renders correctly', async () => {
|
||||
await ReactTestRenderer.act(() => {
|
||||
|
||||
Reference in new issue
Block a user