changeset 8:959175ee5ebd

Implement interaction with ChartView
author Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com>
date Mon, 26 Apr 2021 23:06:42 +0200
parents a9690565726b
children e1f2c119a9c6
files InteractiveCharts.xcodeproj/project.pbxproj InteractiveCharts.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate InteractiveCharts/LineChart/ChartView.swift InteractiveCharts/LineChart/Helpers/ChartLabel.swift InteractiveCharts/LineChart/Helpers/LinePath.swift InteractiveCharts/LineChart/Helpers/LineView.swift
diffstat 6 files changed, 110 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/InteractiveCharts.xcodeproj/project.pbxproj	Mon Apr 26 23:06:25 2021 +0200
+++ b/InteractiveCharts.xcodeproj/project.pbxproj	Mon Apr 26 23:06:42 2021 +0200
@@ -12,6 +12,9 @@
 		95075B472637153E005E0066 /* ChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95075B462637153E005E0066 /* ChartView.swift */; };
 		95075B4B263718C7005E0066 /* IndicatorPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95075B4A263718C7005E0066 /* IndicatorPoint.swift */; };
 		95075B4F2637227D005E0066 /* ChartLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95075B4E2637227D005E0066 /* ChartLabel.swift */; };
+		951D9BE026375E10006B6A6D /* ChartViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D9BDF26375E10006B6A6D /* ChartViewPreview.swift */; };
+		951D9BE526375E74006B6A6D /* GenerateSampleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D9BE426375E74006B6A6D /* GenerateSampleData.swift */; };
+		951D9BE926376131006B6A6D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 951D9BE826376131006B6A6D /* ContentView.swift */; };
 		955788432636B8D800D1192D /* InteractiveCharts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 955788392636B8D800D1192D /* InteractiveCharts.framework */; };
 		955788482636B8D800D1192D /* InteractiveChartsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955788472636B8D800D1192D /* InteractiveChartsTests.swift */; };
 		9557884A2636B8D800D1192D /* InteractiveCharts.h in Headers */ = {isa = PBXBuildFile; fileRef = 9557883C2636B8D800D1192D /* InteractiveCharts.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -33,6 +36,9 @@
 		95075B462637153E005E0066 /* ChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartView.swift; sourceTree = "<group>"; };
 		95075B4A263718C7005E0066 /* IndicatorPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorPoint.swift; sourceTree = "<group>"; };
 		95075B4E2637227D005E0066 /* ChartLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLabel.swift; sourceTree = "<group>"; };
+		951D9BDF26375E10006B6A6D /* ChartViewPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartViewPreview.swift; sourceTree = "<group>"; };
+		951D9BE426375E74006B6A6D /* GenerateSampleData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateSampleData.swift; sourceTree = "<group>"; };
+		951D9BE826376131006B6A6D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
 		955788392636B8D800D1192D /* InteractiveCharts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = InteractiveCharts.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		9557883C2636B8D800D1192D /* InteractiveCharts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InteractiveCharts.h; sourceTree = "<group>"; };
 		9557883D2636B8D800D1192D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -80,6 +86,24 @@
 			path = Helpers;
 			sourceTree = "<group>";
 		};
+		951D9BDE26375DDA006B6A6D /* UI Previews */ = {
+			isa = PBXGroup;
+			children = (
+				951D9BE826376131006B6A6D /* ContentView.swift */,
+				951D9BDF26375E10006B6A6D /* ChartViewPreview.swift */,
+				951D9BE326375E44006B6A6D /* Sample data */,
+			);
+			path = "UI Previews";
+			sourceTree = "<group>";
+		};
+		951D9BE326375E44006B6A6D /* Sample data */ = {
+			isa = PBXGroup;
+			children = (
+				951D9BE426375E74006B6A6D /* GenerateSampleData.swift */,
+			);
+			path = "Sample data";
+			sourceTree = "<group>";
+		};
 		9557882F2636B8D700D1192D = {
 			isa = PBXGroup;
 			children = (
@@ -104,6 +128,7 @@
 				9557883C2636B8D800D1192D /* InteractiveCharts.h */,
 				9557883D2636B8D800D1192D /* Info.plist */,
 				95075B5426372506005E0066 /* LineChart */,
+				951D9BDE26375DDA006B6A6D /* UI Previews */,
 			);
 			path = InteractiveCharts;
 			sourceTree = "<group>";
@@ -227,7 +252,10 @@
 			buildActionMask = 2147483647;
 			files = (
 				95075B4326370EAA005E0066 /* LinePath.swift in Sources */,
+				951D9BE526375E74006B6A6D /* GenerateSampleData.swift in Sources */,
+				951D9BE926376131006B6A6D /* ContentView.swift in Sources */,
 				95075B3F26370E81005E0066 /* LineView.swift in Sources */,
+				951D9BE026375E10006B6A6D /* ChartViewPreview.swift in Sources */,
 				95075B4B263718C7005E0066 /* IndicatorPoint.swift in Sources */,
 				95075B4F2637227D005E0066 /* ChartLabel.swift in Sources */,
 				95075B472637153E005E0066 /* ChartView.swift in Sources */,
Binary file InteractiveCharts.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed
--- a/InteractiveCharts/LineChart/ChartView.swift	Mon Apr 26 23:06:25 2021 +0200
+++ b/InteractiveCharts/LineChart/ChartView.swift	Mon Apr 26 23:06:42 2021 +0200
@@ -6,37 +6,22 @@
 //
 
 import SwiftUI
-import GameplayKit
 
 struct ChartView: View {
     var data: [Double]
     var dates: [String]?
     var hours: [String]?
     
-    @State private var showingLabel = false
-    @State private var IndicatorPointPosition: CGPoint = .zero
+    @State private var showingIndicators = false
     @State private var indexPosition = Int()
     
     var body: some View {
-        ZStack {
-            if showingLabel {
-                ChartLabel(data: data, dates: dates, hours: hours, indexPosition: $indexPosition)
-            }
-            
-            LineView(data: data)
+        VStack {
+            ChartLabel(data: data, dates: dates, hours: hours, indexPosition: $indexPosition)
+                .opacity(showingIndicators ? 1: 0)
+                .padding(.vertical)
+
+            LineView(data: data, showingIndicators: $showingIndicators, indexPosition: $indexPosition)
         }
     }
 }
-
-struct ChartView_Previews: PreviewProvider {
-    static var previews: some View {
-        ChartView(data: [10.0, 11.1, 10.5, 11.0, 11.9, 11.7, 10.4, 10.9])
-    }
-    
-    /*
-     Generate sample data
-     */
-    static func generateSampleData(_ n: Int) -> [Double] {
-        return (0..<n).map { _ in .random(in: 1...20) }
-    }
-}
--- a/InteractiveCharts/LineChart/Helpers/ChartLabel.swift	Mon Apr 26 23:06:25 2021 +0200
+++ b/InteractiveCharts/LineChart/Helpers/ChartLabel.swift	Mon Apr 26 23:06:42 2021 +0200
@@ -26,9 +26,9 @@
                     Text(hour)
                 }
                 Text("\(data[indexPosition], specifier: "%.2f")")
-//                    .foregroundColor(colour)
+                    .foregroundColor(Color(.systemBlue))
             }
-            .font(.subheadline)
+            .font(.headline)
         }
     }
     
@@ -48,9 +48,3 @@
         return finalDate
     }
 }
-
-struct Label_Previews: PreviewProvider {
-    static var previews: some View {
-        ChartLabel(data: [10.0, 11.1, 10.5, 10.0, 11.9, 11.7, 10.4, 10.9], indexPosition: .constant(0))
-    }
-}
--- a/InteractiveCharts/LineChart/Helpers/LinePath.swift	Mon Apr 26 23:06:25 2021 +0200
+++ b/InteractiveCharts/LineChart/Helpers/LinePath.swift	Mon Apr 26 23:06:42 2021 +0200
@@ -10,6 +10,7 @@
 struct LinePath: Shape {
     var data: [Double]
     var (width, height): (CGFloat, CGFloat)
+    @Binding var pathPoints: [CGPoint]
     
     func path(in rect: CGRect) -> Path {
         var path = Path()
@@ -19,8 +20,6 @@
         let initialPoint = normalizedData[0] * Double(height)
         var x: Double = 0
         
-        var pathPoints = [CGPoint]()
-        
         path.move(to: CGPoint(x: x, y: initialPoint))
         for y in normalizedData {
             if normalizedData.firstIndex(of: y) != 0 {  // Skip first point
--- a/InteractiveCharts/LineChart/Helpers/LineView.swift	Mon Apr 26 23:06:25 2021 +0200
+++ b/InteractiveCharts/LineChart/Helpers/LineView.swift	Mon Apr 26 23:06:42 2021 +0200
@@ -9,14 +9,47 @@
 
 struct LineView: View {
     var data: [Double]
+    var dates: [String]?
+    var hours: [String]?
+    
+    @Binding var showingIndicators: Bool
+    @Binding var indexPosition: Int
+    @State private var IndicatorPointPosition: CGPoint = .zero
+    @State private var pathPoints = [CGPoint]()
     
     var body: some View {
-        GeometryReader { proxy in
-            LinePath(data: data, width: proxy.size.width, height: proxy.size.height)
-                .stroke(colorLine(), lineWidth: 2)
-                .rotationEffect(.degrees(180), anchor: .center)
-                .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0))
+        ZStack {            
+            GeometryReader { proxy in
+                LinePath(data: data, width: proxy.size.width, height: proxy.size.height, pathPoints: $pathPoints)
+                    .stroke(colorLine(), lineWidth: 2)
+            }
+            
+            if showingIndicators {
+                IndicatorPoint()
+                    .position(x: IndicatorPointPosition.x, y: IndicatorPointPosition.y)
+            }
         }
+        .rotationEffect(.degrees(180), anchor: .center)
+        .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0))
+        .contentShape(Rectangle())  // Control tappable area
+        .gesture(
+            LongPressGesture(minimumDuration: 0.2)
+                .sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .local))
+                .onChanged({ value in  // Get value of the gesture
+                    switch value {
+                    case .second(true, let drag):
+                        if let longPressLocation = drag?.location {
+                            dragGesture(longPressLocation)
+                        }
+                    default:
+                        break
+                    }
+                })
+                // Hide indicator when finish
+                .onEnded({ value in
+                    self.showingIndicators = false
+                })
+        )
     }
     
     /*
@@ -30,7 +63,41 @@
         } else if data.first! == data.last! {
             color = Color(.systemTeal)
         }
+        else if showingIndicators {
+            color = Color(.systemBlue)
+        }
         
         return color
     }
+    
+    /*
+     When the user drag on Path -> Modifiy indicator point to move it on the path accordingly
+     */
+    private func dragGesture(_ longPressLocation: CGPoint) {
+        let (closestXPoint, closestYPoint, yPointIndex) = getClosestValueFrom(longPressLocation, inData: pathPoints)
+        self.IndicatorPointPosition.x = closestXPoint
+        self.IndicatorPointPosition.y = closestYPoint
+        self.showingIndicators = true
+        self.indexPosition = yPointIndex
+    }
+    
+    /*
+     First, search the closest X point in Path from the tapped location.
+     Then, find the correspondent Y point in Path.
+     */
+    private func getClosestValueFrom(_ value: CGPoint, inData: [CGPoint]) -> (CGFloat, CGFloat, Int) {
+        let touchPoint: (CGFloat, CGFloat) = (value.x, value.y)
+        let xPathPoints = inData.map { $0.x }
+        let yPathPoints = inData.map { $0.y }
+        
+        // Closest X value
+        let closestXPoint = xPathPoints.enumerated().min( by: { abs($0.1 - touchPoint.0) < abs($1.1 - touchPoint.0) } )!
+        let closestYPointIndex = xPathPoints.firstIndex(of: closestXPoint.element)!
+        let closestYPoint = yPathPoints[closestYPointIndex]
+        
+        // Index of the closest points in the array
+        let yPointIndex = yPathPoints.firstIndex(of: closestYPoint)!
+
+        return (closestXPoint.element, closestYPoint, yPointIndex)
+    }
 }