You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI Charts中点击柱状图时如何在柱顶显示标注?

问题:点击柱状图时标注始终显示在图表中间,无法定位到对应柱体顶部

在使用SwiftUI的Charts框架时,给柱状图添加点击触发的标注后,标注总是出现在图表中间,而非对应柱体的顶部(预期效果如下截图所示),尝试多种方法均无法解决。

预期标注位置

解决方案

核心问题在于手动计算柱体高度导致Chart框架无法正确关联标注位置,且默认的.annotation会自动显示而非点击触发。可通过以下调整解决:

  1. 让Chart自动管理Y轴数据
    移除手动计算柱体高度的代码,直接使用原始focusCount作为Y轴值,让Chart框架自动处理缩放,确保标注能绑定到柱体的真实顶部坐标。

  2. 用chartOverlay实现点击触发的标注
    通过chartOverlay添加手势,捕获点击位置对应的图表数据,动态计算标注坐标,精准定位到点击的柱体顶部。

  3. 保留隐藏Y轴的需求
    保持原有设计,隐藏Y轴不影响功能实现。

修改后的完整代码

import SwiftUI
import Charts

struct FocusChartView: View {
    var colorStyleTask: Array<Color> = [Color(.black).opacity(0.7), Color(.white)]
    
    let focusStats: [FocusTime] = [
        .init(date: Date.from(year: 2024, month: 1, day: 1), focusCount: 1),
        .init(date: Date.from(year: 2024, month: 2, day: 1), focusCount: 3),
        .init(date: Date.from(year: 2024, month: 3, day: 1), focusCount: 7),
        .init(date: Date.from(year: 2024, month: 4, day: 1), focusCount: 12),
        .init(date: Date.from(year: 2024, month: 5, day: 1), focusCount: 5),
        .init(date: Date.from(year: 2024, month: 6, day: 1), focusCount: 12),
        .init(date: Date.from(year: 2024, month: 7, day: 1), focusCount: 1),]
    
    // 存储选中的柱体数据,用于显示标注
    @State private var selectedFocus: FocusTime?
    
    var body: some View {
        VStack {
            taskCharts
        }
    }
    
    private var taskCharts: some View {
        ZStack {
            Color(.gray)
                .ignoresSafeArea()
            Chart(focusStats) { viewMonth in
                BarMark(
                    x: .value("Month", viewMonth.date, unit: .month),
                    y: .value("Views", viewMonth.focusCount)
                )
                .foregroundStyle(
                    LinearGradient(
                        gradient: Gradient(colors: colorStyleTask),
                        startPoint: .bottom,
                        endPoint: .top
                    )
                )
                .clipShape(RoundedRectangle(cornerRadius: 5))
            }
            .frame(height: 151)
            .chartXAxis {
                AxisMarks(values: .stride(by: .month)) { _ in
                    AxisValueLabel(format: .dateTime.month(.abbreviated), centered: true)
                }
            }
            .chartYAxis(.hidden) // 隐藏Y轴
            .chartOverlay { proxy in
                GeometryReader { geometry in
                    Rectangle()
                        .fill(.clear)
                        .contentShape(Rectangle())
                        .onTapGesture { location in
                            // 将点击位置转换为图表内部坐标
                            let plotAreaFrame = geometry[proxy.plotAreaFrame]
                            let plotLocation = CGPoint(
                                x: location.x - plotAreaFrame.origin.x,
                                y: location.y - plotAreaFrame.origin.y
                            )
                            
                            // 获取点击位置对应的月份数据
                            if let tappedMonth: Date = proxy.value(atX: plotLocation.x) {
                                selectedFocus = focusStats.first {
                                    Calendar.current.isDate($0.date, equalTo: tappedMonth, toGranularity: .month)
                                }
                            }
                        }
                        .overlay(
                            // 动态显示选中的标注
                            Group {
                                if let selected = selectedFocus {
                                    // 获取选中柱体的顶部坐标
                                    if let markPosition = proxy.position(
                                        for: BarMark(
                                            x: .value("Month", selected.date, unit: .month),
                                            y: .value("Views", selected.focusCount)
                                        )
                                    ) {
                                        let annotationX = geometry[proxy.plotAreaFrame].origin.x + markPosition.x
                                        let annotationY = geometry[proxy.plotAreaFrame].origin.y + markPosition.y - 10 // 向上偏移避免遮挡柱体
                                        
                                        VStack {
                                            Text(selected.date.formattedToMonthAndYear())
                                            Text(selected.focusCount.toHoursAndMinutes())
                                                .fontWeight(.bold)
                                        }
                                        .padding(.top, 8)
                                        .padding(.horizontal, 12)
                                        .background(Color(.white))
                                        .cornerRadius(5)
                                        .shadow(color: .black.opacity(0.1), radius: 10, x: 0, y: 3)
                                        .position(x: annotationX, y: annotationY)
                                    }
                                }
                            }
                        )
                }
            }
        }
    }
}

#Preview {
    FocusChartView()
}


struct FocusTime: Identifiable {
    let id = UUID()
    let date: Date
    let focusCount: Int
}

extension Int {
    func toHoursAndMinutes() -> String {
        let hours = self / 60
        let minutes = self % 60
        return "\(hours)h \(minutes)m"
    }
}

extension Date {
    static func from(year: Int, month: Int, day: Int) -> Date {
        let components = DateComponents(year: year, month: month, day: day)
        return Calendar.current.date(from: components)!
    }
    
    func formattedToMonthAndYear() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMMM, yyyy"
        return formatter.string(from: self)
    }
}

关键修改说明

  • 移除了手动计算scaledHeight、relativeHeight的代码,直接使用原始focusCount作为Y轴值,确保Chart能正确映射柱体坐标。
  • 添加selectedFocus状态变量存储点击选中的柱体数据。
  • 借助chartOverlay和proxy获取点击位置对应的图表数据,并计算标注的精准显示位置。
  • 通过overlay动态渲染标注,确保其定位到对应柱体的顶部。

内容的提问来源于stack exchange,提问作者Yaroslav Nechyporuk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 17:45:02