# HG changeset patch # User Dennis Concepción Martín <66180929+denniscm190@users.noreply.github.com> # Date 1619471202 -7200 # Node ID 959175ee5ebda4de1e05460318b2e781673db619 # Parent a9690565726b69b7c9ca7c8c69f07e7671e23e21 Implement interaction with ChartView diff -r a9690565726b -r 959175ee5ebd InteractiveCharts.xcodeproj/project.pbxproj --- 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 = ""; }; 95075B4A263718C7005E0066 /* IndicatorPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndicatorPoint.swift; sourceTree = ""; }; 95075B4E2637227D005E0066 /* ChartLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLabel.swift; sourceTree = ""; }; + 951D9BDF26375E10006B6A6D /* ChartViewPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartViewPreview.swift; sourceTree = ""; }; + 951D9BE426375E74006B6A6D /* GenerateSampleData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateSampleData.swift; sourceTree = ""; }; + 951D9BE826376131006B6A6D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 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 = ""; }; 9557883D2636B8D800D1192D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -80,6 +86,24 @@ path = Helpers; sourceTree = ""; }; + 951D9BDE26375DDA006B6A6D /* UI Previews */ = { + isa = PBXGroup; + children = ( + 951D9BE826376131006B6A6D /* ContentView.swift */, + 951D9BDF26375E10006B6A6D /* ChartViewPreview.swift */, + 951D9BE326375E44006B6A6D /* Sample data */, + ); + path = "UI Previews"; + sourceTree = ""; + }; + 951D9BE326375E44006B6A6D /* Sample data */ = { + isa = PBXGroup; + children = ( + 951D9BE426375E74006B6A6D /* GenerateSampleData.swift */, + ); + path = "Sample data"; + sourceTree = ""; + }; 9557882F2636B8D700D1192D = { isa = PBXGroup; children = ( @@ -104,6 +128,7 @@ 9557883C2636B8D800D1192D /* InteractiveCharts.h */, 9557883D2636B8D800D1192D /* Info.plist */, 95075B5426372506005E0066 /* LineChart */, + 951D9BDE26375DDA006B6A6D /* UI Previews */, ); path = InteractiveCharts; sourceTree = ""; @@ -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 */, diff -r a9690565726b -r 959175ee5ebd InteractiveCharts.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate Binary file InteractiveCharts.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed diff -r a9690565726b -r 959175ee5ebd InteractiveCharts/LineChart/ChartView.swift --- 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.. 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 diff -r a9690565726b -r 959175ee5ebd InteractiveCharts/LineChart/Helpers/LineView.swift --- 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) + } }