Skip to content

Commit

Permalink
Fix line chart view indicator point (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
xspyhack authored and AppPear committed Jan 22, 2020
1 parent 37c51d9 commit 88db9ae
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 18 deletions.
19 changes: 1 addition & 18 deletions Sources/SwiftUICharts/LineChart/Line.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
251 changes: 251 additions & 0 deletions Sources/SwiftUICharts/LineChart/Path+QuadCurve.swift
Original file line number Diff line number Diff line change
@@ -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..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps
let a = point(to: to, t: t0, control: control)
let b = point(to: to, t: t1, control: control)

dist += a.line(to: b)
}
return dist
}

func quadCurve(to: CGPoint, control: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100

for i in 0..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps
let a = point(to: to, t: t0, control: control)
let b = point(to: to, t: t1, control: control)

if a.x >= 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..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps

let a = point(to: to, t: t0, control1: control1, control2: control2)
let b = point(to: to, t: t1, control1: control1, control2: control2)

dist += a.line(to: b)
}

return dist
}

func curve(to: CGPoint, control1: CGPoint, control2: CGPoint, x: CGFloat) -> CGFloat {
var dist: CGFloat = 0
let steps: CGFloat = 100

for i in 0..<Int(steps) {
let t0 = CGFloat(i) / steps
let t1 = CGFloat(i+1) / steps

let a = point(to: to, t: t0, control1: control1, control2: control2)
let b = point(to: to, t: t1, control1: control1, control2: control2)

if a.x >= 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
}
}

0 comments on commit 88db9ae

Please sign in to comment.