swiftswiftuiios17swiftui-charts

Swift Charts: chartScrollTargetBehavior fails to snap to a day unit


I am working on a scrollable chart that displays days on the horizontal axis. As the user scrolls, I always want them to be able to snap to a specific day. I implemented the following steps described in this WWDC23 session to achieve this.

  1. I have set the chartScrollTargetBehavior to .valueAligned(matching: DateComponents(hour: 0))
  2. I have set the x value unit on the BarMark to Calendar.Component.day

I ended up with the chart code that looks like this:

Chart(dates, id: \.self) { date in
    BarMark(
        x: .value("Date", date, unit: Calendar.Component.day),
        y: .value("Number", 1)
    )
    .annotation {
        Text(date.formatted(.dateTime.day()))
            .font(.caption2)
    }
}
.chartXAxis {
    AxisMarks(format: .dateTime.day())
}
.chartScrollableAxes(.horizontal)
.chartScrollTargetBehavior(.valueAligned(matching: DateComponents(hour: 0)))
.chartXVisibleDomain(length: fifteenDays)
.chartScrollPosition(x: $selection)

However, this fails to work reliably. There is often a situation where the chart scroll position lands on, for instance, Oct 20, 11:56 PM, but the chart snaps to Oct 21.

enter image description here

I attempted to solve this problem by introducing an intermediate binding between a state value and a chart selection. This binding aims to normalize the selection always to be the first moment of any given date. But this hasn't been successful.

private var selectionBinding: Binding<Date> {
    Binding {
        Calendar.current.startOfDay(for: selection)
    } set: { newValue in
        self.selection = Calendar.current.startOfDay(for: newValue)
    }
}

It's also worth mentioning that this issue also exists in Apple's sample project on Swift Charts.

How would you approach solving this? How can I find a way to make the chart scroll position blind to time values and only recognize whole days?

Here's the minimal reproducible example project for your reference.


Solution

  • To fix the issue, replace:

    .chartScrollTargetBehavior(.valueAligned(matching: DateComponents(hour: 0)))
    

    with

    .chartScrollTargetBehavior(.valueAligned(matching: DateComponents(hour: 0),
                                             majorAlignment: .matching(DateComponents(day: 1))))
    

    As seen in their WWDC video: https://developer.apple.com/videos/play/wwdc2023/10037/

    enter image description here

    With regards to the fact that you're not getting the precise scroll position snapped to the specified time interval, I also have this issue.

    Moreover, it seems that the offset that you're experiencing, i.e. about 4 minutes from the midnight is something that I experience too, in the same ballpark.

    One solution would be to "nudge" or "quantize" the value to the specific time interval. e.g. if you're operating with days in the chart, then check whether the scroll position is before or after noon, or some other buffer time and then treat "quantized" position as already changed.

    To sum up: scroll position is unreliable and while the snapping behavior is working visually correctly, it might report a number slightly behind the date the chart has been snapped to.