I have a problem with generating data for a chart view in SwiftUI.
In my app I have counter data, here is an example of a data series:
The data consists of the pair "Date - Value":
30.09.22 - 712
31.10.22 - 716
30.11.22 - 721
31.12.22 - 726
31.01.23 - 730
28.02.23 - 734
31.03.23 - 739
18.04.23 - 742
30.04.23 - 744
31.05.23 - 747
30.06.23 - 749
31.07.23 - 750
31.08.23 - 752
30.9.23 - 754
31.10.23 - 757
From this data, the data for a chart in the form "Month - Value" is now to be generated for a year selected by the user.
The value for the month results from the difference from month to month. Based on the above data, the following chart data should be generated should be generated:
Year 2022:
10 - 4
11 - 5
12 - 5
Year 2023:
01 - 4
02 - 4
03 - 5
04 - 5
05 - 3
06 -2
07 - 1
08 - 2
09 - 2
10 - 3
From my point of view, the following special features must be taken into account:
- one or more entries per month may exist. Only the last entry per month may be used for the difference calculation.
- the first month in the entire data, based on the above data, is 09/2022 may not be displayed in the chart, as I cannot calculate a difference for this month.
- for the first month of the year (01 - January), the difference must be calculated with the last month of the previous year, as long as there is a previous year. If there is no previous year, then it is the first month in the entire data series and point 2 applies.
Based on the above requirements, I have created the following SwiftUI ChartView. The display in the chart also works so far, except for the problem from point 3 above.
In my implementation, the month 01 is not displayed in the above example data series for the year 2023. In my opinion, this is because I have implemented point 2,
But without correctly taking point 3 into account.
I have no idea how to implement this and am grateful for any ideas, because I have already spent some time testing and researching, so far without success, so I now hope that there is one or more clever minds here in the community who can help me.
Here is my current implementation of the ChartView:
import SwiftUI
import Charts
struct ChartView: View {
var meterInfo: String
var meterNumber: MeterNumber
var unit: UnitTypes
@ObservedObject var readings: MeterReadings
@State private var selectedYear: String = "2023" // Startjahr
var sortedItems: [MeterReadingItem] {
readings.items.sorted()
}
// var data: [ChartItem] = [
// .init(month: "09", kwhperMonth: 245),
// .init(month: "10", kwhperMonth: 198),
// .init(month: "11", kwhperMonth: 267),
// .init(month: "12", kwhperMonth: 287),
// .init(month: "01", kwhperMonth: 195),
// .init(month: "02", kwhperMonth: 178)
// ]
var data: [ChartItem] {
return createChartData(sortedItem: sortedItems.filter({$0.meterNumber == meterNumber.rawValue}), selectedYear: selectedYear)
}
var period: String {
return createPeriod(sortedItem: sortedItems.filter({$0.meterNumber == meterNumber.rawValue}))
}
var valueForPeriod: String {
let value = createValueforPeriod(sortedItem: sortedItems.filter({$0.meterNumber == meterNumber.rawValue}))
if value == "0" {
return "keine Daten"
} else {
return value
}
}
var body: some View {
GeometryReader { geometry in
VStack {
VStack {
Text("Zähler \(meterNumber.rawValue)")
.titleStyle()
.foregroundColor(.primary)
Text(meterInfo)
.font(.caption)
.foregroundColor(.primary.opacity(0.5))
HStack {
Text("Verbrauch im Zeitraum:")
.font(.caption)
Text(valueForPeriod)
.subHeadlineStyle()
Text(unit.rawValue)
.subHeadlineStyle()
}
.padding(.vertical, 2)
HStack {
Text("Effektiver Verbrauch (\(unit.rawValue))")
.font(.caption)
.foregroundColor(.primary.opacity(0.5))
.padding(.top, 1)
// Auswahl des Jahres
Picker("Jahr", selection: $selectedYear) {
ForEach(uniqueYears(from: sortedItems), id: \.self) { year in
Text(year)
}
}
}
}
Chart {
ForEach(data) { value in
LineMark(x: .value("Monat", value.month),
y: .value("Value", value.value))
}
}
.frame(maxWidth: geometry.size.width * 1)
.padding([.vertical,.horizontal])
Text("Zeitraum: \(period)")
.font(.caption)
.foregroundColor(.primary.opacity(0.5))
}
}
}
// Function for extracting unique years from the data
func uniqueYears(from data: [MeterReadingItem]) -> [String] {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy"
let years = data.map { dateFormatter.string(from: $0.date) }
return Array(Set(years))
}
func createValueforPeriod(sortedItem: [MeterReadingItem]) -> String {
var value: Int = 0
if let valueFirst = sortedItem.first?.value {
if let valueLast = sortedItem.last?.value {
value = valueFirst - valueLast
}
}
return "\(value)"
}
func createPeriod(sortedItem: [MeterReadingItem]) -> String {
let startDate: String = sortedItem.last?.date.formatted(date: .numeric, time: .omitted) ?? "keine Daten"
let endDate: String = sortedItem.first?.date.formatted(date: .numeric, time: .omitted) ?? "keine Daten"
return "\(startDate) - \(endDate)"
}
func createChartData(sortedItem: [MeterReadingItem], selectedYear: String) -> [ChartItem] {
var data: [ChartItem] = []
var valueArray: [Int] = []
var dateArray: [Date] = []
var highestDatesByMonth: [Int: Date] = [:]
// Invert the array for easier processing
var reversedArray = Array(sortedItem.reversed())
// Secure conversion from selectedYear to Int
if let selectedYearInt = Int(selectedYear) {
// Filter by the selected year and ensure that only the highest value per month is taken into account
reversedArray = reversedArray.filter { item in
let year = Calendar.current.component(.year, from: item.date)
return year == selectedYearInt
}
for item in reversedArray {
let month = Calendar.current.component(.month, from: item.date)
if let highestDate = highestDatesByMonth[month] {
if item.date > highestDate {
highestDatesByMonth[month] = item.date
}
} else {
highestDatesByMonth[month] = item.date
}
//print("HDM: \(highestDatesByMonth)")
}
reversedArray = reversedArray.filter { item in
let month = Calendar.current.component(.month, from: item.date)
if let highestDate = highestDatesByMonth[month] {
return item.date == highestDate
} else {
return false
}
}
//print("RA: \(reversedArray)")
if reversedArray.count > 1 {
for items in reversedArray {
valueArray.append(items.value)
dateArray.append(items.date)
}
//print("dateArray 1: \(dateArray)")
// Ensure that dateArray is not empty before removing the first element
if dateArray.count > 1 {
dateArray.removeFirst()
}
// Check whether there is a previous year
let previousYear = Calendar.current.date(byAdding: .year, value: -1, to: dateArray.first ?? Date())
print("PY: \(String(describing: previousYear))")
if let previousYearItems = sortedItem.filter({ Calendar.current.isDate($0.date, inSameDayAs: previousYear ?? Date()) }).last {
// Calculation of the effective values from month to month
let diffValues = zip([previousYearItems.value] + valueArray.dropLast(), valueArray).map({ $1 - $0 })
// print("dateArray 2: \(dateArray)")
for i in 0..<dateArray.count {
let dateComponents = Calendar.current.dateComponents([.month, .year], from: dateArray[i])
// ...
print(dateComponents.month ?? 0)
let chartData = ChartItem(month: "\(dateComponents.month ?? 0)", value: diffValues[i])
data.append(chartData)
}
} else {
// Calculation of effective values from month to month without previous year's value
let diffValues = zip(valueArray.dropLast(), valueArray.dropFirst()).map({ $1 - $0 })
for i in 0..<dateArray.count {
let dateComponents = Calendar.current.dateComponents([.month, .year], from: dateArray[i])
// ...
print(dateComponents.month ?? 0)
let chartData = ChartItem(month: "\(dateComponents.month ?? 0)", value: diffValues[i])
data.append(chartData)
}
}
} else if reversedArray.count == 1 {
for items in reversedArray {
valueArray.append(items.value)
dateArray.append(items.date)
}
for i in 0..<dateArray.count {
let dateComponents = Calendar.current.dateComponents([.month, .year], from: dateArray[i])
// ...
let chartData = ChartItem(month: "\(dateComponents.month ?? 0)", value: valueArray[i])
data.append(chartData)
}
}
}
return data
}
}
struct ChartView_Previews: PreviewProvider {
static var previews: some View {
ChartView(meterInfo: "Preview", meterNumber: .meter180, unit: .kwh, readings: MeterReadings())
}
}