I have a cross hair on the chart implemented with RuleMark.
This causes the chart to be very laggy (it is updated every second with data).
I've tried various hacks but still laggy. When commenting out the rule mark the chart is a snappy 60FPS.
struct DataPoint: Identifiable {
let x: Date,
let y: Int,
var id: Date = {x}
}
struct MyView: View {
@State var xPosition : Date?
@State var dataPoints: [DataPoint]
var body: some View {
if let xPosition {
Text("xPosition \(xPosition.description)")
}
Chart {
ForEach(datapoints) { point in
LineMark(...point...)
}
if let xPosition {
Plot {
RuleMark(x: .value("x", xPosition))
.foregroundStyle(.blue)
}
}
}
.chartXSelection(value: $xPosition)
}
}
UPDATE: Swapped to using a gesture according to the example found here https://developer.apple.com/documentation/swiftui/view/chartoverlay(alignment:content:)
After experimentation with this method the problematic issue occurs when translating the x,y position of the gesture to the corresponding value of the element in the chart:
chartOverlay { proxy {
Rectangle().fill(.clear).contentShape(Rectangle()).gesture(
{
// omitting boiler plate code from apple example in the link
// Get the x (date) and y (amount) value from the location.
// this takes 30 - 50 ms from my experimentation
if let (date, value) = proxy.value(at: location, as: (Date, Float64).self) {
xPosition = date
yPosition = value
} else {
xPosition = nil
yPosition = nil
}
Here is a minimal reproducible example using the chartX/Yselection :
import SwiftUI
import Charts
import Combine
import QuartzCore
import Foundation
struct ContentView: View {
@State var data: DataPump = DataPump()
@State var selectedX: Date? = nil
@State var selectedY: Int? = nil
@State var crossHairs = true
var body: some View {
FrameRateView()
Toggle("cross hair on", isOn: $crossHairs)
if crossHairs {
chart
.chartXSelection(value: $selectedX)
.chartYSelection(value: $selectedY)
} else {
chart
}
}
@ViewBuilder
var chart: some View {
Chart {
ForEach(Array(data.datapoints.enumerated()), id: \.offset) { i, point in
LineMark(x: .value("time", point.date), y: .value("amount", point.value))
RectangleMark(xStart: .value("", data.datapoints2[i].date.addingTimeInterval(-0.2)),
xEnd: .value("", data.datapoints2[i].date.addingTimeInterval(0.2)),
yStart: .value("", data.datapoints2[i].value - 3),
yEnd: .value("", data.datapoints2[i].value + 3)).foregroundStyle(.red)
}
if crossHairs, let selectedX, let selectedY {
RuleMark(x: .value("X", selectedX))
RuleMark(y: .value("Y", selectedY))
}
}
}
}
@Observable class DataPump {
var cancelable : Cancellable? = nil
var datapoints: [DataPoint] = {
let now = Date()
var arr = [DataPoint]()
for i in 0...180 {
arr.append(DataPoint(date: now.addingTimeInterval(-Double(180 - i))))
}
return arr
}()
var datapoints2: [DataPoint] = {
let now = Date()
var arr = [DataPoint]()
for i in 0...180 {
arr.append(DataPoint(date: now.addingTimeInterval(-Double(180 - i))))
}
return arr
}()
init() {
cancelable = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink(receiveValue: { value in
self.datapoints.append(DataPoint(date: value))
self.datapoints2.append(DataPoint(date: value))
if self.datapoints.count > 180 {
self.datapoints.removeFirst(self.datapoints.count - 180)
self.datapoints2.removeFirst(self.datapoints2.count - 180)
}
})
}
}
struct DataPoint: Identifiable {
let date: Date
let value: Int = Int.random(in: 7...15)
var id: Date { date }
}
struct FrameRateView: View {
@State private var displayLink = DisplayLink()
var body: some View {
Text(String(format: "Frame Rate: %.0f FPS", displayLink.frameRate))
}
}
@Observable class DisplayLink {
var frameRate: Double = 0
private var displayLink: CADisplayLink?
private var lastTimeStamp: CFTimeInterval = 0
private var cancellable: Cancellable?
init() {
self.displayLink = NSScreen.main?.displayLink(target: self, selector: #selector(updateFrameRate))
self.displayLink?.add(to: .main, forMode: .common)
self.lastTimeStamp = self.displayLink!.timestamp
}
@objc func updateFrameRate() {
let currentTimeStamp = displayLink!.timestamp
let elapsedTime = currentTimeStamp - lastTimeStamp
lastTimeStamp = currentTimeStamp
frameRate = 1 / elapsedTime
}
}