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

移动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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 22:28:09