Skip to content

Commit

Permalink
Merge pull request #687 from Shayokh144/linechart-options
Browse files Browse the repository at this point in the history
[Feature] Added different types of line chart, fix chart x axis, y axis labels
  • Loading branch information
Shayokh144 committed Aug 22, 2024
2 parents 496c50d + 1237321 commit 60077ec
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
04DB73752C725FDF0038AEFD /* LineChartDataModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DB73742C725FDF0038AEFD /* LineChartDataModel.swift */; };
04DB73772C7265300038AEFD /* LineChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DB73762C7265300038AEFD /* LineChartViewModel.swift */; };
04DB73792C7346140038AEFD /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DB73782C7346140038AEFD /* Double+Extension.swift */; };
04DB737B2C748FDE0038AEFD /* OptionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DB737A2C748FDE0038AEFD /* OptionPickerView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -47,6 +48,7 @@
04DB73742C725FDF0038AEFD /* LineChartDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartDataModel.swift; sourceTree = "<group>"; };
04DB73762C7265300038AEFD /* LineChartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartViewModel.swift; sourceTree = "<group>"; };
04DB73782C7346140038AEFD /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
04DB737A2C748FDE0038AEFD /* OptionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionPickerView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -143,6 +145,7 @@
isa = PBXGroup;
children = (
048B86C82C67E75C008036D4 /* CSVFileReader.swift */,
04DB737A2C748FDE0038AEFD /* OptionPickerView.swift */,
);
path = Helper;
sourceTree = "<group>";
Expand Down Expand Up @@ -253,6 +256,7 @@
048B86C32C67D6D8008036D4 /* ProductSelectionViewModel.swift in Sources */,
043E74D02BABF77700B9AF91 /* ContentView.swift in Sources */,
048B86C12C67D6CB008036D4 /* ProductSelectionScreen.swift in Sources */,
04DB737B2C748FDE0038AEFD /* OptionPickerView.swift in Sources */,
04DB73752C725FDF0038AEFD /* LineChartDataModel.swift in Sources */,
048B86D22C6B58E9008036D4 /* SingleBarViewModel.swift in Sources */,
048B86D02C6B58D6008036D4 /* SingleBarScreen.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
"size" : "256x256"
},
{
"filename" : "cellphone-5.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "cellphone.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,11 @@ extension DateFormatter {
dateFormatter.dateFormat = "yy-MM"
return dateFormatter
}()

public static let yearMontDayumber: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.calendar = Calendar(identifier: .gregorian)
dateFormatter.dateFormat = "yy-MM-dd"
return dateFormatter
}()
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,22 @@ struct LineChartScreen: View {
AxisTick()
AxisValueLabel() {
if let date = value.as(Date.self) {
Text(DateFormatter.monthYearShort.string(from: date))
if viewModel.selectedChartOption.value == LineChartOption.last2Months.value {
Text(DateFormatter.dayMonthYearShort.string(from: date))
.rotationEffect(.degrees(-90))
.padding(.bottom, 20.0)
.padding(.leading, -16.0)
} else {
Text(DateFormatter.yearMontDayumber.string(from: date))
.rotationEffect(.degrees(-90))
.padding(.bottom, 20.0)
.padding(.leading, -16.0)
}
}
}
}
}
.chartYScale(domain: viewModel.minYValue...viewModel.maxYValue)
.chartXScale(
domain: firstDate...lastDate
)
Expand All @@ -68,6 +79,19 @@ struct LineChartScreen: View {
Text(viewModel.viewModelData.currencyUnitText)
Spacer()
Text(viewModel.viewModelData.dataSource)
Spacer()
OptionPickerView(
optionList: viewModel.chartOptions,
selectedOption: .init(
get: {
viewModel.selectedChartOption.value
},
set: { newValue in
viewModel.updateSelection(newSelection: newValue)
}
)
)
.frame(maxWidth: 300.0)
}
.font(.system(size: 14.0, weight: .bold))
.foregroundStyle(Color.purple)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ final class LineChartViewModel: ObservableObject {
@Published var uiModels: [LineChartDataSeries]
@Published var uiFirstDate: Date?
@Published var uiLastDate: Date?
@Published var maxYValue: Int = 0
@Published var minYValue: Int = 0
@Published var chartYAxisValues: [Int] = []
@Published var chartXAxisValues: [Date] = []
@Published var chartOptions: [String] = []
@Published var selectedChartOption: LineChartOption = .allYear
private var years: [String] = []

let viewModelData: LineChartViewModelData

init(lineChartViewModelData: LineChartViewModelData) {
Expand All @@ -25,36 +31,105 @@ final class LineChartViewModel: ObservableObject {
guard uiModels.isEmpty else {
return
}
createUIModelForAllData(dataList: viewModelData.lineChartDataList)
setInitialChartOptions()
}

func updateSelection(newSelection: String) {
selectedChartOption = LineChartOption(value: newSelection)
switch selectedChartOption {
case .allYear:
createUIModelForAllData(dataList: viewModelData.lineChartDataList)
case .allMonthlyAverage:
let monthlyAvgData = calculateMonthlyAverages(from: viewModelData.lineChartDataList)
createUIModelForAllData(dataList: monthlyAvgData)
case .last2Months:
let last60Elements = Array(viewModelData.lineChartDataList.suffix(60))
createUIModelForAllData(dataList: last60Elements)
case .year(let string):
let yearData = filterData(forYear: string, from: viewModelData.lineChartDataList)
createUIModelForAllData(dataList: yearData)
}
}

private func getRuleMarkList(
chartDataPoints: [LineChartDataModel]
) -> [RuleMarkDataModel] {
let avgValue: Double = chartDataPoints.reduce(0.0) {
return $1.yValue + $0
} / Double(chartDataPoints.count)
return [
RuleMarkDataModel(
id: 1,
yValue: avgValue,
yName: "Average price:",
ruleMarkName: "Average price"
)
]
}

private func setInitialChartOptions() {
// Set filter options
guard let firstDate = viewModelData.lineChartDataList.first?.xTimeValue,
let lastDate = viewModelData.lineChartDataList.last?.xTimeValue else {
return
}
years = distinctYears(from: firstDate, to: lastDate)
chartOptions = LineChartOption.allStaticCases
for year in years {
chartOptions.append(year)
}
selectedChartOption = .allYear
}

private func distinctYears(from firstDate: Date, to lastDate: Date) -> [String] {
let calendar = Calendar.current
var years: [String] = []

var currentYear = calendar.component(.year, from: firstDate)
let lastYear = calendar.component(.year, from: lastDate)

while currentYear <= lastYear {
years.append("\(currentYear)")
currentYear += 1
}
return years
}

private func createUIModelForAllData(dataList: [LineChartDataModel]) {
guard let firstDate = dataList.first?.xTimeValue,
let lastDate = dataList.last?.xTimeValue else {
return
}
uiFirstDate = firstDate
uiLastDate = lastDate

// RuleMark Data
let ruleMarks = getRuleMarkList(
chartDataPoints: viewModelData.lineChartDataList
)
let uiModel = LineChartDataSeries(
type: "All year",
firstDate: firstDate,
lastDate: lastDate,
lineChartDataList: viewModelData.lineChartDataList,
ruleMarkDataList: ruleMarks
chartDataPoints: dataList
)
uiModels = [uiModel]

// Chart Data
let maxValue = Int(viewModelData.lineChartDataList.map { $0.yValue }.max() ?? 0.0) + 20
var minValue = Int(viewModelData.lineChartDataList.map { $0.yValue }.min() ?? 0.0) - 20
if minValue < 0 {
minValue = 0
// Chart Axis Data
maxYValue = Int(dataList.map { $0.yValue }.max() ?? 0.0) + 20
minYValue = Int(dataList.map { $0.yValue }.min() ?? 0.0) - 20
if minYValue < 0 {
minYValue = 0
}
chartYAxisValues = stride(from: minValue, to: maxValue, by: 20).map { $0 }

chartYAxisValues = stride(from: minYValue, to: maxYValue, by: 10).map { $0 }
chartXAxisValues.removeAll()
var dayInterval = Int(dataList.count / 20)
if dayInterval < 14 {
dayInterval = 14
}
var dateFormat = "(yy-mm-dd)"
if selectedChartOption.value == LineChartOption.last2Months.value {
dayInterval = 5
dateFormat = ""
} else if selectedChartOption.value == LineChartOption.allMonthlyAverage.value {
dayInterval = 32
}
var currentDate = firstDate
let calendar = Calendar.current
let dayInterval = Int(viewModelData.lineChartDataList.count / 10)
while currentDate <= lastDate {
chartXAxisValues.append(currentDate)
if let nextDate = calendar.date(byAdding: .day, value: dayInterval, to: currentDate) {
Expand All @@ -63,22 +138,55 @@ final class LineChartViewModel: ObservableObject {
break
}
}

// UPDATE UI DATA
let uiModel = LineChartDataSeries(
type: "\(selectedChartOption.value) \(dateFormat)",
firstDate: firstDate,
lastDate: lastDate,
lineChartDataList: dataList,
ruleMarkDataList: ruleMarks
)
uiModels = [uiModel]
}

private func getRuleMarkList(
chartDataPoints: [LineChartDataModel]
) -> [RuleMarkDataModel] {
let avgValue: Double = chartDataPoints.reduce(0.0) {
return $1.yValue + $0
} / Double(chartDataPoints.count)
return [
RuleMarkDataModel(
id: 1,
yValue: avgValue,
yName: "Average price:",
ruleMarkName: "Average price"
)
]
private func filterData(forYear year: String, from allData: [LineChartDataModel]) -> [LineChartDataModel] {
let calendar = Calendar.current
// Convert the input year string to an integer
guard let yearInt = Int(year) else {
return [] // Return an empty list if the input year is not valid
}
// Filter the data where the xTimeValue year matches the input year
let filteredData = allData.filter { data in
let dataYear = calendar.component(.year, from: data.xTimeValue)
return dataYear == yearInt
}
return filteredData
}

private func calculateMonthlyAverages(from allData: [LineChartDataModel]) -> [LineChartDataModel] {
let calendar = Calendar.current
// Group data by month and year
let groupedData = Dictionary(grouping: allData) { data in
calendar.date(from: calendar.dateComponents([.year, .month], from: data.xTimeValue))!
}

// Calculate monthly averages
var monthlyAverageData: [LineChartDataModel] = []
var index = 1
for (date, dataList) in groupedData {
let totalYValue = dataList.reduce(0) { $0 + $1.yValue }
let averageYValue = totalYValue / Double(dataList.count)

// Create a new LineChartDataModel with the average yValue
let monthlyAverage = LineChartDataModel(id: index,
xTimeValue: date,
yValue: averageYValue)
index += 1
monthlyAverageData.append(monthlyAverage)
}

return monthlyAverageData.sorted { $0.xTimeValue < $1.xTimeValue }
}
}

Expand All @@ -91,3 +199,44 @@ struct LineChartViewModelData {
let dataSource: String
let lineChartDataList: [LineChartDataModel]
}

enum LineChartOption {

case allYear, allMonthlyAverage, last2Months
case year(String)

var value: String {
switch self {
case .allYear:
"All Year"
case .allMonthlyAverage:
"All Monthly Average"
case .last2Months:
"Last 2 Months"
case .year(let string):
string
}
}

static var allStaticCases: [String] {
[
LineChartOption.allYear.value,
LineChartOption.allMonthlyAverage.value,
LineChartOption.last2Months.value
]
}

init(value: String) {

switch value {
case LineChartOption.allYear.value:
self = .allYear
case LineChartOption.allMonthlyAverage.value:
self = .allMonthlyAverage
case LineChartOption.last2Months.value:
self = .last2Months
default:
self = .year(value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// OptionPickerView.swift
// VisualReport
//
// Created by Taher's nimble macbook on 20/8/24.
//

import SwiftUI

struct OptionPickerView: View {

private let optionList: [String]
@Binding private var selectedOption: String

var body: some View {
HStack(spacing: .zero) {
Spacer()
Picker("", selection: $selectedOption) {
ForEach(optionList, id: \.self) {
Text($0)
}
}
.pickerStyle(.menu)
.clipped()
.labelsHidden()
.overlay(
RoundedRectangle(cornerRadius: 8.0)
.stroke(
Color.purple,
lineWidth: 1.0
)
)
}
}

init(optionList: [String], selectedOption: Binding<String>) {
self.optionList = optionList
_selectedOption = selectedOption
}
}

0 comments on commit 60077ec

Please sign in to comment.