import SwiftUI import Charts struct AllocationPieChart: View { let data: [(category: String, value: Decimal, color: String)] @State private var selectedSlice: String? var total: Decimal { data.reduce(Decimal.zero) { $0 + $1.value } } var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Asset Allocation") .font(.headline) if !data.isEmpty { HStack(alignment: .top, spacing: 20) { // Pie Chart Chart(data, id: \.category) { item in SectorMark( angle: .value("Value", NSDecimalNumber(decimal: item.value).doubleValue), innerRadius: .ratio(0.6), angularInset: 1.5 ) .foregroundStyle(Color(hex: item.color) ?? .gray) .cornerRadius(4) .opacity(selectedSlice == nil || selectedSlice == item.category ? 1 : 0.5) } .chartLegend(.hidden) .frame(width: 180, height: 180) .overlay { // Center content VStack(spacing: 2) { if let selected = selectedSlice, let item = data.first(where: { $0.category == selected }) { Text(selected) .font(.caption) .foregroundColor(.secondary) Text(item.value.compactCurrencyString) .font(.headline) let percentage = total > 0 ? NSDecimalNumber(decimal: item.value / total).doubleValue * 100 : 0 Text(String(format: "%.1f%%", percentage)) .font(.caption) .foregroundColor(.secondary) } else { Text("Total") .font(.caption) .foregroundColor(.secondary) Text(total.compactCurrencyString) .font(.headline) } } } .gesture( DragGesture(minimumDistance: 0) .onChanged { value in // Simple tap detection } ) // Legend VStack(alignment: .leading, spacing: 8) { ForEach(data, id: \.category) { item in Button { if selectedSlice == item.category { selectedSlice = nil } else { selectedSlice = item.category } } label: { HStack(spacing: 8) { Circle() .fill(Color(hex: item.color) ?? .gray) .frame(width: 10, height: 10) VStack(alignment: .leading, spacing: 0) { Text(item.category) .font(.caption) .foregroundColor(.primary) let percentage = total > 0 ? NSDecimalNumber(decimal: item.value / total).doubleValue * 100 : 0 Text(String(format: "%.1f%%", percentage)) .font(.caption2) .foregroundColor(.secondary) } } .opacity(selectedSlice == nil || selectedSlice == item.category ? 1 : 0.5) } .buttonStyle(.plain) } } .frame(maxWidth: .infinity, alignment: .leading) } AllocationTargetsComparisonChart(data: data) } else { Text("No allocation data available") .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: - Allocation Targets Comparison struct AllocationTargetsComparisonChart: View { let data: [(category: String, value: Decimal, color: String)] @StateObject private var categoryRepository = CategoryRepository() private var total: Decimal { data.reduce(Decimal.zero) { $0 + $1.value } } private var targetData: [(category: String, actual: Double, target: Double, color: Color)] { data.map { item in let actual = total > 0 ? NSDecimalNumber(decimal: item.value / total).doubleValue * 100 : 0 let target = AllocationTargetStore.target(for: categoryId(for: item.category)) ?? 0 let color = Color(hex: item.color) ?? .gray return (item.category, actual, target, color) } } var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Targets vs Actual") .font(.headline) if targetData.allSatisfy({ $0.target == 0 }) { Text("Set allocation targets to compare your portfolio against your plan.") .font(.subheadline) .foregroundColor(.secondary) } else { Chart { ForEach(targetData, id: \.category) { item in BarMark( x: .value("Category", item.category), y: .value("Actual", item.actual) ) .foregroundStyle(item.color) BarMark( x: .value("Category", item.category), y: .value("Target", item.target) ) .foregroundStyle(Color.gray.opacity(0.35)) } } .chartYAxis { AxisMarks(position: .leading) { value in AxisValueLabel { if let doubleValue = value.as(Double.self) { Text(String(format: "%.0f%%", doubleValue)) .font(.caption) } } } } .chartXAxis { AxisMarks { value in AxisValueLabel() } } .frame(height: 220) ForEach(targetData, id: \.category) { item in let drift = item.actual - item.target let prefix = drift >= 0 ? "+" : "" HStack { Circle() .fill(item.color) .frame(width: 8, height: 8) Text(item.category) .font(.caption) Spacer() Text("Actual \(String(format: "%.1f%%", item.actual))") .font(.caption2) .foregroundColor(.secondary) Text("Target \(String(format: "%.0f%%", item.target))") .font(.caption2) .foregroundColor(.secondary) Text("\(prefix)\(String(format: "%.1f%%", drift))") .font(.caption2.weight(.semibold)) .foregroundColor(drift >= 0 ? .positiveGreen : .negativeRed) } } } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } private func categoryId(for name: String) -> UUID { if let category = categoryRepository.categories.first(where: { $0.name == name }) { return category.id } return UUID() } } // MARK: - Allocation List View (Alternative) struct AllocationListView: View { let data: [(category: String, value: Decimal, color: String)] var total: Decimal { data.reduce(Decimal.zero) { $0 + $1.value } } var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Asset Allocation") .font(.headline) ForEach(data, id: \.category) { item in VStack(spacing: 4) { HStack { HStack(spacing: 8) { Circle() .fill(Color(hex: item.color) ?? .gray) .frame(width: 10, height: 10) Text(item.category) .font(.subheadline) } Spacer() let percentage = total > 0 ? NSDecimalNumber(decimal: item.value / total).doubleValue * 100 : 0 Text(String(format: "%.1f%%", percentage)) .font(.subheadline.weight(.medium)) Text(item.value.compactCurrencyString) .font(.caption) .foregroundColor(.secondary) .frame(width: 70, alignment: .trailing) } // Progress bar GeometryReader { geometry in let percentage = total > 0 ? NSDecimalNumber(decimal: item.value / total).doubleValue : 0 ZStack(alignment: .leading) { Rectangle() .fill(Color.gray.opacity(0.1)) .frame(height: 6) .cornerRadius(3) Rectangle() .fill(Color(hex: item.color) ?? .gray) .frame(width: geometry.size.width * percentage, height: 6) .cornerRadius(3) } } .frame(height: 6) } } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } } #Preview { let sampleData: [(category: String, value: Decimal, color: String)] = [ ("Stocks", 50000, "#10B981"), ("Bonds", 25000, "#3B82F6"), ("Real Estate", 15000, "#F59E0B"), ("Crypto", 10000, "#8B5CF6") ] return VStack(spacing: 20) { AllocationPieChart(data: sampleData) AllocationListView(data: sampleData) } .padding() }