移动RuleMark选择器时SwiftUI折线图变形问题求助
SwiftUI Chart RuleMark选择器仅前两个数据点可移动的问题解决
问题描述
开发SwiftUI折线图并添加RuleMark选择器后,仅前两个数据点能正常触发选择器移动;后续操作时注解值显示正常,但选择器停止移动,折线图Y值绑定到停滞的选择器导致曲线变形。
问题代码
import SwiftUI import Charts struct RigsYTDChartiPadOS: View { @ObservedObject var variables: Variables @State private var showingPopover = false @State private var lineSelection: String? var body: some View { HStack { VStack { Button { } label: { Text("Close") .font(.system(size: 12)) .foregroundColor(.white) .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .frame(width: 70) .frame(maxHeight: 20) } .simultaneousGesture(TapGesture().onEnded { _ in withAnimation { variables.showYTDChart.toggle() } }) .frame(alignment: .topLeading) .background(.secondary) Button { } label: { Text("Expand") .font(.system(size: 12)) .foregroundColor(.white) .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .frame(width: 70) .frame(maxHeight: 20) } .simultaneousGesture(TapGesture().onEnded { _ in variables.showYTDChart = false }) .frame(alignment: .topLeading) .background(.secondary) } VStack { Text("\(variables.selectedRig) Tripping YTD Flat Time") .font(.system(size: 14)) .frame(alignment: .center) .padding(.top, 10) VStack (alignment: .leading, spacing: 0) { let tempMaxYaxis = (variables.rigsYTDTrippingKPIs.map { $0.flatTime }.max() ?? 0) let tempMinYaxis = (variables.rigsYTDTrippingKPIs.map { $0.flatTime }.min() ?? 0) let minYaxis = (tempMinYaxis > 0 ? 0 : tempMinYaxis) let maxYaxis = (tempMaxYaxis < 0 || minYaxis == tempMaxYaxis ? 0 : tempMaxYaxis) let count = variables.rigsYTDTrippingKPIs.count var stops: [Gradient.Stop] { let reference = 0.0 // Or whichever your threshold is let minimum = minYaxis let maximum = maxYaxis let denominator = maximum - minimum let nominator = reference - minimum let result = nominator / denominator let positiveColor = Color(red: 0.643, green: 0.031, blue: 0) let negativeColor = Color(red: 0, green: 0.431, blue: 0.016) return [Gradient.Stop(color: positiveColor, location: 0.0), Gradient.Stop(color: positiveColor, location: result), Gradient.Stop(color: negativeColor, location: result + 0.0001), Gradient.Stop(color: negativeColor, location: 1.0)] } let graphLineColor = Color(red: 0, green: 0.431, blue: 0.016) let graphLineAreaGradient = LinearGradient( gradient: Gradient( colors: [ graphLineColor.opacity(0.6), graphLineColor.opacity(0.1), .clear ] ) , startPoint: .top, endPoint: .bottom ) Chart{ ForEach(variables.rigsYTDTrippingKPIs) { item in AreaMark( x: .value("Date", item.reportDate), y: .value("Flat Time", item.flatTime) ) .interpolationMethod(.cardinal) .foregroundStyle(graphLineAreaGradient) LineMark( x: .value("Date", item.reportDate), y: .value("Flat Time", item.flatTime) ) .interpolationMethod(.cardinal) .foregroundStyle(LinearGradient(stops: stops, startPoint: .bottom, endPoint: .top)) // 问题所在:RuleMark在ForEach内部,每个数据项都生成一个 if let lineSelection { RuleMark( x: .value("Date", lineSelection) ) .foregroundStyle(Color.gray.opacity(0.3)) .offset(yStart: -10) .zIndex(-1) .annotation( position: .top, spacing: 0, overflowResolution: .init( x: .fit(to: .chart), y: .disabled ) ) { Text("Lets See \(lineSelection)") } } } } .chartXSelection(value: $lineSelection) .padding(.top, 30) .padding(.bottom, 50) .padding(.leading, 30) .padding(.trailing, 30) .chartYScale(domain: minYaxis...maxYaxis) .frame(height: .infinity) .chartXAxis{ let dates = variables.rigsYTDTrippingKPIs.map{$0.reportDate} AxisMarks (preset: .automatic, position: .bottom) { data in AxisGridLine() let value = dates[data.index] AxisValueLabel(centered: true, collisionResolution: .truncate, orientation: AxisValueLabelOrientation.horizontal){ Text(value) .multilineTextAlignment(.center) } .font(.system(size: 8)) .foregroundStyle(Color.white.opacity(0.6)) } } .padding(.leading, 3) .chartYAxis { AxisMarks(values: .automatic(desiredCount: 3)) { let value = $0.as(Double.self)! AxisValueLabel { Text("\(String(format: "%.0f", (value * 100)))%") } } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black.opacity(0.7)) .cornerRadius(15) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } func IntToCGFloat(v: Int??) -> CGFloat?? { v.flatMap { $0.flatMap { CGFloat($0) } } } }
解决方案
问题根源
RuleMark被放置在ForEach循环内部,导致每个数据点都会生成一个对应的RuleMark。当选择后续数据点时,前面的RuleMark会覆盖新生成的选择器,视觉上表现为选择器停滞;同时重复的Mark元素会干扰图表的布局计算,引发折线变形。
修复代码
将RuleMark移至Chart根层级,确保整个图表仅存在一个选择器:
import SwiftUI import Charts struct RigsYTDChartiPadOS: View { @ObservedObject var variables: Variables @State private var showingPopover = false @State private var lineSelection: String? var body: some View { HStack { VStack { Button { } label: { Text("Close") .font(.system(size: 12)) .foregroundColor(.white) .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .frame(width: 70) .frame(maxHeight: 20) } .simultaneousGesture(TapGesture().onEnded { _ in withAnimation { variables.showYTDChart.toggle() } }) .frame(alignment: .topLeading) .background(.secondary) Button { } label: { Text("Expand") .font(.system(size: 12)) .foregroundColor(.white) .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .frame(width: 70) .frame(maxHeight: 20) } .simultaneousGesture(TapGesture().onEnded { _ in variables.showYTDChart = false }) .frame(alignment: .topLeading) .background(.secondary) } VStack { Text("\(variables.selectedRig) Tripping YTD Flat Time") .font(.system(size: 14)) .frame(alignment: .center) .padding(.top, 10) VStack (alignment: .leading, spacing: 0) { let tempMaxYaxis = (variables.rigsYTDTrippingKPIs.map { $0.flatTime }.max() ?? 0) let tempMinYaxis = (variables.rigsYTDTrippingKPIs.map { $0.flatTime }.min() ?? 0) let minYaxis = (tempMinYaxis > 0 ? 0 : tempMinYaxis) let maxYaxis = (tempMaxYaxis < 0 || minYaxis == tempMaxYaxis ? 0 : tempMaxYaxis) let count = variables.rigsYTDTrippingKPIs.count var stops: [Gradient.Stop] { let reference = 0.0 // Or whichever your threshold is let minimum = minYaxis let maximum = maxYaxis let denominator = maximum - minimum let nominator = reference - minimum let result = nominator / denominator let positiveColor = Color(red: 0.643, green: 0.031, blue: 0) let negativeColor = Color(red: 0, green: 0.431, blue: 0.016) return [Gradient.Stop(color: positiveColor, location: 0.0), Gradient.Stop(color: positiveColor, location: result), Gradient.Stop(color: negativeColor, location: result + 0.0001), Gradient.Stop(color: negativeColor, location: 1.0)] } let graphLineColor = Color(red: 0, green: 0.431, blue: 0.016) let graphLineAreaGradient = LinearGradient( gradient: Gradient( colors: [ graphLineColor.opacity(0.6), graphLineColor.opacity(0.1), .clear ] ) , startPoint: .top, endPoint: .bottom ) Chart{ ForEach(variables.rigsYTDTrippingKPIs) { item in AreaMark( x: .value("Date", item.reportDate), y: .value("Flat Time", item.flatTime) ) .interpolationMethod(.cardinal) .foregroundStyle(graphLineAreaGradient) LineMark( x: .value("Date", item.reportDate), y: .value("Flat Time", item.flatTime) ) .interpolationMethod(.cardinal) .foregroundStyle(LinearGradient(stops: stops, startPoint: .bottom, endPoint: .top)) } // 修复:将RuleMark移至ForEach外部,仅生成一个选择器 if let lineSelection { RuleMark( x: .value("Date", lineSelection) ) .foregroundStyle(Color.gray.opacity(0.3)) .offset(yStart: -10) .zIndex(-1) .annotation( position: .top, spacing: 0, overflowResolution: .init( x: .fit(to: .chart), y: .disabled ) ) { Text("Lets See \(lineSelection)") } } } .chartXSelection(value: $lineSelection) .padding(.top, 30) .padding(.bottom, 50) .padding(.leading, 30) .padding(.trailing, 30) .chartYScale(domain: minYaxis...maxYaxis) .frame(height: .infinity) .chartXAxis{ let dates = variables.rigsYTDTrippingKPIs.map{$0.reportDate} AxisMarks (preset: .automatic, position: .bottom) { data in AxisGridLine() let value = dates[data.index] AxisValueLabel(centered: true, collisionResolution: .truncate, orientation: AxisValueLabelOrientation.horizontal){ Text(value) .multilineTextAlignment(.center) } .font(.system(size: 8)) .foregroundStyle(Color.white.opacity(0.6)) } } .padding(.leading, 3) .chartYAxis { AxisMarks(values: .automatic(desiredCount: 3)) { let value = $0.as(Double.self)! AxisValueLabel { Text("\(String(format: "%.0f", (value * 100)))%") } } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.black.opacity(0.7)) .cornerRadius(15) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } func IntToCGFloat(v: Int??) -> CGFloat?? { v.flatMap { $0.flatMap { CGFloat($0) } } } }
说明
调整后,RuleMark会根据lineSelection的变化正确更新位置,不会因为重复生成的Mark元素干扰图表布局,选择器可以沿整条折线正常移动,折线变形的问题也会解决。
内容的提问来源于stack exchange,提问作者Mostafa Sadek
相关产品推荐
相关产品推荐

