I've seen some poor practices in example code for Apple's Charts Framework.
Not to point fingers... because I do know that may examples are designed to be as simple as possible to make one or two ideas shine. And that many times that example code has poor practices embedded because it is simpler and not pertanite to the centeral concept.
But have you been using someones example code and building up from that ... something slightly more complex when BAMB the results just look wrong. Well if this has happened to you in the new iOS Charts Framework by Apple - I can understand your pain. It has happened to me more than once. I've learned by reading and experimenting with these example code snippets. I've learned stuff I could never have reasoned from the Apple Docs. Yet, I've also seen some really poor understanding of the Charts API in usage... Case in point - the Chart Content body is a loop! It is an implied ForEach looping construct.
Because this Chart Content is a looping construct ... one sees lots of example code that uses the convenience constructor of Chart(). Nothing at all wrong with that.
But then use that convenience looping construct and apply a RuleMark - that's the BIG NO-NO!
A rule mark is generally a one-time mark on the chart... like the average yearly temperature in this example. So if we put that RuleMark in with the loop over each data item - as shown below. The rule mark will be drawn many times. And in some cases, you get a bit of fuzzy line/text that hints at the underlying problem.
Chart(weatherData) { weather in
Loops over all the data... but then plots the rule mark and its annotation 12 times (once for each month of data).
It is also a poor practice to do some type of calculation inside that loop. One does not need to compute the average temperture each time through that loop. Put that code outside of the Chart.
Source Code:
//
// BarChartOutterRange.swift
// LearnChartsFramework
//
// Created by David on 1/14/23.
//
import SwiftUI
import Charts
struct BarChartOutterRange: View {
// these may be @State properties for dynamic Apps
let weatherData = Weather.previewData
// do calculations ONCE - outside of the Chart loop
let yearlyAvg = Weather.previewData.map(\.average).reduce(0.0, +) / Double(Weather.previewData.count)
// properties for this specific style of Chart
let dashPattern: [CGFloat] = [10, 20]
let heatGradient = Gradient(colors: [.orange, .blue])
let avgLineColor = Color.black
let chartWidth: CGFloat = 400
let chartHeight: CGFloat = 500
let chartTempRange = 30...100
var body: some View {
VStack {
Chart {
ForEach(weatherData) { month in
BarMark(
x: .value("Date", month.name),
yStart: .value("Min Temp", month.lowTemp),
yEnd: .value("Max Temp", month.highTemp)
)
.foregroundStyle(heatGradient.opacity(0.8))
// Inner Bar - specific Temp
RectangleMark(x: .value("Month", month.name), y: .value("Monthly Avg", month.average))
.foregroundStyle(.blue.gradient)
.annotation {
Text("\(month.average.formatted())℉")
.font(.caption)
}
} // ForEach
// RuleMarks are ONLY drawn ONE time on chart; outside ForEach
RuleMark(y: .value("Average", yearlyAvg) )
.foregroundStyle(avgLineColor)
.lineStyle(StrokeStyle(lineWidth: 2, lineCap: .round, dash: dashPattern) )
// put this Text on top of dashed line to right
.annotation(position: .top, alignment: .trailing) {
Text("Yearly Average: \(yearlyAvg, format: .number.precision(.fractionLength(1)))℉ ")
.foregroundStyle(avgLineColor)
}
} // Chart()
.chartYScale(domain: chartTempRange)
.frame(width: chartWidth, height: chartHeight)
}.padding()
}
}
struct BarChartOutterRange_Previews: PreviewProvider {
static var previews: some View {
BarChartOutterRange()
}
}
//
// Weather_Data.swift
// LearnChartsFramework
//
// Created by David on 1/14/23.
//
import Foundation
struct Weather: Identifiable {
let id = UUID()
let name: String
let highTemp: Double
let lowTemp: Double
// a computed property - note it may have fractional values that will be delt with
var average: Double {
(highTemp + lowTemp) / 2
}
}
extension Weather {
static var previewData: [Weather] = [
.init(name: "Jan", highTemp: 76, lowTemp: 46),
.init(name: "Feb", highTemp: 73, lowTemp: 34),
.init(name: "Mar", highTemp: 78, lowTemp: 44),
.init(name: "Apr", highTemp: 85, lowTemp: 57),
.init(name: "May", highTemp: 83, lowTemp: 56),
.init(name: "Jun", highTemp: 90, lowTemp: 59),
.init(name: "Jul", highTemp: 88, lowTemp: 63),
.init(name: "Aug", highTemp: 96, lowTemp: 62),
.init(name: "Sep", highTemp: 87, lowTemp: 65),
.init(name: "Oct", highTemp: 83, lowTemp: 57),
.init(name: "Nov", highTemp: 78, lowTemp: 50),
.init(name: "Dec", highTemp: 67, lowTemp: 48),
]
}