Practice Good Charting Structure

What get's draw EACH time on the Chart?

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.

Poor Practice...

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),

]

}