I have used the new SwiftUI Charts to make a simple bar chart, with month names on the x axis and cost on the y axis. I have added a .chartOverlay
view modifier which tracks where I tap on the chart, and shows a BarMark
with a text of the cost of the bar tapped.
This was very straightforward to implement, but when starting tapping on the bars, I found that tapping on the first and second bar produced the wanted result, but starting with the third bar, the bar tapped and the second bar was swapped, but the BarMark
text showed was correct.
I have used a few hours on this problem, but can't find out what is wrong here.
The complete code is shown here, if anyone can see what is wrong:
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
}
CodePudding user response:
try this approach, using a ForEach
loop and having the RuleMark
after it:
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)
}
}
}
}
}