import SwiftUI // MARK: - Stat definition struct ChartStat { let label: String let value: String let color: Color } // MARK: - Stats summary row (horizontal scroll of chips) struct ChartStatsRow: View { let stats: [ChartStat] var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 10) { ForEach(stats.indices, id: \.self) { i in VStack(alignment: .center, spacing: 3) { Text(stats[i].label) .font(.caption2) .foregroundColor(.secondary) Text(stats[i].value) .font(.subheadline.weight(.semibold)) .foregroundColor(stats[i].color) } .frame(minWidth: 60) .padding(.horizontal, 12) .padding(.vertical, 8) .background(Color(.systemGray6)) .cornerRadius(8) } } } } } // MARK: - Data table row model struct ChartDataTableRow: Identifiable { let id = UUID() let label: String let value: String let deltaPrev: String? let deltaFirst: String? let isPrevPositive: Bool let isFirstPositive: Bool } // MARK: - Expandable data table struct ChartDataTable: View { let rows: [ChartDataTableRow] let valueHeader: String let deltaPrevHeader: String? let deltaFirstHeader: String? @State private var isExpanded = false private let previewCount = 5 var body: some View { VStack(spacing: 0) { tableHeader Divider() let displayed = isExpanded ? rows : Array(rows.suffix(previewCount)) ForEach(displayed) { row in tableDataRow(row) if row.id != displayed.last?.id { Divider().opacity(0.35) } } if rows.count > previewCount { Divider().opacity(0.35) Button { withAnimation(.easeInOut(duration: 0.15)) { isExpanded.toggle() } } label: { HStack(spacing: 4) { Text(isExpanded ? "Show less" : "Show all \(rows.count)") .font(.caption) Image(systemName: isExpanded ? "chevron.up" : "chevron.down") .font(.caption2) } .foregroundColor(.appPrimary) .frame(maxWidth: .infinity) .padding(.vertical, 8) } } } .background(Color(.systemGray6)) .cornerRadius(8) } private var tableHeader: some View { HStack(spacing: 0) { Text("Date").font(.caption2.weight(.semibold)).foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) Text(valueHeader).font(.caption2.weight(.semibold)).foregroundColor(.secondary) .frame(width: 72, alignment: .trailing) if let h = deltaPrevHeader { Text(h).font(.caption2.weight(.semibold)).foregroundColor(.secondary) .frame(width: 62, alignment: .trailing) } if let h = deltaFirstHeader { Text(h).font(.caption2.weight(.semibold)).foregroundColor(.secondary) .frame(width: 62, alignment: .trailing) } } .padding(.horizontal, 12) .padding(.vertical, 6) } private func tableDataRow(_ row: ChartDataTableRow) -> some View { HStack(spacing: 0) { Text(row.label).font(.caption2).foregroundColor(.primary) .frame(maxWidth: .infinity, alignment: .leading) Text(row.value).font(.caption2.weight(.medium)) .frame(width: 72, alignment: .trailing) if let dp = row.deltaPrev, deltaPrevHeader != nil { Text(dp).font(.caption2.weight(.medium)) .foregroundColor(row.isPrevPositive ? .positiveGreen : .negativeRed) .frame(width: 62, alignment: .trailing) } if let df = row.deltaFirst, deltaFirstHeader != nil { Text(df).font(.caption2.weight(.medium)) .foregroundColor(row.isFirstPositive ? .positiveGreen : .negativeRed) .frame(width: 62, alignment: .trailing) } } .padding(.horizontal, 12) .padding(.vertical, 5) } } // MARK: - Compact date formatter private let tableMonthFormatter: DateFormatter = { let f = DateFormatter() f.setLocalizedDateFormatFromTemplate("MMMy") return f }() func chartTableDateLabel(_ date: Date) -> String { tableMonthFormatter.string(from: date) }