import SwiftUI import Charts struct PeriodComparisonChartView: View { @ObservedObject var viewModel: ChartsViewModel private let colorA = Color(hex: "#3478F6") ?? .blue private let colorB = Color(hex: "#FF9500") ?? .orange var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Period vs Period") .font(.headline) periodPickersSection chartContent } .padding() .background(Color(.systemBackground)) .cornerRadius(AppConstants.UI.cornerRadius) .shadow(color: .black.opacity(0.05), radius: 8, y: 2) } // MARK: - Period Pickers private var periodPickersSection: some View { VStack(spacing: 8) { periodRow(label: "Period A", color: colorA, start: $viewModel.periodAStart, end: $viewModel.periodAEnd) Divider() .background(Color.secondary.opacity(0.15)) periodRow(label: "Period B", color: colorB, start: $viewModel.periodBStart, end: $viewModel.periodBEnd) } .padding(12) .background( LinearGradient( colors: [colorA.opacity(0.06), colorB.opacity(0.06)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .cornerRadius(12) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.secondary.opacity(0.1), lineWidth: 1) ) } private func periodRow(label: String, color: Color, start: Binding, end: Binding) -> some View { HStack(spacing: 10) { RoundedRectangle(cornerRadius: 2) .fill(color) .frame(width: 3, height: 30) Text(label) .font(.caption.weight(.bold)) .foregroundColor(color) .frame(width: 60, alignment: .leading) DatePicker( "", selection: Binding( get: { firstOfMonth(start.wrappedValue) }, set: { start.wrappedValue = firstOfMonth($0) } ), displayedComponents: [.date] ) .datePickerStyle(.compact) .labelsHidden() .frame(maxWidth: .infinity) Text("→") .font(.caption) .foregroundColor(.secondary) DatePicker( "", selection: Binding( get: { firstOfMonth(end.wrappedValue) }, set: { end.wrappedValue = firstOfMonth($0) } ), displayedComponents: [.date] ) .datePickerStyle(.compact) .labelsHidden() .frame(maxWidth: .infinity) } } // MARK: - Chart Content @ViewBuilder private var chartContent: some View { if viewModel.periodComparisonData.isEmpty { emptyView } else { VStack(spacing: 16) { numericalSummary periodChart legend } } } private var numericalSummary: some View { HStack(spacing: 12) { ForEach(viewModel.periodComparisonData) { series in let color = Color(hex: series.colorHex) ?? .gray let finalReturn = series.points.last?.returnPct ?? 0 let maxReturn = series.points.map { $0.returnPct }.max() ?? 0 let minReturn = series.points.map { $0.returnPct }.min() ?? 0 HStack(spacing: 0) { Rectangle() .fill(color) .frame(width: 4) VStack(alignment: .leading, spacing: 6) { HStack(spacing: 4) { Circle() .fill(color) .frame(width: 6, height: 6) Text(series.label) .font(.caption2.weight(.medium)) .foregroundColor(.secondary) .lineLimit(1) } Text(String(format: "%+.2f%%", finalReturn)) .font(.title2.weight(.bold)) .foregroundColor(finalReturn >= 0 ? .positiveGreen : .negativeRed) HStack(spacing: 10) { HStack(spacing: 3) { Image(systemName: "arrow.up") .font(.caption2.weight(.semibold)) .foregroundColor(.positiveGreen) Text(String(format: "%.1f%%", maxReturn)) .font(.caption2) .foregroundColor(.positiveGreen) } HStack(spacing: 3) { Image(systemName: "arrow.down") .font(.caption2.weight(.semibold)) .foregroundColor(.negativeRed) Text(String(format: "%.1f%%", minReturn)) .font(.caption2) .foregroundColor(.negativeRed) } } } .padding(.horizontal, 12) .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) } .background(color.opacity(0.07)) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(color.opacity(0.15), lineWidth: 1) ) } } } private var emptyView: some View { VStack(spacing: 12) { Image(systemName: "calendar.badge.clock") .font(.system(size: 36)) .foregroundColor(.secondary) Text("No data available for the selected periods.") .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .frame(height: 260) } // MARK: - Flat data for Chart private struct PeriodPoint: Identifiable { let id: String let seriesId: String let seriesLabel: String let colorHex: String let monthOffset: Int let returnPct: Double } private var flatPoints: [PeriodPoint] { var result: [PeriodPoint] = [] for series in viewModel.periodComparisonData { for point in series.points { result.append(PeriodPoint( id: "\(series.id)-\(point.monthOffset)", seriesId: series.id, seriesLabel: series.label, colorHex: series.colorHex, monthOffset: point.monthOffset, returnPct: point.returnPct )) } } return result } private var periodChart: some View { let points = flatPoints let seriesIds = viewModel.periodComparisonData.map { $0.id } let seriesColors: [Color] = viewModel.periodComparisonData.map { Color(hex: $0.colorHex) ?? .gray } return Chart { ForEach(points) { point in LineMark( x: .value("Month", point.monthOffset), y: .value("Return", point.returnPct), series: .value("Period", point.seriesLabel) ) .interpolationMethod(.catmullRom) .foregroundStyle(by: .value("Period", point.seriesLabel)) .lineStyle(StrokeStyle(lineWidth: 2.5)) PointMark( x: .value("Month", point.monthOffset), y: .value("Return", point.returnPct) ) .foregroundStyle(by: .value("Period", point.seriesLabel)) .symbolSize(30) } RuleMark(y: .value("Zero", 0)) .foregroundStyle(Color.secondary.opacity(0.4)) .lineStyle(StrokeStyle(lineWidth: 1, dash: [4, 4])) } .chartForegroundStyleScale( domain: viewModel.periodComparisonData.map { $0.label }, range: seriesColors ) .chartXAxis { AxisMarks(values: xAxisValues) { value in AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [3, 3])) .foregroundStyle(Color.secondary.opacity(0.2)) AxisValueLabel { if let idx = value.as(Int.self) { Text("M\(idx + 1)") .font(.caption2) } } } } .chartYAxis { AxisMarks(position: .leading) { value in AxisValueLabel { if let d = value.as(Double.self) { Text(String(format: "%.1f%%", d)) .font(.caption2) } } AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [3, 3])) .foregroundStyle(Color.secondary.opacity(0.15)) } } .frame(height: 260) .drawingGroup() .onChange(of: seriesIds) { _, _ in } } private var legend: some View { HStack(spacing: 16) { ForEach(viewModel.periodComparisonData) { series in HStack(spacing: 6) { RoundedRectangle(cornerRadius: 2) .fill(Color(hex: series.colorHex) ?? .gray) .frame(width: 20, height: 3) Text(series.label) .font(.caption) .foregroundColor(.secondary) } } Spacer() } } // MARK: - Helpers private var xAxisValues: [Int] { let maxOffset = viewModel.periodComparisonData .flatMap { $0.points } .map { $0.monthOffset } .max() ?? 0 return Array(0...maxOffset) } private func firstOfMonth(_ date: Date) -> Date { let calendar = Calendar.current var components = calendar.dateComponents([.year, .month], from: date) components.day = 1 return calendar.date(from: components) ?? date } }