Question

I want to make a graph in a UIView that shows numerical data. So I need to draw axis, a few coordinate lines, some tick marks, and then the data as connected straight lines. The data might typically consist of a few hundred x values in the range -500. to +1000. with corresponding y values in the range 300. to 350.

So I thought a good approach would be to transform the coordinates of the UIView so (for the example values given) the left side of the view is -500, and right side is 1000, the top is 400 and the bottom is 300. And y increases upwards. Then in drawRect: I could write a bunch of CGContextMoveToPoint() and CGContextAddLineToPoint() statements with my own coordinate system and not have to mentally translate each call to the UIView coordinates.

I wrote the following function to generate my own CGContextRef but it doesn't do what I expected. I've been trying variations on it for a couple days now and wasting so much time. Can someone say how to fix it? I realize I can't get clear in my mind whether the transform is supposed to specify the UIView coordinates in terms of my coordinates, or vice versa, or something else entirely.

static inline CGContextRef myCTX(CGRect rect, CGFloat xLeft, CGFloat xRight, CGFloat yBottom, CGFloat yTop) {
    CGAffineTransform ctxTranslate = CGAffineTransformMakeTranslation(xLeft, rect.size.height - yTop);
    CGAffineTransform ctxScale = CGAffineTransformMakeScale( rect.size.width / (xRight - xLeft), -rect.size.height / (yTop - yBottom) );  //minus so y increases toward top
    CGAffineTransform combinedTransform = CGAffineTransformConcat(ctxTranslate, ctxScale);
    CGContextRef c = UIGraphicsGetCurrentContext();
    CGContextConcatCTM(c, combinedTransform);
    return c;
}

The way I'm using this is that inside drawRect I just have:

CGContextRef ctx = myCTX(rect, self.xLeft, self.xRight, self.yBottom, self.yTop);

and then a series of statements like:

CGContextAddLineToPoint(ctx, [x[i] floatValue], [y[i] floatValue]);
Was it helpful?

Solution

I figured this out by experimenting. The transform requires 3 steps instead of 2 (or, if not required, at least it works this way):

static inline CGContextRef myCTX(CGRect rect, CGFloat xLeft, CGFloat xRight, CGFloat yBottom, CGFloat yTop) {
    CGAffineTransform translate1 =  CGAffineTransformMakeTranslation(-xLeft, -yBottom);
    CGAffineTransform scale =       CGAffineTransformMakeScale( rect.size.width / (xRight - xLeft), -rect.size.height / (yTop - yBottom) );
    CGAffineTransform transform =   CGAffineTransformConcat(translate1, scale);
    CGAffineTransform translate2 =  CGAffineTransformMakeTranslation(1, rect.size.height);
    transform =                     CGAffineTransformConcat(transform, translate2);
    CGContextRef c = UIGraphicsGetCurrentContext();
    CGContextConcatCTM(c, transform);
    return c;
}

You use this function inside drawRect. In my case the xLeft, xRight, etc. values are properties of a UIView subclass and are set by the viewController. So the view's drawRect looks like so:

- (void)drawRect:(CGRect)rect
{
    CGContextRef c = UIGraphicsGetCurrentContext();
    CGContextSaveGState(c);
    CGContextRef ctx = myCTX(rect, self.xLeft, self.xRight, self.yBottom, self.yTop);
    …    
    all of the CGContextMoveToPoint(), CGContextAddLineToPoint(), calls to 
    draw your desired lines, rectangles, curves, etc. but not stroke or fill them
    …

    CGContextRestoreGState(c);
    CGContextSetLineWidth(c, 1);
    CGContextStrokePath(c);
}

The CGContextSetLineWidth call isn't needed if you want a line width of 1. If you don't restore the graphics state before strokePath the path width is affected by the scaling.

Now I have to figure out how to draw text onto the view.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top