mgamer
mgamer

Reputation: 14060

Efficient way to draw a graph line by line in CALayer

I need to draw a line chart from values that come to me every half a seconds. I've come up with my custom CALayer for this graph which stores all the previous lines and every two seconds redraws all previous lines and adds one new line. I find this solution non-optimal because there's only need to draw one additional line to the layer, no reason to redraw potentially thousands of previous lines.

What do you think would be the best solution in this case?

Upvotes: 1

Views: 1298

Answers (2)

James
James

Reputation: 184

I am looking at an identical implementation. Graph updates every 500 ms. Similarly I felt uncomfortable drawing the entire graph each iteration. I implemented a solution 'similar' to what Nikolai Ruhe proposed as follows:

First some declarations:

#define TIME_INCREMENT 10
@property (nonatomic) UIImage *lastSnapshotOfPlot;

and then the drawLayer:inContext method of my CALayer delegate

- (void) drawLayer:( CALayer*)layer inContext:(CGContextRef)ctx
{

    // Restore the image of the layer from the last time through, if it exists

    if( self.lastSnapshotOfPlot )
    {
        // For some reason the image is being redrawn upside down!
        // This block of code adjusts the context to correct it.

        CGContextSaveGState(ctx);
        CGContextTranslateCTM(ctx, 0, layer.bounds.size.height);
        CGContextScaleCTM(ctx, 1.0, -1.0);

        // Now we can redraw the image right side up but shifted over a little bit
        // to allow space for the new data

        CGRect r = CGRectMake( -TIME_INCREMENT, 0, layer.bounds.size.width, layer.bounds.size.height );  
        CGContextDrawImage(ctx, r, self.lastSnapshotOfPlot.CGImage );

        // And finally put the context back the way it was

        CGContextRestoreGState(ctx); 
    }

    CGContextStrokePath(ctx);

    CGContextSetLineWidth(ctx, 2.0);
    CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor );
    CGContextBeginPath( ctx );

    // This next section is where I draw the line segment on the extreme right end
    // which matches up with the stored graph on the image.  This part of the code 
    // is application specific and I have only left it here for
    // conceptual reference.  Basically I draw a tiny line segment
    // from the last value to the new value at the extreme right end of the graph.

    CGFloat ppy = layer.bounds.size.height - _lastValue / _displayRange * layer.bounds.size.height;
    CGFloat cpy = layer.bounds.size.height - self.sensorData.currentvalue  / _displayRange * layer.bounds.size.height;

    CGContextMoveToPoint(ctx,layer.bounds.size.width - TIME_INCREMENT, ppy ); // Move to the previous point
    CGContextAddLineToPoint(ctx, layer.bounds.size.width, cpy );  // Draw to the latest point

    CGContextStrokePath(ctx);

    // Finally save the entire current layer to an image.  This will include our latest
    // drawn line segment 

    UIGraphicsBeginImageContext(layer.bounds.size);
    [layer renderInContext: UIGraphicsGetCurrentContext()];
    self.lastSnapshotOfPlot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

}

Is this the most efficient way? I have not been programming in ObjectiveC long enough to know so all suggestions/improvements welcome.

Upvotes: 0

Nikolai Ruhe
Nikolai Ruhe

Reputation: 81878

Use your own NSBitmapContext or UIImage as a backing store. Whenever new data comes in draw to this context and set your layer's contents property to the context's image.

Upvotes: 1

Related Questions