I have been using the drag lines to view the values of the plotted points in the chart. However, the does not show the value of the plotted point plotted on the right hand side of the chart. I have been trying to fix it but I can't get that last point. I want the drag line to only be able to move within the domain of the charts x axis.
@ViewBuilder
func TargetTempChart() -> some View {
if viewModel.filteredEvents.isEmpty {
Text("No data is available")
.foregroundColor(.gray)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
} else {
GeometryReader { geometry in
Chart {
ForEach(viewModel.filteredEvents, id: \.id) { event in
LineMark(
x: .value("Time", event.timestamp, unit: .hour),
y: .value("Target Temperature", event.targetTemperature)
)
.symbol(by: .value("Attribution", event.attribution))
.foregroundStyle(colorForAttribution(event.attribution))
}
}
.frame(height: 350)
.overlay(
Group {
if let dragLocation = dragLocation, let currentActiveItem = currentActiveItem {
Rectangle()
.fill(Color.blue.opacity(0.5))
.frame(width: 1, height: geometry.size.height - 50)
.position(x: dragLocation, y: (geometry.size.height - 50) / 2)
Text("\(currentActiveItem.targetTemperature, specifier: "%.1f")°C")
.padding(5)
.background(Color.green.opacity(0.75))
.foregroundColor(Color.black)
.cornerRadius(5)
.position(x: dragLocation, y: 40)
VStack {
HStack {
VStack {
Text("Time")
.font(.callout)
.foregroundColor(Color.gray)
Text("\(itemFormatter.string(from: currentActiveItem.timestamp))")
.font(.caption)
.foregroundColor(Color.blue)
}
.position(x: 70, y: 385)
VStack {
Text("Attribution")
.font(.callout)
.foregroundColor(Color.gray)
Text("\(currentActiveItem.attribution)")
.font(.caption)
.foregroundColor(Color.blue)
}
.position(x: 80, y: 385)
}
}
}
}
)
.gesture(
DragGesture()
.onChanged { value in
let totalWidth = geometry.size.width
let estimatedEndX = 0.98 * totalWidth
let locationX = value.location.x
let clampedX = max(min(locationX, estimatedEndX), 0)
self.dragLocation = clampedX
self.currentActiveItem = self.closestEvent(to: clampedX, in: geometry.size)
}
.onEnded { _ in
self.dragLocation = nil
self.currentActiveItem = nil
}
)
}
}
}
func colorForAttribution(_ attribution: String) -> Color {
switch attribution {
case "User Changed":
return .blue
case "System Changed":
return .green
default:
return .gray
}
}
private var itemFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}
private func closestEvent(to position: CGFloat, in size: CGSize) -> TargetTempEventData? {
guard !viewModel.filteredEvents.isEmpty, let firstTimestamp = viewModel.filteredEvents.first?.timestamp, let lastTimestamp = viewModel.filteredEvents.last?.timestamp else {
return nil
}
let totalDuration = lastTimestamp.timeIntervalSince(firstTimestamp)
let chartWidth = size.width
let positions = viewModel.filteredEvents.map { event -> (event: TargetTempEventData, position: CGFloat) in
let durationSinceFirst = event.timestamp.timeIntervalSince(firstTimestamp)
let proportionOfTotal = CGFloat(durationSinceFirst / totalDuration)
let estimatedXPosition = proportionOfTotal * chartWidth
return (event, estimatedXPosition)
}
let closest = positions.min(by: { abs($0.position - position) < abs($1.position - position) })?.event
return closest
}