299 lines
10 KiB
Swift
299 lines
10 KiB
Swift
import SwiftUI
|
|
import Charts
|
|
|
|
struct ChartsContainerView: View {
|
|
@EnvironmentObject var iapService: IAPService
|
|
@StateObject private var viewModel: ChartsViewModel
|
|
|
|
init() {
|
|
_viewModel = StateObject(wrappedValue: ChartsViewModel(iapService: IAPService()))
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
// Chart Type Selector
|
|
chartTypeSelector
|
|
|
|
// Time Range Selector
|
|
if viewModel.selectedChartType != .allocation &&
|
|
viewModel.selectedChartType != .performance {
|
|
timeRangeSelector
|
|
}
|
|
|
|
// Category Filter
|
|
if viewModel.selectedChartType == .evolution ||
|
|
viewModel.selectedChartType == .prediction {
|
|
categoryFilter
|
|
}
|
|
|
|
// Chart Content
|
|
chartContent
|
|
}
|
|
.padding()
|
|
}
|
|
.navigationTitle("Charts")
|
|
.sheet(isPresented: $viewModel.showingPaywall) {
|
|
PaywallView()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Chart Type Selector
|
|
|
|
private var chartTypeSelector: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 12) {
|
|
ForEach(ChartsViewModel.ChartType.allCases) { chartType in
|
|
ChartTypeButton(
|
|
chartType: chartType,
|
|
isSelected: viewModel.selectedChartType == chartType,
|
|
isPremium: chartType.isPremium,
|
|
userIsPremium: viewModel.isPremium
|
|
) {
|
|
viewModel.selectChart(chartType)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 4)
|
|
}
|
|
}
|
|
|
|
// MARK: - Time Range Selector
|
|
|
|
private var timeRangeSelector: some View {
|
|
HStack(spacing: 8) {
|
|
ForEach(ChartsViewModel.TimeRange.allCases) { range in
|
|
Button {
|
|
viewModel.selectedTimeRange = range
|
|
} label: {
|
|
Text(range.rawValue)
|
|
.font(.subheadline.weight(.medium))
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 8)
|
|
.background(
|
|
viewModel.selectedTimeRange == range
|
|
? Color.appPrimary
|
|
: Color.gray.opacity(0.1)
|
|
)
|
|
.foregroundColor(
|
|
viewModel.selectedTimeRange == range
|
|
? .white
|
|
: .primary
|
|
)
|
|
.cornerRadius(20)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Category Filter
|
|
|
|
private var categoryFilter: some View {
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 8) {
|
|
Button {
|
|
viewModel.selectedCategory = nil
|
|
} label: {
|
|
Text("All")
|
|
.font(.caption.weight(.medium))
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(
|
|
viewModel.selectedCategory == nil
|
|
? Color.appPrimary
|
|
: Color.gray.opacity(0.1)
|
|
)
|
|
.foregroundColor(
|
|
viewModel.selectedCategory == nil
|
|
? .white
|
|
: .primary
|
|
)
|
|
.cornerRadius(16)
|
|
}
|
|
|
|
ForEach(viewModel.categories) { category in
|
|
Button {
|
|
viewModel.selectedCategory = category
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Circle()
|
|
.fill(category.color)
|
|
.frame(width: 8, height: 8)
|
|
Text(category.name)
|
|
}
|
|
.font(.caption.weight(.medium))
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 6)
|
|
.background(
|
|
viewModel.selectedCategory?.id == category.id
|
|
? category.color
|
|
: Color.gray.opacity(0.1)
|
|
)
|
|
.foregroundColor(
|
|
viewModel.selectedCategory?.id == category.id
|
|
? .white
|
|
: .primary
|
|
)
|
|
.cornerRadius(16)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Chart Content
|
|
|
|
@ViewBuilder
|
|
private var chartContent: some View {
|
|
if viewModel.isLoading {
|
|
ProgressView()
|
|
.frame(height: 300)
|
|
} else if !viewModel.hasData {
|
|
emptyStateView
|
|
} else {
|
|
switch viewModel.selectedChartType {
|
|
case .evolution:
|
|
EvolutionChartView(data: viewModel.evolutionData)
|
|
case .allocation:
|
|
AllocationPieChart(data: viewModel.allocationData)
|
|
case .performance:
|
|
PerformanceBarChart(data: viewModel.performanceData)
|
|
case .drawdown:
|
|
DrawdownChart(data: viewModel.drawdownData)
|
|
case .volatility:
|
|
VolatilityChartView(data: viewModel.volatilityData)
|
|
case .prediction:
|
|
PredictionChartView(
|
|
predictions: viewModel.predictionData,
|
|
historicalData: viewModel.evolutionData
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var emptyStateView: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "chart.bar.xaxis")
|
|
.font(.system(size: 48))
|
|
.foregroundColor(.secondary)
|
|
|
|
Text("No Data Available")
|
|
.font(.headline)
|
|
|
|
Text("Add some investment sources and snapshots to see charts.")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
.frame(height: 300)
|
|
}
|
|
}
|
|
|
|
// MARK: - Chart Type Button
|
|
|
|
struct ChartTypeButton: View {
|
|
let chartType: ChartsViewModel.ChartType
|
|
let isSelected: Bool
|
|
let isPremium: Bool
|
|
let userIsPremium: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
VStack(spacing: 8) {
|
|
ZStack(alignment: .topTrailing) {
|
|
Image(systemName: chartType.icon)
|
|
.font(.title2)
|
|
|
|
if isPremium && !userIsPremium {
|
|
Image(systemName: "lock.fill")
|
|
.font(.caption2)
|
|
.foregroundColor(.appWarning)
|
|
.offset(x: 8, y: -4)
|
|
}
|
|
}
|
|
|
|
Text(chartType.rawValue)
|
|
.font(.caption)
|
|
}
|
|
.frame(width: 80, height: 70)
|
|
.background(
|
|
isSelected
|
|
? Color.appPrimary
|
|
: Color(.systemBackground)
|
|
)
|
|
.foregroundColor(isSelected ? .white : .primary)
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
.shadow(color: .black.opacity(0.05), radius: 4, y: 2)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Evolution Chart View
|
|
|
|
struct EvolutionChartView: View {
|
|
let data: [(date: Date, value: Decimal)]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Portfolio Evolution")
|
|
.font(.headline)
|
|
|
|
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.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)
|
|
}
|
|
.chartXAxis {
|
|
AxisMarks(values: .stride(by: .month, count: 2)) { 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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(height: 300)
|
|
} else {
|
|
Text("Not enough data")
|
|
.foregroundColor(.secondary)
|
|
.frame(height: 300)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.cornerRadius(AppConstants.UI.cornerRadius)
|
|
.shadow(color: .black.opacity(0.05), radius: 8, y: 2)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ChartsContainerView()
|
|
.environmentObject(IAPService())
|
|
}
|