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

如何在SwiftUI Charts折线图中添加趋势线(双LineMark场景)

SwiftUI Charts 实现阶梯折线+趋势线(支持横向滚动)

核心解决方案

要解决两个LineMark样式冲突的问题,关键是让阶梯折线和趋势线成为独立的数据系列,或者通过chartOverlay直接绘制趋势线,同时用横向滚动容器包裹图表实现滚动需求。以下是两种可行方案:


方案1:双独立系列实现(完整趋势线)

通过分别遍历原始数据和趋势线数据,让Chart识别为两个独立系列,各自配置样式:

import SwiftUI
import Charts

// 数据模型
struct ChartData: Identifiable {
    let id = UUID()
    let xValue: Date
    let yValue: Double
}

// 计算线性趋势线数据
func calculateTrendLineData(from data: [ChartData]) -> [ChartData] {
    guard data.count >= 2 else { return [] }
    let xIndices = data.enumerated().map { Double($0.offset) }
    let yValues = data.map { $0.yValue }
    
    let n = Double(xIndices.count)
    let sumX = xIndices.reduce(0, +)
    let sumY = yValues.reduce(0, +)
    let sumXY = zip(xIndices, yValues).map(*).reduce(0, +)
    let sumX² = xIndices.map { $0 * $0 }.reduce(0, +)
    
    let slope = (n * sumXY - sumX * sumY) / (n * sumX² - sumX * sumX)
    let intercept = (sumY - slope * sumX) / n
    
    return data.enumerated().map { index, point in
        ChartData(xValue: point.xValue, yValue: slope * Double(index) + intercept)
    }
}

// 图表视图
struct TargetChartView: View {
    // 模拟数据(20条带日期的数值)
    let rawData: [ChartData] = {
        let calendar = Calendar.current
        var data = [ChartData]()
        for i in 0..<20 {
            let date = calendar.date(byAdding: .day, value: i, to: Date())!
            data.append(ChartData(xValue: date, yValue: Double.random(in: 10...50)))
        }
        return data
    }()
    
    var trendLineData: [ChartData] {
        calculateTrendLineData(from: rawData)
    }
    
    var body: some View {
        // 横向滚动容器
        ScrollView(.horizontal) {
            Chart {
                // 1. 蓝色阶梯折线+数据点
                ForEach(rawData) { point in
                    LineMark(
                        x: .value("日期", point.xValue),
                        y: .value("数值", point.yValue)
                    )
                    .interpolationMethod(.staircase) // 阶梯插值
                    .foregroundStyle(.blue)
                    
                    PointMark(
                        x: .value("日期", point.xValue),
                        y: .value("数值", point.yValue)
                    )
                    .foregroundStyle(.blue)
                    .symbolSize(10)
                }
                
                // 2. 红色趋势线(独立系列)
                ForEach(trendLineData) { point in
                    LineMark(
                        x: .value("日期", point.xValue),
                        y: .value("趋势值", point.yValue)
                    )
                    .interpolationMethod(.linear) // 线性插值
                    .foregroundStyle(.red)
                    .lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 3])) // 虚线样式
                }
            }
            .frame(height: 300)
            .padding(.horizontal, 16)
        }
    }
}

方案2:Overlay绘制趋势线(仅直线场景)

如果趋势线是简单的线性直线,无需生成完整数据点,直接通过chartOverlay计算首尾坐标绘制路径,性能更优:

import SwiftUI
import Charts

struct ChartData: Identifiable {
    let id = UUID()
    let xValue: Date
    let yValue: Double
}

struct TargetChartView: View {
    let rawData: [ChartData] = {
        let calendar = Calendar.current
        var data = [ChartData]()
        for i in 0..<20 {
            let date = calendar.date(byAdding: .day, value: i, to: Date())!
            data.append(ChartData(xValue: date, yValue: Double.random(in: 10...50)))
        }
        return data
    }()
    
    // 计算趋势线首尾点数值
    private func trendLineValues() -> (startY: Double, endY: Double)? {
        guard rawData.count >= 2 else { return nil }
        let xIndices = rawData.enumerated().map { Double($0.offset) }
        let yValues = rawData.map { $0.yValue }
        
        let n = Double(xIndices.count)
        let sumX = xIndices.reduce(0, +)
        let sumY = yValues.reduce(0, +)
        let sumXY = zip(xIndices, yValues).map(*).reduce(0, +)
        let sumX² = xIndices.map { $0 * $0 }.reduce(0, +)
        
        let slope = (n * sumXY - sumX * sumY) / (n * sumX² - sumX * sumX)
        let intercept = (sumY - slope * sumX) / n
        
        return (slope * 0 + intercept, slope * Double(rawData.count - 1) + intercept)
    }
    
    var body: some View {
        ScrollView(.horizontal) {
            Chart {
                // 阶梯折线+数据点
                ForEach(rawData) { point in
                    LineMark(
                        x: .value("日期", point.xValue),
                        y: .value("数值", point.yValue)
                    )
                    .interpolationMethod(.staircase)
                    .foregroundStyle(.blue)
                    
                    PointMark(
                        x: .value("日期", point.xValue),
                        y: .value("数值", point.yValue)
                    )
                    .foregroundStyle(.blue)
                    .symbolSize(10)
                }
            }
            .chartOverlay { proxy in
                GeometryReader { geometry in
                    if let trendValues = trendLineValues(),
                       let startPoint = proxy.position(for: (rawData.first!.xValue, trendValues.startY)),
                       let endPoint = proxy.position(for: (rawData.last!.xValue, trendValues.endY)) {
                        Path { path in
                            path.move(to: startPoint)
                            path.addLine(to: endPoint)
                        }
                        .stroke(.red, style: StrokeStyle(lineWidth: 2, dash: [5, 3]))
                    }
                }
            }
            .frame(height: 300)
            .padding(.horizontal, 16)
        }
    }
}

关键注意事项

  1. 系列独立性:方案1中通过两个独立ForEach绑定不同数据源,让Chart将两条线识别为不同系列,避免样式继承冲突。
  2. 横向滚动:直接用ScrollView(.horizontal)包裹Chart即可实现,无需给Chart额外配置滚动属性。
  3. 性能优化:当数据量较大时,方案2的Overlay绘制方式更高效,无需生成大量趋势线数据点。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 18:25:16