import SwiftUI import Charts // MARK: - Point value labels for line charts // // Shared pieces for the "points + value labels" behavior across every line chart // in the Charts tab: permanent labels when the series is short (they carry a lot // of value at a glance), and a tap/drag selection bubble when it's dense. enum ChartLabels { /// A series with at most this many points shows its value labels permanently. static let alwaysShowThreshold = 12 /// Max permanent labels to render before thinning — fewer on the narrow /// iPhone screen where crammed labels overlap and become unreadable. static func maxLabels(compact: Bool) -> Int { compact ? 5 : 8 } /// Stride between labelled points so at most `maxLabels` show. static func stride(count: Int, compact: Bool) -> Int { let maxN = maxLabels(compact: compact) guard count > maxN else { return 1 } return Int(ceil(Double(count) / Double(maxN))) } /// Whether point `index` should carry a permanent label: always the first /// and last (the endpoints matter most), plus every `stride`-th in between. /// On iPhone (compact) permanent labels collide with the axis on the narrow /// screen, so only the endpoint keeps its label — exact values live in the /// scrub header instead. static func showsLabel(index: Int, count: Int, compact: Bool) -> Bool { guard count > 0 else { return false } if compact { return count <= 6 ? true : index == count - 1 } if index == 0 || index == count - 1 { return true } let s = stride(count: count, compact: compact) // Avoid a label landing right next to the forced last one. if index >= count - 1 - (s / 2) { return false } return index % s == 0 } static func compactCurrency(_ value: Decimal) -> String { value.compactCurrencyString } static func percent(_ value: Double, decimals: Int = 1) -> String { String(format: "%+.\(decimals)f%%", value) } } /// Small capsule bubble used both for permanent point labels and the selection popover. struct ChartValueBubble: View { let text: String var color: Color = .appPrimary var prominent: Bool = false var body: some View { Text(text) .font(prominent ? .caption.weight(.bold) : .caption2.weight(.semibold)) .foregroundStyle(prominent ? .white : color) .padding(.horizontal, prominent ? 8 : 5) .padding(.vertical, prominent ? 4 : 2) .background( Capsule().fill(prominent ? color : color.opacity(0.12)) ) .fixedSize() } } /// Multi-line selection card (one row per series) used by multi-series charts. struct ChartSelectionCard: View { let title: String let rows: [(label: String, value: String, color: Color)] var body: some View { VStack(alignment: .leading, spacing: 3) { Text(title) .font(.caption2.weight(.semibold)) .foregroundStyle(.secondary) ForEach(Array(rows.enumerated()), id: \.offset) { _, row in HStack(spacing: 5) { Circle().fill(row.color).frame(width: 6, height: 6) Text(row.label) .font(.caption2) .foregroundStyle(.secondary) Text(row.value) .font(.caption2.weight(.semibold)) .foregroundStyle(.primary) } } } .padding(8) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.gray.opacity(0.2), lineWidth: 0.5)) .fixedSize() } }