Swift Charts

RSS for tag

Visualize data with highly customizable charts across all Apple platforms using the compositional syntax of SwifUI.

Posts under Swift Charts tag

195 Posts

Post

Replies

Boosts

Views

Activity

How to read a value when tapping a point on a Chart3D
Hi. Chart3D seems to be perfect to visualize a chart on one of my apps. However I would need a way to read the point of a SurfacePlot of a Chart3D that was touched by the user? Something like a Chart .chartOverlay that has a ChartProxy that can convert screen coordinates of the chart in values, but in 3D (maybe using hit testing ). Thanks and best regards. João Colaço
2
0
193
Jun ’25
.chartScrollableAxes(.horizontal) prevents chartYAxisLabel from being postioned top right, over Y axis
Adding .chartScrollableAxes(.horizontal) to a chart prevents chartYAxisLabel from being positioned top right, over Y axis. We want the label at top right, over the Y-axis, but with chartScrollableAxes it is always top right relative to the initial chartXVisibleDomain, which puts it in the middle of the chart if chartXVisibleDomain < full x domain. import SwiftUI import Charts struct ContentView: View { @State private var numbers = (0..<100) .map { _ in Double.random(in: 0...100) } @State var visibleDomain : Int = 50 var body: some View { Chart(Array(zip(numbers.indices, numbers)), id: \.1) { index, number in LineMark( x: .value("Index", index), y: .value("Number", number) ) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: visibleDomain) .chartScrollPosition(initialX: 70) .chartYAxisLabel(position: .topTrailing, alignment: .center) { /* We want the label at top right, over the Y-axis, but with chartScrollableAxes it is always top right relative to the initial chartXVisibleDomain, which puts it in the middle of the chart if chartXVisibleDomain < full x domain */ Text("units") .foregroundStyle(.red) .fontWeight(.bold) } .padding() } } #Preview { ContentView() }
1
0
407
Jun ’25
How to have different colors in Charts with AreaMark
I would like to have different fill colors in my chart. What I want to achieve is that if the values drop below 0 the fill color should be red. If they are above the fill color should be red. My code looks as follows: import SwiftUI import Charts struct DataPoint: Identifiable {     let id: UUID = UUID()     let x: Int     let y: Int } struct AlternatingChartView: View {          enum Gradients {         static let greenGradient = LinearGradient(gradient: Gradient(colors: [.green, .white]), startPoint: .top, endPoint: .bottom)         static let blueGradient = LinearGradient(gradient: Gradient(colors: [.white, .blue]), startPoint: .top, endPoint: .bottom)     }          let data: [DataPoint] = [         DataPoint(x: 1, y: 10),         DataPoint(x: 2, y: -5),         DataPoint(x: 3, y: 20),         DataPoint(x: 4, y: -8),         DataPoint(x: 5, y: 15),     ]               var body: some View {         Chart {             ForEach(data) { data in                 AreaMark(                     x: .value("Data Point", data.x),                     y: .value("amount", data.y))                 .interpolationMethod(.catmullRom)                 .foregroundStyle(data.y < 0 ? Color.red : Color.green)                                  LineMark(                 x: .value("Data Point", data.x),                 y: .value("amount", data.y))                 .interpolationMethod(.catmullRom)                 .foregroundStyle(Color.black)                 .lineStyle(StrokeStyle.init(lineWidth: 4))                              }         }         .frame(height: 200)     } } #Preview {     AlternatingChartView() } The result looks like this: I also tried using foregroundStyle(by:) and chartForegroundStyleScale(_:) but the result was, that two separate areas had been drawn. One for the below and one for the above zero datapoints. So, what would be the right approach to have two different fill colors?
0
0
272
Jun ’25
AreaMark Always alignsMarkStylesWithPlotArea for linear gradients
I'm trying to make a Swift Chart where 24 AreaMarks an hour apart on X axis over a day display a vertical gradient. The gradient is vertical and is essentially [Color.opacity(0.1),Colour,Color.opacity(0.1] The idea here is where the upper and lower points of each AreaMark are the same or close to each other in the Y axis, the chart essentially displays a line, where they are far apart you get a nice fading vertical gradient. However, it seems that the .alignsMarkStylesWithPlotArea modifier is always set for AreaMarks even if manually applying it false. Investigating further, I've learnt that with AreaMarks in a series, Swift Charts seems to only listen to the first foreground style set in. I've created some sample code to demonstrate this. struct DemoChartView: View { var body: some View { Chart { AreaMark(x: .value("Time", Date().addingTimeInterval(0)), yStart: .value("1", 40), yEnd: .value("2", 60)) .foregroundStyle(LinearGradient(colors: [.pink, .teal], startPoint: .top, endPoint: .bottom)) .alignsMarkStylesWithPlotArea(false) AreaMark(x: .value("Time", Date().addingTimeInterval(3600)), yStart: .value("1", 44), yEnd: .value("2", 58)) .foregroundStyle(LinearGradient(colors: [.orange, .yellow], startPoint: .top, endPoint: .bottom)) .alignsMarkStylesWithPlotArea(false) AreaMark(x: .value("Time", Date().addingTimeInterval(03600*2)), yStart: .value("1", 50), yEnd: .value("2", 90)) .foregroundStyle(LinearGradient(colors: [.green, .blue], startPoint: .top, endPoint: .bottom)) .alignsMarkStylesWithPlotArea(false) } } } Which produces this: So here, all the different .foregroundStyle LinearGradients are being ignored AND the .alignsMarkStylesWithPlotArea(false) is also ignored - the amount of pink on the first mark is different to the second and third 🤷‍♂️ Has anyone encountered this. Are AreaMarks the correct choice or are they just not setup to create this type of data display. Thanks
3
0
1.2k
Jun ’25
Swift Charts: How to prevent scroll position jump when loading more data dynamically
I'm implementing infinite scrolling with Swift Charts where additional historical data loads when scrolling near the beginning of the dataset. However, when new data is loaded, the chart's scroll position jumps unexpectedly. Current behavior: Initially loads 10 data points, displaying the latest 5 When scrolling backwards with only 3 points remaining off-screen, triggers loading of 10 more historical points After loading, the scroll position jumps to the 3rd position of the new dataset instead of maintaining the current view Expected behavior: Scroll position should remain stable when new data is loaded User's current view should not change during data loading Here's my implementation logic using some mock data: import SwiftUI import Charts struct DataPoint: Identifiable { let id = UUID() let date: Date let value: Double } class ChartViewModel: ObservableObject { @Published var dataPoints: [DataPoint] = [] private var isLoading = false init() { loadMoreData() } func loadMoreData() { guard !isLoading else { return } isLoading = true let newData = self.generateDataPoints( endDate: self.dataPoints.first?.date ?? Date(), count: 10 ) self.dataPoints.insert(contentsOf: newData, at: 0) self.isLoading = false print("\(dataPoints.count) data points.") } private func generateDataPoints(endDate: Date, count: Int) -> [DataPoint] { var points: [DataPoint] = [] let calendar = Calendar.current for i in 0..<count { let date = calendar.date( byAdding: .day, value: -i, to: endDate ) ?? endDate let value = Double.random(in: 0...100) points.append(DataPoint(date: date, value: value)) } return points.sorted { $0.date < $1.date } } } struct ScrollableChart: View { @StateObject private var viewModel = ChartViewModel() @State private var scrollPosition: Date @State private var scrollDebounceTask: Task<Void, Never>? init() { self.scrollPosition = .now.addingTimeInterval(-4*24*3600) } var body: some View { Chart(viewModel.dataPoints) { point in BarMark( x: .value("Time", point.date, unit: .day), y: .value("Value", point.value) ) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 5 * 24 * 3600) .chartScrollPosition(x: $scrollPosition) .chartXScale(domain: .automatic(includesZero: false)) .frame(height: 300) .onChange(of: scrollPosition) { oldPosition, newPosition in scrollDebounceTask?.cancel() scrollDebounceTask = Task { try? await Task.sleep(for: .milliseconds(300)) if !Task.isCancelled { checkAndLoadMoreData(currentPosition: newPosition) } } } } private func checkAndLoadMoreData(currentPosition: Date?) { guard let currentPosition, let earliestDataPoint = viewModel.dataPoints.first?.date else { return } let timeInterval = currentPosition.timeIntervalSince(earliestDataPoint) if timeInterval <= 3 * 24 * 3600 { viewModel.loadMoreData() } } } I attempted to compensate for this jump by adding: scrollPosition = scrollPosition.addingTimeInterval(10 * 24 * 3600) after viewModel.loadMoreData(). However, this caused the chart to jump in the opposite direction by 10 days, rather than maintaining the current position. What's the problem with my code and how to fix it?
6
0
691
Jun ’25
How to display numbers as grid marks along axes for plotting in Swift Charts?
If you try to add a graph for a function in Apple Notes you can see that numbers marking coordinates are positioned along the axes (see screenshot 1). But when I am making my own plot view with Swift Charts I don't see that option. Marks for X axis are positioned at the bottom, and marks for Y axis are positioned to the right. I don't see an API that can configure them to be shown along the axes. Is there something that I am missing? Or is Apple just using some private API for that? I could make a custom overlay to display these marks, but then I will have to adjust them while zooming myself, which can be problematic.
1
0
339
Jun ’25
chartXAxis AxisMark consisting of Month:Year
I have a Chart displaying Counts per Date over 2-3 years. I'd like to have the XAxis mark consist of MM-yy or even MM/nyy. Is this possible? OK, just saw AxisValueLabel(format: .dateTime.month().year()) which gives e.g. Mar 2024. This is good. Better might be Mar 24 (maybe a Y3K problem ;-) or even better Mar. OR Mar 24. 2024 Or best increment the year when it changes. Are any of these alternate formats possible? Thanks, David PS, my current .chartXAxis code .chartXAxis { AxisMarks(values: .stride(by: .month, count: 3)) { value in if value.as(Date.self) != nil { AxisValueLabel(format: .dateTime.month().year()) AxisGridLine() AxisTick() } } }
1
0
252
Jun ’25
How to get the date range of the data that is currently displayed in a Apple Chart?
I have a Swiftui app, and I'm using the Apple Chart framework to chart user transactions over time. I'd like to be able to show an annotation at the top of the chart that shows the date range of the records currently visible on the chart. Much like Apple does in their Heath App charts. But I just can't seem to find a method to read the current date range displayed on the chart once the user has scrolled though the chart. Has anyone done anything similar, or maybe seen some sample code? So far I'm striking out on this... Thanks in advance
1
0
287
May ’25
Issue: Chart Scroll Not Working in iOS 18 with Chart Overlay Tap Gesture
Summary I have a SwiftUI Chart that worked correctly in iOS 17, allowing both horizontal scrolling and tap gesture selection. However, in iOS 18, the same exact chart will not allow for both tap gestures and scrolling to work -- it's either we allow scrolling or we allow tap gestures but not both. We have tried everything to try to circumvent this issue but have had to resort to old methods of creating the chart. This is an issue that has negatively impacted our customers as well. Again, the charts were working fine on iOS 17, but on iOS 18 the chart scroll + tap gesture capability is not working. Expected Behavior (iOS 17) Users can scroll horizontally through the chart. Users can tap on data points to highlight them. The selected data point updates when tapped. Observed Behavior (iOS 18) The chart no longer scrolls when chartOverlay with the Tap Gesture is applied. Tap selection still works as expected. Code Snippet Below is the working implementation from iOS 17: private var iOS17ChartView: some View { Chart { RectangleMark( yStart: .value(String(firstLevelAlertBand), firstLevelAlertBand), yEnd: .value("100", 100) ) .foregroundStyle(Theme.Colors.green.opacity(0.15)) RectangleMark( yStart: .value(String(secondLevelAlertBand), secondLevelAlertBand), yEnd: .value(String(firstLevelAlertBand), firstLevelAlertBand) ) .foregroundStyle(Theme.Colors.orange.opacity(0.15)) RectangleMark( yStart: .value("0", 0), yEnd: .value(String(secondLevelAlertBand), secondLevelAlertBand) ) .foregroundStyle(Theme.Colors.red.opacity(0.15)) ForEach(telemetryData, id: \.timestamp) { entry in if let utcDate = dateFormatter.date(from: entry.timestamp) { let localDate = convertToUserTimeZone(date: utcDate) let tankLevel = entry.tankLevel ?? 0 LineMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) .foregroundStyle(statusColor) AreaMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) .foregroundStyle(statusColor.opacity(0.50)) PointMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) .foregroundStyle(selectedDataPoint?.date == localDate ? Theme.Colors.primaryColor : statusColor) .symbolSize(selectedDataPoint?.date == localDate ? 120 : 80) PointMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) //.foregroundStyle(.white).symbolSize(10) .foregroundStyle(Theme.Colors.white(colorScheme: colorScheme)) .symbolSize(12) } } } .chartXScale(domain: (firstTimestamp ?? Date())...(latestTimestamp ?? Date())) .chartXVisibleDomain(length: visibleDomainSize) .chartScrollableAxes(.horizontal) .chartScrollPosition(x: $chartScrollPositionX) .chartXAxis { AxisMarks(values: .stride(by: xAxisStrideUnit, count: xAxisCount())) { value in if let utcDate = value.as(Date.self) { let localDate = convertToUserTimeZone(date: utcDate) let formatStyle = self.getFormatStyle(for: interval) AxisValueLabel { Text(localDate, format: formatStyle) .font(Theme.Fonts.poppinsRegularExtraSmall) .foregroundStyle(Theme.Colors.black(colorScheme: colorScheme)) } AxisTick() .foregroundStyle(Theme.Colors.black(colorScheme: colorScheme).opacity(1)) } } } .chartOverlay { proxy in GeometryReader { geometry in Rectangle().fill(Color.clear).contentShape(Rectangle()) .onTapGesture { location in let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x // Use proxy to get the x-axis value at the tapped position if let selectedDate: Date = proxy.value(atX: xPosition) { if let closestEntry = telemetryData.min(by: { abs(dateFormatter.date(from: $0.timestamp)!.timeIntervalSince1970 - selectedDate.timeIntervalSince1970) < abs(dateFormatter.date(from: $1.timestamp)!.timeIntervalSince1970 - selectedDate.timeIntervalSince1970) }) { selectedDataPoint = (convertToUserTimeZone(date: dateFormatter.date(from: closestEntry.timestamp)!), closestEntry.tankLevel ?? 0) if let dateXPos = proxy.position(forX: convertToUserTimeZone(date: dateFormatter.date(from: closestEntry.timestamp)!)), let tankLevelYPos = proxy.position(forY: closestEntry.tankLevel ?? 0) { // Offset the x-position based on the scroll position let adjustedXPos = dateXPos - proxy.position(forX: chartScrollPositionX)! withAnimation(.spring()) { selectedPointLocation = CGPoint(x: adjustedXPos, y: tankLevelYPos - 60) // Offset popup above the point showPopup = true } } } } } } .onChange(of: chartScrollPositionX) { newValue in // Dynamically update the popup position when scroll changes if let selectedDataPoint = selectedDataPoint { if let dateXPos = proxy.position(forX: selectedDataPoint.date) { let adjustedXPos = dateXPos - proxy.position(forX: chartScrollPositionX)! selectedPointLocation.x = adjustedXPos } } } } } Please help! Nick
1
1
805
Apr ’25
Display Calculated Data in SwiftUI Charts ...
I am trying to discover how to display my application’s calculated Solar Information values in a chart. My application identifies a selected location in MapKit. The application identifies the location’s longitude, latitude, and current time of day. The application calculates the selected location’s NOAA [SOLAR ELEVATION], and the [SOLAR AZIMUTH] for the time of day. The application calculates the data, then stores the calculated values as a [Plist] file within my application’s Document Directory. For the moment, complete with repeated scouring of the Internet, I am not sure how to properly convert, transfer, or create a Structure, required by the chart to display the calculated values. I would like to create the chart once the calculations are complete, but I introduced a Plist to store the calculations for future use, too. The calculated values coincide with the NOAA Solar Calculations, complete to the displayed [h : m : s], whereas I also designed the application to create the [Array of Dictionary Objects] to store the calculated values for each subsequent six minute interval, until the end of the selected location’s day. The calculated values are properly appended to the [Array of Dictionary Objects] after each completed calculation, with data transfer constants. There are 240 calculations per day from [00:06:00 to 23:54:00], presented as a [STRING], complete with the [Elevation] presented as a [DOUBLE]. For example :: The application generates the following [Calculated Array of Dictionary Objects], then recreates, and appends a new Plist in the Document Directory. mySolarElevationDataArrayOfDictionaries :: [(theRequiredTimeOfDay: "00:06:00", theCalculatedElevation: -62.60301082991259), (theRequiredTimeOfDay: "00:12:00", theCalculatedElevation: -62.94818095051292), (theRequiredTimeOfDay: "00:18:00", theCalculatedElevation: -63.245198186807215), (theRequiredTimeOfDay: "00:24:00", theCalculatedElevation: -63.49236786176319), (theRequiredTimeOfDay: "00:30:00", theCalculatedElevation: -63.688223890934175), (theRequiredTimeOfDay: "00:36:00", theCalculatedElevation: -63.831564163806945), (theRequiredTimeOfDay: "00:42:00", theCalculatedElevation: -63.921486675739004), (theRequiredTimeOfDay: "00:48:00", theCalculatedElevation: -63.95741610687708), to the end of the data :: ===&gt; (theRequiredTimeOfDay: "23:54:00", theCalculatedElevation: -60.69355458181633)] The application presents the initial data as follows :: Then presents a compass view to illustrate the results :: I modified the Chart’s [MOCK DATA] from the calculated values to test the Chart’s display in a [SwiftUI Hosting Controller]. For example :: The following Chart Mock Data in a [HourlySunElevation_MockChartData.swift] file is called by the application’s [Content View]. import Foundation struct Value { let theRequiredTimeOfDay: String let theCalculatedElevation: Double static func theSunElevationMockData() -&gt; [Value] { return [Value(theRequiredTimeOfDay: "00:06:00", theCalculatedElevation: -62.60301082991259), Value(theRequiredTimeOfDay: "00:12:00", theCalculatedElevation: -62.94818095051292), Value(theRequiredTimeOfDay: "00:18:00", theCalculatedElevation: -63.245198186807215), Value(theRequiredTimeOfDay: "00:24:00", theCalculatedElevation: -63.49236786176319), Value(theRequiredTimeOfDay: "00:30:00", theCalculatedElevation: -63.688223890934175), Value(theRequiredTimeOfDay: "00:36:00", theCalculatedElevation: -63.831564163806945), Value(theRequiredTimeOfDay: "00:42:00", theCalculatedElevation: -63.921486675739004), Value(theRequiredTimeOfDay: "00:48:00", theCalculatedElevation: -63.95741610687708), to the end of the data :: ===&gt; Value(theRequiredTimeOfDay: "23:54:00", theCalculatedElevation: -60.69355458181633)] The Chart illustrates the Mock Data as follows :: I also created a Struct within the [MySunElevationChart_ViewController] to try to append the calculated data, using the same logic with the Plist data transfer constants, as employed by the [Array of Dictionary Objects] :: struct ChartSolarElevationValues { var theRequiredTimeOfDay: String var theCalculatedElevation: Double // Structs have an implicit [init]. This is here for reference. init(theRequiredTimeOfDay: String, theCalculatedElevation: Double) { self.theRequiredTimeOfDay = theRequiredTimeOfDay self.theCalculatedElevation = theCalculatedElevation //mySolarElevationChartData.append(self) } // End of [init(theRequiredTimeOfDay: String, theCalculatedElevation: Double)] } // End of [struct ChartSolarElevationValues] Unfortunately, the result did not append each subsequent calculation, but continued to create the same calculation as a new distinct object :: NOTE :: I only called three calculations with the Struct test. // NOTE :: To prevent an [ERROR] at [var mySolarElevationChartData = [ChartSolarElevationValues]] since it has an init. // Therefore you must add () at the end of [var mySolarElevationChartData = [ChartSolarElevationValues]] let theData = [ChartSolarElevationValues]() //print("theData :: \(theData)\n") let someData = ChartSolarElevationValues(theRequiredTimeOfDay: TheTimeForDaySunElevation.theTheTimeForDaySunElevation, theCalculatedElevation:VerifyCityLocationSearchRequestCorrectedSolarElevation.theVerifyCityLocationSearchRequestCorrectedSolarElevation) var theData_New = theData theData_New.append(someData) print("theData_New :: \(theData_New)\n") // Prints :: theData_New :: [My_Map.ChartSolarElevationValues(theRequiredTimeOfDay: "00:06:00", theCalculatedElevation: -61.11000735370401)]] // Prints :: [theData_New :: [My_Map.ChartSolarElevationValues(theRequiredTimeOfDay: "00:12:00", theCalculatedElevation: -61.315092082911875)]] // Prints :: [theData_New :: [My_Map.ChartSolarElevationValues(theRequiredTimeOfDay: "00:18:00", theCalculatedElevation: -61.47403413313205)]] So, I am misintepreting the required coding structure to properly append the Elevation Chart, and the Azimuth Chart with the calculated data. I know something is amiss, but for the moment, I do not know how to address this issue. Your suggestions would be welcome ... :] jim_k
3
1
879
Mar ’25
How to reverse a custom Domain in Swift Charts?
I'm currently building an app in SwiftUI that needs to show some charts. Swift Charts has been quite helpful but I can't seem to set the domain of the chart just how I want it. The following chart display events that happend in a week and I want to display every hour even if nothing happened there, and I want the hours to go up, instead of the default where the new day start at the top of the chart. struct ChartView: View { let dataSets: [someData] // simplified date init let datemin = date(year:2025, month: 2, day: 24) let datemax = date(year:2025, month: 2, day: 25) var body: some View { Chart(dataset) { data in PointMark(x: .value("Day", data.day, unit: .weekday), y: .value("Time", data.time, unit: .minute)) } } // The focus is on this line .chartYScale(domain: .automatic(reversed: true)) } } The .chartYScale(domain:) modifier allows me to set the start of the day at the bottom, but still ignores some hours. If I instead use this Chart(...) { ... } .chartYScale(domain: datemin...datemax) The chart display every hour of the day, but now the start of the day is at the bottom. I can't seem to find a way to get both things at the same time. if I add both modifiers only the first one get applied while the other ignored. Any solutions or workarounds?
2
0
473
Mar ’25
SwiftUI Gestures: Sequenced Long Press and Drag
In creating a sequenced gesture combining a LongPressGesture and a DragGesture, I found that the combined gesture exhibits two problems: The @GestureState does not properly update as the gesture progresses through its phases. Specifically, the updating(_:body:) closure (documented here) is only ever executed during the drag interaction. Long presses and drag-releases do not call the updating(_:body:) closure. Upon completing the long press gesture and activating the drag gesture, the drag gesture remains empty until the finger or cursor has moved. The expected behavior is for the drag gesture to begin even when its translation is of size .zero. This second problem – the nonexistence of a drag gesture once the long press has completed – prevents access to the location of the long-press-then-drag. Access to this location is critical for displaying to the user that the drag interaction has commenced. The below code is based on Apple's example presented here. I've highlighted the failure points in the code with // *. My questions are as follows: What is required to properly update the gesture state? Is it possible to have a viable drag gesture immediately upon fulfilling the long press gesture, even with a translation of .zero? Alternatively to the above question, is there a way to gain access to the location of the long press gesture? import SwiftUI import Charts enum DragState { case inactive case pressing case dragging(translation: CGSize) var isDragging: Bool { switch self { case .inactive, .pressing: return false case .dragging: return true } } } struct ChartGestureOverlay<Value: Comparable & Hashable>: View { @Binding var highlightedValue: Value? let chartProxy: ChartProxy let valueFromChartProxy: (CGFloat, ChartProxy) -> Value? let onDragChange: (DragState) -> Void @GestureState private var dragState = DragState.inactive var body: some View { Rectangle() .fill(Color.clear) .contentShape(Rectangle()) .onTapGesture { location in if let newValue = valueFromChartProxy(location.x, chartProxy) { highlightedValue = newValue } } .gesture(longPressAndDrag) } private var longPressAndDrag: some Gesture { let longPress = LongPressGesture(minimumDuration: 0.2) let drag = DragGesture(minimumDistance: .zero) .onChanged { value in if let newValue = valueFromChartProxy(value.location.x, chartProxy) { highlightedValue = newValue } } return longPress.sequenced(before: drag) .updating($dragState) { value, gestureState, _ in switch value { case .first(true): // * This is never called gestureState = .pressing case .second(true, let drag): // * Drag is often nil // * When drag is nil, we lack access to the location gestureState = .dragging(translation: drag?.translation ?? .zero) default: // * This is never called gestureState = .inactive } onDragChange(gestureState) } } } struct DataPoint: Identifiable { let id = UUID() let category: String let value: Double } struct ContentView: View { let dataPoints = [ DataPoint(category: "A", value: 5), DataPoint(category: "B", value: 3), DataPoint(category: "C", value: 8), DataPoint(category: "D", value: 2), DataPoint(category: "E", value: 7) ] @State private var highlightedCategory: String? = nil @State private var dragState = DragState.inactive var body: some View { VStack { Text("Bar Chart with Gesture Interaction") .font(.headline) .padding() Chart { ForEach(dataPoints) { dataPoint in BarMark( x: .value("Category", dataPoint.category), y: .value("Value", dataPoint.value) ) .foregroundStyle(highlightedCategory == dataPoint.category ? Color.red : Color.gray) .annotation(position: .top) { if highlightedCategory == dataPoint.category { Text("\(dataPoint.value, specifier: "%.1f")") .font(.caption) .foregroundColor(.primary) } } } } .frame(height: 300) .chartOverlay { chartProxy in ChartGestureOverlay<String>( highlightedValue: $highlightedCategory, chartProxy: chartProxy, valueFromChartProxy: { xPosition, chartProxy in if let category: String = chartProxy.value(atX: xPosition) { return category } return nil }, onDragChange: { newDragState in dragState = newDragState } ) } .onChange(of: highlightedCategory, { oldCategory, newCategory in }) } .padding() } } #Preview { ContentView() } Thank you!
7
1
1.7k
Mar ’25
Swiftui Charts
I have a SwiftUI LineMark chart that inverts the y axis when the data the chart is plotting is all zeros. I'm expecting the y axis 0 to be at the bottom but when the data is all zeros it's at the top. Below is an example demonstrating the problem: import SwiftUI import Charts struct ChartView: View { let data: [Double] = [0,0,0,0,0] var body: some View { Chart { ForEach(data.indices, id: \.self) { index in LineMark( x: .value("Index", index), y: .value("Value", data[index]) ) } } .chartYAxis { AxisMarks(values: [0, 10, 20, 30, 40, 50]) { value in AxisValueLabel() AxisTick() AxisGridLine() } } .padding() } } I can't use .chartYScale(domain: ) because it overrides the chartYAxis where the real code creates custom a leading and trailing y axis. Does anyone have any suggestions how I may fix this?
5
0
649
Feb ’25
Removing Grid Lines from Charts in SwiftUI
Hi, I have created a line graph using LineMark in Charts, which by default includes grid lines and axes lines. My requirement is to remove the grid lines but retain the axes lines and the values. I have tried the following code: .chartXAxis { AxisMarks(preset: .extended, values: .stride(by: 2), stroke: StrokeStyle(lineWidth: 0)) } This is removing grid lines as well as axes lines. How to retain axes lines while removing grid lines ?
2
0
399
Feb ’25
Swift charts displaying wrong theme through UIHostingController
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.
1
0
1.3k
Feb ’25
Affiliate View Struct is probably hidden by Charts on iOS app
The problem is: As per screenshot below, one can only see the lineChart. I have another struct AffiliateView coded under this Chart: import SnapKit import Charts import DGCharts class AffiliateViewController: UIViewController { private lazy var chartView: LineChartView = { let chart = LineChartView() chart.noDataText = "No data available." chart.chartDescription.enabled = false chart.xAxis.labelPosition = .bottom chart.rightAxis.enabled = false chart.legend.enabled = true chart.backgroundColor = .lightGray // For debugging visibility return chart }() private lazy var containerView: UIView = { let view = UIView() view.backgroundColor = .white return view }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white // Add container view and chart view to the main view view.addSubview(containerView) view.addSubview(chartView) // Add SwiftUI View inside the container view let affiliateView = AffiliateView() let hostingController = UIHostingController(rootView: affiliateView) addChild(hostingController) containerView.addSubview(hostingController.view) hostingController.view.frame = containerView.bounds hostingController.didMove(toParent: self) layout() setupChartData() } private func layout() { // Layout the container view (SwiftUI content) containerView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide.snp.top) make.left.right.equalToSuperview() make.height.equalTo(350) // Increase the height for the SwiftUI content } // Layout the chart view below the container view chartView.snp.makeConstraints { make in make.top.equalTo(containerView.snp.bottom).offset(20) // Space between chart and the affiliate content make.left.equalToSuperview().offset(20) make.right.equalToSuperview().offset(-20) make.height.equalTo(200) // Set a fixed height for the chart } } private func setupChartData() { let dataEntries = [ ChartDataEntry(x: 1, y: 10), ChartDataEntry(x: 2, y: 20), ChartDataEntry(x: 3, y: 15), ChartDataEntry(x: 4, y: 30), ChartDataEntry(x: 5, y: 25) ] let dataSet = LineChartDataSet(entries: dataEntries, label: "Clicks per Day") dataSet.colors = [.blue] dataSet.valueColors = [.black] dataSet.circleColors = [.red] dataSet.circleRadius = 4.0 let data = LineChartData(dataSet: dataSet) chartView.data = data chartView.notifyDataSetChanged() } } // SwiftUI View remains in the same file struct AffiliateView: View { @State private var customMessage: String = "" @State private var uniqueLink: String = "Your unique link will appear here." @State private var clickData: [Double] = [10, 20, 15, 30, 25] // Example data var body: some View { NavigationView { VStack(spacing: 20) { // TextField for custom message input TextField("Enter your custom message...", text: $customMessage) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.horizontal) // Generate Link Button Button(action: generateLink) { Text("Generate Sign-Up Link") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: 50) .background(Color.red) .cornerRadius(10) } .padding(.horizontal) // Generated Link Label Text(uniqueLink) .font(.body) .multilineTextAlignment(.center) .padding(.horizontal) // You can add a chart here if you want to show it in SwiftUI too /* LineChartView(data: clickData, title: "Clicks per Day", legend: "Daily Clicks") */ } .navigationTitle("Affiliate Marketing") .navigationBarTitleDisplayMode(.inline) } } private func generateLink() { let encodedMessage = customMessage.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" uniqueLink = "https://affiliate.example.com/referral?message=\(encodedMessage)" addClickData() } private func addClickData() { clickData.append(Double.random(in: 0...100)) } } As you see, the AffiliateView has been declared outside of Controller View class. The View content was visible before the lineChart was added into this code. Now the View content is not visible anymore. I have tried to increment/decrement values at make.height.equalTo() but to no avail. Could anyone kindly point me in the right direction?
0
0
376
Jan ’25
How to Visualize Data in a DataFrame Using Charts
I want to visualize the data stored in a DataFrame using various charts (barmark, sectormark, linemark, etc.). My questions are as follows: Can a DataFrame be used directly within a chart? If so, could you provide a simple example? If it cannot be used directly, what is the correct way to use it? Could you provide an example? Thank you for your help. Best regards.
0
1
436
Jan ’25
TabView and Swift Charts giving inconsistent behaviour when swiping between pages
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()]) } } }
10
0
1.9k
Jan ’25
How to read a value when tapping a point on a Chart3D
Hi. Chart3D seems to be perfect to visualize a chart on one of my apps. However I would need a way to read the point of a SurfacePlot of a Chart3D that was touched by the user? Something like a Chart .chartOverlay that has a ChartProxy that can convert screen coordinates of the chart in values, but in 3D (maybe using hit testing ). Thanks and best regards. João Colaço
Replies
2
Boosts
0
Views
193
Activity
Jun ’25
.chartScrollableAxes(.horizontal) prevents chartYAxisLabel from being postioned top right, over Y axis
Adding .chartScrollableAxes(.horizontal) to a chart prevents chartYAxisLabel from being positioned top right, over Y axis. We want the label at top right, over the Y-axis, but with chartScrollableAxes it is always top right relative to the initial chartXVisibleDomain, which puts it in the middle of the chart if chartXVisibleDomain < full x domain. import SwiftUI import Charts struct ContentView: View { @State private var numbers = (0..<100) .map { _ in Double.random(in: 0...100) } @State var visibleDomain : Int = 50 var body: some View { Chart(Array(zip(numbers.indices, numbers)), id: \.1) { index, number in LineMark( x: .value("Index", index), y: .value("Number", number) ) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: visibleDomain) .chartScrollPosition(initialX: 70) .chartYAxisLabel(position: .topTrailing, alignment: .center) { /* We want the label at top right, over the Y-axis, but with chartScrollableAxes it is always top right relative to the initial chartXVisibleDomain, which puts it in the middle of the chart if chartXVisibleDomain < full x domain */ Text("units") .foregroundStyle(.red) .fontWeight(.bold) } .padding() } } #Preview { ContentView() }
Replies
1
Boosts
0
Views
407
Activity
Jun ’25
How to have different colors in Charts with AreaMark
I would like to have different fill colors in my chart. What I want to achieve is that if the values drop below 0 the fill color should be red. If they are above the fill color should be red. My code looks as follows: import SwiftUI import Charts struct DataPoint: Identifiable {     let id: UUID = UUID()     let x: Int     let y: Int } struct AlternatingChartView: View {          enum Gradients {         static let greenGradient = LinearGradient(gradient: Gradient(colors: [.green, .white]), startPoint: .top, endPoint: .bottom)         static let blueGradient = LinearGradient(gradient: Gradient(colors: [.white, .blue]), startPoint: .top, endPoint: .bottom)     }          let data: [DataPoint] = [         DataPoint(x: 1, y: 10),         DataPoint(x: 2, y: -5),         DataPoint(x: 3, y: 20),         DataPoint(x: 4, y: -8),         DataPoint(x: 5, y: 15),     ]               var body: some View {         Chart {             ForEach(data) { data in                 AreaMark(                     x: .value("Data Point", data.x),                     y: .value("amount", data.y))                 .interpolationMethod(.catmullRom)                 .foregroundStyle(data.y < 0 ? Color.red : Color.green)                                  LineMark(                 x: .value("Data Point", data.x),                 y: .value("amount", data.y))                 .interpolationMethod(.catmullRom)                 .foregroundStyle(Color.black)                 .lineStyle(StrokeStyle.init(lineWidth: 4))                              }         }         .frame(height: 200)     } } #Preview {     AlternatingChartView() } The result looks like this: I also tried using foregroundStyle(by:) and chartForegroundStyleScale(_:) but the result was, that two separate areas had been drawn. One for the below and one for the above zero datapoints. So, what would be the right approach to have two different fill colors?
Replies
0
Boosts
0
Views
272
Activity
Jun ’25
AreaMark Always alignsMarkStylesWithPlotArea for linear gradients
I'm trying to make a Swift Chart where 24 AreaMarks an hour apart on X axis over a day display a vertical gradient. The gradient is vertical and is essentially [Color.opacity(0.1),Colour,Color.opacity(0.1] The idea here is where the upper and lower points of each AreaMark are the same or close to each other in the Y axis, the chart essentially displays a line, where they are far apart you get a nice fading vertical gradient. However, it seems that the .alignsMarkStylesWithPlotArea modifier is always set for AreaMarks even if manually applying it false. Investigating further, I've learnt that with AreaMarks in a series, Swift Charts seems to only listen to the first foreground style set in. I've created some sample code to demonstrate this. struct DemoChartView: View { var body: some View { Chart { AreaMark(x: .value("Time", Date().addingTimeInterval(0)), yStart: .value("1", 40), yEnd: .value("2", 60)) .foregroundStyle(LinearGradient(colors: [.pink, .teal], startPoint: .top, endPoint: .bottom)) .alignsMarkStylesWithPlotArea(false) AreaMark(x: .value("Time", Date().addingTimeInterval(3600)), yStart: .value("1", 44), yEnd: .value("2", 58)) .foregroundStyle(LinearGradient(colors: [.orange, .yellow], startPoint: .top, endPoint: .bottom)) .alignsMarkStylesWithPlotArea(false) AreaMark(x: .value("Time", Date().addingTimeInterval(03600*2)), yStart: .value("1", 50), yEnd: .value("2", 90)) .foregroundStyle(LinearGradient(colors: [.green, .blue], startPoint: .top, endPoint: .bottom)) .alignsMarkStylesWithPlotArea(false) } } } Which produces this: So here, all the different .foregroundStyle LinearGradients are being ignored AND the .alignsMarkStylesWithPlotArea(false) is also ignored - the amount of pink on the first mark is different to the second and third 🤷‍♂️ Has anyone encountered this. Are AreaMarks the correct choice or are they just not setup to create this type of data display. Thanks
Replies
3
Boosts
0
Views
1.2k
Activity
Jun ’25
Swift Charts: How to prevent scroll position jump when loading more data dynamically
I'm implementing infinite scrolling with Swift Charts where additional historical data loads when scrolling near the beginning of the dataset. However, when new data is loaded, the chart's scroll position jumps unexpectedly. Current behavior: Initially loads 10 data points, displaying the latest 5 When scrolling backwards with only 3 points remaining off-screen, triggers loading of 10 more historical points After loading, the scroll position jumps to the 3rd position of the new dataset instead of maintaining the current view Expected behavior: Scroll position should remain stable when new data is loaded User's current view should not change during data loading Here's my implementation logic using some mock data: import SwiftUI import Charts struct DataPoint: Identifiable { let id = UUID() let date: Date let value: Double } class ChartViewModel: ObservableObject { @Published var dataPoints: [DataPoint] = [] private var isLoading = false init() { loadMoreData() } func loadMoreData() { guard !isLoading else { return } isLoading = true let newData = self.generateDataPoints( endDate: self.dataPoints.first?.date ?? Date(), count: 10 ) self.dataPoints.insert(contentsOf: newData, at: 0) self.isLoading = false print("\(dataPoints.count) data points.") } private func generateDataPoints(endDate: Date, count: Int) -> [DataPoint] { var points: [DataPoint] = [] let calendar = Calendar.current for i in 0..<count { let date = calendar.date( byAdding: .day, value: -i, to: endDate ) ?? endDate let value = Double.random(in: 0...100) points.append(DataPoint(date: date, value: value)) } return points.sorted { $0.date < $1.date } } } struct ScrollableChart: View { @StateObject private var viewModel = ChartViewModel() @State private var scrollPosition: Date @State private var scrollDebounceTask: Task<Void, Never>? init() { self.scrollPosition = .now.addingTimeInterval(-4*24*3600) } var body: some View { Chart(viewModel.dataPoints) { point in BarMark( x: .value("Time", point.date, unit: .day), y: .value("Value", point.value) ) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 5 * 24 * 3600) .chartScrollPosition(x: $scrollPosition) .chartXScale(domain: .automatic(includesZero: false)) .frame(height: 300) .onChange(of: scrollPosition) { oldPosition, newPosition in scrollDebounceTask?.cancel() scrollDebounceTask = Task { try? await Task.sleep(for: .milliseconds(300)) if !Task.isCancelled { checkAndLoadMoreData(currentPosition: newPosition) } } } } private func checkAndLoadMoreData(currentPosition: Date?) { guard let currentPosition, let earliestDataPoint = viewModel.dataPoints.first?.date else { return } let timeInterval = currentPosition.timeIntervalSince(earliestDataPoint) if timeInterval <= 3 * 24 * 3600 { viewModel.loadMoreData() } } } I attempted to compensate for this jump by adding: scrollPosition = scrollPosition.addingTimeInterval(10 * 24 * 3600) after viewModel.loadMoreData(). However, this caused the chart to jump in the opposite direction by 10 days, rather than maintaining the current position. What's the problem with my code and how to fix it?
Replies
6
Boosts
0
Views
691
Activity
Jun ’25
Using multiple Line Styles on a single Line Mark
Hello, I am wondering if it is possible to have a Line Mark with different line styles. I am trying to create a Line Mark where part of the line is solid and another part of the line is dashed. Even with a conditional it only displays one or the other. Is it currently possible in SwiftCharts to do something like the attached image? Thank you.
Replies
1
Boosts
1
Views
274
Activity
Jun ’25
How to display numbers as grid marks along axes for plotting in Swift Charts?
If you try to add a graph for a function in Apple Notes you can see that numbers marking coordinates are positioned along the axes (see screenshot 1). But when I am making my own plot view with Swift Charts I don't see that option. Marks for X axis are positioned at the bottom, and marks for Y axis are positioned to the right. I don't see an API that can configure them to be shown along the axes. Is there something that I am missing? Or is Apple just using some private API for that? I could make a custom overlay to display these marks, but then I will have to adjust them while zooming myself, which can be problematic.
Replies
1
Boosts
0
Views
339
Activity
Jun ’25
chartXAxis AxisMark consisting of Month:Year
I have a Chart displaying Counts per Date over 2-3 years. I'd like to have the XAxis mark consist of MM-yy or even MM/nyy. Is this possible? OK, just saw AxisValueLabel(format: .dateTime.month().year()) which gives e.g. Mar 2024. This is good. Better might be Mar 24 (maybe a Y3K problem ;-) or even better Mar. OR Mar 24. 2024 Or best increment the year when it changes. Are any of these alternate formats possible? Thanks, David PS, my current .chartXAxis code .chartXAxis { AxisMarks(values: .stride(by: .month, count: 3)) { value in if value.as(Date.self) != nil { AxisValueLabel(format: .dateTime.month().year()) AxisGridLine() AxisTick() } } }
Replies
1
Boosts
0
Views
252
Activity
Jun ’25
How to get the date range of the data that is currently displayed in a Apple Chart?
I have a Swiftui app, and I'm using the Apple Chart framework to chart user transactions over time. I'd like to be able to show an annotation at the top of the chart that shows the date range of the records currently visible on the chart. Much like Apple does in their Heath App charts. But I just can't seem to find a method to read the current date range displayed on the chart once the user has scrolled though the chart. Has anyone done anything similar, or maybe seen some sample code? So far I'm striking out on this... Thanks in advance
Replies
1
Boosts
0
Views
287
Activity
May ’25
Issue: Chart Scroll Not Working in iOS 18 with Chart Overlay Tap Gesture
Summary I have a SwiftUI Chart that worked correctly in iOS 17, allowing both horizontal scrolling and tap gesture selection. However, in iOS 18, the same exact chart will not allow for both tap gestures and scrolling to work -- it's either we allow scrolling or we allow tap gestures but not both. We have tried everything to try to circumvent this issue but have had to resort to old methods of creating the chart. This is an issue that has negatively impacted our customers as well. Again, the charts were working fine on iOS 17, but on iOS 18 the chart scroll + tap gesture capability is not working. Expected Behavior (iOS 17) Users can scroll horizontally through the chart. Users can tap on data points to highlight them. The selected data point updates when tapped. Observed Behavior (iOS 18) The chart no longer scrolls when chartOverlay with the Tap Gesture is applied. Tap selection still works as expected. Code Snippet Below is the working implementation from iOS 17: private var iOS17ChartView: some View { Chart { RectangleMark( yStart: .value(String(firstLevelAlertBand), firstLevelAlertBand), yEnd: .value("100", 100) ) .foregroundStyle(Theme.Colors.green.opacity(0.15)) RectangleMark( yStart: .value(String(secondLevelAlertBand), secondLevelAlertBand), yEnd: .value(String(firstLevelAlertBand), firstLevelAlertBand) ) .foregroundStyle(Theme.Colors.orange.opacity(0.15)) RectangleMark( yStart: .value("0", 0), yEnd: .value(String(secondLevelAlertBand), secondLevelAlertBand) ) .foregroundStyle(Theme.Colors.red.opacity(0.15)) ForEach(telemetryData, id: \.timestamp) { entry in if let utcDate = dateFormatter.date(from: entry.timestamp) { let localDate = convertToUserTimeZone(date: utcDate) let tankLevel = entry.tankLevel ?? 0 LineMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) .foregroundStyle(statusColor) AreaMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) .foregroundStyle(statusColor.opacity(0.50)) PointMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) .foregroundStyle(selectedDataPoint?.date == localDate ? Theme.Colors.primaryColor : statusColor) .symbolSize(selectedDataPoint?.date == localDate ? 120 : 80) PointMark( x: .value("Date", localDate), y: .value("Tank Level", tankLevel) ) //.foregroundStyle(.white).symbolSize(10) .foregroundStyle(Theme.Colors.white(colorScheme: colorScheme)) .symbolSize(12) } } } .chartXScale(domain: (firstTimestamp ?? Date())...(latestTimestamp ?? Date())) .chartXVisibleDomain(length: visibleDomainSize) .chartScrollableAxes(.horizontal) .chartScrollPosition(x: $chartScrollPositionX) .chartXAxis { AxisMarks(values: .stride(by: xAxisStrideUnit, count: xAxisCount())) { value in if let utcDate = value.as(Date.self) { let localDate = convertToUserTimeZone(date: utcDate) let formatStyle = self.getFormatStyle(for: interval) AxisValueLabel { Text(localDate, format: formatStyle) .font(Theme.Fonts.poppinsRegularExtraSmall) .foregroundStyle(Theme.Colors.black(colorScheme: colorScheme)) } AxisTick() .foregroundStyle(Theme.Colors.black(colorScheme: colorScheme).opacity(1)) } } } .chartOverlay { proxy in GeometryReader { geometry in Rectangle().fill(Color.clear).contentShape(Rectangle()) .onTapGesture { location in let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x // Use proxy to get the x-axis value at the tapped position if let selectedDate: Date = proxy.value(atX: xPosition) { if let closestEntry = telemetryData.min(by: { abs(dateFormatter.date(from: $0.timestamp)!.timeIntervalSince1970 - selectedDate.timeIntervalSince1970) < abs(dateFormatter.date(from: $1.timestamp)!.timeIntervalSince1970 - selectedDate.timeIntervalSince1970) }) { selectedDataPoint = (convertToUserTimeZone(date: dateFormatter.date(from: closestEntry.timestamp)!), closestEntry.tankLevel ?? 0) if let dateXPos = proxy.position(forX: convertToUserTimeZone(date: dateFormatter.date(from: closestEntry.timestamp)!)), let tankLevelYPos = proxy.position(forY: closestEntry.tankLevel ?? 0) { // Offset the x-position based on the scroll position let adjustedXPos = dateXPos - proxy.position(forX: chartScrollPositionX)! withAnimation(.spring()) { selectedPointLocation = CGPoint(x: adjustedXPos, y: tankLevelYPos - 60) // Offset popup above the point showPopup = true } } } } } } .onChange(of: chartScrollPositionX) { newValue in // Dynamically update the popup position when scroll changes if let selectedDataPoint = selectedDataPoint { if let dateXPos = proxy.position(forX: selectedDataPoint.date) { let adjustedXPos = dateXPos - proxy.position(forX: chartScrollPositionX)! selectedPointLocation.x = adjustedXPos } } } } } Please help! Nick
Replies
1
Boosts
1
Views
805
Activity
Apr ’25
Display Calculated Data in SwiftUI Charts ...
I am trying to discover how to display my application’s calculated Solar Information values in a chart. My application identifies a selected location in MapKit. The application identifies the location’s longitude, latitude, and current time of day. The application calculates the selected location’s NOAA [SOLAR ELEVATION], and the [SOLAR AZIMUTH] for the time of day. The application calculates the data, then stores the calculated values as a [Plist] file within my application’s Document Directory. For the moment, complete with repeated scouring of the Internet, I am not sure how to properly convert, transfer, or create a Structure, required by the chart to display the calculated values. I would like to create the chart once the calculations are complete, but I introduced a Plist to store the calculations for future use, too. The calculated values coincide with the NOAA Solar Calculations, complete to the displayed [h : m : s], whereas I also designed the application to create the [Array of Dictionary Objects] to store the calculated values for each subsequent six minute interval, until the end of the selected location’s day. The calculated values are properly appended to the [Array of Dictionary Objects] after each completed calculation, with data transfer constants. There are 240 calculations per day from [00:06:00 to 23:54:00], presented as a [STRING], complete with the [Elevation] presented as a [DOUBLE]. For example :: The application generates the following [Calculated Array of Dictionary Objects], then recreates, and appends a new Plist in the Document Directory. mySolarElevationDataArrayOfDictionaries :: [(theRequiredTimeOfDay: "00:06:00", theCalculatedElevation: -62.60301082991259), (theRequiredTimeOfDay: "00:12:00", theCalculatedElevation: -62.94818095051292), (theRequiredTimeOfDay: "00:18:00", theCalculatedElevation: -63.245198186807215), (theRequiredTimeOfDay: "00:24:00", theCalculatedElevation: -63.49236786176319), (theRequiredTimeOfDay: "00:30:00", theCalculatedElevation: -63.688223890934175), (theRequiredTimeOfDay: "00:36:00", theCalculatedElevation: -63.831564163806945), (theRequiredTimeOfDay: "00:42:00", theCalculatedElevation: -63.921486675739004), (theRequiredTimeOfDay: "00:48:00", theCalculatedElevation: -63.95741610687708), to the end of the data :: ===&gt; (theRequiredTimeOfDay: "23:54:00", theCalculatedElevation: -60.69355458181633)] The application presents the initial data as follows :: Then presents a compass view to illustrate the results :: I modified the Chart’s [MOCK DATA] from the calculated values to test the Chart’s display in a [SwiftUI Hosting Controller]. For example :: The following Chart Mock Data in a [HourlySunElevation_MockChartData.swift] file is called by the application’s [Content View]. import Foundation struct Value { let theRequiredTimeOfDay: String let theCalculatedElevation: Double static func theSunElevationMockData() -&gt; [Value] { return [Value(theRequiredTimeOfDay: "00:06:00", theCalculatedElevation: -62.60301082991259), Value(theRequiredTimeOfDay: "00:12:00", theCalculatedElevation: -62.94818095051292), Value(theRequiredTimeOfDay: "00:18:00", theCalculatedElevation: -63.245198186807215), Value(theRequiredTimeOfDay: "00:24:00", theCalculatedElevation: -63.49236786176319), Value(theRequiredTimeOfDay: "00:30:00", theCalculatedElevation: -63.688223890934175), Value(theRequiredTimeOfDay: "00:36:00", theCalculatedElevation: -63.831564163806945), Value(theRequiredTimeOfDay: "00:42:00", theCalculatedElevation: -63.921486675739004), Value(theRequiredTimeOfDay: "00:48:00", theCalculatedElevation: -63.95741610687708), to the end of the data :: ===&gt; Value(theRequiredTimeOfDay: "23:54:00", theCalculatedElevation: -60.69355458181633)] The Chart illustrates the Mock Data as follows :: I also created a Struct within the [MySunElevationChart_ViewController] to try to append the calculated data, using the same logic with the Plist data transfer constants, as employed by the [Array of Dictionary Objects] :: struct ChartSolarElevationValues { var theRequiredTimeOfDay: String var theCalculatedElevation: Double // Structs have an implicit [init]. This is here for reference. init(theRequiredTimeOfDay: String, theCalculatedElevation: Double) { self.theRequiredTimeOfDay = theRequiredTimeOfDay self.theCalculatedElevation = theCalculatedElevation //mySolarElevationChartData.append(self) } // End of [init(theRequiredTimeOfDay: String, theCalculatedElevation: Double)] } // End of [struct ChartSolarElevationValues] Unfortunately, the result did not append each subsequent calculation, but continued to create the same calculation as a new distinct object :: NOTE :: I only called three calculations with the Struct test. // NOTE :: To prevent an [ERROR] at [var mySolarElevationChartData = [ChartSolarElevationValues]] since it has an init. // Therefore you must add () at the end of [var mySolarElevationChartData = [ChartSolarElevationValues]] let theData = [ChartSolarElevationValues]() //print("theData :: \(theData)\n") let someData = ChartSolarElevationValues(theRequiredTimeOfDay: TheTimeForDaySunElevation.theTheTimeForDaySunElevation, theCalculatedElevation:VerifyCityLocationSearchRequestCorrectedSolarElevation.theVerifyCityLocationSearchRequestCorrectedSolarElevation) var theData_New = theData theData_New.append(someData) print("theData_New :: \(theData_New)\n") // Prints :: theData_New :: [My_Map.ChartSolarElevationValues(theRequiredTimeOfDay: "00:06:00", theCalculatedElevation: -61.11000735370401)]] // Prints :: [theData_New :: [My_Map.ChartSolarElevationValues(theRequiredTimeOfDay: "00:12:00", theCalculatedElevation: -61.315092082911875)]] // Prints :: [theData_New :: [My_Map.ChartSolarElevationValues(theRequiredTimeOfDay: "00:18:00", theCalculatedElevation: -61.47403413313205)]] So, I am misintepreting the required coding structure to properly append the Elevation Chart, and the Azimuth Chart with the calculated data. I know something is amiss, but for the moment, I do not know how to address this issue. Your suggestions would be welcome ... :] jim_k
Replies
3
Boosts
1
Views
879
Activity
Mar ’25
How to reverse a custom Domain in Swift Charts?
I'm currently building an app in SwiftUI that needs to show some charts. Swift Charts has been quite helpful but I can't seem to set the domain of the chart just how I want it. The following chart display events that happend in a week and I want to display every hour even if nothing happened there, and I want the hours to go up, instead of the default where the new day start at the top of the chart. struct ChartView: View { let dataSets: [someData] // simplified date init let datemin = date(year:2025, month: 2, day: 24) let datemax = date(year:2025, month: 2, day: 25) var body: some View { Chart(dataset) { data in PointMark(x: .value("Day", data.day, unit: .weekday), y: .value("Time", data.time, unit: .minute)) } } // The focus is on this line .chartYScale(domain: .automatic(reversed: true)) } } The .chartYScale(domain:) modifier allows me to set the start of the day at the bottom, but still ignores some hours. If I instead use this Chart(...) { ... } .chartYScale(domain: datemin...datemax) The chart display every hour of the day, but now the start of the day is at the bottom. I can't seem to find a way to get both things at the same time. if I add both modifiers only the first one get applied while the other ignored. Any solutions or workarounds?
Replies
2
Boosts
0
Views
473
Activity
Mar ’25
SwiftUI Gestures: Sequenced Long Press and Drag
In creating a sequenced gesture combining a LongPressGesture and a DragGesture, I found that the combined gesture exhibits two problems: The @GestureState does not properly update as the gesture progresses through its phases. Specifically, the updating(_:body:) closure (documented here) is only ever executed during the drag interaction. Long presses and drag-releases do not call the updating(_:body:) closure. Upon completing the long press gesture and activating the drag gesture, the drag gesture remains empty until the finger or cursor has moved. The expected behavior is for the drag gesture to begin even when its translation is of size .zero. This second problem – the nonexistence of a drag gesture once the long press has completed – prevents access to the location of the long-press-then-drag. Access to this location is critical for displaying to the user that the drag interaction has commenced. The below code is based on Apple's example presented here. I've highlighted the failure points in the code with // *. My questions are as follows: What is required to properly update the gesture state? Is it possible to have a viable drag gesture immediately upon fulfilling the long press gesture, even with a translation of .zero? Alternatively to the above question, is there a way to gain access to the location of the long press gesture? import SwiftUI import Charts enum DragState { case inactive case pressing case dragging(translation: CGSize) var isDragging: Bool { switch self { case .inactive, .pressing: return false case .dragging: return true } } } struct ChartGestureOverlay<Value: Comparable & Hashable>: View { @Binding var highlightedValue: Value? let chartProxy: ChartProxy let valueFromChartProxy: (CGFloat, ChartProxy) -> Value? let onDragChange: (DragState) -> Void @GestureState private var dragState = DragState.inactive var body: some View { Rectangle() .fill(Color.clear) .contentShape(Rectangle()) .onTapGesture { location in if let newValue = valueFromChartProxy(location.x, chartProxy) { highlightedValue = newValue } } .gesture(longPressAndDrag) } private var longPressAndDrag: some Gesture { let longPress = LongPressGesture(minimumDuration: 0.2) let drag = DragGesture(minimumDistance: .zero) .onChanged { value in if let newValue = valueFromChartProxy(value.location.x, chartProxy) { highlightedValue = newValue } } return longPress.sequenced(before: drag) .updating($dragState) { value, gestureState, _ in switch value { case .first(true): // * This is never called gestureState = .pressing case .second(true, let drag): // * Drag is often nil // * When drag is nil, we lack access to the location gestureState = .dragging(translation: drag?.translation ?? .zero) default: // * This is never called gestureState = .inactive } onDragChange(gestureState) } } } struct DataPoint: Identifiable { let id = UUID() let category: String let value: Double } struct ContentView: View { let dataPoints = [ DataPoint(category: "A", value: 5), DataPoint(category: "B", value: 3), DataPoint(category: "C", value: 8), DataPoint(category: "D", value: 2), DataPoint(category: "E", value: 7) ] @State private var highlightedCategory: String? = nil @State private var dragState = DragState.inactive var body: some View { VStack { Text("Bar Chart with Gesture Interaction") .font(.headline) .padding() Chart { ForEach(dataPoints) { dataPoint in BarMark( x: .value("Category", dataPoint.category), y: .value("Value", dataPoint.value) ) .foregroundStyle(highlightedCategory == dataPoint.category ? Color.red : Color.gray) .annotation(position: .top) { if highlightedCategory == dataPoint.category { Text("\(dataPoint.value, specifier: "%.1f")") .font(.caption) .foregroundColor(.primary) } } } } .frame(height: 300) .chartOverlay { chartProxy in ChartGestureOverlay<String>( highlightedValue: $highlightedCategory, chartProxy: chartProxy, valueFromChartProxy: { xPosition, chartProxy in if let category: String = chartProxy.value(atX: xPosition) { return category } return nil }, onDragChange: { newDragState in dragState = newDragState } ) } .onChange(of: highlightedCategory, { oldCategory, newCategory in }) } .padding() } } #Preview { ContentView() } Thank you!
Replies
7
Boosts
1
Views
1.7k
Activity
Mar ’25
Swiftui Charts
I have a SwiftUI LineMark chart that inverts the y axis when the data the chart is plotting is all zeros. I'm expecting the y axis 0 to be at the bottom but when the data is all zeros it's at the top. Below is an example demonstrating the problem: import SwiftUI import Charts struct ChartView: View { let data: [Double] = [0,0,0,0,0] var body: some View { Chart { ForEach(data.indices, id: \.self) { index in LineMark( x: .value("Index", index), y: .value("Value", data[index]) ) } } .chartYAxis { AxisMarks(values: [0, 10, 20, 30, 40, 50]) { value in AxisValueLabel() AxisTick() AxisGridLine() } } .padding() } } I can't use .chartYScale(domain: ) because it overrides the chartYAxis where the real code creates custom a leading and trailing y axis. Does anyone have any suggestions how I may fix this?
Replies
5
Boosts
0
Views
649
Activity
Feb ’25
Removing Grid Lines from Charts in SwiftUI
Hi, I have created a line graph using LineMark in Charts, which by default includes grid lines and axes lines. My requirement is to remove the grid lines but retain the axes lines and the values. I have tried the following code: .chartXAxis { AxisMarks(preset: .extended, values: .stride(by: 2), stroke: StrokeStyle(lineWidth: 0)) } This is removing grid lines as well as axes lines. How to retain axes lines while removing grid lines ?
Replies
2
Boosts
0
Views
399
Activity
Feb ’25
Swift charts displaying wrong theme through UIHostingController
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.
Replies
1
Boosts
0
Views
1.3k
Activity
Feb ’25
Affiliate View Struct is probably hidden by Charts on iOS app
The problem is: As per screenshot below, one can only see the lineChart. I have another struct AffiliateView coded under this Chart: import SnapKit import Charts import DGCharts class AffiliateViewController: UIViewController { private lazy var chartView: LineChartView = { let chart = LineChartView() chart.noDataText = "No data available." chart.chartDescription.enabled = false chart.xAxis.labelPosition = .bottom chart.rightAxis.enabled = false chart.legend.enabled = true chart.backgroundColor = .lightGray // For debugging visibility return chart }() private lazy var containerView: UIView = { let view = UIView() view.backgroundColor = .white return view }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white // Add container view and chart view to the main view view.addSubview(containerView) view.addSubview(chartView) // Add SwiftUI View inside the container view let affiliateView = AffiliateView() let hostingController = UIHostingController(rootView: affiliateView) addChild(hostingController) containerView.addSubview(hostingController.view) hostingController.view.frame = containerView.bounds hostingController.didMove(toParent: self) layout() setupChartData() } private func layout() { // Layout the container view (SwiftUI content) containerView.snp.makeConstraints { make in make.top.equalTo(view.safeAreaLayoutGuide.snp.top) make.left.right.equalToSuperview() make.height.equalTo(350) // Increase the height for the SwiftUI content } // Layout the chart view below the container view chartView.snp.makeConstraints { make in make.top.equalTo(containerView.snp.bottom).offset(20) // Space between chart and the affiliate content make.left.equalToSuperview().offset(20) make.right.equalToSuperview().offset(-20) make.height.equalTo(200) // Set a fixed height for the chart } } private func setupChartData() { let dataEntries = [ ChartDataEntry(x: 1, y: 10), ChartDataEntry(x: 2, y: 20), ChartDataEntry(x: 3, y: 15), ChartDataEntry(x: 4, y: 30), ChartDataEntry(x: 5, y: 25) ] let dataSet = LineChartDataSet(entries: dataEntries, label: "Clicks per Day") dataSet.colors = [.blue] dataSet.valueColors = [.black] dataSet.circleColors = [.red] dataSet.circleRadius = 4.0 let data = LineChartData(dataSet: dataSet) chartView.data = data chartView.notifyDataSetChanged() } } // SwiftUI View remains in the same file struct AffiliateView: View { @State private var customMessage: String = "" @State private var uniqueLink: String = "Your unique link will appear here." @State private var clickData: [Double] = [10, 20, 15, 30, 25] // Example data var body: some View { NavigationView { VStack(spacing: 20) { // TextField for custom message input TextField("Enter your custom message...", text: $customMessage) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding(.horizontal) // Generate Link Button Button(action: generateLink) { Text("Generate Sign-Up Link") .font(.headline) .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: 50) .background(Color.red) .cornerRadius(10) } .padding(.horizontal) // Generated Link Label Text(uniqueLink) .font(.body) .multilineTextAlignment(.center) .padding(.horizontal) // You can add a chart here if you want to show it in SwiftUI too /* LineChartView(data: clickData, title: "Clicks per Day", legend: "Daily Clicks") */ } .navigationTitle("Affiliate Marketing") .navigationBarTitleDisplayMode(.inline) } } private func generateLink() { let encodedMessage = customMessage.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" uniqueLink = "https://affiliate.example.com/referral?message=\(encodedMessage)" addClickData() } private func addClickData() { clickData.append(Double.random(in: 0...100)) } } As you see, the AffiliateView has been declared outside of Controller View class. The View content was visible before the lineChart was added into this code. Now the View content is not visible anymore. I have tried to increment/decrement values at make.height.equalTo() but to no avail. Could anyone kindly point me in the right direction?
Replies
0
Boosts
0
Views
376
Activity
Jan ’25
How to Visualize Data in a DataFrame Using Charts
I want to visualize the data stored in a DataFrame using various charts (barmark, sectormark, linemark, etc.). My questions are as follows: Can a DataFrame be used directly within a chart? If so, could you provide a simple example? If it cannot be used directly, what is the correct way to use it? Could you provide an example? Thank you for your help. Best regards.
Replies
0
Boosts
1
Views
436
Activity
Jan ’25
TabView and Swift Charts giving inconsistent behaviour when swiping between pages
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()]) } } }
Replies
10
Boosts
0
Views
1.9k
Activity
Jan ’25
SwiftChart with secondary Y Axis
Does SwiftUI now support the ability for a chart to have two different Y Axes? ChaptGPT seems to think it does, but I keep getting compiler errors in XCode.
Replies
0
Boosts
0
Views
501
Activity
Dec ’24