import SwiftUI import Charts struct PeriodComparisonChartView: View { @ObservedObject var viewModel: ChartsViewModel 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: 10) { periodRow( label: "Period A", color: Color(hex: "#3478F6") ?? .blue, start: $viewModel.periodAStart, end: $viewModel.periodAEnd ) periodRow( label: "Period B", color: Color(hex: "#FF9500") ?? .orange, start: $viewModel.periodBStart, end: $viewModel.periodBEnd ) } .padding(12) .background(Color(.systemGray6)) .cornerRadius(10) } private func periodRow( label: String, color: Color, start: Binding, end: Binding ) -> some View { HStack(spacing: 8) { Circle() .fill(color) .frame(width: 8, height: 8) Text(label) .font(.caption.weight(.semibold)) .foregroundColor(color) .frame(width: 58, 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: 12) { periodChart legend } } } 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(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)) PointMark( x: .value("Month", point.monthOffset), y: .value("Return", point.returnPct) ) .foregroundStyle(by: .value("Period", point.seriesLabel)) .symbolSize(20) } .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 } // silence unused warning } private var legend: some View { ScrollView(.horizontal, showsIndicators: false) { 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) } } } } } // 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 } }