diff --git a/Sources/SwiftUICharts/LineChart/Line.swift b/Sources/SwiftUICharts/LineChart/Line.swift index fb8629c2..b0e51944 100644 --- a/Sources/SwiftUICharts/LineChart/Line.swift +++ b/Sources/SwiftUICharts/LineChart/Line.swift @@ -75,8 +75,7 @@ struct Line: View { } func getClosestPointOnPath(touchLocation: CGPoint) -> CGPoint { - let percentage:CGFloat = min(max(touchLocation.x,0)/self.frame.width,1) - let closest = self.path.percentPoint(percentage) + let closest = self.path.point(to: touchLocation.x) return closest } @@ -151,22 +150,6 @@ extension Path { path.closeSubpath() return path } - - func percentPoint(_ percent: CGFloat) -> CGPoint { - // percent difference between points - let diff: CGFloat = 0.001 - let comp: CGFloat = 1 - diff - - // handle limits - let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) - - let f = pct > comp ? comp : pct - let t = pct > comp ? 1 : pct + diff - let tp = self.trimmedPath(from: f, to: t) - - return CGPoint(x: tp.boundingRect.midX, y: tp.boundingRect.midY) - } - } struct Line_Previews: PreviewProvider { diff --git a/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift b/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift new file mode 100644 index 00000000..ce565958 --- /dev/null +++ b/Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift @@ -0,0 +1,251 @@ +// +// File.swift +// +// +// Created by xspyhack on 2020/1/21. +// + +import SwiftUI + +extension Path { + func trimmedPath(for percent: CGFloat) -> Path { + // percent difference between points + let boundsDistance: CGFloat = 0.001 + let completion: CGFloat = 1 - boundsDistance + + let pct = percent > 1 ? 0 : (percent < 0 ? 1 : percent) + + let start = pct > completion ? completion : pct - boundsDistance + let end = pct > completion ? 1 : pct + boundsDistance + return trimmedPath(from: start, to: end) + } + + func point(for percent: CGFloat) -> CGPoint { + let path = trimmedPath(for: percent) + return CGPoint(x: path.boundingRect.midX, y: path.boundingRect.midY) + } + + func point(to maxX: CGFloat) -> CGPoint { + let total = length + let sub = length(to: maxX) + let percent = sub / total + return point(for: percent) + } + + var length: CGFloat { + var ret: CGFloat = 0.0 + var start: CGPoint? + var point = CGPoint.zero + + forEach { ele in + switch ele { + case .move(let to): + if start == nil { + start = to + } + point = to + case .line(let to): + ret += point.line(to: to) + point = to + case .quadCurve(let to, let control): + ret += point.quadCurve(to: to, control: control) + point = to + case .curve(let to, let control1, let control2): + ret += point.curve(to: to, control1: control1, control2: control2) + point = to + case .closeSubpath: + if let to = start { + ret += point.line(to: to) + point = to + } + start = nil + } + } + return ret + } + + func length(to maxX: CGFloat) -> CGFloat { + var ret: CGFloat = 0.0 + var start: CGPoint? + var point = CGPoint.zero + var finished = false + + forEach { ele in + if finished { + return + } + switch ele { + case .move(let to): + if to.x > maxX { + finished = true + return + } + if start == nil { + start = to + } + point = to + case .line(let to): + if to.x > maxX { + finished = true + ret += point.line(to: to, x: maxX) + return + } + ret += point.line(to: to) + point = to + case .quadCurve(let to, let control): + if to.x > maxX { + finished = true + ret += point.quadCurve(to: to, control: control, x: maxX) + return + } + ret += point.quadCurve(to: to, control: control) + point = to + case .curve(let to, let control1, let control2): + if to.x > maxX { + finished = true + ret += point.curve(to: to, control1: control1, control2: control2, x: maxX) + return + } + ret += point.curve(to: to, control1: control1, control2: control2) + point = to + case .closeSubpath: + fatalError("Can't include closeSubpath") + } + } + return ret + } +} + +extension CGPoint { + func point(to: CGPoint, x: CGFloat) -> CGPoint { + let a = (to.y - self.y) / (to.x - self.x) + let y = self.y + (x - self.x) * a + return CGPoint(x: x, y: y) + } + + func line(to: CGPoint) -> CGFloat { + dist(to: to) + } + + func line(to: CGPoint, x: CGFloat) -> CGFloat { + dist(to: point(to: to, x: x)) + } + + func quadCurve(to: CGPoint, control: CGPoint) -> CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0.. CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0..= x { + return dist + } else if b.x > x { + dist += a.line(to: b, x: x) + return dist + } else if b.x == x { + dist += a.line(to: b) + return dist + } + + dist += a.line(to: b) + } + return dist + } + + func point(to: CGPoint, t: CGFloat, control: CGPoint) -> CGPoint { + let x = CGPoint.value(x: self.x, y: to.x, t: t, c: control.x) + let y = CGPoint.value(x: self.y, y: to.y, t: t, c: control.y) + + return CGPoint(x: x, y: y) + } + + func curve(to: CGPoint, control1: CGPoint, control2: CGPoint) -> CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0.. CGFloat { + var dist: CGFloat = 0 + let steps: CGFloat = 100 + + for i in 0..= x { + return dist + } else if b.x > x { + dist += a.line(to: b, x: x) + return dist + } else if b.x == x { + dist += a.line(to: b) + return dist + } + + dist += a.line(to: b) + } + + return dist + } + + func point(to: CGPoint, t: CGFloat, control1: CGPoint, control2: CGPoint) -> CGPoint { + let x = CGPoint.value(x: self.x, y: to.x, t: t, c1: control1.x, c2: control2.x) + let y = CGPoint.value(x: self.y, y: to.y, t: t, c1: control1.y, c2: control2.x) + + return CGPoint(x: x, y: y) + } + + static func value(x: CGFloat, y: CGFloat, t: CGFloat, c: CGFloat) -> CGFloat { + var value: CGFloat = 0.0 + // (1-t)^2 * p0 + 2 * (1-t) * t * c1 + t^2 * p1 + value += pow(1-t, 2) * x + value += 2 * (1-t) * t * c + value += pow(t, 2) * y + return value + } + + static func value(x: CGFloat, y: CGFloat, t: CGFloat, c1: CGFloat, c2: CGFloat) -> CGFloat { + var value: CGFloat = 0.0 + // (1-t)^3 * p0 + 3 * (1-t)^2 * t * c1 + 3 * (1-t) * t^2 * c2 + t^3 * p1 + value += pow(1-t, 3) * x + value += 3 * pow(1-t, 2) * t * c1 + value += 3 * (1-t) * pow(t, 2) * c2 + value += pow(t, 3) * y + return value + } +} +