Making a very simple graph with uibezierpath - ios

my question was
I want to create a simple line graph with certain values. This is done in a view within the mainviewcontroller. I created a UIview named chart. I pass the data to the chart when its retrieved from the API. I figured out how to draw the axis but I am stuck now. I cant find anything on google on how to set labels on intervals and to make the points appear dynamically.
draw the xasis and its labels.
draw the dots in the graph.
My salution
i figured out how to do all the things i asked for.
The code I have now:
class ChartView: UIView {
//some variables
var times: [String] = []
var AmountOfRain: [Double] = []
let pathy = UIBezierPath()
let pathx = UIBezierPath()
var beginwitharray = Array<CGFloat>()
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
//draw the y line
pathy.move(to: CGPoint(x: 30, y: 10))
pathy.addLine(to: CGPoint(x: 30, y: 10))
pathy.addLine(to: CGPoint(x: 30, y: frame.size.height - 30))
UIColor.black.setStroke()
pathy.lineWidth = 1.0
pathy.stroke()
//draw the x line
pathx.move(to: CGPoint(x: 30, y: frame.size.height - 30))
pathx.addLine(to: CGPoint(x: 30, y: frame.size.height - 30))
pathx.addLine(to: CGPoint(x: frame.size.width - 30, y: frame.size.height - 30))
UIColor.black.setStroke()
pathx.lineWidth = 1.0
pathx.stroke()
//when the data arrives form the SUPER slow duienradar API refresh it with the data
if beginwitharray != []{
//remove the label retriving data
let label = viewWithTag(1)
DispatchQueue.main.sync {
label?.removeFromSuperview()
}
//create the dots in the graph
var point = CGPoint()
//simple way to do 2 loop in 1 loop.
var intforbeginarray = 0
let stoke = UIBezierPath()
//get the first 6 itmes out of the rain array cuz of space issues
let first6aumountarray = AmountOfRain[0...5]
stoke.move(to: CGPoint(x: 30, y: self.frame.size.height - 30))
//loop trough the data in the amounts array
for amount in first6aumountarray{
//determen the hight of the dot
let InitialHeight = (CGFloat(amount) * (self.frame.size.height - 30))/6
let pointHeight = (frame.size.height - 30) - InitialHeight
//make the point so we can draw it using UIbezierpath()
point = CGPoint(x: beginwitharray[intforbeginarray] + 20, y: pointHeight)
intforbeginarray += 1
//create the dot
let dot = UIBezierPath()
dot.addArc(withCenter: point, radius: CGFloat(5), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true)
UIColor.black.setFill()
dot.lineWidth = 30
dot.fill()
//create the line between dots will give a warning on the last one cuz the last one doenst go anyway
stoke.addLine(to: point)
stoke.move(to: point)
stoke.lineWidth = 1
UIColor.black.setStroke()
}
//make the strokes
stoke.stroke()
}
}
func getvalues(RainData: [Double], TimesData:[String]){
//assing the data to the subview
self.AmountOfRain = RainData
self.times = TimesData
//xaxis values
let maxint = [0, 1, 2, 3, 4, 5, 6]
//calculate the hight spacing to fit the graph
let heightperstep = ((self.frame.size.height - 5)/6)-5
var beginheight = self.frame.size.height - 35
//calculate the width spacing to fit the graph
let widthperstep = ((self.frame.size.width - 5)/6)-5
var beginwith = CGFloat(30)
//extra check to see if we have data at all.
if times != []{
//get the first 6 items out of the times array for use in our graph
let first6 = times[0...5]
//draw the label on the main queue
DispatchQueue.main.sync {
//draw the xaxis labels accroding to the spacing
for number in maxint{
let label = UILabel(frame: CGRect(x: 5, y: beginheight, width: 25, height: 15))
label.text = "\(number)"
self.addSubview(label)
beginheight = beginheight - heightperstep
}
//draw the yaxis labels according to the spacing
for time in first6{
let label = UILabel(frame: CGRect(x: beginwith, y: self.frame.size.height - 20, width: 55, height: 15))
label.text = time
self.addSubview(label)
beginwitharray.append(beginwith)
beginwith = beginwith + widthperstep
}
}
}
//redrawthe graph with new data.
setNeedsDisplay()
}}
Any help would be appreciated. I also can't use a lib or a pod since this is a school project and I need to create a simple graph.
EDIT:
Completed my code, cleared up an error when running this code
What I did first was to draw the x-asis and the y-axis. After this I considered reasonable values for the aumountofrain data. this turns out cannot really be higher then 6. Since I could fit around 6 labels in the space I have the steps where easy go down by 1 till I hit 0. The calculations I did are for my specific frame height. After I figured it all out and the padding for the y-asxis. It was a matter of figuring out how to get the dots in the right place. Since I already have the data in the beginwitharray I just needed to calculate the height. Then it was simply loop trough the data and draw each dot. Then I just had to connect the dots using the uibezierpath.
i hope my troubles will save someone a lot of time when they read how i done it.

This might be helpful: Draw Graph curves with UIBezierPath
Essentially what you need to do is for every data set you have you need to know the y-axis range of values and based on those ranges assign each value a CGFloat value (in your case inches of rain needs to correlate to a certain CGFloat value). Let's say you have your set amountOfRain = [0.1, 1.3, 1.5, 0.9, 0.1, 0] so your range is var rangeY = amountOfRain.max() - amountOfRain.min(). now lets find out where your first data point 0.1 should go on your graph by converting inches of rain to a CGFloat value that corresponds to the axis you've drawn already, this equation is just basic algebra: let y1 = (amountOfRain[0]/rangeY)*((frame.size.height-30) - 10) + 10 now it looks like your rain samples are at regular intervals so maybe let x1:CGFloat = 10 now you can add a dot or something at the CGPoint corresponding with (x1,y1). If you did this with all the data points it would create a graph that has your maximum value at the top of the graph and minimum value at the bottom. Good Luck!

Related

Sprite physics showing up but not visually showing up in Swift (SpriteKit)

I set up the sprite's physics body, zPosition, color, size, etc., but it won't appear visually. (It has the highest zPosition of my sprites, and YES I did import SpriteKit)
let wall = SKSpriteNode()
wall.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
//Takes screen size / width to make each piece one/(value set by "maze" at top) of the screen
let width: CGFloat = (UIScreen.main.bounds.width) / CGFloat(mazeSize)
let height: CGFloat = (UIScreen.main.bounds.height - 134) / CGFloat(mazeSize) //subtract so there's extra room on top+bottom of screen
wall.color = UIColor(ciColor: .black)
wall.isHidden = false
wall.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: width, height: height))
wall.physicsBody?.restitution = 0
wall.physicsBody?.friction = 0
wall.physicsBody?.isDynamic = false
wall.zPosition = 4
//sets up x+y location based on row count and which row this is on
//locations (ONLY positive)
let xLoc: CGFloat = ((UIScreen.main.bounds.width) / CGFloat((mazeSize/2))) * CGFloat(i/mazeSize)
let yLoc: CGFloat = ((UIScreen.main.bounds.height - 134) / CGFloat(mazeSize/2)) * CGFloat(i/mazeSize)
if((i/mazeSize) > (mazeSize/2)){ //if positive on x
wall.position.x = xLoc
}
else{ //if negative on x
wall.position.x = -xLoc
}
if(i > ((mazeSize^2)/2)){ //if positive on y
wall.position.x = yLoc
}
else{ //if negative on y
wall.position.y = yLoc
}
I'm using this to set up walls in a maze (Yes I know the x+y positions are wrong, but they still "appear" clumped in the middle), but only the physics of these nodes show up. Is there a way I can make these black walls appear?
The geometry of the sprite node and the geometry of the physics body don't have to be related, and in this case you've made a rectangular physics body and basically an empty sprite node. Try something like
let wall = SKSpriteNode(color: .black, size: CGSize(width: 100, height: 100)
or using the SKSpriteNode(texture:) initializer.

ARKit Convert 3d object position to UIView coordinates

I have created measure demo which allow to put multiple points and show distance between them. which works fine
I want to show preview that what so far has been drawn in real world to the UIView using UIBezierPath . Just like http://armeasure.com/
I have tried many things to achieve this but I couldn't find any right way to do it.
if self.linkList.count == 1 {
bezierPath.removeAllPoints()
bezierPath.move(to: CGPoint(x: 10,y: 10))
} else {
guard self.linkList.count > 1 ,let object2 = self.linkList.lastNode, let object1 = self.linkList.lastNode?.previous else {return}
let value = self.getMeasurementXandYBetween(vector1: object1.node.mainNode.position, and: object2.node.mainNode.position)
print(value)
let x = Double((object1.node.mainNode.position.x + value ) * 377.9527559055 )
let y = Double((object1.node.mainNode.position.y + value) * 377.9527559055)
let pointCoordinates = CGPoint(x: x , y: y)
print("x : Y ",x,y)
bezierPath.addLine(to: pointCoordinates)
}
shapeLayer.removeFromSuperlayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.lineWidth = 0.5
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.position = CGPoint(x: 0, y: 0)
self.viewToDraw.layer.addSublayer(shapeLayer)
func getMeasurementXandYBetween(vector1:SCNVector3, and vector2:SCNVector3) -> Float {
return sqrtf((vector1.x - vector2.x) * (vector1.x - vector2.x) + (vector1.y - vector2.y) * (vector1.y - vector2.y))
}
The logic I used (which is not working is) Location of previous node + distance I got from getMeasurementXandYBetween multiply by 377.
Please suggest a hint or any other solution
You can get the coordinates of any point in screen-space by using the projectPoint() on your SCNSceneRenderer.
This will give you a vector with 3 elements, build your CGPoint using the first two and build your shape from those points.

Why is CAShapeLayer not centering to my UIView when I scale it?

let centerPointX = colorSizeGuide.bounds.midX / 2
let centerPointY = colorSizeGuide.bounds.midY / 2
let circleWidth: CGFloat = 10
let circleHeight: CGFloat = 10
shape.path = UIBezierPath(ovalIn: CGRect(x: centerPointX + circleWidth / 4, y: centerPointY + circleHeight / 4, width: circleWidth, height: circleHeight)).cgPath
shape.strokeColor = UIColor(r: 160, g: 150, b: 180).cgColor
shape.fillColor = UIColor(r: 160, g: 150, b: 180).cgColor
shape.anchorPoint = CGPoint(x: 0.5, y: 0.5)
shape.lineWidth = 0.1
shape.transform = CATransform3DMakeScale(4.0, 4.0, 1.0)
colorSizeGuide.layer.addSublayer(shape)
Here's what's happening. I need the CAShapeLayer to stay in the middle of the small gray area:
I struggle with affine transforms a little myself, but here's what I think is going on:
The scale takes place centered around 0,0, so it will grow out from that point. That means it will "push away" from the origin.
In order to grow from the center, you should shift the origin to the center point of your shape, scale, and then shift the origin back, by the now-scaled amount:
var transform = CATransform3DMakeTranslation(centerPointX, centerPointY, 0)
transform = CATransformScale(transform, 4.0, 4.0, 1.0)
var transform = CATransform3DTranslate(
transform,
-4.0 * centerPointX,
-4.0 * centerPointY,
0)
shape.transform = transform
BTW, I can't make any sense of the image you posted with your question. You say "I need the CAShapeLayer to stay in the middle of the small gray area" I gather your shape layer is one of the circles, but it isn't clear what you mean by "the small gray area." It looks like there might be an outline that got cropped somehow.

animating UIBezier to get a live curve?

I am trying to create a simple line graph which is being updated live. Some kind of seismograph .
I was thinking about UIBezierPath , by only moving a point on the y-axis according to an input var, I can create a line moving on the time axis.
The problem is that you have to "push" the previous points to free up space for the new ones.(so the graph goes from left to right)
Can anybody help with some direction ?
var myBezier = UIBezierPath()
myBezier.moveToPoint(CGPoint(x: 0, y: 0))
myBezier.addLineToPoint(CGPoint(x: 100, y: 0))
myBezier.addLineToPoint(CGPoint(x: 50, y: 100))
myBezier.closePath()
UIColor.blackColor().setStroke()
myBezier.stroke()
You're correct: you need to push the previous points. Either divide the total width of the graph so it becomes increasingly scaled but retains all data, or drop the first point each time you add a new one to the end. You'll need to store an array of these points and recreate the path each time. Something like:
//Given...
let graphWidth: CGFloat = 50
let graphHeight: CGFloat = 20
var values: [CGFloat] = [0, 4, 3, 2, 6]
//Here's how you make your curve...
var myBezier = UIBezierPath()
myBezier.moveToPoint(CGPoint(x: 0, y: values.first!))
for (index, value) in values.enumerated() {
let point = CGPoint(x: CGFloat(index)/CGFloat(values.count) * graphWidth, y: value/values.max()! * graphHeight)
myBezier.addLineToPoint(point)
}
UIColor.blackColor().setStroke()
myBezier.stroke()
//And here's how you'd add a point...
values.removeFirst() //do this if you want to scroll rather than squish
values.append(8)

How to make text the same size on all devices in SpriteKit?

In SpriteKit, is there a way to make an SKLabelNode look the same size, regardless of the device, eg: Looks the same size on a iPhone 5 as a 6Plus?
I've tried using this method someone else recommended:
let textRect = CGRect(x: 0, y: 0, width: frame.width * 0.4, height: frame.height * 0.045)
let scalingFactor = min(textRect.width / text.frame.width, textRect.height / text.frame.height)
text.fontSize *= scalingFactor
But it doesn't make all text the same size, as words like "man" aren't as physically tall as words like "High" (due to it's "y" and "h" sticking out).
So is there a method to make text look the same size on all devices? At the moment I create the SKLabelNode like so:
let text = SKLabelNode(text: "Start")
text.fontSize = 30
text.position = CGPoint(x: 0, y: 0)
addChild(text)
The issue here is that you are trying to scale the fontSize, and this does not really play well with complex decimal numbers. Instead, after you create your label, just scale that to the scale factor that you are using to scale everything else
let text = SKLabelNode(text: "Start")
text.fontSize = 30
text.position = CGPoint(x: 0, y: 0)
text.xScale = xScaleFactor
text.yScale = yScaleFactor
where xScaleFactor and yScaleFactor are the factors you are using to determine your scale. (This number should only have to be calculated once, and then stored, if you are not doing that, I would recommend making that change)
Basically in the code you provided it is done like this:
let textRect = CGRect(x: 0, y: 0, width: frame.width * 0.4, height: frame.height * 0.045)
let scaleFactorX = textRect.width / text.frame.width
let scaleFactorY = textRect.height / text.frame.height
I think it's more like an algorithm question. Think about you need to implement the same thing in TV, iPad or in the iPhone device. You should think about storing its absolute value rather than its actual value.
The formula should be width for store value = actual width for this device / device width. The same with the height. Then, if you use the same image data in other devices. You will just need to multiply the new device width/height.

Resources