Rediseño Charts iPad, zoom en gráficas, eliminación de Focus Mode y fixes de rendimiento

Charts iPad (rediseño):
- Fila de KPIs sobre el chart: Total Value, Period Return, CAGR, Volatility y Max
  Drawdown del rango activo (nuevo ChartsViewModel.portfolioMetrics, calculado sobre
  los TOTALES MENSUALES AGREGADOS — pasar snapshots multi-source crudos a
  calculateMetrics producía KPIs absurdos)
- Sidebar (248pt) con tiles de chart type en grid 2 col agrupados por sección:
  Overview / Analyze / Risk / Forecast, con candado premium y accesibilidad
- Selector de periodo como Picker segmentado nativo en la cabecera del chart
  (fuera del sidebar); título + descripción del chart visibles
- Eliminado sheet de paywall duplicado del layout iPad

Zoom (todas las series temporales):
- Nuevo ChartZoom.swift: pinch + botones +/- y reset (HIG: el gesto nunca es el
  único camino), chartScrollableAxes + chartXVisibleDomain al hacer zoom
- Integrado en Evolution, Contributions, Rolling 12M, Cashflow, Drawdown y Volatility

Focus Mode eliminado (todo siempre disponible):
- Fuera el toggle de Dashboard/Charts/Settings/Onboarding y los 6 @AppStorage
- Todos los chart types siempre visibles; variantes completas en SourceDetail/SourceList
- Home: Total Portfolio Value muestra SIEMPRE el cambio desde el último check-in
- PeriodReturnsCard siempre visible

Rendimiento y bugs (de la auditoría):
- El cache de snapshots ya no se invalida al cambiar solo de chart type/rango/breakdown
- calculateAnnualizedGrowth y el forecast a 12 meses ahora componen (la aproximación
  lineal sobreestimaba con historiales cortos)
- Drawdown sin force unwrap; simulador: rama muerta reemplazada por preservación
  real de las asignaciones simuladas del usuario
- UITest de captura de Charts con manejo del alert de notificaciones

12 strings nuevas localizadas en los 7 idiomas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qUZrBusG82T37R7PeokqJ
This commit is contained in:
alexandrev-tibco
2026-07-03 10:17:10 +02:00
parent d0a51d0162
commit db844d5e80
19 changed files with 557 additions and 230 deletions
@@ -494,8 +494,19 @@ class DashboardViewModel: ObservableObject {
let totalGrowth = lastPoint.value - firstPoint.value
let monthlyGrowth = totalGrowth / Decimal(monthsBetween)
// Project 12 months ahead
let forecastValue = currentValue + (monthlyGrowth * 12)
// Project 12 months ahead using compound growth (matches CAGR semantics);
// fall back to the linear projection if the ratio is degenerate.
let firstD = NSDecimalNumber(decimal: firstPoint.value).doubleValue
let lastD = NSDecimalNumber(decimal: lastPoint.value).doubleValue
let currentD = NSDecimalNumber(decimal: currentValue).doubleValue
let forecastValue: Decimal
if firstD > 0, lastD > 0 {
let monthlyRate = pow(lastD / firstD, 1.0 / Double(monthsBetween))
let projected = currentD * pow(monthlyRate, 12)
forecastValue = projected.isFinite ? Decimal(projected) : currentValue + (monthlyGrowth * 12)
} else {
forecastValue = currentValue + (monthlyGrowth * 12)
}
// Calculate confidence interval based on volatility
let volatility = calculatePortfolioVolatility(from: values)
@@ -537,10 +548,13 @@ class DashboardViewModel: ObservableObject {
let monthsBetween = max(1, first.date.monthsBetween(last.date))
let totalReturn = (last.value - first.value) / first.value
// Annualize: (1 + total_return)^(12/months) - 1
let monthlyReturn = totalReturn / Decimal(monthsBetween)
let annualized = monthlyReturn * 12
return annualized
// Annualize compounding: (1 + total_return)^(12/months) - 1.
// The previous linear approximation (monthly*12) overstated short histories.
let totalReturnD = NSDecimalNumber(decimal: totalReturn).doubleValue
guard totalReturnD > -1 else { return 0 }
let annualizedD = pow(1 + totalReturnD, 12.0 / Double(monthsBetween)) - 1
guard annualizedD.isFinite else { return 0 }
return Decimal(annualizedD)
}
private func computeStreak(from evolutionData: [(date: Date, value: Decimal)]) -> Int {