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)
Related
I've been playing around with the MDLVoxelArray functionality in iOS, to create a mesh algorithmically via voxels.
Currently I create the voxel array like this:
let minBounds = vector_float3(x: 0.0, y: 0.0, z: 0.0)
let maxBounds = vector_float3(x: Float(width), y: Float(height), z: Float(maxDepth))
let boundingBox = MDLAxisAlignedBoundingBox(maxBounds: maxBounds, minBounds: minBounds)
let voxels = MDLVoxelArray(data: Data(capacity: MemoryLayout<MDLVoxelIndex>.size * width * height * depth), boundingBox: boundingBox, voxelExtent: 1.0)
Then I populate it with indices:
var index = MDLVoxelIndex(x: 0, y: 0, z: 0, w: 0)
for i in 0 ..< height {
for j in 0 ..< width {
index.x = Int32(j)
index.y = Int32(i)
let depth = Int32(heightMap[j][i])
let midPoint = depth / 2
for k in 0 ... depth {
// w should be 0 at the surface and increase in magnitude towards the middle
let w = abs(k - midPoint) - midPoint
index.z = k
index.w = w
voxels.setVoxelAtIndex(index)
}
}
}
Later I then convert the MDLVoxelArray to a mesh and save it out to a USDZ file. It works (and produces pretty nice results, actually) but it's really slow and gets progressively slower and slower as it iterates further and further.
I'm finding very few examples / little documentation about MDLVoxelArrays online, so my question is:
Is this the right way to go about creating and populating the voxel array? I was just guessing at how to give it an initial capacity when populating the Data object, but am imagining that might not be correct.
Any tips would also be welcome on how to go about profiling why it's slow. I'm not sure what tools are available for diagnosing performance bottlenecks when they're inside Apple's internal APIs like this?
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!
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.
I have been doing a ton of research but found nothing. With MapKit, I have got a map that shows current location and elsewhere a function that calculates a heading/bearing value (not necessarily the actual heading). How can I draw a line on the map that will start at current location, and point in direction of the given heading ? (Does not matter how long the line is, as in it has no meaningful end point). I am not asking you to write the code for me but would appreciate some detailed direction. Hope this helps others too.
Cheers
Your coordinates are polar, which means you have a direction and a length. You just need to convert them to Cartesian, which gives you a horizontal offset and a vertical offset. You do that with a little trigonometry.
let origin = CGPoint(x: 10, y: 10)
let heading: CGFloat = CGFloat.pi
let length: CGFloat = 20
let endpoint = CGPoint(x: origin.x + cos(heading)*length,
y: origin.y + sin(heading)*length)
let path = UIBezierPath()
path.move(to: origin)
path.addLine(to: endpoint)
Note that trigonometric functions generally work in radians (2*PI = one revolution). Bearings are often in degrees (360 degrees = one revolution). Converting is straightforward, however:
func radians(forDegrees angle: CGFloat) -> CGFloat {
return CGFloat.pi * angle / 180.0
}
I am working with scene kit and am trying to make a first person game. I found this sample code for making the first person camera with a pan gesture. Everything works but I do not understand what is going on here. Could someone explain what is happening?
func lookGestureRecognized(gesture: UIPanGestureRecognizer) {
//get translation and convert to rotation
let translation = gesture.translationInView(self.view)
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2)
//rotate hero
heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true)
//tilt camera
elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle))
camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation)
//reset translation
gesture.setTranslation(CGPointZero, inView: self.view)
}
Here is the same code, with a few additional comments:
func lookGestureRecognized(gesture: UIPanGestureRecognizer) {
// Create Translation variable, containing the
// "distance" traveled by the finger since the last event
let translation = gesture.translationInView(self.view)
// From this distance, calculate how much the camera should move
// 1) horizontally, 2) vertically using angles (FPS controls)
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2)
// Apply the horizontal angle to the Hero node as a force to
// Make it rotate accordingly (physics body use forces to move)
heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true)
// Use the other angle to look up and down, clamped to ±pi/4
elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle))
// Apply the new angle to teh camera on the X axis
camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation)
// Set the translation to 0 to avoid accumulation
// the next time the event is triggered
gesture.setTranslation(CGPointZero, inView: self.view)
}
This should help understand, let me know if you need more details on how it works!
(Note: the "Distance" is actually a 2D Vector)
EDIT: here's a better explanation of the angle:
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
First, the translation (so pixel distance on x) is divided by 200. This is to both slow down the movement and (unsafely) keep x between -1 and 1.
Acos gives the arc cosinus of a value. The result is between 0 to pi and (to simplify) only works for x from -1 to 1. Here's a graph for it: http://www.wolframalpha.com/input/?i=acos%28x%29-pi%2F2
Since we want to move in positive and negative directions, we remove half of the max value (M_PI_2, which is pi/2) to keep the result within -pi/2 to pi/2
In the end, if you move your finger 200 pixels in a direction, you would look pi/2=90° on your screen.