Files
InvestmentTrackerApp/PortfolioJournal/Utilities/ChartGapFill.swift
T
alexandrev-tibco cbcb1da0ac Feature: rellenar huecos entre snapshots en las gráficas (interpolación lineal)
Nueva utilidad ChartGapFill: para cada source, rellena los meses sin snapshot
entre dos conocidos con interpolación lineal (Feb=100k, Jun=130k → Mar/Abr/May
~110/120/130k), arrastra el último valor tras el último snapshot y no aporta
antes del primero. Solo afecta al pintado de gráficas — NO crea ni modifica
snapshots (cero riesgo para la sync iCloud). Aplicado a las evolution del
Dashboard (total + por categoría) y de Charts. Toggle en Settings "Smooth Gaps
in Charts" (ON por defecto), localizado en los 7 idiomas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L2p3gUZRNWW388rWFRjiU7
2026-07-23 19:51:26 +02:00

66 lines
2.7 KiB
Swift

import Foundation
/// Fills gaps between snapshots for chart display only it never creates or
/// mutates Core Data objects. A source that has a snapshot in February and the
/// next in June leaves March/April/May empty; without filling, per-source and
/// category lines dip or break. This computes a value for every month in the
/// grid so the graphs read continuously.
///
/// - Internal gaps (between two known snapshots) are linearly interpolated when
/// `interpolate` is true, or carried forward (stepped) when false.
/// - After a source's last snapshot the value is carried forward (there is no
/// future point to interpolate towards) matches the portfolio total.
/// - Before a source's first snapshot the value is `nil` (the source did not
/// exist yet, so it must not contribute 0 and drag the total down).
enum ChartGapFill {
/// The user-facing toggle (Settings). Defaults to ON so charts look right out
/// of the box; reading via `object(forKey:)` so an unset key means enabled.
static let defaultsKey = "smoothChartGaps"
static var isEnabled: Bool {
(UserDefaults.standard.object(forKey: defaultsKey) as? Bool) ?? true
}
/// For a single source: `known` maps a month index (position in the ordered
/// month grid) to that month's value. Returns a value per month index, with
/// internal gaps filled and a trailing carry-forward. `nil` means "absent".
static func denseValues(
known: [Int: Decimal],
monthCount: Int,
interpolate: Bool
) -> [Decimal?] {
var result = [Decimal?](repeating: nil, count: monthCount)
let indices = known.keys.sorted()
guard let first = indices.first, let last = indices.last else { return result }
for i in indices { result[i] = known[i] }
if interpolate {
// Linear interpolation across each internal gap.
for k in 0..<(indices.count - 1) {
let a = indices[k]
let b = indices[k + 1]
guard b - a > 1, let va = known[a], let vb = known[b] else { continue }
for i in (a + 1)..<b {
let frac = Decimal(i - a) / Decimal(b - a)
result[i] = va + (vb - va) * frac
}
}
} else {
// Stepped carry-forward within internal gaps.
var lastVal = known[first] ?? .zero
for i in first...last {
if let v = result[i] { lastVal = v } else { result[i] = lastVal }
}
}
// Carry the last known value forward past the final snapshot.
if let lastVal = known[last], last + 1 < monthCount {
for i in (last + 1)..<monthCount { result[i] = lastVal }
}
return result
}
}