7

I've drawn a graph using a UIBezierPath. I can fill the area under the graph with a solid color but I want to fill the area under the graph with a gradient rather than with a solid color. But I'm not sure how to make the gradient apply only to the graph and not to the whole view, I've read a few questions but not found anything applicable.

enter image description here

This is the main graph drawing code:

// Draw the graph
UIBezierPath *barGraph = [UIBezierPath bezierPath];
barGraph.lineWidth = 1.0f;
[blueColor setStroke];
TCDataPoint *dataPoint = self.testData[0];
CGFloat x = [self convertTimeToXPoint:dataPoint.time];
CGFloat y = [self convertDataToYPoint:dataPoint.dataUsage];
CGPoint plotPoint = CGPointMake(x,y);
[barGraph moveToPoint:plotPoint];
for (int ii = 1; ii < [self.testData count]; ++ii)
{
    dataPoint = self.testData[ii];
    x = [self convertTimeToXPoint:dataPoint.time];
    y = [self convertDataToYPoint:dataPoint.dataUsage];
    plotPoint = CGPointMake(x, y);
    [barGraph addLineToPoint:plotPoint];
}
[barGraph stroke];

I've been attempting to fill the graph by experimenting with code like the following, but to be honest am not %100 sure what I'm doing despite going through various tutorials and documentation:

[barGraph closePath];

CGFloat colors [] = {
    1.0, 0.0, 0.0, 1.0,
    1.0, 1.0, 1.0, 1.0
};

CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace);

CGPoint startPoint = CGPointMake(CGRectGetMidX(self.bounds), self.topY);
CGPoint endPoint = CGPointMake(CGRectGetMidX(self.bounds), self.bottomY);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGGradientRelease(gradient);
Gruntcakes
  • 37,738
  • 44
  • 184
  • 378

1 Answers1

11

The easiest approach is to use the original graph bezier path to help you construct a mask and impose this mask on a gradient layer. Now impose the graph layer on top of the gradient layer.

So, for example, make a CAShapeLayer, close the graph bezier path, set its path as the shape layer's path, and fill it black. Now you have a mask that is the shape of the area under the graph. Now make a CAGradientLayer and make the CAShapeLayer its mask. In front of that, place the actual graph.

So for example (EDITED):

enter image description here

Here's the code I used to create that drawing (my bezier path is very simple, just four points joined by three lines, but you can see clearly that the area under it is a gradient):

CAShapeLayer* shape = [[CAShapeLayer alloc] init];
shape.frame = self.graph.bounds;
CGFloat h = shape.frame.size.height;
CGFloat w = shape.frame.size.width;

NSArray* points = @[
                    [NSValue valueWithCGPoint:CGPointMake(0,h-50)],
                    [NSValue valueWithCGPoint:CGPointMake(70,h-100)],
                    [NSValue valueWithCGPoint:CGPointMake(140,h-75)],
                    [NSValue valueWithCGPoint:CGPointMake(w,h-150)],
                    ];

UIBezierPath* p = [UIBezierPath new];
[p moveToPoint:[points[0] CGPointValue]];
for (NSInteger i = 1; i < points.count; i++)
    [p addLineToPoint:[points[i] CGPointValue]];

shape.path = p.CGPath;
shape.strokeColor = [UIColor blackColor].CGColor;
shape.lineWidth = 2;
shape.fillColor = nil;

CAGradientLayer* grad = [[CAGradientLayer alloc] init];
grad.frame = self.graph.bounds;
grad.colors = @[(id)[UIColor blueColor].CGColor,
                (id)[UIColor yellowColor].CGColor];

CAShapeLayer* mask = [[CAShapeLayer alloc] init];
mask.frame = self.graph.bounds;
[p addLineToPoint:CGPointMake(w,h)];
[p addLineToPoint:CGPointMake(0,h)];
[p closePath];
mask.path = p.CGPath;
mask.fillColor = [UIColor blackColor].CGColor;
grad.mask = mask;

[self.graph.layer addSublayer:grad];
[self.graph.layer addSublayer:shape];
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • See also this: http://stackoverflow.com/questions/9249921/how-to-create-uibezierpath-gradient-fill – matt Apr 21 '14 at 17:48
  • And this: http://stackoverflow.com/questions/22986815/sine-cashapelayer-as-a-mask-of-calayer – matt Apr 21 '14 at 17:48
  • Thank you, I'll experiment with your suggestions. In the code I posted, why doesn't the call to CGContectClip limit the subsequent drawing of the gradient to the graph path? – Gruntcakes Apr 21 '14 at 18:38
  • Update - I replaced the call to CGContextClip with [barGraph addClip] and now its doing as I want. But I'll experiment with your suggestions as a learning exercise. – Gruntcakes Apr 21 '14 at 18:40
  • Nice. Clipping paths can be a bit tricky the first time round; see my discussion here: http://www.apeth.com/iOSBook/ch15.html#_clipping – matt Apr 21 '14 at 18:43
  • Actually I found out my code was only giving the *appearance* of working because the first and last plot points were both on the 0 of y axis and when closing the path it worked. But change one of those points and it no longer does as the closing of the path intersects the graph.So I'll digest your material and apply your answer. Thanks – Gruntcakes Apr 21 '14 at 23:15
  • Yes, I thought of that too. So the mask / clipping path cannot always be exactly the same as merely closing the graph path. But it is still based on the graph path in a fairly obvious way. – matt Apr 22 '14 at 00:17
  • Edited my answer (and the resulting graph) to be a tiny bit more sophisticated about the notion "the area under the graph". – matt Apr 22 '14 at 00:40
  • Thanks for taking so much time with attention to your answer – Gruntcakes Apr 22 '14 at 18:48
  • @Piepants It was fun! – matt Apr 22 '14 at 18:55
  • How can I achieve the same result with the gradient but to use the gradient with blur effect instead of color gradient. – Reza.Ab Feb 18 '16 at 17:49
  • 1
    @Reza.Ab Draw the gradient and blur it. That would be a different question. You can achieve any effect you know how to draw. This question is not about any particular effect (though a gradient is used by way of example because it's what the OP wanted), but about how to make that effect appear only in a region defined by the bezier path. – matt Feb 18 '16 at 18:06
  • I understand this is a different question but the reason I asked it is that I want to apply a blur effect on everything visible in the view except the close path that has been drawn in view. So can I simply apply any effect that I want on the view outside the close path? – Reza.Ab Feb 18 '16 at 18:11
  • 1
    @Reza.Ab You're just making me repeat myself (in two different places). Saying the same thing over and over won't change anything. I've already answered this question. Just try it! – matt Feb 18 '16 at 18:16