CorePlot - Annotations swinging in the Scatter Plot - ios

In my app I've a CorePlot which hosts three scatterplots, I've also configured as follows,
let borderStyle = CPTMutableLineStyle()
borderStyle.lineColor = maxPlotColor
let textLayer = CPTTextLayer(text: "MAX")
textLayer.fill = CPTFill(color: .white())
textLayer.cornerRadius = 10.0
textLayer.borderLineStyle = borderStyle
maxLabelAnnotation = CPTPlotSpaceAnnotation(plotSpace: maxLinePlot.plotSpace!, anchorPlotPoint: [5, NSNumber(value: kMaxLineValue)])
maxLabelAnnotation.contentLayer = textLayer
maxLinePlot.graph?.plotAreaFrame?.plotArea?.addAnnotation(maxLabelAnnotation)
The annotations are updated dynamically when the plot data is updated. The code snippet is shown below,
func newData(_ theTimer: Timer) {
// MARK: Dynmic annotations
maxLabelAnnotation.anchorPlotPoint = [NSNumber(value: currentIndex - 18), NSNumber(value: kMaxLineValue)]
riskLabelAnnotation.anchorPlotPoint = [NSNumber(value: currentIndex - 18), NSNumber(value: kRiskLineValue)] }
The problem is shown in the figure, where the annotation is moving back and froth as the graph moving continuously
I just want to stick the annotation in a position with out any swinging. any help would be much appreciated. Thanks in advance.

The annotations are locked to the plot space as it scrolls, but you update their anchor positions with each new data point, causing them to jump to the next position. Use layer annotations instead of plot space annotations to lock them in place. You'll need to recalculate the y-positions if the scale of the y-axis changes but the labels will stay in place as the graph scrolls below them.
Do this after the view hierarchy is build and everything is laid out so the view calculations are correct.
maxLinePlot.layoutIfNeeded()
if let plotArea = maxLinePlot.graph?.plotAreaFrame?.plotArea {
let plotSpace = maxLinePlot.plotSpace!
let viewPoint = plotSpace.plotAreaViewPoint(forPlotPoint: [0.0, kMaxLineValue])
let maxLabelAnnotation = CPTLayerAnnotation(anchorLayer: plotArea)
maxLabelAnnotation.contentLayer = textLayer
maxLabelAnnotation.xConstraints = CPTConstraints(relativeOffset: 0.25)
maxLabelAnnotation.yConstraints = CPTConstraints(lowerOffset: viewPoint.y)
plotArea.addAnnotation(maxLabelAnnotation)
}

Related

Mosaic light show CAReplicatorLayer animation

I'm trying to achieve this mosaic light show effect for my background view with the CAReplicatorLayer object:
https://downloops.com/stock-footage/mosaic-light-show-blue-illuminated-pixel-grid-looping-background/
Each tile/CALayer is a single image that was replicated horizontally & vertically. That part I have done.
It seems to me this task is broken into at least 4 separate parts:
Pick a random tile
Select a random range of color offset for the selected tile
Apply that color offset over a specified duration in seconds
If the random color offset exceeds a specific threshold then apply a glow effect with the color offset animation.
But I'm not actually sure this would be the correct algorithm.
My current code was taken from this tutorial:
https://www.swiftbysundell.com/articles/ca-gems-using-replicator-layers-in-swift/
Animations are not my strong suite & I don't actually know how to apply continuous/repeating animation on all tiles. Here is my current code:
#IBOutlet var animationView: UIView!
func cleanUpAnimationView() {
self.animationView.layer.removeAllAnimations()
self.animationView.layer.sublayers?.removeAll()
}
/// Start a background animation with a replicated pattern image in tiled formation.
func setupAnimationView(withPatternImage patternImage: UIImage, animate: Bool = true) {
// Tutorial: https://www.swiftbysundell.com/articles/ca-gems-using-replicator-layers-in-swift/
let imageSize = patternImage.size.halve
self.cleanUpAnimationView()
// Animate pattern image
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame.size = self.animationView.frame.size
replicatorLayer.masksToBounds = true
self.animationView.layer.addSublayer(replicatorLayer)
// Give the replicator layer a sublayer to replicate
let imageLayer = CALayer()
imageLayer.contents = patternImage.cgImage
imageLayer.frame.size = imageSize
replicatorLayer.addSublayer(imageLayer)
// Tell the replicator layer how many copies (or instances) of the image needs to be rendered. But we won't see more than one since they are, per default, all rendered/stacked on top of each other.
let instanceCount = self.animationView.frame.width / imageSize.width
replicatorLayer.instanceCount = Int(ceil(instanceCount))
// Instance offsets & transforms is needed to move them
// 'CATransform3D' transform will be used on each instance: shifts them to the right & reduces the red & green color component of each instance's tint color.
// Shift each instance by the width of the image
replicatorLayer.instanceTransform = CATransform3DMakeTranslation(imageSize.width, 0, 0)
// Reduce the red & green color component of each instance, effectively making each copy more & more blue while horizontally repeating the gradient pattern
let colorOffset = -1 / Float(replicatorLayer.instanceCount)
replicatorLayer.instanceRedOffset = colorOffset
replicatorLayer.instanceGreenOffset = colorOffset
//replicatorLayer.instanceBlueOffset = colorOffset
//replicatorLayer.instanceColor = UIColor.random.cgColor
// Extend the original pattern to also repeat vertically using another tint color gradient
let verticalReplicatorLayer = CAReplicatorLayer()
verticalReplicatorLayer.frame.size = self.animationView.frame.size
verticalReplicatorLayer.masksToBounds = true
verticalReplicatorLayer.instanceBlueOffset = colorOffset
self.animationView.layer.addSublayer(verticalReplicatorLayer)
let verticalInstanceCount = self.animationView.frame.height / imageSize.height
verticalReplicatorLayer.instanceCount = Int(ceil(verticalInstanceCount))
verticalReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, imageSize.height, 0)
verticalReplicatorLayer.addSublayer(replicatorLayer)
guard animate else { return }
// Set both the horizontal & vertical replicators to add a slight delay to all animations applied to the layer they're replicating
let delay = TimeInterval(0.1)
replicatorLayer.instanceDelay = delay
verticalReplicatorLayer.instanceDelay = delay
// This will make the image layer change color
let animColor = CABasicAnimation(keyPath: "instanceRedOffset")
animColor.duration = animationDuration
animColor.fromValue = verticalReplicatorLayer.instanceRedOffset
animColor.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor.autoreverses = true
animColor.repeatCount = .infinity
replicatorLayer.add(animColor, forKey: "colorshift")
let animColor1 = CABasicAnimation(keyPath: "instanceGreenOffset")
animColor1.duration = animationDuration
animColor1.fromValue = verticalReplicatorLayer.instanceGreenOffset
animColor1.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor1.autoreverses = true
animColor1.repeatCount = .infinity
replicatorLayer.add(animColor1, forKey: "colorshift1")
let animColor2 = CABasicAnimation(keyPath: "instanceBlueOffset")
animColor2.duration = animationDuration
animColor2.fromValue = verticalReplicatorLayer.instanceBlueOffset
animColor2.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
animColor2.autoreverses = true
animColor2.repeatCount = .infinity
replicatorLayer.add(animColor2, forKey: "colorshift2")
}
let imageSize = patternImage.size.halve
and
animColor.toValue = -1 / Float(Int.random(replicatorLayer.instanceCount-1))
both generated errors.
I removed the halve and commented-out the animColor lines and the code runs and animates. I could not get ANY replicator layer to display or animate at all (not even the most basic apple or tutorial code) until I used your code. Thank you so much!

Drawing a confidence ellipse on top of a scatter plot

I'm currently working on an iOS app where I'm using the CorePlot library (Version 2.1) to draw a scatter plot. My scatter plot draws fine, and in the next step I'd like to draw an translucent confidence ellipse on top of the plot. I've written a class computing the main and minor axis and the required rotation angle of my ellipse. My ConfidenceEllipse class implements a getPath() method which returns a CGPath representing the ellipse to draw.
func getPath() -> CGPath
{
var ellipse: CGPath
var transform = CGAffineTransformIdentity
transform = CGAffineTransformTranslate (transform, CGFloat(-self.meanX), CGFloat(-self.meanY))
transform = CGAffineTransformRotate (transform, CGFloat(self.angle))
transform = CGAffineTransformTranslate (transform, CGFloat(self.meanX), CGFloat(self.meanY))
ellipse = CGPathCreateWithEllipseInRect(CGRectMake (CGFloat(-self.mainHalfAxis), CGFloat(-self.minorHalfAxis), CGFloat(2 * self.mainHalfAxis), CGFloat(2 * self.minorHalfAxis)),&transform)
return ellipse
}
After searching the web for a while, it appears that Annotations are the way to go, so I tried this:
let graph = hostView.hostedGraph!
let space = graph.defaultPlotSpace
let ellipse = ConfidenceEllipse(chiSquare: 5.991)
ellipse.analyze(self.samples)
let annotation = CPTPlotSpaceAnnotation (plotSpace: space!, anchorPlotPoint: [0,0])
let overlay = CPTBorderedLayer (frame: graph.frame)
overlay.outerBorderPath = ellipse.getPath()
let fillColor = CPTColor.yellowColor()
overlay.fill = CPTFill (color: fillColor)
annotation.contentLayer = overlay
annotation.contentLayer?.opacity = 0.5
graph.addAnnotation(annotation)
Doing this, will give me the following
Screenshot
As you can see, the overlay takes up the full size of the frame, which seems logical given the fact that I passed the frames dimensions when creating the CPTBorderedLayer object. I also tried leaving the constructor empty, but then the overlay doesn't show at all. So I'm wondering, is there anything I'm missing here ?
You need to scale the ellipse to match the plot. Use the plot area bounds for the frame of the annotation layer and attach the annotation to the plot area. Scale the ellipse in the x- and y-directions to match the transform used by the plot space to fit plots in the plot area.
Edit:
After looking into how bordered layers work, I realized my suggestion above won't work. CPTBorderedLayer sets the outerBorderPath automatically whenever the layer bounds change. Instead of trying to affect the layer border, draw the ellipse into an image and use that as the fill for the bordered layer. You should size the layer so the ellipse just fits inside.
After failing to get the Annotations to work properly, I decided to take a different road. My final solution consists in overlaying my original scatter plot with a second one, which only contains one datapoint, namely the center of my confidence ellipse. Here's the code
func drawConfidenceEllipse () {
let graph = hostView.hostedGraph!
let plotSpace = graph.defaultPlotSpace as! CPTXYPlotSpace
let scaleX = (graph.bounds.size.width - graph.paddingLeft - graph.paddingRight) / CGFloat(plotSpace.xRange.lengthDouble)
let scaleY = (graph.bounds.size.height - graph.paddingTop - graph.paddingBottom) / CGFloat(plotSpace.yRange.lengthDouble)
let analysis = ConfidenceEllipse(chiSquare: 5.991)
analysis.analyze(self.samples)
let unscaledPath = analysis.getPath()
let bounds = CGPathGetBoundingBox(unscaledPath)
var scaler = CGAffineTransformIdentity
scaler = CGAffineTransformScale (scaler, scaleX, scaleY)
scaler = CGAffineTransformTranslate (scaler, CGFloat (-bounds.origin.x), CGFloat (-bounds.origin.y))
let scaledPath = CGPathCreateCopyByTransformingPath (unscaledPath, &scaler)
let scaledBounds = CGPathGetPathBoundingBox(scaledPath)
let symbol = CPTPlotSymbol ()
symbol.symbolType = CPTPlotSymbolType.Custom
symbol.customSymbolPath = scaledPath
symbol.fill = CPTFill (color: CPTColor.yellowColor().colorWithAlphaComponent(0.25))
symbol.size = CGSize (width: scaledBounds.size.width, height: scaledBounds.size.height)
let lineStyle = CPTMutableLineStyle()
lineStyle.lineWidth = 1
lineStyle.lineColor = CPTColor.yellowColor()
symbol.lineStyle = lineStyle
let ellipse = CPTScatterPlot (frame: hostView.frame)
ellipse.title = "Confidence Ellipse"
ellipse.delegate = self
ellipse.dataSource = self
ellipse.plotSymbol = symbol
ellipse.dataLineStyle = nil
graph.addPlot(ellipse)
}
Here's a screenshot of the final result:
95% Confidence Ellipse on top of scatter plot
Hope this helps

How to rotate an SCNBox

I'm trying to rotate an SCNBox I created using swipe gestures. For example, when I swipe right the box should rotate 90degs in the Y-axis and -90degs when I swipe left. To achieve this I have been using the node's SCNAction.rotateByX method to perform the rotation animation. Now the problem I'm having is when rotating along either the X-axis or Z-axis after a rotation in the Y-axis and vice-versa is that the positions of the axes change.
What I have notice is that any rotation perform on either of the X,Y,Z axes changes the direction in which the other axes point.
Example: Default position
Then after a rotation in the Z-axis:
Of course this pose a problem because now when I swipe left or right I no longer get the desire effect because the X-axis and Y-axis have now swapped positions. What I would like to know is why does this happen? and is there anyway to perform the rotation animation without it affecting the other axes?
I apologize for my lack of understanding on this subject as this is my first go at 3d graphics.
Solution:
func swipeRight(recognizer: UITapGestureRecognizer) {
// rotation animation
let action = SCNAction.rotateByX(0, y: CGFloat(GLKMathDegreesToRadians(90)), z: 0, duration: 0.5)
boxNode.runAction(action)
//repositoning of the x,y,z axes after the rotation has been applied
let currentPivot = boxNode.pivot
let changePivot = SCNMatrix4Invert(boxNode.transform)
boxNode.pivot = SCNMatrix4Mult(changePivot, currentPivot)
boxNode.transform = SCNMatrix4Identity
}
I haven't ran into any problems yet but it may be safer to use a completion handler to ensure any changes to X,Y,Z axes are done before repositioning them.
I had the same issue, here's what I use to give the desired behavior:
func panGesture(sender: UIPanGestureRecognizer) {
let translation = sender.translationInView(sender.view!)
let pan_x = Float(translation.x)
let pan_y = Float(-translation.y)
let anglePan = sqrt(pow(pan_x,2)+pow(pan_y,2))*(Float)(M_PI)/180.0
var rotVector = SCNVector4()
rotVector.x = -pan_y
rotVector.y = pan_x
rotVector.z = 0
rotVector.w = anglePan
// apply to your model container node
boxNode.rotation = rotVector
if(sender.state == UIGestureRecognizerState.Ended) {
let currentPivot = boxNode.pivot
let changePivot = SCNMatrix4Invert(boxNode.transform)
boxNode.pivot = SCNMatrix4Mult(changePivot, currentPivot)
boxNode.transform = SCNMatrix4Identity
}
}

How to animate only x Axis in core plot?

I am animating x Axis value in core plot but with that my xAxis , yAxis is also animating and disappearing . My set up for graph is shown in here as I have some issue related to graph data I have asked question and here as u can see in code graph set up is correct : Graph set up code. What is missing? MY code for animation is as below:
let plotSpace = graph.defaultPlotSpace as! CPTXYPlotSpace!
let location = currentIndex
let oldRange = CPTPlotRange(location: location , length: kMaxDataPoints - 2)
let newRange = CPTPlotRange(location: location + 1, length: kMaxDataPoints - 2)
CPTAnimation.animate(plotSpace, property: "xRange", fromPlotRange: oldRange, toPlotRange: newRange, duration: 1.0)
currentIndex += 1
(moved from the comments into an answer)
You can use axis constraints to lock the y-axis in position. Look at the "Real Time Plot" in the Plot Gallery example app.

Axes, labels, and titles are hidden or overlapped/occluded by the plot when the dataSource is updated

I'm using Core Plot successfully in an Objective-C app. I'm writing a new app in Swift and I'm having some trouble. Before I set a dataSource for my graph, the axes appear correctly and as expected. See the following screenshot:
Screenshot With Correct Axes
The problem is that when I set a dataSource, the data is rendered properly, but the axes disappear. See the following screenshot:
Screenshot With Axes Gone
I want the axes, labels, and titles to remain visible when the data is rendered. There are lots of possible causes, and I've tried to rule out all the ones I can think of. Here's what I've tried:
The problem isn't the axis title positions or graph padding, because they show up correctly at first.
I read one post saying graph.plotAreaFrame.masksToBorder = false should work. That didn't do it.
I tried setting the graph and plot area fills to nil to try to reveal the titles if they were occluded by the fill. A view behind the graph was revealed, but not the titles.
I tried various values for graph.topDownLayerOrder, trying to ensure the titles were the topmost item, esp. on top of the plot area. Note the docs say the default is axis titles on top, so I didn't expect this to fix it.
I confirmed with the debugger that the axis labels were not set as hidden.
Thanks in advance for any help, and thank you Eric for sharing this sweet library with us!
Following is my source code:
class TemperatureGraph: CPTGraphHostingView, CPTPlotSpaceDelegate {
// Just needed to retain dataSource
var dataSource: CPTPlotDataSource?
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func setupGraphWithSession(session: Session?) {
self.configureGraph()
self.configurePlot()
self.configureAxes()
if (session != nil)
{
self.configureDataSource(session!)
}
}
func configureGraph() {
let graph: CPTGraph = CPTXYGraph(frame: self.bounds)
graph.applyTheme(CPTTheme(named: kCPTSlateTheme))
graph.title = "Temperature History (°F)"
graph.paddingTop = 0.0;
graph.paddingBottom = 0.0;
graph.paddingLeft = 0.0;
graph.paddingRight = 0.0;
// graph.titleDisplacement = CGPoint(x: 0, y: 20)
graph.plotAreaFrame.paddingTop = 30.0;
graph.plotAreaFrame.paddingBottom = 50.0;
graph.plotAreaFrame.paddingLeft = 50.0;
graph.plotAreaFrame.paddingRight = 10.0;
// This doesn't help
// graph.fill = nil;
// graph.plotAreaFrame.fill = nil;
// This doesn't help
// graph.plotAreaFrame.masksToBorder = false
// This doesn't help either
//graph.topDownLayerOrder = [NSNumber(unsignedInt: CPTGraphLayerTypeAxisTitles.value)]
// Some themes have a rounded border around the plot area. It looks bad since we removed the insets, so remove the border too.
graph.plotAreaFrame.borderLineStyle = nil;
graph.plotAreaFrame.cornerRadius = 0;
self.hostedGraph = graph
}
func configurePlot() {
let graph = self.hostedGraph
var plot = CPTScatterPlot()
// Make the data source line use curved interpolation
// plot.interpolation = CPTScatterPlotInterpolationCurved;
var lineStyle: CPTMutableLineStyle = plot.dataLineStyle.mutableCopy() as CPTMutableLineStyle
lineStyle.lineWidth = 1.0;
lineStyle.lineColor = CPTColor.redColor()
plot.dataLineStyle = lineStyle;
// For some reason, areaBaseValue isn't visible in swift. It's in the header and in Objective-C, don't know why it's not seen here.
//plot.areaFill = CPTFill(color: lineStyle.lineColor.colorWithAlphaComponent(0.2))
//plot.areaBaseValue = NSDecimalNumber.zero()
graph.addPlot(plot)
}
func configureAxes() {
let graph = self.hostedGraph
let plot = graph.plotAtIndex(0)
// The timestamps are large, which would make an inordinant number of axis label text by default, so to even render the graph in under a minute, simplify the axes.
var axisSet: CPTAxisSet = graph.axisSet
var axisTitleStyle = CPTMutableTextStyle()
axisTitleStyle.color = CPTColor.blackColor()
axisTitleStyle.fontName = "Helvetica-Bold"
axisTitleStyle.fontSize = 12
var xAxis = axisSet.axisForCoordinate(CPTCoordinateX, atIndex: 0)
xAxis.labelingPolicy = CPTAxisLabelingPolicyNone
xAxis.title = "Time"
xAxis.titleOffset = 15
xAxis.titleTextStyle = axisTitleStyle
var yAxis = axisSet.axisForCoordinate(CPTCoordinateY, atIndex: 0)
// yAxis.labelingPolicy = CPTAxisLabelingPolicyNone
yAxis.title = "Temperature (°F)"
yAxis.titleOffset = 25
yAxis.titleTextStyle = axisTitleStyle
}
func configureDataSource(sesh: Session) {
self.dataSource = TemperatureDatasource(session: sesh)
let plot = self.hostedGraph.plotAtIndex(0)
plot.dataSource = self.dataSource
plot.plotSpace.scaleToFitPlots([plot])
plot.plotSpace.allowsUserInteraction = true
plot.plotSpace.delegate = self
}
func plotSpace(space: CPTPlotSpace!, shouldScaleBy interactionScale: CGFloat, aboutPoint interactionPoint: CGPoint) -> Bool {
return true
}
func plotSpace(space: CPTPlotSpace!, willChangePlotRangeTo newRange: CPTPlotRange!, forCoordinate coordinate: CPTCoordinate) -> CPTPlotRange! {
// Adjust axis to keep them in view at the left and bottom;
// adjust scale-labels to match the scroll.
var allowedRange = newRange
if (coordinate.value == CPTCoordinateY.value) {
let space = self.hostedGraph.defaultPlotSpace as CPTXYPlotSpace
allowedRange = space.yRange
}
return allowedRange;
}
}
By default the axes always cross at (0, 0). I suspect your plot data doesn't cover that point, so scaleToFitPlots pushes (0, 0) outside the visible plot area and hides the axes. You have several options:
Use axisConstraints to lock the axes to a certain spot (e.g., the left edge of the plot area for the y-axis).
After calling scaleToFitPlots, update the orthogonalPosition of each axis within the corresponding plot range.
After calling scaleToFitPlots, expand the plot ranges so they include zero (0), if needed.

Resources