I have a SwitUI chart with about 180 points. I have an annotation as part of the chart. The annotation tracks the mouse movement slowly, not fast enough. Is there anything I need to do differently in the code below to speed up the tracking of the annotation to the mouse/finger?
It is basically a Line and Area chart combo with simulated values. thanks in advance
Below is the main ContentView code which basically creates the LineArea chart. The annotation is attached to the LineMark
Dummy data is generated in .task{ }
struct ContentView: View {
@State var selectedDate: Date?
@State private var isLoading: Bool = true
@State private var data : [DateValue] = []
var body: some View {
VStack {
Chart {
ForEach(data, id:\.date) { point in
LineMark(
x: .value("Month", point.date, unit: .month),
y: .value("Value", point.value)
)
.lineStyle(StrokeStyle(lineWidth: 3))
if let selectedDate {
RuleMark(x: .value("Month", selectedDate.endOfMonth()))
.annotation(position: .automatic, alignment: .bottom,
overflowResolution: .init(x: .fit, y: .fit)
) {
VStack {
let value = getValue(for: selectedDate.endOfMonth())
Text("Month: \(value.0)")
Text("Value: \(value.1)")
}
.foregroundColor(.white)
.padding()
.background {
RoundedRectangle(cornerRadius: 5)
}
}
}
AreaMark(
x: .value("Month", point.date, unit: .month),
y: .value("Value", point.value)
)
}
.interpolationMethod(.catmullRom)
}
.chartXSelection(value: $selectedDate)
.chartXAxis {
AxisMarks(values: .automatic) { month in
AxisTick()
AxisGridLine()
AxisValueLabel {
if let m = month.as(Date.self) {
Text("\(m.toString(format: "MM/yy"))")
}
}
}
}
.chartYAxis {
AxisMarks(values: .automatic) { value in
AxisTick(centered: false)
AxisGridLine()
AxisValueLabel {
if let v = value.as(Double.self) {
Text("\(v/1000.0, specifier: "%0.0fK")")
}
}
}
}
}
.task {
guard isLoading else { return }
let start = Calendar.current.date(from: DateComponents(year: 2007, month: 1, day: 10))!
let end = Calendar.current.date(from: DateComponents(year: 2023, month: 12, day: 10))!
let months = end.months(from: start)
self.data = (0 ..< months).map{index -> DateValue in
return DateValue(date: start.plusMonths(index).endOfMonth(), value: Double.random(in: 1000...1600))
}
isLoading.toggle()
}
}
private func getValue(for date: Date) -> (String, String) {
let value = data.first(where: {Calendar.current.isDate($0.date, equalTo: date, toGranularity: .month) == true})?.value ?? 0
return (date.toString(format: "MM/yyyy"), String(format: "%.2f", value))
}
}
struct DateValue {
var date: Date
var value: Double
}
extension Date {
func startOfMonth() -> Date {
var components = Calendar.current.dateComponents([.year, .month], from: self)
components.day = 1
return Calendar.current.date(from: components)!
}
func endOfMonth() -> Date {
Calendar.current.date(byAdding: DateComponents(month: 1, day: -1), to: startOfMonth())!
}
func plusMonths(_ plus: Int) -> Date {
Calendar.current.date(byAdding: .month, value: plus, to: self)!
}
func months(from date: Date) -> Int {
Calendar.current.dateComponents([.month], from: date, to:self).month ?? 0
}
func toString(format: String = "yyyy-MM-dd") -> String {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.dateFormat = format
return formatter.string(from: self)
}
}
You have put the RuleMark
inside the ForEach
. This means that when selectedDate
is not nil, there will be about 180 RuleMark
s all overlapping each other on the chart. It's no surprise that this is slow.
You only need one RuleMark
. Put it outside the ForEach
.
Chart {
ForEach(data, id:\.date) { point in
LineMark(
x: .value("Month", point.date, unit: .month),
y: .value("Value", point.value)
)
.lineStyle(StrokeStyle(lineWidth: 3))
AreaMark(
x: .value("Month", point.date, unit: .month),
y: .value("Value", point.value)
)
}
.interpolationMethod(.catmullRom)
if let selectedDate {
RuleMark(x: .value("Month", selectedDate.endOfMonth()))
.annotation(position: .automatic, alignment: .bottom,
overflowResolution: .init(x: .fit, y: .fit)
) {
VStack {
let value = getValue(for: selectedDate.endOfMonth())
Text("Month: \(value.0)")
Text("Value: \(value.1)")
}
.foregroundColor(.white)
.padding()
.background {
RoundedRectangle(cornerRadius: 5)
}
}
}
}