IOS Charts positioning of gridlines - swift

I am struggling to find out how to set gridlines on my chart to specific points on the x axis and to centre labels on the lines corresponding to the time of day. I have a simple chart, the data represents a day with about 280 data points (one for every 5 minute period). I want to show gridlines at times of the day equal to 03:00, 06:00, 12:00, 15:00, 18:00 and 21:00 and show the corresponding labels centred below the gridlines. I have have also tried LimitLines, which works for placing the vertical lines at the correct points, but there does not seem to be the ability to align the time labels with the centre of the limit lines.
I know this can be done because I have seen screenshots on-line showing time labels centred on gridlines (or limitlines) for specific periods on the day, but I've spent hours looking for how to do this an I'm drawing a blank.
My code below (note the commented out failed attempt to get limitlines to work).
let tideHeights = LineChartDataSet(entries: dataEntries)
tideHeights.drawCirclesEnabled = false
tideHeights.colors = [NSUIColor.black]
//let timings = ["00:00", "03:00", "06:00", "12:00", "15:00", "18:00", "21:00"]
let data = LineChartData(dataSet: tideHeights)
lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)
//lineChartView.xAxis.drawGridLinesEnabled = false
//let gridLine = values.count / timings.count
//var xPos = 0
//for label in timings {
//let limitLine = ChartLimitLine(limit: Double(xPos), label: label)
//limitLine.labelPosition = .bottomLeft
//limitLine.lineWidth = 0.5
//limitLine.lineColor = .black
//limitLine.valueTextColor = .black
//lineChartView.xAxis.addLimitLine(limitLine)
//xPos += gridLine
//}
lineChartView.xAxis.labelCount = 7
lineChartView.xAxis.labelPosition = .bottom
lineChartView.xAxis.granularity = 1
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.legend.enabled = false
lineChartView.data = data
The output is below, the gridlines are at system set positions, how can I control where they are placed?
Update:
After applying the very helpful suggestion by Stephan Schlecht, I am still getting strange results. With lineChartView.xAxis.setLabelCount(9, force: true) I get:
and with lineChartView.xAxis.setLabelCount(4, force: true) I get:
It seems for some reason that the 1st gridline after the Axis is ignored.
Code is:
lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)
let marker = ChartMarker()
marker.setMinuteInterval(interval: 5)
marker.chartView = lineChartView
lineChartView.marker = marker
lineChartView.xAxis.setLabelCount(4, force: true)
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.xAxis.labelPosition = .bottom
lineChartView.leftAxis.labelPosition = .insideChart
lineChartView.rightAxis.drawLabelsEnabled = false
lineChartView.legend.enabled = false
lineChartView.data = data
lineChartView.data?.highlightEnabled = true
Update 2
To reproduce, I have the following class which creates the data (btw, it is a stub class, eventually it will contain a paid api, so for now I generate the data for test purposes:
class TideHeights {
var tideHeights = [Double]()
var tideTimes = [Date]()
var strTimes = [String]()
var intervalMins = 5
init (intervalMins: Int) {
self.intervalMins = intervalMins
let slots = 24 * 60.0 / Double(intervalMins)
let seconds = 24 * 60 * 60.0 / slots
let radians = 2 * Double.pi / slots
let endDate = Date().endOfDay
var date = Date().startOfDay
var radianOffset = 0.0
repeat {
tideHeights.append(sin(radianOffset) + 1)
tideTimes.append(date)
let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "HH:mm"
strTimes.append(timeFormatter.string(from: date))
date = date.advanced(by: seconds)
radianOffset += radians
} while date <= endDate
}
}
this class is instantiated and used to draw the graph in the following two functions in ViewController.
func drawChart() -> Void {
let tideData = TideHeights(intervalMins: 5)
lineChartView.dragEnabled = true
lineChartView.setScaleEnabled(false)
lineChartView.pinchZoomEnabled = false
setChartValues(dataPoints: tideData.strTimes, values: tideData.tideHeights)
}
func setChartValues(dataPoints: [String], values: [Double]) -> Void {
//initialise and load array of chart data entries for drawing
var dataEntries: [ChartDataEntry] = []
for i in 0..<values.count {
let dataEntry = ChartDataEntry(x: Double(i), y: values[i])
dataEntries.append(dataEntry)
}
let tideHeights = LineChartDataSet(entries: dataEntries)
tideHeights.drawCirclesEnabled = false
tideHeights.colors = [NSUIColor.black]
let gradientColors = [ChartColorTemplates.colorFromString("#72E700").cgColor,
ChartColorTemplates.colorFromString("#724BEB").cgColor]
let gradient = CGGradient(colorsSpace: nil, colors: gradientColors as CFArray, locations: nil)!
tideHeights.fillAlpha = 0.7
tideHeights.fill = Fill(linearGradient: gradient, angle: 90)
tideHeights.drawFilledEnabled = true
let data = LineChartData(dataSet: tideHeights)
lineChartView.xAxis.valueFormatter = IndexAxisValueFormatter(values: dataPoints)
let marker = ChartMarker()
marker.setMinuteInterval(interval: 5)
marker.chartView = lineChartView
lineChartView.marker = marker
lineChartView.xAxis.setLabelCount(4, force: true)
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.xAxis.labelPosition = .bottom
lineChartView.leftAxis.labelPosition = .insideChart
lineChartView.rightAxis.drawLabelsEnabled = false
lineChartView.legend.enabled = false
lineChartView.data = data
lineChartView.data?.highlightEnabled = true
}

The documentation says:
setLabelCount(int count, boolean force): Sets the number of labels for
the y-axis. Be aware that this number is not fixed (if force == false)
and can only be approximated. If force is enabled (true), then the
exact specified label-count is drawn – this can lead to uneven numbers
on the axis.
see https://weeklycoding.com/mpandroidchart-documentation/axis-general/
Remark: although the y-axis is mentioned in the documentation of the function, it can actually be applied to both axes.
So instead of
lineChartView.xAxis.labelCount = 7
use
lineChartView.xAxis.setLabelCount(9, force: true)
Note: you need 9 label counts not 7.
Test
If one combines a sine wave of 288 points with your code above
for i in 0...288 {
let val = sin(Double(i) * 2.0 * Double.pi / 288.0) + 1
dataEntries.append(ChartDataEntry(x: Double(i), y: val))
}
and forces the label count = 9 as shown, then it looks like this:

Stephan was correct in his answer to this question. To force gridlines and alignment, use setLabelCount(x, force: true) the additional complexity related to my specific project and the data contained in the labels.

Related

iOS Charts - Labelling Columns

In an iOS Charts bar chart I have no problem applying the Y numbers to the columns:
for i in 0..<dataPoints.count {
let dataEntry1 = BarChartDataEntry(x: Double(i) , y: self.percentageAmount1[i])
dataEntries1.append(dataEntry1)
let dataEntry2 = BarChartDataEntry(x: Double(i) , y: self.percentageAmount2[i])
dataEntries2.append(dataEntry2)
}
But how do I label each column, along the X axis, so that you know what they are?
And as a very minor aside any ideas why 100 doesn't appear in the first green column?
Wonderful as iOS Charts is the documentation and examples are few and far between. I was trying to label the columns along the lines of:
let columnTitles = ["Max", "Tilt", "Cloud", "Total"]
barChartView.leftAxis.valueFormatter = IndexAxisValueFormatter(values: columnTitles)
I have reworked the code adding in a formatter, which works.
let columnTitles = ["Max", "Tilt", "Cloud", "Total"]
let formatter = IndexAxisValueFormatter(values: columnTitles)
barChartView.xAxis.valueFormatter = formatter
I hope other people will find this useful.

Generate random values based on a seed Swift 3

lets say you were making a game and this game has procedurally generated terrain from a seed that the user inputted in his world-creation menu
and this seed generates a set of values that only change if the seed changes
for instance lets say you want to get a random set of integers to generate in a for-loop and pull randomly from an array the set of integers stay the same every time and pull the same items from the array in the exact same order every time you run the for-loop until you change the seed
how would one achieve this in swift
in my code here i can get the terrain to spawn in but it does not generate the same every time which is what i am trying to get it to do
override func didMove(to view: SKView) {
let tile1 = SKTileDefinition(texture: SKTexture(imageNamed: "stone") ,size: CGSize(width: 64, height: 64))
let tile2 = SKTileDefinition(texture: SKTexture(imageNamed: "water") ,size: CGSize(width: 64, height: 64))
let tile3 = SKTileDefinition(texture: SKTexture(imageNamed: "sand") ,size: CGSize(width: 64, height: 64))
let tile4 = SKTileDefinition(texture: SKTexture(imageNamed: "grass") ,size: CGSize(width: 64, height: 64))
let tileGroup1 = SKTileGroup(tileDefinition: tile1)
let tileGroup2 = SKTileGroup(tileDefinition: tile2)
let tileGroup3 = SKTileGroup(tileDefinition: tile3)
let tileGroup4 = SKTileGroup(tileDefinition: tile4)
let tileGroup5 = SKTileGroup(tileDefinition: tile4)
let tileGroup6 = SKTileGroup(tileDefinition: tile4)
let tileSet = SKTileSet(tileGroups: [tileGroup1,tileGroup2,tileGroup3,tileGroup4,tileGroup5,tileGroup6])
let columns = 5
let rows = 5
let tileSize = CGSize(width: 64, height: 64)
//this is another GKNoise class called Noise
let noise = Noise()
let noiseMap = GKNoiseMap(noise, size: vector_double2(10.0,10.0), origin: vector_double2(0.0,0.0), sampleCount: vector_int2(100), seamless: true)
//this is another SKTileMapNode Class called TileMap and a class func called tileMapNodes
let tileMap = TileMap.tileMapNodes(tileSet: tileSet, columns: columns, rows: rows, tileSize: tileSize, from: noiseMap, tileTypeNoiseMapThresholds: [(-1.0 as NSNumber),(+1.0 as NSNumber)])
tileMapNode = tileMap.first!
let seed = Int
for column in 0 ..< tileMapNode.numberOfColumns {
for row in 0 ..< tileMapNode.numberOfRows {
let rand = Int(arc4random_uniform(UInt32(tileSet.tileGroups.count)))
print(rand)
let tile = tileMapNode.tileSet.tileGroups[rand]
tileMapNode.setTileGroup(tile, forColumn: column, row: row)
}
}
If you want to generate a random value based on a specific seed use: srand48 and drand48:
srand allow you to specify the seed:
srand48(230)
Then drand48 will give you a double between 0 and 1:
let doubleValue = drand48()
So in your case, if you want an Int between 0 and 9, you can do something like:
srand48(230)
print("Random number: \(1 + Int((drand48() * 8) + 0.5))")
print("Random number: \(1 + Int((drand48() * 8) + 0.5))")
print("Random number: \(1 + Int((drand48() * 8) + 0.5))")
It will give you the 3 pseudo random numbers until you change the seed
Here is the drand48 applied to your new question:
let seed = srand48(230)
for column in 0 ..< tileMapNode.numberOfColumns {
for row in 0 ..< tileMapNode.numberOfRows {
let rand = Int(Double(tileSet.tileGroups.count) * drand48() - 0.5)
print(rand)
let tile = tileMapNode.tileSet.tileGroups[rand]
tileMapNode.setTileGroup(tile, forColumn: column, row: row)
}
}

Detect overlaping if enumerating nodes

I would like to know how should I detect overlapping nodes while enumerating them? Or how should I make that every random generated position in Y axis is at least some points higher or lower.
This is what I do:
1 - Generate random number between -400 and 400
2 - Add those into array
3 - Enumerate and add nodes to scene with generated positions like this:
var leftPositions = [CGPoint]()
for _ in 0..<randRange(lower: 1, upper: 5){
leftPositions.append(CGPoint(x: -295, y: Helper().randomBetweenTwoNumbers(firstNumber: leftSparkMinimumY, secondNumber: leftSparkMaximumY)))
}
leftPositions.enumerated().forEach { (index, point) in
let leftSparkNode = SKNode()
leftSparkNode.position = point
leftSparkNode.name = "LeftSparks"
let leftSparkTexture = SKTexture(imageNamed: "LeftSpark")
LeftSpark = SKSpriteNode(texture: leftSparkTexture)
LeftSpark.name = "LeftSparks"
LeftSpark.physicsBody = SKPhysicsBody(texture: leftSparkTexture, size: LeftSpark.size)
LeftSpark.physicsBody?.categoryBitMask = PhysicsCatagory.LeftSpark
LeftSpark.physicsBody?.collisionBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.contactTestBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.isDynamic = false
LeftSpark.physicsBody?.affectedByGravity = false
leftSparkNode.addChild(LeftSpark)
addChild(leftSparkNode)
}
But like this sometimes they overlap each other because the generated CGPoint is too close to the previous one.
I am trying to add some amount of triangles to the wall and those triangles are rotated by 90°
To describe in image what I want to achieve:
And I want to avoid thing like this:
Your approach to this is not the best, i would suggest only storing the Y values in your position array and check against those values to make sure your nodes will not overlap. The following will insure no two sparks are within 100 points of each other. You may want to change that value depending on your node's actual height or use case.
Now, obviously if you end up adding too many sparks within an 800 point range, this just will not work and cause an endless loop.
var leftPositions = [Int]()
var yWouldOverlap = false
for _ in 0..<randRange(lower: 1, upper: 5){
//Moved the random number generator to a function
var newY = Int(randY())
//Start a loop based on the yWouldOverlap Bool
repeat{
yWouldOverlap = false
//Nested loop to range from +- 100 from the randomly generated Y
for p in newY - 100...newY + 100{
//If array already contains one of those values
if leftPosition.contains(p){
//Set the loop Bool to true, get a new random value, and break the nested for.
yWouldOverlap = true
newY = Int(randY())
break
}
}
}while(yWouldOverlap)
//If we're here, the array does not contain the new value +- 100, so add it and move on.
leftPositions.append(newY)
}
func randY() -> CGFloat{
return Helper().randomBetweenTwoNumbers(firstNumber: leftSparkMinimumY, secondNumber: leftSparkMaximumY)
}
And here is a different version of your following code.
for (index,y) in leftPositions.enumerated() {
let leftSparkNode = SKNode()
leftSparkNode.position = CGPoint(x:-295,y:CGFloat(y))
leftSparkNode.name = "LeftSparks\(index)" //All node names should be unique
let leftSparkTexture = SKTexture(imageNamed: "LeftSpark")
LeftSpark = SKSpriteNode(texture: leftSparkTexture)
LeftSpark.name = "LeftSparks"
LeftSpark.physicsBody = SKPhysicsBody(texture: leftSparkTexture, size: LeftSpark.size)
LeftSpark.physicsBody?.categoryBitMask = PhysicsCatagory.LeftSpark
LeftSpark.physicsBody?.collisionBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.contactTestBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.isDynamic = false
LeftSpark.physicsBody?.affectedByGravity = false
leftSparkNode.addChild(LeftSpark)
addChild(leftSparkNode)
}

Crash on launch, error with SKphysics Joint

I am creating a game and i am trying to create a character that has 2 different nodes, the legs and the torso. I tried to link them using a fixed type joint, however when i do that the torso slides off of the legs sometimes while moving around. Now i am trying to create the character using a distance limit between the 2 nodes hoping that they won't slide away from each other now,however it keeps crashing on launch any ideas?
here is my code
func CreateHero (){
soldierLegs.position = CGPoint(x: 405 , y: 139)
soldierLegs.zPosition = 1
soldierLegs.anchorPoint.x = 0.6
soldierLegs.anchorPoint.y = 0.7
let soldierLegsBody:SKPhysicsBody = SKPhysicsBody(rectangleOfSize:
soldierLegs.size)
soldierLegsBody.dynamic = true
soldierLegsBody.affectedByGravity = true
soldierLegsBody.allowsRotation = false
//body.restitution = 0.4
soldierLegsBody.categoryBitMask = BodyType.soldierL.rawValue
soldierLegsBody.contactTestBitMask = BodyType.enemy1.rawValue |
BodyType.enemy2.rawValue | BodyType.enemy3.rawValue |
BodyType.desertForegroundCase.rawValue
soldierLegs.physicsBody = soldierLegsBody
soldierTorso.position = soldierLegs.position
soldierTorso.zPosition = 2
soldierTorso.anchorPoint.x = 0.25
soldierTorso.anchorPoint.y = 0.1
let soldierTorsoBody:SKPhysicsBody = SKPhysicsBody(rectangleOfSize:
soldierTorso.size)
soldierTorsoBody.dynamic = true
soldierTorsoBody.affectedByGravity = true
soldierTorsoBody.allowsRotation = false
soldierTorsoBody.categoryBitMask = BodyType.soldierT.rawValue
soldierTorsoBody.contactTestBitMask = BodyType.enemy1.rawValue |
BodyType.enemy2.rawValue | BodyType.enemy3.rawValue |
BodyType.desertForegroundCase.rawValue
soldierTorso.physicsBody = soldierTorsoBody
let Joint =
SKPhysicsJointLimit.jointWithBodyA(soldierLegs.physicsBody!, bodyB:
soldierTorso.physicsBody!, anchorA: soldierLegs.position, anchorB:
soldierTorso.position)
Joint.maxLength = 0.01
self.addChild(soldierTorso)
self.addChild(soldierLegs)
self.physicsWorld.addJoint(Joint)
}
According to the Apple official document :
Creating a Limit Joint
class func joint(withBodyA: SKPhysicsBody, bodyB: SKPhysicsBody, anchorA: CGPoint, anchorB: CGPoint)
Creates a new limit joint.
anchorA
A connection point on the first body in the scene’s coordinate system.
anchorB
A connection point on the second body in the scene’s coordinate system.
In your case anchorA is equal to anchorB (the same point), there is no limit, this one cause the crash.

How to make y-axis not start at zero?

I know there is a chart.leftAxis.startAtZeroEnabled = false option but it's not working for me.
I'm making a line graph to track Instagram Followers over time and I have data points that vary very little so I need the y-axis adjusted so that the changes are enabled, but it
Here is the code I have for the graph:
let chart = LineChartView(frame: chart1View.bounds)
let dayTrackerUnits = trackerCrawler.thisWeeksPoints()
var xVals : [Int] = []
var yVals : [ChartDataEntry] = []
var minimum = 1000000000
var maximum = 0
for i in 0..<dayTrackerUnits.count {
xVals.append(i)
yVals.append(ChartDataEntry(value: Double(dayTrackerUnits[i].followerCount), xIndex: i))
if dayTrackerUnits[i].followerCount < minimum {
minimum = dayTrackerUnits[i].followerCount
}
if dayTrackerUnits[i].followerCount > maximum {
maximum = dayTrackerUnits[i].followerCount
}
}
print(yVals)
let chartDataSet = LineChartDataSet(yVals: yVals, label: "Followers")
chartDataSet.setColor(GlobalStyles.sharedStyles.instagramBlue())
chartDataSet.lineWidth = 2.0
chartDataSet.circleRadius = 5.0
chartDataSet.circleColors = [GlobalStyles.sharedStyles.instagramBlue()]
chartDataSet.drawValuesEnabled = false
chartDataSet.drawVerticalHighlightIndicatorEnabled = true
let data = LineChartData(xVals: xVals, dataSet: chartDataSet)
chart.data = data
chart.delegate = self
chart.leftAxis.startAtZeroEnabled = false
chart.rightAxis.startAtZeroEnabled = false
print("min: \(minimum), max: \(maximum)")
chart.rightAxis.enabled = false
chart1View.addSubview(chart)
left1Label.text = "Followers"
I'd like to set the yAxisMinimum to minimum and the yAxisMaximum to maximum but the graph won't display how I want.
Any suggestions?
You can just use customAxisMin/Max to limit its range.
BTW, you have to make sure your data values does not contains 0. It seems like your data has 0, that's why it always start from 0. It has a small algorithm to calculate the axis range in computeAxisValues in y axis renderer.