1
votes

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]);
1
What are the values of self.xLeft, self.xRight etc? - Alfonso
self.xLeft = -500.f; self.xRight = 1000.f; self.yBottom = 300.f; self.yTop = 400.f; - RobertL
Ok, in this case I think you'd need to translate by -xLeft, i.e. shift by 500px to the right, because you'll start drawing to the left of the original rect (-500). - Alfonso
Also the y translation should probably be -yBottom, since this is before scaling, so you'd still drawing "upside down" at this point. Then in the second step you turn the drawing upside down. - Alfonso
Thanks for your thoughts. But that still doesn't do it. I've been experimenting with this and other variations and I've come to realize that this whole approach may be no good because it's scaling the line width. So with the x range several times larger than self.bounds.size.width the line thickness is greater in the x direction than the y direction and so it produces a wide line. - RobertL

1 Answers

1
votes

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.