Implementing a Simple Line Graph in iOS - objective-c

What would be the best way to implament a simple line graph inside my iPad app. Im only looking to plot 4-8 points and the graph does need to be fancy looking. HTML5 will be good enough to use for what I need.
How would I go about implementing HTML code to plot a line graph?
I have been looking at using Google Charting Tools, but any other suggestions that I could use?

Here is a very basic class that will give you the basic line. It takes an NSArray of NSNumbers for the Y axis. It should provide a good starting point for you as it is as simple as it gets. You might consider adding labels to the points and axis markers to give a sense of scale.
Header:
#import <UIKit/UIKit.h>
#interface SOGenericGraphView : UIView {
NSArray *yValues;
}
#property (strong, atomic) NSArray *yValues;
#end
Implementation:
#import "SOGenericGraphView.h"
#implementation SOGenericGraphView
#synthesize yValues;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor whiteColor];
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
[super drawRect:rect];
CGRect insetRect = CGRectInset(self.frame, 10, 15);
double maxSpeed = [[yValues valueForKeyPath:#"#max.doubleValue"] doubleValue];
CGFloat yRatio = insetRect.size.height/maxSpeed;
CGFloat xRatio = insetRect.size.width/(yValues.count-1);
UIBezierPath *sparkline = [UIBezierPath bezierPath];
for (int x = 0; x< yValues.count; x++) {
CGPoint newPoint = CGPointMake(x*xRatio + insetRect.origin.x, insetRect.size.height - (yRatio*[[yValues objectAtIndex:x] doubleValue] - insetRect.origin.y));
if (x == 0) {
[sparkline moveToPoint:newPoint];
}
else {
[sparkline addLineToPoint:newPoint];
}
}
[[UIColor redColor] set];
[sparkline stroke];
}
#end

Related

How to implement a multiline NSTokenField?

I want to implement an NSTokenField that spans multiple lines. For example:
Multi-line NSTokenField
I found a some sample code of multi-line NSTokenField on the internet:
IBOutlet NSTokenField *tokenField;
- (void)awakeFromNib
{
[[tokenField cell] setWraps:YES];
}
made my class delegate of the NSTokenField and implement the following method
- (void)controlTextDidChange:(NSNotification *)obj {
NSRect oldTokenFieldFrame = [tokenField frame];
NSRect tokenFieldBounds = [tokenField bounds];
float height = oldTokenFieldFrame.size.height;
tokenFieldBounds.size.height = CGFLOAT_MAX;
NSSize cellSize = [[tokenField cell] cellSizeForBounds:tokenFieldBounds];
float y = oldTokenFieldFrame.origin.y + height - cellSize.height;
[tokenField setFrame:NSMakeRect(oldTokenFieldFrame.origin.x,
y,
oldTokenFieldFrame.size.width,
cellSize.height)];
}
but this code does not work correctly.
Could you please help me with this issue?
Thank you in advance.
For everybody who is just looking for an answer with working code, I've found this super short solution. Here we go:
#import "MyExpandingTokenField.h"
#implementation MyExpandingTokenField
- (NSSize)intrinsicContentSize {
NSSize intrinsicContentSize = [self sizeThatFits:NSMakeSize(self.frame.size.width, CGFLOAT_MAX)];
intrinsicContentSize = NSMakeSize(intrinsicContentSize.width, intrinsicContentSize.height + 5);
return intrinsicContentSize;
}
- (void)textDidChange:(NSNotification *)notification {
[super textDidChange:notification];
[self invalidateIntrinsicContentSize];
}
#end

CGContextFillRects: CGRect* not working. Assigned CGRect items do not get saved

I am trying to save an array of CGRect to use with CGContextFillRects but the CGRect variables I assign to my minorPlotLines array don't seem to get saved. By the time the object here draws itself minorPlotLines is empty! Does anyone know what is going on?
#interface GraphLineView () {
int numberOfLines;
CGRect *minorPlotLines;
}
#end
#implementation GraphLineView
- (instancetype) initWithFrame: (CGRect) frame {
self = [super initWithFrame:frame];
if (self) {
// Init code
[self setupView];
}
return self;
}
- (instancetype) initWithCoder: (NSCoder *) aDecoder {
if(self == [super initWithCoder:aDecoder]){
[self setupView];
}
return self;
}
- (void) dealloc {
free(minorPlotLines);
}
- (void) setupView {
numberOfLines = 40;
minorPlotLines = malloc(sizeof(struct CGRect)*40);
for(int x = 0; x < numberOfLines; x += 2){
//minorPlotLines[x] = *(CGRect*)malloc(sizeof(CGRect));
minorPlotLines[x] = CGRectMake(x*(self.frame.size.width/numberOfLines), 0, 2, self.frame.size.height);
// minorPlotLines[x+1] = *(CGRect*)malloc(sizeof(CGRect));
minorPlotLines[x+1] = CGRectMake(0, x*(self.frame.size.height/numberOfLines), self.frame.size.width, 2);
}
[self setNeedsDisplay];
}
- (void) drawRect:(CGRect)rect {
// Drawing code
[super drawRect:rect];
for(int x = 0; x < numberOfLines; x += 2){
NSLog(#"R %d = %f", x, minorPlotLines[x].origin.x);
NSLog(#"R %d = %f", x+1, minorPlotLines[x+1].origin.y);
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [[UIColor yellowColor] CGColor]);
CGContextFillRects(context, minorPlotLines, numberOfLines);
}
I tried pulling your code (to construct the contents of minorPlotLines and later read the contents back out) into another project, and it does seem to preserve the contents just fine, so your basic code itself seems sound.
I would check to make sure that you actually have a non-zero frame at the time you're constructing the array minorPlotLines (i.e. in -setupView). It's quite common for early-stage UI class loading callbacks to be called at a time when your class is only partially constructed (e.g. UIViewController class' callback -viewDidLoad), leaving you no choice but to defer certain decisions until later in the loading process. Layout in particular happens relatively late in the game, and since your -setupView method is called right within an -init method I'm guessing the framework hasn't provided any layout to your class yet, and hence it has no usable frame (i.e. your frame is effectively equivalent to CGRectZero).

CAEmitterLayer not visible in my View Controller

I'm trying to add a simple particle effect overlay to one of my UIViewController instances. I've followed a couple of tutorials I found, but neither of them deal with view controllers and their storyboards are just a view out on its own with no controller, which is confusing.
Here's my code, I'm just trying to figure out why I can't see the particle effects. What am I missing? The view itself is definitely there and added (if I change its color or something I can see it), it's just empty, and isn't showing any particle effects. The image file referenced is definitely in the project and target, so what else have I done wrong here? Do I need to add the CAEmitterLayer to rainView somehow? The tutorials didn't offer any help on this part!
RainfallOverlay.h
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#interface RainfallOverlay : UIView {
CAEmitterLayer *rainEmitter;
}
#end
RainfallOverlay.m
#import "RainfallOverlay.h"
#implementation RainfallOverlay
- (void) awakeFromNib
{
rainEmitter = (CAEmitterLayer *) self.layer;
rainEmitter.emitterPosition = CGPointMake(160, 100);
rainEmitter.emitterSize = CGSizeMake(10, 10);
rainEmitter.renderMode = kCAEmitterLayerAdditive;
CAEmitterCell *rain = [CAEmitterCell emitterCell];
rain.birthRate = 200;
rain.lifetime = 2.0;
rain.lifetimeRange = 1.5;
rain.color = [[UIColor colorWithRed: 0.2 green: 0.4 blue: 0.8 alpha: 0.1] CGColor];
rain.contents = (id) [[UIImage imageNamed: #"Particles_rain.png"] CGImage];
rain.name = #"rain";
rain.velocity = 150;
rain.velocityRange = 100;
rain.emissionRange = M_PI_2;
rain.emissionLongitude = 0.025 * 180 / M_PI;
rain.scaleSpeed = 0;
rain.spin = 0.5;
rainEmitter.emitterCells = [NSArray arrayWithObject: rain];
}
#end
ViewController.m viewDidLoad
RainfallOverlay *rainView = [[RainfallOverlay alloc] initWithFrame: CGRectMake(0, 0, 320, 250)];
[rainView setUserInteractionEnabled: NO];
[self.view bringSubviewToFront: rainView];
[self.view addSubview: rainView];
There are 2 problems:
awakeFromNib would only be called if the view is loaded from a nib file. In your case, you have to implement initWithFrame.
You have to override layerClass to that self.layer returns a CAEmitterLayer. Just casting the layer to CAEmitterLayer does not work.
So your RainfallOverlay implementation should look like this:
+ (Class)layerClass
{
return [CAEmitterLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
rainEmitter = (CAEmitterLayer *) self.layer;
// ... remaining setup
}
return self;
}

NSScrollView infinite / endless scroll | subview reuse

I'm searching for a way to implement something like reusable cells for UI/NSTableView but for NSScrollView. Basically I want the same like the WWDC 2011 video "Session 104 - Advanced Scroll View Techniques" but for Mac.
I have several problems realizing this. The first: NSScrollView doesn't have -layoutSubviews. I tried to use -adjustScroll instead but fail in setting a different contentOffset:
- (NSRect)adjustScroll:(NSRect)proposedVisibleRect {
if (proposedVisibleRect.origin.x > 600) {
// non of them work properly
// proposedVisibleRect.origin.x = 0;
// [self setBoundsOrigin:NSZeroPoint];
// [self setFrameOrigin:NSZeroPoint];
// [[parentScrollView contentView] scrollPoint:NSZeroPoint];
// [[parentScrollView contentView] setBoundsOrigin:NSZeroPoint];
}
return proposedVisibleRect;
}
The next thing I tried was to set a really huge content view with a width of millions of pixel (which actually works in comparison to iOS!) but now the question is, how to install a reuse-pool?
Is it better to move the subviews while scrolling to a new position or to remove all subviews and insert them again? and how and where should I do that?
As best I can tell, -adjustScroll: is not where you want to tap into the scrolling events because it doesn't get called universally. I think -reflectScrolledClipView: is probably a better hookup point.
I cooked up the following example that should hit the high points of one way to do a view-reusing scroll view. For simplicity, I set the dimensions of the scrollView's documentView to "huge", as you suggest, rather than trying to "fake up" the scrolling behavior to look infinite. Obviously drawing the constituent tile views for real is up to you. (In this example I created a dummy view that just fills itself with red with a blue outline to convince myself that everything was working.) It came out like this:
// For the header file
#interface SOReuseScrollView : NSScrollView
#end
// For the implementation file
#interface SOReuseScrollView () // Private
- (void)p_updateTiles;
#property (nonatomic, readonly, retain) NSMutableArray* p_reusableViews;
#end
// Just a small diagnosting view to convince myself that this works.
#interface SODiagnosticView : NSView
#end
#implementation SOReuseScrollView
#synthesize p_reusableViews = mReusableViews;
- (void)dealloc
{
[mReusableViews release];
[super dealloc];
}
- (NSMutableArray*)p_reusableViews
{
if (nil == mReusableViews)
{
mReusableViews = [[NSMutableArray alloc] init];
}
return mReusableViews;
}
- (void)reflectScrolledClipView:(NSClipView *)cView
{
[super reflectScrolledClipView: cView];
[self p_updateTiles];
}
- (void)p_updateTiles
{
// The size of a tile...
static const NSSize gGranuleSize = {250.0, 250.0};
NSMutableArray* reusableViews = self.p_reusableViews;
NSRect documentVisibleRect = self.documentVisibleRect;
// Determine the needed tiles for coverage
const CGFloat xMin = floor(NSMinX(documentVisibleRect) / gGranuleSize.width) * gGranuleSize.width;
const CGFloat xMax = xMin + (ceil((NSMaxX(documentVisibleRect) - xMin) / gGranuleSize.width) * gGranuleSize.width);
const CGFloat yMin = floor(NSMinY(documentVisibleRect) / gGranuleSize.height) * gGranuleSize.height;
const CGFloat yMax = ceil((NSMaxY(documentVisibleRect) - yMin) / gGranuleSize.height) * gGranuleSize.height;
// Figure out the tile frames we would need to get full coverage
NSMutableSet* neededTileFrames = [NSMutableSet set];
for (CGFloat x = xMin; x < xMax; x += gGranuleSize.width)
{
for (CGFloat y = yMin; y < yMax; y += gGranuleSize.height)
{
NSRect rect = NSMakeRect(x, y, gGranuleSize.width, gGranuleSize.height);
[neededTileFrames addObject: [NSValue valueWithRect: rect]];
}
}
// See if we already have subviews that cover these needed frames.
for (NSView* subview in [[[self.documentView subviews] copy] autorelease])
{
NSValue* frameRectVal = [NSValue valueWithRect: subview.frame];
// If we don't need this one any more...
if (![neededTileFrames containsObject: frameRectVal])
{
// Then recycle it...
[reusableViews addObject: subview];
[subview removeFromSuperview];
}
else
{
// Take this frame rect off the To-do list.
[neededTileFrames removeObject: frameRectVal];
}
}
// Add needed tiles from the to-do list
for (NSValue* neededFrame in neededTileFrames)
{
NSView* view = [[[reusableViews lastObject] retain] autorelease];
[reusableViews removeLastObject];
if (nil == view)
{
// Create one if we didnt find a reusable one.
view = [[[SODiagnosticView alloc] initWithFrame: NSZeroRect] autorelease];
NSLog(#"Created a view.");
}
else
{
NSLog(#"Reused a view.");
}
// Place it and install it.
view.frame = [neededFrame rectValue];
[view setNeedsDisplay: YES];
[self.documentView addSubview: view];
}
}
#end
#implementation SODiagnosticView
- (void)drawRect:(NSRect)dirtyRect
{
// Draw a red tile with a blue border.
[[NSColor blueColor] set];
NSRectFill(self.bounds);
[[NSColor redColor] setFill];
NSRectFill(NSInsetRect(self.bounds, 2,2));
}
#end
This worked pretty well as best I could tell. Again, drawing something meaningful in the reused views is where the real work is here.
Hope that helps.

Move NSView around until it hits a border

In a Cocoa-based App i'm having a canvas for drawing, inherited from NSView, as well as a rectangle, also inherited from NSView. Dragging the rectangle around inside of the canvas is no problem:
-(void)mouseDragged:(NSEvent *)theEvent {
NSPoint myOrigin = self.frame.origin;
[self setFrameOrigin:NSMakePoint(myOrigin.x + [theEvent deltaX],
myOrigin.y - [theEvent deltaY])];
}
Works like a charm. The issue i'm having now: How can i prevent the rectangle from being moved outside the canvas?
So, first of all i would like to fix this just for the left border, adapting the other edges afterwards. My first idea is: "check whether the x-origin of the rectangle is negative". But: once it is negative the rectangle can't be moved anymore around (naturally). I solved this with moving the rectangle to zero x-offset in the else-branch. This works but it's ... ugly.
So i'm little puzzled with this one, any hints? Definitely the solution is really near and easy. That easy, that i cannot figure it out (as always with easy solutions ;).
Regards
Macs
I'd suggest not using the deltaX and deltaY; try using the event's location in the superview. You'll need a reference to the subview.
// In the superview
- (void)mouseDragged:(NSEvent *)event {
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
// Could also add the width of the moving rectangle to this check
// to keep any part of it from going outside the superview
mousePoint.x = MAX(0, MIN(mousePoint.x, self.bounds.size.width));
mousePoint.y = MAX(0, MIN(mousePoint.y, self.bounds.size.height));
// position is a custom ivar that indicates the center of the object;
// you could also use frame.origin, but it looks nicer if objects are
// dragged from their centers
myMovingRectangle.position = mousePoint;
[self setNeedsDisplay:YES];
}
You'd do essentially the same bounds checking in mouseUp:.
UPDATE: You should also have a look at the View Programming Guide, which walks you through creating a draggable view: Creating a Custom View.
Sample code that should be helpful, though not strictly relevant to your original question:
In DotView.m:
- (void)drawRect:(NSRect)dirtyRect {
// Ignoring dirtyRect for simplicity
[[NSColor colorWithDeviceRed:0.85 green:0.8 blue:0.8 alpha:1] set];
NSRectFill([self bounds]);
// Dot is the custom shape class that can draw itself; see below
// dots is an NSMutableArray containing the shapes
for (Dot *dot in dots) {
[dot draw];
}
}
- (void)mouseDown:(NSEvent *)event {
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
currMovingDot = [self clickedDotForPoint:mousePoint];
// Move the dot to the point to indicate that the user has
// successfully "grabbed" it
if( currMovingDot ) currMovingDot.position = mousePoint;
[self setNeedsDisplay:YES];
}
// -mouseDragged: already defined earlier in post
- (void)mouseUp:(NSEvent *)event {
if( !currMovingDot ) return;
NSPoint mousePoint = [self convertPoint:[event locationInWindow]
fromView:nil];
spot.x = MAX(0, MIN(mousePoint.x, self.bounds.size.width));
spot.y = MAX(0, MIN(mousePoint.y, self.bounds.size.height));
currMovingDot.position = mousePoint;
currMovingDot = nil;
[self setNeedsDisplay:YES];
}
- (Dot *)clickedDotForPoint:(NSPoint)point {
// DOT_NUCLEUS_RADIUS is the size of the
// dot's internal "handle"
for( Dot *dot in dots ){
if( (abs(dot.position.x - point.x) <= DOT_NUCLEUS_RADIUS) &&
(abs(dot.position.y - point.y) <= DOT_NUCLEUS_RADIUS)) {
return dot;
}
}
return nil;
}
Dot.h
#define DOT_NUCLEUS_RADIUS (5)
#interface Dot : NSObject {
NSPoint position;
}
#property (assign) NSPoint position;
- (void)draw;
#end
Dot.m
#import "Dot.h"
#implementation Dot
#synthesize position;
- (void)draw {
//!!!: Demo only: assume that focus is locked on a view.
NSColor *clr = [NSColor colorWithDeviceRed:0.3
green:0.2
blue:0.8
alpha:1];
// Draw a nice border
NSBezierPath *outerCirc;
outerCirc = [NSBezierPath bezierPathWithOvalInRect:
NSMakeRect(position.x - 23, position.y - 23, 46, 46)];
[clr set];
[outerCirc stroke];
[[clr colorWithAlphaComponent:0.7] set];
[outerCirc fill];
[clr set];
// Draw the "handle"
NSRect nucleusRect = NSMakeRect(position.x - DOT_NUCLEUS_RADIUS,
position.y - DOT_NUCLEUS_RADIUS,
DOT_NUCLEUS_RADIUS * 2,
DOT_NUCLEUS_RADIUS * 2);
[[NSBezierPath bezierPathWithOvalInRect:nucleusRect] fill];
}
#end
As you can see, the Dot class is very lightweight, and uses bezier paths to draw. The superview can handle the user interaction.