import SwiftUI import Charts struct EvolutionChartCard: View { let data: [(date: Date, value: Decimal)] @State private var selectedDataPoint: (date: Date, value: Decimal)? var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("Portfolio Evolution") .font(.headline) Spacer() if let selected = selectedDataPoint { VStack(alignment: .trailing) { Text(selected.value.compactCurrencyString) .font(.subheadline.weight(.semibold)) Text(selected.date.monthYearString) .font(.caption) .foregroundColor(.secondary) } } } if data.count >= 2 { Chart { ForEach(data, id: \.date) { item in LineMark( x: .value("Date", item.date), y: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue) ) .foregroundStyle(Color.appPrimary) .interpolationMethod(.catmullRom) AreaMark( x: .value("Date", item.date), y: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue) ) .foregroundStyle( LinearGradient( colors: [Color.appPrimary.opacity(0.3), Color.appPrimary.opacity(0.0)], startPoint: .top, endPoint: .bottom ) ) .interpolationMethod(.catmullRom) } if let selected = selectedDataPoint { RuleMark(x: .value("Selected", selected.date)) .foregroundStyle(Color.gray.opacity(0.3)) .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 5])) PointMark( x: .value("Date", selected.date), y: .value("Value", NSDecimalNumber(decimal: selected.value).doubleValue) ) .foregroundStyle(Color.appPrimary) .symbolSize(100) } } .chartXAxis { AxisMarks(values: .stride(by: .month, count: 3)) { value in AxisValueLabel(format: .dateTime.month(.abbreviated)) } } .chartYAxis { AxisMarks(position: .leading) { value in AxisValueLabel { if let doubleValue = value.as(Double.self) { Text(Decimal(doubleValue).shortCurrencyString) .font(.caption) } } } } .chartOverlay { proxy in GeometryReader { geometry in Rectangle() .fill(.clear) .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in let x = value.location.x - geometry[proxy.plotAreaFrame].origin.x guard let date: Date = proxy.value(atX: x) else { return } if let closest = data.min(by: { abs($0.date.timeIntervalSince(date)) < abs($1.date.timeIntervalSince(date)) }) { selectedDataPoint = closest } } .onEnded { _ in selectedDataPoint = nil } ) } } .frame(height: 200) } else { Text("Not enough data to display chart") .font(.subheadline) .foregroundColor(.secondary) .frame(height: 200) .frame(maxWidth: .infinity) } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } } // MARK: - Mini Sparkline struct SparklineView: View { let data: [(date: Date, value: Decimal)] let color: Color var body: some View { if data.count >= 2 { Chart(data, id: \.date) { item in LineMark( x: .value("Date", item.date), y: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue) ) .foregroundStyle(color) .interpolationMethod(.catmullRom) } .chartXAxis(.hidden) .chartYAxis(.hidden) .chartLegend(.hidden) } else { Rectangle() .fill(Color.gray.opacity(0.1)) } } } #Preview { let sampleData: [(date: Date, value: Decimal)] = [ (Date().adding(months: -6), 10000), (Date().adding(months: -5), 10500), (Date().adding(months: -4), 10200), (Date().adding(months: -3), 11000), (Date().adding(months: -2), 11500), (Date().adding(months: -1), 11200), (Date(), 12000) ] return EvolutionChartCard(data: sampleData) .padding() }