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

SwiftUI Charts标注适配问题:点击边缘柱状图时图表偏移

SwiftUI Charts 柱状图点击边缘标注导致偏移的解决方案

问题原因

点击柱状图边缘区域时,通过RuleMark的.annotation添加的标注会因容器适配需求触发Chart的布局重计算,导致柱状图整体偏移。本质是annotation属于Chart内部元素,其尺寸变化会影响Chart的布局空间分配。

解决方案

将标注移至chartOverlay中实现,让标注作为Chart的上层视图独立渲染,避免干扰Chart本身的布局。同时添加边缘位置修正,确保标注始终在容器内显示。

修改后的完整代码

struct BarChartView: View {
    struct BarData: Identifiable, Equatable {
        var type: String
        var date: Date
        var count: Float
        var id: Int
    }
    
    var data: [BarData] = [
        BarData(type: "test1", date: Date(), count: 10.0, id: 1),
        BarData(type: "test2", date: Date().addingTimeInterval(86400), count: 25.0, id: 2),
        BarData(type: "test3", date: Date().addingTimeInterval(172800), count: 40.0, id: 3)
    ]

    @State var activeItem: BarData?
    private let annotationWidth: CGFloat = 150 // 标注预估宽度,可根据实际内容调整
    
    var body: some View {
        NavigationStack{
            VStack(alignment : .leading, spacing: 15) {
                Text("testing errors")
                VStack{
                    Chart(data) { item in
                        BarMark(
                            x: .value("Date",  item.date, unit: .day),
                            y: .value("Sum", item.count )
                        )
                        // 保留虚线标记,移除原annotation
                        if let activeItem, activeItem.id == item.id {
                            RuleMark(x: .value("Day", activeItem.date))
                                .lineStyle(.init(lineWidth: 2, miterLimit: 2, dash: [2], dashPhase: 5))
                        }
                    }
                    .chartOverlay { proxy in
                        GeometryReader { geometry in
                            Rectangle()
                                .fill(.clear)
                                .contentShape(Rectangle())
                                .gesture(
                                    DragGesture()
                                        .onChanged { value in
                                            let location = value.location
                                            guard let date = proxy.value(atX: location.x) as Date? else {
                                                activeItem = nil
                                                return
                                            }
                                            // 匹配最近的日期数据
                                            activeItem = data.min(by: {
                                                abs($0.date.timeIntervalSince(date)) < abs($1.date.timeIntervalSince(date))
                                            })
                                        }
                                )
                            
                            // 在Overlay中渲染标注
                            if let activeItem {
                                let xPos = proxy.position(forX: activeItem.date) ?? 0
                                let yPos = proxy.position(forY: activeItem.count) ?? 0
                                
                                VStack(alignment: .leading, spacing: 6){
                                    HStack{
                                        Text("Type")
                                            .font(.caption)
                                            .foregroundColor(.gray)
                                        
                                        Text(activeItem.type)
                                            .font(.title3.bold())
                                    }
                                    HStack{
                                        Text("Sum")
                                            .font(.caption)
                                            .foregroundColor(.gray)
                                        Text("\(activeItem.count, specifier: "%.2f")")
                                            .font(.title3.bold())
                                    }
                                }
                                .padding(.horizontal, 10)
                                .padding(.vertical, 4)
                                .background(
                                    RoundedRectangle(cornerRadius: 6, style: .continuous)
                                        .fill(.black.shadow(.drop(radius: 2)))
                                )
                                .foregroundColor(.white) // 添加白色文字,提升可读性
                                .position(x: xPos, y: yPos - 20)
                                // 修正边缘偏移,避免标注超出容器
                                .offset(x: calculateAnnotationOffset(xPos: xPos, containerWidth: geometry.size.width))
                            }
                        }
                    }
                    .chartYScale(domain: 0...50)
                    .frame(height: 250)
                }
                .padding()
                .background{
                    RoundedRectangle(cornerRadius: 10, style: .continuous)
                        .stroke(.gray, lineWidth: 1)
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
            .padding(.top, 10)
            .navigationTitle("Test charts")
        }
    }
    
    // 计算标注的水平偏移量,确保不超出容器
    private func calculateAnnotationOffset(xPos: CGFloat, containerWidth: CGFloat) -> CGFloat {
        let halfWidth = annotationWidth / 2
        if xPos < halfWidth {
            return halfWidth - xPos
        } else if xPos > containerWidth - halfWidth {
            return containerWidth - halfWidth - xPos
        }
        return 0
    }
}

关键修改点

  1. 移除RuleMark的annotation:将标注移至chartOverlay,使其成为独立于Chart布局的上层视图
  2. 添加边缘偏移修正:通过calculateAnnotationOffset方法,确保标注在靠近容器边缘时自动调整位置,避免超出可视区域
  3. 补全手势逻辑:完善chartOverlay中的拖拽手势,实现点击/拖拽时的选中逻辑
  4. 优化标注样式:添加白色文字提升深色背景下的可读性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 08:36:06