Swift Charts API
New in iOS 16
Pie Charts in iOS17
It is a bit weird that the iOS 17 update to Charts Framework introduces the simplest graphic - the Pie Chart. But - Yes! In Xcode 15 we get the Pie Chart. And it makes a wonderful intro to the powerful Charting Framework. See Stewart Lynch's YouTube Example code to learn the Charting framework. And just know the framework goes much deeper than a donut hole.
See Also:
How to Modify Swift Charts - a lot of code examples that might remind me of how to change some of the chart diagrams.
Outstanding Questions I've had on the Charts API. Feel free to solve these for me.
Practicing good Chart Structure
Read the FINE Manual - Charts API is difficult.
Learning to Read the Doc for AxisValueLabel - Example.
Excited to see the Companion for SwiftUI v 4.1 has the Charts API with code examples! Just what I need to start exploring the new Charts features. I've tried and tried to understand the documented syntax of Apple Docs... but I think that there is just too much implied but unstated verbiage in those terse document pages. What will really help me is a good old fashion working example. I can follow and understand an example.
A realization just struck me - I've got to upgrade my MacBook Pro (2016)... it will not run Ventura, which the Companion needs to run LIVE Examples. It just got deprecated to the used hardware shelf - so sad. And expensive...
Here's a scheme... buy an Apple TV with the A15 chip - grab some open source Compute Server -> install on Apple TV and off load my MacBook to the TV... for $150!
I'm really excited to see the new Charts API - a declaritive framework for displaying data in various charts, and interactive elements just like one would expect with a SwiftUI look & feel.
I'm dissapointed in the developer documentation - poor and hardly usefull appears to be par for the course... with such a visual context a few pictures would go a long way toward explaination, Apple.
https://developer.apple.com/documentation/Charts
As a case in point - suppose you are wanting to make a simple bar chart of your store's monthly sells, and your goal is to see which item sells best. One would typically make a monthly bar chart grouped by product sells. Search the documentation for "grouped by" ... you will get lots of hits. And you can chase that rabbit into lots of reading holes... but you are not likely to find the info you want.
The code below has the magic... and it's not "grouped" anything... you are searching for the Position modifier. The image on the right "Grouped Bar Chart" is the result of this code. Reading the (Beta) developer documentation will most likely leave you wondering if "position" is the modifier you are searching for. An image on that page would do wonders toward understanding.
struct groupedBarView: View {
var body: some View {
VStack {
Text("Grouped Bar Chart")
Chart {
ForEach(quarterSales) { product in
BarMark(x: .value("name", product.name), y: .value("sales", product.count))
.foregroundStyle(by: .value("month", product.month))
.position(by: .value("month", product.month))
}
}
}
.padding()
}
}
I can forsee a wonderful book by Big Mountain Studio for Charts - Updated for iOS 16 SwiftUI View Mastery.
See: Apple's WWDC videos: Hello Swift Charts from WWDC 2022 and Swift Charts - Raise the bar
Apple's Chart documentation example starts with toy shapes. Here is that example's code.
I wish they would get their color names straight - that's not a PINK cube. I tried to change the "Pink" to "Red" string within the dictionary of the chartForegroundStyleScale but got an interesting error... pondering - try it and see if you get: "ToyShapesCharts crashed due to implicitly unwrapped optional in ConcreteScale+Discrete.swift at line 102."
//
// ToyShapesChartsApp.swift
// ToyShapesCharts
//
// Created by David on 7/2/22.
//
// Implementation of Apple's Charts Documentation Example
// see: https://developer.apple.com/documentation/charts/creating-a-chart-using-swift-charts
import SwiftUI
import Charts
struct ToyShape: Identifiable {
var color: String
var type: String
var count: Double
var id = UUID()
}
var stackedBarData: [ToyShape] = [
.init(color: "Green", type: "Cube", count: 2),
.init(color: "Green", type: "Sphere", count: 0),
.init(color: "Green", type: "Pyramid", count: 1),
.init(color: "Purple", type: "Cube", count: 1),
.init(color: "Purple", type: "Sphere", count: 1),
.init(color: "Purple", type: "Pyramid", count: 1),
.init(color: "Pink", type: "Cube", count: 1),
.init(color: "Pink", type: "Sphere", count: 2),
.init(color: "Pink", type: "Pyramid", count: 0),
.init(color: "Yellow", type: "Cube", count: 1),
.init(color: "Yellow", type: "Sphere", count: 1),
.init(color: "Yellow", type: "Pyramid", count: 2)
]
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "chart.bar.xaxis")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, Charts!")
Chart {
ForEach(stackedBarData) { shape in
BarMark(
x: .value("Shape Type", shape.type),
y: .value("Total Count", shape.count)
)
.foregroundStyle(by: .value("Shape Color", shape.color))
}
}
.chartForegroundStyleScale([
"Green": .green, "Purple": .purple, "Pink": .pink, "Yellow": .yellow
])
Image("ToyShapes")
.resizable().aspectRatio(contentMode: .fit)
.frame(width: 250)
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@main
struct ToyShapesChartsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Apple has two great introduction videos in WWDC 2022.
I found that by default the Chart does NOT clip the marks to the assumed plot area of the chart. So I had to go looking for a way to turn on this behavior. It is the Chart modifier:
.clipped(antialiased: true)
I didn't like that it clipped the Y axis label text at the top. But it works. There may be a use of padding() modifier that would help with this.
Swift Charts Framework for iOS16+
A cheet sheet of useful constructs
How will I test the Charts?
A really great Example
If you are looking for a real-world example; then checkout Stewart Lynch's Charts Framework 2 - Visualizing Large Data Sets with Bar Charts
He teaches by _crafting_ the example code - you come away with a greater understanding of why the code is written this way. As apposed to an instructor that just types out the pre-written example code on a video. Compare his teaching style to others - if you are like me, it is a huge difference!
SW Dev Notes
Another great example page on iOS Charts is https://swdevnotes.com/swift/2022/customise-a-line-chart-with-swiftui-charts-in-ios-16/
The examples are well illustrated and the Chart code is well written.
Simplest Thing that
Could Possibly Work
Could Possibly Work
With SwiftUI - there are many ways to resolve an issue - the real trick is finding the Simplest Way. For example the poor text wrapping on the "Microsoft" strings over there. OUCH! It hurts the eyes...
What's the Simplest solution? It appears the graphic is getting the priority when horizontal spacing is concerned. This is bound to happen alot in the future. I could reduce the chart size... but that seems to be only a fix on this instance.
Font resizing is an option in SwiftUI. But what modifiers to use?
I think an option is to use font rescaling, so finding some good articles on those options I found this https://malauch.com/posts/auto-resizable-text-size-in-swiftui/
This is a whole sub-discilpline of font details I don't care to learn.
Maybe:
.lineLimit(1)
.minimumScaleFactor(0.01)
.truncationMode(.tail)
Note the variable font size on the Watch List company name.
Next fix the currency displayed value.
Learning about Chart AxisMarks
OK ... I'd call them Axis Labels... but I guess theres lots of value in labeling the Marks on a Chart as... AxisMarks. Now getting the labels (marks) to jump up into the plotting area ... that's a term called "preset" and the value of preset should be "inset" - I've learned to reverse engeneer the Apple Docs thru pure GRIT.
This code example is Eric Callanan found on his GitHub account. I managed to modify just enough to Inset the AxisMarks ChartXAxis. It took multiple attempts; but having some working code to start from is priceless.
Weekly Charts
I've substituted the Weekly Chart in for the Daily Sparkline chart & fetching the data now takes 20-40 seconds for 12 stocks. I've heard that constantly monitoring an app to know when to optimize code is a smart dev practice. My first attempt with this was on a Sunday and I killed the simulator right as it was refreshing - I'd assumed the worst, in reality it was working as fast as it could go. Not weekend related... but just wait.
To save time... I will configure up some preview data to experiment with the Weekly Charts.
An objective of that swap (Weekly for Daily charts) was to display the ChartXAxis AxisMarks in the plotting area via the "preset" of "inset" as learned above.
It is obvious reading the Apple Docs that I want to manipulate the AxisValueLabels struct. But go read that part of the Docs and tell me how??? This is where I need an example - ok Apple?
Well one attempt is seen to the right - just getting labels for "T", "M", "F", "W" - wonder what the heck is going on there. I'm still learning...
Hmm... incrementally better after a whole afternoon of coding and learning - with ONE Apple Feedback report.
Cannot get full day names from Calendar().standaloneWeekdaySymbols
FB11335975
Plotting the single letter values of day Names results in aggration of "S" (Sat & Sun) & "T" (Tue & Thur) along the X-axis. I tried to use the full day name - but got caught up in a bug/or understanding vortex.
Now with 3 letter abbreviations, I get the 7 days but my data has 8 point values - so the wrapping continues. Yes - if my data only contained the 7 days of a week... but what will happen when I move to a Month view? Wrapping! Also the 3 letters overflow the room for the day columns. One tiny step forward... three to the left...
Putting the day 3 letter abbrevation in my data - seems to help... some.
A 5 day Chart
Still pondering how to draw the 5 day chart for a stock. Something is fishy about those charts... in a downward trending market, they seem so optimistic. The Yahoo Financial 5Day AAPL Chart below for reference. I wonder if I've sorted it the wrong way? And only 4 days?
Yeap! Swaping the sort order and removing the Date from the X-axis and I get a chart that is similar to the reference from Yahoo.
Human Interface Guidelines
I saw that Apple updated the HIG Docs with blurbs on Charts. Take a look.
I've found some interesting... questions that I ponder...
How did they create those beautiful charts? What do they mean data....
Where did the "PM" in that X-Axis Value Label come from?
Pictures++
The first dozen times I've read the Apple Docs for Charts Framework in iOS 16. I came away with a big misconception about what the axis labels were. With no visual guide I thought the modifier .chartXAxisLabel was setting the text for the axis that would decorate the tick marks on the axis. WRONG.
With one well written example with code and a vizual Mark from BigMountainStudio has me straigntened out. There are multiple general places an axis label might appear - but none were what I thought I was labeling.
I'm still very unsure how to specify the tick-mark labels that I need to set on the Stock chart above - but I'm learning. And making progress now.
Apple states (rather curtly) AxisValueLabel
"A label that describes the value for an axis mark."
I'm sure someone can read into that definition, that the Axis Label floats around all over the plot and does NOT attach itself to any ONE Value.
That AxisValueLabel is a structure - not an instance method like the chartXAxisLabel() method. It is the method that I'm using in the code.
I'm trying to add some X-Axis Grid Lines... now I'm searching Mark's book for that topic. It doesn't seem he covers that detail. I'm stuck with the Apple Docs.
"A line that a chart draws across its plot area to indicate a reference point along a particular axis."
I'm attempting to change the number of grid lines on my chart. I did this some many example/tutorial programs ago. Can not remember the trick. So I have been searching the Docs...
Maybe I've found it... deep deep into an initializer for:
"Describes the values the axis markers will present (one for each value)."
It has two methods with keyword "stride".
static func stride<P>(by: P, roundLowerBound: Bool?, roundUpperBound: Bool?) -> AxisMarkValues
Creates values with the given number step.
One does strides by calendar components (dates and time) the other looks promising if I can decpipher the meaning and usage.
Standby as I parse this method. The bounds are optional booleans - I can most likely leave them blank. The by parameter is a generic P - that's not very helpful.
Clicking into the description I get the Declaration which tells me the P is a BinaryFloatingPoint - OK what could that be... reading that Doc page... I've got nothing...
Let's just try something and see if it works out.
Since I'm dealing with a plot in X-Y space the word Point denotes an X-Y two dimensional concept. Yet the adjective Floating... before Point denotes a decimal value (floating-point number). Placing another adjective Binary in front really throws me for a loop. Maybe I'm overthinking this... is it two values or one... is it a boolean (binary) or a floating point number... definitely over-thinking... let's pass in a 5 and see if that works - let the compiler decide.
Well, this is starting out very poorly. One cannot initialize the structure - it's all static methods. Oh... well!
The STRIDER trick...
I don't know how I found an answer to my problem. Not sure this is an answer. Yet... But it's feeling Swifty and seems to pass the judgment of the compiler.
.chartXAxis {
AxisMarks(values: .stride(by: .hour, count: 24) ) { _ in
AxisGridLine(centered: true, stroke: StrokeStyle(dash: [1, 2, 4]))
.foregroundStyle(Color.indigo)
AxisTick(stroke: StrokeStyle(lineWidth: 2))
.foregroundStyle(Color.mint)
AxisValueLabel(format: .dateTime.weekday(.narrow).hour(), centered: false)
}
}
OK - some notes on that. Because the AxisMarkValues structure is hidden down in some code shorthand notation.
I think this is being used via that AxisMarks(values: .stride( )) call. I could not parse this from the documentation - I need an example. So I found this code that uses "stride" on 'Mobile.blog Mobile development at Automattic' in article: An Adventure with Swift Charts by Huong Do July 4, 2022. I often look at the publication dates when searching for Swift Charts info... if it is earlier than June of 2022 - it is typically about some other framework.
.chartXAxis {
AxisMarks(values: .stride(by: xAxisStride, count: xAxisStrideCount)) { date in
AxisValueLabel(format: xAxisLabelFormatStyle(for: date.as(Date.self) ?? Date()))
}
}
There are a few computed properties in that (xAxisStride, xAxisStrideCount) these resolve to .day and int 5. Which compares well to one of the AxisMarkValues static functions that return AxisMarkValues.
OK... but it did NOT work!
Well, that was really good in theory and passed the compiler's judgy complaints. But all in all it didn't function. See over there - no X-axis values/labels/text (whatever Apple wants to call it).
I had this problem before and it was caused by .chartXAxis(.hidden) line of code that I checked - commented out now.
While we are here - if you want to limit your Y-axis to a range like this:
.chartYScale(domain: priceMin...priceMax)
Also notice whatever I've done with the grid lines has removed all - even the Y-axis grid lines.
This code will get back the axis labels for Days and Hour... but no grid lines!
.chartXAxis {
AxisMarks { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.narrow).hour(), centered: false)
//AxisValueLabel(format: .dateTime.month().day().hour(), centered: false)
}
}
Sean Allen does Charts
YouTube channel https://www.youtube.com/watch?v=4utsyqhnS4g
SwiftUI Bar Chart with Customizations
It is a quick high-level intro, but he covers a few of the common APIs that I wanted to know how to use.
Adding a border to the chart
I've found that adding the typical border (which is inset) leaves just touches of color bleeding out behind the border. See those screen shots over there.
Digging into the Apple Docs I found a nice comment that the solution is to add padding to the Plot with the same width as the border line. And this works.
Plot GridLines Blue
I wanted to see the grid lines in my chart a bit better - so make them blue. This is done in the .foregroundStyle() modifier.
Jump to selected ScrollView item.
How do I make the selected chart duration show up in the ScrollView when the Chart reappears on the screen? I found several examples but they didn't appear to work in my case... hmm, what is wrong here... I was not making my items identifiable. I needed to specify the id parameter in ForEach. Then use the proxy.scrollTo() in onAppear().
What I learned... the short answer...
Adding the id to the ForEach so that each item in the list had a unique ID (in my instance this is the Enum that allRangeTypes array is derived from.
struct DateRangePickerView: View {
let allRangeTypes = ChartRange.allCases
@Binding var selectedRange: ChartRange
I want to investigate the @Namespace trick used in some of the examples I saw... but didn't work out for my needs.
Search for the ScrollViewReader thats the start down the rabbit hole.
References:
https://sarunw.com/posts/how-to-use-scrollview-in-swiftui/
https://www.hackingwithswift.com/quick-start/swiftui/how-to-make-a-scroll-view-move-to-a-location-using-scrollviewreader
https://nilcoalescing.com/blog/ScrollToNewlyAddedItemUsingScrollViewReaderAndOnChangeModifier/
https://www.midnightswift.com/posts/tags/scroll-view?sk=scrollviewreader
It has been a few weeks and I've forgotten most of the Charts API that I thought I should remember. I guess I have not practice (with positive returns) enough. Today I'm struggling again with the chart axis labels.
Looking at some of the example code it appears one should be able to write:
Chart { ... }
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 5)) { _ in
AxisGridLine()
AxisTick()
AxisValueLabel("Quarters.", position: .leading, alignment: .center, spacing: 8)
}
}
But Xcode has 4 errors for the above code - which looks very similar to example code I've copied and used before. The most obvious error and one Xcode is willing to fix is: Incorrect argument labels in call (have 'values:_:', expected 'format:preset:position:values:stroke:') for AxisMarks().
So why does the compiler allow AxisMarks(values: ...) in one instance and complain in the other?
I cannot figure out the docs meaning... but after reading all the init methods of AxisMarks - I feel this issue may have to do with:
init<Value>(preset: AxisMarkPreset, position: AxisMarkPosition, values: [Value], stroke: StrokeStyle?)
Can someone tell me more... why does this init sometimes get called... (what does it mean to have the generic Value?
I've also now seen this idiom:
.chartXAxis(content: {
AxisMarks(position: .top)
})
StrokeStyle dashPhase
The Companion for SwiftUI helped me to understand the dashPhase parameter to the StrokeStyle. I'm not sure I could put it into words but the interactive tooling allows me to intuitively understand the concept of a phase.
I think it may be a 0 - 1 percentage of the dash pattern specified. Where 0 means the line will start at the very beginning of the dash pattern, whereas 0.5 means it will start 50% of the way into the dash pattern.
Much easier to see - than to explain.
Combining Charts
A nice article on multiple charts by Danijela Vrzan
Combine charts to create stunning designs with Swift Charts
The overall chart consist of:
Line Chart
Line mark symbols
Area Chart
with Gradient color
Custom X Axis
Custom Y Axis scale
Rule Mark