Skip to content

Instantly share code, notes, and snippets.

@Midbin
Last active May 19, 2025 08:38
Show Gist options
  • Save Midbin/c275098ed1151e51a0f3441ea69f921f to your computer and use it in GitHub Desktop.
Save Midbin/c275098ed1151e51a0f3441ea69f921f to your computer and use it in GitHub Desktop.
A big and long but performant scrolling Swift Chart
import SwiftUI
import Charts
import GameplayKit
let gaussianRandoms = GKGaussianDistribution(lowestValue: 0, highestValue: 20)
func date(year: Int, month: Int, day: Int = 1) -> Date {
Calendar.current.date(from: DateComponents(year: year, month: month, day: day)) ?? Date()
}
func randomSalesForDay(_ dayNumber: Double) -> Int {
// Add noise to the generated data.
let yearlySeasonality = 100.0 * (0.5 - 0.5 * cos(2.0 * .pi * (dayNumber / 364.0)))
let monthlySeasonality = 10.0 * (0.5 - 0.5 * cos(2.0 * .pi * (dayNumber / 30.0)))
let weeklySeasonality = 30.0 * (1 - cos(2.0 * .pi * ((dayNumber + 2.0) / 7.0)))
return Int(yearlySeasonality + monthlySeasonality + weeklySeasonality + Double(gaussianRandoms.nextInt()))
}
let allData: [(day: Date, sales: Int)] = stride(from: 0, to: 7283, by: 1).compactMap {
let startDay: Date = date(year: 2003, month: 07, day: 01) // 200 days before WWDC
let day: Date = Calendar.current.date(byAdding: .day, value: $0, to: startDay)!
let dayNumber = Double($0)
var sales = randomSalesForDay(dayNumber)
let dayOfWeek = Calendar.current.component(.weekday, from: day)
if dayOfWeek == 6 {
sales += gaussianRandoms.nextInt() * 3
} else if dayOfWeek == 7 {
sales += gaussianRandoms.nextInt()
} else {
sales = Int(Double(sales) * Double.random(in: 4...5) / Double.random(in: 5...6))
}
return (
day: day,
sales: sales
)
}
func daysAround(from startDay: Date) -> [(day: Date, sales: Int)] {
allData.filter{$0.day > startDay - 3600 * 24 * 70 && $0.day < startDay + 3600 * 24 * 70}
}
func marksAround(from startDay: Date, bounds: Range<Date>) -> [Date] {
let cal = Calendar.current
let totalStart = cal.date(byAdding: .day, value: -31, to: startDay)!
let alignedStart = cal.date(bySettingHour: 0, minute: 0, second: 0, of: totalStart)!
//Should be calendar week to be persistant but demonstrates the basics
return stride(from: 0, to: 93, by: 7).map {
cal.date(byAdding: .day, value: $0, to: alignedStart)!
}.filter{bounds.contains($0)}
}
struct ContentView: View {
@State var scrollPosition: TimeInterval = date(year: 2023, month: 05, day: 01).timeIntervalSinceReferenceDate
@State var lastScrollPosition = date(year: 2023, month: 05, day: 01).timeIntervalSinceReferenceDate
@State var data = daysAround(from: date(year: 2023, month: 05, day: 01))
@State var dataBounds = date(year: 2003, month: 07, day: 01) ..< date(year: 2023, month: 06, day: 30) // Lets say we have daily Data for the last 20 Years
@State var chartMarks = marksAround(from: date(year: 2023, month: 05, day: 01), bounds: date(year: 1900, month: 07, day: 01) ..< date(year: 2023, month: 06, day: 30))
@ChartContentBuilder
var bounds: some ChartContent {
PointMark(x: .value("Left Bound", dataBounds.lowerBound))
.opacity(0)
.accessibilityHidden(true)
PointMark(x: .value("Right Bound", dataBounds.upperBound))
.opacity(0)
.accessibilityHidden(true)
}
var body: some View {
Chart{
bounds
ForEach(data, id: \.day) {
BarMark(
x: .value("Day", $0.day, unit: .day),
y: .value("Sales", $0.sales)
)
}
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 30)
.chartScrollTargetBehavior(
.valueAligned(
matching: .init(hour: 0),
majorAlignment: .matching(.init(day: 1)),
limitBehavior: .always))
.chartScrollPosition(x: $scrollPosition)
.chartXAxis {
AxisMarks(values: chartMarks){
AxisTick()
AxisGridLine()
AxisValueLabel(format: .dateTime.month().day().year())
}
}
.chartYScale(domain: 0...200)
.onChange(of: scrollPosition, initial: false) {
if abs(scrollPosition - lastScrollPosition) > 3600 * 24 * 20 {
let scrollDate = Date(timeIntervalSinceReferenceDate: scrollPosition)
self.data = daysAround(from: scrollDate)
self.chartMarks = marksAround(from: scrollDate, bounds: dataBounds) // If the Marks are calculated by Swift Charts the performance gets pretty bad.
lastScrollPosition = scrollPosition
}
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment