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