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

SwiftUI折线图RuleMark优化:仅显示已有数据点信息

解决SwiftUI Chart拖动时仅在已有数据点显示RuleMark的问题

核心需求:拖动股票折线图时,仅在已有数据点位置展示对应的close值和小时级日期,禁止Chart计算非数据点的插值close值。

修改思路

原来的proxy.value(at:location, as: (Date, Double).self)会基于图表的线性插值返回任意位置的计算值,导致无数据区域也能触发RuleMark。我们需要:

  • 从触摸位置解析出对应的Date
  • 在已有stockDatas数组中找到最接近该Date的数据项
  • 仅当找到匹配的数据项时,才展示RuleMark和对应的信息

修改后的完整代码

数据模型(无需修改)

struct StockPriceData: Identifiable {
    var id = UUID()
    var timestamp: Date
    var open: Double
    var high: Double
    var low: Double
    var close: Double
}

var stockData: [StockPriceData] = [
    StockPriceData(timestamp: Date().addingTimeInterval(1696291200), open: 28.73093, high: 28.76422, low: 28.72581, close: 28.74253),
    StockPriceData(timestamp: Date().addingTimeInterval(1696294800), open: 28.7427, high: 28.75354, low: 28.70582, close: 28.75354),
    StockPriceData(timestamp: Date().addingTimeInterval(1696298400), open: 28.743, high: 28.79384, low: 28.73365, close: 28.75371),
    StockPriceData(timestamp: Date().addingTimeInterval(1696302000), open: 28.75486, high: 28.79107, low: 28.69415, close: 28.75723),
    StockPriceData(timestamp: Date().addingTimeInterval(1696305600), open: 28.75346, high: 28.77518, low: 28.69728, close: 28.75168),
    StockPriceData(timestamp: Date().addingTimeInterval(1696309200), open: 28.75148, high: 28.76248, low: 28.71292, close: 28.75618),
    StockPriceData(timestamp: Date().addingTimeInterval(1696312800), open: 28.7555, high: 28.77304, low: 28.69977, close: 28.76718),
    StockPriceData(timestamp: Date().addingTimeInterval(1696316400), open: 28.79195, high: 29.07266, low: 28.76586, close: 28.92544),
    StockPriceData(timestamp: Date().addingTimeInterval(1696320000), open: 28.92569, high: 29.07191, low: 28.75944, close: 28.82226),
    StockPriceData(timestamp: Date().addingTimeInterval(1696323600), open: 28.82452, high: 28.86211, low: 28.81454, close: 28.85591),
    StockPriceData(timestamp: Date().addingTimeInterval(1696327200), open: 28.85199, high: 28.88431, low: 28.84245, close: 28.86133),
    StockPriceData(timestamp: Date().addingTimeInterval(1696330800), open: 28.86129, high: 28.87093, low: 28.84494, close: 28.84873)
]

修改后的ContentView

struct ContentView: View {
    @State var stockDatas: [StockPriceData] = stockData
    @State private var selectedItem: StockPriceData?
    
    // 定义小时级日期格式化器
    private static let hourFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter
    }()
    
    var body: some View {
        ChartView
            .chartXScale(domain: stockDatas.map {$0.timestamp}.min()!...stockDatas.map{$0.timestamp}.max()!)
            .chartYScale(domain: stockDatas.map {$0.close}.min()!...stockDatas.map{$0.close}.max()!)
            .chartPlotStyle{ chartPlotStyle($0) }
            .chartXAxis {
                AxisMarks(format: .dateTime.hour())
            }
    }
    
    private var ChartView: some View {
        Chart {
            ForEach(stockDatas) { item in
                LineMark(x: .value("time", item.timestamp),
                         y: .value("price", item.close))
                .foregroundStyle(Color.green)
                
                AreaMark(x: .value("time", item.timestamp),
                         yStart: .value("Min", stockDatas.map{$0.close}.min()!),
                         yEnd: .value("Max", item.close))
                .foregroundStyle(LinearGradient(gradient: Gradient(colors: [.green, .clear]), startPoint: .top, endPoint: .bottom))
                .opacity(0.1)
            }
            .symbol(Circle())
            
            // 仅当选中有效数据项时显示RuleMark
            if let selectedItem = selectedItem {
                RuleMark(x: .value("Selected Date", selectedItem.timestamp))
                    .foregroundStyle(.gray)
                    .annotation(position: .top, alignment: .top) {
                        VStack(alignment: .leading, spacing: 4) {
                            Text(String(format: "Close: %.2f", selectedItem.close))
                                .font(.system(size: 12))
                            Text("Time: \(Self.hourFormatter.string(from: selectedItem.timestamp))")
                                .font(.system(size: 12))
                        }
                        .padding(6)
                        .background(RoundedRectangle(cornerRadius: 5).fill(Color.gray))
                        .foregroundColor(.white)
                    }
            }
        }
        .frame(width: UIScreen.main.bounds.width - 23, height: 200)
        .chartOverlay { proxy in
            GeometryReader { geometry in
                Rectangle().fill(.clear).contentShape(Rectangle())
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .onChanged { value in
                                let origin = geometry[proxy.plotAreaFrame].origin
                                let location = CGPoint(
                                    x: value.location.x - origin.x,
                                    y: value.location.y - origin.y
                                )
                                
                                // 先获取触摸位置对应的Date
                                guard let touchedDate = proxy.value(at: location, as: Date.self) else {
                                    selectedItem = nil
                                    return
                                }
                                
                                // 在已有数据中找到最接近的日期对应的数据项
                                selectedItem = stockDatas.min(by: {
                                    abs($0.timestamp.timeIntervalSince(touchedDate)) < abs($1.timestamp.timeIntervalSince(touchedDate))
                                })
                            }
                            .onEnded { _ in
                                selectedItem = nil
                            }
                    )
            }
        }
    }
    
    private func chartPlotStyle(_ plotContent: ChartPlotContent) -> some View {
        plotContent
            .frame(height: 200)
            .overlay{
                Rectangle()
                    .foregroundColor(.gray.opacity(0.5))
                    .mask(
                        ZStack{
                            VStack{
                                Spacer()
                                Rectangle().frame(height: 1)
                            }
                            HStack{
                                Spacer()
                                Rectangle().frame(width: 0.3)
                            }
                        }
                    )
            }
    }
}

关键修改点

  1. 替换状态变量:用selectedItem: StockPriceData?代替原来的selectedDate和selectedClose,直接绑定完整数据项,避免拆分数据导致的不一致。
  2. 修改手势逻辑:
    • 先从触摸位置解析出Date,而非直接获取(Date, Double)插值
    • 通过min(by:)方法在stockDatas中找到与触摸日期最接近的数据项,确保仅使用已有数据
  3. 优化日期显示:添加hourFormatter统一格式化小时级时间,避免原始Date的默认显示混乱。
  4. 调整RuleMark触发条件:仅当selectedItem存在时才显示,确保无匹配数据时不展示任何标记。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 13:54:51