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

192 Posts

Post

Replies

Boosts

Views

Activity

Handling of dates with no value when creating a date bar chart in Charts
Some data have skipped dates, as in the following data. TrainingSession(date: formatter.date(from: "2024-05-12 07:37:30 +0000")!, maxRM: 10.0, totalVolume: 0.0), TrainingSession(date: formatter.date(from: "2024-06-01 15:00:00 +0000")!, maxRM: 10.5, totalVolume: 105.0), TrainingSession(date: formatter.date(from: "2024-06-03 15:00:00 +0000")!, maxRM: 10.0, totalVolume: 100.0) In this case, the graph shows nothing for the corresponding date as shown in the image. s it possible to create a continuous graph by displaying only the data with values and not the dates with no values? The source code is as follows // ContentView.swift // GraphSample // // Created by 齋藤卓馬 on 2024/06/09. // import SwiftUI import Charts struct TrainingSession { var date: Date var maxRM: Double var totalVolume: Double } struct GraphView: View { var sessions: [TrainingSession] var body: some View { ScrollView { VStack(alignment: .leading) { // 最大RMのグラフ VStack(alignment: .leading) { Text("最大RM") .font(.headline) .padding() Chart(sessions, id: \.date) { session in BarMark( x: .value("Date", session.date), y: .value("Max RM", session.maxRM) ) } .chartXAxis { AxisMarks(values: .stride(by: .day, count:7)) // 日付の表示間隔を調整 } .chartScrollableAxes(.horizontal) // 横スクロールを有効にする .padding([.leading, .trailing, .bottom]) } // 総負荷量のグラフ VStack(alignment: .leading) { Text("総負荷量") .font(.headline) .padding() Chart(sessions, id: \.date) { session in BarMark( x: .value("Date", session.date), y: .value("Total Volume", session.totalVolume) ) } .chartXAxis { AxisMarks(values: .stride(by: .day, count:7)) // 日付の表示間隔を調整 } .chartScrollableAxes(.horizontal) // 横スクロールを有効にする .padding([.leading, .trailing, .bottom]) } } } } } struct ContentView: View { var body: some View { GraphView(sessions: sampleData) } var sampleData: [TrainingSession] { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" return [ TrainingSession(date: formatter.date(from: "2024-05-12 07:37:30 +0000")!, maxRM: 10.0, totalVolume: 0.0), TrainingSession(date: formatter.date(from: "2024-06-01 15:00:00 +0000")!, maxRM: 10.5, totalVolume: 105.0), TrainingSession(date: formatter.date(from: "2024-06-03 15:00:00 +0000")!, maxRM: 10.0, totalVolume: 100.0) ] } } struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } } }
2
0
844
Jun ’24
Creating a navigation link within a chart?
I’d like to create a simple Gantt chart where each horizontal BarMark is a navigation link to a detail view. When I embed a navigation link within a chart, I get the error “Static method 'buildExpression' requires that 'some ChartContent' conform to 'View’” NavigationLink(value: taskGroup) { BarMark( xStart: .value("Start", taskGroup.start), xEnd: .value("End", taskGroup.end), y: .value("Event", taskGroup.taskGroupName), height: barHeight ) } I could use a chart overlay and manage the navigation from there, but it appears I can only grab published chart data at a given tap gesture. I need the object itself to inject into the detail view (in this case TaskGroup) and the data I’m plotting in the chart isn’t unique - so no obvious way to identify which TaskGroup the user tapped.
3
0
795
Jun ’24
How can Charts display sales data for a full month and support monthly paging?
Due to the varying number of days in each month, I am unsure how to enable monthly paging in Charts. In Apple's official example, SwiftChartsExample, there is only an example showing the sales of the "last 30 days": .chartXVisibleDomain(length: 3600 * 24 * 30) I have tried using scrollPosition to calculate the number of days in the current month, like this: var days: Int { let current = Calendar.current let dateRange = current.range(of: .day, in: .month, for: scrollPosition) return dateRange?.count ?? 0 } ... .chartXVisibleDomain(length: 3600 * 24 * days) ... .chartScrollPosition(x: $scrollPosition) ... But I found that it does not work as expected. 😢
2
0
980
Jun ’24
SwiftUI chart - take screenshot of the chart view on macOS
Hello all, if I enable the .chartScrollableAxes(.horizontal) and .chartXVisibleDomain(length: length) for a chart view to zoom in the screenshot of the view misses the graphs. I use this extension: `extension View { @MainActor func snapshot() { let renderer = ImageRenderer(content: self) if let exportImage = renderer.nsImage { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.writeObjects([exportImage]) } } }` The screenshot is taken with: Button("Snap") { let view = ChartView(text: $statusText, length: $chartLength) .padding() .frame(width: 1500, height: 500) view.snapshot() } If I omit .chartScrollableAxes(.horizontal) the snapshot is ok and the graphs are visible in the image but then a zoom is not possible and the whole range is shown. Any ideas?
1
1
828
Jun ’24
SwiftUI chart legend overflow
Using Charts in SwiftUI to create a horizontal bar chart, if the text of the legend is sufficiently long, the text overflows outside of the view rather than wrapping or moving to the next line. (can see problem when running code on on iPhone) Is this a bug or am I doing something incorrectly? I can use .clipped() to ensure the legend doesn't overflow. But that doesn't fully solve the problem because the text is then just cut off. import Charts import SwiftUI struct ChartData: Identifiable { let id: Int let title: String let count: Double let color: Color } struct ContentView: View { private let data = [ ChartData(id: 1, title: "Item 1", count: 4, color: .yellow), ChartData(id: 2, title: "Item 2 With a Long Title and then some more", count: 6, color: .blue), ChartData(id: 3, title: "Item 3 With a Long Title", count: 12, color: .green) ] private let chartHeight: CGFloat = 40 private let chartCornerRadius: CGFloat = 5 var body: some View { VStack { Chart(data) { item in BarMark( x: .value("Count", item.count), stacking: .normalized ) .foregroundStyle(by: .value("Title", item.title)) } .chartXAxis(.hidden) .chartYAxis(.hidden) .chartLegend(.visible) .chartPlotStyle { chartContent in chartContent .frame(height: chartHeight) } .chartForegroundStyleScale(range: data.map { $0.color }) } .padding() } } #Preview { ContentView() }
2
0
1.4k
Jun ’24
Swift Chart causing app to crash when deleting an item from the data array
When deleting the last added item from a list view in my app a bar chart in a different view crashes my app. If I delete any other item in the list view everything work as expected. I'm using SwiftData in my app. Does anyone have any idea how I can prevent the app from crashing? I filter the data in the init to only have the current days data Chart View struct ConsumedDrinkChartView: View { @Environment(\.modelContext) var modelContext let screenVerticalSizeClass = UIScreen.VerticalSizeClass var compactScreen: Bool { return screenVerticalSizeClass == "compact" } @State private var chartCalendarUnit: Calendar.Component = .hour @State private var chartRange: ClosedRange<Date> @State private var axisValueLabelFormat: Date.FormatStyle @State private var axisValueLabelCount: Int @State private var startDate: Date @State private var endDate: Date @State private var plotStartPadding: Double = 0 @State private var plotEndPadding: Double = 0 @Binding var selectedTimeFrame:String @Query var consumedFluids: [ConsumedDrink] let defaultVolume = DataStore.defaultVolume init(selectedTimeFrame: Binding<String>, dateRange: ClosedRange<Date>) { _selectedTimeFrame = selectedTimeFrame _startDate = State(initialValue: Date().startOfDay) _endDate = State(initialValue: Date().endOfDay) let endDate = dateRange.upperBound let startDate = dateRange.lowerBound _consumedFluids = Query(filter: #Predicate { $0.date > startDate && $0.date < endDate }, sort: \ConsumedDrink.date) _chartRange = State(initialValue: dateRange) _axisValueLabelFormat = State(initialValue: .dateTime.hour(.conversationalDefaultDigits(amPM: .narrow))) _axisValueLabelCount = State(initialValue: 2) } var body: some View { Chart { ForEach(consumedFluids) { consumedFluid in BarMark(x: .value("Date", consumedFluid.date, unit: chartCalendarUnit), y: .value("Fluid Ounces", consumedFluid.drink.amount)) } .foregroundStyle(.pink) } .frame(height: 180) .padding() .chartXAxis { AxisMarks(values: .stride(by: chartCalendarUnit, count: axisValueLabelCount,roundLowerBound: true, roundUpperBound: true)) { _ in AxisGridLine() AxisValueLabel(format: axisValueLabelFormat, centered: true) } } .chartXScale(domain: chartRange, range: .plotDimension(startPadding: plotStartPadding, endPadding: plotEndPadding)) .background(RoundedRectangle(cornerRadius: 12).fill(Color(.secondarySystemBackground))) .onChange(of: selectedTimeFrame) { selectChartRange() } .onChange(of: consumedFluids) { print("consumedFluids: \(consumedFluids.count)") } .onAppear { selectChartRange() } } func selectChartRange() { plotStartPadding = 0 plotEndPadding = 0 switch selectedTimeFrame { case "Day": startDate = Date().startOfDay endDate = Date().endOfDay chartCalendarUnit = .hour axisValueLabelCount = 2 axisValueLabelFormat = .dateTime.hour(.conversationalDefaultDigits(amPM: .narrow)) case "Week": startDate = Date().add(days: -7) chartCalendarUnit = .day axisValueLabelCount = 1 axisValueLabelFormat = .dateTime.weekday() case "Month": startDate = Date().add(days: -30) chartCalendarUnit = .day axisValueLabelCount = 2 axisValueLabelFormat = .dateTime.day() plotStartPadding = 10 plotEndPadding = 10 case "SixMonths": let endOfMonth = Date().endOfMonth() startDate = endOfMonth.add(months: -6) chartCalendarUnit = .month axisValueLabelCount = 1 axisValueLabelFormat = .dateTime.month() plotStartPadding = 10 plotEndPadding = 32 case "Year": let endOfMonth = Date().endOfMonth() startDate = endOfMonth.add(months: -12) chartCalendarUnit = .month axisValueLabelCount = 1 axisValueLabelFormat = .dateTime.month(.narrow) plotStartPadding = 15 plotEndPadding = 15 default: chartCalendarUnit = .day } chartRange = startDate...endDate } } List View struct ConsumedDrinkListView: View { @Environment(\.modelContext) var modelContext @Query(sort: \ConsumedDrink.date) var dailyConsumedFluids: [ConsumedDrink] @State private var showingAlert = false @State private var alertMessage: String = "" @State private var alertTitle: String = "" var body: some View { NavigationStack { if dailyConsumedFluids.isEmpty { ContentUnavailableView("No Consumed Drinks", systemImage: "mug.fill", description: Text("Drink some water and stay hydrated.")) } else { List { ForEach(dailyConsumedFluids, id: \.self) { consumedDrink in NavigationLink { EditConsumedDrinkView(consumedDrink: consumedDrink) } label: { ConsumedDrinkRowView(consumedDrink: consumedDrink) } .swipeActions{ Button("Delete", systemImage: "trash", role: .destructive) { deleteConsumedDrink(consumedDrink: consumedDrink) } .tint(.red) } } } .listStyle(.plain) .alert(isPresented: $showingAlert) { Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")) ) } } Text("") .navigationTitle("Consumed Drinks") .navigationBarTitleDisplayMode(.inline) } } func deleteConsumedDrink(consumedDrink: ConsumedDrink) { do { if modelContext.hasChanges { print("ConsumedDrinkListView.deleteConsumedDrink") print("modelContext has Changes. Saving modelContext") try modelContext.save() } try DataStore.deleteConsumedDrink(drink: consumedDrink, modelContext: modelContext) } catch { self.alertTitle = "Error deleting consumed drink - \(consumedDrink.drink.name)" self.alertMessage = error.localizedDescription self.showingAlert = true } } }
2
0
778
Jun ’24
Multiple Swift Charts in List diplay inconsistently
I have multiple barmark Charts in a List for SwiftUI. For app running on iOS 17.4.1, and one running on MacOS Sonoma 14.4.1, by scrolling up and down on the list, the order of charts changes. Sometimes one chart will replace another chart, showing duplicate charts in the List. This did not happen for iOS 17.3, and earlier OS versions. Want to see if anyone else face the same issue. I have checked that correct chart model is used in onAppear, but what is displayed in Chart is not corresponding to the chart model.
4
0
1.6k
Jun ’24
Swift Chats chartScrollTargetBehavior is not working correctly when domain count is relatively small
I’m seeing a strange behavior when using chartScrollTargetBehavior and trying to scroll to a majorAlignment (.matching(DateComponents(day: 1))). let numberOfBarMarks = 10 .chartXVisibleDomain(length:3600 * 24 * numberOfBarMarks) .chartScrollTargetBehavior( .valueAligned( matching: DateComponents(hour: 0), majorAlignment: .matching(DateComponents(day: 1))) ) ) The issue is fully reproducible. I believe the issue is related to the number of bar marks being displayed in a domain. If I use 10 then the issue shows up, if I use 30 the issue doesn’t happen. Filed as FB13889037 including video and sample code.
0
0
582
Jun ’24
Swift Charts animation woes using centered AxisValueLabel
Hi, I'm having some trouble when animating my chart with a custom AxisValueLabel. Specifically, as soon as I set its init parameter centered to true, the x axis' leftmost value of the previous dataset sticks around during the animation to the next dataset. Here's a GIF of a screen recording from a minimum reproducible example I built. Keep a close eye on the x axis of the third BarMark, and notice how the 0 from the first BarMark sticks around longer than necessary / intended. While it isn't visible in the GIF, the extra 0 eventually does disappear, but only after the transition if fully complete, making the animation feel distracting and amateur-ish, rather than smooth. This is my code for the x axis. If I turn centered to false, this problem immediately goes away. .chartXAxis { AxisMarks( preset: .aligned, values: .stride( by: .day, count: 1, calendar: .current ) ) { value in AxisValueLabel(centered: true) { Text("\(value.index)") } } } As you might be able to tell, my x axis is date based, and I'm working on showing one BarMark per day of the week. I have a ZIP of my minimum reproducible example that I can provide for anyone interested, although I don't know how to share it here. Any advice on what I can do to fix this?
2
0
792
Jun ’24
Charts showing yesterday as todays data
I'm at my Witts end trying to figure out why charts is incorrectly labeling the days! struct SunlightSupportBox: View { @ObservedObject var viewModel = SunlightViewModel() @EnvironmentObject var themeSettings: ThemeSettings var sortedSunlightData: [SunlightData] { viewModel.sunlightData.sorted(by: { $0.date < $1.date }) } var body: some View { VStack { if !sortedSunlightData.isEmpty { Chart { ForEach(sortedSunlightData) { data in BarMark( x: .value("Day", formattedDate(date: data.date)), y: .value("Triggers/Reflections", Double((data.triggersCount * 10 + data.reflectionsCount * 10))) // Each trigger/reflection represents 5 minutes ) .foregroundStyle(Color.green.opacity(0.5)) BarMark( x: .value("Day", formattedDate(date: data.date)), yStart: .value("Sunlight Start", 0), yEnd: .value("Minutes of Sunlight", data.duration * 60) // Convert hours to minutes ) .foregroundStyle(Color.orange.opacity(0.5)) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(10) .clipShape(RoundedRectangle(cornerRadius: 25)) .padding() .background(themeSettings.currentColor) .cornerRadius(25) } else { Text("No sunlight data") .foregroundColor(.black) .background(Color.white) .cornerRadius(10) .padding() } } .frame(width: 350, height: 200) .background(themeSettings.currentColor) .cornerRadius(30) } private func formattedDate(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "E" return formatter.string(from: date) } } This view correctly shows todays day with the correct data struct SleepSupportBox: View { @ObservedObject var viewModel = SleepViewModel() @EnvironmentObject var themeSettings: ThemeSettings var body: some View { VStack { if !viewModel.sleepData.isEmpty { Chart(viewModel.sleepData) { data in BarMark( x: .value("Day", formattedDate(date: data.date)), y: .value("Triggers/Reflections", Double(data.triggersCount + data.reflectionsCount)) ) .foregroundStyle(Color.green.opacity(0.5)) BarMark( x: .value("Day", formattedDate(date: data.date)), y: .value("Hours of Sleep", data.hours) ) .foregroundStyle(Color.asblue) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(10) .clipShape(RoundedRectangle(cornerRadius: 25)) .padding() .background(themeSettings.currentColor) .cornerRadius(25) } else { Text("No sleep data") .foregroundColor(.black) .background(Color.white) .cornerRadius(10) .padding() } } .frame(width: 350, height: 200) .background(themeSettings.currentColor) .cornerRadius(30) } private func formattedDate(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "E" return formatter.string(from: date) } }
1
0
650
Jun ’24
SwiftChart with secondary Y Axis
I created a SwiftChart as below and I would like to have two YAxis, one for amount and the second for count. So, the amount YAxis is a different scale then the count YAxis. Does anybody have an example of this or shed some light on coding two different YAxis? Thanks ForEach(seriesArt) { series in ForEach(series.chartSeries.chartEntry) { BarMark( x: .value("Tier", $0.tier), y: .value("Price", $0.keyValue) ) } .foregroundStyle(by: .value("Count", series.chartCategory)) .position(by: .value("Price", series.chartCategory)) } } .frame(width: 400, height: 200) .chartXAxis { AxisMarks(position: .bottom, values: .automatic) { AxisValueLabel() .foregroundStyle(Color.white) } } .chartYAxis { AxisMarks(position: .leading, values: .automatic) { value in AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1)) AxisValueLabel() { if let intValue = value.as(Int.self) { Text("\(intValue)") .font(.system(size: 10)) .foregroundColor(.white) } } } .chartYAixs - for count sum by tier which needs to be a different scale from the amount YAxis } } }
1
0
1.1k
Apr ’24
How to I move the sectorMark annotation to the outside perimeter of the pie chart?
Hello Apple Developers, I am trying to get the annotation text (the percentages) to stay on the outside perimeter of the pie chart and not in the middle of the pie chart like it currently is. Is there possibly a way to increase the radius of the annotation text to be that of the pie chart edge and maybe a little more? I don't know. Please help me out. What I currently have:
2
0
1.6k
Apr ’24
Creating a live chart for real time ble data
Hi All, Please excuse my relatively basic question but I am new to swift programming and I am battling with a project. I currently have an app that receives data from an Arduino using ble and displays the data as an integer. I used this medium article From Arduino programming to iOS App development as a guide for most of the functionality but changed the sensor data being sent to better suit my project requirements. Based on the link above, I have all of the bluetooth handling in PeripheralUseCase.swift file and then I have the ConnectView file for the display: @ObservedObject var viewModel: ConnectViewModel @Environment(\.dismiss) var dismiss @State var isToggleOn: Bool = false @State var isPeripheralReady: Bool = false @State var lastPressure: Int = 0 var body: some View { VStack { Text(viewModel.connectedPeripheral.name ?? "Unknown") .font(.title) ZStack { CardView() VStack { Text("Surface") HStack { Button("Flats") { viewModel.flats() } .disabled(!isPeripheralReady) .buttonStyle(.borderedProminent) Button("FlatPoint") { viewModel.flatPoint() } .disabled(!isPeripheralReady) .buttonStyle(.borderedProminent) Button("Points") { viewModel.points() } .disabled(!isPeripheralReady) .buttonStyle(.borderedProminent) } } } ZStack { CardView() VStack { Text("\(lastPressure) kPa") .font(.largeTitle) HStack { Spacer() .frame(alignment: .trailing) Toggle("Notify", isOn: $isToggleOn) .disabled(!isPeripheralReady) Spacer() .frame(alignment: .trailing) } } } Spacer() .frame(maxHeight:.infinity) Button { dismiss() } label: { Text("Disconnect") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .padding(.horizontal) } .onChange(of: isToggleOn) { newValue in if newValue == true { viewModel.startNotifyPressure() } else { viewModel.stopNotifyPressure() } let startTime = Date().timeIntervalSince1970 } .onReceive(viewModel.$state) { state in switch state { case .ready: isPeripheralReady = true case let .Pressure(temp): lastPressure = temp default: print("Not handled") } } } } struct PeripheralView_Previews: PreviewProvider { final class FakeUseCase: PeripheralUseCaseProtocol { var peripheral: Peripheral? var onWriteLedState: ((Bool) -> Void)? var onReadPressure: ((Int) -> Void)? var onPeripheralReady: (() -> Void)? var onError: ((Error) -> Void)? func writeLedState(isOn: String) {} func readPressure() { onReadPressure?(25) } func notifyPressure(_ isOn: Bool) {} } static var viewModel = { ConnectViewModel(useCase: FakeUseCase(), connectedPeripheral: .init(name: "iOSArduinoBoard")) }() static var previews: some View { ConnectView(viewModel: viewModel, isPeripheralReady: true) } } struct CardView: View { var body: some View { RoundedRectangle(cornerRadius: 16, style: .continuous) .shadow(color: Color(white: 0.5, opacity: 0.2), radius: 6) .foregroundColor(.init(uiColor: .secondarySystemBackground)) } } With the associated View Model: @Published var state = State.idle var useCase: PeripheralUseCaseProtocol let connectedPeripheral: Peripheral init(useCase: PeripheralUseCaseProtocol, connectedPeripheral: Peripheral) { self.useCase = useCase self.useCase.peripheral = connectedPeripheral self.connectedPeripheral = connectedPeripheral self.setCallbacks() } private func setCallbacks() { useCase.onPeripheralReady = { [weak self] in self?.state = .ready } useCase.onReadPressure = { [weak self] value in self?.state = .Pressure(value) } useCase.onWriteLedState = { [weak self] value in self?.state = .ledState(value) } useCase.onError = { error in print("Error \(error)") } } func startNotifyPressure() { useCase.notifyPressure(true) } func stopNotifyPressure() { useCase.notifyPressure(false) } func readPressure() { useCase.readPressure() } func flats() { useCase.writeLedState(isOn: "1") } func flatPoint() { useCase.writeLedState(isOn: "2") } func points() { useCase.writeLedState(isOn: "3") } } extension ConnectViewModel { enum State { case idle case ready case Pressure(Int) case ledState(Bool) } } What I am now trying to do is plot the data that is received from the Arduino in a line graph as it is received. Preferably the graph will scroll with time as well.
1
0
1.2k
Mar ’24
Half Donut Chart using SectorMark
Does anyone knows if it is possible to make a half donut chart using the new one SectorMark? I am exploring the initializers but so far no luck, this is my code like anyone else making a normal donut: struct Score: Identifiable { let id = UUID() let type: String let value: Int } @State private var scores: [Score] = [ .init(type: "Hits", value: 1), .init(type: "Misses", value: 9) ] Chart(scores) { score in SectorMark( angle: .value("Values", score.value), innerRadius: .ratio(0.9), outerRadius: .ratio(0.7) ) .foregroundStyle( by: .value("Type", score.type) ) }
2
0
1.5k
Mar ’24
Swift Charts Accessibility Bug
I've been working on migrating some graphics to Swift Charts for an app I work on. However I've been noticing strange behavior when it comes to VoiceOver. If I create a bar chart and use: BarMark(x: .value("Month", x, unit: .month), y: ...) While the chart looks fine, the voice over values seem to follow arbitrary values set for the bin. From what I can tell they are following the underlying bin values that Swift Charts uses to provide spacing between bars. For instance, this simple example: let monthlyRevenueData = [ (x: try! Date("2024-01-01T00:00:00Z", strategy: .iso8601), y: (income: 55000, revenue: 124000), id: UUID()), (x: try! Date("2024-02-01T00:00:00Z", strategy: .iso8601), y: (income: 58000, revenue: 130000), id: UUID()), (x: try! Date("2024-03-01T00:00:00Z", strategy: .iso8601), y: (income: 59000, revenue: 120000), id: UUID()), ] struct ContentView: View { var body: some View { Chart(monthlyRevenueData, id: \.id) { (x, y, _) in BarMark(x: .value("Month", x, unit: .month), y: .value("Income", y.income)) .foregroundStyle(.green) BarMark(x: .value("Month", x, unit: .month), y: .value("Revenue", y.revenue)) } } } #Preview { ContentView() } Results in the Voice Over reading "January 14th 2024 at 12 AM to January 28th 2024 at 12am ..." despite the fact that the data should be for the entire month of Jan. Is there any way to get VoiceOver to read the input data rather than relying on how the chart is formatted? Preferably without the need to remove all visual spacing between the bars. Video link: https://drive.google.com/file/d/11mxCl3wR2HzoOaihOvci-vZk4zgG1d39/view?usp=drive_link
1
0
743
Mar ’24
LineMark chart reverting Y axis
Dear all, I have a line chart and on the Y axis it shows values from 0 (bottom) to 20 (top). Now, I'd like to show value from 20 (bottom) to 1 (top). Here below the code I used: Chart{ ForEach(andamento, id: \.posizione) { item in LineMark( x: .value("Giornata", item.giornata), y: .value("Posizione", item.posizione) ) PointMark( x: .value("Giornata", item.giornata), y: .value("Posizione", item.posizione) ) // We need .opacity(0) or it will // overlay your `.symbol` .opacity(0) .annotation(position: .overlay, alignment: .bottom, spacing: 10) { Text("\(item.posizione)") .font(.subheadline) } } .symbol(Circle()) } Can anybody help me? Thanks, A.
2
0
888
Mar ’24
SectorMark foreground style colors
Dear all, I am using SwiftUI 15.2 and I have created a donut chart using SectorMark. Now, I have three values to show in the chart. When I set up the foregroundstyle, it returns orange, blu and green colors, whereas I'd like to have different colors (e.g. red, yellow and green). Chart(data, id: \.risultato) { dataItem in SectorMark(angle: .value("Type", dataItem.partite), innerRadius: .ratio(0.7), angularInset: 1.5) .foregroundStyle(by: .value("Type", dataItem.risultato)) .annotation(position: .overlay){ Text("\(dataItem.partite)") .font(.caption) } } .frame(height: 150) I'm reporting the final result here below. Do you know how I can customize them? Thanks in advance for your support, Andrea
2
1
2.5k
Feb ’24
How do I prepare CloudKit data for Swift Charts?
I have created a simple app where a user is tracking their mood. A simple click of the button inserts the necessary data to a CloudKit database. So I consider each record as a 'transactional' record. I am able to successfully query the data and present in a list view. I am able to sort and incorporate simple predicates. I am now at a point in my development that I would like to add a pie chart based on the users data and I am not sure how to roll-up the data / group by the data / aggregate the data [I am not sure what the correct terminology is within Swift] The pie chart would show the various moods that the exists in the CloudKit database and the slices would be sized based on the count of each mood. Any guidance that you can provide would be greatly helpful!
1
0
763
Feb ’24
Swift Charts Won't Update a Variable Value
I am currently working on a project for the Swift Student Challenge. One part of the app is for visualizing goals with a chart created using Swift Charts. In the app, you log your progress for the goal and then it should show up in the chart. My issue is that the data is not showing after it has been logged. Here are some code snippets: Code For Chart View Chart { let currentDate = Date.now BarMark ( x: .value("Day", "Today"), y: .value("Score", goalItem.getLogItemByDate(date: currentDate).score) ) } .frame(maxHeight: 225) .padding() GoalItem Data Object public class GoalItem: Identifiable { public var id: String var name: String var description: String var logItems: [String: GoalLogItem] init(name: String, description: String) { self.id = UUID().uuidString self.name = name self.description = description self.logItems = [:] } func log(date: Date, score: Double, notes: String) { self.logItems[dateToDateString(date: date)] = GoalLogItem(date: date, score: score, notes: notes) } func getLogItemByDate(date: Date) -> GoalLogItem { let logItem = self.logItems[dateToDateString(date: date)] if logItem != nil { return logItem! } else { return GoalLogItem(isPlaceholder: true) } } } After logging something using the GoalItem.log method, why does it not show up in the chart? Are the variables not updated? If so, how would I get the variables to update? Thanks
5
0
1.8k
Feb ’24
CPU Saturation with Charts real time plotting from data received from Bluetooth
Hello everyone, I am new to Swift, it is my first project and I am a PhD Electrical Engineer student. I am designing an iOS app for a device that we are designing that is capable of reading electrical brain data and sending them via BLE with a sampling frequency of 2400 Hz. I created a Bluetooth service for the Swift app that every time it receives new data, processes it to split the different channels and add the new data to the Charts data arrays. Here is the code I've designed: func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if characteristic.uuid == Nordic_UART_TX_CHAR_UUID { guard error == nil, let data = characteristic.value else { print("[Bluetooth] Error receiving data or no data: \(error?.localizedDescription ?? "Unknown Error")") return } DispatchQueue.global(qos: .background).async { self.processReceivedData(data) } } } func processReceivedData(_ data: Data) { var batch = [(Int, Int)]() for i in stride(from: 0, to: data.count - 4, by: 4) { let channel = Int(data[i] & 0xFF) let value = Int((Int(data[i + 3] & 0xFF) << 16) | (Int(data[i + 2] & 0xFF) << 8) | (Int(data[i + 1] & 0xFF))) - 8388608 batch.append((channel, value)) } DispatchQueue.main.async { for (channel, value) in batch { let nowTime = (Date().timeIntervalSince1970 - self.dataGraphService.startTime) let newDataPoint = DataGraphService.VoltagePerTime(time: nowTime, voltage: Double(value)/8388608, channel: "Channel \(channel - 15)") if channel == 16 { self.dataGraphService.lastX1 = nowTime self.dataGraphService.dataCh1.append(newDataPoint) } else if channel == 17 { self.dataGraphService.lastX2 = nowTime self.dataGraphService.dataCh2.append(newDataPoint) } else if channel == 18 { self.dataGraphService.lastX3 = nowTime self.dataGraphService.dataCh3.append(newDataPoint) } else if channel == 19 { self.dataGraphService.lastX4 = nowTime self.dataGraphService.dataCh4.append(newDataPoint) } } } } // DataGraphService.swift struct VoltagePerTime { var time: Double var voltage: Double var channel: String } @Published var dataCh1: [VoltagePerTime] = [] @Published var dataCh2: [VoltagePerTime] = [] @Published var dataCh3: [VoltagePerTime] = [] @Published var dataCh4: [VoltagePerTime] = [] @Published var windowSize: Double = 2.0 @Published var lastX1: Double = 0 @Published var lastX2: Double = 0 @Published var lastX3: Double = 0 @Published var lastX4: Double = 0 I also created a View that shows the real-time data from the different channels. ChartView( data: dataGraphService.dataCh1.filter { dataGraphService.getXAxisRange(for: dataGraphService.dataCh1, windowSize: dataGraphService.windowSize).contains($0.time) }, xAxisRange: dataGraphService.getXAxisRange(for: dataGraphService.dataCh1, windowSize: dataGraphService.windowSize), channel: "Channel 1", windowSize: dataGraphService.windowSize ) // ChartView.swift import SwiftUI import Charts struct ChartView: View { var data: [DataGraphService.VoltagePerTime] var xAxisRange: ClosedRange<Double> var channel: String var windowSize: Double var body: some View { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.1)) .overlay( VStack{ Text("\(channel)") .foregroundColor(Color.gray) .font(.system(size: 16, weight: .semibold)) Chart(data, id: \.time) { item in LineMark( x: .value("Time [s]", item.time), y: .value("Voltage [V]", item.voltage) ) } .chartYAxisLabel(position: .leading) { Text("Voltage [V]") } .chartYScale(domain: [-1.6, 1.6]) .chartYAxis { AxisMarks(position: .leading, values: [-1.6, -0.8, 0, 0.8, 1.6]) AxisMarks(values: [-1.6, -1.2, -0.8, -0.4, 0, 0.4, 0.8, 1.2, 1.6]) { AxisGridLine() } } .chartXAxisLabel(position: .bottom, alignment: .center) { Text("Time [s]") } .chartXScale(domain: xAxisRange) .chartXAxis { AxisMarks(values: .automatic(desiredCount: Int(windowSize)*2)) AxisMarks(values: .automatic(desiredCount: 4*Int(windowSize)-2)) { AxisGridLine() } } .padding(5) } ) .padding(2.5) .padding([.leading, .trailing], 5) } } With these code I can receive and plot the data in real-time but after some time the CPU of the iPhone gets saturated and the app stop working. I have the guess that the code is designed in a way that the functions are called one inside the other one in a very fast speed that the CPU cannot handle. My doubt is if there is any other way to code this real-time plotting actions without make the iPhone's CPU power hungry. Thank you very much for your help!
1
1
741
Feb ’24
Handling of dates with no value when creating a date bar chart in Charts
Some data have skipped dates, as in the following data. TrainingSession(date: formatter.date(from: "2024-05-12 07:37:30 +0000")!, maxRM: 10.0, totalVolume: 0.0), TrainingSession(date: formatter.date(from: "2024-06-01 15:00:00 +0000")!, maxRM: 10.5, totalVolume: 105.0), TrainingSession(date: formatter.date(from: "2024-06-03 15:00:00 +0000")!, maxRM: 10.0, totalVolume: 100.0) In this case, the graph shows nothing for the corresponding date as shown in the image. s it possible to create a continuous graph by displaying only the data with values and not the dates with no values? The source code is as follows // ContentView.swift // GraphSample // // Created by 齋藤卓馬 on 2024/06/09. // import SwiftUI import Charts struct TrainingSession { var date: Date var maxRM: Double var totalVolume: Double } struct GraphView: View { var sessions: [TrainingSession] var body: some View { ScrollView { VStack(alignment: .leading) { // 最大RMのグラフ VStack(alignment: .leading) { Text("最大RM") .font(.headline) .padding() Chart(sessions, id: \.date) { session in BarMark( x: .value("Date", session.date), y: .value("Max RM", session.maxRM) ) } .chartXAxis { AxisMarks(values: .stride(by: .day, count:7)) // 日付の表示間隔を調整 } .chartScrollableAxes(.horizontal) // 横スクロールを有効にする .padding([.leading, .trailing, .bottom]) } // 総負荷量のグラフ VStack(alignment: .leading) { Text("総負荷量") .font(.headline) .padding() Chart(sessions, id: \.date) { session in BarMark( x: .value("Date", session.date), y: .value("Total Volume", session.totalVolume) ) } .chartXAxis { AxisMarks(values: .stride(by: .day, count:7)) // 日付の表示間隔を調整 } .chartScrollableAxes(.horizontal) // 横スクロールを有効にする .padding([.leading, .trailing, .bottom]) } } } } } struct ContentView: View { var body: some View { GraphView(sessions: sampleData) } var sampleData: [TrainingSession] { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z" return [ TrainingSession(date: formatter.date(from: "2024-05-12 07:37:30 +0000")!, maxRM: 10.0, totalVolume: 0.0), TrainingSession(date: formatter.date(from: "2024-06-01 15:00:00 +0000")!, maxRM: 10.5, totalVolume: 105.0), TrainingSession(date: formatter.date(from: "2024-06-03 15:00:00 +0000")!, maxRM: 10.0, totalVolume: 100.0) ] } } struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } } }
Replies
2
Boosts
0
Views
844
Activity
Jun ’24
Creating a navigation link within a chart?
I’d like to create a simple Gantt chart where each horizontal BarMark is a navigation link to a detail view. When I embed a navigation link within a chart, I get the error “Static method 'buildExpression' requires that 'some ChartContent' conform to 'View’” NavigationLink(value: taskGroup) { BarMark( xStart: .value("Start", taskGroup.start), xEnd: .value("End", taskGroup.end), y: .value("Event", taskGroup.taskGroupName), height: barHeight ) } I could use a chart overlay and manage the navigation from there, but it appears I can only grab published chart data at a given tap gesture. I need the object itself to inject into the detail view (in this case TaskGroup) and the data I’m plotting in the chart isn’t unique - so no obvious way to identify which TaskGroup the user tapped.
Replies
3
Boosts
0
Views
795
Activity
Jun ’24
How can Charts display sales data for a full month and support monthly paging?
Due to the varying number of days in each month, I am unsure how to enable monthly paging in Charts. In Apple's official example, SwiftChartsExample, there is only an example showing the sales of the "last 30 days": .chartXVisibleDomain(length: 3600 * 24 * 30) I have tried using scrollPosition to calculate the number of days in the current month, like this: var days: Int { let current = Calendar.current let dateRange = current.range(of: .day, in: .month, for: scrollPosition) return dateRange?.count ?? 0 } ... .chartXVisibleDomain(length: 3600 * 24 * days) ... .chartScrollPosition(x: $scrollPosition) ... But I found that it does not work as expected. 😢
Replies
2
Boosts
0
Views
980
Activity
Jun ’24
SwiftUI chart - take screenshot of the chart view on macOS
Hello all, if I enable the .chartScrollableAxes(.horizontal) and .chartXVisibleDomain(length: length) for a chart view to zoom in the screenshot of the view misses the graphs. I use this extension: `extension View { @MainActor func snapshot() { let renderer = ImageRenderer(content: self) if let exportImage = renderer.nsImage { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.writeObjects([exportImage]) } } }` The screenshot is taken with: Button("Snap") { let view = ChartView(text: $statusText, length: $chartLength) .padding() .frame(width: 1500, height: 500) view.snapshot() } If I omit .chartScrollableAxes(.horizontal) the snapshot is ok and the graphs are visible in the image but then a zoom is not possible and the whole range is shown. Any ideas?
Replies
1
Boosts
1
Views
828
Activity
Jun ’24
SwiftUI chart legend overflow
Using Charts in SwiftUI to create a horizontal bar chart, if the text of the legend is sufficiently long, the text overflows outside of the view rather than wrapping or moving to the next line. (can see problem when running code on on iPhone) Is this a bug or am I doing something incorrectly? I can use .clipped() to ensure the legend doesn't overflow. But that doesn't fully solve the problem because the text is then just cut off. import Charts import SwiftUI struct ChartData: Identifiable { let id: Int let title: String let count: Double let color: Color } struct ContentView: View { private let data = [ ChartData(id: 1, title: "Item 1", count: 4, color: .yellow), ChartData(id: 2, title: "Item 2 With a Long Title and then some more", count: 6, color: .blue), ChartData(id: 3, title: "Item 3 With a Long Title", count: 12, color: .green) ] private let chartHeight: CGFloat = 40 private let chartCornerRadius: CGFloat = 5 var body: some View { VStack { Chart(data) { item in BarMark( x: .value("Count", item.count), stacking: .normalized ) .foregroundStyle(by: .value("Title", item.title)) } .chartXAxis(.hidden) .chartYAxis(.hidden) .chartLegend(.visible) .chartPlotStyle { chartContent in chartContent .frame(height: chartHeight) } .chartForegroundStyleScale(range: data.map { $0.color }) } .padding() } } #Preview { ContentView() }
Replies
2
Boosts
0
Views
1.4k
Activity
Jun ’24
Swift Chart causing app to crash when deleting an item from the data array
When deleting the last added item from a list view in my app a bar chart in a different view crashes my app. If I delete any other item in the list view everything work as expected. I'm using SwiftData in my app. Does anyone have any idea how I can prevent the app from crashing? I filter the data in the init to only have the current days data Chart View struct ConsumedDrinkChartView: View { @Environment(\.modelContext) var modelContext let screenVerticalSizeClass = UIScreen.VerticalSizeClass var compactScreen: Bool { return screenVerticalSizeClass == "compact" } @State private var chartCalendarUnit: Calendar.Component = .hour @State private var chartRange: ClosedRange<Date> @State private var axisValueLabelFormat: Date.FormatStyle @State private var axisValueLabelCount: Int @State private var startDate: Date @State private var endDate: Date @State private var plotStartPadding: Double = 0 @State private var plotEndPadding: Double = 0 @Binding var selectedTimeFrame:String @Query var consumedFluids: [ConsumedDrink] let defaultVolume = DataStore.defaultVolume init(selectedTimeFrame: Binding<String>, dateRange: ClosedRange<Date>) { _selectedTimeFrame = selectedTimeFrame _startDate = State(initialValue: Date().startOfDay) _endDate = State(initialValue: Date().endOfDay) let endDate = dateRange.upperBound let startDate = dateRange.lowerBound _consumedFluids = Query(filter: #Predicate { $0.date > startDate && $0.date < endDate }, sort: \ConsumedDrink.date) _chartRange = State(initialValue: dateRange) _axisValueLabelFormat = State(initialValue: .dateTime.hour(.conversationalDefaultDigits(amPM: .narrow))) _axisValueLabelCount = State(initialValue: 2) } var body: some View { Chart { ForEach(consumedFluids) { consumedFluid in BarMark(x: .value("Date", consumedFluid.date, unit: chartCalendarUnit), y: .value("Fluid Ounces", consumedFluid.drink.amount)) } .foregroundStyle(.pink) } .frame(height: 180) .padding() .chartXAxis { AxisMarks(values: .stride(by: chartCalendarUnit, count: axisValueLabelCount,roundLowerBound: true, roundUpperBound: true)) { _ in AxisGridLine() AxisValueLabel(format: axisValueLabelFormat, centered: true) } } .chartXScale(domain: chartRange, range: .plotDimension(startPadding: plotStartPadding, endPadding: plotEndPadding)) .background(RoundedRectangle(cornerRadius: 12).fill(Color(.secondarySystemBackground))) .onChange(of: selectedTimeFrame) { selectChartRange() } .onChange(of: consumedFluids) { print("consumedFluids: \(consumedFluids.count)") } .onAppear { selectChartRange() } } func selectChartRange() { plotStartPadding = 0 plotEndPadding = 0 switch selectedTimeFrame { case "Day": startDate = Date().startOfDay endDate = Date().endOfDay chartCalendarUnit = .hour axisValueLabelCount = 2 axisValueLabelFormat = .dateTime.hour(.conversationalDefaultDigits(amPM: .narrow)) case "Week": startDate = Date().add(days: -7) chartCalendarUnit = .day axisValueLabelCount = 1 axisValueLabelFormat = .dateTime.weekday() case "Month": startDate = Date().add(days: -30) chartCalendarUnit = .day axisValueLabelCount = 2 axisValueLabelFormat = .dateTime.day() plotStartPadding = 10 plotEndPadding = 10 case "SixMonths": let endOfMonth = Date().endOfMonth() startDate = endOfMonth.add(months: -6) chartCalendarUnit = .month axisValueLabelCount = 1 axisValueLabelFormat = .dateTime.month() plotStartPadding = 10 plotEndPadding = 32 case "Year": let endOfMonth = Date().endOfMonth() startDate = endOfMonth.add(months: -12) chartCalendarUnit = .month axisValueLabelCount = 1 axisValueLabelFormat = .dateTime.month(.narrow) plotStartPadding = 15 plotEndPadding = 15 default: chartCalendarUnit = .day } chartRange = startDate...endDate } } List View struct ConsumedDrinkListView: View { @Environment(\.modelContext) var modelContext @Query(sort: \ConsumedDrink.date) var dailyConsumedFluids: [ConsumedDrink] @State private var showingAlert = false @State private var alertMessage: String = "" @State private var alertTitle: String = "" var body: some View { NavigationStack { if dailyConsumedFluids.isEmpty { ContentUnavailableView("No Consumed Drinks", systemImage: "mug.fill", description: Text("Drink some water and stay hydrated.")) } else { List { ForEach(dailyConsumedFluids, id: \.self) { consumedDrink in NavigationLink { EditConsumedDrinkView(consumedDrink: consumedDrink) } label: { ConsumedDrinkRowView(consumedDrink: consumedDrink) } .swipeActions{ Button("Delete", systemImage: "trash", role: .destructive) { deleteConsumedDrink(consumedDrink: consumedDrink) } .tint(.red) } } } .listStyle(.plain) .alert(isPresented: $showingAlert) { Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")) ) } } Text("") .navigationTitle("Consumed Drinks") .navigationBarTitleDisplayMode(.inline) } } func deleteConsumedDrink(consumedDrink: ConsumedDrink) { do { if modelContext.hasChanges { print("ConsumedDrinkListView.deleteConsumedDrink") print("modelContext has Changes. Saving modelContext") try modelContext.save() } try DataStore.deleteConsumedDrink(drink: consumedDrink, modelContext: modelContext) } catch { self.alertTitle = "Error deleting consumed drink - \(consumedDrink.drink.name)" self.alertMessage = error.localizedDescription self.showingAlert = true } } }
Replies
2
Boosts
0
Views
778
Activity
Jun ’24
Multiple Swift Charts in List diplay inconsistently
I have multiple barmark Charts in a List for SwiftUI. For app running on iOS 17.4.1, and one running on MacOS Sonoma 14.4.1, by scrolling up and down on the list, the order of charts changes. Sometimes one chart will replace another chart, showing duplicate charts in the List. This did not happen for iOS 17.3, and earlier OS versions. Want to see if anyone else face the same issue. I have checked that correct chart model is used in onAppear, but what is displayed in Chart is not corresponding to the chart model.
Replies
4
Boosts
0
Views
1.6k
Activity
Jun ’24
Swift Chats chartScrollTargetBehavior is not working correctly when domain count is relatively small
I’m seeing a strange behavior when using chartScrollTargetBehavior and trying to scroll to a majorAlignment (.matching(DateComponents(day: 1))). let numberOfBarMarks = 10 .chartXVisibleDomain(length:3600 * 24 * numberOfBarMarks) .chartScrollTargetBehavior( .valueAligned( matching: DateComponents(hour: 0), majorAlignment: .matching(DateComponents(day: 1))) ) ) The issue is fully reproducible. I believe the issue is related to the number of bar marks being displayed in a domain. If I use 10 then the issue shows up, if I use 30 the issue doesn’t happen. Filed as FB13889037 including video and sample code.
Replies
0
Boosts
0
Views
582
Activity
Jun ’24
Swift Charts animation woes using centered AxisValueLabel
Hi, I'm having some trouble when animating my chart with a custom AxisValueLabel. Specifically, as soon as I set its init parameter centered to true, the x axis' leftmost value of the previous dataset sticks around during the animation to the next dataset. Here's a GIF of a screen recording from a minimum reproducible example I built. Keep a close eye on the x axis of the third BarMark, and notice how the 0 from the first BarMark sticks around longer than necessary / intended. While it isn't visible in the GIF, the extra 0 eventually does disappear, but only after the transition if fully complete, making the animation feel distracting and amateur-ish, rather than smooth. This is my code for the x axis. If I turn centered to false, this problem immediately goes away. .chartXAxis { AxisMarks( preset: .aligned, values: .stride( by: .day, count: 1, calendar: .current ) ) { value in AxisValueLabel(centered: true) { Text("\(value.index)") } } } As you might be able to tell, my x axis is date based, and I'm working on showing one BarMark per day of the week. I have a ZIP of my minimum reproducible example that I can provide for anyone interested, although I don't know how to share it here. Any advice on what I can do to fix this?
Replies
2
Boosts
0
Views
792
Activity
Jun ’24
Charts showing yesterday as todays data
I'm at my Witts end trying to figure out why charts is incorrectly labeling the days! struct SunlightSupportBox: View { @ObservedObject var viewModel = SunlightViewModel() @EnvironmentObject var themeSettings: ThemeSettings var sortedSunlightData: [SunlightData] { viewModel.sunlightData.sorted(by: { $0.date < $1.date }) } var body: some View { VStack { if !sortedSunlightData.isEmpty { Chart { ForEach(sortedSunlightData) { data in BarMark( x: .value("Day", formattedDate(date: data.date)), y: .value("Triggers/Reflections", Double((data.triggersCount * 10 + data.reflectionsCount * 10))) // Each trigger/reflection represents 5 minutes ) .foregroundStyle(Color.green.opacity(0.5)) BarMark( x: .value("Day", formattedDate(date: data.date)), yStart: .value("Sunlight Start", 0), yEnd: .value("Minutes of Sunlight", data.duration * 60) // Convert hours to minutes ) .foregroundStyle(Color.orange.opacity(0.5)) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(10) .clipShape(RoundedRectangle(cornerRadius: 25)) .padding() .background(themeSettings.currentColor) .cornerRadius(25) } else { Text("No sunlight data") .foregroundColor(.black) .background(Color.white) .cornerRadius(10) .padding() } } .frame(width: 350, height: 200) .background(themeSettings.currentColor) .cornerRadius(30) } private func formattedDate(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "E" return formatter.string(from: date) } } This view correctly shows todays day with the correct data struct SleepSupportBox: View { @ObservedObject var viewModel = SleepViewModel() @EnvironmentObject var themeSettings: ThemeSettings var body: some View { VStack { if !viewModel.sleepData.isEmpty { Chart(viewModel.sleepData) { data in BarMark( x: .value("Day", formattedDate(date: data.date)), y: .value("Triggers/Reflections", Double(data.triggersCount + data.reflectionsCount)) ) .foregroundStyle(Color.green.opacity(0.5)) BarMark( x: .value("Day", formattedDate(date: data.date)), y: .value("Hours of Sleep", data.hours) ) .foregroundStyle(Color.asblue) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(10) .clipShape(RoundedRectangle(cornerRadius: 25)) .padding() .background(themeSettings.currentColor) .cornerRadius(25) } else { Text("No sleep data") .foregroundColor(.black) .background(Color.white) .cornerRadius(10) .padding() } } .frame(width: 350, height: 200) .background(themeSettings.currentColor) .cornerRadius(30) } private func formattedDate(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "E" return formatter.string(from: date) } }
Replies
1
Boosts
0
Views
650
Activity
Jun ’24
SwiftChart with secondary Y Axis
I created a SwiftChart as below and I would like to have two YAxis, one for amount and the second for count. So, the amount YAxis is a different scale then the count YAxis. Does anybody have an example of this or shed some light on coding two different YAxis? Thanks ForEach(seriesArt) { series in ForEach(series.chartSeries.chartEntry) { BarMark( x: .value("Tier", $0.tier), y: .value("Price", $0.keyValue) ) } .foregroundStyle(by: .value("Count", series.chartCategory)) .position(by: .value("Price", series.chartCategory)) } } .frame(width: 400, height: 200) .chartXAxis { AxisMarks(position: .bottom, values: .automatic) { AxisValueLabel() .foregroundStyle(Color.white) } } .chartYAxis { AxisMarks(position: .leading, values: .automatic) { value in AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 1)) AxisValueLabel() { if let intValue = value.as(Int.self) { Text("\(intValue)") .font(.system(size: 10)) .foregroundColor(.white) } } } .chartYAixs - for count sum by tier which needs to be a different scale from the amount YAxis } } }
Replies
1
Boosts
0
Views
1.1k
Activity
Apr ’24
How to I move the sectorMark annotation to the outside perimeter of the pie chart?
Hello Apple Developers, I am trying to get the annotation text (the percentages) to stay on the outside perimeter of the pie chart and not in the middle of the pie chart like it currently is. Is there possibly a way to increase the radius of the annotation text to be that of the pie chart edge and maybe a little more? I don't know. Please help me out. What I currently have:
Replies
2
Boosts
0
Views
1.6k
Activity
Apr ’24
Creating a live chart for real time ble data
Hi All, Please excuse my relatively basic question but I am new to swift programming and I am battling with a project. I currently have an app that receives data from an Arduino using ble and displays the data as an integer. I used this medium article From Arduino programming to iOS App development as a guide for most of the functionality but changed the sensor data being sent to better suit my project requirements. Based on the link above, I have all of the bluetooth handling in PeripheralUseCase.swift file and then I have the ConnectView file for the display: @ObservedObject var viewModel: ConnectViewModel @Environment(\.dismiss) var dismiss @State var isToggleOn: Bool = false @State var isPeripheralReady: Bool = false @State var lastPressure: Int = 0 var body: some View { VStack { Text(viewModel.connectedPeripheral.name ?? "Unknown") .font(.title) ZStack { CardView() VStack { Text("Surface") HStack { Button("Flats") { viewModel.flats() } .disabled(!isPeripheralReady) .buttonStyle(.borderedProminent) Button("FlatPoint") { viewModel.flatPoint() } .disabled(!isPeripheralReady) .buttonStyle(.borderedProminent) Button("Points") { viewModel.points() } .disabled(!isPeripheralReady) .buttonStyle(.borderedProminent) } } } ZStack { CardView() VStack { Text("\(lastPressure) kPa") .font(.largeTitle) HStack { Spacer() .frame(alignment: .trailing) Toggle("Notify", isOn: $isToggleOn) .disabled(!isPeripheralReady) Spacer() .frame(alignment: .trailing) } } } Spacer() .frame(maxHeight:.infinity) Button { dismiss() } label: { Text("Disconnect") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .padding(.horizontal) } .onChange(of: isToggleOn) { newValue in if newValue == true { viewModel.startNotifyPressure() } else { viewModel.stopNotifyPressure() } let startTime = Date().timeIntervalSince1970 } .onReceive(viewModel.$state) { state in switch state { case .ready: isPeripheralReady = true case let .Pressure(temp): lastPressure = temp default: print("Not handled") } } } } struct PeripheralView_Previews: PreviewProvider { final class FakeUseCase: PeripheralUseCaseProtocol { var peripheral: Peripheral? var onWriteLedState: ((Bool) -> Void)? var onReadPressure: ((Int) -> Void)? var onPeripheralReady: (() -> Void)? var onError: ((Error) -> Void)? func writeLedState(isOn: String) {} func readPressure() { onReadPressure?(25) } func notifyPressure(_ isOn: Bool) {} } static var viewModel = { ConnectViewModel(useCase: FakeUseCase(), connectedPeripheral: .init(name: "iOSArduinoBoard")) }() static var previews: some View { ConnectView(viewModel: viewModel, isPeripheralReady: true) } } struct CardView: View { var body: some View { RoundedRectangle(cornerRadius: 16, style: .continuous) .shadow(color: Color(white: 0.5, opacity: 0.2), radius: 6) .foregroundColor(.init(uiColor: .secondarySystemBackground)) } } With the associated View Model: @Published var state = State.idle var useCase: PeripheralUseCaseProtocol let connectedPeripheral: Peripheral init(useCase: PeripheralUseCaseProtocol, connectedPeripheral: Peripheral) { self.useCase = useCase self.useCase.peripheral = connectedPeripheral self.connectedPeripheral = connectedPeripheral self.setCallbacks() } private func setCallbacks() { useCase.onPeripheralReady = { [weak self] in self?.state = .ready } useCase.onReadPressure = { [weak self] value in self?.state = .Pressure(value) } useCase.onWriteLedState = { [weak self] value in self?.state = .ledState(value) } useCase.onError = { error in print("Error \(error)") } } func startNotifyPressure() { useCase.notifyPressure(true) } func stopNotifyPressure() { useCase.notifyPressure(false) } func readPressure() { useCase.readPressure() } func flats() { useCase.writeLedState(isOn: "1") } func flatPoint() { useCase.writeLedState(isOn: "2") } func points() { useCase.writeLedState(isOn: "3") } } extension ConnectViewModel { enum State { case idle case ready case Pressure(Int) case ledState(Bool) } } What I am now trying to do is plot the data that is received from the Arduino in a line graph as it is received. Preferably the graph will scroll with time as well.
Replies
1
Boosts
0
Views
1.2k
Activity
Mar ’24
Half Donut Chart using SectorMark
Does anyone knows if it is possible to make a half donut chart using the new one SectorMark? I am exploring the initializers but so far no luck, this is my code like anyone else making a normal donut: struct Score: Identifiable { let id = UUID() let type: String let value: Int } @State private var scores: [Score] = [ .init(type: "Hits", value: 1), .init(type: "Misses", value: 9) ] Chart(scores) { score in SectorMark( angle: .value("Values", score.value), innerRadius: .ratio(0.9), outerRadius: .ratio(0.7) ) .foregroundStyle( by: .value("Type", score.type) ) }
Replies
2
Boosts
0
Views
1.5k
Activity
Mar ’24
Swift Charts Accessibility Bug
I've been working on migrating some graphics to Swift Charts for an app I work on. However I've been noticing strange behavior when it comes to VoiceOver. If I create a bar chart and use: BarMark(x: .value("Month", x, unit: .month), y: ...) While the chart looks fine, the voice over values seem to follow arbitrary values set for the bin. From what I can tell they are following the underlying bin values that Swift Charts uses to provide spacing between bars. For instance, this simple example: let monthlyRevenueData = [ (x: try! Date("2024-01-01T00:00:00Z", strategy: .iso8601), y: (income: 55000, revenue: 124000), id: UUID()), (x: try! Date("2024-02-01T00:00:00Z", strategy: .iso8601), y: (income: 58000, revenue: 130000), id: UUID()), (x: try! Date("2024-03-01T00:00:00Z", strategy: .iso8601), y: (income: 59000, revenue: 120000), id: UUID()), ] struct ContentView: View { var body: some View { Chart(monthlyRevenueData, id: \.id) { (x, y, _) in BarMark(x: .value("Month", x, unit: .month), y: .value("Income", y.income)) .foregroundStyle(.green) BarMark(x: .value("Month", x, unit: .month), y: .value("Revenue", y.revenue)) } } } #Preview { ContentView() } Results in the Voice Over reading "January 14th 2024 at 12 AM to January 28th 2024 at 12am ..." despite the fact that the data should be for the entire month of Jan. Is there any way to get VoiceOver to read the input data rather than relying on how the chart is formatted? Preferably without the need to remove all visual spacing between the bars. Video link: https://drive.google.com/file/d/11mxCl3wR2HzoOaihOvci-vZk4zgG1d39/view?usp=drive_link
Replies
1
Boosts
0
Views
743
Activity
Mar ’24
LineMark chart reverting Y axis
Dear all, I have a line chart and on the Y axis it shows values from 0 (bottom) to 20 (top). Now, I'd like to show value from 20 (bottom) to 1 (top). Here below the code I used: Chart{ ForEach(andamento, id: \.posizione) { item in LineMark( x: .value("Giornata", item.giornata), y: .value("Posizione", item.posizione) ) PointMark( x: .value("Giornata", item.giornata), y: .value("Posizione", item.posizione) ) // We need .opacity(0) or it will // overlay your `.symbol` .opacity(0) .annotation(position: .overlay, alignment: .bottom, spacing: 10) { Text("\(item.posizione)") .font(.subheadline) } } .symbol(Circle()) } Can anybody help me? Thanks, A.
Replies
2
Boosts
0
Views
888
Activity
Mar ’24
SectorMark foreground style colors
Dear all, I am using SwiftUI 15.2 and I have created a donut chart using SectorMark. Now, I have three values to show in the chart. When I set up the foregroundstyle, it returns orange, blu and green colors, whereas I'd like to have different colors (e.g. red, yellow and green). Chart(data, id: \.risultato) { dataItem in SectorMark(angle: .value("Type", dataItem.partite), innerRadius: .ratio(0.7), angularInset: 1.5) .foregroundStyle(by: .value("Type", dataItem.risultato)) .annotation(position: .overlay){ Text("\(dataItem.partite)") .font(.caption) } } .frame(height: 150) I'm reporting the final result here below. Do you know how I can customize them? Thanks in advance for your support, Andrea
Replies
2
Boosts
1
Views
2.5k
Activity
Feb ’24
How do I prepare CloudKit data for Swift Charts?
I have created a simple app where a user is tracking their mood. A simple click of the button inserts the necessary data to a CloudKit database. So I consider each record as a 'transactional' record. I am able to successfully query the data and present in a list view. I am able to sort and incorporate simple predicates. I am now at a point in my development that I would like to add a pie chart based on the users data and I am not sure how to roll-up the data / group by the data / aggregate the data [I am not sure what the correct terminology is within Swift] The pie chart would show the various moods that the exists in the CloudKit database and the slices would be sized based on the count of each mood. Any guidance that you can provide would be greatly helpful!
Replies
1
Boosts
0
Views
763
Activity
Feb ’24
Swift Charts Won't Update a Variable Value
I am currently working on a project for the Swift Student Challenge. One part of the app is for visualizing goals with a chart created using Swift Charts. In the app, you log your progress for the goal and then it should show up in the chart. My issue is that the data is not showing after it has been logged. Here are some code snippets: Code For Chart View Chart { let currentDate = Date.now BarMark ( x: .value("Day", "Today"), y: .value("Score", goalItem.getLogItemByDate(date: currentDate).score) ) } .frame(maxHeight: 225) .padding() GoalItem Data Object public class GoalItem: Identifiable { public var id: String var name: String var description: String var logItems: [String: GoalLogItem] init(name: String, description: String) { self.id = UUID().uuidString self.name = name self.description = description self.logItems = [:] } func log(date: Date, score: Double, notes: String) { self.logItems[dateToDateString(date: date)] = GoalLogItem(date: date, score: score, notes: notes) } func getLogItemByDate(date: Date) -> GoalLogItem { let logItem = self.logItems[dateToDateString(date: date)] if logItem != nil { return logItem! } else { return GoalLogItem(isPlaceholder: true) } } } After logging something using the GoalItem.log method, why does it not show up in the chart? Are the variables not updated? If so, how would I get the variables to update? Thanks
Replies
5
Boosts
0
Views
1.8k
Activity
Feb ’24
CPU Saturation with Charts real time plotting from data received from Bluetooth
Hello everyone, I am new to Swift, it is my first project and I am a PhD Electrical Engineer student. I am designing an iOS app for a device that we are designing that is capable of reading electrical brain data and sending them via BLE with a sampling frequency of 2400 Hz. I created a Bluetooth service for the Swift app that every time it receives new data, processes it to split the different channels and add the new data to the Charts data arrays. Here is the code I've designed: func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if characteristic.uuid == Nordic_UART_TX_CHAR_UUID { guard error == nil, let data = characteristic.value else { print("[Bluetooth] Error receiving data or no data: \(error?.localizedDescription ?? "Unknown Error")") return } DispatchQueue.global(qos: .background).async { self.processReceivedData(data) } } } func processReceivedData(_ data: Data) { var batch = [(Int, Int)]() for i in stride(from: 0, to: data.count - 4, by: 4) { let channel = Int(data[i] & 0xFF) let value = Int((Int(data[i + 3] & 0xFF) << 16) | (Int(data[i + 2] & 0xFF) << 8) | (Int(data[i + 1] & 0xFF))) - 8388608 batch.append((channel, value)) } DispatchQueue.main.async { for (channel, value) in batch { let nowTime = (Date().timeIntervalSince1970 - self.dataGraphService.startTime) let newDataPoint = DataGraphService.VoltagePerTime(time: nowTime, voltage: Double(value)/8388608, channel: "Channel \(channel - 15)") if channel == 16 { self.dataGraphService.lastX1 = nowTime self.dataGraphService.dataCh1.append(newDataPoint) } else if channel == 17 { self.dataGraphService.lastX2 = nowTime self.dataGraphService.dataCh2.append(newDataPoint) } else if channel == 18 { self.dataGraphService.lastX3 = nowTime self.dataGraphService.dataCh3.append(newDataPoint) } else if channel == 19 { self.dataGraphService.lastX4 = nowTime self.dataGraphService.dataCh4.append(newDataPoint) } } } } // DataGraphService.swift struct VoltagePerTime { var time: Double var voltage: Double var channel: String } @Published var dataCh1: [VoltagePerTime] = [] @Published var dataCh2: [VoltagePerTime] = [] @Published var dataCh3: [VoltagePerTime] = [] @Published var dataCh4: [VoltagePerTime] = [] @Published var windowSize: Double = 2.0 @Published var lastX1: Double = 0 @Published var lastX2: Double = 0 @Published var lastX3: Double = 0 @Published var lastX4: Double = 0 I also created a View that shows the real-time data from the different channels. ChartView( data: dataGraphService.dataCh1.filter { dataGraphService.getXAxisRange(for: dataGraphService.dataCh1, windowSize: dataGraphService.windowSize).contains($0.time) }, xAxisRange: dataGraphService.getXAxisRange(for: dataGraphService.dataCh1, windowSize: dataGraphService.windowSize), channel: "Channel 1", windowSize: dataGraphService.windowSize ) // ChartView.swift import SwiftUI import Charts struct ChartView: View { var data: [DataGraphService.VoltagePerTime] var xAxisRange: ClosedRange<Double> var channel: String var windowSize: Double var body: some View { RoundedRectangle(cornerRadius: 10) .fill(Color.gray.opacity(0.1)) .overlay( VStack{ Text("\(channel)") .foregroundColor(Color.gray) .font(.system(size: 16, weight: .semibold)) Chart(data, id: \.time) { item in LineMark( x: .value("Time [s]", item.time), y: .value("Voltage [V]", item.voltage) ) } .chartYAxisLabel(position: .leading) { Text("Voltage [V]") } .chartYScale(domain: [-1.6, 1.6]) .chartYAxis { AxisMarks(position: .leading, values: [-1.6, -0.8, 0, 0.8, 1.6]) AxisMarks(values: [-1.6, -1.2, -0.8, -0.4, 0, 0.4, 0.8, 1.2, 1.6]) { AxisGridLine() } } .chartXAxisLabel(position: .bottom, alignment: .center) { Text("Time [s]") } .chartXScale(domain: xAxisRange) .chartXAxis { AxisMarks(values: .automatic(desiredCount: Int(windowSize)*2)) AxisMarks(values: .automatic(desiredCount: 4*Int(windowSize)-2)) { AxisGridLine() } } .padding(5) } ) .padding(2.5) .padding([.leading, .trailing], 5) } } With these code I can receive and plot the data in real-time but after some time the CPU of the iPhone gets saturated and the app stop working. I have the guess that the code is designed in a way that the functions are called one inside the other one in a very fast speed that the CPU cannot handle. My doubt is if there is any other way to code this real-time plotting actions without make the iPhone's CPU power hungry. Thank you very much for your help!
Replies
1
Boosts
1
Views
741
Activity
Feb ’24