import SwiftUI // MARK: - Weekly Chart Card struct WeeklyChartCard: View { let heatmapData: [HeatmapEntry] enum ChartType: String, CaseIterable { case sleep = "Сон" case hrv = "HRV" case steps = "Шаги" } @State private var selectedChart: ChartType = .sleep @State private var appeared = false var body: some View { VStack(alignment: .leading, spacing: 16) { // Header HStack { GradientIcon(icon: "chart.bar.fill", colors: [Color(hex: "7c3aed"), Color(hex: "00d4aa")]) Text("За неделю") .font(.headline.weight(.semibold)) .foregroundColor(.white) Spacer() } // Segmented picker HStack(spacing: 4) { ForEach(ChartType.allCases, id: \.self) { type in Button { withAnimation(.easeInOut(duration: 0.2)) { selectedChart = type UIImpactFeedbackGenerator(style: .light).impactOccurred() } } label: { Text(type.rawValue) .font(.caption.weight(.medium)) .foregroundColor(selectedChart == type ? .white : Color(hex: "8888aa")) .padding(.horizontal, 16) .padding(.vertical, 8) .background( selectedChart == type ? Color(hex: "7c3aed").opacity(0.5) : Color.clear ) .cornerRadius(10) } } } .padding(4) .background(Color(hex: "1a1a3e")) .cornerRadius(12) // Chart BarChartView( values: chartValues, color: chartColor, maxValue: chartMaxValue, unit: chartUnit, appeared: appeared ) .frame(height: 160) } .padding(20) .background( RoundedRectangle(cornerRadius: 20) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 20) .fill(Color(hex: "12122a").opacity(0.7)) ) ) .shadow(color: .black.opacity(0.2), radius: 10, y: 5) .padding(.horizontal) .onAppear { withAnimation(.easeOut(duration: 0.8).delay(0.3)) { appeared = true } } } private var chartValues: [(date: String, value: Double)] { heatmapData.map { entry in let val: Double switch selectedChart { case .sleep: val = entry.sleep ?? 0 case .hrv: val = entry.hrv ?? 0 case .steps: val = Double(entry.steps ?? 0) } return (date: entry.displayDate, value: val) } } private var chartColor: Color { switch selectedChart { case .sleep: return Color(hex: "7c3aed") case .hrv: return Color(hex: "00d4aa") case .steps: return Color(hex: "ffa502") } } private var chartMaxValue: Double { switch selectedChart { case .sleep: return 10 case .hrv: return 120 case .steps: return 12000 } } private var chartUnit: String { switch selectedChart { case .sleep: return "ч" case .hrv: return "мс" case .steps: return "" } } } // MARK: - Bar Chart struct BarChartView: View { let values: [(date: String, value: Double)] let color: Color let maxValue: Double let unit: String let appeared: Bool var body: some View { GeometryReader { geo in let barWidth = max((geo.size.width - CGFloat(values.count - 1) * 8) / CGFloat(max(values.count, 1)), 10) let chartHeight = geo.size.height - 30 HStack(alignment: .bottom, spacing: 8) { ForEach(Array(values.enumerated()), id: \.offset) { index, item in VStack(spacing: 4) { // Value label if item.value > 0 { Text(formatValue(item.value)) .font(.system(size: 9, weight: .medium)) .foregroundColor(Color(hex: "8888aa")) } // Bar RoundedRectangle(cornerRadius: 6) .fill( LinearGradient( colors: [color, color.opacity(0.5)], startPoint: .top, endPoint: .bottom ) ) .frame( width: barWidth, height: appeared ? max(CGFloat(item.value / maxValue) * chartHeight, 4) : 4 ) .animation( .spring(response: 0.6, dampingFraction: 0.7).delay(Double(index) * 0.05), value: appeared ) // Date label Text(item.date) .font(.system(size: 10)) .foregroundColor(Color(hex: "8888aa")) } .frame(maxWidth: .infinity) } } } } private func formatValue(_ value: Double) -> String { if value >= 1000 { return String(format: "%.1fк", value / 1000) } else if value == floor(value) { return "\(Int(value))\(unit)" } else { return String(format: "%.1f\(unit)", value) } } }