Confused as to why the Chart flips with each user input. The console is also output unique id for each slice which was not my intention. Not sure if the unique .id is the culprit behind the flip.
selectedCount changed to: Optional(3)
Selected slice: Optional(App.EmojiUsage(id: 69090646-0D0A-4FE8-86EC-4103608DC3F7, emojiTab: App.emojiTab.sad, count: 1))
Scheduling reset task to run in 2 seconds
Resetting selected slice and count
selectedCount changed to: Optional(1)
Selected slice: Optional(App.EmojiUsage(id: DE4A76D1-CC57-4FA0-A261-9AD1A6E28F95, emojiTab: App.emojiTab.happy, count: 2))
Scheduling reset task to run in 2 seconds
Resetting selected slice and count
selectedCount changed to: Optional(3)
Selected slice: Optional(App.EmojiUsage(id: 5052F8EA-2582-4E72-A61D-01FCCDF3DB03, emojiTab: App.emojiTab.sad, count: 1))
Scheduling reset task to run in 2 seconds
Resetting selected slice and count
selectedCount changed to: Optional(0)
Selected slice: Optional(App.EmojiUsage(id: 5C1AB577-6CFC-4BA8-A9DF-30822EF79B91, emojiTab: App.emojiTab.happy, count: 2))
Scheduling reset task to run in 2 seconds
@Model
class AppModel {
var id: String
var journalEntry: String
var date: Date
var emojiTab: emojiTab
init(journalEntry: String, date: Date, emojiTab: emojiTab) {
self.id = UUID().uuidString
self.journalEntry = journalEntry
self.date = date
self.emojiTab = emojiTab
}
}
struct EmojiPrompt: Identifiable {
var id = UUID()
var icon: RiveViewModel
var emojitab: emojiTab
var title: String
}
enum emojiTab: String, Codable, Plottable {
case happy
case sad
case sleep
var primitivePlottable: Double {
switch self {
case .sleep:
return 0.0
case .happy:
return 1.0
case .sad:
return 2.0
}
}
}
var emojiPrompt = [
EmojiPrompt(
icon: RiveViewModel(
fileName: "app",
stateMachineName: "happyBtnSM",
artboardName: "happyBtn"
),
emojitab: .happy,
title: "Happy 1"
),
EmojiPrompt(
icon: RiveViewModel(
fileName: "app",
stateMachineName: "sadBtnSM",
artboardName: "sadBtn"
),
emojitab: .sad,
title: "Sad 2"
),
EmojiPrompt(
icon: RiveViewModel(
fileName: "app",
stateMachineName: "happyBtnSM",
artboardName: "happyBtn"
),
emojitab: .sleep,
title: "Sleep"
)
]
import SwiftUI
import SwiftData
import RiveRuntime
import Charts
struct SectorChartView: View {
@Environment(\.modelContext) private var context: ModelContext
@Binding var selectedEmojiUsage: EmojiUsage?
@State private var selectedCount: Int?
@Binding var selectedSlice: EmojiUsage?
@State private var resetTask: DispatchWorkItem? // State variable for the reset task
var emojiUsageData: [EmojiUsage]
var resetDelay: TimeInterval = 2.0 // Adjustable delay for reset
var body: some View {
ZStack {
Chart {
ForEach(emojiUsageData) { data in
SectorMark(
angle: .value("Count", data.count),
innerRadius: .ratio(0.70),
outerRadius: selectedSlice?.emojiTab == data.emojiTab ? .ratio(1.0) : .ratio(0.75),
angularInset: 1.5
)
.cornerRadius(4)
.foregroundStyle(by: .value("Emoji", data.emojiTab.rawValue.capitalized))
}
}
.chartAngleSelection(value: $selectedCount)
.chartBackground { chartProxy in
GeometryReader { geo in
let frame = geo[chartProxy.plotFrame!]
VStack {
if let selectedEmojiUsage = selectedEmojiUsage {
RiveViewModel(fileName: "app", stateMachineName: "\(selectedEmojiUsage.emojiTab.rawValue)BtnSM", artboardName: "\(selectedEmojiUsage.emojiTab.rawValue)Btn")
.view()
.frame(width: 120, height: 120)
.id(selectedEmojiUsage.emojiTab.rawValue) // Force re-render when the emojiTab changes
} else {
RiveViewModel(fileName: "app", stateMachineName: "sleepBtnSM", artboardName: "sleepBtn")
.view()
.frame(width: 120, height: 120)
.id("sleep") // Force re-render when default state
}
}
.position(x: frame.midX, y: frame.midY)
}
}
}
.onChange(of: selectedCount) { oldValue, newValue in
// Ensure reset task is only scheduled if there is a valid new value
guard newValue != nil else { return }
resetTask?.cancel() // Cancel any existing reset task
print("selectedCount changed to: \(String(describing: newValue))")
if let newValue = newValue {
withAnimation {
getSelectedSlice(value: newValue)
}
let task = DispatchWorkItem {
withAnimation(.easeIn) {
print("Resetting selected slice and count")
self.selectedSlice = nil
self.selectedCount = nil
self.selectedEmojiUsage = nil
}
}
resetTask = task
print("Scheduling reset task to run in 2 seconds")
DispatchQueue.main.asyncAfter(deadline: .now() + resetDelay, execute: task) // Schedule reset after specified delay
}
}
.frame(width: 250, height: 250)
}
private func getSelectedSlice(value: Int) {
var cumulativeTotal = 0
_ = emojiUsageData.first { emojiRange in
cumulativeTotal += emojiRange.count
if value <= cumulativeTotal {
selectedSlice = emojiRange
selectedEmojiUsage = emojiRange
print("Selected slice: \(String(describing: selectedSlice))")
return true
}
return false
}
}
}
var emojiUsageData: [EmojiUsage] {
let groupedEntries = Dictionary(grouping: entries, by: { $0.emojiTab })
return groupedEntries.map { (key, value) in
EmojiUsage(emojiTab: key, count: value.count)
}
}
struct EmojiUsage: Identifiable {
var id = UUID()
var emojiTab: emojiTab
var count: Int
}
Swift Charts
RSS for tagVisualize data with highly customizable charts across all Apple platforms using the compositional syntax of SwifUI.
Posts under Swift Charts tag
57 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
(I'm using macOS 14.5 and Xcode 15.4)
I have a Swift Chart on macOS that needs to scroll horizontally. Simplified version:
Chart(dataPoints) { data in
LineMark(x: .value("X Axis", data.x),
y: .value("Y Axis", data.y))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 10)
The above code works fine, except that it does not show scroll bars.
On a Mac with no trackpad, this means there's no mechanism to scroll the chart.
On my MacBook Pro with a trackpad, I can scroll the chart with a 2-finger swipe gesture, but there are no transient scroll bars to show the relative size of the visible part of the chart.
How do I add visible scroll bars to the chart so that I can scroll on Macs with no trackpad?
Is there any approach or sample code available to use these APIs:
.chartScrollableAxes(.horizontal)
.chartScrollPosition(x: ...)
.chartScrollPosition(initialX: ...)
.chartScrollTargetBehavior(...)
.chartXVisibleDomain(length: ...)
Plus a gesture recognised or Pinch or Magnification to create a Swift Chart with an X axis that can be zoomed in or out with a pinch gesture? And when zoomed in at any level above 0, the chart can then be scrolled left to right along the X axis.
I've had success using .chartScrollableAxes with .chartXSelection in parallel, so would also like to keep the ability to select X values too.
Hi,
I'm currently wrestling with the .chartXScale(domain:) modifier in order to get my Chart to display correctly. The basics of the Chart look like this.
Chart(measurements, id: \.timestamp) { measurement in
if let total = measurement.production?.total {
BarMark(
x: .value(
"Timestamp",
measurement.timestamp,
unit: .weekOfYear,
calendar: .current
),
y: .value(
"Solar production",
total
)
)
}
}
As anyone familiar with Charts can see, I sort data into columns based on what week of the year the measurements belong to. Some of them can be null, and when they are, I still want space in the Chart where a BarMark would've been to be taken up, like week number 4 in this example chart (in which I've defaulted all measurements that are null in week 4 to 0, for demonstration purposes):
To achieve that, as I understand, I'm meant to use the .chartXScale(domain:) modifier, but when I apply the following modifier...
.chartXScale(domain: firstDayOfMonth...firstDayOfNextMonth)
... (where the domain is from the first day of the month to the first day of the next month), to the code above, I end up with this weird half step when the last week of measurements are all null:
For reference, here's how the domain dates are set in my minimum reproducible example:
firstDayOfMonth = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: .now))!
firstDayOfNextMonth = Calendar.current.date(byAdding: .month, value: 1, to: firstDayOfMonth)!
Am I misusing this modifier somehow, or is this a bug? Would love some help figuring this out, thanks!
I have a SwiftUI page that I want to simplify by showing basic information by default, and putting the additional info behind a "Details" DisclosureGroup for advanced users.
I started by laying out all the components and breaking things into individual Views. These all are laid out and look fine.
Then I took several of them and added them inside a DisclosureGroupView.
But all of a sudden, the views inside started getting crunched together and the contents of the DisclosureGroup got clipped about 2/3 of the way down the page. The problem I'm trying to solve is how to show everything inside the DIsclosureGroup.
The top-level View looks like this:
VStack {
FirstItemView()
SecondView()
DetailView() // <- Shows disclosure arrow
}
Where DetailView is:
struct DetailView: View {
@State var isExpanded = true
var body: some View {
GeometryReader { geometry in
DisclosureGroup("Details", isExpanded: $isExpanded) {
ThirdRowView()
Spacer()
FourthRowView()
VStack {
FifthRowWithChartView()
CaptionLabelView(label: "Third", iconName: "chart.bar.xaxis")
}
}
}
}
}
The FifthRowWithChartView is half-clipped. One thing that might contribute is that there is a Chart view at the bottom of this page.
I've tried setting the width and height of the DisclosureGroup based on the height returned by the GeometryReader, but that didn't do anything.
This is all on iOS 17.6, testing on an iPhone 15ProMax. Any tips or tricks are most appreciated.
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()
}
I'm building a Swift Chart that displays locations in a horizontal timeline. The chart is scrollable.
When the chart is zoomed in, I want to show an annotation for every 6 hours.
Unfortunately, when axisMarks are set to .stride(by: .hour, count: 6), the annotations do not appear for the first several months in the timeline. I tried setting .stride(by: .minute, count: 360), but the result is the same.
Is this a Swift Charts bug, or am I doing something wrong?
A reproducible example is below. To reproduce:
Run the code below
See that annotations are missing at the leading edge of the chart. They only show up from a certain point on the chart's domain.
Tested on various iPhone and iPad simulators and physical devices, the issue appears everywhere.
P.S. I am aware that the example code below is not performant and that the annotations overlap when the chart is zoomed out. I have workarounds for that, but it's beyond the scope of my question and the minimum reproducible example.
struct ChartAnnotationsBug: View {
/// Sample data
let data = SampleData.samples
let startDate = SampleData.samples.first?.startDate ?? Date()
let endDate = Date()
/// Scroll position of the chart, expressed as Date along the x-axis.
@State var chartPosition: Date = SampleData.samples.first?.startDate ?? Date()
/// Sets the granularity of the shown view.
@State var visibleDomain: VisibleDomain = .month
var body: some View {
Chart(data, id: \.id) { element in
BarMark(xStart: .value("Start", element.startDate),
xEnd: .value("End", element.endDate),
yStart: 0,
yEnd: 50)
.foregroundStyle(by: .value("Type", element.type.rawValue))
.clipShape(.rect(cornerRadius: 8, style: .continuous))
}
.chartScrollableAxes(.horizontal) // enable scroll
.chartScrollPosition(x: $chartPosition) // track scroll offset
.chartXVisibleDomain(length: visibleDomain.seconds)
.chartXScale(domain: startDate...endDate)
.chartXAxis {
AxisMarks(values: .stride(by: .hour, count: 6)) { value in
if let date = value.as(Date.self) {
let hour = Calendar.current.component(.hour, from: date)
if hour == 0 { // midnight
AxisValueLabel(collisionResolution: .truncate) {
VStack(alignment: .leading) {
Text(date, format: .dateTime.hour().minute())
Text(date, format: .dateTime.weekday().month().day())
.bold()
}
}
AxisTick(stroke: .init(lineWidth: 1))
} else if [6, 12, 18].contains(hour) { // period
AxisValueLabel(collisionResolution: .truncate) {
Text(date, format: .dateTime.hour().minute())
}
AxisTick(length: .label)
}
}
}
}
.frame(height: 100)
.padding(.bottom, 40) // for overlay picker
.overlay {
Picker("", selection: $visibleDomain.animation()) {
ForEach(VisibleDomain.allCases) { variant in
Text(variant.label)
.tag(variant)
}
}
.pickerStyle(.segmented)
.frame(width: 240)
.padding(.trailing)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
} //: overlay
} //: body
} //: struct
// MARK: - Preview
#Preview {
ChartAnnotationsBug()
}
// MARK: - Data
enum SampleDataType: String, CaseIterable {
case city, wood, field
var label: String {
switch self {
case .city:
"City"
case .wood:
"Wood"
case .field:
"Field"
}
}
}
enum VisibleDomain: Identifiable, CaseIterable {
case day
case week
case month
var id: Int {
self.seconds
}
var seconds: Int {
switch self {
case .day:
3600 * 24 * 2
case .week:
3600 * 24 * 10
case .month:
3600 * 24 * 40
}
}
var label: String {
switch self {
case .day:
"Days"
case .week:
"Weeks"
case .month:
"Months"
}
}
}
struct SampleData: Identifiable {
let startDate: Date
let endDate: Date
let name: String
let type: SampleDataType
var id: String { name }
static let samples: [SampleData] = [
.init(startDate: Date.from(year: 2024, month: 3, day: 1, hour: 23, minute: 59),
endDate: Date.from(year: 2024, month: 3, day: 10),
name: "New York",
type: .city),
.init(startDate: Date.from(year: 2024, month: 3, day: 10, hour: 6),
endDate: Date.from(year: 2024, month: 3, day: 20),
name: "London",
type: .city),
.init(startDate: Date.from(year: 2024, month: 3, day: 20),
endDate: Date.from(year: 2024, month: 4, day: 10),
name: "Backcountry ABC",
type: .field),
.init(startDate: Date.from(year: 2024, month: 4, day: 10),
endDate: Date.from(year: 2024, month: 4, day: 20),
name: "Field DEF",
type: .field),
.init(startDate: Date.from(year: 2024, month: 4, day: 20),
endDate: Date.from(year: 2024, month: 5, day: 10),
name: "Wood 123",
type: .wood),
.init(startDate: Date.from(year: 2024, month: 5, day: 10),
endDate: Date.from(year: 2024, month: 5, day: 20),
name: "Paris",
type: .city),
.init(startDate: Date.from(year: 2024, month: 5, day: 20),
endDate: Date.from(year: 2024, month: 6, day: 5),
name: "Field GHI",
type: .field),
.init(startDate: Date.from(year: 2024, month: 6, day: 5),
endDate: Date.from(year: 2024, month: 6, day: 10),
name: "Wood 456",
type: .wood),
.init(startDate: Date.from(year: 2024, month: 6, day: 10),
endDate: Date(),
name: "Field JKL",
type: .field)
]
}
extension Date {
/**
Constructs a Date from a given year (Int). Use like `Date.from(year: 2020)`.
*/
static func from(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil) -> Date {
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = Calendar.current.date(from: components) else {
print(#function, "Failed to construct date. Returning current date.")
return Date()
}
return date
}
}
I'm building a Swift Chart that displays locations in a horizontal timeline. The chart is scrollable.
Unfortunately, when chartScrollPosition is offset by some amount (i.e. the user has scrolled the chart), changing chartXVisibleDomain results in chartScrollPosition jumping backwards by some amount.
This results in bad user experience.
A minimum reproducible example is below. To reproduce:
Run the code below
Using the picker, change chartXVisibleDomain. ThechartScrollPosition remains the same, as expected.
Scroll the chart on the horizontal axis.
Using the picker, change chartXVisibleDomain. ThechartScrollPosition changes unexpectedly.
You can verify this by watching the labels at the bottom of the chart. The chart simply ends up showing a different area of the domain. Tested on various iPhone and iPad simulators and physical devices, the issue appears everywhere.
Is this a SwiftUI bug, or am I doing something wrong?
struct ScrollableChartBug: View {
/// Sample data
let data = SampleData.samples
let startDate = SampleData.samples.first?.startDate ?? Date()
let endDate = Date()
/// Scroll position of the chart, expressed as Date along the x-axis.
@State var chartPosition: Date = SampleData.samples.first?.startDate ?? Date()
/// Sets the granularity of the shown view.
@State var visibleDomain: VisibleDomain = .year
var body: some View {
Chart(data, id: \.id) { element in
BarMark(xStart: .value("Start", element.startDate),
xEnd: .value("End", element.endDate),
yStart: 0,
yEnd: 50)
.foregroundStyle(by: .value("Type", element.type.rawValue))
.clipShape(.rect(cornerRadius: 8, style: .continuous))
}
.chartScrollableAxes(.horizontal) // enable scroll
.chartScrollPosition(x: $chartPosition) // track scroll offset
.chartXVisibleDomain(length: visibleDomain.seconds)
.chartXScale(domain: startDate...endDate)
.chartForegroundStyleScale { typeName in
// custom colors for bars and for legend
SampleDataType(rawValue: typeName)?.color ?? .clear
}
.chartXAxis {
AxisMarks(values: .stride(by: .month, count: 1)) { value in
if let date = value.as(Date.self) {
AxisValueLabel {
Text(date, format: .dateTime.year().month().day())
.bold()
}
AxisTick(length: .label)
}
}
}
.frame(height: 90)
.padding(.bottom, 40) // for overlay picker
.overlay {
Picker("", selection: $visibleDomain.animation()) {
ForEach(VisibleDomain.allCases) { variant in
Text(variant.label)
.tag(variant)
}
}
.pickerStyle(.segmented)
.frame(width: 240)
.padding(.trailing)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
} //: overlay
} //: body
} //: struct
// MARK: - Preview
#Preview {
ScrollableChartBug()
}
// MARK: - Data
enum SampleDataType: String, CaseIterable {
case city, wood, field
var color: Color {
switch self {
case .city:
.gray
case .wood:
.green
case .field:
.brown
}
}
var label: String {
switch self {
case .city:
"City"
case .wood:
"Wood"
case .field:
"Field"
}
}
}
enum VisibleDomain: Identifiable, CaseIterable {
case day
case week
case month
case year
var id: Int {
self.seconds
}
var seconds: Int {
switch self {
case .day:
3600 * 24 * 2
case .week:
3600 * 24 * 10
case .month:
3600 * 24 * 40
case .year:
3600 * 24 * 400
}
}
var label: String {
switch self {
case .day:
"Days"
case .week:
"Weeks"
case .month:
"Months"
case .year:
"Years"
}
}
}
struct SampleData: Identifiable {
let startDate: Date
let endDate: Date
let name: String
let type: SampleDataType
var id: String { name }
static let samples: [SampleData] = [
.init(startDate: Date.from(year: 2022, month: 3, day: 1),
endDate: Date.from(year: 2022, month: 3, day: 10),
name: "New York",
type: .city),
.init(startDate: Date.from(year: 2022, month: 3, day: 20, hour: 6),
endDate: Date.from(year: 2022, month: 5, day: 1),
name: "London",
type: .city),
.init(startDate: Date.from(year: 2022, month: 5, day: 4),
endDate: Date.from(year: 2022, month: 7, day: 5),
name: "Backcountry ABC",
type: .field),
.init(startDate: Date.from(year: 2022, month: 7, day: 5),
endDate: Date.from(year: 2022, month: 10, day: 10),
name: "Field DEF",
type: .field),
.init(startDate: Date.from(year: 2022, month: 10, day: 10),
endDate: Date.from(year: 2023, month: 2, day: 10),
name: "Wood 123",
type: .wood),
.init(startDate: Date.from(year: 2023, month: 2, day: 10),
endDate: Date.from(year: 2023, month: 3, day: 20),
name: "Paris",
type: .city),
.init(startDate: Date.from(year: 2023, month: 3, day: 21),
endDate: Date.from(year: 2023, month: 10, day: 5),
name: "Field GHI",
type: .field),
.init(startDate: Date.from(year: 2023, month: 10, day: 5),
endDate: Date.from(year: 2024, month: 3, day: 5),
name: "Wood 456",
type: .wood),
.init(startDate: Date.from(year: 2024, month: 3, day: 6),
endDate: Date(),
name: "Field JKL",
type: .field)
]
}
extension Date {
/**
Constructs a Date from a given year (Int). Use like `Date.from(year: 2020)`.
*/
static func from(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil) -> Date {
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = Calendar.current.date(from: components) else {
print(#function, "Failed to construct date. Returning current date.")
return Date()
}
return date
}
}
Swift 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.
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?
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
}
}
}
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()
}
}
}
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)
}
}
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.
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. 😢
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?
I tried to narrow down the y-axis and use the
clipped() to crop the excess. However, the clipped portion is too small, causing some of the chart to render above the x-axis. Is there any way to fix this, or any way to have the framework automatically set the y-axis range based on the data?
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.
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:
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.