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

解决方案
核心问题在于手动计算柱体高度导致Chart框架无法正确关联标注位置,且默认的.annotation会自动显示而非点击触发。可通过以下调整解决:
让Chart自动管理Y轴数据
移除手动计算柱体高度的代码,直接使用原始focusCount作为Y轴值,让Chart框架自动处理缩放,确保标注能绑定到柱体的真实顶部坐标。用
chartOverlay实现点击触发的标注
通过chartOverlay添加手势,捕获点击位置对应的图表数据,动态计算标注坐标,精准定位到点击的柱体顶部。保留隐藏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
相关产品推荐
相关产品推荐

