I have an app with dates along the x-axis. The user can select a date range for which data will be displayed. This date range can be moved along the x-axis by inputting a date for the date range. By knowing the position of the date range, I can adjust the y-axis position and resolution so that all data points within the range are in view.
Now I want to achieve the same functionality when scrolling the chart. This is my code so far:
Chart {
...
}
.chartScrollableAxes(.horizontal)
.chartScrollPosition(initialX: today)
.chartXVisibleDomain(length: range)
Scrolling works fine, but data points move in and out of view. If I knew which data points lie within the date range (chartXVisibleDomain
), I could adjust the y-axis accordingly.
This is the reason for my question. Or are there other view modifiers that could accomplish this?
You know the length of the x axis (as you used .chartXVisibleDomain(length: range)
). You can also know the current scroll position using chartScrollPosition(x:)
, so the visible range of X values is just all the values from scroll position to (scroll position + range).
And once you know the range of visible X values, you can use that to filter your data.
Here is an example:
struct ChartData: Identifiable {
var id: Date { date }
let date: Date
let y: Double
}
let data = (1..<20).map { i in ChartData(
date: Calendar.current.startOfDay(for: .now).addingTimeInterval(86400 * Double(i)),
y: Double(i)
) }
struct ContentView: View {
@State var scrollPos = Date()
var body: some View {
VStack {
Chart(data) { p in
BarMark(x: .value("X", p.date, unit: .day), y: .value("Y", p.y))
}
.chartScrollPosition(x: $scrollPos)
// two days visible at a time
.chartXVisibleDomain(length: 86400 * 2)
.chartScrollableAxes(.horizontal)
.containerRelativeFrame(.vertical, count: 2, spacing: 0)
Text(scrollPos, format: .dateTime)
// a list to show the visible Y values
List {
let visibleRange = scrollPos.addingTimeInterval(-86400)...scrollPos.addingTimeInterval(86400 * 2)
let visibleData = data.filter { visibleRange.contains($0.date) }
ForEach(visibleData) { p in Text(p.y, format: .number) }
}
}
.padding()
}
}
Note that I subtracted another day from the lower bound of the visible range, because a part of the bar is still visible even if you scroll past the x coordinate of the bar. This is probably not necessary if you are using a line chart.
Another way is to "abuse" chartOverlay
to get your hands on a ChartProxy
, and get the x values through that. This allows you to get the max x value even without knowing the visible length of the x axis.
@State var minX = Date()
@State var maxX = Date()
var body: some View {
VStack {
Chart(data) { p in
BarMark(x: .value("X", p.date, unit: .day), y: .value("Y", p.y))
}
.chartXVisibleDomain(length: 86400 * 2)
.chartScrollableAxes(.horizontal)
.chartOverlay { chart in
GeometryReader { geo in
if let anchor = chart.plotFrame {
let frame = geo[anchor]
if let minX = chart.value(atX: -frame.minX, as: Date.self),
let maxX = chart.value(atX: -frame.minX + geo.size.width, as: Date.self) {
Color.clear
.onChange(of: minX, initial: true) { _, newValue in
self.minX = newValue.addingTimeInterval(-86400)
}
.onChange(of: maxX, initial: true) { _, newValue in
self.maxX = newValue
}
}
}
}
}
.containerRelativeFrame(.vertical, count: 2, spacing: 0)
Text(minX, format: .dateTime)
List {
let visibleRange = minX...maxX
let visibleData = data.filter { visibleRange.contains($0.date) }
ForEach(visibleData) { p in Text(p.y, format: .number) }
}
}
.padding()
}