I'm building a Swift Chart that displays locations in a horizontal timeline. The chart is scrollable.
Unfortunately, when chartScrollPosition is offset by some amount (i.e. the user has scrolled the chart), changing chartXVisibleDomain results in chartScrollPosition jumping backwards by some amount.
This results in bad user experience.
A minimum reproducible example is below. To reproduce:
Run the code below
Using the picker, change chartXVisibleDomain. ThechartScrollPosition remains the same, as expected.
Scroll the chart on the horizontal axis.
Using the picker, change chartXVisibleDomain. ThechartScrollPosition changes unexpectedly.
You can verify this by watching the labels at the bottom of the chart. The chart simply ends up showing a different area of the domain. Tested on various iPhone and iPad simulators and physical devices, the issue appears everywhere.
Is this a SwiftUI bug, or am I doing something wrong?
struct ScrollableChartBug: View {
/// Sample data
let data = SampleData.samples
let startDate = SampleData.samples.first?.startDate ?? Date()
let endDate = Date()
/// Scroll position of the chart, expressed as Date along the x-axis.
@State var chartPosition: Date = SampleData.samples.first?.startDate ?? Date()
/// Sets the granularity of the shown view.
@State var visibleDomain: VisibleDomain = .year
var body: some View {
Chart(data, id: \.id) { element in
BarMark(xStart: .value("Start", element.startDate),
xEnd: .value("End", element.endDate),
yStart: 0,
yEnd: 50)
.foregroundStyle(by: .value("Type", element.type.rawValue))
.clipShape(.rect(cornerRadius: 8, style: .continuous))
}
.chartScrollableAxes(.horizontal) // enable scroll
.chartScrollPosition(x: $chartPosition) // track scroll offset
.chartXVisibleDomain(length: visibleDomain.seconds)
.chartXScale(domain: startDate...endDate)
.chartForegroundStyleScale { typeName in
// custom colors for bars and for legend
SampleDataType(rawValue: typeName)?.color ?? .clear
}
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { value in
if let date = value.as(Date.self) {
AxisValueLabel {
Text(date, format: .dateTime.year().month().day())
.bold()
}
AxisTick(length: .label)
}
}
}
.frame(height: 90)
.padding(.bottom, 40) // for overlay picker
.overlay {
Picker("", selection: $visibleDomain.animation()) {
ForEach(VisibleDomain.allCases) { variant in
Text(variant.label)
.tag(variant)
}
}
.pickerStyle(.segmented)
.frame(width: 240)
.padding(.trailing)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
} //: overlay
} //: body
} //: struct
// MARK: - Preview
#Preview {
ScrollableChartBug()
}
// MARK: - Data
enum SampleDataType: String, CaseIterable {
case city, wood, field
var color: Color {
switch self {
case .city:
.gray
case .wood:
.green
case .field:
.brown
}
}
var label: String {
switch self {
case .city:
"City"
case .wood:
"Wood"
case .field:
"Field"
}
}
}
enum VisibleDomain: Identifiable, CaseIterable {
case day
case week
case month
case year
var id: Int {
self.seconds
}
var seconds: Int {
switch self {
case .day:
3600 * 24 * 2
case .week:
3600 * 24 * 10
case .month:
3600 * 24 * 40
case .year:
3600 * 24 * 400
}
}
var label: String {
switch self {
case .day:
"Days"
case .week:
"Weeks"
case .month:
"Months"
case .year:
"Years"
}
}
}
struct SampleData: Identifiable {
let startDate: Date
let endDate: Date
let name: String
let type: SampleDataType
var id: String { name }
static let samples: [SampleData] = [
.init(startDate: Date.from(year: 2022, month: 3, day: 1),
endDate: Date.from(year: 2022, month: 3, day: 10),
name: "New York",
type: .city),
.init(startDate: Date.from(year: 2022, month: 3, day: 20, hour: 6),
endDate: Date.from(year: 2022, month: 5, day: 1),
name: "London",
type: .city),
.init(startDate: Date.from(year: 2022, month: 5, day: 4),
endDate: Date.from(year: 2022, month: 7, day: 5),
name: "Backcountry ABC",
type: .field),
.init(startDate: Date.from(year: 2022, month: 7, day: 5),
endDate: Date.from(year: 2022, month: 10, day: 10),
name: "Field DEF",
type: .field),
.init(startDate: Date.from(year: 2022, month: 10, day: 10),
endDate: Date.from(year: 2023, month: 2, day: 10),
name: "Wood 123",
type: .wood),
.init(startDate: Date.from(year: 2023, month: 2, day: 10),
endDate: Date.from(year: 2023, month: 3, day: 20),
name: "Paris",
type: .city),
.init(startDate: Date.from(year: 2023, month: 3, day: 21),
endDate: Date.from(year: 2023, month: 10, day: 5),
name: "Field GHI",
type: .field),
.init(startDate: Date.from(year: 2023, month: 10, day: 5),
endDate: Date.from(year: 2024, month: 3, day: 5),
name: "Wood 456",
type: .wood),
.init(startDate: Date.from(year: 2024, month: 3, day: 6),
endDate: Date(),
name: "Field JKL",
type: .field)
]
}
extension Date {
/**
Constructs a Date from a given year (Int). Use like `Date.from(year: 2020)`.
*/
static func from(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil) -> Date {
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = Calendar.current.date(from: components) else {
print(#function, "Failed to construct date. Returning current date.")
return Date()
}
return date
}
}
Swift Charts
RSS for tagVisualize data with highly customizable charts across all Apple platforms using the compositional syntax of SwifUI.
Posts under Swift Charts tag
44 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
Hi there,
I have a TabView in page style. Inside that TabView I have a number of views, each view is populated with a model object from an array. The array is iterated to provide the chart data.
Here is the code:
TabView(selection: $displayedChartIndex) {
ForEach((0..<data.count), id: \.self) { index in
ZStack {
AccuracyLineView(graphData: tabSelectorModel.lineChartModels[index])
.padding(5)
}
.tag((index))
}
}
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
I am seeing odd behaviour, as I swipe left and right, occasionally the chart area shows the chart from another page in the TabView. I know the correct view is being shown as there are text elements.
See the screenshot below. The screen on the right is running iOS 17.2 and this works correctly. The screen on the left is running iOS 17.4 and the date at the top is correct which tells me that the data object is correct. However the graph is showing a chart from a different page. When I click on the chart on the left (I have interaction enabled) then it immediately draws the correct chart. If I disable the interaction then I still get the behaviour albeit the chart never corrects itself because there is no interaction!
I can reproduce this in the 17.4 simulator and it is happening in my live app on iOS17.4. This has only started happening since iOS 17.4 dropped and works perfectly in iOS 17.2 simulator and I didn't notice it in the live app when I was running 17.3.
Is this a bug and/or is there a workaround?
For info this is the chart view code, it is not doing anything clever:
struct AccuracyLineView: View {
@State private var selectedIndex: Int?
let graphData: LineChartModel
func calcHourMarkers (maxTime: Int) -> [Int] {
let secondsInDay = 86400 // 60 * 60 * 24
var marks: [Int] = []
var counter = 0
while counter <= maxTime {
if (counter > 0) {
marks.append(counter)
}
counter += secondsInDay
}
return marks
}
var selectedGraphMark: GraphMark? {
var returnMark: GraphMark? = nil
var prevPoint = graphData.points.first
for point in graphData.points {
if let prevPoint {
if let selectedIndex, let lastPoint = graphData.points.last, ((point.interval + prevPoint.interval) / 2 > selectedIndex || point == lastPoint) {
if point == graphData.points.last {
if selectedIndex > (point.interval + prevPoint.interval) / 2 {
returnMark = point
} else {
returnMark = prevPoint
}
} else {
returnMark = prevPoint
break
}
}
}
prevPoint = point
}
return returnMark
}
var body: some View {
let lineColour:Color = Color(AppTheme.globalAccentColour)
VStack {
HStack {
Image(systemName: "clock")
Text(graphData.getStartDate() + " - " + graphData.getEndDate()) // 19-29 Sept
.font(.caption)
.fontWeight(.light)
Spacer()
}
Spacer()
Chart {
// Lines
ForEach(graphData.points) { item in
LineMark(
x: .value("Interval", item.interval),
y: .value("Offset", item.timeOffset),
series: .value("A", "A")
)
.interpolationMethod(.catmullRom)
.foregroundStyle(lineColour)
.symbol {
Circle()
.stroke(Color(Color(UIColor.secondarySystemGroupedBackground)), lineWidth: 4)
.fill(AppTheme.globalAccentColour)
.frame(width: 10)
}
}
ForEach(graphData.trend) { item in
LineMark (
x: .value("Interval", item.interval),
y: .value("Offset", item.timeOffset)
)
.foregroundStyle(Color(UIColor.systemGray2))
}
if let selectedGraphMark {
RuleMark(x: .value("Offset", selectedGraphMark.interval))
.foregroundStyle(Color(UIColor.systemGray4))
}
}
.chartXSelection(value: $selectedIndex)
.chartXScale(domain: [0, graphData.getMaxTime()])
}
}
}
I'm currently evaluating Swift Charts to use in my macOS app, where I need to (potentially) display a few millions of data points, ideally all of them at one time. I want to give users the possibility to zoom in & out, so the entire range of values could be displayed at one moment.
However, starting at around 20K data points (on my computer), the Chart takes a little bit to set up, but the window resizing is laggy. The performance seems to decrease linearly (?), when dealing with 100K data points you can barely resize the window and the Chart setup/creation is noticeable enough. Dealing with 500K data points is out of the question, the app is pretty much not useable.
So I'm wondering if anybody else had a similar issue and what can be done? Is there any "magic" Swift Charts setting that could improve the performance? I have a "data decimation" algorithm, and given no choice I will use it, but somehow I was hoping for Swift Charts to gracefully handle at least 100K data points (there are other libs which do this!). Also, limiting the displayed data range is out of the question for my case, this is a crucial feature of the app.
Here's the code that I'm using, but it's the most basic one:
struct DataPoint: Identifiable {
var id: Double { Double(xValue) }
let xValue: Int
let yValue: Double
}
let dataPoints: [DataPoint] = (0..<100_000).map { DataPoint(xValue: $0, yValue: Double($0)) }
struct MyChart: View {
var body: some View {
Chart(dataPoints) { dataPoint in
PointMark(x: .value("Index", dataPoint.xValue),
y: .value("Y Value", dataPoint.yValue))
}
}
}
Some additional info, if it helps:
The Chart is included in a AppKit window via NSHostingController (in my sample project the window contains nothing but the chart)
The computer is a MacBook Pro, 2019 and is running macOS 10.14
Hi there, I'm currently using UIHostingController to display swift charts in uikit. The problem im facing is that the UIHostingController isn't outputting the intended theme. When the simulator/phone is on dark mode the view is still in light mode. Iv'e tried to force the view to use dark mode with:
.environment(\.colorScheme, .dark)
But it doesn't seem to help. Here's how I implement the UIHostingController to my view:
let controller = UIHostingController(rootView: StatVC())
controller.view.translatesAutoresizingMaksIntoConstraints = false
addChild(controller)
controller.didMove(toParent: self)
view.addSubview(controller.view)
where StatVC() is the swiftui view which contains the swift chart.