1

I have some points like (x1,y1), (x2,y2), (x3,y3)...

Now I want to draw a chart with smooth curve?

I'm trying to draw as below

-(void)drawPrices
{
    NSInteger count = self.prices.count;
    
    UIBezierPath *path = [UIBezierPath bezierPath]; 
    path.lineCapStyle = kCGLineCapRound;

    for(int i=0; i<count-1; i++)
    {
        CGPoint controlPoint[2];
        
        CGPoint p = [self pointWithIndex:i inData:self.prices];
        if(i==0)
        {
            [path moveToPoint:p];
        }

        CGPoint nextPoint, previousPoint, m;
        nextPoint = [self pointWithIndex:i+1 inData:self.prices];
        previousPoint = [self pointWithIndex:i-1 inData:self.prices];
        
        if(i > 0) {
            m.x = (nextPoint.x - previousPoint.x) / 2;
            m.y = (nextPoint.y - previousPoint.y) / 2;
        } else {
            m.x = (nextPoint.x - p.x) / 2;
            m.y = (nextPoint.y - p.y) / 2;
        }
        
        controlPoint[0].x = p.x + m.x * 0.2;
        controlPoint[0].y = p.y + m.y * 0.2;
        
        // Second control point
        nextPoint = [self pointWithIndex:i+2 inData:self.prices];
        previousPoint = [self pointWithIndex:i inData:self.prices];
        p = [self pointWithIndex:i + 1 inData:self.prices];
        m = zeroPoint;
        
        if(i < self.prices.count - 2) {
            m.x = (nextPoint.x - previousPoint.x) / 2;
            m.y = (nextPoint.y - previousPoint.y) / 2;
        } else {
            m.x = (p.x - previousPoint.x) / 2;
            m.y = (p.y - previousPoint.y) / 2;
        }
        
        controlPoint[1].x = p.x - m.x * 0.2;
        controlPoint[1].y = p.y - m.y * 0.2;
        
        [path addCurveToPoint:p controlPoint1:controlPoint[0] controlPoint2:controlPoint[1]];
    }
    
    CAShapeLayer *lineLayer = [CAShapeLayer layer];
    lineLayer.path = path.CGPath;
    lineLayer.lineWidth = LINE_WIDTH;
    lineLayer.strokeColor = _priceColor.CGColor;
    lineLayer.fillColor = [UIColor clearColor].CGColor;

    [self.layer addSublayer:lineLayer];
}

but in some situation, the line will "go back" like

Is there any better way to do that?

Community
  • 1
  • 1
Rick
  • 235
  • 4
  • 13

2 Answers2

12

I've got you another solution which I used to success. It requires SpriteKit, which has a fantastic tool called SKKeyFrameSequence which is capable of spline interpolation as shown in this tutorial provided by Apple.

So the idea is that you'll create the correct SKKeyFrameSequence object like this (assuming your data is in an array of (CGFloat, CGFloat) (x, y) tuples):

let xValues = data.map { $0.0 }
let yValues = data.map { $0.1 }
let sequence = SKKeyFrameSequence(keyFrameValues: yValues,
                                  times: xValues.map { NSNumber($0) })
sequence.interpolationMode = .spline

let xMin = xValues.min()!
let xMax = xValues.max()!

Then, if you divide the interpolated spline into 200 pieces (change this value if you want, but for me this resulted in smooth waves to human eyes), you can draw a path consisting of small lines.

var splinedValues = [(CGFloat, CGFloat)]()
stride(from: xMin, to: xMax, by: (xMax - xMin) / 200).forEach {
    splinedValues.append((CGFloat($0),
                          sequence.sample(atTime: CGFloat($0)) as! CGFloat))
}

Then finally the path (I will use SwiftUI, but you can use UIKit the same way too.):

Path { path in
    path.move(to: CGPoint(x: splinedValues[0].0, y: splinedValues[0].1))

    for splineValue in splinedValues.dropFirst() {
        path.addLine(to: CGPoint(x: splineValue.0, y: splineValue.1))
    }
}

For the values

[(1, 4), (2, 6), (3, 7), (4, 5), (5, 3), (6, -1), (7, -2), (8, -2.5), (9, -2), (10, 0), (11, 4)]

I've gotten a graph like this with the method described above: (I added the points too to evaluate the result better)

enter image description here

iSpain17
  • 2,502
  • 3
  • 17
  • 26
0

I find the answer at Draw Graph curves with UIBezierPath

And try implement with the code

+ (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;
}

Thanks user1244109 ^_^.

Community
  • 1
  • 1
Rick
  • 235
  • 4
  • 13