InvestmentTrackerApp/InvestmentTracker/Views/Dashboard/EvolutionChart.swift

159 lines
5.9 KiB
Swift

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()
}