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
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
53 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
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!
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
I'm trying to visualize some data using AreaMark of Swift Charts, however, different data structures seem to affect the result. Really confused here.
The following two example structs hold the same data, but in different ways:
import SwiftUI
import Charts
struct Food: Identifiable {
let name: String
let sales: Int
let day: Int
let id = UUID()
init(name: String, sales: Int, day: Int) {
self.name = name
self.sales = sales
self.day = day
}
}
struct Sales: Identifiable {
let day: Int
let burger: Int
let salad: Int
let steak: Int
let id = UUID()
var total: Int {
burger + salad + steak
}
init(day: Int, burger: Int, salad: Int, steak: Int) {
self.day = day
self.burger = burger
self.salad = salad
self.steak = steak
}
}
Now if I populate data and use Swift Charts to plot the data, the first struct works perfectly with all types of charts. However, the second struct, while works well with BarMark and PointMark, doesn't seem to work with AreaMark.
To reproduce, change "AreaMark" to "BarMark" or "PointMark" in the following code:
struct ExperimentView: View {
let cheeseburgerSalesByItem: [Food] = [
.init(name: "Burger", sales: 29, day: 1),
.init(name: "Salad", sales: 35, day: 1),
.init(name: "Steak", sales: 30, day: 1),
.init(name: "Burger", sales: 32, day: 2),
.init(name: "Salad", sales: 38, day: 2),
.init(name: "Steak", sales: 42, day: 2),
.init(name: "Burger", sales: 35, day: 3),
.init(name: "Salad", sales: 29, day: 3),
.init(name: "Steak", sales: 41, day: 3),
.init(name: "Burger", sales: 29, day: 4),
.init(name: "Salad", sales: 38, day: 4),
.init(name: "Steak", sales: 39, day: 4),
.init(name: "Burger", sales: 43, day: 5),
.init(name: "Salad", sales: 42, day: 5),
.init(name: "Steak", sales: 30, day: 5),
.init(name: "Burger", sales: 45, day: 6),
.init(name: "Salad", sales: 39, day: 6),
.init(name: "Steak", sales: 31, day: 6),
.init(name: "Burger", sales: 37, day: 7),
.init(name: "Salad", sales: 35, day: 7),
.init(name: "Steak", sales: 30, day: 7),
]
let cheeseburgerSalesByDay: [Sales] = [
.init(day: 1, burger: 29, salad: 35, steak: 30),
.init(day: 2, burger: 32, salad: 38, steak: 42),
.init(day: 3, burger: 35, salad: 29, steak: 41),
.init(day: 4, burger: 29, salad: 38, steak: 39),
.init(day: 5, burger: 43, salad: 42, steak: 30),
.init(day: 6, burger: 45, salad: 39, steak: 31),
.init(day: 7, burger: 37, salad: 35, steak: 30)
]
var body: some View {
VStack {
Chart(cheeseburgerSalesByItem) { sale in
AreaMark(
x: .value("Day", sale.day),
y: .value("Sales", sale.sales)
)
.foregroundStyle(by: .value("Food Item", sale.name))
}
.chartXScale(domain: 1...7)
.padding()
Spacer()
Chart(cheeseburgerSalesByDay) { sale in
AreaMark(
x: .value("Day", sale.day),
y: .value("Burger", sale.burger)
)
.foregroundStyle(.brown)
AreaMark(
x: .value("Day", sale.day),
y: .value("Salad", sale.salad)
)
.foregroundStyle(.green)
AreaMark(
x: .value("Day", sale.day),
y: .value("Steak", sale.steak)
)
.foregroundStyle(.red)
}
.padding()
}
}
}
If two of the three AreaMarks are commented out, the left one would work. Just more than one AreaMark plots don't work for this struct.
So what is the problem here? What to do if I want to use the second structure and plot a AreaMark chart?
Hi, I'm making use of iOS17 Charts and getting data from Core Data.
Chart {
ForEach(weightContext, id: \.timestamp) { series in
LineMark(
x: .value("Day", series.timestamp!, unit: .day),
y: .value("Measurement", WeightFunctions.weightConversions(weightValue: series.value, metric: selectedWeight))
)
PointMark(
x: .value("Day", series.timestamp!, unit: .day),
y: .value("Measurement", WeightFunctions.weightConversions(weightValue: series.value, metric: selectedWeight))
)
}
}
.chartYScale(domain: lowestValue...highestValue)
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: xChartVisible)
.chartScrollPosition(x: $xScrollPosition)
.chartScrollPosition(initialX: xInitialPosition)
// .chartXVisibleDomain(length: xChartVisible)
.chartXScale(domain: startDate...endDate)
I've linked the .chartXVisibleDomain(length: xChartVisible) to a Picker which changes the length to show month, quarter, half year, year:
length = 3600 * 24 * 30, length = 3600 * 24 * 90 etc.
Each time the xChartVisible changes the chart sometimes stays in the right area if I'm at the end of the x axis, but otherwise moves out of the view. I've noticed the $xScrollPosition number stays exactly the same, even though the visibility has changed but not sure what to do about that.
.onAppear {
xInitialPosition = weightPeriodFunc.initialScrollDate
xScrollPosition = weightPeriodFunc.initialScrollDate.timeIntervalSinceReferenceDate
xChartVisible = weightPeriodFunc.length
}
.onChange(of: weightPeriod) { newValue in
xChartVisible = weightPeriodFunc.length
xScrollPosition = newPeriodStartDate.timeIntervalSinceReferenceDate
}
I've set the xScrollPosition as a TimerInterval as I'm also getting the dates from it's location to provide date information above the chart.
@State private var xChartVisible : Int = 3600 * 24 * 90
@State private var xScrollPosition : TimeInterval = TimeInterval()
@State private var xInitialPosition : Date = Date()
@State private var newPeriodStartDate : Date = Date()
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!
Hello,
When I input Data 1 into SectorMark and then switch to Data 2, my application crashes. Any suggestions for resolving this issue?
Data 1
[ReadingLog.PieChartData(id: "E24A2F4F-5A80-4734-8497-1AE33EF4F007", hour: 4.3, category: "biography"), ReadingLog.PieChartData(id: "710C328D-0B58-4329-A3C1-66CC42A9C602", hour: 0.75, category: "philosophy"), ReadingLog.PieChartData(id: "37F0F9CE-7144-4B78-99C8-921292F6E730", hour: 0.17, category: "novel")]
Data 2
[ReadingLog.PieChartData(id: "E24A2F4F-5A80-4734-8497-1AE33EF4F007", hour: 6.3, category: "biography")]
Error Message:
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x0000000216287cb8
Termination Reason: SIGNAL 5 Trace/BPT trap: 5
Terminating Process: exc handler [1978]
Triggered by Thread: 0
Kernel Triage:
VM - (arg = 0x3) mach_vm_allocate_kernel failed within call to vm_map_enter
VM - (arg = 0x3) mach_vm_allocate_kernel failed within call to vm_map_enter
VM - (arg = 0x3) mach_vm_allocate_kernel failed within call to vm_map_enter
VM - (arg = 0x3) mach_vm_allocate_kernel failed within call to vm_map_enter
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed
Hi
I am trying to build a pie chart with SwiftUI. I have created one with a dark background, and it seems like labels that correspond to each sector (labels are of black color) are not visible. It would be better to switch these labels' foreground color to white, but I don't see any suitable method to modify this. I tried both chartYAxis and chartXAxis (they worked for BarChart), but the color didn't change. Also, I added a separate struct that conforms to LabelStyle and defines makeBody in the following way
struct WhiteLabelStyle : LabelStyle {
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title.foregroundColor(.white)
} icon: {
configuration.icon
}
}
}
However, that also doesn't change color of labels on a chart.
Below the complete code of the view:
ZStack {
CommonConstants.defaultBackground
Chart(data, id: \.name) { name, sales in
SectorMark(angle: .value("Value", sales))
.foregroundStyle(by: .value("Product category", name))
}
.labelStyle(WhiteLabelStyle())
}
Can you suggest any ways to manipulate with a chart comprised of SectorMarks
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
}
}
}
I'm doing a dead simple bar chart. It will show one bar per hour for a few days. The bar chart seems to have a bug where it will overlap the bars as soon as the overall width of the chart reaches some hardcoded value. The chart has .chartScrollableAxes(.horizontal) so horizontal space is no issue, there's infinite amounts of it.
This screenshot shows the same content, but the bottom one has 25pt wide bars and the top one 16pt. 16 is the last width before they started overlapping.
To test play with numberOfValues as well as the fixed widths for the BarMark:s. Under no circumstances should the bars overlap unless I tell it to, it should use some configurable minimum spacing between bars. In my real case I do not artificially color the bars like this and the chart is really hard to read.
I've tried to look in the docs, but most modifiers are totally undocumented and I can't seem to find anything that would apply. By setting .chartXVisibleDomain to some really low value I can force it to spread the bars out more, but then the my bar width is not honoured.
import SwiftUI
import Charts
struct Value: Hashable {
let x: Int
let y: Int
}
struct ContentView: View {
let values: [Value]
let colors: [Color] = [.red, .green, .blue]
var body: some View {
VStack {
Chart {
ForEach(values, id: \.self) { value in
BarMark(
x: .value("X", value.x),
y: .value("Y", value.y),
width: .fixed(16)
)
.foregroundStyle(colors[value.x % colors.count])
}
}
.chartScrollableAxes(.horizontal)
Chart {
ForEach(values, id: \.self) { value in
BarMark(
x: .value("X", value.x),
y: .value("Y", value.y),
width: .fixed(25)
)
.foregroundStyle(colors[value.x % colors.count])
}
}
.chartScrollableAxes(.horizontal)
}
.padding()
}
}
#Preview {
var rnd = SystemRandomNumberGenerator()
let numberOfValues = 50
var values: [Value] = []
for index in 0 ..< numberOfValues {
values.append(Value(x: index, y: Int(rnd.next() % 50)))
}
return ContentView(values: values)
}
Example with bars the same color. It's pretty much unusable.
I'm seeing an issue related to Swift Charts when I use a @Binding to dynamically change the selection of the chart. Below is a quick example View that demonstrates the issue. This example is a chart that shows a count of events that happened in a day, and the user can touch a bar to reveal the count as an annotation above the bar.
The expected behavior is that as the user can touch a bar and see the annotation appear above the bar. In some cases, the chart scale may need to change to allow space for the annotation, which is expected.
However, unexpectedly, the whole chart's width changes unexpectedly by a few points, sometimes, when touching a bar. It would be fine if this was consistent with times when the updated scale needs to include an additional digit, however even when that's not the case the whole chart's width seems to change by a few points, and then inconsistently sometimes change back (or not) with subsequent touches.
This behavior is not present in all cases, so you may need to run the preview a few times to get data where it's reproducible.
Is there something I can do here to fix the issue? Or is it just a bug in Swift Charts?
import SwiftUI
import Charts
struct ContentView: View {
var body: some View {
VStack {
WeekHistogram()
.frame(maxHeight: 180)
}
.padding()
}
}
#Preview {
ContentView()
}
// A simple struct to contain the data for the chart.
public struct EventCount: Identifiable
{
/// Start date of a day
let date: Date
/// Counts of events on that day
public let count: Int
/// The ID: date stored as a string
public var id: String { date.ISO8601Format() }
// Storing a Date as the ID changes how Swift Charts lays them out along the axis to an undesired way.
init(day: Date, count: Int)
{
self.date = day
self.count = count
}
}
struct WeekHistogram: View
{
// Dummy data that gets refreshed every time you run the preview
private let countByDay: [EventCount] = EventCount.dummyData
// Used to extract the date back from the EventCount.id
private let formatter = ISO8601DateFormatter()
// The currently selected bar (while user is touching the bar)
@State private var selection: String? = nil
var body: some View
{
Chart(countByDay)
{ element in
// X-axis: Date of the event count
// Y-axis: Count of events on that date
BarMark(
x: .value("Day", element.id),
y: .value("Count", element.count)
)
.annotation(position: .top)
{
// Only show the annotation when this bar is being touched
if element.id == selection
{
Text("\(element.count)")
.font(.headline)
}
}
}
.chartXSelection(value: $selection)
.chartXAxis
{
// Custom view to show the weekday and day of the month
// Removing this custom .chartXAxis does not fix the issue
AxisMarks { value in
// Convert the text of the date back to an actual date
let date = formatter.date(from: value.as(String.self)!)!
AxisValueLabel
{
VStack
{
Text(date.formatted(.dateTime.weekday()))
Text(date.formatted(.dateTime.day()))
.font(.headline)
}
}
}
}
}
}
// Generate dummy data
extension EventCount
{
static let dummyData: [EventCount] =
{
let startOfToday = Calendar.current.startOfDay(for: .now)
let secondsPerDay = 24 * 60 * 60
// Generate [EventCount] with a random count from 3–8 for toady and each of the past 7 days.
var counts = [EventCount]()
for i in 0...7
{
let day = startOfToday
.addingTimeInterval(TimeInterval(-i * secondsPerDay))
let count = Int.random(in: 3...8)
let eventCount = EventCount(day: day,
count: count)
counts.append(eventCount)
}
// Reverse the direction to order it for the chart
counts.reverse()
return counts
}()
}
I've been trying to reproduce the example used in the WWDC 23 Presentation "Explore Pit Charts and Interactivity in SwiftCharts" where a popover annotation is set on top of the chart and vertical; RuleMark. However when doing so the annotation doesn't appear at all.
I worked around that issue by setting: y: .fit(to: .chart) in the init of the overflowResolution, like:
.annotation(position: .top, spacing: 0, overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart)))
Probably a SwiftUI bug given this API is only a few months old. If anyone has been able to reproduce that example let me know!
I'm currently evaluating Swift Charts to use in my macOS app, where I need to (potentially) display a few millions of data points, ideally all of them at one time. I want to give users the possibility to zoom in & out, so the entire range of values could be displayed at one moment.
However, starting at around 20K data points (on my computer), the Chart takes a little bit to set up, but the window resizing is laggy. The performance seems to decrease linearly (?), when dealing with 100K data points you can barely resize the window and the Chart setup/creation is noticeable enough. Dealing with 500K data points is out of the question, the app is pretty much not useable.
So I'm wondering if anybody else had a similar issue and what can be done? Is there any "magic" Swift Charts setting that could improve the performance? I have a "data decimation" algorithm, and given no choice I will use it, but somehow I was hoping for Swift Charts to gracefully handle at least 100K data points (there are other libs which do this!). Also, limiting the displayed data range is out of the question for my case, this is a crucial feature of the app.
Here's the code that I'm using, but it's the most basic one:
struct DataPoint: Identifiable {
var id: Double { Double(xValue) }
let xValue: Int
let yValue: Double
}
let dataPoints: [DataPoint] = (0..<100_000).map { DataPoint(xValue: $0, yValue: Double($0)) }
struct MyChart: View {
var body: some View {
Chart(dataPoints) { dataPoint in
PointMark(x: .value("Index", dataPoint.xValue),
y: .value("Y Value", dataPoint.yValue))
}
}
}
Some additional info, if it helps:
The Chart is included in a AppKit window via NSHostingController (in my sample project the window contains nothing but the chart)
The computer is a MacBook Pro, 2019 and is running macOS 10.14