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) } } ) } } }
关键修改点
- 替换状态变量:用
selectedItem: StockPriceData?代替原来的selectedDate和selectedClose,直接绑定完整数据项,避免拆分数据导致的不一致。 - 修改手势逻辑:
- 先从触摸位置解析出
Date,而非直接获取(Date, Double)插值 - 通过
min(by:)方法在stockDatas中找到与触摸日期最接近的数据项,确保仅使用已有数据
- 先从触摸位置解析出
- 优化日期显示:添加
hourFormatter统一格式化小时级时间,避免原始Date的默认显示混乱。 - 调整RuleMark触发条件:仅当
selectedItem存在时才显示,确保无匹配数据时不展示任何标记。
内容的提问来源于stack exchange,提问作者user20166709
相关产品推荐
相关产品推荐

