import SwiftUI import Charts struct AllocationSimulatorView: View { @ObservedObject var viewModel: ChartsViewModel var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Allocation Simulator") .font(.headline) if viewModel.simulatorSources.isEmpty { emptyStateView } else { allocationCard chartSection } } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Empty State private var emptyStateView: some View { VStack(spacing: 12) { Image(systemName: "slider.horizontal.3") .font(.system(size: 36)) .foregroundColor(.secondary) Text("No sources available") .font(.subheadline) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .frame(height: 300) } // MARK: - Allocation Card private var allocationCard: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("Adjust Allocation") .font(.subheadline.weight(.semibold)) Spacer() totalAllocationBadge resetButton } ForEach(Array(viewModel.simulatorSources.enumerated()), id: \.element.id) { index, source in sourceSliderRow(index: index, source: source) } } .padding(12) .background(Color(.systemGray6)) .cornerRadius(10) } private var totalAllocationBadge: some View { let total = viewModel.simulatorSources.reduce(0) { $0 + $1.simulatedPct } let isNear100 = abs(total - 100) < 5 return Text(String(format: "%.0f%%", total)) .font(.caption.weight(.semibold)) .padding(.horizontal, 8) .padding(.vertical, 3) .background(isNear100 ? Color.green.opacity(0.15) : Color.orange.opacity(0.15)) .foregroundColor(isNear100 ? .green : .orange) .cornerRadius(8) } private var resetButton: some View { Button { var updated = viewModel.simulatorSources for i in updated.indices { updated[i] = ChartsViewModel.SimulatorSource( id: updated[i].id, name: updated[i].name, currentPct: updated[i].currentPct, simulatedPct: updated[i].currentPct, colorHex: updated[i].colorHex ) } viewModel.simulatorSources = updated } label: { Image(systemName: "arrow.counterclockwise") .font(.caption) .foregroundColor(.secondary) } .buttonStyle(.plain) } private func sourceSliderRow(index: Int, source: ChartsViewModel.SimulatorSource) -> some View { VStack(spacing: 4) { HStack { Circle() .fill(Color(hex: source.colorHex) ?? .gray) .frame(width: 8, height: 8) Text(source.name) .font(.caption.weight(.medium)) .lineLimit(1) Spacer() Text(String(format: "%.0f%%", source.simulatedPct)) .font(.caption.weight(.semibold)) .foregroundColor(.primary) .frame(width: 36, alignment: .trailing) Text(String(format: "(%.0f%%)", source.currentPct)) .font(.caption2) .foregroundColor(.secondary) } Slider( value: Binding( get: { source.simulatedPct }, set: { newValue in var updated = viewModel.simulatorSources if index < updated.count { updated[index] = ChartsViewModel.SimulatorSource( id: source.id, name: source.name, currentPct: source.currentPct, simulatedPct: newValue, colorHex: source.colorHex ) viewModel.simulatorSources = updated } } ), in: 0...100, step: 1 ) .tint(Color(hex: source.colorHex) ?? .appPrimary) } } // MARK: - Chart Section private var chartSection: some View { VStack(alignment: .leading, spacing: 8) { if viewModel.simulatorActualData.isEmpty && viewModel.simulatorData.isEmpty { Text("Not enough data to simulate.") .font(.subheadline) .foregroundColor(.secondary) .frame(height: 220) } else { simulationChart chartLegend } } } private var simulationChart: some View { Chart { ForEach(viewModel.simulatorActualData, id: \.date) { item in LineMark( x: .value("Date", item.date), y: .value("Value", item.value), series: .value("Series", "Actual") ) .foregroundStyle(Color.blue) .interpolationMethod(.catmullRom) } ForEach(viewModel.simulatorData, id: \.date) { item in LineMark( x: .value("Date", item.date), y: .value("Value", item.value), series: .value("Series", "Simulated") ) .foregroundStyle(Color.orange) .lineStyle(StrokeStyle(lineWidth: 2, dash: [6, 3])) .interpolationMethod(.catmullRom) } } .chartXAxis { AxisMarks(values: .stride(by: .month, count: xAxisStride)) { value in AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [3, 3])) .foregroundStyle(Color.secondary.opacity(0.2)) AxisValueLabel(format: .dateTime.month(.abbreviated).year(.twoDigits)) .font(.caption2) } } .chartYAxis { AxisMarks(position: .leading) { value in AxisValueLabel { if let d = value.as(Double.self) { Text(String(format: "%.0f", d)) .font(.caption2) } } AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [3, 3])) .foregroundStyle(Color.secondary.opacity(0.15)) } } .frame(height: 220) .drawingGroup() } private var chartLegend: some View { HStack(spacing: 20) { legendItem(color: .blue, label: "Actual", dashed: false) legendItem(color: .orange, label: "Simulated", dashed: true) Spacer() } } private func legendItem(color: Color, label: String, dashed: Bool) -> some View { HStack(spacing: 6) { if dashed { HStack(spacing: 2) { RoundedRectangle(cornerRadius: 2) .fill(color) .frame(width: 8, height: 3) RoundedRectangle(cornerRadius: 2) .fill(color) .frame(width: 8, height: 3) } } else { RoundedRectangle(cornerRadius: 2) .fill(color) .frame(width: 20, height: 3) } Text(label) .font(.caption) .foregroundColor(.secondary) } } // MARK: - Helpers private var xAxisStride: Int { let count = max(viewModel.simulatorActualData.count, viewModel.simulatorData.count) switch count { case ...6: return 1 case ...12: return 2 case ...24: return 3 default: return 6 } } }