view Sources/StockCharts/LineChart/Helpers/LineView.swift @ 108:f53d8b9ca92b

Custom style implemented
author Dennis Concepción Martín <dennisconcepcionmartin@gmail.com>
date Sun, 04 Jul 2021 16:40:24 +0100
parents 0c0d38dca6d8
children 5057c45046c1
line wrap: on
line source

//
//  LineView.swift
//  StockCharts
//
//  Created by Dennis Concepción Martín on 30/4/21.
//

import SwiftUI

public struct LineView: View {
    public var data: [Double]
    public var dates: [String]?
    public var hours: [String]?
    public var dragGesture: Bool?
    public var style: LineChartStyle
    
    @Binding var showingIndicators: Bool
    @Binding var indexPosition: Int
    @State var IndicatorPointPosition: CGPoint = .zero
    @State var pathPoints = [CGPoint]()
    
    public var body: some View {
        ZStack {
            GeometryReader { proxy in
                LinePath(data: data, width: proxy.size.width, height: proxy.size.height, pathPoints: $pathPoints)
                    .stroke(colorLine(), lineWidth: 2)
            }
            
            if showingIndicators {
                IndicatorPoint(style: style)
                    .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(dragGesture ?? true ?
            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
                })
            : nil  // On dragGesture = false
        )
    }
    
    /*
     Color path depending on data.
     */
    public func colorLine() -> Color {
        var color = style.uptrendLineColor
        
        if showingIndicators {
            color = style.showingIndicatorLineColor
        } else if data.first! > data.last! {
            color = style.downtrendLineColor
        } else if data.first! == data.last! {
            color = style.flatTrendLineColor
        }
        
        return color
    }
    
    /*
     When the user drag on Path -> Modifiy indicator point to move it on the path accordingly
     */
    public 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.
     */
    public 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)
    }
}