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

SwiftUI Charts点击柱状图显示标注时柱状图位置偏移问题求助

解决SwiftUI Charts点击柱状图后位置偏移问题

问题根源

当前代码给所有柱状图都绑定了固定的annotation,无论是否选中,多余的标注会干扰图表布局导致偏移;同时嵌套在水平ScrollView中时,点击位置的计算未考虑滚动偏移,也会间接影响图表的重绘逻辑。

修复方案

1. 仅给选中的柱状图显示标注

修改BarMark的annotation逻辑,只在当前柱子匹配选中状态时才渲染标注,避免多余元素挤占布局空间:

BarMark(
    x: .value("Week", barMark.week),
    y: .value("Recalls", Double(barMark.count))
)
.foregroundStyle(Color("BorderColor"))
// 仅选中的柱子显示标注
.annotation(position: .top, alignment: .center) {
    if barMark.week == selectedState {
        Text(selectedState ?? "")
            .padding(4)
            .background(Color.white)
            .cornerRadius(4)
            .shadow(radius: 2)
    }
}

2. 修正ScrollView中的点击位置计算

由于图表嵌套在水平ScrollView中,需要跟踪滚动偏移来修正点击坐标:
首先定义偏好键跟踪滚动偏移:

private struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

然后给ScrollView添加偏移跟踪:

// 添加状态变量记录滚动偏移
@State private var scrollOffset: CGFloat = 0

// 修改ScrollView代码
ScrollView(.horizontal, showsIndicators: false) {
    ScrollViewReader { _ in
        GroupBox ("Recalls by state") {
            // 原有Chart代码保留
        }
        .background(
            GeometryReader { geo in
                Color.clear
                    .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).origin.x)
            }
        )
    }
}
.coordinateSpace(name: "scrollView")
.onPreferenceChange(ScrollOffsetKey.self) { value in
    scrollOffset = value
}

最后更新点击处理方法,修正x坐标计算:

func updateSelectedState(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
    // 结合滚动偏移修正点击位置
    let adjustedX = location.x - geometry[proxy.plotAreaFrame].origin.x - scrollOffset
    guard let state: String = proxy.value(atX: adjustedX) else {
        return
    }
    selectedState = state
    selectedCount = statsVM.getRecallsByState(with: coreVM.recallResults).first { $0.week == state }?.count ?? 0
}

3. 固定图表绘图区域避免重绘偏移

给Chart添加固定高度的绘图区域背景,防止标注的显示/消失触发布局重排:

Chart {
    // 原有ForEach和BarMark代码
}
.chartPlotAreaBackground {
    Color.clear.frame(height: 200) // 固定绘图区域高度
}

完整修改后代码片段

// 新增偏好键
private struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {}
}

// 状态变量
@State private var selectedState: String?
@State private var selectedCount: Int = 0
@State private var scrollOffset: CGFloat = 0

// 视图部分
ScrollView(.horizontal, showsIndicators: false) {
    ScrollViewReader { _ in
        GroupBox ("Recalls by state") {
            Chart {
                let recallData = statsVM.getRecallsByState(with: recallMockData)
                ForEach(recallData, id: \.self) { barMark in
                    BarMark(
                        x: .value("Week", barMark.week),
                        y: .value("Recalls", Double(barMark.count))
                    )
                    .foregroundStyle(Color("BorderColor"))
                    .annotation(position: .top, alignment: .center) {
                        if barMark.week == selectedState {
                            Text(selectedState ?? "")
                                .padding(4)
                                .background(Color.white)
                                .cornerRadius(4)
                                .shadow(radius: 2)
                        }
                    }
                }
            }
            .chartOverlay { proxy in
                GeometryReader { geometry in
                    Rectangle().fill(.clear).contentShape(Rectangle())
                        .onTapGesture { location in
                            updateSelectedState(at: location, proxy: proxy, geometry: geometry)
                        }
                }
            }
            .chartPlotAreaBackground {
                Color.clear.frame(height: 200)
            }
            .padding()
            .chartYAxis {
                AxisMarks(position: .leading)
            }
            .frame(width: 450)
        }
        .background(
            GeometryReader { geo in
                Color.clear
                    .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).origin.x)
            }
        )
    }
}
.coordinateSpace(name: "scrollView")
.onPreferenceChange(ScrollOffsetKey.self) { value in
    scrollOffset = value
}
.frame(height: 250)

// 点击处理方法
func updateSelectedState(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
    let adjustedX = location.x - geometry[proxy.plotAreaFrame].origin.x - scrollOffset
    guard let state: String = proxy.value(atX: adjustedX) else {
        return
    }
    selectedState = state
    selectedCount = statsVM.getRecallsByState(with: coreVM.recallResults).first { $0.week == state }?.count ?? 0
}

关键说明

  • 仅渲染选中柱子的标注,避免多余元素干扰图表布局;
  • 结合ScrollView滚动偏移修正点击位置,确保选中逻辑准确;
  • 固定绘图区域高度,防止标注状态变化触发图表重绘偏移。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 06:12:06