I've been working the last few days on a contraction counter iPhone app. My wife's due for our second child any day now, so i browsed the app store looking for a suitable app for when we have to start timing those things. But alas! They're all pink! What's a red-blooded man to do?

So, since i'm at home waiting around for a baby to arrive, I've made my own contraction counter app. And made it look decidedly un-pink. And gave it a blokey name: 'Contractionator'. And used some awesome manly fonts. Because when I'm about to become a dad to two girls, this app just might be my last chance to man things up a notch. Can you tell i'm a bit bored? Ha...

I'm not sure whether or not to put it on the app store, i might do so just for a laugh. Here's a pic. Take note of the graph in the bottom half, that's what the below code is for:

Contractionator

Anyway, the point of this article is that I had to make a small graph to show how the contractions' frequency and duration are going, and I thought i'd share the code. It's a very simple way of making graphs on the iPhone, but it should get you started if you need to make your own graphs and you don't want to go to the effort of integrating something like CorePlot.

// Render a graph of the given width and height and return it as an image. Main thread only.
- (UIImage*)renderGraph:(int)w h:(int)h {
    // Figure out the font and it's metrics
    UIFont* font = [UIFont fontWithName:@"AUdimat" size:10];
    int fontHeight = [@"ABC123" sizeWithFont:font].height;

    // Constants
    UIColor *lengthCol = [UIColor redColor];
    UIColor *gapCol = [UIColor greenColor];
    UIColor *rangeCol = [UIColor whiteColor];
    int ranges = 9; // Number of points on the Y axis scale
    int pad = 7; // How far to pad in the edges
    int topCount = 15; // How many data points to show
    int rangeWidth = 45; // How wide is the y-axis range labels
    int labelWidth=rangeWidth-10, dashWidth=4, dashInset=rangeWidth-3; // Widths for the y-axis range parts

    // Begin the graphics context
    UIGraphicsBeginImageContextWithOptions(CGSizeMake(w, h), NO, [[UIScreen mainScreen] scale]);
    CGContextRef context = UIGraphicsGetCurrentContext();

    // Look after padding
    CGContextTranslateCTM(context, pad, pad); // Pad everything by a bit
    w-=pad*2; h-=pad*2; // Shrink the size so it thinks it's 10px smaller due to padding

    // Do we have enough data to plot?
    if (self.contractions.count<2) {
        [[UIColor grayColor] setFill];
        [@"Waiting for data" drawInRect:CGRectMake(0, h/2-fontHeight/2, w, fontHeight)
                               withFont:font lineBreakMode:(UILineBreakModeClip) alignment:(UITextAlignmentCenter)];
    } else { // We *do* have enough data
        // Get the top 15 data points (or all points if <= 15)
        NSArray* topOnes = self.contractions.count > topCount ?
            [self.contractions subarrayWithRange:NSMakeRange(self.contractions.count-topCount, topCount)] :
            self.contractions;

        // Find the maximum scale for the y-axis
        int maxScale=0;
        for (Contraction* c in topOnes) { // Find the max lengths of the contractions
            maxScale = MAX(maxScale, (int)round(c.length));
        }
        for (int i=0; i<topOnes.count-1; i++) { // Find the max gap between contractions
            Contraction* c1 = [topOnes objectAtIndex:i];
            Contraction* c2 = [topOnes objectAtIndex:i+1];
            int gapSeconds = round([c2.start timeIntervalSinceDate:c1.start]);
            maxScale = MAX(maxScale, gapSeconds);
        }

        // Tweak the scale so that the gaps between the y-axis range lines are sensible
        int rangeGap = maxScale/(ranges-1); // Gap between range lines
        if (rangeGap<15) rangeGap=15;
        else if (rangeGap<30) rangeGap=30;
        else if (rangeGap<5*60) rangeGap=ceil(rangeGap/60.0)*60.0; // Every minute up to 5 mins
        else if (rangeGap<10*60) rangeGap=10*60; // 10 min gap
        else if (rangeGap<15*60) rangeGap=15*60; // 15 min gap
        else if (rangeGap<30*60) rangeGap=30*60; // 30 min gap
        else rangeGap=ceil(rangeGap/3600.0)*3600.0; // Hourly gaps
        maxScale = rangeGap*(ranges-1); // Convert from gap back to max scale again

        // Draw the y-axis range lines and labels
        [rangeCol setFill];
        [rangeCol setStroke];
        for (int range=0; range<ranges; range++) {
            int thisRangeY = h-h*range/(ranges-1);
            [self line:w-dashInset y:thisRangeY x2:w-dashInset+dashWidth y2:thisRangeY];
            [[self hms:maxScale*range/(ranges-1)] drawInRect:CGRectMake(w-labelWidth, thisRangeY-fontHeight/2, labelWidth, fontHeight)
                                                    withFont:font lineBreakMode:UILineBreakModeTailTruncation alignment:UITextAlignmentLeft];
        }

        // Draw the labels for the series
        [lengthCol setFill];
        [@"Duration" drawInRect:CGRectMake(w/2-105, 0, 100, fontHeight) withFont:font lineBreakMode:UILineBreakModeClip alignment:UITextAlignmentRight];
        [gapCol setFill];
        [@"Frequency" drawInRect:CGRectMake(w/2+5, 0, 100, fontHeight) withFont:font lineBreakMode:UILineBreakModeClip alignment:UITextAlignmentLeft];

        // Draw the graph for contraction lengths
        int cWid = (w-rangeWidth)/(topOnes.count-1);
        {
            [lengthCol setStroke];
            int i=0;
            int lastX=-1, lastY=-1;
            for (Contraction* c in topOnes) {
                int thisX = i*cWid;
                int thisY = h-h*round(c.length)/maxScale;
                [self dot:thisX y:thisY];
                if (lastX>=0) {
                    [self line:lastX y:lastY x2:thisX y2:thisY];
                }
                i++;
                lastX = thisX;
                lastY = thisY;
            }
        }

        // Draw the graph for contraction ggaps
        {
            [gapCol setStroke];
            int lastX=-1, lastY=-1;
            for (int i=0; i<topOnes.count-1; i++) {
                Contraction* c1 = [topOnes objectAtIndex:i];
                Contraction* c2 = [topOnes objectAtIndex:i+1];
                int gapSeconds = round([c2.start timeIntervalSinceDate:c1.start]);

                int thisX = i*cWid + cWid/2;
                int thisY = h-h*gapSeconds/maxScale;
                [self dot:thisX y:thisY];
                if (lastX>=0) {
                    [self line:lastX y:lastY x2:thisX y2:thisY];
                }
                lastX = thisX;
                lastY = thisY;
            }
        }
    }

    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
}

// Draw a line from point to point
- (void)line:(int)x y:(int)y x2:(int)x2 y2:(int)y2 {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, x, y);
    CGContextAddLineToPoint(context, x2, y2);
    CGContextStrokePath(context);
}

// Draw a dot at a certain point by doing a line with round caps
- (void)dot:(int)x y:(int)y {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context); // So the setlinecap+setlinewidth don't leak out beyond this function
    CGContextSetLineCap(context, kCGLineCapRound);
    CGContextSetLineWidth(context, 5);
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, x, y);
    CGContextAddLineToPoint(context, x, y);
    CGContextStrokePath(context);
    CGContextRestoreGState(context);
}

Hope someone out there finds this useful!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.