Mercurial > public > stock-charts
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) + } }