SwiftUI 图表点击条形图

问题描述 投票:0回答:2

我使用新的 SwiftUI 图表制作了一个简单的条形图,x 轴上是月份名称,y 轴上是成本。我添加了一个

.chartOverlay
视图修改器,用于跟踪我在图表上点击的位置,并显示
BarMark
以及点击的栏成本文本。

这实现起来非常简单,但是当开始点击栏时,我发现点击第一个和第二个栏会产生想要的结果,但是从第三个栏开始,点击栏并交换第二个栏,但是

BarMark
文字显示正确。

我在这个问题上花了几个小时,但找不到这里出了什么问题。

这里显示了完整的代码,如果有人能看到哪里出了问题:

import SwiftUI
import Charts

final class ViewModel: ObservableObject {
    let months = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr"]
    let costs = [20.0, 40.0, 80.0, 60.0, 75.0, 30.0]
    @Published var forecast: [Forecast] = []
    @Published var selectedMonth: String?
    @Published var selectedCost: Double?

    init() {
        forecast = months.indices.map { .init(id: UUID(), month: months[$0], cost: costs[$0]) }
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        Chart(viewModel.forecast) { data in
            BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
                .foregroundStyle(Color.blue)
            if let selectedMonth = viewModel.selectedMonth, let selectedCost = viewModel.selectedCost {
                RuleMark(x: .value("Selected month", selectedMonth))
                    .annotation(position: .top, alignment: .top) {
                        VStack {
                            Text(estimated(for: selectedMonth, and: selectedCost))
                        }
                    }
            }
        }
        .chartYAxis {
            AxisMarks(position: .leading)
        }
        .chartOverlay { proxy in
            GeometryReader { geometry in
                ZStack(alignment: .top) {
                    Rectangle().fill(.clear).contentShape(Rectangle())
                        .onTapGesture { location in
                            updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
                        }
                }
            }
        }
    }
    
    func updateSelectedMonth(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
        let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
        guard let month: String = proxy.value(atX: xPosition) else {
            return
        }
        viewModel.selectedMonth = month
        viewModel.selectedCost = viewModel.forecast.first(where: { $0.month == month })?.cost
    }

    func estimated(for month: String, and cost: Double) -> String {
        let estimatedString = "estimated: \(cost)"
        return estimatedString
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .padding()
    }
}

struct Forecast: Identifiable {
    let id: UUID
    let month: String
    let cost: Double
}
ios swift swiftui charts swiftui-charts
2个回答
6
投票

尝试这种方法,使用

ForEach
循环并在其后添加
RuleMark

var body: some View {
    Chart { // <-- here
        ForEach(viewModel.forecast) { data in  // <-- here
            BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
                .foregroundStyle(Color.blue)
        }  // <-- here
        
        if let selectedMonth = viewModel.selectedMonth,
           let selectedCost = viewModel.selectedCost {
            RuleMark(x: .value("Selected month", selectedMonth))
                .annotation(position: .top, alignment: .top) {
                    VStack {
                        Text(estimated(for: selectedMonth, and: selectedCost))
                    }
                }
        }
    }
    .chartYScale(domain: 0...100) // <-- here
    .chartYAxis {
        AxisMarks(position: .leading)
    }
    .chartOverlay { proxy in
        GeometryReader { geometry in
            ZStack(alignment: .top) {
                Rectangle().fill(.clear).contentShape(Rectangle())
                    .onTapGesture { location in
                        updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
                    }
            }
        }
    }
}

0
投票

在 iOS 17 中,Apple 引入了一种使用 ChartXSelection 处理图表中选择的本机方法。这使得无需自定义处理即可更轻松地创建交互式可视化。

import SwiftUI
import Charts

struct ChartExample: View {
    @State private var barSelection: String?

    var body: some View {
        Chart {
            ForEach(data) { shape in
                BarMark(
                    x: .value("Month", shape.month),
                    y: .value("Play Count", shape.playCount)
                )
                .cornerRadius(4)
                .foregroundStyle(Color(cgColor: colour3))
            }
            
            // Add a rule mark to highlight the selected bar
            if let barSelection {
                RuleMark(x: .value("Month", barSelection))
                    .foregroundStyle(.green.opacity(0.5))
                    .zIndex(-15)
                    .offset(yStart: -15)
                    .annotation(position: .top,
                                spacing: 0,
                                overflowResolution: .init(x: .disabled, y: .disabled)) {
                        VStack {
                            Text(barSelection)
                        }
                    }
            }
        }
        .chartXSelection(value: $barSelection)
    }
}

// Example data
struct ToyShape: Identifiable {
    var id = UUID()
    var month: String
    var playCount: Int
}

let data = [
    ToyShape(month: "January", playCount: 10),
    ToyShape(month: "February", playCount: 20),
    ToyShape(month: "March", playCount: 15)
]

let colour3 = CGColor(gray: 0.8, alpha: 1)
© www.soinside.com 2019 - 2024. All rights reserved.