changeset 272:9e23e9b0ab36

Implementing Custom Line Chart
author Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com>
date Mon, 15 Mar 2021 20:06:24 +0100
parents e1610b54015d
children 39428219f832
files LazyBear.xcodeproj/project.pbxproj LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate LazyBear/ContentView.swift LazyBear/DeviceSize.swift LazyBear/LazyBearApp.swift LazyBear/Tests/DemoChart.swift LazyBear/UI/ChartView.swift LazyBear/UI/CompanyView.swift LazyBear/UI/LineChart.swift LazyBear/UI/LinePath.swift
diffstat 10 files changed, 206 insertions(+), 94 deletions(-) [+]
line wrap: on
line diff
--- a/LazyBear.xcodeproj/project.pbxproj	Sun Mar 14 13:25:21 2021 +0100
+++ b/LazyBear.xcodeproj/project.pbxproj	Mon Mar 15 20:06:24 2021 +0100
@@ -49,6 +49,9 @@
 		95ACB5A925E0397B00A3CCC8 /* CompanyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ACB5A825E0397B00A3CCC8 /* CompanyView.swift */; };
 		95ACB5AC25E03A7D00A3CCC8 /* themes.json in Resources */ = {isa = PBXBuildFile; fileRef = 95ACB5AB25E03A7D00A3CCC8 /* themes.json */; };
 		95ACB5AF25E03AA100A3CCC8 /* ThemeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95ACB5AE25E03AA100A3CCC8 /* ThemeModel.swift */; };
+		95AEF3AC25FFBB4D001B77BB /* LinePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEF3AB25FFBB4D001B77BB /* LinePath.swift */; };
+		95AEF3B025FFD8CF001B77BB /* LineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEF3AF25FFD8CF001B77BB /* LineChart.swift */; };
+		95AEF3B325FFDC04001B77BB /* DeviceSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AEF3B225FFDC04001B77BB /* DeviceSize.swift */; };
 		95B3E09F25E127D7007EFDE3 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B3E09E25E127D7007EFDE3 /* Request.swift */; };
 		95B3E0A625E1318D007EFDE3 /* SwiftlySearch in Frameworks */ = {isa = PBXBuildFile; productRef = 95B3E0A525E1318D007EFDE3 /* SwiftlySearch */; };
 		95BB43C025EA667700B6C965 /* DateSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BB43BF25EA667700B6C965 /* DateSelection.swift */; };
@@ -56,7 +59,6 @@
 		95BFAE4E25E2B0C200A70EC3 /* HudManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFAE4D25E2B0C200A70EC3 /* HudManager.swift */; };
 		95BFAE5425E2C52300A70EC3 /* HistoricalPriceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFAE5325E2C52300A70EC3 /* HistoricalPriceModel.swift */; };
 		95BFAE5825E2C5A700A70EC3 /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BFAE5725E2C5A700A70EC3 /* ChartView.swift */; };
-		95D28EC625FD2BBE00FBE5F8 /* DemoChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D28EC525FD2BBE00FBE5F8 /* DemoChart.swift */; };
 		95D34C2725EFD5FE006F4A81 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 95D34C2625EFD5FE006F4A81 /* SDWebImageSwiftUI */; };
 		95DED9D525F2A752000DFCBA /* transactionCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DED9D425F2A752000DFCBA /* transactionCodes.swift */; };
 		95DED9D825F2B1EF000DFCBA /* TopInsiderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DED9D725F2B1EF000DFCBA /* TopInsiderModel.swift */; };
@@ -120,13 +122,15 @@
 		95ACB5A825E0397B00A3CCC8 /* CompanyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyView.swift; sourceTree = "<group>"; };
 		95ACB5AB25E03A7D00A3CCC8 /* themes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = themes.json; sourceTree = "<group>"; };
 		95ACB5AE25E03AA100A3CCC8 /* ThemeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModel.swift; sourceTree = "<group>"; };
+		95AEF3AB25FFBB4D001B77BB /* LinePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinePath.swift; sourceTree = "<group>"; };
+		95AEF3AF25FFD8CF001B77BB /* LineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChart.swift; sourceTree = "<group>"; };
+		95AEF3B225FFDC04001B77BB /* DeviceSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSize.swift; sourceTree = "<group>"; };
 		95B3E09E25E127D7007EFDE3 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
 		95BB43BF25EA667700B6C965 /* DateSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSelection.swift; sourceTree = "<group>"; };
 		95BFAE4A25E2AEA000A70EC3 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
 		95BFAE4D25E2B0C200A70EC3 /* HudManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HudManager.swift; sourceTree = "<group>"; };
 		95BFAE5325E2C52300A70EC3 /* HistoricalPriceModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoricalPriceModel.swift; sourceTree = "<group>"; };
 		95BFAE5725E2C5A700A70EC3 /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = "<group>"; };
-		95D28EC525FD2BBE00FBE5F8 /* DemoChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChart.swift; sourceTree = "<group>"; };
 		95DED9D425F2A752000DFCBA /* transactionCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = transactionCodes.swift; sourceTree = "<group>"; };
 		95DED9D725F2B1EF000DFCBA /* TopInsiderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopInsiderModel.swift; sourceTree = "<group>"; };
 		95DED9DA25F2B268000DFCBA /* InsiderSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsiderSummary.swift; sourceTree = "<group>"; };
@@ -160,7 +164,6 @@
 		950BA46D25E9450B00D065EF /* Tests */ = {
 			isa = PBXGroup;
 			children = (
-				95D28EC525FD2BBE00FBE5F8 /* DemoChart.swift */,
 			);
 			path = Tests;
 			sourceTree = "<group>";
@@ -200,6 +203,7 @@
 				95BFAE4D25E2B0C200A70EC3 /* HudManager.swift */,
 				95672B9725DDA54700DCBE4A /* Persistence.swift */,
 				95A5D95925FCEDDB0090C1EA /* CompanyOption.swift */,
+				95AEF3B225FFDC04001B77BB /* DeviceSize.swift */,
 				95B1874925DDAC4D0068A364 /* UI */,
 				95B1874825DDAC470068A364 /* Models */,
 				958A735525E01F7E00FD7ECA /* Functions */,
@@ -278,6 +282,8 @@
 				9520C26E25F4D43D0070DD71 /* TransactionDetail.swift */,
 				95DED9DA25F2B268000DFCBA /* InsiderSummary.swift */,
 				95E9D09625F6AA0400A947A1 /* ActionView.swift */,
+				95AEF3AB25FFBB4D001B77BB /* LinePath.swift */,
+				95AEF3AF25FFD8CF001B77BB /* LineChart.swift */,
 			);
 			path = UI;
 			sourceTree = "<group>";
@@ -392,7 +398,6 @@
 			buildActionMask = 2147483647;
 			files = (
 				95ABDD3825E167E500310776 /* NewsModel.swift in Sources */,
-				95D28EC625FD2BBE00FBE5F8 /* DemoChart.swift in Sources */,
 				95ABDD3C25E1717300310776 /* NewsRow.swift in Sources */,
 				950B674925E99FA900BF8593 /* IconPicker.swift in Sources */,
 				958A735725E01F9E00FD7ECA /* ReadJson.swift in Sources */,
@@ -401,6 +406,7 @@
 				95F0461025E976B5006A5A17 /* SettingRow.swift in Sources */,
 				95F0460825E9704F006A5A17 /* ThemePicker.swift in Sources */,
 				95A5D95A25FCEDDB0090C1EA /* CompanyOption.swift in Sources */,
+				95AEF3AC25FFBB4D001B77BB /* LinePath.swift in Sources */,
 				95672B9825DDA54700DCBE4A /* Persistence.swift in Sources */,
 				958A735B25E0264E00FD7ECA /* CompanyModel.swift in Sources */,
 				95B3E09F25E127D7007EFDE3 /* Request.swift in Sources */,
@@ -418,11 +424,13 @@
 				95ACB5AF25E03AA100A3CCC8 /* ThemeModel.swift in Sources */,
 				95672B8F25DDA54700DCBE4A /* LazyBearApp.swift in Sources */,
 				9517626025EEB37E00733235 /* PriceModel.swift in Sources */,
+				95AEF3B325FFDC04001B77BB /* DeviceSize.swift in Sources */,
 				958A734525E00D3D00FD7ECA /* CompanyRow.swift in Sources */,
 				95DED9D525F2A752000DFCBA /* transactionCodes.swift in Sources */,
 				95ABDD3525E166BA00310776 /* NewsView.swift in Sources */,
 				957B816825F2A02C0005E5C0 /* InsiderTranModel.swift in Sources */,
 				95672B9B25DDA54800DCBE4A /* LazyBear.xcdatamodeld in Sources */,
+				95AEF3B025FFD8CF001B77BB /* LineChart.swift in Sources */,
 				95F0460B25E970DB006A5A17 /* LanguagePicker.swift in Sources */,
 				95E9D09725F6AA0400A947A1 /* ActionView.swift in Sources */,
 				95ABDD3125E1602D00310776 /* PriceView.swift in Sources */,
Binary file LazyBear.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed
--- a/LazyBear/ContentView.swift	Sun Mar 14 13:25:21 2021 +0100
+++ b/LazyBear/ContentView.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -9,55 +9,63 @@
 
 struct ContentView: View {
     @EnvironmentObject var hudManager: HudManager
+    @EnvironmentObject var deviceSize: DeviceSize
     
     // Fetch user appearence settings
     @FetchRequest(entity: UserSettings.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \UserSettings.changedAt, ascending: false)])
     var userSettings: FetchedResults<UserSettings>
     
     var body: some View {
-        ZStack(alignment: .top) {
-            TabView {
-                // First view
-                Watchlist()
-                    .tabItem {
-                        Label("Watchlist", systemImage: "list.dash")
-                    }
+        GeometryReader { geo in
+            ZStack(alignment: .top) {
+                TabView {
+                    // First view
+                    Watchlist()
+                        .tabItem {
+                            Label("Watchlist", systemImage: "list.dash")
+                        }
+                    
+                    // First view
+                    Search()
+                        .tabItem {
+                            Label("Search", systemImage: "magnifyingglass")
+                        }
+                    
+                    // First view
+                    Settings()
+                        .tabItem {
+                            Label("Settings", systemImage: "gear")
+                        }
+                }
                 
-                // First view
-                Search()
-                    .tabItem {
-                        Label("Search", systemImage: "magnifyingglass")
-                    }
+                // Show HUDs
+                // Notification
+                Notification(text: "Company saved", image: "checkmark.circle")
+                    .offset(y: hudManager.showNotification ? 0 : -100)
+                    .animation(.easeInOut)
                 
-                // First view
-                Settings()
-                    .tabItem {
-                        Label("Settings", systemImage: "gear")
-                    }
+                // Action sheet
+                ZStack(alignment: .bottom) {
+                    Color(.black)
+                        .edgesIgnoringSafeArea(.all)
+                        .opacity(hudManager.showAction ? 0.2: 0)
+                        .animation(.easeInOut)
+                        .onTapGesture { hudManager.showAction = false }
+                    
+                    ActionView()
+                        .offset(y: hudManager.showAction ? 0 : 250)
+                        .animation(.easeInOut)
+                        .padding()
+                }
             }
-            
-            // Show HUDs
-            // Notification
-            Notification(text: "Company saved", image: "checkmark.circle")
-                .offset(y: hudManager.showNotification ? 0 : -100)
-                .animation(.easeInOut)
-            
-            // Action sheet
-            ZStack(alignment: .bottom) {
-                Color(.black)
-                    .edgesIgnoringSafeArea(.all)
-                    .opacity(hudManager.showAction ? 0.2: 0)
-                    .animation(.easeInOut)
-                    .onTapGesture { hudManager.showAction = false }
-                
-                ActionView()
-                    .offset(y: hudManager.showAction ? 0 : 250)
-                    .animation(.easeInOut)
-                    .padding()
+            .accentColor(Color("\(userSettings.first?.theme?.lowercased() ?? "default")Accent"))
+            // If this value is not optional it will cause a crash
+            .onAppear {
+                // Assign device screen size to the class
+                self.deviceSize.width = geo.size.width
+                self.deviceSize.height = geo.size.height
             }
         }
-        .accentColor(Color("\(userSettings.first?.theme?.lowercased() ?? "default")Accent"))
-        // If this value is not optional it will cause a crash
     }
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LazyBear/DeviceSize.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -0,0 +1,13 @@
+//
+//  DeviceSize.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 15/3/21.
+//
+
+import SwiftUI
+
+class DeviceSize: ObservableObject {
+    @Published var width = CGFloat(400)
+    @Published var height = CGFloat(400)
+}
--- a/LazyBear/LazyBearApp.swift	Sun Mar 14 13:25:21 2021 +0100
+++ b/LazyBear/LazyBearApp.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -14,6 +14,7 @@
     // Start ObservedObjects
     @ObservedObject var hudManager = HudManager()
     @ObservedObject var companyOption = CompanyOption()
+    @ObservedObject var deviceSize = DeviceSize()
 
     var body: some Scene {
         WindowGroup {
@@ -21,6 +22,7 @@
                 .environment(\.managedObjectContext, persistenceController.container.viewContext)
                 .environmentObject(hudManager)
                 .environmentObject(companyOption)
+                .environmentObject(deviceSize)
         }
     }
 }
--- a/LazyBear/Tests/DemoChart.swift	Sun Mar 14 13:25:21 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,43 +0,0 @@
-//
-//  DemoChart.swift
-//  LazyBear
-//
-//  Created by Dennis Concepción Martín on 13/3/21.
-//
-
-import SwiftUI
-
-struct DemoChart: View {
-    let sampleData: [Double] = [10, 11.2, 13.4, 10.2, 13.4, 12.4, 15.6, 18.7, 20.9, 21.2, 10.3]
-
-    var body: some View {
-        let normalizedData = normalize(sampleData)
-        Path { path in
-            path.move(to: CGPoint(x: 0, y: 0))
-            for point in normalizedData {
-                path.addLine(to: CGPoint(x: 1, y: point))
-            }
-            
-        }
-        .fill(Color.green)
-    }
-    
-    func normalize(_ data: [Double]) -> [Double] {
-        var normalData = [Double]()
-        let min = data.min()!
-        let max = data.max()!
-        
-        for value in data {
-            let normal = (value - min) / (max - min)
-            normalData.append(normal)
-        }
-        
-        return normalData
-    }
-}
-
-struct DemoChart_Previews: PreviewProvider {
-    static var previews: some View {
-        DemoChart()
-    }
-}
--- a/LazyBear/UI/ChartView.swift	Sun Mar 14 13:25:21 2021 +0100
+++ b/LazyBear/UI/ChartView.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -9,7 +9,6 @@
 
 struct ChartView: View {
     var symbol: String
-    var chartHeight: CGFloat
     @State private var historicalPrices = [HistoricalPriceModel]()
    
     // Date picker
@@ -25,17 +24,19 @@
                     request(url: url, model: [HistoricalPriceModel].self) { self.historicalPrices = $0 }
                 })
             
-
+            let prices = historicalPrices.map { $0.close }
+            LineChart(data: prices)
         }
         .onAppear {
             let url = getUrl(endpoint: .historicalPrices, symbol: symbol, range: "3m")
             request(url: url, model: [HistoricalPriceModel].self) { self.historicalPrices = $0 }
         }
+
     }
 }
 
 struct HistoricalPriceView_Previews: PreviewProvider {
     static var previews: some View {
-        ChartView(symbol: "aapl", chartHeight: 200)
+        ChartView(symbol: "aapl")
     }
 }
--- a/LazyBear/UI/CompanyView.swift	Sun Mar 14 13:25:21 2021 +0100
+++ b/LazyBear/UI/CompanyView.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -20,20 +20,20 @@
     @FetchRequest(entity: Company.entity(), sortDescriptors: []) var companies: FetchedResults<Company>
     
     var body: some View {
-        GeometryReader { geo in
-            ScrollView {
+        ScrollView {
+            //VStack {
                 if companyOption.view == .stock {
                     PriceView(symbol: symbol, showVertical: false)
-                        ChartView(symbol: symbol, chartHeight: geo.size.width / 2)
-                        NewsView(symbol: symbol)
+                    ChartView(symbol: symbol)
+                    NewsView(symbol: symbol)
                         
                 } else if companyOption.view == .insiders {
                     InsiderSummary(symbol: symbol)
                     InsiderTransactions(symbol: symbol)
                 }
-            }
-            .onAppear { companyOption.view = .stock }
+            //}
         }
+        .onAppear { companyOption.view = .stock }
         .toolbar {
             ToolbarItem(placement: .principal) {
                 Button(action: { self.hudManager.showAction.toggle() }) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LazyBear/UI/LineChart.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -0,0 +1,56 @@
+//
+//  LineChart.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 15/3/21.
+//
+
+import SwiftUI
+
+struct LineChart: View {
+    var data: [Double]
+    @EnvironmentObject var deviceSize: DeviceSize
+    
+    var body: some View {
+            VStack {
+                ZStack {
+                    Grid(height: deviceSize.width, width: deviceSize.width)
+                        .clipped()
+                        .background(LinePath(width: deviceSize.width, data: data))
+                    
+                        
+                }
+            }
+            .frame(width: deviceSize.width, height: deviceSize.width / 2)
+        
+    }
+}
+
+struct LineChart_Previews: PreviewProvider {
+    static var previews: some View {
+        LineChart(data: [50.0, 50.5, 51.0, 50.4, 50.8, 51.3, 51.5, 52, 51.9, 52.4])
+            .environmentObject(DeviceSize())
+    }
+}
+
+struct Grid: View {
+    var height: CGFloat
+    var width: CGFloat
+    
+    var body: some View {
+        VStack {
+            Rectangle()
+                .stroke(Color.gray.opacity(0.2), lineWidth: 0.5)
+            
+            Group {
+                Rectangle()
+                    .stroke(Color.gray.opacity(0.2), lineWidth: 0.5)
+                
+                Rectangle()
+                    .stroke(Color.gray.opacity(0.2), lineWidth: 0.5)
+            }
+            .offset(y: -8)
+            .padding(.bottom, -8)
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LazyBear/UI/LinePath.swift	Mon Mar 15 20:06:24 2021 +0100
@@ -0,0 +1,67 @@
+//
+//  LinePath.swift
+//  LazyBear
+//
+//  Created by Dennis Concepción Martín on 15/3/21.
+//
+
+import SwiftUI
+
+struct LinePath: View {
+    var width: CGFloat
+    var data: [Double]
+
+    var body: some View {
+        let height = width/2
+        if !data.isEmpty {
+            let normalizedData = normalize(data)
+                
+            // Substract 2 to skip the first and the last item
+            let widthBetweenPoints = Double(width) / Double(normalizedData.count - 2)
+            
+            Path { path in
+                let initialPoint = normalizedData[0] * Double(height)
+                var x: Double = 0
+                path.move(to: CGPoint(x: x, y: initialPoint))
+                
+                for y in normalizedData {
+                    // Skip first item
+                    if normalizedData.firstIndex(of: y) != 0 {
+                        x += widthBetweenPoints
+                        let y = y * Double(height)
+                        path.addLine(to: CGPoint(x: x, y: y))
+                    }
+                }
+            }
+            .stroke(Color.green, lineWidth: 2)
+            .rotationEffect(.degrees(180), anchor: .center)  // The path must be rotated
+            .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0))
+            
+        }
+    }
+    
+    func normalize(_ data: [Double]) -> [Double] {
+        var normalData = [Double]()
+        let min = data.min()!
+        let max = data.max()!
+
+        for value in data {
+            let normal = (value - min) / (max - min)
+            normalData.append(normal)
+        }
+
+        return normalData
+    }
+    
+}
+
+struct LinePath_Previews: PreviewProvider {
+    static var previews: some View {
+        GeometryReader { geo in
+            VStack {
+                let data: [Double] = [50.0, 50.5, 51.0, 50.4, 50.8, 51.3, 51.5, 52, 51.9, 52.4]
+                LinePath(width: geo.size.width, data: data)
+            }
+        }
+    }
+}