Draw Graph curves with UIBezierPath
I'm drawing a graph in my application. My problem is that I want to draw line joining vertex points as curves. Currently I'm drawing them with UIBezierPath
's function addLineToPoint:
. I want to draw them as curves. I'm well aware that UIBezierPath
has following two functions to support this feature.
Cubic curve:addCurveToPoint:controlPoint1:controlPoint2:
Quadratic curve:addQuadCurveToPoint:controlPoint:
But problem is that I don't have control points. All i have is two end points. Neither I found a method/formula to determine control points. Can anyone help me here? I will appreciate if someone can suggest some alternative...
Expand on Abid Hussain's answer on Dec 5 '12.
i implemeted the code, and it worked but result looked like that:
With little adjustment, i was able to get what i wanted:
What i did was addQuadCurveToPoint to mid points as well, using mid-mid points as control points. Below is the code based on Abid's sample; hope it helps someone.
+ (UIBezierPath *)quadCurvedPathWithPoints:(NSArray *)points
{
UIBezierPath *path = [UIBezierPath bezierPath];
NSValue *value = points[0];
CGPoint p1 = [value CGPointValue];
[path moveToPoint:p1];
if (points.count == 2) {
value = points[1];
CGPoint p2 = [value CGPointValue];
[path addLineToPoint:p2];
return path;
}
for (NSUInteger i = 1; i < points.count; i++) {
value = points[i];
CGPoint p2 = [value CGPointValue];
CGPoint midPoint = midPointForPoints(p1, p2);
[path addQuadCurveToPoint:midPoint controlPoint:controlPointForPoints(midPoint, p1)];
[path addQuadCurveToPoint:p2 controlPoint:controlPointForPoints(midPoint, p2)];
p1 = p2;
}
return path;
}
static CGPoint midPointForPoints(CGPoint p1, CGPoint p2) {
return CGPointMake((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
}
static CGPoint controlPointForPoints(CGPoint p1, CGPoint p2) {
CGPoint controlPoint = midPointForPoints(p1, p2);
CGFloat diffY = abs(p2.y - controlPoint.y);
if (p1.y < p2.y)
controlPoint.y += diffY;
else if (p1.y > p2.y)
controlPoint.y -= diffY;
return controlPoint;
}
Using @user1244109's answer I implement a better algorithm in Swift 5.
class GraphView: UIView {
var data: [CGFloat] = [2, 6, 12, 4, 5, 7, 5, 6, 6, 3] {
didSet {
setNeedsDisplay()
}
}
func coordYFor(index: Int) -> CGFloat {
return bounds.height - bounds.height * data[index] / (data.max() ?? 0)
}
override func draw(_ rect: CGRect) {
let path = quadCurvedPath()
UIColor.black.setStroke()
path.lineWidth = 1
path.stroke()
}
func quadCurvedPath() -> UIBezierPath {
let path = UIBezierPath()
let step = bounds.width / CGFloat(data.count - 1)
var p1 = CGPoint(x: 0, y: coordYFor(index: 0))
path.move(to: p1)
drawPoint(point: p1, color: UIColor.red, radius: 3)
if (data.count == 2) {
path.addLine(to: CGPoint(x: step, y: coordYFor(index: 1)))
return path
}
var oldControlP: CGPoint?
for i in 1..<data.count {
let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
drawPoint(point: p2, color: UIColor.red, radius: 3)
var p3: CGPoint?
if i < data.count - 1 {
p3 = CGPoint(x: step * CGFloat(i + 1), y: coordYFor(index: i + 1))
}
let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)
path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)
p1 = p2
oldControlP = antipodalFor(point: newControlP, center: p2)
}
return path;
}
/// located on the opposite side from the center point
func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
guard let p1 = point, let center = center else {
return nil
}
let newX = 2 * center.x - p1.x
let diffY = abs(p1.y - center.y)
let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)
return CGPoint(x: newX, y: newY)
}
/// halfway of two points
func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
}
/// Find controlPoint2 for addCurve
/// - Parameters:
/// - p1: first point of curve
/// - p2: second point of curve whose control point we are looking for
/// - next: predicted next point which will use antipodal control point for finded
func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
guard let p3 = p3 else {
return nil
}
let leftMidPoint = midPointForPoints(p1: p1, p2: p2)
let rightMidPoint = midPointForPoints(p1: p2, p2: p3)
var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)
if p1.y.between(a: p2.y, b: controlPoint.y) {
controlPoint.y = p1.y
} else if p2.y.between(a: p1.y, b: controlPoint.y) {
controlPoint.y = p2.y
}
let imaginContol = antipodalFor(point: controlPoint, center: p2)!
if p2.y.between(a: p3.y, b: imaginContol.y) {
controlPoint.y = p2.y
}
if p3.y.between(a: p2.y, b: imaginContol.y) {
let diffY = abs(p2.y - p3.y)
controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
}
// make lines easier
controlPoint.x += (p2.x - p1.x) * 0.1
return controlPoint
}
func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
color.setFill()
ovalPath.fill()
}
}
extension CGFloat {
func between(a: CGFloat, b: CGFloat) -> Bool {
return self >= Swift.min(a, b) && self <= Swift.max(a, b)
}
}
If you only have two points then you cannot really extrapolate them out into a curve.
If you have more than two points (i.e. several points along a curve) then you can roughly draw a curve to follow them.
If you do the following...
OK, say you have 5 points. p1, p2, p3, p4, and p5. You need to work out the midpoint between each pair of points. m1 = midpoint of p1 and p2 (and so on)...
So you have m1, m2, m3, and m4.
Now you can use the mid points as the end points of the sections of the curve and the points as the control points for a quad curve...
So...
Move to point m1. Add quad curve to point m2 with p2 as a control point.
Add quad curve to point m3 with p3 as a control point.
Add quad curve to point m4 with p4 as a control point.
and so on...
This will get you all apart from the ends of the curve (can't remember how to get them at the moment, sorry).