InvestmentTrackerApp/PortfolioJournal/Views/Charts/AllocationPieChart.swift

300 lines
12 KiB
Swift

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